From 446a58717e833ee5f159304a166f2a739863be03 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 5 May 2026 17:50:55 -0400 Subject: [PATCH] conductor(checkpoint): Checkpoint end of Phase 4 - UI Features & History List --- src/gui_2.py | 43 +++++++++++++++++++++++++++++++++++++++++++ src/history.py | 17 +++++++++++++++++ tests/test_history.py | 18 ++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/src/gui_2.py b/src/gui_2.py index 1f88ac4..d42d6ed 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -229,6 +229,7 @@ class App: self.show_windows.setdefault("Tier 4: QA", False) self.show_windows.setdefault('External Tools', False) self.show_windows.setdefault('Shader Editor', False) + self.show_windows.setdefault('Undo/Redo History', False) self.ui_multi_viewport = gui_cfg.get("multi_viewport", False) self.layout_presets = self.config.get("layout_presets", {}) self._new_preset_name = "" @@ -357,6 +358,14 @@ class App: sys.stderr.flush() self._apply_snapshot(entry.state) + def _handle_jump_to_history(self, index: int) -> None: + sys.stderr.write(f"[DEBUG History] Jumping to index {index}\n") + sys.stderr.flush() + current = self._take_snapshot() + entry = self.history.jump_to_undo(index, current, "Before Jump") + if entry: + self._apply_snapshot(entry.state) + def _handle_redo(self) -> None: sys.stderr.write(f"[DEBUG History] _handle_redo called. can_redo={self.history.can_redo}\n") sys.stderr.flush() @@ -654,9 +663,43 @@ class App: changed_bloom, self.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', self.shader_uniforms['bloom'], 0.0, 1.0) imgui.end() + def _render_history_window(self) -> None: + if not self.show_windows.get('Undo/Redo History', False): + return + + exp, opened = imgui.begin("Undo/Redo History", self.show_windows['Undo/Redo History']) + self.show_windows['Undo/Redo History'] = bool(opened) + if exp: + if imgui.button("Undo") and self.history.can_undo: + self._handle_undo() + imgui.same_line() + if imgui.button("Redo") and self.history.can_redo: + self._handle_redo() + + imgui.separator() + imgui.begin_child("history_list", imgui.ImVec2(0, 0), True) + history = self.history.get_history() + if not history: + imgui.text("No history available.") + else: + for i, entry in enumerate(reversed(history)): + # Actual index in undo stack + actual_idx = len(history) - 1 - i + desc = entry.get("description", "UI Change") + ts = entry.get("timestamp", 0.0) + import datetime + ts_str = datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S") + + label = f"[{ts_str}] {desc}##{actual_idx}" + if imgui.selectable(label)[0]: + self._handle_jump_to_history(actual_idx) + imgui.end_child() + imgui.end() + def _gui_func(self) -> None: self._render_custom_title_bar() self._render_shader_live_editor() + self._render_history_window() pushed_prior_tint = False # Render background shader bg = bg_shader.get_bg() diff --git a/src/history.py b/src/history.py index dd86149..5da2041 100644 --- a/src/history.py +++ b/src/history.py @@ -112,3 +112,20 @@ class HistoryManager: {"description": e.description, "timestamp": e.timestamp} for e in self._undo_stack ] + + def jump_to_undo(self, index: int, current_state: typing.Any, current_description: str = "Before Jump") -> typing.Optional[HistoryEntry]: + """ + Jumps to a specific state in the undo stack by moving subsequent states + and the current_state to the redo stack. + """ + if index < 0 or index >= len(self._undo_stack): + return None + + # Move current state to redo + self._redo_stack.append(HistoryEntry(state=current_state, description=current_description)) + + # Move states between index and top of undo to redo + while len(self._undo_stack) > index + 1: + self._redo_stack.append(self._undo_stack.pop()) + + return self._undo_stack.pop() diff --git a/tests/test_history.py b/tests/test_history.py index 7120dfd..872671b 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -73,3 +73,21 @@ def test_redo_cleared_on_push(): assert hm.can_redo is True hm.push("S2", "D2") assert hm.can_redo is False + +def test_jump_to_undo(): + hm = HistoryManager(max_capacity=10) + hm.push("S0", "D0") + hm.push("S1", "D1") + hm.push("S2", "D2") + hm.push("S3", "D3") + + # Current state is S4 + # Jump to S1 (index 1) + entry = hm.jump_to_undo(1, "S4", "Before Jump") + assert entry.state == "S1" + assert hm.can_undo is True # S0 is still there + assert len(hm._undo_stack) == 1 + assert hm.can_redo is True + # Redo stack should have [S4, S3, S2] + assert len(hm._redo_stack) == 3 + assert hm._redo_stack[-1].state == "S2"