mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
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:
parent
6b0fe4b381
commit
3ad86f1852
6 changed files with 95 additions and 54 deletions
|
@ -104,7 +104,7 @@ class AddCards(QDialog):
|
||||||
self.historyButton = b
|
self.historyButton = b
|
||||||
|
|
||||||
def setAndFocusNote(self, note: Note) -> None:
|
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:
|
def show_notetype_selector(self) -> None:
|
||||||
self.editor.saveNow(self.notetype_chooser.choose_notetype)
|
self.editor.saveNow(self.notetype_chooser.choose_notetype)
|
||||||
|
|
|
@ -209,14 +209,21 @@ class DataModel(QAbstractTableModel):
|
||||||
finally:
|
finally:
|
||||||
self.endReset()
|
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:
|
def reset(self) -> None:
|
||||||
self.beginReset()
|
self.beginReset()
|
||||||
self.endReset()
|
self.endReset()
|
||||||
self.refresh_needed = False
|
|
||||||
|
|
||||||
# caller must have called editor.saveNow() before calling this or .reset()
|
# caller must have called editor.saveNow() before calling this or .reset()
|
||||||
def beginReset(self) -> None:
|
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.browser.mw.progress.start()
|
||||||
self.saveSelection()
|
self.saveSelection()
|
||||||
self.beginResetModel()
|
self.beginResetModel()
|
||||||
|
@ -299,7 +306,8 @@ class DataModel(QAbstractTableModel):
|
||||||
|
|
||||||
def refresh_if_needed(self) -> None:
|
def refresh_if_needed(self) -> None:
|
||||||
if self.refresh_needed:
|
if self.refresh_needed:
|
||||||
self.reset()
|
self.redraw_cells()
|
||||||
|
self.refresh_needed = False
|
||||||
|
|
||||||
# Column data
|
# Column data
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -507,6 +515,11 @@ class Browser(QMainWindow):
|
||||||
def on_operation_did_execute(self, changes: OpChanges) -> None:
|
def on_operation_did_execute(self, changes: OpChanges) -> None:
|
||||||
self.setUpdatesEnabled(True)
|
self.setUpdatesEnabled(True)
|
||||||
self.model.op_executed(changes, current_top_level_widget() == self)
|
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:
|
def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None:
|
||||||
if current_top_level_widget() == self:
|
if current_top_level_widget() == self:
|
||||||
|
@ -836,11 +849,11 @@ QTableView {{ gridline-color: {grid} }}
|
||||||
self.form.splitter.widget(1).setVisible(bool(show))
|
self.form.splitter.widget(1).setVisible(bool(show))
|
||||||
|
|
||||||
if not show:
|
if not show:
|
||||||
self.editor.setNote(None)
|
self.editor.set_note(None)
|
||||||
self.singleCard = False
|
self.singleCard = False
|
||||||
self._renderPreview()
|
self._renderPreview()
|
||||||
else:
|
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.focusTo = None
|
||||||
self.editor.card = self.card
|
self.editor.card = self.card
|
||||||
self.singleCard = True
|
self.singleCard = True
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import aqt.editor
|
import aqt.editor
|
||||||
|
from anki.collection import OpChanges
|
||||||
|
from anki.errors import NotFoundError
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.main import ResetReason
|
|
||||||
from aqt.qt import *
|
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):
|
class EditCurrent(QDialog):
|
||||||
|
@ -23,33 +25,38 @@ class EditCurrent(QDialog):
|
||||||
)
|
)
|
||||||
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self)
|
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self)
|
||||||
self.editor.card = self.mw.reviewer.card
|
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")
|
restoreGeom(self, "editcurrent")
|
||||||
gui_hooks.state_did_reset.append(self.onReset)
|
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
||||||
self.mw.requireReset(reason=ResetReason.EditCurrentInit, context=self)
|
|
||||||
self.show()
|
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:
|
def on_operation_did_execute(self, changes: OpChanges) -> None:
|
||||||
# lazy approach for now: throw away edits
|
if not (changes.note or changes.notetype):
|
||||||
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()
|
|
||||||
return
|
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:
|
def reopen(self, mw: aqt.AnkiQt) -> None:
|
||||||
tooltip("Please finish editing the existing card first.")
|
if card := self.mw.reviewer.card:
|
||||||
self.onReset()
|
self.editor.set_note(card.note())
|
||||||
|
|
||||||
def reject(self) -> None:
|
def reject(self) -> None:
|
||||||
self.saveAndClose()
|
self.saveAndClose()
|
||||||
|
@ -58,20 +65,7 @@ class EditCurrent(QDialog):
|
||||||
self.editor.saveNow(self._saveAndClose)
|
self.editor.saveNow(self._saveAndClose)
|
||||||
|
|
||||||
def _saveAndClose(self) -> None:
|
def _saveAndClose(self) -> None:
|
||||||
gui_hooks.state_did_reset.remove(self.onReset)
|
self.cleanup_and_close()
|
||||||
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)
|
|
||||||
|
|
||||||
def closeWithCallback(self, onsuccess: Callable[[], None]) -> None:
|
def closeWithCallback(self, onsuccess: Callable[[], None]) -> None:
|
||||||
def callback() -> None:
|
def callback() -> None:
|
||||||
|
@ -79,3 +73,5 @@ class EditCurrent(QDialog):
|
||||||
onsuccess()
|
onsuccess()
|
||||||
|
|
||||||
self.editor.saveNow(callback)
|
self.editor.saveNow(callback)
|
||||||
|
|
||||||
|
onReset = on_operation_did_execute
|
||||||
|
|
|
@ -90,8 +90,16 @@ _html = """
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# caller is responsible for resetting note on reset
|
|
||||||
class Editor:
|
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__(
|
def __init__(
|
||||||
self, mw: AnkiQt, widget: QWidget, parentWindow: QWidget, addMode: bool = False
|
self, mw: AnkiQt, widget: QWidget, parentWindow: QWidget, addMode: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -101,6 +109,7 @@ class Editor:
|
||||||
self.note: Optional[Note] = None
|
self.note: Optional[Note] = None
|
||||||
self.addMode = addMode
|
self.addMode = addMode
|
||||||
self.currentField: Optional[int] = None
|
self.currentField: Optional[int] = None
|
||||||
|
self._is_updating_note = False
|
||||||
# current card, for card layout
|
# current card, for card layout
|
||||||
self.card: Optional[Card] = None
|
self.card: Optional[Card] = None
|
||||||
self.setupOuter()
|
self.setupOuter()
|
||||||
|
@ -491,7 +500,7 @@ class Editor:
|
||||||
# Setting/unsetting the current note
|
# Setting/unsetting the current note
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setNote(
|
def set_note(
|
||||||
self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None
|
self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"Make NOTE the current note."
|
"Make NOTE the current note."
|
||||||
|
@ -543,7 +552,14 @@ class Editor:
|
||||||
|
|
||||||
def _save_current_note(self) -> None:
|
def _save_current_note(self) -> None:
|
||||||
"Call after note is updated with data from webview."
|
"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]]:
|
def fonts(self) -> List[Tuple[str, int, bool]]:
|
||||||
return [
|
return [
|
||||||
|
@ -596,10 +612,14 @@ class Editor:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
self.setNote(None)
|
self.set_note(None)
|
||||||
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
|
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
|
||||||
self.web = None
|
self.web = None
|
||||||
|
|
||||||
|
# legacy
|
||||||
|
|
||||||
|
setNote = set_note
|
||||||
|
|
||||||
# HTML editing
|
# HTML editing
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
|
@ -722,6 +722,7 @@ class AnkiQt(QMainWindow):
|
||||||
*,
|
*,
|
||||||
success: PerformOpOptionalSuccessCallback = None,
|
success: PerformOpOptionalSuccessCallback = None,
|
||||||
failure: Optional[Callable[[Exception], Any]] = None,
|
failure: Optional[Callable[[Exception], Any]] = None,
|
||||||
|
after_hooks: Optional[Callable[[], None]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run the provided operation on a background thread.
|
"""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.
|
Be careful not to call any UI routines in `op`, as that may crash Qt.
|
||||||
This includes things select .selectedCards() in the browse screen.
|
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
|
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()
|
gui_hooks.operation_will_execute()
|
||||||
|
@ -769,11 +774,13 @@ class AnkiQt(QMainWindow):
|
||||||
status = self.col.undo_status()
|
status = self.col.undo_status()
|
||||||
self._update_undo_actions_for_status_and_save(status)
|
self._update_undo_actions_for_status_and_save(status)
|
||||||
# fire change hooks
|
# 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)
|
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):
|
if isinstance(result, OpChanges):
|
||||||
changes = result
|
changes = result
|
||||||
else:
|
else:
|
||||||
|
@ -786,6 +793,8 @@ class AnkiQt(QMainWindow):
|
||||||
# fire legacy hook so old code notices changes
|
# fire legacy hook so old code notices changes
|
||||||
if self.col.op_made_changes(changes):
|
if self.col.op_made_changes(changes):
|
||||||
gui_hooks.state_did_reset()
|
gui_hooks.state_did_reset()
|
||||||
|
if after_hooks:
|
||||||
|
after_hooks()
|
||||||
|
|
||||||
def _synthesize_op_did_execute_from_reset(self) -> None:
|
def _synthesize_op_did_execute_from_reset(self) -> None:
|
||||||
"""Fire the `operation_did_execute` hook with everything marked as changed,
|
"""Fire the `operation_did_execute` hook with everything marked as changed,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional, Sequence
|
from typing import Callable, Optional, Sequence
|
||||||
|
|
||||||
from anki.lang import TR
|
from anki.lang import TR
|
||||||
from anki.notes import Note
|
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)
|
mw.perform_op(lambda: mw.col.add_note(note, target_deck_id), success=success)
|
||||||
|
|
||||||
|
|
||||||
def update_note(*, mw: AnkiQt, note: Note) -> None:
|
def update_note(*, mw: AnkiQt, note: Note, after_hooks: Callable[[], None]) -> None:
|
||||||
mw.perform_op(lambda: mw.col.update_note(note))
|
mw.perform_op(
|
||||||
|
lambda: mw.col.update_note(note),
|
||||||
|
after_hooks=after_hooks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_notes(
|
def remove_notes(
|
||||||
|
|
Loading…
Reference in a new issue