From 3ad86f18526764c0cfd57984797ff57eb46e6634 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 16 Mar 2021 16:39:41 +1000 Subject: [PATCH] prevent editor from refreshing itself after a save - add after_hooks arg to perform_op() - when refreshing browse screen, just redraws cells, and handle editor update in Browser instead of the model --- qt/aqt/addcards.py | 2 +- qt/aqt/browser.py | 23 ++++++++++---- qt/aqt/editcurrent.py | 70 ++++++++++++++++++++----------------------- qt/aqt/editor.py | 28 ++++++++++++++--- qt/aqt/main.py | 17 ++++++++--- qt/aqt/note_ops.py | 9 ++++-- 6 files changed, 95 insertions(+), 54 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index b91609504..5d5e82446 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -104,7 +104,7 @@ class AddCards(QDialog): self.historyButton = b def setAndFocusNote(self, note: Note) -> None: - self.editor.setNote(note, focusTo=0) + self.editor.set_note(note, focusTo=0) def show_notetype_selector(self) -> None: self.editor.saveNow(self.notetype_chooser.choose_notetype) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 089528c8a..c5477944e 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -209,14 +209,21 @@ class DataModel(QAbstractTableModel): finally: self.endReset() + def redraw_cells(self) -> None: + "Update cell contents, without changing search count/columns/sorting." + if not self.cards: + return + top_left = self.index(0, 0) + bottom_right = self.index(len(self.cards)-1, len(self.activeCols)-1) + self.dataChanged.emit(top_left, bottom_right) + def reset(self) -> None: self.beginReset() self.endReset() - self.refresh_needed = False # caller must have called editor.saveNow() before calling this or .reset() def beginReset(self) -> None: - self.browser.editor.setNote(None, hide=False) + self.browser.editor.set_note(None, hide=False) self.browser.mw.progress.start() self.saveSelection() self.beginResetModel() @@ -299,7 +306,8 @@ class DataModel(QAbstractTableModel): def refresh_if_needed(self) -> None: if self.refresh_needed: - self.reset() + self.redraw_cells() + self.refresh_needed = False # Column data ###################################################################### @@ -507,6 +515,11 @@ class Browser(QMainWindow): def on_operation_did_execute(self, changes: OpChanges) -> None: self.setUpdatesEnabled(True) self.model.op_executed(changes, current_top_level_widget() == self) + if (changes.note or changes.notetype) and not self.editor.is_updating_note(): + note = self.editor.note + if note: + note.load() + self.editor.set_note(note) def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None: if current_top_level_widget() == self: @@ -836,11 +849,11 @@ QTableView {{ gridline-color: {grid} }} self.form.splitter.widget(1).setVisible(bool(show)) if not show: - self.editor.setNote(None) + self.editor.set_note(None) self.singleCard = False self._renderPreview() else: - self.editor.setNote(self.card.note(reload=True), focusTo=self.focusTo) + self.editor.set_note(self.card.note(reload=True), focusTo=self.focusTo) self.focusTo = None self.editor.card = self.card self.singleCard = True diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index 4a540d76b..92c2867c4 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -1,10 +1,12 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + import aqt.editor +from anki.collection import OpChanges +from anki.errors import NotFoundError from aqt import gui_hooks -from aqt.main import ResetReason from aqt.qt import * -from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tooltip, tr +from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tr class EditCurrent(QDialog): @@ -23,33 +25,38 @@ class EditCurrent(QDialog): ) self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) self.editor.card = self.mw.reviewer.card - self.editor.setNote(self.mw.reviewer.card.note(), focusTo=0) + self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0) restoreGeom(self, "editcurrent") - gui_hooks.state_did_reset.append(self.onReset) - self.mw.requireReset(reason=ResetReason.EditCurrentInit, context=self) + gui_hooks.operation_did_execute.append(self.on_operation_did_execute) self.show() - # reset focus after open, taking care not to retain webview - # pylint: disable=unnecessary-lambda - self.mw.progress.timer(100, lambda: self.editor.web.setFocus(), False) - def onReset(self) -> None: - # lazy approach for now: throw away edits - try: - n = self.editor.note - n.load() # reload in case the model changed - except: - # card's been deleted - gui_hooks.state_did_reset.remove(self.onReset) - self.editor.setNote(None) - self.mw.reset() - aqt.dialogs.markClosed("EditCurrent") - self.close() + def on_operation_did_execute(self, changes: OpChanges) -> None: + if not (changes.note or changes.notetype): return - self.editor.setNote(n) + if self.editor.is_updating_note(): + return + + # reload note + note = self.editor.note + try: + note.load() + except NotFoundError: + # note's been deleted + self.cleanup_and_close() + return + + self.editor.set_note(note) + + def cleanup_and_close(self) -> None: + gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) + self.editor.cleanup() + saveGeom(self, "editcurrent") + aqt.dialogs.markClosed("EditCurrent") + QDialog.reject(self) def reopen(self, mw: aqt.AnkiQt) -> None: - tooltip("Please finish editing the existing card first.") - self.onReset() + if card := self.mw.reviewer.card: + self.editor.set_note(card.note()) def reject(self) -> None: self.saveAndClose() @@ -58,20 +65,7 @@ class EditCurrent(QDialog): self.editor.saveNow(self._saveAndClose) def _saveAndClose(self) -> None: - gui_hooks.state_did_reset.remove(self.onReset) - r = self.mw.reviewer - try: - r.card.load() - except: - # card was removed by clayout - pass - else: - self.mw.reviewer.cardQueue.append(self.mw.reviewer.card) - self.editor.cleanup() - self.mw.moveToState("review") - saveGeom(self, "editcurrent") - aqt.dialogs.markClosed("EditCurrent") - QDialog.reject(self) + self.cleanup_and_close() def closeWithCallback(self, onsuccess: Callable[[], None]) -> None: def callback() -> None: @@ -79,3 +73,5 @@ class EditCurrent(QDialog): onsuccess() self.editor.saveNow(callback) + + onReset = on_operation_did_execute diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 9386a6bd6..5a6da7b6a 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -90,8 +90,16 @@ _html = """ """ -# caller is responsible for resetting note on reset class Editor: + """The screen that embeds an editing widget should listen for changes via + the `operation_did_execute` hook, and call set_note() when the editor needs + redrawing. + + The editor will cause that hook to be fired when it saves changes. To avoid + an unwanted refresh, the parent widget should call editor.is_updating_note(), + and avoid re-setting the note if it returns true. + """ + def __init__( self, mw: AnkiQt, widget: QWidget, parentWindow: QWidget, addMode: bool = False ) -> None: @@ -101,6 +109,7 @@ class Editor: self.note: Optional[Note] = None self.addMode = addMode self.currentField: Optional[int] = None + self._is_updating_note = False # current card, for card layout self.card: Optional[Card] = None self.setupOuter() @@ -491,7 +500,7 @@ class Editor: # Setting/unsetting the current note ###################################################################### - def setNote( + def set_note( self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None ) -> None: "Make NOTE the current note." @@ -543,7 +552,14 @@ class Editor: def _save_current_note(self) -> None: "Call after note is updated with data from webview." - update_note(mw=self.mw, note=self.note) + self._is_updating_note = True + update_note(mw=self.mw, note=self.note, after_hooks=self._after_updating_note) + + def _after_updating_note(self) -> None: + self._is_updating_note = False + + def is_updating_note(self) -> bool: + return self._is_updating_note def fonts(self) -> List[Tuple[str, int, bool]]: return [ @@ -596,10 +612,14 @@ class Editor: return True def cleanup(self) -> None: - self.setNote(None) + self.set_note(None) # prevent any remaining evalWithCallback() events from firing after C++ object deleted self.web = None + # legacy + + setNote = set_note + # HTML editing ###################################################################### diff --git a/qt/aqt/main.py b/qt/aqt/main.py index cb1d0a77c..8c8ff9cd4 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -722,6 +722,7 @@ class AnkiQt(QMainWindow): *, success: PerformOpOptionalSuccessCallback = None, failure: Optional[Callable[[Exception], Any]] = None, + after_hooks: Optional[Callable[[], None]] = None, ) -> None: """Run the provided operation on a background thread. @@ -740,10 +741,14 @@ class AnkiQt(QMainWindow): Be careful not to call any UI routines in `op`, as that may crash Qt. This includes things select .selectedCards() in the browse screen. - on_success() will be called with the return value of op(). + success() will be called with the return value of op(). If op() throws an exception, it will be shown in a popup, or - passed to on_exception() if it is provided. + passed to failure() if it is provided. + + after_hooks() will be called after hooks are fired, if it is provided. + Components can use this to ignore change notices generated by operations + they invoke themselves. """ gui_hooks.operation_will_execute() @@ -769,11 +774,13 @@ class AnkiQt(QMainWindow): status = self.col.undo_status() self._update_undo_actions_for_status_and_save(status) # fire change hooks - self._fire_change_hooks_after_op_performed(result) + self._fire_change_hooks_after_op_performed(result, after_hooks) self.taskman.with_progress(op, wrapped_done) - def _fire_change_hooks_after_op_performed(self, result: ResultWithChanges) -> None: + def _fire_change_hooks_after_op_performed( + self, result: ResultWithChanges, after_hooks: Optional[Callable[[], None]] + ) -> None: if isinstance(result, OpChanges): changes = result else: @@ -786,6 +793,8 @@ class AnkiQt(QMainWindow): # fire legacy hook so old code notices changes if self.col.op_made_changes(changes): gui_hooks.state_did_reset() + if after_hooks: + after_hooks() def _synthesize_op_did_execute_from_reset(self) -> None: """Fire the `operation_did_execute` hook with everything marked as changed, diff --git a/qt/aqt/note_ops.py b/qt/aqt/note_ops.py index f5438fc7a..d9861dccd 100644 --- a/qt/aqt/note_ops.py +++ b/qt/aqt/note_ops.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Optional, Sequence +from typing import Callable, Optional, Sequence from anki.lang import TR from anki.notes import Note @@ -23,8 +23,11 @@ def add_note( mw.perform_op(lambda: mw.col.add_note(note, target_deck_id), success=success) -def update_note(*, mw: AnkiQt, note: Note) -> None: - mw.perform_op(lambda: mw.col.update_note(note)) +def update_note(*, mw: AnkiQt, note: Note, after_hooks: Callable[[], None]) -> None: + mw.perform_op( + lambda: mw.col.update_note(note), + after_hooks=after_hooks, + ) def remove_notes(