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.
This commit is contained in:
Damien Elmes 2021-03-15 00:03:41 +10:00
parent 0a5be6543e
commit 30c7cf1fdd
9 changed files with 68 additions and 61 deletions

View file

@ -1246,7 +1246,6 @@ where id in %s"""
nids = self.selectedNotes() nids = self.selectedNotes()
self.mw.perform_op(lambda: func(nids, tags)) self.mw.perform_op(lambda: func(nids, tags))
self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self)
def clearUnusedTags(self) -> None: def clearUnusedTags(self) -> None:
self.editor.saveNow(self._clearUnusedTags) self.editor.saveNow(self._clearUnusedTags)
@ -1272,13 +1271,15 @@ where id in %s"""
def _suspend_selected_cards(self) -> None: def _suspend_selected_cards(self) -> None:
want_suspend = not self.current_card_is_suspended() want_suspend = not self.current_card_is_suspended()
c = self.selectedCards()
if want_suspend: def op() -> None:
self.col.sched.suspend_cards(c) if want_suspend:
else: self.col.sched.suspend_cards(cids)
self.col.sched.unsuspend_cards(c) else:
self.model.reset() self.col.sched.unsuspend_cards(cids)
self.mw.requireReset(reason=ResetReason.BrowserSuspend, context=self)
cids = self.selectedCards()
self.mw.perform_op(op)
# Exporting # Exporting
###################################################################### ######################################################################

View file

@ -62,7 +62,7 @@ class DeckBrowser:
self.bottom = BottomBar(mw, mw.bottomWeb) self.bottom = BottomBar(mw, mw.bottomWeb)
self.scrollPos = QPoint(0, 0) self.scrollPos = QPoint(0, 0)
self._v1_message_dismissed_at = 0 self._v1_message_dismissed_at = 0
self.refresh_needed = False self._refresh_needed = False
def show(self) -> None: def show(self) -> None:
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
@ -74,19 +74,21 @@ class DeckBrowser:
def refresh(self) -> None: def refresh(self) -> None:
self._renderPage() self._renderPage()
self.refresh_needed = False self._refresh_needed = False
def refresh_if_needed(self) -> None: def refresh_if_needed(self) -> None:
if self.refresh_needed: if self._refresh_needed:
self.refresh() 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): if self.mw.col.op_affects_study_queue(op):
self.refresh_needed = True self._refresh_needed = True
if focused: if focused:
self.refresh_if_needed() self.refresh_if_needed()
return self._refresh_needed
# Event handlers # Event handlers
########################################################################## ##########################################################################

View file

