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:
parent
1f77be01e7
commit
9f3f6bab7d
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
######################################################################
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
36
qt/aqt/undo.py
Normal 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,
|
||||
)
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user