Compare commits

...

15 Commits

Author SHA1 Message Date
06107a3d78 feat: Implement dynamic cursor height for double-height characters
All checks were successful
Build Linux / Build Linux (push) Successful in 1m27s
Build Windows / Build Windows (push) Successful in 4m45s
2026-02-06 17:31:39 +01:00
33e3ed2615 Fix NameError: import missing hamming functions in UI
All checks were successful
Build Linux / Build Linux (push) Successful in 1m47s
Build Windows / Build Windows (push) Successful in 5m21s
2026-02-05 13:03:34 +01:00
6ed8a79660 Fix AttributeError: Initialize language_names before use
All checks were successful
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Successful in 4m51s
2026-01-31 13:38:13 +01:00
56657efa7c Implement 'Set Language' UI to persist language changes to page header
All checks were successful
Build Linux / Build Linux (push) Successful in 1m29s
Build Windows / Build Windows (push) Successful in 4m49s
2026-01-31 13:20:11 +01:00
fa195f2695 Implement session-based language overrides for the viewer
All checks were successful
Build Linux / Build Linux (push) Successful in 1m39s
Build Windows / Build Windows (push) Successful in 4m53s
2026-01-31 13:11:12 +01:00
988178f1c6 Fix header page number display: use hex formatting (P175 instead of P1117)
All checks were successful
Build Linux / Build Linux (push) Successful in 1m33s
Build Windows / Build Windows (push) Successful in 5m18s
2026-01-31 12:19:46 +01:00
71019bf399 Fix Windows build: use --collect-all PyQt6 to ensure platform plugins are bundled
All checks were successful
Build Linux / Build Linux (push) Successful in 1m30s
Build Windows / Build Windows (push) Successful in 4m46s
2026-01-31 11:52:45 +01:00
6a5f223a88 Fix Windows build: add missing PyQt6.sip hidden import
All checks were successful
Build Linux / Build Linux (push) Successful in 1m40s
Build Windows / Build Windows (push) Successful in 2m52s
2026-01-31 11:45:37 +01:00
274a6778b3 Fix Windows build: add missing pkgutil hidden import
All checks were successful
Build Linux / Build Linux (push) Successful in 1m37s
Build Windows / Build Windows (push) Successful in 2m52s
2026-01-31 11:14:18 +01:00
772827082e fix: prevent crash on save without loaded file and add status message
All checks were successful
Build Linux / Build Linux (push) Successful in 1m31s
Build Windows / Build Windows (push) Successful in 2m52s
2026-01-26 13:28:23 +01:00
f8a9ad0065 fix: track Black BG changes in background segments for correct DH backfill
All checks were successful
Build Linux / Build Linux (push) Successful in 1m30s
Build Windows / Build Windows (push) Successful in 2m49s
2026-01-26 13:23:26 +01:00
9726a82851 feat: add TTI export functionality
All checks were successful
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Successful in 2m53s
2026-01-26 12:42:12 +01:00
233eed1ca7 fix: set desktop file name before QApplication init to resolve QDBusError
All checks were successful
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Successful in 2m58s
2026-01-23 21:33:05 +01:00
4c3d860dc4 ui: replace file closed dialog with status bar message 2026-01-23 21:31:30 +01:00
670a2d9f8c ui: move graphics and color controls to right sidebar 2026-01-23 21:29:16 +01:00
6 changed files with 283 additions and 39 deletions

View File

@@ -23,7 +23,7 @@ jobs:
- name: Build Executable - name: Build Executable
run: | run: |
wine pyinstaller --onefile --windowed --name TeletextEditor_Windows.exe --paths src --add-data "app_icon.png;." --icon=app_icon.ico src/main.py wine pyinstaller --onefile --windowed --hidden-import=pkgutil --hidden-import=PyQt6.sip --collect-all PyQt6 --name TeletextEditor_Windows.exe --paths src --add-data "app_icon.png;." --icon=app_icon.ico src/main.py
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

View File