@ -27,7 +27,6 @@ from anki.httpclient import HttpClient
from anki.notes import Note from anki.notes import Note
from anki.utils import checksum, isLin, isWin, namedtmp from anki.utils import checksum, isLin, isWin, namedtmp
from aqt import AnkiQt, colors, gui_hooks from aqt import AnkiQt, colors, gui_hooks
from aqt.main import ResetReason
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player from aqt.sound import av_player
from aqt.theme import theme_manager from aqt.theme import theme_manager
@ -450,7 +449,6 @@ class Editor:
if not self.addMode: if not self.addMode:
self._save_current_note() self._save_current_note()
self.mw.requireReset(reason=ResetReason.EditorBridgeCmd, context=self)
if type == "blur": if type == "blur":
self.currentField = None self.currentField = None
# run any filters # run any filters
@ -544,7 +542,8 @@ 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."
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]]: def fonts(self) -> List[Tuple[str, int, bool]]:
return [ return [

View file

@ -762,11 +762,16 @@ class AnkiQt(QMainWindow):
"Notify current screen of changes." "Notify current screen of changes."
focused = current_top_level_widget() == self focused = current_top_level_widget() == self
if self.state == "review": if self.state == "review":
self.reviewer.op_executed(op, focused) dirty = self.reviewer.op_executed(op, focused)
elif self.state == "overview": elif self.state == "overview":
self.overview.op_executed(op, focused) dirty = self.overview.op_executed(op, focused)
elif self.state == "deckBrowser": 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( def on_focus_did_change(
self, new_focus: Optional[QWidget], _old: Optional[QWidget] self, new_focus: Optional[QWidget], _old: Optional[QWidget]
@ -780,6 +785,12 @@ class AnkiQt(QMainWindow):
elif self.state == "deckBrowser": elif self.state == "deckBrowser":
self.deckBrowser.refresh_if_needed() 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: def reset(self, unused_arg: bool = False) -> None:
"""Legacy method of telling UI to refresh after changes made to DB. """Legacy method of telling UI to refresh after changes made to DB.

View file

@ -43,7 +43,7 @@ class Overview:
self.mw = mw self.mw = mw
self.web = mw.web self.web = mw.web
self.bottom = BottomBar(mw, mw.bottomWeb) self.bottom = BottomBar(mw, mw.bottomWeb)
self.refresh_needed = False self._refresh_needed = False
def show(self) -> None: def show(self) -> None:
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
@ -57,19 +57,21 @@ class Overview:
self._renderBottom() self._renderBottom()
self.mw.web.setFocus() self.mw.web.setFocus()
gui_hooks.overview_did_refresh(self) gui_hooks.overview_did_refresh(self)
self.refresh_needed = False self._refresh_needed = False
def refresh_if_needed(self) -> None: def refresh_if_needed(self) -> None:
if self.refresh_needed: if self._refresh_needed:
self.refresh() 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): if self.mw.col.op_affects_study_queue(op):
self.refresh_needed = True self._refresh_needed = True
if focused: if focused:
self.refresh_if_needed() self.refresh_if_needed()
return self._refresh_needed
# Handlers # Handlers
############################################################ ############################################################

View file

@ -7,7 +7,6 @@ import html
import json import json
import re import re
import unicodedata as ucd import unicodedata as ucd
from enum import Enum, auto
from typing import Any, Callable, List, Match, Optional, Sequence, Tuple, Union from typing import Any, Callable, List, Match, Optional, Sequence, Tuple, Union
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@ -15,7 +14,6 @@ from PyQt5.QtCore import Qt
from anki import hooks from anki import hooks
from anki.cards import Card from anki.cards import Card
from anki.collection import Config, OperationInfo from anki.collection import Config, OperationInfo
from anki.types import assert_exhaustive
from anki.utils import stripHTML from anki.utils import stripHTML
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.profiles import VideoDriver from aqt.profiles import VideoDriver
@ -40,14 +38,6 @@ class ReviewerBottomBar:
self.reviewer = reviewer 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: def replay_audio(card: Card, question_side: bool) -> None:
if question_side: if question_side:
av_player.play_tags(card.question_av_tags()) av_player.play_tags(card.question_av_tags())
@ -71,7 +61,7 @@ class Reviewer:
self._recordedAudio: Optional[str] = None self._recordedAudio: Optional[str] = None
self.typeCorrect: str = None # web init happens before this is set self.typeCorrect: str = None # web init happens before this is set
self.state: Optional[str] = None self.state: Optional[str] = None
self.refresh_needed = RefreshNeeded.NO self._refresh_needed = False
self.bottom = BottomBar(mw, mw.bottomWeb) self.bottom = BottomBar(mw, mw.bottomWeb)
hooks.card_did_leech.append(self.onLeech) hooks.card_did_leech.append(self.onLeech)
@ -80,7 +70,7 @@ class Reviewer:
self.web.set_bridge_command(self._linkHandler, self) self.web.set_bridge_command(self._linkHandler, self)
self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self)) self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self))
self._reps: int = None self._reps: int = None
self.refresh_needed = RefreshNeeded.QUEUE self._refresh_needed = True
self.refresh_if_needed() self.refresh_if_needed()
def lastCard(self) -> Optional[Card]: def lastCard(self) -> Optional[Card]:
@ -98,41 +88,40 @@ class Reviewer:
self.card = None self.card = None
def refresh_if_needed(self) -> None: def refresh_if_needed(self) -> None:
if self.refresh_needed is RefreshNeeded.NO: if self._refresh_needed:
return self.mw.col.reset()
elif self.refresh_needed is RefreshNeeded.NOTE_MARK: 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.card.load()
self._update_mark_icon() self._update_mark_icon()
elif self.refresh_needed is RefreshNeeded.CARD_FLAG: elif op.kind == OperationInfo.SET_CARD_FLAG:
# fixme: v3 mtime check # fixme: v3 mtime check
self.card.load() self.card.load()
self._update_flag_icon() self._update_flag_icon()
elif self.refresh_needed is RefreshNeeded.QUEUE: elif op.kind == OperationInfo.UPDATE_NOTE:
self.mw.col.reset() self._redraw_current_card()
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 self.mw.col.op_affects_study_queue(op): 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: elif op.changes.note or op.changes.notetype or op.changes.tag:
self.refresh_needed = RefreshNeeded.CARD self._redraw_current_card()
else:
self.refresh_needed = RefreshNeeded.NO
if focused: if focused and self._refresh_needed:
self.refresh_if_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 # Fetching a card
########################################################################## ##########################################################################

View file

@ -1457,6 +1457,7 @@ message OperationInfo {
OTHER = 0; OTHER = 0;
UPDATE_NOTE_TAGS = 1; UPDATE_NOTE_TAGS = 1;
SET_CARD_FLAG = 2; SET_CARD_FLAG = 2;
UPDATE_NOTE = 3;
} }
Kind kind = 1; Kind kind = 1;

View file

@ -23,6 +23,7 @@ impl From<Op> for Kind {
match o { match o {
Op::SetFlag => Kind::SetCardFlag, Op::SetFlag => Kind::SetCardFlag,
Op::UpdateTag => Kind::UpdateNoteTags, Op::UpdateTag => Kind::UpdateNoteTags,
Op::UpdateNote => Kind::UpdateNote,
_ => Kind::Other, _ => Kind::Other,
} }
} }

View file

@ -12,6 +12,7 @@ body {
color: var(--text-fg); color: var(--text-fg);
background: var(--window-bg); background: var(--window-bg);
margin: 1em; margin: 1em;
transition: opacity 0.5s ease-out;
} }
a { a {