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 = {
|
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: '~'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user