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.
This commit is contained in:
Damien Elmes 2021-05-19 15:18:39 +10:00
parent 1f77be01e7
commit 9f3f6bab7d
10 changed files with 126 additions and 56 deletions

View file

@ -30,5 +30,6 @@ qt-accel-support-anki = &Support Anki...
qt-accel-switch-profile = &Switch Profile qt-accel-switch-profile = &Switch Profile
qt-accel-tools = &Tools qt-accel-tools = &Tools
qt-accel-undo = &Undo qt-accel-undo = &Undo
qt-accel-redo = &Redo
qt-accel-set-due-date = Set &Due Date... qt-accel-set-due-date = Set &Due Date...
qt-accel-forget = &Forget qt-accel-forget = &Forget

View file

@ -885,7 +885,7 @@ table.review-log {{ {revlog_style} }}
########################################################################## ##########################################################################
def undo_status(self) -> UndoStatus: def undo_status(self) -> UndoStatus:
"Return the undo status. At the moment, redo is not supported." "Return the undo status."
# check backend first # check backend first
if status := self._check_backend_undo_status(): if status := self._check_backend_undo_status():
return status return status
@ -939,6 +939,14 @@ table.review-log {{ {revlog_style} }}
self.models._clear_cache() self.models._clear_cache()
return out 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: def undo_legacy(self) -> LegacyUndoResult:
"Returns None if the legacy undo queue is empty." "Returns None if the legacy undo queue is empty."
if isinstance(self._undo, _ReviewsUndo): if isinstance(self._undo, _ReviewsUndo):

View file

