Compare commits
10 Commits
7f9d8304be
...
42e189635b
| Author | SHA1 | Date | |
|---|---|---|---|
| 42e189635b | |||
| 4b7b73e9a3 | |||
| 8c393c8f9e | |||
| 783e5006f7 | |||
| 944556f259 | |||
| dedabcd12a | |||
| 67840ad899 | |||
| 1cdc35850a | |||
| 66cb788fb0 | |||
| 132dc50de8 |
Binary file not shown.
@@ -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
|
||||
@@ -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
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
dist/
|
||||
build/
|
||||
*.spec
|
||||
*.t42
|
||||
60
build_app.py
Normal file
60
build_app.py
Normal 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
28
check_ttx6.py
Normal 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")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
180
src/teletext/renderer.py
Normal file → Executable 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)))
|
||||
|
||||
|
||||
@@ -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_out.t42
BIN
test_out.t42
Binary file not shown.
Reference in New Issue
Block a user