2025-12-28 21:38:21 +01:00
|
|
|
|
2025-12-30 21:20:27 +01:00
|
|
|
from PyQt6.QtWidgets import (
|
|
|
|
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
|
|
|
QListWidget, QListWidgetItem, QComboBox, QLabel, QLineEdit, QPushButton,
|
|
|
|
|
QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication
|
|
|
|
|
)
|
2025-12-28 21:57:44 +01:00
|
|
|
|
|
|
|
|
# ... (imports remain)
|
|
|
|
|
|
|
|
|
|
# ... (imports remain)
|
2025-12-28 21:38:21 +01:00
|
|
|
from PyQt6.QtGui import QAction, QKeyEvent
|
|
|
|
|
from PyQt6.QtCore import Qt
|
|
|
|
|
|
|
|
|
|
from .io import load_t42, save_t42
|
2025-12-31 11:45:17 +01:00
|
|
|
from .renderer import TeletextCanvas, create_blank_packet
|
2025-12-31 13:40:22 +01:00
|
|
|
import copy
|
2025-12-30 21:20:27 +01:00
|
|
|
import sys
|
|
|
|
|
import os
|
2025-12-28 21:38:21 +01:00
|
|
|
from .models import TeletextService, Page, Packet
|
|
|
|
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.setWindowTitle("Teletext Editor")
|
|
|
|
|
self.resize(1024, 768)
|
|
|
|
|
|
|
|
|
|
self.service = TeletextService()
|
|
|
|
|
self.current_page: Page = None
|
2025-12-31 11:45:17 +01:00
|
|
|
self.clipboard = [] # List of (row, data_bytes)
|
2025-12-31 13:40:22 +01:00
|
|
|
self.undo_stack = []
|
|
|
|
|
self.redo_stack = []
|
2025-12-31 14:20:01 +01:00
|
|
|
self.is_modified = False
|
2025-12-28 21:38:21 +01:00
|
|
|
|
|
|
|
|
# UI Components
|
|
|
|
|
self.central_widget = QWidget()
|
|
|
|
|
self.setCentralWidget(self.central_widget)
|
|
|
|
|
|
|
|
|
|
self.layout = QHBoxLayout(self.central_widget)
|
|
|
|
|
|
2025-12-28 21:41:09 +01:00
|
|
|
# Left Panel: Page List
|
|
|
|
|
left_layout = QVBoxLayout()
|
|
|
|
|
left_label = QLabel("Pages")
|
|
|
|
|
left_layout.addWidget(left_label)
|
2025-12-28 21:38:21 +01:00
|
|
|
|
2025-12-28 21:41:09 +01:00
|
|
|
self.page_list = QListWidget()
|
|
|
|
|
self.page_list.setFixedWidth(150)
|
|
|
|
|
self.page_list.itemClicked.connect(self.on_page_selected)
|
|
|
|
|
left_layout.addWidget(self.page_list)
|
|
|
|
|
|
2025-12-28 21:57:44 +01:00
|
|
|
# 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()
|
|
|
|
|
|
2025-12-28 21:41:09 +01:00
|
|
|
self.layout.addLayout(left_layout)
|
|
|
|
|
|
|
|
|
|
# Center Area Layout (Top Bar + Canvas)
|
|
|
|
|
center_layout = QVBoxLayout()
|
|
|
|
|
|
|
|
|
|
# Top Bar: Subpage Selector
|
|
|
|
|
top_bar = QHBoxLayout()
|
|
|
|
|
self.subpage_label = QLabel("Subpage:")
|
|
|
|
|
self.subpage_combo = QComboBox()
|
|
|
|
|
self.subpage_combo.setMinimumWidth(250)
|
|
|
|
|
self.subpage_combo.currentIndexChanged.connect(self.on_subpage_changed)
|
2025-12-30 21:07:12 +01:00
|
|
|
|
|
|
|
|
self.btn_prev_sub = QPushButton("<")
|
|
|
|
|
self.btn_prev_sub.setFixedWidth(30)
|
|
|
|
|
self.btn_prev_sub.clicked.connect(self.prev_subpage)
|
|
|
|
|
|
|
|
|
|
self.btn_next_sub = QPushButton(">")
|
|
|
|
|
self.btn_next_sub.setFixedWidth(30)
|
|
|
|
|
self.btn_next_sub.clicked.connect(self.next_subpage)
|
|
|
|
|
|
2025-12-28 21:41:09 +01:00
|
|
|
top_bar.addWidget(self.subpage_label)
|
2025-12-30 21:07:12 +01:00
|
|
|
top_bar.addWidget(self.btn_prev_sub)
|
2025-12-28 21:41:09 +01:00
|
|
|
top_bar.addWidget(self.subpage_combo)
|
2025-12-30 21:07:12 +01:00
|
|
|
top_bar.addWidget(self.btn_next_sub)
|
2025-12-28 21:41:09 +01:00
|
|
|
top_bar.addStretch()
|
|
|
|
|
|
|
|
|
|
center_layout.addLayout(top_bar)
|
|
|
|
|
|
2025-12-28 21:57:44 +01:00
|
|
|
# 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)
|
|
|
|
|
|
2025-12-28 21:41:09 +01:00
|
|
|
# Canvas
|
2025-12-28 21:38:21 +01:00
|
|
|
self.canvas = TeletextCanvas()
|
2025-12-28 21:57:44 +01:00
|
|
|
self.canvas.cursorChanged.connect(self.on_cursor_changed)
|
2025-12-28 21:41:09 +01:00
|
|
|
center_layout.addWidget(self.canvas, 1) # Expand
|
|
|
|
|
|
|
|
|
|
self.layout.addLayout(center_layout, 1)
|
2025-12-28 21:38:21 +01:00
|
|
|
|
2025-12-30 21:22:09 +01:00
|
|
|
# Status Bar
|
|
|
|
|
self.status_bar = QStatusBar()
|
|
|
|
|
self.setStatusBar(self.status_bar)
|
|
|
|
|
|
|
|
|
|
self.progress_bar = QProgressBar()
|
|
|
|
|
self.progress_bar.setFixedWidth(200)
|
|
|
|
|
self.progress_bar.setVisible(False)
|
|
|
|
|
self.status_bar.addPermanentWidget(self.progress_bar)
|
|
|
|
|
|
|
|
|
|
self.status_label = QLabel("Ready")
|
|
|
|
|
self.status_bar.addWidget(self.status_label)
|
|
|
|
|
|
2025-12-28 21:38:21 +01:00
|
|
|
# Menus
|
|
|
|
|
self.create_menus()
|
2025-12-30 21:22:09 +01:00
|
|
|
|
2025-12-31 14:20:01 +01:00
|
|
|
def set_modified(self, modified: bool):
|
|
|
|
|
self.is_modified = modified
|
|
|
|
|
title = "Teletext Editor"
|
|
|
|
|
if self.current_file_path:
|
|
|
|
|
title += f" - {os.path.basename(self.current_file_path)}"
|
|
|
|
|
else:
|
|
|
|
|
title += " - Untitled"
|
|
|
|
|
|
|
|
|
|
if self.is_modified:
|
|
|
|
|
title += " *"
|
|
|
|
|
self.setWindowTitle(title)
|
|
|
|
|
|
|
|
|
|
def maybe_save_changes(self) -> bool:
|
|
|
|
|
if not self.is_modified:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
ret = QMessageBox.warning(self, "Unsaved Changes",
|
|
|
|
|
"The document has been modified.\nDo you want to save your changes?",
|
|
|
|
|
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
|
|
|
|
|
|
|
|
|
|
if ret == QMessageBox.StandardButton.Save:
|
|
|
|
|
self.save_file()
|
|
|
|
|
return True # check if save succeeded? save_file catches exceptions but we might want to check
|
|
|
|
|
elif ret == QMessageBox.StandardButton.Discard:
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
return False # Cancel
|
|
|
|
|
|
|
|
|
|
def closeEvent(self, event):
|
|
|
|
|
if self.maybe_save_changes():
|
|
|
|
|
event.accept()
|
|
|
|
|
else:
|
|
|
|
|
event.ignore()
|
|
|
|
|
|
2025-12-30 21:22:09 +01:00
|
|
|
def update_progress(self, current, total):
|
|
|
|
|
self.progress_bar.setMaximum(total)
|
|
|
|
|
self.progress_bar.setValue(current)
|
|
|
|
|
QApplication.processEvents() # Force UI update
|
2025-12-28 21:38:21 +01:00
|
|
|
|
|
|
|
|
def create_menus(self):
|
|
|
|
|
menu_bar = self.menuBar()
|
|
|
|
|
|
|
|
|
|
file_menu = menu_bar.addMenu("File")
|
|
|
|
|
|
|
|
|
|
open_action = QAction("Open T42...", self)
|
|
|
|
|
open_action.triggered.connect(self.open_file)
|
|
|
|
|
file_menu.addAction(open_action)
|
|
|
|
|
|
|
|
|
|
save_action = QAction("Save T42...", self)
|
|
|
|
|
save_action.triggered.connect(self.save_file)
|
|
|
|
|
file_menu.addAction(save_action)
|
|
|
|
|
|
2025-12-30 21:29:09 +01:00
|
|
|
save_as_action = QAction("Save As...", self)
|
|
|
|
|
save_as_action.triggered.connect(self.save_as_file)
|
|
|
|
|
file_menu.addAction(save_as_action)
|
|
|
|
|
|
2025-12-30 21:05:25 +01:00
|
|
|
close_action = QAction("Close File", self)
|
|
|
|
|
close_action.triggered.connect(self.close_file)
|
|
|
|
|
file_menu.addAction(close_action)
|
|
|
|
|
|
|
|
|
|
file_menu.addSeparator()
|
|
|
|
|
|
2025-12-28 21:38:21 +01:00
|
|
|
exit_action = QAction("Exit", self)
|
|
|
|
|
exit_action.triggered.connect(self.close)
|
|
|
|
|
file_menu.addAction(exit_action)
|
|
|
|
|
|
2025-12-31 11:45:17 +01:00
|
|
|
# Edit Menu
|
|
|
|
|
edit_menu = menu_bar.addMenu("Edit")
|
|
|
|
|
|
|
|
|
|
copy_action = QAction("Copy Page Content", self)
|
|
|
|
|
copy_action.triggered.connect(self.copy_page_content)
|
|
|
|
|
edit_menu.addAction(copy_action)
|
|
|
|
|
|
|
|
|
|
paste_action = QAction("Paste Page Content", self)
|
|
|
|
|
paste_action.triggered.connect(self.paste_page_content)
|
|
|
|
|
edit_menu.addAction(paste_action)
|
|
|
|
|
|
2025-12-31 13:40:22 +01:00
|
|
|
edit_menu.addSeparator()
|
|
|
|
|
|
|
|
|
|
undo_action = QAction("Undo", self)
|
|
|
|
|
undo_action.setShortcut("Ctrl+Z")
|
|
|
|
|
undo_action.triggered.connect(self.undo)
|
|
|
|
|
edit_menu.addAction(undo_action)
|
|
|
|
|
|
|
|
|
|
redo_action = QAction("Redo", self)
|
|
|
|
|
redo_action.setShortcut("Ctrl+Y")
|
|
|
|
|
redo_action.triggered.connect(self.redo)
|
|
|
|
|
edit_menu.addAction(redo_action)
|
|
|
|
|
|
2025-12-28 21:38:21 +01:00
|
|
|
view_menu = menu_bar.addMenu("View")
|
|
|
|
|
|
|
|
|
|
lang_menu = view_menu.addMenu("Language")
|
|
|
|
|
langs = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
|
|
|
|
|
for i, lang in enumerate(langs):
|
|
|
|
|
action = QAction(lang, self)
|
|
|
|
|
action.setData(i)
|
|
|
|
|
action.triggered.connect(self.set_language)
|
|
|
|
|
lang_menu.addAction(action)
|
|
|
|
|
|
|
|
|
|
def set_language(self):
|
|
|
|
|
action = self.sender()
|
|
|
|
|
if action:
|
|
|
|
|
idx = action.data()
|
|
|
|
|
self.canvas.subset_idx = idx
|
|
|
|
|
self.canvas.redraw()
|
|
|
|
|
self.canvas.update()
|
|
|
|
|
|
2025-12-30 21:07:12 +01:00
|
|
|
def prev_subpage(self):
|
|
|
|
|
count = self.subpage_combo.count()
|
|
|
|
|
if count <= 1: return
|
|
|
|
|
|
|
|
|
|
current = self.subpage_combo.currentIndex()
|
|
|
|
|
new_index = (current - 1) % count
|
|
|
|
|
self.subpage_combo.setCurrentIndex(new_index)
|
|
|
|
|
|
|
|
|
|
def next_subpage(self):
|
|
|
|
|
count = self.subpage_combo.count()
|
|
|
|
|
if count <= 1: return
|
|
|
|
|
|
|
|
|
|
current = self.subpage_combo.currentIndex()
|
|
|
|
|
new_index = (current + 1) % count
|
|
|
|
|
self.subpage_combo.setCurrentIndex(new_index)
|
|
|
|
|
|
2025-12-30 21:05:25 +01:00
|
|
|
def close_file(self):
|
2025-12-31 14:20:01 +01:00
|
|
|
if not self.maybe_save_changes():
|
|
|
|
|
return
|
|
|
|
|
|
2025-12-30 21:05:25 +01:00
|
|
|
# Reset everything
|
|
|
|
|
self.service = TeletextService()
|
|
|
|
|
self.current_page = None
|
2025-12-30 21:20:27 +01:00
|
|
|
self.current_file_path = None
|
2025-12-30 21:05:25 +01:00
|
|
|
self.populate_list()
|
|
|
|
|
self.subpage_combo.clear()
|
|
|
|
|
self.page_groups = {}
|
|
|
|
|
# Clear canvas
|
|
|
|
|
self.canvas.set_page(None)
|
|
|
|
|
# Maybe reset text of hex input
|
|
|
|
|
self.hex_input.clear()
|
2025-12-31 14:20:01 +01:00
|
|
|
|
|
|
|
|
self.undo_stack.clear()
|
|
|
|
|
self.redo_stack.clear()
|
|
|
|
|
|
2025-12-30 21:05:25 +01:00
|
|
|
QMessageBox.information(self, "Closed", "File closed.")
|
2025-12-30 21:20:27 +01:00
|
|
|
self.status_label.setText("Ready")
|
2025-12-31 14:20:01 +01:00
|
|
|
self.set_modified(False)
|
|
|
|
|
|
2025-12-30 21:05:25 +01:00
|
|
|
|
2025-12-28 21:38:21 +01:00
|
|
|
def open_file(self):
|
2025-12-31 14:20:01 +01:00
|
|
|
if not self.maybe_save_changes():
|
|
|
|
|
return
|
|
|
|
|
|
2025-12-28 21:38:21 +01:00
|
|
|
fname, _ = QFileDialog.getOpenFileName(self, "Open T42", "", "Teletext Files (*.t42);;All Files (*)")
|
|
|
|
|
if fname:
|
|
|
|
|
try:
|
2025-12-30 21:20:27 +01:00
|
|
|
self.progress_bar.setVisible(True)
|
|
|
|
|
self.status_label.setText(f"Loading {os.path.basename(fname)}...")
|
|
|
|
|
self.progress_bar.setValue(0)
|
|
|
|
|
|
|
|
|
|
self.service = load_t42(fname, progress_callback=self.update_progress)
|
|
|
|
|
self.current_file_path = fname
|
2025-12-28 21:41:09 +01:00
|
|
|
self.populate_list()
|
2025-12-30 21:20:27 +01:00
|
|
|
|
|
|
|
|
self.progress_bar.setVisible(False)
|
|
|
|
|
self.status_label.setText(f"Loaded {len(self.service.pages)} pages from {os.path.basename(fname)}")
|
2025-12-31 14:20:01 +01:00
|
|
|
|
|
|
|
|
self.undo_stack.clear()
|
|
|
|
|
self.redo_stack.clear()
|
|
|
|
|
self.set_modified(False)
|
2025-12-28 21:38:21 +01:00
|
|
|
except Exception as e:
|
2025-12-30 21:20:27 +01:00
|
|
|
self.progress_bar.setVisible(False)
|
2025-12-28 21:38:21 +01:00
|
|
|
QMessageBox.critical(self, "Error", f"Failed to load file: {e}")
|
2025-12-30 21:20:27 +01:00
|
|
|
self.status_label.setText("Error loading file")
|
2025-12-28 21:38:21 +01:00
|
|
|
|
2025-12-30 21:25:31 +01:00
|
|
|
def save_as_file(self):
|
|
|
|
|
fname, _ = QFileDialog.getSaveFileName(self, "Save T42 As...", "", "Teletext Files (*.t42)")
|
|
|
|
|
if not fname: return
|
|
|
|
|
|
|
|
|
|
self.current_file_path = fname
|
|
|
|
|
self.save_file()
|
|
|
|
|
|
2025-12-28 21:38:21 +01:00
|
|
|
def save_file(self):
|
2025-12-30 21:20:27 +01:00
|
|
|
if not self.current_file_path:
|
|
|
|
|
fname, _ = QFileDialog.getSaveFileName(self, "Save T42", "", "Teletext Files (*.t42)")
|
|
|
|
|
if not fname: return
|
|
|
|
|
self.current_file_path = fname
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
self.progress_bar.setVisible(True)
|
|
|
|
|
self.status_label.setText(f"Saving {os.path.basename(self.current_file_path)}...")
|
|
|
|
|
|
|
|
|
|
save_t42(self.current_file_path, self.service, progress_callback=self.update_progress)
|
|
|
|
|
|
|
|
|
|
self.progress_bar.setVisible(False)
|
|
|
|
|
self.status_label.setText(f"Saved {len(self.service.pages)} pages to {os.path.basename(self.current_file_path)}")
|
2025-12-31 14:20:01 +01:00
|
|
|
self.set_modified(False)
|
2025-12-30 21:20:27 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
self.progress_bar.setVisible(False)
|
|
|
|
|
QMessageBox.critical(self, "Error", f"Failed to save file: {e}")
|
|
|
|
|
self.status_label.setText("Error saving file")
|
2025-12-28 21:38:21 +01:00
|
|
|
|
2025-12-31 11:45:17 +01:00
|
|
|
def copy_page_content(self):
|
|
|
|
|
if not self.current_page:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self.clipboard = []
|
|
|
|
|
# Copy rows 1 to 24 (or up to 25 if exists)
|
|
|
|
|
# Skip row 0 (Header)
|
|
|
|
|
for packet in self.current_page.packets:
|
|
|
|
|
if 1 <= packet.row <= 24:
|
|
|
|
|
# Store row number and copy of data
|
|
|
|
|
self.clipboard.append((packet.row, bytearray(packet.data)))
|
|
|
|
|
|
|
|
|
|
self.status_label.setText(f"Copied {len(self.clipboard)} rows to clipboard.")
|
|
|
|
|
|
|
|
|
|
def paste_page_content(self):
|
|
|
|
|
if not self.current_page:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not self.clipboard:
|
|
|
|
|
self.status_label.setText("Clipboard is empty.")
|
|
|
|
|
return
|
|
|
|
|
|
2025-12-31 13:40:22 +01:00
|
|
|
self.push_undo_state()
|
|
|
|
|
|
2025-12-31 11:45:17 +01:00
|
|
|
# Paste content
|
|
|
|
|
# Strategy: For each copied row, update existing packet or create new one.
|
|
|
|
|
|
|
|
|
|
# Create a map of existing packets by row for quick lookup
|
|
|
|
|
existing_packets = {p.row: p for p in self.current_page.packets}
|
|
|
|
|
|
|
|
|
|
modified_count = 0
|
|
|
|
|
|
|
|
|
|
for row, data in self.clipboard:
|
|
|
|
|
if row in existing_packets:
|
|
|
|
|
# Update existing
|
|
|
|
|
packet = existing_packets[row]
|
|
|
|
|
packet.data = bytearray(data) # Clone again to be safe
|
|
|
|
|
# Note: Packet header (original_data) is now stale if we just update .data
|
|
|
|
|
# but save functionality reconstructs header from .magazine and .row
|
|
|
|
|
# so it should be fine.
|
|
|
|
|
modified_count += 1
|
|
|
|
|
else:
|
|
|
|
|
# Create new packet
|
|
|
|
|
new_packet = create_blank_packet(self.current_page.magazine, row)
|
|
|
|
|
new_packet.data = bytearray(data)
|
|
|
|
|
self.current_page.packets.append(new_packet)
|
|
|
|
|
# Update lookup map just in case (though we won't hit it again usually)
|
|
|
|
|
existing_packets[row] = new_packet
|
|
|
|
|
modified_count += 1
|
|
|
|
|
|
|
|
|
|
# Force redraw
|
|
|
|
|
self.canvas.redraw()
|
|
|
|
|
self.canvas.update()
|
|
|
|
|
self.status_label.setText(f"Pasted {modified_count} rows.")
|
2025-12-31 13:40:22 +01:00
|
|
|
self.push_undo_state() # Push state after paste? NO, before!
|
|
|
|
|
# Wait, usually we push before modifying.
|
|
|
|
|
# But here I just modified it.
|
|
|
|
|
# Correct pattern: Push state BEFORE modifying.
|
|
|
|
|
# So I need to refactor paste_page_content to call push_undo_state() first.
|
|
|
|
|
# For now, I'll add the methods here.
|
|
|
|
|
|
|
|
|
|
def push_undo_state(self):
|
|
|
|
|
if not self.current_page: return
|
|
|
|
|
# Push deep copy of current page
|
|
|
|
|
snapshot = copy.deepcopy(self.current_page)
|
|
|
|
|
self.undo_stack.append(snapshot)
|
|
|
|
|
self.redo_stack.clear() # Clear redo on new edit
|
|
|
|
|
# Limit stack size
|
|
|
|
|
if len(self.undo_stack) > 50:
|
|
|
|
|
self.undo_stack.pop(0)
|
2025-12-31 14:20:01 +01:00
|
|
|
self.set_modified(True)
|
2025-12-31 13:40:22 +01:00
|
|
|
|
|
|
|
|
def undo(self):
|
|
|
|
|
if not self.undo_stack:
|
|
|
|
|
self.status_label.setText("Nothing to undo.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Push current state to redo
|
|
|
|
|
if self.current_page:
|
|
|
|
|
self.redo_stack.append(copy.deepcopy(self.current_page))
|
|
|
|
|
|
|
|
|
|
snapshot = self.undo_stack.pop()
|
|
|
|
|
self.restore_snapshot(snapshot)
|
|
|
|
|
self.status_label.setText("Undone.")
|
2025-12-31 14:20:01 +01:00
|
|
|
self.set_modified(True)
|
2025-12-31 13:40:22 +01:00
|
|
|
|
|
|
|
|
def redo(self):
|
|
|
|
|
if not self.redo_stack:
|
|
|
|
|
self.status_label.setText("Nothing to redo.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Push current state to undo
|
|
|
|
|
if self.current_page:
|
|
|
|
|
self.undo_stack.append(copy.deepcopy(self.current_page))
|
|
|
|
|
|
|
|
|
|
snapshot = self.redo_stack.pop()
|
|
|
|
|
self.restore_snapshot(snapshot)
|
|
|
|
|
self.status_label.setText("Redone.")
|
2025-12-31 14:20:01 +01:00
|
|
|
self.set_modified(True)
|
2025-12-31 13:40:22 +01:00
|
|
|
|
|
|
|
|
def restore_snapshot(self, snapshot: Page):
|
|
|
|
|
# We need to update self.current_page content
|
|
|
|
|
# AND update the usage in service.pages?
|
|
|
|
|
# Actually, self.current_page IS the object in service.pages for now (referenced).
|
|
|
|
|
# But we need to make sure we are modifying the SAME object or replacing it in the list.
|
|
|
|
|
|
|
|
|
|
# Best way: Update attributes of self.current_page matches snapshot
|
|
|
|
|
# snapshot is a Page object.
|
|
|
|
|
if not self.current_page: return
|
|
|
|
|
|
|
|
|
|
# Verify it's the same page ID just in case
|
|
|
|
|
if (self.current_page.magazine != snapshot.magazine or
|
|
|
|
|
self.current_page.page_number != snapshot.page_number):
|
|
|
|
|
# This is tricky if we changed pages. Undo should typically track page switches?
|
|
|
|
|
# For now, we assume undo is local to editing the CURRENT page content.
|
|
|
|
|
# If user switched pages, we might prevent undo or warn.
|
|
|
|
|
# But let's just restoring content.
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
self.current_page.packets = snapshot.packets
|
|
|
|
|
# Also attributes
|
|
|
|
|
self.current_page.sub_code = snapshot.sub_code
|
|
|
|
|
|
|
|
|
|
self.canvas.set_page(self.current_page)
|
|
|
|
|
self.canvas.redraw()
|
|
|
|
|
self.canvas.update()
|
2025-12-31 11:45:17 +01:00
|
|
|
|
2025-12-28 21:41:09 +01:00
|
|
|
def populate_list(self):
|
|
|
|
|
self.page_list.clear()
|
|
|
|
|
|
|
|
|
|
# Group pages by Mag+PageNum
|
|
|
|
|
# We want unique list items
|
|
|
|
|
self.page_groups = {} # Key: (mag, page_num) -> List[Page]
|
2025-12-28 21:38:21 +01:00
|
|
|
|
|
|
|
|
for p in self.service.pages:
|
2025-12-28 21:41:09 +01:00
|
|
|
key = (p.magazine, p.page_number)
|
|
|
|
|
if key not in self.page_groups:
|
|
|
|
|
self.page_groups[key] = []
|
|
|
|
|
self.page_groups[key].append(p)
|
2025-12-28 21:38:21 +01:00
|
|
|
|
2025-12-28 21:41:09 +01:00
|
|
|
# Sort keys
|
|
|
|
|
sorted_keys = sorted(self.page_groups.keys())
|
|
|
|
|
|
|
|
|
|
for mag, pnum in sorted_keys:
|
|
|
|
|
label = f"{mag}{pnum:02d}"
|
|
|
|
|
item = QListWidgetItem(label)
|
|
|
|
|
item.setData(Qt.ItemDataRole.UserRole, (mag, pnum))
|
|
|
|
|
self.page_list.addItem(item)
|
|
|
|
|
|
|
|
|
|
def on_page_selected(self, item):
|
|
|
|
|
mag, pnum = item.data(Qt.ItemDataRole.UserRole)
|
|
|
|
|
pages = self.page_groups.get((mag, pnum), [])
|
|
|
|
|
|
|
|
|
|
# Populate Subpage Combo
|
|
|
|
|
self.subpage_combo.blockSignals(True)
|
|
|
|
|
self.subpage_combo.clear()
|
|
|
|
|
|
|
|
|
|
for i, p in enumerate(pages):
|
|
|
|
|
# Display format: Index or Subcode?
|
|
|
|
|
# Subcode is often 0000. Index 1/N is clearer for editing.
|
|
|
|
|
label = f"{i+1}/{len(pages)} (Sub {p.sub_code:04X})"
|
|
|
|
|
self.subpage_combo.addItem(label, p)
|
2025-12-28 21:38:21 +01:00
|
|
|
|
2025-12-28 21:41:09 +01:00
|
|
|
self.subpage_combo.blockSignals(False)
|
|
|
|
|
|
|
|
|
|
if pages:
|
|
|
|
|
self.subpage_combo.setCurrentIndex(0)
|
|
|
|
|
# Trigger update (manual because blockSignals)
|
|
|
|
|
self.on_subpage_changed(0)
|
2025-12-28 21:38:21 +01:00
|
|
|
|
2025-12-28 21:41:09 +01:00
|
|
|
def on_subpage_changed(self, index):
|
|
|
|
|
if index < 0: return
|
|
|
|
|
page = self.subpage_combo.itemData(index)
|
2025-12-28 21:38:21 +01:00
|
|
|
if isinstance(page, Page):
|
|
|
|
|
self.current_page = page
|
|
|
|
|
self.canvas.set_page(page)
|
|
|
|
|
self.canvas.setFocus()
|
2025-12-28 21:57:44 +01:00
|
|
|
|
|
|
|
|
def insert_char(self, char_code):
|
2025-12-31 13:40:22 +01:00
|
|
|
self.push_undo_state()
|
2025-12-28 21:57:44 +01:00
|
|
|
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.
|
2025-12-31 13:40:22 +01:00
|
|
|
self.push_undo_state()
|
2025-12-28 21:57:44 +01:00
|
|
|
self.canvas.set_byte_at_cursor(val)
|
|
|
|
|
self.canvas.setFocus() # Return focus to canvas
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass # Ignore invalid hex
|
2025-12-28 21:38:21 +01:00
|
|
|
|
|
|
|
|
# Input Handling (Editor Logic)
|
|
|
|
|
def keyPressEvent(self, event: QKeyEvent):
|
|
|
|
|
if not self.current_page:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
key = event.key()
|
|
|
|
|
text = event.text()
|
|
|
|
|
|
|
|
|
|
# Navigation
|
|
|
|
|
if key == Qt.Key.Key_Up:
|
|
|
|
|
self.canvas.move_cursor(0, -1)
|
|
|
|
|
elif key == Qt.Key.Key_Down:
|
|
|
|
|
self.canvas.move_cursor(0, 1)
|
|
|
|
|
elif key == Qt.Key.Key_Left:
|
|
|
|
|
self.canvas.move_cursor(-1, 0)
|
|
|
|
|
elif key == Qt.Key.Key_Right:
|
|
|
|
|
self.canvas.move_cursor(1, 0)
|
2025-12-28 21:57:44 +01:00
|
|
|
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)
|
2025-12-28 21:38:21 +01:00
|
|
|
else:
|
|
|
|
|
# Typing
|
2025-12-30 21:05:25 +01:00
|
|
|
# Allow wider range of chars for national support
|
|
|
|
|
if text and len(text) == 1 and ord(text) >= 32:
|
2025-12-31 13:40:22 +01:00
|
|
|
self.push_undo_state()
|
2025-12-28 21:38:21 +01:00
|
|
|
self.canvas.handle_input(text)
|
|
|
|
|
elif key == Qt.Key.Key_Backspace:
|
2025-12-31 13:40:22 +01:00
|
|
|
self.push_undo_state()
|
2025-12-28 21:38:21 +01:00
|
|
|
# Move back and delete
|
|
|
|
|
self.canvas.move_cursor(-1, 0)
|
|
|
|
|
self.canvas.handle_input(' ')
|
|
|
|
|
self.canvas.move_cursor(-1, 0) # Compensate for the auto-advance logic if any
|
|
|
|
|
|