more perform_op() tweaks

- pass the handler directly
- reviewer special-cases for flags and notes are now applied at
call site
- drop the kind attribute on OpChanges which is not needed
This commit is contained in:
Damien Elmes 2021-04-06 10:14:11 +10:00
parent 9c8148ff0d
commit 3f62f54f14
18 changed files with 114 additions and 113 deletions

View File

@ -879,11 +879,6 @@ table.review-log {{ {revlog_style} }}
assert_exhaustive(self._undo)
assert False
def op_affects_study_queue(self, changes: OpChanges) -> bool:
if changes.kind == changes.SET_CARD_FLAG:
return False
return changes.card or changes.deck or changes.preference
def op_made_changes(self, changes: OpChanges) -> bool:
for field in changes.DESCRIPTOR.fields:
if field.name != "kind":

View File

@ -24,7 +24,6 @@ from aqt.editor import Editor
from aqt.exporting import ExportDialog
from aqt.find_and_replace import FindAndReplaceDialog
from aqt.main import ResetReason
from aqt.operations import OpMeta
from aqt.operations.card import set_card_deck, set_card_flag
from aqt.operations.collection import undo
from aqt.operations.note import remove_notes
@ -128,12 +127,14 @@ class Browser(QMainWindow):
gui_hooks.browser_will_show(self)
self.show()
def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None:
def on_operation_did_execute(
self, changes: OpChanges, handler: Optional[object]
) -> None:
focused = current_top_level_widget() == self
self.table.op_executed(changes, meta, focused)
self.sidebar.op_executed(changes, meta, focused)
self.table.op_executed(changes, handler, focused)
self.sidebar.op_executed(changes, handler, focused)
if changes.note or changes.notetype:
if meta.handler is not self.editor:
if handler is not self.editor:
# fixme: this will leave the splitter shown, but with no current
# note being edited
note = self.editor.note

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from typing import Any
from typing import Any, Optional
import aqt
from anki.collection import OpChanges
@ -76,8 +76,10 @@ class DeckBrowser:
if self._refresh_needed:
self.refresh()
def op_executed(self, changes: OpChanges, focused: bool) -> bool:
if self.mw.col.op_affects_study_queue(changes):
def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool
) -> bool:
if changes.study_queues:
self._refresh_needed = True
if focused:

View File

@ -1,11 +1,11 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Optional
import aqt.editor
from anki.collection import OpChanges
from anki.errors import NotFoundError
from aqt import gui_hooks
from aqt.operations import OpMeta
from aqt.qt import *
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr
@ -31,8 +31,10 @@ class EditCurrent(QDialog):
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
self.show()
def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None:
if changes.editor and meta.handler is not self.editor:
def on_operation_did_execute(
self, changes: OpChanges, handler: Optional[object]
) -> None:
if changes.editor and handler is not self.editor:
# reload note
note = self.editor.note
try:

View File

