anki/qt/aqt/overview.py

322 lines
10 KiB
Python
Raw Normal View History

2019-02-05 04:59:03 +01:00
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
2020-02-17 16:49:21 +01:00
from dataclasses import dataclass
from typing import Any, Callable
2020-02-17 16:49:21 +01:00
import aqt
import aqt.operations
undoable ops now return changes directly; add new *_ops.py files - Introduced a new transact() method that wraps the return value in a separate struct that describes the changes that were made. - Changes are now gathered from the undo log, so we don't need to guess at what was changed - eg if update_note() is called with identical note contents, no changes are returned. Card changes will only be set if cards were actually generated by the update_note() call, and tag will only be set if a new tag was added. - mw.perform_op() has been updated to expect the op to return the changes, or a structure with the changes in it, and it will use them to fire the change hook, instead of fetching the changes from undo_status(), so there is no risk of race conditions. - the various calls to mw.perform_op() have been split into separate files like card_ops.py. Aside from making the code cleaner, this works around a rather annoying issue with mypy. Because we run it with no_strict_optional, mypy is happy to accept an operation that returns None, despite the type signature saying it requires changes to be returned. Turning no_strict_optional on for the whole codebase is not practical at the moment, but we can enable it for individual files. Still todo: - The cursor keeps moving back to the start of a field when typing - we need to ignore the refresh hook when we are the initiator. - The busy cursor icon should probably be delayed a few hundreds ms. - Still need to think about a nicer way of handling saveNow() - op_made_changes(), op_affects_study_queue() might be better embedded as properties in the object instead
2021-03-16 05:26:42 +01:00
from anki.collection import OpChanges
from anki.scheduler import UnburyDeck
from aqt import gui_hooks
from aqt.deckdescription import DeckDescriptionDialog
from aqt.deckoptions import display_options_for_deck
from aqt.operations import QueryOp
from aqt.operations.scheduling import (
empty_filtered_deck,
rebuild_filtered_deck,
unbury_deck,
)
2020-01-20 11:10:38 +01:00
from aqt.sound import av_player
from aqt.toolbar import BottomBar
from aqt.utils import askUserDialog, openLink, shortcut, tooltip, tr
2019-12-20 10:19:03 +01:00
class OverviewBottomBar:
2021-02-01 14:28:21 +01:00
def __init__(self, overview: Overview) -> None:
self.overview = overview
2020-02-17 16:49:21 +01:00
@dataclass
class OverviewContent:
2020-02-17 16:53:47 +01:00
"""Stores sections of HTML content that the overview will be
populated with.
Attributes:
deck {str} -- Plain text deck name
shareLink {str} -- HTML of the share link section
desc {str} -- HTML of the deck description section
table {str} -- HTML of the deck stats table section
"""
2020-02-17 16:49:21 +01:00
deck: str
shareLink: str
desc: str
table: str
class Overview:
"Deck overview."
def __init__(self, mw: aqt.AnkiQt) -> None:
self.mw = mw
self.web = mw.web
self.bottom = BottomBar(mw, mw.bottomWeb)
self._refresh_needed = False
def show(self) -> None:
2020-01-20 11:10:38 +01:00
av_player.stop_and_clear_queue()
self.web.set_bridge_command(self._linkHandler, self)
self.mw.setStateShortcuts(self._shortcutKeys())
self.refresh()
def refresh(self) -> None:
def success(_counts: tuple) -> None:
self._refresh_needed = False
self._renderPage()
self._renderBottom()
self.mw.web.setFocus()
gui_hooks.overview_did_refresh(self)
QueryOp(
parent=self.mw, op=lambda col: col.sched.counts(), success=success
).run_in_background()
def refresh_if_needed(self) -> None:
if self._refresh_needed:
self.refresh()
2021-03-14 10:54:15 +01:00
def op_executed(
self, changes: OpChanges, handler: object | None, focused: bool
) -> bool:
if changes.study_queues:
self._refresh_needed = True
if focused:
self.refresh_if_needed()
2021-03-14 10:54:15 +01:00
return self._refresh_needed
# Handlers
############################################################
def _linkHandler(self, url: str) -> bool:
if url == "study":
self.mw.col.startTimebox()
self.mw.moveToState("review")
2013-04-11 07:57:03 +02:00
if self.mw.state == "overview":
2021-03-26 04:48:26 +01:00
tooltip(tr.studying_no_cards_are_due_yet())
elif url == "anki":
print("anki menu")
elif url == "opts":
display_options_for_deck(self.mw.col.decks.current())
elif url == "cram":
2021-03-24 04:17:12 +01:00
aqt.dialogs.open("FilteredDeckConfigDialog", self.mw)
elif url == "refresh":
self.rebuild_current_filtered_deck()
elif url == "empty":
self.empty_current_filtered_deck()
elif url == "decks":
self.mw.moveToState("deckBrowser")
elif url == "review":
openLink(f"{aqt.appShared}info/{self.sid}?v={self.sidVer}")
elif url == "studymore" or url == "customStudy":
self.onStudyMore()
elif url == "unbury":
self.on_unbury()
elif url == "description":
self.edit_description()
elif url.lower().startswith("http"):
openLink(url)
return False
def _shortcutKeys(self) -> list[tuple[str, Callable]]:
return [
("o", lambda: display_options_for_deck(self.mw.col.decks.current())),
("r", self.rebuild_current_filtered_deck),
("e", self.empty_current_filtered_deck),
("c", self.onCustomStudyKey),
("u", self.on_unbury),
]
def _current_deck_is_filtered(self) -> int:
2019-12-23 01:34:10 +01:00
return self.mw.col.decks.current()["dyn"]
def rebuild_current_filtered_deck(self) -> None:
rebuild_filtered_deck(
parent=self.mw, deck_id=self.mw.col.decks.selected()
).run_in_background()
def empty_current_filtered_deck(self) -> None:
empty_filtered_deck(
parent=self.mw, deck_id=self.mw.col.decks.selected()
).run_in_background()
def onCustomStudyKey(self) -> None:
if not self._current_deck_is_filtered():
self.onStudyMore()
def on_unbury(self) -> None:
mode = UnburyDeck.Mode.ALL
info = self.mw.col.sched.congratulations_info()
if info.have_sched_buried and info.have_user_buried:
opts = [
tr.studying_manually_buried_cards(),
tr.studying_buried_siblings(),
tr.studying_all_buried_cards(),
tr.actions_cancel(),
]
diag = askUserDialog(tr.studying_what_would_you_like_to_unbury(), opts)
diag.setDefault(0)
ret = diag.run()
if ret == opts[0]:
mode = UnburyDeck.Mode.USER_ONLY
elif ret == opts[1]:
mode = UnburyDeck.Mode.SCHED_ONLY
elif ret == opts[3]:
return
unbury_deck(
parent=self.mw, deck_id=self.mw.col.decks.get_current_id(), mode=mode
).run_in_background()
onUnbury = on_unbury
# HTML
############################################################
def _renderPage(self) -> None:
but = self.mw.button
deck = self.mw.col.decks.current()
self.sid = deck.get("sharedFrom")
if self.sid:
self.sidVer = deck.get("ver", None)
shareLink = '<a class=smallLink href="review">Reviews and Updates</a>'
else:
shareLink = ""
if self.mw.col.sched._is_finished():
self._show_finished_screen()
return
2020-02-17 16:49:21 +01:00
content = OverviewContent(
deck=deck["name"],
shareLink=shareLink,
desc=self._desc(deck),
table=self._table(),
)
gui_hooks.overview_will_render_content(self, content)
2019-12-23 01:34:10 +01:00
self.web.stdHtml(
2020-02-17 16:49:21 +01:00
self._body % content.__dict__,
css=["css/overview.css"],
2021-04-13 19:09:27 +02:00
js=["js/vendor/jquery.min.js"],
context=self,
2019-12-23 01:34:10 +01:00
)
def _show_finished_screen(self) -> None:
self.web.load_ts_page("congrats")
def _desc(self, deck: dict[str, Any]) -> str:
2019-12-23 01:34:10 +01:00
if deck["dyn"]:
2021-03-26 04:48:26 +01:00
desc = tr.studying_this_is_a_special_deck_for()
desc += f" {tr.studying_cards_will_be_automatically_returned_to()}"
desc += f" {tr.studying_deleting_this_deck_from_the_deck()}"
else:
desc = deck.get("desc", "")
if deck.get("md", False):
desc = self.mw.col.render_markdown(desc)
if not desc:
return "<p>"
2019-12-23 01:34:10 +01:00
if deck["dyn"]:
dyn = "dyn"
else:
dyn = ""
return f'<div class="descfont descmid description {dyn}">{desc}</div>'
def _table(self) -> str | None:
counts = list(self.mw.col.sched.counts())
2022-02-22 11:33:11 +01:00
current_did = self.mw.col.decks.get_current_id()
deck_node = self.mw.col.sched.deck_due_tree(current_did)
2022-02-22 11:33:11 +01:00
but = self.mw.button
if self.mw.col.v3_scheduler():
buried_new = deck_node.new_count - counts[0]
buried_learning = deck_node.learn_count - counts[1]
buried_review = deck_node.review_count - counts[2]
else:
buried_new = buried_learning = buried_review = 0
buried_label = tr.studying_counts_differ()
def number_row(title: str, klass: str, count: int, buried_count: int) -> str:
buried = f"{buried_count:+}" if buried_count else ""
return f"""
<tr>
<td>{title}:</td>
<td>
<b>
<span class={klass}>{count}</span>
<span class=bury-count title="{buried_label}">{buried}</span>
</b>
</td>
</tr>
"""
return f"""
<table width=400 cellpadding=5>
<tr><td align=center valign=top>
<table cellspacing=5>
{number_row(tr.actions_new(), "new-count", counts[0], buried_new)}
{number_row(tr.scheduling_learning(), "learn-count", counts[1], buried_learning)}
{number_row(tr.studying_to_review(), "review-count", counts[2], buried_review)}
</table>
</td><td align=center>
{but("study", tr.studying_study_now(), id="study", extra=" autofocus")}</td></tr></table>"""
_body = """
<center>
<h3>%(deck)s</h3>
%(shareLink)s
%(desc)s
%(table)s
</center>
"""
def edit_description(self) -> None:
DeckDescriptionDialog(self.mw)
# Bottom area
######################################################################
def _renderBottom(self) -> None:
links = [
2021-03-26 04:48:26 +01:00
["O", "opts", tr.actions_options()],
]
is_dyn = self.mw.col.decks.current()["dyn"]
if is_dyn:
2021-03-26 04:48:26 +01:00
links.append(["R", "refresh", tr.actions_rebuild()])
links.append(["E", "empty", tr.studying_empty()])
else:
2021-03-26 04:48:26 +01:00
links.append(["C", "studymore", tr.actions_custom_study()])
2019-12-23 01:34:10 +01:00
# links.append(["F", "cram", _("Filter/Cram")])
if self.mw.col.sched.have_buried():
2021-03-26 04:48:26 +01:00
links.append(["U", "unbury", tr.studying_unbury()])
if not is_dyn:
links.append(["", "description", tr.scheduling_description()])
link_handler = gui_hooks.overview_will_render_bottom(
self._linkHandler,
links,
)
if not callable(link_handler):
link_handler = self._linkHandler
buf = ""
for b in links:
if b[0]:
b[0] = tr.actions_shortcut_key(val=shortcut(b[0]))
buf += """
2019-12-23 01:34:10 +01:00
<button title="%s" onclick='pycmd("%s")'>%s</button>""" % tuple(
b
)
self.bottom.draw(
buf=buf,
link_handler=link_handler,
web_context=OverviewBottomBar(self),
)
# Studying more
######################################################################
def onStudyMore(self) -> None:
import aqt.customstudy
2019-12-23 01:34:10 +01:00
aqt.customstudy.CustomStudy.fetch_data_and_show(self.mw)