@@ -27,6 +27,9 @@ def build():
sys.executable, "-m", "PyInstaller", sys.executable, "-m", "PyInstaller",
"--onefile", "--onefile",
"--windowed", "--windowed",
"--hidden-import=pkgutil",
"--hidden-import=PyQt6.sip",
"--collect-all", "PyQt6",
"--paths", "src", "--paths", "src",
f"--add-data=app_icon.png{sep}.", f"--add-data=app_icon.png{sep}.",
"src/main.py" "src/main.py"

View File

@@ -23,10 +23,10 @@ def main():
myappid = 'ddybing.teletexteditor.1.0' # arbitrary string myappid = 'ddybing.teletexteditor.1.0' # arbitrary string
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
QApplication.setDesktopFileName("no.ddybing.TeletextEditor") # Helps Linux DEs group windows
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setApplicationName("TeletextEditor") app.setApplicationName("TeletextEditor")
app.setOrganizationName("DanielDybing") app.setOrganizationName("DanielDybing")
app.setDesktopFileName("no.ddybing.TeletextEditor") # Helps Linux DEs group windows
# Debug Image Formats # Debug Image Formats
supported_formats = [str(fmt, 'utf-8') for fmt in QImageReader.supportedImageFormats()] supported_formats = [str(fmt, 'utf-8') for fmt in QImageReader.supportedImageFormats()]

View File

@@ -228,3 +228,65 @@ def parse_header(data: bytearray):
language = ((c_bits_2 & 1) << 1) | ((c_bits_2 & 2) >> 1) | (c_bits_2 & 4) language = ((c_bits_2 & 1) << 1) | ((c_bits_2 & 2) >> 1) | (c_bits_2 & 4)
return page_num, sub_code, language return page_num, sub_code, language
def save_tti(file_path: str, page: Page):
"""
Saves a single Page object to a TTI file.
"""
with open(file_path, 'w', encoding='latin-1') as f:
# Header Info
# DS - Source? Description?
f.write(f"DS,Teletext Editor Export\n")
f.write(f"SP,{file_path}\n")
# PN - Page Number mpp00
# Typically TTI uses decimal integer for mppss?
# Or mppss as hex digits?
# Standard convention: mppss where m is 1-8, pp is 00-FF, ss is 00-99
# Example: Page 100 -> PN,10000
# Example: Page 1F0 -> PN,1F000
f.write(f"PN,{page.magazine}{page.page_number:02X}00\n")
# SC - Subcode ssss
f.write(f"SC,{page.sub_code:04X}\n")
# PS - Page Status
# 8000 is typical for "Transmission"
f.write(f"PS,8000\n")
# RE - Region (Language)
f.write(f"RE,{page.language}\n")
# Lines
# We need to construct the 40-char string for each row
# Row 0 is special (Header)
# Get all packets for this page
# Map row -> packet
rows = {}
for p in page.packets:
rows[p.row] = p
for r in range(26): # 0 to 25
if r in rows:
packet = rows[r]
# Packet data is 40 bytes (after the 2-byte header we stripped in Packet class? No wait)
# Packet.data in our model IS the 40 bytes of character data (we strip MRAG in __post_init__)
# So we just decode it as latin-1 to get the chars
# However, we must ensure we don't have newlines or nulls breaking the text file structure
# TTI format usually accepts raw bytes 0x00-0xFF if strictly handled, but often expects
# mapped control codes.
# Standard VBIT2 TTI handling treats it as a binary-safe string if mapped to char.
# Row 0 special handling: The first 8 bytes of Row 0 are usually header flags in the packet,
# but visually they are "P100 ".
# TTI usually expects the visual representation for the line content.
# But for transmission, we want the raw bytes.
# OL,r,String
data_str = packet.data.decode('latin-1')
f.write(f"OL,{r},{data_str}\n")
else:
# Empty line? Usually omitted or written as empty
pass

View File