@ -100,7 +100,7 @@ class Editor:
redrawing.
The editor will cause that hook to be fired when it saves changes. To avoid
an unwanted refresh, the parent widget should check if meta.handler
an unwanted refresh, the parent widget should check if handler
corresponds to this editor instance, and ignore the change if it does.
"""

View File

@ -61,7 +61,6 @@ 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 import OpMeta
from aqt.operations.collection import undo
from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import *
@ -773,7 +772,7 @@ class AnkiQt(QMainWindow):
success: PerformOpOptionalSuccessCallback = None,
failure: PerformOpOptionalFailureCallback = None,
after_hooks: Optional[Callable[[], None]] = None,
meta: OpMeta = OpMeta(),
handler: Optional[object] = None,
) -> None:
"""Run the provided operation on a background thread.
@ -827,7 +826,7 @@ 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, after_hooks, meta)
self._fire_change_hooks_after_op_performed(result, after_hooks, handler)
self.taskman.with_progress(op, wrapped_done)
@ -846,7 +845,7 @@ class AnkiQt(QMainWindow):
self,
result: ResultWithChanges,
after_hooks: Optional[Callable[[], None]],
meta: OpMeta,
handler: Optional[object],
) -> None:
if isinstance(result, OpChanges):
changes = result
@ -856,7 +855,7 @@ class AnkiQt(QMainWindow):
# fire new hook
print("op changes:")
print(changes)
gui_hooks.operation_did_execute(changes, meta)
gui_hooks.operation_did_execute(changes, handler)
# fire legacy hook so old code notices changes
if self.col.op_made_changes(changes):
gui_hooks.state_did_reset()
@ -872,15 +871,17 @@ class AnkiQt(QMainWindow):
setattr(op, field.name, True)
gui_hooks.operation_did_execute(op, None)
def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None:
def on_operation_did_execute(
self, changes: OpChanges, handler: Optional[object]
) -> None:
"Notify current screen of changes."
focused = current_top_level_widget() == self
if self.state == "review":
dirty = self.reviewer.op_executed(changes, focused)
dirty = self.reviewer.op_executed(changes, handler, focused)
elif self.state == "overview":
dirty = self.overview.op_executed(changes, focused)
dirty = self.overview.op_executed(changes, handler, focused)
elif self.state == "deckBrowser":
dirty = self.deckBrowser.op_executed(changes, focused)
dirty = self.deckBrowser.op_executed(changes, handler, focused)
else:
dirty = False

View File

@ -1,16 +1,2 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from dataclasses import dataclass
from typing import Optional
@dataclass
class OpMeta:
"""Metadata associated with an operation.
The `handler` field can be used by screens to ignore change
events they initiated themselves, if they have already made
the required changes."""
handler: Optional[object] = None

View File

@ -3,16 +3,28 @@
from __future__ import annotations
from typing import Sequence
from typing import Optional, Sequence
from anki.cards import CardId
from anki.decks import DeckId
from aqt import AnkiQt
from aqt.main import PerformOpOptionalSuccessCallback
def set_card_deck(*, mw: AnkiQt, card_ids: Sequence[CardId], deck_id: DeckId) -> None:
mw.perform_op(lambda: mw.col.set_deck(card_ids, deck_id))
def set_card_flag(*, mw: AnkiQt, card_ids: Sequence[CardId], flag: int) -> None:
mw.perform_op(lambda: mw.col.set_user_flag_for_cards(flag, card_ids))
def set_card_flag(
*,
mw: AnkiQt,
card_ids: Sequence[CardId],
flag: int,
handler: Optional[object] = None,
success: PerformOpOptionalSuccessCallback = None,
) -> None:
mw.perform_op(
lambda: mw.col.set_user_flag_for_cards(flag, card_ids),
handler=handler,
success=success,
)

View File

@ -8,7 +8,6 @@ from typing import Callable, Optional, Sequence
from anki.decks import DeckCollapseScope, DeckId
from aqt import AnkiQt, QWidget
from aqt.main import PerformOpOptionalSuccessCallback
from aqt.operations import OpMeta
from aqt.utils import getOnlyText, tooltip, tr
@ -83,5 +82,5 @@ def set_deck_collapsed(
lambda: mw.col.decks.set_collapsed(
deck_id=deck_id, collapsed=collapsed, scope=scope
),
meta=OpMeta(handler=handler),
handler=handler,
)

View File

@ -9,7 +9,6 @@ from anki.decks import DeckId
from anki.notes import Note, NoteId
from aqt import AnkiQt
from aqt.main import PerformOpOptionalSuccessCallback
from aqt.operations import OpMeta
def add_note(
@ -25,7 +24,7 @@ def add_note(
def update_note(*, mw: AnkiQt, note: Note, handler: Optional[object]) -> None:
mw.perform_op(
lambda: mw.col.update_note(note),
meta=OpMeta(handler=handler),
handler=handler,
)

View File

@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Callable, Sequence
from typing import Callable, Optional, Sequence
from anki.collection import OpChangesWithCount
from anki.notes import NoteId
@ -18,9 +18,12 @@ def add_tags_to_notes(
note_ids: Sequence[NoteId],
space_separated_tags: str,
success: PerformOpOptionalSuccessCallback = None,
handler: Optional[object] = None,
) -> None:
mw.perform_op(
lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags), success=success
lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags),
success=success,
handler=handler,
)
@ -30,9 +33,12 @@ def remove_tags_from_notes(
note_ids: Sequence[NoteId],
space_separated_tags: str,
success: PerformOpOptionalSuccessCallback = None,
handler: Optional[object] = None,
) -> None:
mw.perform_op(
lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags), success=success
lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags),
success=success,
handler=handler,
)

