From 9f3f6bab7dfda8250cc8ff3617c287c6f23e3267 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 19 May 2021 15:18:39 +1000 Subject: [PATCH] enable redo support Also: - fix issues where the Undo action in the Browse screen was not consistent with the main window. The existing hook signature has been changed; from a snapshot of the add-on code from a few months ago, it was not a hook that was being used by anyone. - change the undo shortcut in the Browse window to match the main window. It was different because undoing a change in the editing area could accidentally trigger an undo of an operation, but the damage is limited now that (most) operations can be redone. If it still proves to be a problem, perhaps we should just always swallow ctrl+z when an editing field is focused. --- ftl/qt/qt-accel.ftl | 1 + pylib/anki/collection.py | 10 +++++- qt/aqt/browser/browser.py | 24 +++++++++----- qt/aqt/forms/browser.ui | 19 ++++++++--- qt/aqt/forms/main.ui | 14 +++++++- qt/aqt/main.py | 59 ++++++++++++++------------------- qt/aqt/operations/__init__.py | 5 ++- qt/aqt/operations/collection.py | 9 +++++ qt/aqt/undo.py | 36 ++++++++++++++++++++ qt/tools/genhooks_gui.py | 5 ++- 10 files changed, 126 insertions(+), 56 deletions(-) create mode 100644 qt/aqt/undo.py diff --git a/ftl/qt/qt-accel.ftl b/ftl/qt/qt-accel.ftl index db7346212..e2ac4ae80 100644 --- a/ftl/qt/qt-accel.ftl +++ b/ftl/qt/qt-accel.ftl @@ -30,5 +30,6 @@ qt-accel-support-anki = &Support Anki... qt-accel-switch-profile = &Switch Profile qt-accel-tools = &Tools qt-accel-undo = &Undo +qt-accel-redo = &Redo qt-accel-set-due-date = Set &Due Date... qt-accel-forget = &Forget diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index f403aa904..4c34c96e3 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -885,7 +885,7 @@ table.review-log {{ {revlog_style} }} ########################################################################## def undo_status(self) -> UndoStatus: - "Return the undo status. At the moment, redo is not supported." + "Return the undo status." # check backend first if status := self._check_backend_undo_status(): return status @@ -939,6 +939,14 @@ table.review-log {{ {revlog_style} }} self.models._clear_cache() return out + def redo(self) -> OpChangesAfterUndo: + """Returns result of backend redo operation, or throws UndoEmpty.""" + out = self._backend.redo() + self.clear_python_undo() + if out.changes.notetype: + self.models._clear_cache() + return out + def undo_legacy(self) -> LegacyUndoResult: "Returns None if the legacy undo queue is empty." if isinstance(self._undo, _ReviewsUndo): diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index c2b581137..95eddfd2a 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -19,7 +19,7 @@ from aqt import AnkiQt, gui_hooks from aqt.editor import Editor from aqt.exporting import ExportDialog from aqt.operations.card import set_card_deck, set_card_flag -from aqt.operations.collection import undo +from aqt.operations.collection import redo, undo from aqt.operations.note import remove_notes from aqt.operations.scheduling import ( forget_cards, @@ -35,6 +35,7 @@ from aqt.operations.tag import ( ) from aqt.qt import * from aqt.switch import Switch +from aqt.undo import UndoActionsInfo from aqt.utils import ( HelpPage, KeyboardModifiersPressed, @@ -101,7 +102,8 @@ class Browser(QMainWindow): self.setupMenus() self.setupHooks() self.setupEditor() - self.onUndoState(self.mw.form.actionUndo.isEnabled()) + # disable undo/redo + self.on_undo_state_change(mw.undo_actions_info()) self.setupSearch(card, search) gui_hooks.browser_will_show(self) self.show() @@ -139,6 +141,7 @@ class Browser(QMainWindow): f = self.form # edit qconnect(f.actionUndo.triggered, self.undo) + qconnect(f.actionRedo.triggered, self.redo) qconnect(f.actionInvertSelection.triggered, self.table.invert_selection) qconnect(f.actionSelectNotes.triggered, self.selectNotes) if not isMac: @@ -786,14 +789,14 @@ where id in %s""" ###################################################################### def setupHooks(self) -> None: - gui_hooks.undo_state_did_change.append(self.onUndoState) + gui_hooks.undo_state_did_change.append(self.on_undo_state_change) gui_hooks.backend_will_block.append(self.table.on_backend_will_block) gui_hooks.backend_did_block.append(self.table.on_backend_did_block) gui_hooks.operation_did_execute.append(self.on_operation_did_execute) gui_hooks.focus_did_change.append(self.on_focus_change) def teardownHooks(self) -> None: - gui_hooks.undo_state_did_change.remove(self.onUndoState) + gui_hooks.undo_state_did_change.remove(self.on_undo_state_change) gui_hooks.backend_will_block.remove(self.table.on_backend_will_block) gui_hooks.backend_did_block.remove(self.table.on_backend_will_block) gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) @@ -805,10 +808,15 @@ where id in %s""" def undo(self) -> None: undo(parent=self) - def onUndoState(self, on: bool) -> None: - self.form.actionUndo.setEnabled(on) - if on: - self.form.actionUndo.setText(self.mw.form.actionUndo.text()) + def redo(self) -> None: + redo(parent=self) + + def on_undo_state_change(self, info: UndoActionsInfo) -> None: + self.form.actionUndo.setText(info.undo_text) + self.form.actionUndo.setEnabled(info.can_undo) + self.form.actionRedo.setText(info.redo_text) + self.form.actionRedo.setEnabled(info.can_redo) + self.form.actionRedo.setVisible(info.show_redo) # Edit: replacing ###################################################################### diff --git a/qt/aqt/forms/browser.ui b/qt/aqt/forms/browser.ui index 9f499fe60..eb5528d9a 100644 --- a/qt/aqt/forms/browser.ui +++ b/qt/aqt/forms/browser.ui @@ -144,12 +144,12 @@ false - - false - 20 + + false + true @@ -209,7 +209,7 @@ 0 0 750 - 21 + 24 @@ -217,6 +217,7 @@ qt_accel_edit + @@ -319,7 +320,7 @@ qt_accel_undo - Ctrl+Alt+Z + Ctrl+Z @@ -613,6 +614,14 @@ Alt+T + + + qt_accel_redo + + + Ctrl+Shift+Z + + diff --git a/qt/aqt/forms/main.ui b/qt/aqt/forms/main.ui index 3b270eab7..cfd2a38ad 100644 --- a/qt/aqt/forms/main.ui +++ b/qt/aqt/forms/main.ui @@ -46,7 +46,7 @@ 0 0 667 - 22 + 24 @@ -63,6 +63,7 @@ qt_accel_edit + @@ -237,6 +238,17 @@ Ctrl+Shift+A + + + false + + + qt_accel_redo + + + Ctrl+Shift+Z + + diff --git a/qt/aqt/main.py b/qt/aqt/main.py index e28c3aaf4..3faa3feff 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -53,7 +53,7 @@ from aqt.emptycards import show_empty_cards from aqt.legacy import install_pylib_legacy from aqt.mediacheck import check_media_db from aqt.mediasync import MediaSyncer -from aqt.operations.collection import undo +from aqt.operations.collection import redo, undo from aqt.operations.deck import set_current_deck from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * @@ -61,6 +61,7 @@ from aqt.qt import sip from aqt.sync import sync_collection, sync_login from aqt.taskman import TaskManager from aqt.theme import theme_manager +from aqt.undo import UndoActionsInfo from aqt.utils import ( HelpPage, KeyboardModifiersPressed, @@ -1070,44 +1071,31 @@ title="%s" %s>%s""" % ( ########################################################################## def undo(self) -> None: - "Call collection_ops.py:undo() directly instead." + "Call operations/collection.py:undo() directly instead." undo(parent=self) - def update_undo_actions(self, status: Optional[UndoStatus] = None) -> None: - """Update menu text and enable/disable menu item as appropriate. - Plural as this may handle redo in the future too.""" - if self.col: - status = status or self.col.undo_status() - undo_action = status.undo or None - else: - undo_action = None + def redo(self) -> None: + "Call operations/collection.py:redo() directly instead." + redo(parent=self) - if undo_action: - undo_action = tr.undo_undo_action(val=undo_action) - self.form.actionUndo.setText(undo_action) - self.form.actionUndo.setEnabled(True) - gui_hooks.undo_state_did_change(True) - else: - self.form.actionUndo.setText(tr.undo_undo()) - self.form.actionUndo.setEnabled(False) - gui_hooks.undo_state_did_change(False) + def undo_actions_info(self) -> UndoActionsInfo: + "Info about the current undo/redo state for updating menus." + status = self.col.undo_status() if self.col else UndoStatus() + return UndoActionsInfo.from_undo_status(status) - def _update_undo_actions_for_status_and_save(self, status: UndoStatus) -> None: - """Update menu text and enable/disable menu item as appropriate. - Plural as this may handle redo in the future too.""" - undo_action = status.undo + def update_undo_actions(self) -> None: + """Tell the UI to redraw the undo/redo menu actions based on the current state. - if undo_action: - undo_action = tr.undo_undo_action(val=undo_action) - self.form.actionUndo.setText(undo_action) - self.form.actionUndo.setEnabled(True) - gui_hooks.undo_state_did_change(True) - else: - self.form.actionUndo.setText(tr.undo_undo()) - self.form.actionUndo.setEnabled(False) - gui_hooks.undo_state_did_change(False) - - self.col.autosave() + Usually you do not need to call this directly; it is called when a + CollectionOp is run, and will be called when the legacy .reset() or + .checkpoint() methods are used.""" + info = self.undo_actions_info() + self.form.actionUndo.setText(info.undo_text) + self.form.actionUndo.setEnabled(info.can_undo) + self.form.actionRedo.setText(info.redo_text) + self.form.actionRedo.setEnabled(info.can_redo) + self.form.actionRedo.setVisible(info.show_redo) + gui_hooks.undo_state_did_change(info) def checkpoint(self, name: str) -> None: self.col.save(name) @@ -1233,7 +1221,8 @@ title="%s" %s>%s""" % ( qconnect(m.actionExit.triggered, self.close) qconnect(m.actionPreferences.triggered, self.onPrefs) qconnect(m.actionAbout.triggered, self.onAbout) - qconnect(m.actionUndo.triggered, self.onUndo) + qconnect(m.actionUndo.triggered, self.undo) + qconnect(m.actionRedo.triggered, self.redo) if qtminor < 11: m.actionUndo.setShortcut(QKeySequence("Ctrl+Alt+Z")) qconnect(m.actionFullDatabaseCheck.triggered, self.onCheckDB) diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index 4c5d1db35..01e6d17e8 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -111,9 +111,8 @@ class CollectionOp(Generic[ResultWithChanges]): if self._success: self._success(result) finally: - # update undo status - status = mw.col.undo_status() - mw._update_undo_actions_for_status_and_save(status) + mw.update_undo_actions() + mw.autosave() # fire change hooks self._fire_change_hooks_after_op_performed(result, initiator) diff --git a/qt/aqt/operations/collection.py b/qt/aqt/operations/collection.py index 373921b5b..a2087f19b 100644 --- a/qt/aqt/operations/collection.py +++ b/qt/aqt/operations/collection.py @@ -32,6 +32,15 @@ def undo(*, parent: QWidget) -> None: ).run_in_background() +def redo(*, parent: QWidget) -> None: + "Redo the last operation, and refresh the UI." + + def on_success(out: OpChangesAfterUndo) -> None: + tooltip(tr.undo_action_redone(action=out.operation), parent=parent) + + CollectionOp(parent, lambda col: col.redo()).success(on_success).run_in_background() + + def _legacy_undo(*, parent: QWidget) -> None: from aqt import mw diff --git a/qt/aqt/undo.py b/qt/aqt/undo.py new file mode 100644 index 000000000..ccb96e7f3 --- /dev/null +++ b/qt/aqt/undo.py @@ -0,0 +1,36 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +from dataclasses import dataclass + +from anki.collection import UndoStatus + + +@dataclass +class UndoActionsInfo: + can_undo: bool + can_redo: bool + + undo_text: str + redo_text: str + + # menu item is hidden when legacy undo is active, since it can't be undone + show_redo: bool + + @staticmethod + def from_undo_status(status: UndoStatus) -> UndoActionsInfo: + from aqt import tr + + return UndoActionsInfo( + can_undo=bool(status.undo), + can_redo=bool(status.redo), + undo_text=tr.undo_undo_action(val=status.undo) + if status.undo + else tr.undo_undo(), + redo_text=tr.undo_redo_action(action=status.undo) + if status.redo + else tr.undo_redo(), + show_redo=status.last_step > 0, + ) diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 42e7fb085..de3fa6ccb 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -30,6 +30,7 @@ from anki.models import NotetypeDict from anki.collection import OpChangesAfterUndo from aqt.qt import QDialog, QEvent, QMenu, QWidget from aqt.tagedit import TagEdit +from aqt.undo import UndoActionsInfo """ # Hook list @@ -675,9 +676,7 @@ gui_hooks.webview_did_inject_style_into_page.append(mytest) args=["col: anki.collection.Collection"], legacy_hook="colLoading", ), - Hook( - name="undo_state_did_change", args=["can_undo: bool"], legacy_hook="undoState" - ), + Hook(name="undo_state_did_change", args=["info: UndoActionsInfo"]), Hook(name="review_did_undo", args=["card_id: int"], legacy_hook="revertedCard"), Hook( name="style_did_init",