From 088ad1a3209bfed237d2bbfe6c1bd3856379e761 Mon Sep 17 00:00:00 2001 From: Daniel Dybing Date: Sun, 28 Dec 2025 21:57:44 +0100 Subject: [PATCH] Feature: Enhanced Editing, Hex Inspector, Color Shortcuts - Enhanced Editing: Auto-create packets, Enter key support. - Cursor: Fixed visibility (composition mode), click-to-move, immediate redraw. - Hex Inspector: Added Hex Value display and input panel. - Shortcuts: Added Color Insert buttons. - Fixes: Resolved Hamming encoding for save, fixed duplicate spaces bug, fixed IndentationError. --- src/teletext/__pycache__/io.cpython-312.pyc | Bin 3099 -> 4609 bytes .../__pycache__/renderer.cpython-312.pyc | Bin 11218 -> 15460 bytes src/teletext/__pycache__/ui.cpython-312.pyc | Bin 10914 -> 14309 bytes src/teletext/io.py | 74 +++++++-- src/teletext/renderer.py | 143 +++++++++++++++++- src/teletext/ui.py | 68 ++++++++- 6 files changed, 265 insertions(+), 20 deletions(-) diff --git a/src/teletext/__pycache__/io.cpython-312.pyc b/src/teletext/__pycache__/io.cpython-312.pyc index 83c85889405475bf547217a18221c5832812edc6..f39a1748aa7f936060457ff19c982322fb0786cf 100644 GIT binary patch delta 1997 zcma)7&2Jk;6rb7kuJ^+qaT4dFNkh`2CU%j6oj?jf6>=j7dPoV8IHVHB$!^n>Iv-=V zX`*#)q)OODRc)USMUiJOz^-2gv^vl`hzMitL2P@^_v!{hyXkJt})vbssf61jtw_Nj* zA_FTc9@TS+QM|x-H*v`K=ysXQEn=GFV6d>#x&J1x_mGAZyd@%}Fk67L7%=0&3}DuQ zS->s_b}5X8w>$`GjAdQaX1Ny20?r}Lrg@<4ZIo`ZZjFO0z?C!%rD5H2gU>_Rrq4jh zrtem`md`4D%Xca6rk@outq0bG@33G$`4v_l!W}svY@8J}5sL8e?D*3Wgb1TBD_Fr< zNfR_l5v_Ec@HB!Z%%PApLl`(nTwr#nw*yx~I~L@q2e(Ehj_g1}6k<$vVR^;m7FVyW zWJ0pZT~A)UrkaeBi-3v39D_M-GHZa>0I!XiEK$>@kXAE^h2=~dsPVK6xdsmhIZ11@ zt1FAEiki5bOr@4rmJ(+YT3$B)RZm zv8!~V*jFO>%2I!@t{wz9M((U11i&9` z@W|t%DR$kg+?hZ3)Q8$SN@HK#xO=mB(^%a0pUA&g^LHCVMa@|M%|BQSRMzvcy31XN zm4=GbRTrG8%f9^ieRu&#@8kQJIHRB8CvZbwhKkM_nxwTYF|9~IWAri(asjC7(;xCfcu#-K zkLH9Kli66

