Compare commits

...

10 Commits

Author SHA1 Message Date
42e189635b Configure Gitea Workflows and Add Build Scripts
Some checks failed
Build Linux / Build Linux (push) Successful in 1m34s
Build Windows / Build Windows (push) Failing after 40s
- Update build-linux.yaml to use standard Ubuntu runner.
- Update build-windows.yaml to use tobix/pywine container for cross-compilation on Linux.
- Add build_app.py and check_ttx6.py helper scripts.
2026-01-13 17:48:00 +01:00
4b7b73e9a3 Support Hexadecimal Page Numbers (e.g. 1FF, 12E)
- Refactored  to parse page numbers as nibbles ((T<<4)|U) instead of decimal, preventing collisions between hex and decimal pages.
- Updated  to format page IDs as Hex.
- Updated  to display page IDs as Hex in the list.
- Ensures filler/housekeeping pages are correctly isolated.
2026-01-11 11:52:29 +01:00
8c393c8f9e Fix language detection bit swap in T42 header parsing
Correctly map C12 and C13 control bits to fix misidentification of Swedish/Finnish (010) and German (001).
Also ensures Page model, Renderer, and UI properly propagate and display the detected language.
2026-01-11 11:40:20 +01:00
783e5006f7 Adjust cell dimensions and font size for better visibility 2026-01-05 22:20:39 +01:00
944556f259 Ignore and stop tracking .t42 files 2026-01-05 22:18:44 +01:00
dedabcd12a Remove stale swap file 2026-01-05 22:15:41 +01:00
67840ad899 Add .gitignore and stop tracking pycache 2026-01-05 22:12:48 +01:00
1cdc35850a Fix double height character rendering logic 2026-01-04 18:23:25 +01:00
66cb788fb0 Fixed block rendering issues
Fixed issues where graphic blocks had horizontal stripes in them
2026-01-02 00:20:28 +01:00
132dc50de8 Feat: Add Home/End key navigation 2025-12-31 15:10:13 +01:00
19 changed files with 321 additions and 82 deletions

View File

@@ -5,12 +5,7 @@ jobs:
build:
name: Build Linux
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Configure Git Redirect
run: git config --global url."http://192.168.50.24:3333/".insteadOf "http://server:3000/"
- name: Checkout
uses: actions/checkout@v3
@@ -21,7 +16,7 @@ jobs:
- name: Install Dependencies
run: |
python --version
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build Executable
@@ -32,4 +27,4 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: TeletextEditor-Linux
path: dist/TeletextEditor_Linux
path: dist/TeletextEditor_Linux

View File

@@ -4,29 +4,24 @@ on: [push, pull_request]
jobs:
build:
name: Build Windows
runs-on: windows-latest
runs-on: ubuntu-latest
container:
image: tobix/pywine:3.10
steps:
- name: Configure Git Redirect
run: git config --global url."http://192.168.50.24:3000/".insteadOf "http://server:3000/"
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Dependencies
run: |
pip install -r requirements.txt
wine python -m pip install --upgrade pip
wine pip install -r requirements.txt
- name: Build Executable
run: |
pyinstaller --onefile --windowed --name TeletextEditor_Windows.exe --paths src src/main.py
wine pyinstaller --onefile --windowed --name TeletextEditor_Windows.exe --paths src src/main.py
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: TeletextEditor-Windows
path: dist/TeletextEditor_Windows.exe
path: dist/TeletextEditor_Windows.exe

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
venv/
__pycache__/
*.pyc
dist/
build/
*.spec
*.t42

BIN
bbc.t42

Binary file not shown.

60
build_app.py Normal file
View File