@@ -252,6 +252,9 @@ class TeletextCanvas(QWidget):
held_char = 0x20 # Space held_char = 0x20 # Space
double_height = False double_height = False
last_visible_idx = -1
bg_segments = [(0, bg)] # Track BG changes: (index, color)
y = row * self.cell_h y = row * self.cell_h
data = b'' data = b''
@@ -263,7 +266,7 @@ class TeletextCanvas(QWidget):
# Header string for Row 0 columns 0-7 # Header string for Row 0 columns 0-7
header_prefix = "" header_prefix = ""
if row == 0 and self.page: if row == 0 and self.page:
header_prefix = f"P{self.page.magazine}{self.page.page_number:02d}" header_prefix = f"P{self.page.magazine}{self.page.page_number:02X}"
# Pad to 8 chars # Pad to 8 chars
header_prefix = header_prefix.ljust(8) header_prefix = header_prefix.ljust(8)
@@ -311,12 +314,33 @@ class TeletextCanvas(QWidget):
graphics_mode = True graphics_mode = True
elif byte_val == 0x1C: # Black BG elif byte_val == 0x1C: # Black BG
bg = COLORS[0] bg = COLORS[0]
bg_segments.append((c, bg))
elif byte_val == 0x1D: # New BG elif byte_val == 0x1D: # New BG
bg = fg bg = fg
bg_segments.append((c, bg))
elif byte_val == 0x0C: # Normal Height elif byte_val == 0x0C: # Normal Height
double_height = False double_height = False
elif byte_val == 0x0D: # Double Height elif byte_val == 0x0D: # Double Height
double_height = True double_height = True
# Backfill Height if we are in leading controls
if last_visible_idx == -1:
# Update occlusion mask for 0..c-1
for k in range(c):
next_occlusion_mask[k] = True
# Repaint 0..c-1 with DH
if draw_bg:
# Repaint each cell with its historical BG
for k in range(c):
# Resolve BG for k
cell_bg = bg_segments[0][1]
for idx, color in reversed(bg_segments):
if idx <= k:
cell_bg = color
break
painter.fillRect(k * self.cell_w, y, self.cell_w, self.cell_h * 2, cell_bg)
elif byte_val == 0x19: # Contiguous Graphics elif byte_val == 0x19: # Contiguous Graphics
contiguous = True contiguous = True
elif byte_val == 0x1A: # Separated Graphics elif byte_val == 0x1A: # Separated Graphics
@@ -326,6 +350,23 @@ class TeletextCanvas(QWidget):
elif byte_val == 0x1F: # Release Graphics elif byte_val == 0x1F: # Release Graphics
hold_graphics = False hold_graphics = False
# Update visibility tracker
# If it's a control code, it's "invisible" (space) UNLESS Held Graphics draws something
visible_content = False
if is_control:
if hold_graphics and graphics_mode:
visible_content = True
else:
# Treat Space (0x20) as "invisible" for backfill purposes
# This allows backfilling height over leading spaces in banners
if byte_val > 0x20:
visible_content = True
if visible_content:
# If this is the first visible char, mark it
if last_visible_idx == -1:
last_visible_idx = c
# Record Double Height for next row # Record Double Height for next row
if double_height and not is_occluded: if double_height and not is_occluded:
next_occlusion_mask[c] = True next_occlusion_mask[c] = True
@@ -398,8 +439,8 @@ class TeletextCanvas(QWidget):
if draw_fg and 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) painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Difference)
# Difference with white creates inversion # Difference with white creates inversion
# Note: Cursor follows double height? Probably just the active cell. h_cursor = self.cell_h * 2 if double_height else self.cell_h
painter.fillRect(x, y, self.cell_w, self.cell_h, QColor(255, 255, 255)) painter.fillRect(x, y, self.cell_w, h_cursor, QColor(255, 255, 255))
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver) painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
return next_occlusion_mask return next_occlusion_mask

View File