1~9BIFc>!_+`;p^!k@sNX@2mzp3oRJJC!u$ckna~otiqmpi*LhZ!L z)<^gBQ!_v}vZ>^f$)uoTQqVCe=pYg?`P@Pz0o`TtutH}52|+@G&|c&X;G+les1@fg zt4T#A>2^qY*ji1`ZZRR9ysjoP<56+}b}hk7zYjkfdGti|7M6|9ZE>K1j&tG4P^~L> z{Ex0swJWrT`o(Y|-jGm7@4eCTXyIJV-(E`IzG18z9Ss3>bQyv2h>LW+Ew)|QGlcW8tD)8U*zG)-WEQI{f+Z@0EZ2>fnceu Y?$c%OUF^am4NpHFC~ZDPuvq#20a3v8!vFvP delta 567 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 DmvRas delta 2279 zcmZWqYitx%6uvXFGy9nCmbLBnLAxvk+MQAej{>DUEa54S*0u<;MwX%Xw%xkj-Q1Zz zgDH?2!T5k&71U^mAtr>7sEtt*|B#UQ!ykg-j}s)Cn3#;jsTeL%J{~Z1(IK6j%>M;N^}40a!Cax_ z{sAz&NX`*P5)vf|nd+xVpG2i8e?p=16iIlf0;p0C)B0qpPI(eus)5Bvy?}nqng~!o zpiTpTC8x-klnC|^qr?gAXK7|Lk@ZC7+p>7gv$2-Sm*qkY5;T8|B#V_0s)#>4+eBJb2v%L(=4v(nXaQK8U)08lp?8Qkylcw$8?}yV4d)nle92@g zld_Y^>0iA;N$AS%b-WgKOjH);Ic9G)Gb5(8yy%Bxv3!tz_Bgra3!e7g@Kv3U-1No7 z4*#rtpNLn3)$%nWE(NcNE1`Lty^bl$6Bj6@+yg9S?Ic8gq!n3390Nv6YCCxI9lI7I#z%)(16L)XNjcQ@8 z9-EEXj#k+2NIA!Mblhgi5i@N8`AMu2#triZ(J2`)Gc?VB7r9|uT&S+|ZUeLRDG}GI zKhYXUFw9%0E9YO7rE2j-U9_bV(}Z~(KHNaSXNxx>EJeU(9k0dg9odY{o6!^%p=f>Q z9u)WDs>;nwKXZII*(7vkXoaew-BiW~ntJ+qit=vsl{oK$ucZNC(Q{?TZ;dpoHgPL>+Bw=ibil9=-@|oe1dc_=e1Wb0U>tj-E67Su!&`bQEe=(}Tm7 z&7VcTdV~&yeeRCFR65vVzx%%qGbbDZ_1;Hvd< zWEbxdpU0PJc>4UX_$6K|otX9;LB-2%G_#xGK2g`a2WC3BG9~LNbV4q3DsHdd*po*Y zE`rO~(Yc%;MGQDffQVANd*M2FL zxwQrh?DTNV5f}Sfk38k5qbX_+INAV9^$*yNA4&NH94#}v1y-tTgaMPW0!N3-*3ayu z8{h(3TGYic3%TQFzeWBn3ol{l|VJr6F|~A@SY%C6oBp@+}B>TX`wME`@%y5Ym7IECa`!)_`>2R8D=AVIz1zBgrM;q$odp_~=hJo<&iqv%G-~)nRciZ3S CehugV diff --git a/src/teletext/__pycache__/ui.cpython-312.pyc b/src/teletext/__pycache__/ui.cpython-312.pyc index f2056a06255e3af11a2d4730471c87cd0c3e08b2..e0a45032d08b96fc9e33d09a6b6575777412e895 100644 GIT binary patch delta 4668 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 U{)`M?RjLvldZGI_g1-U9Kf885p8x;= diff --git a/src/teletext/io.py b/src/teletext/io.py index a9db7ff..82001e5 100644 --- a/src/teletext/io.py +++ b/src/teletext/io.py @@ -62,24 +62,74 @@ def load_t42(file_path: str) -> TeletextService: return service + +def encode_hamming_8_4(value): + # Value is 4 bits (0-15) + d1 = (value >> 0) & 1 + d2 = (value >> 1) & 1 + d3 = (value >> 2) & 1 + d4 = (value >> 3) & 1 + + # Parity bits (Odd parity default? Or standard Hamming?) + # Teletext spec: + # P1 = 1 + D1 + D2 + D4 (mod 2) -> Inverse of even parity check? + # Actually, simpler to look up or calculate. + # Let's match typical implementation: + # P1 (b0) covers 1,3,7 (D1, D2, D4) + # P2 (b2) covers 1,5,7 (D1, D3, D4) + # P3 (b4) covers 3,5,7 (D2, D3, D4) + # P4 (b6) covers all. + # Teletext uses ODD parity for the hamming bits usually? + # "Hamming 8/4 with odd parity" + + p1 = 1 ^ d1 ^ d2 ^ d4 + p2 = 1 ^ d1 ^ d3 ^ d4 + p3 = 1 ^ d2 ^ d3 ^ d4 + + res = (p1 << 0) | (d1 << 1) | \ + (p2 << 2) | (d2 << 3) | \ + (p3 << 4) | (d3 << 5) | \ + (d4 << 7) + + # P4 (bit 6) makes total bits odd + # Count set bits so far + set_bits = bin(res).count('1') + p4 = 1 if (set_bits % 2 == 0) else 0 + + res |= (p4 << 6) + + return res + def save_t42(file_path: str, service: TeletextService): with open(file_path, 'wb') as f: - # User requirement: "without rearranging the order of the packets" - # Implies we should iterate the original list. - # However, if we edit data, we modify the Packet objects in place. - # If we Add/Delete packets, we need to handle that. - for packet in service.all_packets: - # Reconstruct the 42 bytes from the packet fields - # The packet.data (bytearray) should be mutable and edited by the UI. - # packet.original_data (first 2 bytes) + packet.data + # Reconstruct header bytes from packet.magazine and packet.row + # Byte 1: M1 M2 M3 R1 + # Byte 2: R2 R3 R4 R5 - # Note: If we changed Magazine or Row, we'd need to re-encode the first 2 bytes. - # For now, assume we primarily edit content (bytes 2-41). + mag = packet.magazine + if mag == 8: mag = 0 # 0 encoded as 8 - header = packet.original_data[:2] # Keep original address for now - # TODO: regenerating header if Mag/Row changed + # Bits: + # B1 data: M1(0) M2(1) M3(2) R1(3) + m1 = (mag >> 0) & 1 + m2 = (mag >> 1) & 1 + m3 = (mag >> 2) & 1 + r1 = (packet.row >> 0) & 1 + b1_val = m1 | (m2 << 1) | (m3 << 2) | (r1 << 3) + b1_enc = encode_hamming_8_4(b1_val) + + # B2 data: R2(0) R3(1) R4(2) R5(3) + r2 = (packet.row >> 1) & 1 + r3 = (packet.row >> 2) & 1 + r4 = (packet.row >> 3) & 1 + r5 = (packet.row >> 4) & 1 + + b2_val = r2 | (r3 << 1) | (r4 << 2) | (r5 << 3) + b2_enc = encode_hamming_8_4(b2_val) + + header = bytes([b1_enc, b2_enc]) f.write(header + packet.data) def decode_hamming_8_4(byte_val): diff --git a/src/teletext/renderer.py b/src/teletext/renderer.py index fdf8cb9..54824fa 100644 --- a/src/teletext/renderer.py +++ b/src/teletext/renderer.py @@ -6,6 +6,40 @@ from PyQt6.QtCore import Qt, QRect, QSize, pyqtSignal from .models import Page, Packet from .charsets import get_char +# Helper to create a blank packet +def create_blank_packet(magazine: int, row: int) -> Packet: + # 42 bytes + # Bytes 0-1: Address (Hamming encoded) + # We need to compute Hamming for M and R. + # For now, let's just make a placeholder that "looks" okay or use 0x00 and let a future saver fix it. + # But wait, `Packet` expects 42 bytes input. + + # Ideally we should implement the hamming encoder from `io.py` or similar. + # For now, we will create a Packet with dummy address bytes and correct mag/row properties set manually if needed, + # OR we just construct the bytes. + + # Let's rely on the Packet class parsing... which is annoying if we can't easily encode. + # Hack: default to 0x00 0x00, then manually set .magazine and .row + + data = bytearray(42) + # Set spaces + data[2:] = b'\x20' * 40 + + p = Packet(bytes(data)) + # Override + p.magazine = magazine + p.row = row + p.data = bytearray(b'\x20' * 40) + + # We really should set the address bytes correctly so saving works "naturally". + # But `save_t42` uses `packet.original_data[:2]`. + # So we MUST encode properly or `save_t42` needs to regenerate headers. + + # Let's defer proper Hamming encoding to the Save step (as noted in TODO in io.py). + # For in-memory editing, this is fine. + return p + + # Teletext Palette COLORS = [ QColor(0, 0, 0), # Black @@ -19,8 +53,11 @@ COLORS = [ ] class TeletextCanvas(QWidget): + cursorChanged = pyqtSignal(int, int, int) # x, y, byte_val + def __init__(self, parent=None): super().__init__(parent) + self.setMouseTracking(True) # Just in case self.setMinimumSize(480, 500) # 40x12 * 25x20 approx self.page: Page = None self.subset_idx = 0 # Default English @@ -48,22 +85,65 @@ class TeletextCanvas(QWidget): self.cursor_visible = True # Blinking cursor timer could be added, for now static inverted is fine or toggle on timer elsewhere + def get_byte_at(self, x, y): + if not self.page: return 0 + + # Row 0 special logic (header) - returning what is displayed or raw? + # User probably wants RAW byte for editing. + + packet = None + for p in self.page.packets: + if p.row == y: + packet = p + break + + if packet and 0 <= x < 40: + return packet.data[x] + return 0 + + 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) + def set_cursor(self, x, y): self.cursor_x = max(0, min(self.cols - 1, x)) self.cursor_y = max(0, min(self.rows - 1, y)) + self.redraw() self.update() + self.emit_cursor_change() def move_cursor(self, dx, dy): self.cursor_x = max(0, min(self.cols - 1, self.cursor_x + dx)) self.cursor_y = max(0, min(self.rows - 1, self.cursor_y + dy)) + self.redraw() self.update() + self.emit_cursor_change() + def set_byte_at_cursor(self, byte_val): + if not self.page: return + + packet = None + for p in self.page.packets: + if p.row == self.cursor_y: + packet = p + break + + if packet: + if 0 <= self.cursor_x < 40: + packet.data[self.cursor_x] = byte_val + self.redraw() + self.emit_cursor_change() + else: + # Create packet if missing + self.handle_input(chr(byte_val)) # Lazy reuse or duplicate create logic + def set_page(self, page: Page): self.page = page self.cursor_x = 0 self.cursor_y = 0 self.redraw() self.update() + self.emit_cursor_change() def handle_input(self, text): if not self.page: @@ -96,10 +176,24 @@ class TeletextCanvas(QWidget): self.redraw() self.move_cursor(1, 0) else: - # Create a packet if it doesn't exist for this row? - # Creating new packets in a T42 stream is tricky (insertion). - # For now, only edit existing rows. - pass + # Create a packet if it doesn't exist for this row + if 0 <= self.cursor_x < 40: + # Check if we should create it + # Only if input is valid char? + if len(text) == 1: + # Create new packet + new_packet = create_blank_packet(self.page.magazine, self.cursor_y) + self.page.packets.append(new_packet) + # Sort packets by row? Or just append. Renderer handles unordered. + # But for sanity, let's just append. + + # Write the char + byte_val = ord(text) + if byte_val > 255: byte_val = 0x3F + new_packet.data[self.cursor_x] = byte_val + + self.redraw() + self.move_cursor(1, 0) def redraw(self): self.buffer.fill(Qt.GlobalColor.black) @@ -223,11 +317,48 @@ class TeletextCanvas(QWidget): # Draw Cursor # Invert the cell at cursor position if self.cursor_visible and c == self.cursor_x and row == self.cursor_y: - painter.setCompositionMode(QPainter.CompositionMode.RasterOp_NotSourceXorDestination) - # XOR with white (creates inversion) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Difference) + # Difference with white creates inversion painter.fillRect(x, y, self.cell_w, self.cell_h, QColor(255, 255, 255)) painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver) + def mousePressEvent(self, event): + self.setFocus() + # Calculate cell from mouse position + # Need to account for scaling? + # The widget sizes to fit, but we render to a fixed buffer then scale up. + # paintEvent scales buffer to self.rect(). + + # This is a bit tricky because paintEvent uses aspect-ratio scaling and centering. + # We need to reverse the mapping. + + target_rect = self.rect() + # Re-calc scale and offset + # Note: This logic duplicates paintEvent, commonize? + + # Simple approximation for MVP: + # If the window is roughly aspect ratio correct, direct mapping works. + # If not, we need offset. + + # Let's get the exact rect used in paintEvent + scale_x = target_rect.width() / self.img_w + scale_y = target_rect.height() / self.img_h + scale = min(scale_x, scale_y) + + dw = self.img_w * scale + dh = self.img_h * scale + + ox = (target_rect.width() - dw) / 2 + oy = (target_rect.height() - dh) / 2 + + mx = event.pos().x() - ox + my = event.pos().y() - oy + + if 0 <= mx < dw and 0 <= my < dh: + col = int(mx / (self.cell_w * scale)) + row = int(my / (self.cell_h * scale)) + self.set_cursor(col, row) + def draw_mosaic(self, painter, x, y, char_code, color, contiguous): val = char_code & 0x7F bits = 0 diff --git a/src/teletext/ui.py b/src/teletext/ui.py index 4890052..e32b2dd 100644 --- a/src/teletext/ui.py +++ b/src/teletext/ui.py @@ -1,7 +1,10 @@ -import os from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, - QListWidget, QListWidgetItem, QComboBox, QLabel, QFileDialog, QMenuBar, QMenu, QMessageBox) + QListWidget, QListWidgetItem, QComboBox, QLabel, QLineEdit, QPushButton, QFileDialog, QMenuBar, QMenu, QMessageBox) + +# ... (imports remain) + +# ... (imports remain) from PyQt6.QtGui import QAction, QKeyEvent from PyQt6.QtCore import Qt @@ -34,6 +37,19 @@ class MainWindow(QMainWindow): self.page_list.itemClicked.connect(self.on_page_selected) left_layout.addWidget(self.page_list) + # Hex Inspector + hex_layout = QVBoxLayout() + hex_label = QLabel("Hex Value:") + self.hex_input = QLineEdit() + self.hex_input.setMaxLength(2) + self.hex_input.setPlaceholderText("00") + self.hex_input.returnPressed.connect(self.on_hex_entered) + hex_layout.addWidget(hex_label) + hex_layout.addWidget(self.hex_input) + left_layout.addLayout(hex_layout) + + left_layout.addStretch() + self.layout.addLayout(left_layout) # Center Area Layout (Top Bar + Canvas) @@ -51,8 +67,30 @@ class MainWindow(QMainWindow): center_layout.addLayout(top_bar) + # Color Shortcuts + color_layout = QHBoxLayout() + colors = [ + ("Red", 0x01, "#FF0000"), + ("Green", 0x02, "#00FF00"), + ("Yellow", 0x03, "#FFFF00"), + ("Blue", 0x04, "#0000FF"), + ("Magenta", 0x05, "#FF00FF"), + ("Cyan", 0x06, "#00FFFF"), + ("White", 0x07, "#FFFFFF"), + ] + + 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)) + color_layout.addWidget(btn) + + color_layout.addStretch() + center_layout.addLayout(color_layout) + # Canvas self.canvas = TeletextCanvas() + self.canvas.cursorChanged.connect(self.on_cursor_changed) center_layout.addWidget(self.canvas, 1) # Expand self.layout.addLayout(center_layout, 1) @@ -164,6 +202,28 @@ class MainWindow(QMainWindow): self.current_page = page self.canvas.set_page(page) self.canvas.setFocus() + + def insert_char(self, char_code): + self.canvas.set_byte_at_cursor(char_code) + # Advance cursor + self.canvas.move_cursor(1, 0) + self.canvas.setFocus() + + def on_cursor_changed(self, x, y, val): + self.hex_input.setText(f"{val:02X}") + + def on_hex_entered(self): + text = self.hex_input.text() + try: + val = int(text, 16) + if 0 <= val <= 255: + # Update canvas input + # We can call handle_input with char, OR set byte directly. + # Direct byte set is safer for non-printable. + self.canvas.set_byte_at_cursor(val) + self.canvas.setFocus() # Return focus to canvas + except ValueError: + pass # Ignore invalid hex # Input Handling (Editor Logic) def keyPressEvent(self, event: QKeyEvent): @@ -182,6 +242,10 @@ 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_Return or key == Qt.Key.Key_Enter: + # Move to start of next line + self.canvas.cursor_x = 0 + self.canvas.move_cursor(0, 1) else: # Typing # Filter non-printable