From 30c7cf1fddc13ab011274a56bdf651c77c2e9485 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 15 Mar 2021 00:03:41 +1000 Subject: [PATCH] fade out webview when pending updates; do some reviewer updates immediately Issues that need fixing: - when the editor saves the note with perform_op(), if it isn't modified, no new undo entry is created, and perform_op then returns the changes made by the previous operation instead - the approach of fetching the last action in a subsequent backend method is unsound, as another queued operation may sneak in first before we have a chance to query the result - it would be better if it were returned in a single atomic action - redrawing the current card while editing is likely to make sound autoplay annoyingly, and it has an unpleasant redraw. We may be better off fading it out instead Side note: the editor cursor moves to the start of the field when the note is updated in another window - it might be nicer to have it move the cursor to the end instead. --- qt/aqt/browser.py | 17 ++++++----- qt/aqt/deckbrowser.py | 12 ++++---- qt/aqt/editor.py | 5 ++-- qt/aqt/main.py | 17 +++++++++-- qt/aqt/overview.py | 12 ++++---- qt/aqt/reviewer.py | 63 +++++++++++++++++----------------------- rslib/backend.proto | 1 + rslib/src/backend/ops.rs | 1 + ts/sass/core.scss | 1 + 9 files changed, 68 insertions(+), 61 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index a1e4b425d..75b247b19 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1246,7 +1246,6 @@ where id in %s""" nids = self.selectedNotes() self.mw.perform_op(lambda: func(nids, tags)) - self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self) def clearUnusedTags(self) -> None: self.editor.saveNow(self._clearUnusedTags) @@ -1272,13 +1271,15 @@ where id in %s""" def _suspend_selected_cards(self) -> None: want_suspend = not self.current_card_is_suspended() - c = self.selectedCards() - if want_suspend: - self.col.sched.suspend_cards(c) - else: - self.col.sched.unsuspend_cards(c) - self.model.reset() - self.mw.requireReset(reason=ResetReason.BrowserSuspend, context=self) + + def op() -> None: + if want_suspend: + self.col.sched.suspend_cards(cids) + else: + self.col.sched.unsuspend_cards(cids) + + cids = self.selectedCards() + self.mw.perform_op(op) # Exporting ###################################################################### diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index a25b5e4f0..759866f0a 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -62,7 +62,7 @@ class DeckBrowser: self.bottom = BottomBar(mw, mw.bottomWeb) self.scrollPos = QPoint(0, 0) self._v1_message_dismissed_at = 0 - self.refresh_needed = False + self._refresh_needed = False def show(self) -> None: av_player.stop_and_clear_queue() @@ -74,19 +74,21 @@ class DeckBrowser: def refresh(self) -> None: self._renderPage() - self.refresh_needed = False + self._refresh_needed = False def refresh_if_needed(self) -> None: - if self.refresh_needed: + if self._refresh_needed: self.refresh() - def op_executed(self, op: OperationInfo, focused: bool) -> None: + def op_executed(self, op: OperationInfo, focused: bool) -> bool: if self.mw.col.op_affects_study_queue(op): - self.refresh_needed = True + self._refresh_needed = True if focused: self.refresh_if_needed() + return self._refresh_needed + # Event handlers ########################################################################## diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index ec2d69dd8..979e61aed 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -27,7 +27,6 @@ from anki.httpclient import HttpClient from anki.notes import Note from anki.utils import checksum, isLin, isWin, namedtmp from aqt import AnkiQt, colors, gui_hooks -from aqt.main import ResetReason from aqt.qt import * from aqt.sound import av_player from aqt.theme import theme_manager @@ -450,7 +449,6 @@ class Editor: if not self.addMode: self._save_current_note() - self.mw.requireReset(reason=ResetReason.EditorBridgeCmd, context=self) if type == "blur": self.currentField = None # run any filters @@ -544,7 +542,8 @@ class Editor: def _save_current_note(self) -> None: "Call after note is updated with data from webview." - self.mw.col.update_note(self.note) + note = self.note + self.mw.perform_op(lambda: self.mw.col.update_note(note)) def fonts(self) -> List[Tuple[str, int, bool]]: return [ diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 59f133800..b77701fe9 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -762,11 +762,16 @@ class AnkiQt(QMainWindow): "Notify current screen of changes." focused = current_top_level_widget() == self if self.state == "review": - self.reviewer.op_executed(op, focused) + dirty = self.reviewer.op_executed(op, focused) elif self.state == "overview": - self.overview.op_executed(op, focused) + dirty = self.overview.op_executed(op, focused) elif self.state == "deckBrowser": - self.deckBrowser.op_executed(op, focused) + dirty = self.deckBrowser.op_executed(op, focused) + else: + dirty = False + + if not focused and dirty: + self.fade_out_webview() def on_focus_did_change( self, new_focus: Optional[QWidget], _old: Optional[QWidget] @@ -780,6 +785,12 @@ class AnkiQt(QMainWindow): elif self.state == "deckBrowser": self.deckBrowser.refresh_if_needed() + def fade_out_webview(self) -> None: + self.web.eval("document.body.style.opacity = 0.3") + + def fade_in_webview(self) -> None: + self.web.eval("document.body.style.opacity = 1") + def reset(self, unused_arg: bool = False) -> None: """Legacy method of telling UI to refresh after changes made to DB. diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index b3239313a..ac62de45c 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -43,7 +43,7 @@ class Overview: self.mw = mw self.web = mw.web self.bottom = BottomBar(mw, mw.bottomWeb) - self.refresh_needed = False + self._refresh_needed = False def show(self) -> None: av_player.stop_and_clear_queue() @@ -57,19 +57,21 @@ class Overview: self._renderBottom() self.mw.web.setFocus() gui_hooks.overview_did_refresh(self) - self.refresh_needed = False + self._refresh_needed = False def refresh_if_needed(self) -> None: - if self.refresh_needed: + if self._refresh_needed: self.refresh() - def op_executed(self, op: OperationInfo, focused: bool) -> None: + def op_executed(self, op: OperationInfo, focused: bool) -> bool: if self.mw.col.op_affects_study_queue(op): - self.refresh_needed = True + self._refresh_needed = True if focused: self.refresh_if_needed() + return self._refresh_needed + # Handlers ############################################################ diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index c0b87a2b6..db96b0852 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -7,7 +7,6 @@ import html import json import re import unicodedata as ucd -from enum import Enum, auto from typing import Any, Callable, List, Match, Optional, Sequence, Tuple, Union from PyQt5.QtCore import Qt @@ -15,7 +14,6 @@ from PyQt5.QtCore import Qt from anki import hooks from anki.cards import Card from anki.collection import Config, OperationInfo -from anki.types import assert_exhaustive from anki.utils import stripHTML from aqt import AnkiQt, gui_hooks from aqt.profiles import VideoDriver @@ -40,14 +38,6 @@ class ReviewerBottomBar: self.reviewer = reviewer -class RefreshNeeded(Enum): - NO = auto() - NOTE_MARK = auto() - CARD_FLAG = auto() - QUEUE = auto() - CARD = auto() - - def replay_audio(card: Card, question_side: bool) -> None: if question_side: av_player.play_tags(card.question_av_tags()) @@ -71,7 +61,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 = False self.bottom = BottomBar(mw, mw.bottomWeb) hooks.card_did_leech.append(self.onLeech) @@ -80,7 +70,7 @@ class Reviewer: self.web.set_bridge_command(self._linkHandler, self) self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self)) self._reps: int = None - self.refresh_needed = RefreshNeeded.QUEUE + self._refresh_needed = True self.refresh_if_needed() def lastCard(self) -> Optional[Card]: @@ -98,41 +88,40 @@ class Reviewer: self.card = None def refresh_if_needed(self) -> None: - if self.refresh_needed is RefreshNeeded.NO: - return - elif self.refresh_needed is RefreshNeeded.NOTE_MARK: + if self._refresh_needed: + self.mw.col.reset() + self.nextCard() + self._refresh_needed = False + self.mw.fade_in_webview() + + def op_executed(self, op: OperationInfo, focused: bool) -> bool: + + if op.kind == OperationInfo.UPDATE_NOTE_TAGS: self.card.load() self._update_mark_icon() - elif self.refresh_needed is RefreshNeeded.CARD_FLAG: + elif op.kind == OperationInfo.SET_CARD_FLAG: # fixme: v3 mtime check self.card.load() self._update_flag_icon() - elif self.refresh_needed is RefreshNeeded.QUEUE: - self.mw.col.reset() - self.nextCard() - elif self.refresh_needed is RefreshNeeded.CARD: - self.card.load() - self._showQuestion() - else: - assert_exhaustive(self.refresh_needed) - - self.refresh_needed = RefreshNeeded.NO - - def op_executed(self, op: OperationInfo, focused: bool) -> None: - if op.kind == OperationInfo.UPDATE_NOTE_TAGS: - self.refresh_needed = RefreshNeeded.NOTE_MARK - elif op.kind == OperationInfo.SET_CARD_FLAG: - self.refresh_needed = RefreshNeeded.CARD_FLAG + elif op.kind == OperationInfo.UPDATE_NOTE: + self._redraw_current_card() elif self.mw.col.op_affects_study_queue(op): - self.refresh_needed = RefreshNeeded.QUEUE + self._refresh_needed = True elif op.changes.note or op.changes.notetype or op.changes.tag: - self.refresh_needed = RefreshNeeded.CARD - else: - self.refresh_needed = RefreshNeeded.NO + self._redraw_current_card() - if focused: + if focused and self._refresh_needed: self.refresh_if_needed() + return self._refresh_needed + + def _redraw_current_card(self) -> None: + self.card.load() + if self.state == "answer": + self._showAnswer() + else: + self._showQuestion() + # Fetching a card ########################################################################## diff --git a/rslib/backend.proto b/rslib/backend.proto index 759f3eb65..1f6daed11 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -1457,6 +1457,7 @@ message OperationInfo { OTHER = 0; UPDATE_NOTE_TAGS = 1; SET_CARD_FLAG = 2; + UPDATE_NOTE = 3; } Kind kind = 1; diff --git a/rslib/src/backend/ops.rs b/rslib/src/backend/ops.rs index 4d1447215..71fb2fa11 100644 --- a/rslib/src/backend/ops.rs +++ b/rslib/src/backend/ops.rs @@ -23,6 +23,7 @@ impl From for Kind { match o { Op::SetFlag => Kind::SetCardFlag, Op::UpdateTag => Kind::UpdateNoteTags, + Op::UpdateNote => Kind::UpdateNote, _ => Kind::Other, } } diff --git a/ts/sass/core.scss b/ts/sass/core.scss index f0df18217..47fd71c7c 100644 --- a/ts/sass/core.scss +++ b/ts/sass/core.scss @@ -12,6 +12,7 @@ body { color: var(--text-fg); background: var(--window-bg); margin: 1em; + transition: opacity 0.5s ease-out; } a {