@@ -0,0 +1,60 @@
import sys
import os
import subprocess
import shutil
import platform
def clean_build_dirs():
"""Removes build and dist directories if they exist."""
for d in ["build", "dist"]:
if os.path.exists(d):
print(f"Cleaning {d}...")
shutil.rmtree(d)
spec_file = "TeletextEditor_Linux.spec" if platform.system() == "Linux" else "TeletextEditor_Windows.spec"
if os.path.exists(spec_file):
os.remove(spec_file)
def build():
system = platform.system()
print(f"Detected OS: {system}")
base_cmd = [
sys.executable, "-m", "PyInstaller",
"--onefile",
"--windowed",
"--paths", "src",
"src/main.py"
]
if system == "Linux":
name = "TeletextEditor_Linux"
elif system == "Windows":
name = "TeletextEditor_Windows.exe"
else:
print(f"Unsupported platform: {system}")
return
cmd = base_cmd + ["--name", name]
print("Running build command:")
print(" ".join(cmd))
try:
subprocess.check_call(cmd)
print("\n" + "="*40)
print(f"Build successful! Executable is in 'dist/{name}'")
print("="*40)
except subprocess.CalledProcessError as e:
print(f"Build failed with error code {e.returncode}")
sys.exit(1)
# Cross-compilation note
if system == "Linux":
print("\nNote: To build the Windows executable, please run this script on Windows.")
elif system == "Windows":
print("\nNote: To build the Linux executable, please run this script on Linux.")
if __name__ == "__main__":
clean_build_dirs()
build()

28
check_ttx6.py Normal file
View File

@@ -0,0 +1,28 @@
import sys
import os
# Add src to path
sys.path.append(os.path.join(os.getcwd(), 'src'))
from teletext.io import load_t42
def check_file(filename):
if not os.path.exists(filename):
print(f"File {filename} not found")
return
service = load_t42(filename)
print(f"Analysis of {filename}:")
print(f"Total packets: {len(service.all_packets)}")
print(f"Total pages: {len(service.pages)}")
language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
for i, page in enumerate(service.pages):
lang_idx = page.language
lang_name = language_names[lang_idx] if 0 <= lang_idx < len(language_names) else f"Unknown ({lang_idx})"
print(f"Page {i+1}: Mag {page.magazine} Num {page.page_number:02d}, Lang: {lang_idx} ({lang_name})")
if __name__ == "__main__":
check_file("TTX-6_RAW.t42")

View File