@@ -6,9 +6,9 @@ from PyQt6.QtWidgets import (
QCheckBox, QDialog, QGridLayout QCheckBox, QDialog, QGridLayout
) )
from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor
from PyQt6.QtCore import Qt, QRect from PyQt6.QtCore import Qt, QRect, QTimer
from .io import load_t42, save_t42 from .io import load_t42, save_t42, save_tti, decode_hamming_8_4, encode_hamming_8_4
from .renderer import TeletextCanvas, create_blank_packet from .renderer import TeletextCanvas, create_blank_packet
import copy import copy
import sys import sys
@@ -121,10 +121,14 @@ class MainWindow(QMainWindow):
self.service = TeletextService() self.service = TeletextService()
self.current_page: Page = None self.current_page: Page = None
self.current_file_path = None
self.clipboard = [] # List of (row, data_bytes) self.clipboard = [] # List of (row, data_bytes)
self.undo_stack = [] self.undo_stack = []
self.redo_stack = [] self.redo_stack = []
self.is_modified = False self.is_modified = False
self.language_overrides = {} # Session-based viewer overrides: (mag, pnum) -> lang_idx
self.language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
# UI Components # UI Components
self.central_widget = QWidget() self.central_widget = QWidget()
@@ -183,13 +187,30 @@ class MainWindow(QMainWindow):
center_layout.addLayout(top_bar) center_layout.addLayout(top_bar)
# Color Shortcuts # Middle Layout (Canvas + Right Sidebar)
color_layout = QHBoxLayout() middle_layout = QHBoxLayout()
center_layout.addLayout(middle_layout, 1)
# Canvas
self.canvas = TeletextCanvas()
self.canvas.cursorChanged.connect(self.on_cursor_changed)
middle_layout.addWidget(self.canvas, 1) # Expand
# Right Sidebar
right_sidebar = QWidget()
right_sidebar.setFixedWidth(160)
right_layout = QVBoxLayout(right_sidebar)
right_layout.setContentsMargins(5, 0, 0, 0)
middle_layout.addWidget(right_sidebar)
# Color Shortcuts
# Graphics Mode Toggle # Graphics Mode Toggle
self.chk_graphics = QCheckBox("Graphics") self.chk_graphics = QCheckBox("Graphics")
self.chk_graphics.setToolTip("If checked, inserts Graphics Color codes (e.g. Red Graphics 0x11) instead of Alpha (0x01)") self.chk_graphics.setToolTip("If checked, inserts Graphics Color codes (e.g. Red Graphics 0x11) instead of Alpha (0x01)")
color_layout.addWidget(self.chk_graphics) right_layout.addWidget(self.chk_graphics)
colors_grid = QGridLayout()
colors_grid.setSpacing(5)
colors = [ colors = [
("Black", 0x00, "#000000"), ("Black", 0x00, "#000000"),
@@ -202,9 +223,10 @@ class MainWindow(QMainWindow):
("White", 0x07, "#FFFFFF"), ("White", 0x07, "#FFFFFF"),
] ]
row, col = 0, 0
for name, code, hex_color in colors: for name, code, hex_color in colors:
btn = QPushButton(name) btn = QPushButton(name)
btn.setFixedSize(60, 30) # Fixed size for uniformity btn.setFixedSize(60, 30)
# Common style # Common style
style = f"background-color: {hex_color}; font-weight: bold; border: 1px solid #555; border-radius: 3px;" style = f"background-color: {hex_color}; font-weight: bold; border: 1px solid #555; border-radius: 3px;"
@@ -220,20 +242,25 @@ class MainWindow(QMainWindow):
# Use separate method to handle graphics check # Use separate method to handle graphics check
btn.clicked.connect(lambda checked, c=code: self.insert_color(c)) btn.clicked.connect(lambda checked, c=code: self.insert_color(c))
color_layout.addWidget(btn) colors_grid.addWidget(btn, row, col)
col += 1
if col > 1:
col = 0
row += 1
right_layout.addLayout(colors_grid)
# Mosaics Button # Mosaics Button
btn_mosaic = QPushButton("Mosaics...") btn_mosaic = QPushButton("Mosaics...")
btn_mosaic.clicked.connect(self.open_mosaic_dialog) btn_mosaic.clicked.connect(self.open_mosaic_dialog)
color_layout.addWidget(btn_mosaic) right_layout.addWidget(btn_mosaic)
color_layout.addStretch() right_layout.addSpacing(10)
center_layout.addLayout(color_layout)
# Background Controls # Background Controls
bg_layout = QHBoxLayout()
bg_label = QLabel("Background:") bg_label = QLabel("Background:")
bg_layout.addWidget(bg_label) right_layout.addWidget(bg_label)
# New Background (0x1D) # New Background (0x1D)
btn_new_bg = QPushButton("New BG") btn_new_bg = QPushButton("New BG")
@@ -241,7 +268,7 @@ class MainWindow(QMainWindow):
btn_new_bg.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;") btn_new_bg.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
btn_new_bg.setToolTip("Sets the current foreground color as the new background color (0x1D)") btn_new_bg.setToolTip("Sets the current foreground color as the new background color (0x1D)")
btn_new_bg.clicked.connect(lambda: self.insert_char(0x1D)) btn_new_bg.clicked.connect(lambda: self.insert_char(0x1D))
bg_layout.addWidget(btn_new_bg) right_layout.addWidget(btn_new_bg)
# Black Background (0x1C) # Black Background (0x1C)
btn_black_bg = QPushButton("Black BG") btn_black_bg = QPushButton("Black BG")
@@ -249,15 +276,13 @@ class MainWindow(QMainWindow):
btn_black_bg.setStyleSheet("background-color: black; color: white; font-weight: bold; border: 1px solid #555; border-radius: 3px;") btn_black_bg.setStyleSheet("background-color: black; color: white; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
btn_black_bg.setToolTip("Resets the background color to Black (0x1C)") btn_black_bg.setToolTip("Resets the background color to Black (0x1C)")
btn_black_bg.clicked.connect(lambda: self.insert_char(0x1C)) btn_black_bg.clicked.connect(lambda: self.insert_char(0x1C))
bg_layout.addWidget(btn_black_bg) right_layout.addWidget(btn_black_bg)
bg_layout.addStretch() right_layout.addSpacing(10)
center_layout.addLayout(bg_layout)
# Graphics Control # Graphics Control
gfx_ctrl_layout = QHBoxLayout()
gfx_ctrl_label = QLabel("Graphics Control:") gfx_ctrl_label = QLabel("Graphics Control:")
gfx_ctrl_layout.addWidget(gfx_ctrl_label) right_layout.addWidget(gfx_ctrl_label)
# Hold Graphics (0x1E) # Hold Graphics (0x1E)
btn_hold = QPushButton("Hold Gfx") btn_hold = QPushButton("Hold Gfx")
@@ -265,7 +290,7 @@ class MainWindow(QMainWindow):
btn_hold.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;") btn_hold.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
btn_hold.setToolTip("Hold Graphics (0x1E): Displays the last graphic char in place of subsequent control codes.") btn_hold.setToolTip("Hold Graphics (0x1E): Displays the last graphic char in place of subsequent control codes.")
btn_hold.clicked.connect(lambda: self.insert_char(0x1E)) btn_hold.clicked.connect(lambda: self.insert_char(0x1E))
gfx_ctrl_layout.addWidget(btn_hold) right_layout.addWidget(btn_hold)
# Release Graphics (0x1F) # Release Graphics (0x1F)
btn_release = QPushButton("Release Gfx") btn_release = QPushButton("Release Gfx")
@@ -273,15 +298,29 @@ class MainWindow(QMainWindow):
btn_release.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;") btn_release.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
btn_release.setToolTip("Release Graphics (0x1F): Ends the 'Hold Graphics' effect.") btn_release.setToolTip("Release Graphics (0x1F): Ends the 'Hold Graphics' effect.")
btn_release.clicked.connect(lambda: self.insert_char(0x1F)) btn_release.clicked.connect(lambda: self.insert_char(0x1F))
gfx_ctrl_layout.addWidget(btn_release) right_layout.addWidget(btn_release)
gfx_ctrl_layout.addStretch() right_layout.addSpacing(10)
center_layout.addLayout(gfx_ctrl_layout)
# Canvas # Page Language Setting
self.canvas = TeletextCanvas() lang_group_label = QLabel("Page Language:")
self.canvas.cursorChanged.connect(self.on_cursor_changed) right_layout.addWidget(lang_group_label)
center_layout.addWidget(self.canvas, 1) # Expand
lang_layout = QHBoxLayout()
self.lang_combo = QComboBox()
self.lang_combo.addItems(self.language_names)
self.lang_combo.setToolTip("Select the National Option Character Set for this page.")
btn_set_lang = QPushButton("Set")
btn_set_lang.setFixedWidth(40)
btn_set_lang.clicked.connect(self.apply_language_change)
btn_set_lang.setToolTip("Apply this language setting to the page header.")
lang_layout.addWidget(self.lang_combo)
lang_layout.addWidget(btn_set_lang)
right_layout.addLayout(lang_layout)
right_layout.addStretch()
self.layout.addLayout(center_layout, 1) self.layout.addLayout(center_layout, 1)
@@ -304,8 +343,6 @@ class MainWindow(QMainWindow):
self.language_label = QLabel("Lang: English") self.language_label = QLabel("Lang: English")
self.status_bar.addPermanentWidget(self.language_label) self.status_bar.addPermanentWidget(self.language_label)
self.language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
# Menus # Menus
self.create_menus() self.create_menus()
@@ -313,6 +350,7 @@ class MainWindow(QMainWindow):
idx = self.canvas.subset_idx idx = self.canvas.subset_idx
if 0 <= idx < len(self.language_names): if 0 <= idx < len(self.language_names):
self.language_label.setText(f"Lang: {self.language_names[idx]}") self.language_label.setText(f"Lang: {self.language_names[idx]}")
self.lang_combo.setCurrentIndex(idx)
else: else:
self.language_label.setText(f"Lang: Unknown ({idx})") self.language_label.setText(f"Lang: Unknown ({idx})")
@@ -371,6 +409,10 @@ class MainWindow(QMainWindow):
save_as_action.triggered.connect(self.save_as_file) save_as_action.triggered.connect(self.save_as_file)
file_menu.addAction(save_as_action) file_menu.addAction(save_as_action)
export_tti_action = QAction("Export to TTI...", self)
export_tti_action.triggered.connect(self.export_tti)
file_menu.addAction(export_tti_action)
close_action = QAction("Close File", self) close_action = QAction("Close File", self)
close_action.triggered.connect(self.close_file) close_action.triggered.connect(self.close_file)
file_menu.addAction(close_action) file_menu.addAction(close_action)
@@ -422,10 +464,83 @@ class MainWindow(QMainWindow):
if action: if action:
idx = action.data() idx = action.data()
self.canvas.subset_idx = idx self.canvas.subset_idx = idx
# Store session override for the current page (magazine + page number)
if self.current_page:
key = (self.current_page.magazine, self.current_page.page_number)
self.language_overrides[key] = idx
self.canvas.redraw() self.canvas.redraw()
self.canvas.update() self.canvas.update()
self.update_language_label() self.update_language_label()
def apply_language_change(self):
if not self.current_page:
return
idx = self.lang_combo.currentIndex()
if idx < 0: return
# Update the model
self.current_page.language = idx
# Also update the session override/viewer to match
self.canvas.subset_idx = idx
key = (self.current_page.magazine, self.current_page.page_number)
self.language_overrides[key] = idx
# Patch Row 0 packet data to persist language selection to file
# Language bits are in Byte 8 (Control Bits 2): C12, C13, C14
# We need to preserve C11 (bit 3 of encoded 4-bit val) which is "Inhibit Display" usually 0
# Find Row 0 packet
header_packet = None
for p in self.current_page.packets:
if p.row == 0:
header_packet = p
break
if header_packet and len(header_packet.data) > 8:
try:
old_val = decode_hamming_8_4(header_packet.data[8])
# Encoded nibble structure: D1(b0), D2(b1), D3(b2), D4(b3)
# D1 maps to C12
# D2 maps to C13
# D3 maps to C14
# D4 maps to C11
# io.py logic for reading:
# language = ((c_bits_2 & 1) << 1) | ((c_bits_2 & 2) >> 1) | (c_bits_2 & 4)
# i.e. Lang Bit 0 comes from D2, Lang Bit 1 comes from D1, Lang Bit 2 comes from D3
# So for writing:
# D1 = Lang Bit 1
# D2 = Lang Bit 0
# D3 = Lang Bit 2
l0 = (idx >> 0) & 1
l1 = (idx >> 1) & 1
l2 = (idx >> 2) & 1
d1 = l1
d2 = l0
d3 = l2
d4 = (old_val >> 3) & 1 # Preserve C11
new_val = d1 | (d2 << 1) | (d3 << 2) | (d4 << 3)
header_packet.data[8] = encode_hamming_8_4(new_val)
self.set_modified(True)
self.status_label.setText(f"Language set to {self.language_names[idx]} (saved to header).")
except Exception as e:
self.status_label.setText(f"Error setting language: {e}")
else:
self.status_label.setText("No header packet found to update.")
self.canvas.redraw()
self.canvas.update()
self.update_language_label()
def prev_subpage(self): def prev_subpage(self):
count = self.subpage_combo.count() count = self.subpage_combo.count()
if count <= 1: return if count <= 1: return
@@ -460,9 +575,10 @@ class MainWindow(QMainWindow):
self.undo_stack.clear() self.undo_stack.clear()
self.redo_stack.clear() self.redo_stack.clear()
self.language_overrides.clear()
QMessageBox.information(self, "Closed", "File closed.") self.status_label.setText("File closed.")
self.status_label.setText("Ready") QTimer.singleShot(3000, lambda: self.status_label.setText("Ready"))
self.set_modified(False) self.set_modified(False)
@@ -501,9 +617,9 @@ class MainWindow(QMainWindow):
def save_file(self) -> bool: def save_file(self) -> bool:
if not self.current_file_path: if not self.current_file_path:
fname, _ = QFileDialog.getSaveFileName(self, "Save T42", "", "Teletext Files (*.t42)") # User requested status message instead of Save As behavior for empty state
if not fname: return False self.status_label.setText("No file loaded to save. Please use 'Save As...' or 'Open' first.")
self.current_file_path = fname return False
try: try:
self.progress_bar.setVisible(True) self.progress_bar.setVisible(True)
@@ -529,6 +645,20 @@ class MainWindow(QMainWindow):
self.status_label.setText("Error saving file") self.status_label.setText("Error saving file")
return False return False
def export_tti(self):
if not self.current_page:
QMessageBox.warning(self, "Export TTI", "No page selected to export.")
return
fname, _ = QFileDialog.getSaveFileName(self, "Export to TTI", "", "Teletext Text Image (*.tti)")
if not fname: return
try:
save_tti(fname, self.current_page)
QMessageBox.information(self, "Export Successful", f"Page exported to {os.path.basename(fname)}")
except Exception as e:
QMessageBox.critical(self, "Export Failed", f"Failed to export TTI: {e}")
def copy_page_content(self): def copy_page_content(self):
if not self.current_page: if not self.current_page:
return return
@@ -736,6 +866,14 @@ class MainWindow(QMainWindow):
if isinstance(page, Page): if isinstance(page, Page):
self.current_page = page self.current_page = page
self.canvas.set_page(page) self.canvas.set_page(page)
# Apply session language override if it exists for this page
key = (page.magazine, page.page_number)
if key in self.language_overrides:
self.canvas.subset_idx = self.language_overrides[key]
self.canvas.redraw()
self.canvas.update()
self.update_language_label() self.update_language_label()
self.canvas.setFocus() self.canvas.setFocus()