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

View File

@@ -4,29 +4,24 @@ on: [push, pull_request]
jobs: jobs:
build: build:
name: Build Windows name: Build Windows
runs-on: windows-latest runs-on: ubuntu-latest
container:
image: tobix/pywine:3.10
steps: steps:
- name: Configure Git Redirect
run: git config --global url."http://192.168.50.24:3000/".insteadOf "http://server:3000/"
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Dependencies - name: Install Dependencies
run: | run: |
pip install -r requirements.txt wine python -m pip install --upgrade pip
wine pip install -r requirements.txt
- name: Build Executable - name: Build Executable
run: | 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 - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: TeletextEditor-Windows 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/Hungarian - Option 010 (2)
SWEDISH_FINNISH = { SWEDISH_FINNISH = {
0x23: '#', 0x24: '¤', 0x40: 'É', 0x23: '#', 0x24: '\u00A4', 0x40: '\u00C9',
0x5B: 'Ä', 0x5C: 'Ö', 0x5D: 'Å', 0x5E: 'Ü', 0x5B: '\u00C4', 0x5C: '\u00D6', 0x5D: '\u00C5', 0x5E: '\u00DC',
0x5F: '_', 0x60: 'é', 0x5F: '_', 0x60: '\u00E9',
0x7B: 'ä', 0x7C: 'ö', 0x7D: 'å', 0x7E: 'ü' 0x7B: '\u00E4', 0x7C: '\u00F6', 0x7D: '\u00E5', 0x7E: '\u00FC'
} }
# German - Option 001 (1) # German - Option 001 (1)
@@ -58,3 +58,22 @@ def get_char(byte_val, subset_idx):
return mapping[valid_byte] return mapping[valid_byte]
return chr(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). # 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. # 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 # 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) new_page.packets.append(packet)
service.pages.append(new_page) service.pages.append(new_page)
else: else:
@@ -191,7 +191,13 @@ def parse_header(data: bytearray):
pu = decode_hamming_8_4(data[0]) pu = decode_hamming_8_4(data[0])
pt = decode_hamming_8_4(data[1]) 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 # Subcode: S1, S2, S3, S4
# S1 (low), S2, S3, S4 (high) # S1 (low), S2, S3, S4 (high)
@@ -209,4 +215,16 @@ def parse_header(data: bytearray):
sub_code = s1 | (s2 << 4) | (s3 << 8) | (s4 << 12) 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 magazine: int
page_number: int # 00-99 page_number: int # 00-99
sub_code: int = 0 # Subpage code (0000 to 3F7F hex usually, simplest is 0-99 equivalent) 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) packets: List[Packet] = field(default_factory=list)
@property @property
def full_page_number(self): 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 @dataclass
class TeletextService: 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 PyQt6.QtCore import Qt, QRect, QSize, pyqtSignal
from .models import Page, Packet 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 # Helper to create a blank packet
def create_blank_packet(magazine: int, row: int) -> Packet: def create_blank_packet(magazine: int, row: int) -> Packet:
@@ -58,7 +58,7 @@ class TeletextCanvas(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setMouseTracking(True) # Just in case self.setMouseTracking(True) # Just in case
self.setMinimumSize(480, 500) # 40x12 * 25x20 approx self.setMinimumSize(800, 600) # 40x20 * 25x24
self.page: Page = None self.page: Page = None
self.subset_idx = 0 # Default English self.subset_idx = 0 # Default English
@@ -66,8 +66,8 @@ class TeletextCanvas(QWidget):
# We will render to a fixed size QImage and scale it # We will render to a fixed size QImage and scale it
self.cols = 40 self.cols = 40
self.rows = 25 self.rows = 25
self.cell_w = 12 self.cell_w = 20
self.cell_h = 20 self.cell_h = 24
self.img_w = self.cols * self.cell_w self.img_w = self.cols * self.cell_w
self.img_h = self.rows * self.cell_h self.img_h = self.rows * self.cell_h
@@ -75,7 +75,7 @@ class TeletextCanvas(QWidget):
self.buffer.fill(Qt.GlobalColor.black) self.buffer.fill(Qt.GlobalColor.black)
# Font for text # Font for text
self.font = QFont("Courier New", 14) self.font = QFont("Courier New", 18)
self.font.setStyleHint(QFont.StyleHint.Monospace) self.font.setStyleHint(QFont.StyleHint.Monospace)
self.font.setBold(True) self.font.setBold(True)
@@ -139,6 +139,12 @@ class TeletextCanvas(QWidget):
def set_page(self, page: Page): def set_page(self, page: 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_x = 0
self.cursor_y = 0 self.cursor_y = 0
self.redraw() self.redraw()
@@ -168,7 +174,8 @@ class TeletextCanvas(QWidget):
# Check if text is a single char # Check if text is a single char
if len(text) == 1: if len(text) == 1:
byte_val = ord(text) byte_val = get_byte_from_char(text, self.subset_idx)
# Simple filter # Simple filter
if byte_val > 255: byte_val = 0x3F # ? if byte_val > 255: byte_val = 0x3F # ?
@@ -188,7 +195,7 @@ class TeletextCanvas(QWidget):
# But for sanity, let's just append. # But for sanity, let's just append.
# Write the char # Write the char
byte_val = ord(text) byte_val = get_byte_from_char(text, self.subset_idx)
if byte_val > 255: byte_val = 0x3F if byte_val > 255: byte_val = 0x3F
new_packet.data[self.cursor_x] = byte_val new_packet.data[self.cursor_x] = byte_val
@@ -214,13 +221,27 @@ class TeletextCanvas(QWidget):
if 0 <= p.row <= 25: if 0 <= p.row <= 25:
grid[p.row] = p grid[p.row] = p
# Pass 1: Backgrounds
occlusion_mask = [False] * 40
for r in range(25): for r in range(25):
packet = grid[r] 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() 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 # Default State at start of row
fg = COLORS[7] # White fg = COLORS[7] # White
bg = COLORS[0] # Black bg = COLORS[0] # Black
@@ -228,6 +249,7 @@ class TeletextCanvas(QWidget):
contiguous = True # Mosaic contiguous = True # Mosaic
hold_graphics = False hold_graphics = False
held_char = 0x20 # Space held_char = 0x20 # Space
double_height = False
y = row * self.cell_h y = row * self.cell_h
@@ -247,6 +269,24 @@ class TeletextCanvas(QWidget):
for c in range(40): for c in range(40):
x = c * self.cell_w 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 # Decide byte value
if row == 0 and c < 8: if row == 0 and c < 8:
# Use generated header prefix # Use generated header prefix
@@ -273,9 +313,9 @@ class TeletextCanvas(QWidget):
elif byte_val == 0x1D: # New BG elif byte_val == 0x1D: # New BG
bg = fg bg = fg
elif byte_val == 0x0C: # Normal Height elif byte_val == 0x0C: # Normal Height
pass double_height = False
elif byte_val == 0x0D: # Double Height elif byte_val == 0x0D: # Double Height
pass # Not implemented yet double_height = True
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
@@ -285,43 +325,79 @@ class TeletextCanvas(QWidget):
elif byte_val == 0x1F: # Release Graphics elif byte_val == 0x1F: # Release Graphics
hold_graphics = False 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 # 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 # Draw Foreground
if is_control: if draw_fg:
# "Set-at" spacing attribute? Teletext control codes occupy a space # Calculate height
# unless "Hold Graphics" replaces it with previous graphic char. # For Mosaics, we use the height param.
if hold_graphics and graphics_mode: # For Alphanumerics, we scale the painter.
self.draw_mosaic(painter, x, y, held_char, fg, contiguous)
else: if is_control:
# Draw space (nothing, since we filled BG) # "Set-at" spacing attribute? Teletext control codes occupy a space
pass # unless "Hold Graphics" replaces it with previous graphic char.
else: if hold_graphics and graphics_mode:
if graphics_mode: if double_height:
# Mosaic Graphics self.draw_mosaic(painter, x, y, held_char, fg, contiguous, height=self.cell_h * 2)
if (0x20 <= byte_val <= 0x3F) or (0x60 <= byte_val <= 0x7F): else:
self.draw_mosaic(painter, x, y, byte_val, fg, contiguous) self.draw_mosaic(painter, x, y, held_char, fg, contiguous)
held_char = byte_val
else: 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) char = get_char(byte_val, self.subset_idx)
painter.setPen(fg) painter.setPen(fg)
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char) if double_height:
held_char = 0x20 painter.save()
else: painter.translate(x, y)
# Alphanumeric painter.scale(1, 2)
char = get_char(byte_val, self.subset_idx) painter.drawText(QRect(0, 0, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
painter.setPen(fg) painter.restore()
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char) else:
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
# Draw Cursor # Draw Cursor
# Invert the cell at cursor position # 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) 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.
painter.fillRect(x, y, self.cell_w, self.cell_h, QColor(255, 255, 255)) painter.fillRect(x, y, self.cell_w, self.cell_h, QColor(255, 255, 255))
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver) painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
return next_occlusion_mask
def mousePressEvent(self, event): def mousePressEvent(self, event):
self.setFocus() self.setFocus()
# Calculate cell from mouse position # Calculate cell from mouse position
@@ -359,13 +435,21 @@ class TeletextCanvas(QWidget):
row = int(my / (self.cell_h * scale)) row = int(my / (self.cell_h * scale))
self.set_cursor(col, row) 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 val = char_code & 0x7F
bits = 0 bits = 0
if val >= 0x20: if val >= 0x20:
bits = 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, 0), (1, 0), # Top
(0, 1), (1, 1), # Mid (0, 1), (1, 1), # Mid
(0, 2), (1, 2) # Bot (0, 2), (1, 2) # Bot
@@ -373,24 +457,26 @@ class TeletextCanvas(QWidget):
bit_mask = [1, 2, 4, 8, 16, 64] # 64 is bit 6 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.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QBrush(color)) painter.setBrush(QBrush(color))
for i in range(6): for i in range(6):
if bits & bit_mask[i]: if bits & bit_mask[i]:
bx = x + blocks[i][0] * (self.cell_w / 2) c, r = block_indices[i]
by = y + blocks[i][1] * (self.cell_h / 3)
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: if not contiguous:
bx += 1 bx += 1
by += 1 by += 1
bw -= 1
bh -= 1
painter.drawRect(QRect(int(bx), int(by), int(bw), int(bh))) 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_label = QLabel("Ready")
self.status_bar.addWidget(self.status_label) 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 # Menus
self.create_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): def set_modified(self, modified: bool):
self.is_modified = modified self.is_modified = modified
title = "Teletext Editor" title = "Teletext Editor"
@@ -152,8 +164,7 @@ class MainWindow(QMainWindow):
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel) QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
if ret == QMessageBox.StandardButton.Save: if ret == QMessageBox.StandardButton.Save:
self.save_file() return self.save_file()
return True # check if save succeeded? save_file catches exceptions but we might want to check
elif ret == QMessageBox.StandardButton.Discard: elif ret == QMessageBox.StandardButton.Discard:
return True return True
else: else:
@@ -223,8 +234,7 @@ class MainWindow(QMainWindow):
view_menu = menu_bar.addMenu("View") view_menu = menu_bar.addMenu("View")
lang_menu = view_menu.addMenu("Language") lang_menu = view_menu.addMenu("Language")
langs = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"] for i, lang in enumerate(self.language_names):
for i, lang in enumerate(langs):
action = QAction(lang, self) action = QAction(lang, self)
action.setData(i) action.setData(i)
action.triggered.connect(self.set_language) action.triggered.connect(self.set_language)
@@ -237,6 +247,7 @@ class MainWindow(QMainWindow):
self.canvas.subset_idx = idx self.canvas.subset_idx = idx
self.canvas.redraw() self.canvas.redraw()
self.canvas.update() 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()
@@ -311,25 +322,35 @@ class MainWindow(QMainWindow):
self.current_file_path = fname self.current_file_path = fname
self.save_file() self.save_file()
def save_file(self): 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)") fname, _ = QFileDialog.getSaveFileName(self, "Save T42", "", "Teletext Files (*.t42)")
if not fname: return if not fname: return False
self.current_file_path = fname self.current_file_path = fname
try: try:
self.progress_bar.setVisible(True) self.progress_bar.setVisible(True)
self.status_label.setText(f"Saving {os.path.basename(self.current_file_path)}...") 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) save_t42(self.current_file_path, self.service, progress_callback=self.update_progress)
self.progress_bar.setVisible(False) 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.status_label.setText(f"Saved {len(self.service.pages)} pages to {os.path.basename(self.current_file_path)}")
self.set_modified(False) self.set_modified(False)
return True
except Exception as e: except Exception as e:
self.progress_bar.setVisible(False) self.progress_bar.setVisible(False)
QMessageBox.critical(self, "Error", f"Failed to save file: {e}") QMessageBox.critical(self, "Error", f"Failed to save file: {e}")
self.status_label.setText("Error saving file") self.status_label.setText("Error saving file")
return False
def copy_page_content(self): def copy_page_content(self):
if not self.current_page: if not self.current_page:
@@ -475,7 +496,8 @@ class MainWindow(QMainWindow):
sorted_keys = sorted(self.page_groups.keys()) sorted_keys = sorted(self.page_groups.keys())
for mag, pnum in sorted_keys: for mag, pnum in sorted_keys:
label = f"{mag}{pnum:02d}" # Display as Hex
label = f"{mag}{pnum:02X}"
item = QListWidgetItem(label) item = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, (mag, pnum)) item.setData(Qt.ItemDataRole.UserRole, (mag, pnum))
self.page_list.addItem(item) self.page_list.addItem(item)
@@ -507,6 +529,7 @@ 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)
self.update_language_label()
self.canvas.setFocus() self.canvas.setFocus()
def insert_char(self, char_code): def insert_char(self, char_code):
@@ -550,6 +573,12 @@ class MainWindow(QMainWindow):
self.canvas.move_cursor(-1, 0) self.canvas.move_cursor(-1, 0)
elif key == Qt.Key.Key_Right: elif key == Qt.Key.Key_Right:
self.canvas.move_cursor(1, 0) 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: elif key == Qt.Key.Key_Return or key == Qt.Key.Key_Enter:
# Move to start of next line # Move to start of next line
self.canvas.cursor_x = 0 self.canvas.cursor_x = 0

BIN
test.t42

Binary file not shown.

Binary file not shown.