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-tools = &Tools
qt-accel-undo = &Undo
qt-accel-redo = &Redo
qt-accel-set-due-date = Set &Due Date...
qt-accel-forget = &Forget

View File

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

View File

@ -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
######################################################################

View File

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

View File

@ -46,7 +46,7 @@
<x>0</x>
<y>0</y>
<width>667</width>
<height>22</height>
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="menuHelp">
@ -63,6 +63,7 @@
<string>qt_accel_edit</string>
</property>
<addaction name="actionUndo"/>
<addaction name="actionRedo"/>
</widget>
<widget class="QMenu" name="menuCol">
<property name="title">
@ -237,6 +238,17 @@
<string notr="true">Ctrl+Shift+A</string>
</property>
</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>
<resources>
<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.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</button>""" % (
##########################################################################
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</button>""" % (
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)

View File

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

View File

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

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 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",