feat: add Mosaic Graphics support and fix char rendering
All checks were successful
Build Linux / Build Linux (push) Successful in 1m29s
Build Windows / Build Windows (push) Successful in 2m53s

- Added Mosaic Picker dialog with visual previews
- Added 'Graphics' mode toggle to UI
- Implemented status bar mode indicator (Text/Graphics)
- Corrected English character mapping for 0x60 to Hyphen (-)
- Verified German and Swedish/Finnish character sets against ETSI spec
This commit is contained in:
2026-01-21 13:30:19 +01:00
parent e06fd2c776
commit 0ebf18ee6e
3 changed files with 141 additions and 12 deletions

View File

@@ -9,7 +9,7 @@ to Unicode characters based on the National Option (3 bits).
ENGLISH = { ENGLISH = {
0x23: '#', 0x24: '$', 0x40: '@', 0x23: '#', 0x24: '$', 0x40: '@',
0x5B: '[', 0x5C: '\\', 0x5D: ']', 0x5E: '^', 0x5B: '[', 0x5C: '\\', 0x5D: ']', 0x5E: '^',
0x5F: '_', 0x60: '`', 0x5F: '_', 0x60: '-',
0x7B: '{', 0x7C: '|', 0x7D: '}', 0x7E: '~' 0x7B: '{', 0x7C: '|', 0x7D: '}', 0x7E: '~'
} }

View File

@@ -53,7 +53,7 @@ COLORS = [
] ]
class TeletextCanvas(QWidget): class TeletextCanvas(QWidget):
cursorChanged = pyqtSignal(int, int, int) # x, y, byte_val cursorChanged = pyqtSignal(int, int, int, bool) # x, y, byte_val, is_graphics
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@@ -83,6 +83,7 @@ class TeletextCanvas(QWidget):
self.cursor_x = 0 self.cursor_x = 0
self.cursor_y = 0 self.cursor_y = 0
self.cursor_visible = True self.cursor_visible = True
self.cursor_is_graphics = False # Tracked during draw
# Blinking cursor timer could be added, for now static inverted is fine or toggle on timer elsewhere # Blinking cursor timer could be added, for now static inverted is fine or toggle on timer elsewhere
def get_byte_at(self, x, y): def get_byte_at(self, x, y):
@@ -103,7 +104,7 @@ class TeletextCanvas(QWidget):
def emit_cursor_change(self): def emit_cursor_change(self):
val = self.get_byte_at(self.cursor_x, self.cursor_y) val = self.get_byte_at(self.cursor_x, self.cursor_y)
self.cursorChanged.emit(self.cursor_x, self.cursor_y, val) self.cursorChanged.emit(self.cursor_x, self.cursor_y, val, self.cursor_is_graphics)
def set_cursor(self, x, y): def set_cursor(self, x, y):
self.cursor_x = max(0, min(self.cols - 1, x)) self.cursor_x = max(0, min(self.cols - 1, x))
@@ -329,6 +330,10 @@ class TeletextCanvas(QWidget):
if double_height and not is_occluded: if double_height and not is_occluded:
next_occlusion_mask[c] = True next_occlusion_mask[c] = True
# Capture cursor state if this is the cursor position
if c == self.cursor_x and row == self.cursor_y:
self.cursor_is_graphics = graphics_mode
# If occluded, do not draw anything for this cell # If occluded, do not draw anything for this cell
if is_occluded: if is_occluded:
continue continue

View File

