diff --git a/src/teletext/ui.py b/src/teletext/ui.py index 5e61d7a..b9ad22e 100644 --- a/src/teletext/ui.py +++ b/src/teletext/ui.py @@ -13,6 +13,7 @@ from PyQt6.QtCore import Qt from .io import load_t42, save_t42 from .renderer import TeletextCanvas, create_blank_packet +import copy import sys import os from .models import TeletextService, Page, Packet @@ -26,6 +27,8 @@ class MainWindow(QMainWindow): self.service = TeletextService() self.current_page: Page = None self.clipboard = [] # List of (row, data_bytes) + self.undo_stack = [] + self.redo_stack = [] # UI Components self.central_widget = QWidget() @@ -170,6 +173,18 @@ class MainWindow(QMainWindow): paste_action.triggered.connect(self.paste_page_content) edit_menu.addAction(paste_action) + 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) + view_menu = menu_bar.addMenu("View") lang_menu = view_menu.addMenu("Language") @@ -286,6 +301,8 @@ class MainWindow(QMainWindow): self.status_label.setText("Clipboard is empty.") return + self.push_undo_state() + # Paste content # Strategy: For each copied row, update existing packet or create new one. @@ -316,6 +333,75 @@ class MainWindow(QMainWindow): self.canvas.redraw() self.canvas.update() self.status_label.setText(f"Pasted {modified_count} rows.") + 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) + + 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.") + + 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.") + + 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() def populate_list(self): self.page_list.clear() @@ -369,6 +455,7 @@ class MainWindow(QMainWindow): self.canvas.setFocus() def insert_char(self, char_code): + self.push_undo_state() self.canvas.set_byte_at_cursor(char_code) # Advance cursor self.canvas.move_cursor(1, 0) @@ -385,6 +472,7 @@ class MainWindow(QMainWindow): # Update canvas input # We can call handle_input with char, OR set byte directly. # Direct byte set is safer for non-printable. + self.push_undo_state() self.canvas.set_byte_at_cursor(val) self.canvas.setFocus() # Return focus to canvas except ValueError: @@ -415,8 +503,10 @@ class MainWindow(QMainWindow): # Typing # Allow wider range of chars for national support if text and len(text) == 1 and ord(text) >= 32: + self.push_undo_state() self.canvas.handle_input(text) elif key == Qt.Key.Key_Backspace: + self.push_undo_state() # Move back and delete self.canvas.move_cursor(-1, 0) self.canvas.handle_input(' ')