feat: add Mosaic Graphics support and fix char rendering
- 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:
@@ -9,7 +9,7 @@ to Unicode characters based on the National Option (3 bits).
|
||||
ENGLISH = {
|
||||
0x23: '#', 0x24: '$', 0x40: '@',
|
||||
0x5B: '[', 0x5C: '\\', 0x5D: ']', 0x5E: '^',
|
||||
0x5F: '_', 0x60: '`',
|
||||
0x5F: '_', 0x60: '-',
|
||||
0x7B: '{', 0x7C: '|', 0x7D: '}', 0x7E: '~'
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ COLORS = [
|
||||
]
|
||||
|
||||
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):
|
||||
super().__init__(parent)
|
||||
@@ -83,6 +83,7 @@ class TeletextCanvas(QWidget):
|
||||
self.cursor_x = 0
|
||||
self.cursor_y = 0
|
||||
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
|
||||
|
||||
def get_byte_at(self, x, y):
|
||||
@@ -103,7 +104,7 @@ class TeletextCanvas(QWidget):
|
||||
|
||||
def emit_cursor_change(self):
|
||||
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):
|
||||
self.cursor_x = max(0, min(self.cols - 1, x))
|
||||
@@ -329,6 +330,10 @@ class TeletextCanvas(QWidget):
|
||||
if double_height and not is_occluded:
|
||||
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 is_occluded:
|
||||
continue
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QListWidget, QListWidgetItem, QComboBox, QLabel, QLineEdit, QPushButton,
|
||||
QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication
|
||||
QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication,
|
||||
QCheckBox, QDialog, QGridLayout
|
||||
)
|
||||
|
||||
# ... (imports remain)
|
||||
|
||||
# ... (imports remain)
|
||||
from PyQt6.QtGui import QAction, QKeyEvent
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor
|
||||
from PyQt6.QtCore import Qt, QRect
|
||||
|
||||
from .io import load_t42, save_t42
|
||||
from .renderer import TeletextCanvas, create_blank_packet
|
||||
@@ -18,6 +15,104 @@ import sys
|
||||
import os
|
||||
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):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -90,6 +185,12 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# Color Shortcuts
|
||||
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 = [
|
||||
("Red", 0x01, "#FF0000"),
|
||||
("Green", 0x02, "#00FF00"),
|
||||
@@ -103,9 +204,15 @@ class MainWindow(QMainWindow):
|
||||
for name, code, hex_color in colors:
|
||||
btn = QPushButton(name)
|
||||
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)
|
||||
|
||||
# Mosaics Button
|
||||
btn_mosaic = QPushButton("Mosaics...")
|
||||
btn_mosaic.clicked.connect(self.open_mosaic_dialog)
|
||||
color_layout.addWidget(btn_mosaic)
|
||||
|
||||
color_layout.addStretch()
|
||||
center_layout.addLayout(color_layout)
|
||||
|
||||
@@ -128,6 +235,10 @@ class MainWindow(QMainWindow):
|
||||
self.status_label = QLabel("Ready")
|
||||
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.status_bar.addPermanentWidget(self.language_label)
|
||||
|
||||
@@ -539,8 +650,21 @@ class MainWindow(QMainWindow):
|
||||
self.canvas.move_cursor(1, 0)
|
||||
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}")
|
||||
mode_str = "Graphics" if is_graphics else "Text"
|
||||
self.mode_label.setText(f"Mode: {mode_str}")
|
||||
|
||||
def on_hex_entered(self):
|
||||
text = self.hex_input.text()
|
||||
|
||||
Reference in New Issue
Block a user