@@ -2,14 +2,11 @@
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QListWidget, QListWidgetItem, QComboBox, QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem, QComboBox, QLabel, QLineEdit, QPushButton,
QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication,
QCheckBox, QDialog, QGridLayout
) )
from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor
# ... (imports remain) from PyQt6.QtCore import Qt, QRect
# ... (imports remain)
from PyQt6.QtGui import QAction, QKeyEvent
from PyQt6.QtCore import Qt
from .io import load_t42, save_t42 from .io import load_t42, save_t42
from .renderer import TeletextCanvas, create_blank_packet from .renderer import TeletextCanvas, create_blank_packet
@@ -18,6 +15,104 @@ import sys
import os import os
from .models import TeletextService, Page, Packet from .models import TeletextService, Page, Packet
class MosaicButton(QPushButton):
def __init__(self, code, main_window):
super().__init__()
self.code = code
self.main_window = main_window
self.setFixedSize(32, 32)
self.setToolTip(f"Hex: {code:02X}")
self.clicked.connect(self.on_click)
def on_click(self):
self.main_window.insert_char(self.code)
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
# Draw content area (centered, smaller than button)
w = self.width()
h = self.height()
m = 4
rect = QRect(m, m, w - 2*m, h - 2*m)
# Background (Black)
painter.fillRect(rect, Qt.GlobalColor.black)
# Foreground (White)
painter.setBrush(QBrush(Qt.GlobalColor.white))
painter.setPen(Qt.PenStyle.NoPen)
# Mosaic Logic
val = self.code & 0x7F
bits = 0
if val >= 0x20:
bits = val - 0x20
# 2x3 grid
cw = rect.width()
ch = rect.height()
x_splits = [0, cw // 2, cw]
y_splits = [0, ch // 3, (2 * ch) // 3, ch]
# bit 0: TL, 1: TR, 2: ML, 3: MR, 4: BL, 6: BR
block_indices = [
(0, 0), (1, 0), # Top
(0, 1), (1, 1), # Mid
(0, 2), (1, 2) # Bot
]
bit_mask = [1, 2, 4, 8, 16, 64]
for i in range(6):
if bits & bit_mask[i]:
c, r = block_indices[i]
bx = rect.x() + x_splits[c]
by = rect.y() + y_splits[r]
bw = x_splits[c+1] - x_splits[c]
bh = y_splits[r+1] - y_splits[r]
painter.drawRect(bx, by, bw, bh)
class MosaicDialog(QDialog):
def __init__(self, main_window):
super().__init__(main_window)
self.setWindowTitle("Insert Mosaic")
self.main_window = main_window
self.setLayout(QVBoxLayout())
lbl = QLabel("Click to insert mosaic character:")
self.layout().addWidget(lbl)
hint = QLabel("Note: Mosaics only appear if the line segment is in Graphics Mode.\n"
"Insert a Graphics Color code (e.g. Red Graphics 0x11) first.")
hint.setStyleSheet("color: gray; font-style: italic;")
self.layout().addWidget(hint)
grid = QGridLayout()
self.layout().addLayout(grid)
# Ranges: 0x20-0x3F, 0x60-0x7F
codes = []
codes.extend(range(0x20, 0x40))
codes.extend(range(0x60, 0x80))
row = 0
col = 0
max_cols = 8
for code in codes:
btn = MosaicButton(code, main_window)
grid.addWidget(btn, row, col)
col += 1
if col >= max_cols:
col = 0
row += 1
close_btn = QPushButton("Close")
close_btn.clicked.connect(self.accept)
self.layout().addWidget(close_btn)
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -90,6 +185,12 @@ class MainWindow(QMainWindow):
# Color Shortcuts # Color Shortcuts
color_layout = QHBoxLayout() color_layout = QHBoxLayout()
# Graphics Mode Toggle
self.chk_graphics = QCheckBox("Graphics")
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)
colors = [ colors = [
("Red", 0x01, "#FF0000"), ("Red", 0x01, "#FF0000"),
("Green", 0x02, "#00FF00"), ("Green", 0x02, "#00FF00"),
@@ -103,9 +204,15 @@ class MainWindow(QMainWindow):
for name, code, hex_color in colors: for name, code, hex_color in colors:
btn = QPushButton(name) btn = QPushButton(name)
btn.setStyleSheet(f"background-color: {hex_color}; font-weight: bold; color: black;") btn.setStyleSheet(f"background-color: {hex_color}; font-weight: bold; color: black;")
btn.clicked.connect(lambda checked, c=code: self.insert_char(c)) # Use separate method to handle graphics check
btn.clicked.connect(lambda checked, c=code: self.insert_color(c))
color_layout.addWidget(btn) color_layout.addWidget(btn)
# Mosaics Button
btn_mosaic = QPushButton("Mosaics...")
btn_mosaic.clicked.connect(self.open_mosaic_dialog)
color_layout.addWidget(btn_mosaic)
color_layout.addStretch() color_layout.addStretch()
center_layout.addLayout(color_layout) center_layout.addLayout(color_layout)
@@ -128,6 +235,10 @@ 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.mode_label = QLabel("Mode: Text")
self.mode_label.setFixedWidth(120)
self.status_bar.addPermanentWidget(self.mode_label)
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)
@@ -539,8 +650,21 @@ class MainWindow(QMainWindow):
self.canvas.move_cursor(1, 0) self.canvas.move_cursor(1, 0)
self.canvas.setFocus() self.canvas.setFocus()
def on_cursor_changed(self, x, y, val): def insert_color(self, base_code):
code = base_code
if self.chk_graphics.isChecked():
# Convert 0x01..0x07 to 0x11..0x17
code += 0x10
self.insert_char(code)
def open_mosaic_dialog(self):
dlg = MosaicDialog(self)
dlg.exec()
def on_cursor_changed(self, x, y, val, is_graphics):
self.hex_input.setText(f"{val:02X}") self.hex_input.setText(f"{val:02X}")
mode_str = "Graphics" if is_graphics else "Text"
self.mode_label.setText(f"Mode: {mode_str}")
def on_hex_entered(self): def on_hex_entered(self):
text = self.hex_input.text() text = self.hex_input.text()