View File

@ -64,8 +64,10 @@ class Overview:
if self._refresh_needed:
self.refresh()
def op_executed(self, changes: OpChanges, focused: bool) -> bool:
if self.mw.col.op_affects_study_queue(changes):
def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool
) -> bool:
if changes.study_queues:
self._refresh_needed = True
if focused:

View File

@ -14,7 +14,7 @@ from PyQt5.QtCore import Qt
from anki import hooks
from anki.cards import Card, CardId
from anki.collection import Config, OpChanges
from anki.collection import Config, OpChanges, OpChangesWithCount
from anki.tags import MARKED_TAG
from anki.utils import stripHTML
from aqt import AnkiQt, gui_hooks
@ -38,7 +38,6 @@ from aqt.webview import AnkiWebView
class RefreshNeeded(Enum):
NO = auto()
NOTE_TEXT = auto()
QUEUES = auto()
@ -71,7 +70,7 @@ class Reviewer:
self._recordedAudio: Optional[str] = None
self.typeCorrect: str = None # web init happens before this is set
self.state: Optional[str] = None
self._refresh_needed = RefreshNeeded.NO
self._refresh_needed: Optional[RefreshNeeded] = None
self.bottom = BottomBar(mw, mw.bottomWeb)
hooks.card_did_leech.append(self.onLeech)
@ -102,29 +101,25 @@ class Reviewer:
self.mw.col.reset()
self.nextCard()
self.mw.fade_in_webview()
self._refresh_needed = RefreshNeeded.NO
self._refresh_needed = None
elif self._refresh_needed is RefreshNeeded.NOTE_TEXT:
self._redraw_current_card()
self.mw.fade_in_webview()
self._refresh_needed = RefreshNeeded.NO
self._refresh_needed = None
def op_executed(self, changes: OpChanges, focused: bool) -> bool:
if changes.note and changes.kind == OpChanges.UPDATE_NOTE_TAGS:
self.card.load()
self._update_mark_icon()
elif changes.card and changes.kind == OpChanges.SET_CARD_FLAG:
# fixme: v3 mtime check
self.card.load()
self._update_flag_icon()
elif self.mw.col.op_affects_study_queue(changes):
self._refresh_needed = RefreshNeeded.QUEUES
elif changes.note or changes.notetype or changes.tag:
self._refresh_needed = RefreshNeeded.NOTE_TEXT
def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool
) -> bool:
if handler is not self:
if changes.study_queues:
self._refresh_needed = RefreshNeeded.QUEUES
elif changes.editor:
self._refresh_needed = RefreshNeeded.NOTE_TEXT
if focused and self._refresh_needed is not RefreshNeeded.NO:
if focused and self._refresh_needed:
self.refresh_if_needed()
return self._refresh_needed is not RefreshNeeded.NO
return bool(self._refresh_needed)
def _redraw_current_card(self) -> None:
self.card.load()
@ -830,23 +825,45 @@ time = %(time)d;
self.mw.onDeckConf(self.mw.col.decks.get(self.card.current_deck_id()))
def set_flag_on_current_card(self, desired_flag: int) -> None:
def redraw_flag(out: OpChanges) -> None:
self.card.load()
self._update_flag_icon()
# need to toggle off?
if self.card.user_flag() == desired_flag:
flag = 0
else:
flag = desired_flag
set_card_flag(mw=self.mw, card_ids=[self.card.id], flag=flag)
set_card_flag(
mw=self.mw,
card_ids=[self.card.id],
flag=flag,
handler=self,
success=redraw_flag,
)
def toggle_mark_on_current_note(self) -> None:
def redraw_mark(out: OpChangesWithCount) -> None:
self.card.load()
self._update_mark_icon()
note = self.card.note()
if note.has_tag(MARKED_TAG):
remove_tags_from_notes(
mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG
mw=self.mw,
note_ids=[note.id],
space_separated_tags=MARKED_TAG,
handler=self,
success=redraw_mark,
)
else:
add_tags_to_notes(
mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG
mw=self.mw,
note_ids=[note.id],
space_separated_tags=MARKED_TAG,
handler=self,
success=redraw_mark,
)
def on_set_due(self) -> None:

