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()
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
######################################################################

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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