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.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user