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
This commit is contained in:
Damien Elmes 2021-03-16 16:39:41 +10:00
parent 6b0fe4b381
commit 3ad86f1852
6 changed files with 95 additions and 54 deletions

View file

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

View file

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

View file

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

View file

@ -90,8 +90,16 @@ _html = """
</div>
"""
# 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
######################################################################

View file

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

View file

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