@ -19,7 +19,7 @@ from aqt import AnkiQt, gui_hooks
from aqt.editor import Editor from aqt.editor import Editor
from aqt.exporting import ExportDialog from aqt.exporting import ExportDialog
from aqt.operations.card import set_card_deck, set_card_flag 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.note import remove_notes
from aqt.operations.scheduling import ( from aqt.operations.scheduling import (
forget_cards, forget_cards,
@ -35,6 +35,7 @@ from aqt.operations.tag import (
) )
from aqt.qt import * from aqt.qt import *
from aqt.switch import Switch from aqt.switch import Switch
from aqt.undo import UndoActionsInfo
from aqt.utils import ( from aqt.utils import (
HelpPage, HelpPage,
KeyboardModifiersPressed, KeyboardModifiersPressed,
@ -101,7 +102,8 @@ class Browser(QMainWindow):
self.setupMenus() self.setupMenus()
self.setupHooks() self.setupHooks()
self.setupEditor() 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) self.setupSearch(card, search)
gui_hooks.browser_will_show(self) gui_hooks.browser_will_show(self)
self.show() self.show()
@ -139,6 +141,7 @@ class Browser(QMainWindow):
f = self.form f = self.form
# edit # edit
qconnect(f.actionUndo.triggered, self.undo) qconnect(f.actionUndo.triggered, self.undo)
qconnect(f.actionRedo.triggered, self.redo)
qconnect(f.actionInvertSelection.triggered, self.table.invert_selection) qconnect(f.actionInvertSelection.triggered, self.table.invert_selection)
qconnect(f.actionSelectNotes.triggered, self.selectNotes) qconnect(f.actionSelectNotes.triggered, self.selectNotes)
if not isMac: if not isMac:
@ -786,14 +789,14 @@ where id in %s"""
###################################################################### ######################################################################
def setupHooks(self) -> None: 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_will_block.append(self.table.on_backend_will_block)
gui_hooks.backend_did_block.append(self.table.on_backend_did_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.operation_did_execute.append(self.on_operation_did_execute)
gui_hooks.focus_did_change.append(self.on_focus_change) gui_hooks.focus_did_change.append(self.on_focus_change)
def teardownHooks(self) -> None: 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_will_block.remove(self.table.on_backend_will_block)
gui_hooks.backend_did_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) gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
@ -805,10 +808,15 @@ where id in %s"""
def undo(self) -> None: def undo(self) -> None:
undo(parent=self) undo(parent=self)
def onUndoState(self, on: bool) -> None: def redo(self) -> None:
self.form.actionUndo.setEnabled(on) redo(parent=self)
if on:
self.form.actionUndo.setText(self.mw.form.actionUndo.text()) 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 # Edit: replacing
###################################################################### ######################################################################

View file

@ -144,12 +144,12 @@
<attribute name="horizontalHeaderCascadingSectionResizes"> <attribute name="horizontalHeaderCascadingSectionResizes">
<bool>false</bool> <bool>false</bool>
</attribute> </attribute>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderMinimumSectionSize"> <attribute name="horizontalHeaderMinimumSectionSize">
<number>20</number> <number>20</number>
</attribute> </attribute>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderShowSortIndicator" stdset="0"> <attribute name="horizontalHeaderShowSortIndicator" stdset="0">
<bool>true</bool> <bool>true</bool>
</attribute> </attribute>
@ -209,7 +209,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>750</width> <width>750</width>
<height>21</height> <height>24</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuEdit"> <widget class="QMenu" name="menuEdit">
@ -217,6 +217,7 @@
<string>qt_accel_edit</string> <string>qt_accel_edit</string>
</property> </property>
<addaction name="actionUndo"/> <addaction name="actionUndo"/>
<addaction name="actionRedo"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_toggle_mode"/> <addaction name="action_toggle_mode"/>
<addaction name="separator"/> <addaction name="separator"/>
@ -319,7 +320,7 @@
<string>qt_accel_undo</string> <string>qt_accel_undo</string>
</property> </property>
<property name="shortcut"> <property name="shortcut">
<string notr="true">Ctrl+Alt+Z</string> <string notr="true">Ctrl+Z</string>
</property> </property>
</action> </action>
<action name="actionInvertSelection"> <action name="actionInvertSelection">
@ -613,6 +614,14 @@
<string notr="true">Alt+T</string> <string notr="true">Alt+T</string>
</property> </property>
</action> </action>
<action name="actionRedo">
<property name="text">
<string>qt_accel_redo</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+Shift+Z</string>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="icons.qrc"/> <include location="icons.qrc"/>

View file

@ -46,7 +46,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>667</width> <width>667</width>
<height>22</height> <height>24</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuHelp"> <widget class="QMenu" name="menuHelp">
@ -63,6 +63,7 @@
<string>qt_accel_edit</string> <string>qt_accel_edit</string>
</property> </property>
<addaction name="actionUndo"/> <addaction name="actionUndo"/>
<addaction name="actionRedo"/>
</widget> </widget>
<widget class="QMenu" name="menuCol"> <widget class="QMenu" name="menuCol">
<property name="title"> <property name="title">
@ -237,6 +238,17 @@
<string notr="true">Ctrl+Shift+A</string> <string notr="true">Ctrl+Shift+A</string>
</property> </property>
</action> </action>
<action name="actionRedo">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>qt_accel_redo</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+Shift+Z</string>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="icons.qrc"/> <include location="icons.qrc"/>

View file

@ -53,7 +53,7 @@ from aqt.emptycards import show_empty_cards
from aqt.legacy import install_pylib_legacy from aqt.legacy import install_pylib_legacy
from aqt.mediacheck import check_media_db from aqt.mediacheck import check_media_db
from aqt.mediasync import MediaSyncer 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.operations.deck import set_current_deck
from aqt.profiles import ProfileManager as ProfileManagerType from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import * from aqt.qt import *
@ -61,6 +61,7 @@ from aqt.qt import sip
from aqt.sync import sync_collection, sync_login from aqt.sync import sync_collection, sync_login
from aqt.taskman import TaskManager from aqt.taskman import TaskManager
from aqt.theme import theme_manager from aqt.theme import theme_manager
from aqt.undo import UndoActionsInfo
from aqt.utils import ( from aqt.utils import (
HelpPage, HelpPage,
KeyboardModifiersPressed, KeyboardModifiersPressed,
@ -1070,44 +1071,31 @@ title="%s" %s>%s</button>""" % (
########################################################################## ##########################################################################
def undo(self) -> None: def undo(self) -> None:
"Call collection_ops.py:undo() directly instead." "Call operations/collection.py:undo() directly instead."
undo(parent=self) undo(parent=self)
def update_undo_actions(self, status: Optional[UndoStatus] = None) -> None: def redo(self) -> None:
"""Update menu text and enable/disable menu item as appropriate. "Call operations/collection.py:redo() directly instead."
Plural as this may handle redo in the future too.""" redo(parent=self)
if self.col:
status = status or self.col.undo_status()
undo_action = status.undo or None
else:
undo_action = None
if undo_action: def undo_actions_info(self) -> UndoActionsInfo:
undo_action = tr.undo_undo_action(val=undo_action) "Info about the current undo/redo state for updating menus."
self.form.actionUndo.setText(undo_action) status = self.col.undo_status() if self.col else UndoStatus()
self.form.actionUndo.setEnabled(True) return UndoActionsInfo.from_undo_status(status)
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 _update_undo_actions_for_status_and_save(self, status: UndoStatus) -> None: def update_undo_actions(self) -> None:
"""Update menu text and enable/disable menu item as appropriate. """Tell the UI to redraw the undo/redo menu actions based on the current state.
Plural as this may handle redo in the future too."""
undo_action = status.undo
if undo_action: Usually you do not need to call this directly; it is called when a
undo_action = tr.undo_undo_action(val=undo_action) CollectionOp is run, and will be called when the legacy .reset() or
self.form.actionUndo.setText(undo_action) .checkpoint() methods are used."""
self.form.actionUndo.setEnabled(True) info = self.undo_actions_info()
gui_hooks.undo_state_did_change(True) self.form.actionUndo.setText(info.undo_text)
else: self.form.actionUndo.setEnabled(info.can_undo)
self.form.actionUndo.setText(tr.undo_undo()) self.form.actionRedo.setText(info.redo_text)
self.form.actionUndo.setEnabled(False) self.form.actionRedo.setEnabled(info.can_redo)
gui_hooks.undo_state_did_change(False) self.form.actionRedo.setVisible(info.show_redo)
gui_hooks.undo_state_did_change(info)
self.col.autosave()
def checkpoint(self, name: str) -> None: def checkpoint(self, name: str) -> None:
self.col.save(name) self.col.save(name)
@ -1233,7 +1221,8 @@ title="%s" %s>%s</button>""" % (
qconnect(m.actionExit.triggered, self.close) qconnect(m.actionExit.triggered, self.close)
qconnect(m.actionPreferences.triggered, self.onPrefs) qconnect(m.actionPreferences.triggered, self.onPrefs)
qconnect(m.actionAbout.triggered, self.onAbout) 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: if qtminor < 11:
m.actionUndo.setShortcut(QKeySequence("Ctrl+Alt+Z")) m.actionUndo.setShortcut(QKeySequence("Ctrl+Alt+Z"))
qconnect(m.actionFullDatabaseCheck.triggered, self.onCheckDB) qconnect(m.actionFullDatabaseCheck.triggered, self.onCheckDB)

View file

@ -111,9 +111,8 @@ class CollectionOp(Generic[ResultWithChanges]):
if self._success: if self._success:
self._success(result) self._success(result)
finally: finally:
# update undo status mw.update_undo_actions()
status = mw.col.undo_status() mw.autosave()
mw._update_undo_actions_for_status_and_save(status)
# fire change hooks # fire change hooks
self._fire_change_hooks_after_op_performed(result, initiator) self._fire_change_hooks_after_op_performed(result, initiator)

View file

@ -32,6 +32,15 @@ def undo(*, parent: QWidget) -> None:
).run_in_background() ).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: def _legacy_undo(*, parent: QWidget) -> None:
from aqt import mw from aqt import mw

36
qt/aqt/undo.py Normal file
View file

@ -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,
)

View file

@ -30,6 +30,7 @@ from anki.models import NotetypeDict
from anki.collection import OpChangesAfterUndo from anki.collection import OpChangesAfterUndo
from aqt.qt import QDialog, QEvent, QMenu, QWidget from aqt.qt import QDialog, QEvent, QMenu, QWidget
from aqt.tagedit import TagEdit from aqt.tagedit import TagEdit
from aqt.undo import UndoActionsInfo
""" """
# Hook list # Hook list
@ -675,9 +676,7 @@ gui_hooks.webview_did_inject_style_into_page.append(mytest)
args=["col: anki.collection.Collection"], args=["col: anki.collection.Collection"],
legacy_hook="colLoading", legacy_hook="colLoading",
), ),
Hook( Hook(name="undo_state_did_change", args=["info: UndoActionsInfo"]),
name="undo_state_did_change", args=["can_undo: bool"], legacy_hook="undoState"
),
Hook(name="review_did_undo", args=["card_id: int"], legacy_hook="revertedCard"), Hook(name="review_did_undo", args=["card_id: int"], legacy_hook="revertedCard"),
Hook( Hook(
name="style_did_init", name="style_did_init",