@@ -15,10 +15,10 @@ ENGLISH = {
# Swedish/Finnish/Hungarian - Option 010 (2)
SWEDISH_FINNISH = {
0x23: '#', 0x24: '¤', 0x40: 'É',
0x5B: 'Ä', 0x5C: 'Ö', 0x5D: 'Å', 0x5E: 'Ü',
0x5F: '_', 0x60: 'é',
0x7B: 'ä', 0x7C: 'ö', 0x7D: 'å', 0x7E: 'ü'
0x23: '#', 0x24: '\u00A4', 0x40: '\u00C9',
0x5B: '\u00C4', 0x5C: '\u00D6', 0x5D: '\u00C5', 0x5E: '\u00DC',
0x5F: '_', 0x60: '\u00E9',
0x7B: '\u00E4', 0x7C: '\u00F6', 0x7D: '\u00E5', 0x7E: '\u00FC'
}
# German - Option 001 (1)
@@ -58,3 +58,22 @@ def get_char(byte_val, subset_idx):
return mapping[valid_byte]
return chr(valid_byte)
import unicodedata
def get_byte_from_char(char, subset_idx):
if len(char) != 1: return 0
# Normalize input to NFC to match our map keys (if they are NFC, which python literals usually are)
char = unicodedata.normalize('NFC', char)
if subset_idx < 0 or subset_idx >= len(SETS):
subset_idx = 0
mapping = SETS[subset_idx]
for code, mapped_char in mapping.items():
if mapped_char == char:
return code
return ord(char)

View File

@@ -43,10 +43,10 @@ def load_t42(file_path: str, progress_callback: Optional[Callable[[int, int], No
# or find the existing one if we want to support updates (but T42 usually is a stream capture).
# If it's an editor file, it's likely sequential.
p_num, sub_code = parse_header(packet.data)
p_num, sub_code, language = parse_header(packet.data)
# Create new page
new_page = Page(magazine=packet.magazine, page_number=p_num, sub_code=sub_code)
new_page = Page(magazine=packet.magazine, page_number=p_num, sub_code=sub_code, language=language)
new_page.packets.append(packet)
service.pages.append(new_page)
else:
@@ -191,7 +191,13 @@ def parse_header(data: bytearray):
pu = decode_hamming_8_4(data[0])
pt = decode_hamming_8_4(data[1])
page_num = (pt & 0xF) * 10 + (pu & 0xF)
# Use BCD/Hex-like storage: High nibble is Tens, Low nibble is Units.
# This preserves Hex pages (A-F) without colliding with decimal pages.
# E.g. Page 1FF -> Tens=F(15), Units=F(15) -> 0xFF (255)
# Page 12E -> Tens=2, Units=E(14) -> 0x2E (46)
# Page 134 -> Tens=3, Units=4 -> 0x34 (52)
# 0x2E != 0x34. No collision.
page_num = ((pt & 0xF) << 4) | (pu & 0xF)
# Subcode: S1, S2, S3, S4
# S1 (low), S2, S3, S4 (high)
@@ -209,4 +215,16 @@ def parse_header(data: bytearray):
sub_code = s1 | (s2 << 4) | (s3 << 8) | (s4 << 12)
return page_num, sub_code
# Control bits C12, C13, C14 are in Byte 8 (index 8)
# They determine the National Option (Language)
c_bits_2 = decode_hamming_8_4(data[8])
# Fix for Language Detection:
# It seems C12 and C13 are swapped in the Hamming decoding or file format relative to expected values.
# C12 is bit 0, C13 is bit 1.
# We swap them so D1 maps to C13 (Swedish bit) and D2 maps to C12 (German bit).
# Original: language = c_bits_2 & 0b111
language = ((c_bits_2 & 1) << 1) | ((c_bits_2 & 2) >> 1) | (c_bits_2 & 4)
return page_num, sub_code, language

View File

@@ -76,11 +76,13 @@ class Page:
magazine: int
page_number: int # 00-99
sub_code: int = 0 # Subpage code (0000 to 3F7F hex usually, simplest is 0-99 equivalent)
language: int = 0 # National Option (0-7)
packets: List[Packet] = field(default_factory=list)
@property
def full_page_number(self):
return f"{self.magazine}{self.page_number:02d}"
# Format as Hex to support A-F pages
return f"{self.magazine}{self.page_number:02X}"
@dataclass
class TeletextService:

180
src/teletext/renderer.py Normal file → Executable file
View File

@@ -4,7 +4,7 @@ from PyQt6.QtGui import QPainter, QColor, QFont, QImage, QBrush, QPen
from PyQt6.QtCore import Qt, QRect, QSize, pyqtSignal
from .models import Page, Packet
from .charsets import get_char
from .charsets import get_char, get_byte_from_char
# Helper to create a blank packet
def create_blank_packet(magazine: int, row: int) -> Packet:
@@ -58,7 +58,7 @@ class TeletextCanvas(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setMouseTracking(True) # Just in case
self.setMinimumSize(480, 500) # 40x12 * 25x20 approx
self.setMinimumSize(800, 600) # 40x20 * 25x24
self.page: Page = None
self.subset_idx = 0 # Default English
@@ -66,8 +66,8 @@ class TeletextCanvas(QWidget):
# We will render to a fixed size QImage and scale it
self.cols = 40
self.rows = 25
self.cell_w = 12
self.cell_h = 20
self.cell_w = 20
self.cell_h = 24
self.img_w = self.cols * self.cell_w
self.img_h = self.rows * self.cell_h
@@ -75,7 +75,7 @@ class TeletextCanvas(QWidget):
self.buffer.fill(Qt.GlobalColor.black)
# Font for text
self.font = QFont("Courier New", 14)
self.font = QFont("Courier New", 18)
self.font.setStyleHint(QFont.StyleHint.Monospace)
self.font.setBold(True)
@@ -139,6 +139,12 @@ class TeletextCanvas(QWidget):
def set_page(self, page: Page):
self.page = page
# Set language from page header
if page:
self.subset_idx = page.language
else:
self.subset_idx = 0
self.cursor_x = 0
self.cursor_y = 0
self.redraw()
@@ -168,7 +174,8 @@ class TeletextCanvas(QWidget):
# Check if text is a single char
if len(text) == 1:
byte_val = ord(text)
byte_val = get_byte_from_char(text, self.subset_idx)
# Simple filter
if byte_val > 255: byte_val = 0x3F # ?
@@ -188,7 +195,7 @@ class TeletextCanvas(QWidget):
# But for sanity, let's just append.
# Write the char
byte_val = ord(text)
byte_val = get_byte_from_char(text, self.subset_idx)
if byte_val > 255: byte_val = 0x3F
new_packet.data[self.cursor_x] = byte_val
@@ -214,13 +221,27 @@ class TeletextCanvas(QWidget):
if 0 <= p.row <= 25:
grid[p.row] = p
# Pass 1: Backgrounds
occlusion_mask = [False] * 40
for r in range(25):
packet = grid[r]
self.draw_row(painter, r, packet)
occlusion_mask = self.draw_row(painter, r, packet, draw_bg=True, draw_fg=False, occlusion_mask=occlusion_mask)
# Pass 2: Foregrounds
occlusion_mask = [False] * 40
for r in range(25):
packet = grid[r]
occlusion_mask = self.draw_row(painter, r, packet, draw_bg=False, draw_fg=True, occlusion_mask=occlusion_mask)
painter.end()
def draw_row(self, painter, row, packet):
def draw_row(self, painter, row, packet, draw_bg=True, draw_fg=True, occlusion_mask=None):
if occlusion_mask is None:
occlusion_mask = [False] * 40
# Output mask for the next row
next_occlusion_mask = [False] * 40
# Default State at start of row
fg = COLORS[7] # White
bg = COLORS[0] # Black
@@ -228,6 +249,7 @@ class TeletextCanvas(QWidget):
contiguous = True # Mosaic
hold_graphics = False
held_char = 0x20 # Space
double_height = False
y = row * self.cell_h
@@ -247,6 +269,24 @@ class TeletextCanvas(QWidget):
for c in range(40):
x = c * self.cell_w
# If this cell is occluded by the row above, skip drawing and attribute processing?
# Spec says "The characters in the row below are ignored."
# Ideally we shouldn't even process attributes, but for simple renderer we just skip draw.
# However, if we skip attribute processing, state (fg/bg) won't update.
# Teletext attributes are serial.
# BUT, if the row above covers it, the viewer sees the row above.
# Does the hidden content affect the *rest* of the row?
# Likely yes, attributes usually propagate.
# But the spec says "ignored". Let's assume we skip *everything* for this cell visually,
# but maybe we should technically maintain state?
# For "Double Height" visual correctness, skipping drawing is the key.
# We will Process attributes (to keep state consistent) but Skip Drawing if occluded.
# Wait, if we process attributes, we might set double_height=True for the NEXT row?
# If this cell is occluded, it shouldn't trigger DH for the next row.
is_occluded = occlusion_mask[c]
# Decide byte value
if row == 0 and c < 8:
# Use generated header prefix
@@ -273,9 +313,9 @@ class TeletextCanvas(QWidget):
elif byte_val == 0x1D: # New BG
bg = fg
elif byte_val == 0x0C: # Normal Height
pass
double_height = False
elif byte_val == 0x0D: # Double Height
pass # Not implemented yet
double_height = True
elif byte_val == 0x19: # Contiguous Graphics
contiguous = True
elif byte_val == 0x1A: # Separated Graphics
@@ -285,43 +325,79 @@ class TeletextCanvas(QWidget):
elif byte_val == 0x1F: # Release Graphics
hold_graphics = False
# Record Double Height for next row
if double_height and not is_occluded:
next_occlusion_mask[c] = True
# If occluded, do not draw anything for this cell
if is_occluded:
continue
# Draw Background
painter.fillRect(x, y, self.cell_w, self.cell_h, bg)
if draw_bg:
# If double height, draw taller background
h_bg = self.cell_h * 2 if double_height else self.cell_h
painter.fillRect(x, y, self.cell_w, h_bg, bg)
# Draw Foreground
if is_control:
# "Set-at" spacing attribute? Teletext control codes occupy a space
# unless "Hold Graphics" replaces it with previous graphic char.
if hold_graphics and graphics_mode:
self.draw_mosaic(painter, x, y, held_char, fg, contiguous)
else:
# Draw space (nothing, since we filled BG)
pass
else:
if graphics_mode:
# Mosaic Graphics
if (0x20 <= byte_val <= 0x3F) or (0x60 <= byte_val <= 0x7F):
self.draw_mosaic(painter, x, y, byte_val, fg, contiguous)
held_char = byte_val
if draw_fg:
# Calculate height
# For Mosaics, we use the height param.
# For Alphanumerics, we scale the painter.
if is_control:
# "Set-at" spacing attribute? Teletext control codes occupy a space
# unless "Hold Graphics" replaces it with previous graphic char.
if hold_graphics and graphics_mode:
if double_height:
self.draw_mosaic(painter, x, y, held_char, fg, contiguous, height=self.cell_h * 2)
else:
self.draw_mosaic(painter, x, y, held_char, fg, contiguous)
else:
# Capital letter in graphics mode? Usually shows char?
# Draw space (nothing, since we filled BG)
pass
else:
if graphics_mode:
# Mosaic Graphics
h_mos = self.cell_h * 2 if double_height else self.cell_h
if (0x20 <= byte_val <= 0x3F) or (0x60 <= byte_val <= 0x7F):
self.draw_mosaic(painter, x, y, byte_val, fg, contiguous, height=h_mos)
held_char = byte_val
else:
# Capital letter in graphics mode? Usually shows char?
char = get_char(byte_val, self.subset_idx)
painter.setPen(fg)
if double_height:
painter.save()
painter.translate(x, y)
painter.scale(1, 2)
painter.drawText(QRect(0, 0, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
painter.restore()
else:
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
held_char = 0x20
else:
# Alphanumeric
char = get_char(byte_val, self.subset_idx)
painter.setPen(fg)
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
held_char = 0x20
else:
# Alphanumeric
char = get_char(byte_val, self.subset_idx)
painter.setPen(fg)
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
if double_height:
painter.save()
painter.translate(x, y)
painter.scale(1, 2)
painter.drawText(QRect(0, 0, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
painter.restore()
else:
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
# Draw Cursor
# Invert the cell at cursor position
if self.cursor_visible and c == self.cursor_x and row == self.cursor_y:
if draw_fg and self.cursor_visible and c == self.cursor_x and row == self.cursor_y:
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Difference)
# Difference with white creates inversion
# Note: Cursor follows double height? Probably just the active cell.
painter.fillRect(x, y, self.cell_w, self.cell_h, QColor(255, 255, 255))
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
return next_occlusion_mask
def mousePressEvent(self, event):
self.setFocus()
# Calculate cell from mouse position
@@ -359,13 +435,21 @@ class TeletextCanvas(QWidget):
row = int(my / (self.cell_h * scale))
self.set_cursor(col, row)
def draw_mosaic(self, painter, x, y, char_code, color, contiguous):
def draw_mosaic(self, painter, x, y, char_code, color, contiguous, height=None):
if height is None:
height = self.cell_h
val = char_code & 0x7F
bits = 0
if val >= 0x20:
bits = val - 0x20
blocks = [
# Grid definitions for 2x3 grid
x_splits = [0, int(self.cell_w / 2), self.cell_w]
y_splits = [0, int(height / 3), int(2 * height / 3), height]
# Block indices (col, row) for the 6 bits
block_indices = [
(0, 0), (1, 0), # Top
(0, 1), (1, 1), # Mid
(0, 2), (1, 2) # Bot
@@ -373,24 +457,26 @@ class TeletextCanvas(QWidget):
bit_mask = [1, 2, 4, 8, 16, 64] # 64 is bit 6
bw = self.cell_w / 2
bh = self.cell_h / 3
if not contiguous:
bw -= 1
bh -= 1
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QBrush(color))
for i in range(6):
if bits & bit_mask[i]:
bx = x + blocks[i][0] * (self.cell_w / 2)
by = y + blocks[i][1] * (self.cell_h / 3)
c, r = block_indices[i]
bx_local = x_splits[c]
by_local = y_splits[r]
bw = x_splits[c+1] - x_splits[c]
bh = y_splits[r+1] - y_splits[r]
bx = x + bx_local
by = y + by_local
if not contiguous:
bx += 1
by += 1
bw -= 1
bh -= 1
painter.drawRect(QRect(int(bx), int(by), int(bw), int(bh)))

View File

@@ -127,10 +127,22 @@ class MainWindow(QMainWindow):
self.status_label = QLabel("Ready")
self.status_bar.addWidget(self.status_label)
self.language_label = QLabel("Lang: English")
self.status_bar.addPermanentWidget(self.language_label)
self.language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
# Menus
self.create_menus()
def update_language_label(self):
idx = self.canvas.subset_idx
if 0 <= idx < len(self.language_names):
self.language_label.setText(f"Lang: {self.language_names[idx]}")
else:
self.language_label.setText(f"Lang: Unknown ({idx})")
def set_modified(self, modified: bool):
self.is_modified = modified
title = "Teletext Editor"
@@ -152,8 +164,7 @@ class MainWindow(QMainWindow):
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
if ret == QMessageBox.StandardButton.Save:
self.save_file()
return True # check if save succeeded? save_file catches exceptions but we might want to check
return self.save_file()
elif ret == QMessageBox.StandardButton.Discard:
return True
else:
@@ -223,8 +234,7 @@ class MainWindow(QMainWindow):
view_menu = menu_bar.addMenu("View")
lang_menu = view_menu.addMenu("Language")
langs = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
for i, lang in enumerate(langs):
for i, lang in enumerate(self.language_names):
action = QAction(lang, self)
action.setData(i)
action.triggered.connect(self.set_language)
@@ -237,6 +247,7 @@ class MainWindow(QMainWindow):
self.canvas.subset_idx = idx
self.canvas.redraw()
self.canvas.update()
self.update_language_label()
def prev_subpage(self):
count = self.subpage_combo.count()
@@ -311,25 +322,35 @@ class MainWindow(QMainWindow):
self.current_file_path = fname
self.save_file()
def save_file(self):
def save_file(self) -> bool:
if not self.current_file_path:
fname, _ = QFileDialog.getSaveFileName(self, "Save T42", "", "Teletext Files (*.t42)")
if not fname: return
if not fname: return False
self.current_file_path = fname
try:
self.progress_bar.setVisible(True)
self.status_label.setText(f"Saving {os.path.basename(self.current_file_path)}...")
# Rebuild all_packets from pages to ensure edits/undos/new packets are included.
# This serializes the pages in order, effectively "cleaning" the stream of orphans
# and ensuring the file matches the editor state.
new_all_packets = []
for page in self.service.pages:
new_all_packets.extend(page.packets)
self.service.all_packets = new_all_packets
save_t42(self.current_file_path, self.service, progress_callback=self.update_progress)
self.progress_bar.setVisible(False)
self.status_label.setText(f"Saved {len(self.service.pages)} pages to {os.path.basename(self.current_file_path)}")
self.set_modified(False)
return True
except Exception as e:
self.progress_bar.setVisible(False)
QMessageBox.critical(self, "Error", f"Failed to save file: {e}")
self.status_label.setText("Error saving file")
return False
def copy_page_content(self):
if not self.current_page:
@@ -475,7 +496,8 @@ class MainWindow(QMainWindow):
sorted_keys = sorted(self.page_groups.keys())
for mag, pnum in sorted_keys:
label = f"{mag}{pnum:02d}"
# Display as Hex
label = f"{mag}{pnum:02X}"
item = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, (mag, pnum))
self.page_list.addItem(item)
@@ -507,6 +529,7 @@ class MainWindow(QMainWindow):
if isinstance(page, Page):
self.current_page = page
self.canvas.set_page(page)
self.update_language_label()
self.canvas.setFocus()
def insert_char(self, char_code):
@@ -550,6 +573,12 @@ class MainWindow(QMainWindow):
self.canvas.move_cursor(-1, 0)
elif key == Qt.Key.Key_Right:
self.canvas.move_cursor(1, 0)
elif key == Qt.Key.Key_Home:
# Move to start of line
self.canvas.set_cursor(0, self.canvas.cursor_y)
elif key == Qt.Key.Key_End:
# Move to end of line (39)
self.canvas.set_cursor(39, self.canvas.cursor_y)
elif key == Qt.Key.Key_Return or key == Qt.Key.Key_Enter:
# Move to start of next line
self.canvas.cursor_x = 0

BIN
test.t42

Binary file not shown.

Binary file not shown.