diff --git a/src/teletext/charsets.py b/src/teletext/charsets.py index 5ebfdc2..564f0d6 100644 --- a/src/teletext/charsets.py +++ b/src/teletext/charsets.py @@ -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: '~' } diff --git a/src/teletext/renderer.py b/src/teletext/renderer.py index 96b1dfd..35030f2 100755 --- a/src/teletext/renderer.py +++ b/src/teletext/renderer.py @@ -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 diff --git a/src/teletext/ui.py b/src/teletext/ui.py index 1dc602c..2faba10 100644 --- a/src/teletext/ui.py +++ b/src/teletext/ui.py @@ -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) @@ -127,6 +234,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) @@ -538,9 +649,22 @@ class MainWindow(QMainWindow): # Advance cursor self.canvas.move_cursor(1, 0) self.canvas.setFocus() + + 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): + 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()