View File

@ -16,7 +16,6 @@ from anki.types import assert_exhaustive
from aqt import colors, gui_hooks
from aqt.clayout import CardLayout
from aqt.models import Models
from aqt.operations import OpMeta
from aqt.operations.deck import (
remove_decks,
rename_deck,
@ -420,8 +419,10 @@ class SidebarTreeView(QTreeView):
# Refreshing
###########################
def op_executed(self, changes: OpChanges, meta: OpMeta, focused: bool) -> None:
if changes.browser_sidebar and not meta.handler is self:
def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool
) -> None:
if changes.browser_sidebar and not handler is self:
self._refresh_needed = True
if focused:
self.refresh_if_needed()

View File

@ -29,7 +29,6 @@ from anki.errors import NotFoundError
from anki.notes import Note, NoteId
from anki.utils import ids2str, isWin
from aqt import colors, gui_hooks
from aqt.operations import OpMeta
from aqt.qt import *
from aqt.theme import theme_manager
from aqt.utils import (
@ -181,7 +180,9 @@ class Table:
def redraw_cells(self) -> None:
self._model.redraw_cells()
def op_executed(self, changes: OpChanges, meta: OpMeta, focused: bool) -> None:
def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool
) -> None:
if changes.browser_table:
self._model.mark_cache_stale()
if focused:

View File

@ -459,7 +459,7 @@ hooks = [
),
Hook(
name="operation_did_execute",
args=["changes: anki.collection.OpChanges", "meta: aqt.operations.OpMeta"],
args=["changes: anki.collection.OpChanges", "handler: Optional[object]"],
doc="""Called after an operation completes.
Changes can be inspected to determine whether the UI needs updating.

View File

@ -1498,26 +1498,17 @@ message GetQueuedCardsOut {
}
message OpChanges {
// this is not an exhaustive list; we can add more cases as we need them
enum Kind {
OTHER = 0;
UPDATE_NOTE_TAGS = 1;
SET_CARD_FLAG = 2;
UPDATE_NOTE = 3;
}
bool card = 1;
bool note = 2;
bool deck = 3;
bool tag = 4;
bool notetype = 5;
bool preference = 6;
Kind kind = 1;
bool card = 2;
bool note = 3;
bool deck = 4;
bool tag = 5;
bool notetype = 6;
bool preference = 7;
bool browser_table = 8;
bool browser_sidebar = 9;
bool editor = 10;
bool study_queues = 11;
bool browser_table = 7;
bool browser_sidebar = 8;
bool editor = 9;
bool study_queues = 10;
}
message UndoStatus {

View File

@ -1,8 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use pb::op_changes::Kind;
use crate::{
backend_proto as pb,
ops::OpChanges,
@ -10,21 +8,9 @@ use crate::{
undo::{UndoOutput, UndoStatus},
};
impl From<Op> for Kind {
fn from(o: Op) -> Self {
match o {
Op::SetFlag => Kind::SetCardFlag,
Op::UpdateTag => Kind::UpdateNoteTags,
Op::UpdateNote => Kind::UpdateNote,
_ => Kind::Other,
}
}
}
impl From<OpChanges> for pb::OpChanges {
fn from(c: OpChanges) -> Self {
pb::OpChanges {
kind: Kind::from(c.op) as i32,
card: c.changes.card,
note: c.changes.note,
deck: c.changes.deck,