anki/qt/aqt/deckbrowser.py

350 lines
10 KiB
Python
Raw Normal View History

2019-02-05 04:59:03 +01:00
# Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
2019-12-20 10:19:03 +01:00
from copy import deepcopy
from dataclasses import dataclass
2019-12-20 08:55:19 +01:00
from typing import Any
import aqt
2019-12-20 10:19:03 +01:00
from anki.errors import DeckRenameError
2019-03-04 02:58:34 +01:00
from anki.lang import _, ngettext
2020-02-27 03:25:19 +01:00
from anki.rsbackend import TR
2020-02-21 10:58:54 +01:00
from anki.utils import ids2str
from aqt import AnkiQt, gui_hooks
2019-12-20 10:19:03 +01:00
from aqt.qt import *
2020-01-20 11:10:38 +01:00
from aqt.sound import av_player
from aqt.toolbar import BottomBar
from aqt.utils import (
askUser,
getOnlyText,
openLink,
shortcut,
showWarning,
tr,
)
2019-12-20 10:19:03 +01:00
class DeckBrowserBottomBar:
def __init__(self, deck_browser: DeckBrowser):
self.deck_browser = deck_browser
@dataclass
class DeckBrowserContent:
"""Stores sections of HTML content that the deck browser will be
populated with.
Attributes:
tree {str} -- HTML of the deck tree section
stats {str} -- HTML of the stats section
"""
tree: str
stats: str
class DeckBrowser:
2019-12-20 08:55:19 +01:00
_dueTree: Any
def __init__(self, mw: AnkiQt) -> None:
self.mw = mw
self.web = mw.web
self.bottom = BottomBar(mw, mw.bottomWeb)
self.scrollPos = QPoint(0, 0)
def show(self):
2020-01-20 11:10:38 +01:00
av_player.stop_and_clear_queue()
self.web.set_bridge_command(self._linkHandler, self)
self._renderPage()
# redraw top bar for theme change
self.mw.toolbar.draw()
def refresh(self):
self._renderPage()
# Event handlers
##########################################################################
def _linkHandler(self, url):
if ":" in url:
(cmd, arg) = url.split(":")
else:
cmd = url
if cmd == "open":
self._selDeck(arg)
elif cmd == "opts":
self._showOptions(arg)
elif cmd == "shared":
self._onShared()
elif cmd == "import":
self.mw.onImport()
elif cmd == "create":
deck = getOnlyText(_("Name for deck:"))
if deck:
self.mw.col.decks.id(deck)
self.refresh()
elif cmd == "drag":
2019-12-23 01:34:10 +01:00
draggedDeckDid, ontoDeckDid = arg.split(",")
self._dragDeckOnto(draggedDeckDid, ontoDeckDid)
elif cmd == "collapse":
self._collapse(arg)
return False
def _selDeck(self, did):
self.mw.col.decks.select(did)
self.mw.onOverview()
# HTML generation
##########################################################################
_body = """
<center>
<table cellspacing=0 cellpading=3>
%(tree)s
</table>
<br>
%(stats)s
</center>
"""
def _renderPage(self, reuse=False):
if not reuse:
self._dueTree = self.mw.col.sched.deckDueTree()
self.__renderPage(None)
2019-06-01 08:35:19 +02:00
return
self.web.evalWithCallback("window.pageYOffset", self.__renderPage)
gui_hooks.deck_browser_did_render(self)
def __renderPage(self, offset):
content = DeckBrowserContent(
2020-03-14 10:41:15 +01:00
tree=self._renderDeckTree(self._dueTree), stats=self._renderStats(),
)
gui_hooks.deck_browser_will_render_content(self, content)
2019-12-23 01:34:10 +01:00
self.web.stdHtml(
self._body % content.__dict__,
css=["deckbrowser.css"],
js=["jquery.js", "jquery-ui.js", "deckbrowser.js"],
context=self,
2019-12-23 01:34:10 +01:00
)
self.web.key = "deckBrowser"
self._drawButtons()
if offset is not None:
self._scrollToOffset(offset)
def _scrollToOffset(self, offset):
self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset)
def _renderStats(self):
2019-12-23 01:34:10 +01:00
cards, thetime = self.mw.col.db.first(
"""
select count(), sum(time)/1000 from revlog
2019-12-23 01:34:10 +01:00
where id > ?""",
(self.mw.col.sched.dayCutoff - 86400) * 1000,
)
cards = cards or 0
thetime = thetime or 0
buf = self.mw.col.backend.studied_today(cards, float(thetime))
return buf
def _renderDeckTree(self, nodes, depth=0):
if not nodes:
return ""
if depth == 0:
buf = """
<tr><th colspan=5 align=left>%s</th><th class=count>%s</th>
<th class=count>%s</th><th class=optscol></th></tr>""" % (
2019-12-23 01:34:10 +01:00
_("Deck"),
2020-02-27 03:25:19 +01:00
tr(TR.STATISTICS_DUE_COUNT),
2019-12-23 01:34:10 +01:00
_("New"),
)
buf += self._topLevelDragRow()
else:
buf = ""
2018-05-31 08:24:34 +02:00
nameMap = self.mw.col.decks.nameMap()
for node in nodes:
2018-05-31 08:24:34 +02:00
buf += self._deckRow(node, depth, len(nodes), nameMap)
if depth == 0:
buf += self._topLevelDragRow()
return buf
2018-05-31 08:24:34 +02:00
def _deckRow(self, node, depth, cnt, nameMap):
name, did, due, lrn, new, children = node
deck = self.mw.col.decks.get(did)
if did == 1 and cnt > 1 and not children:
# if the default deck is empty, hide it
if not self.mw.col.db.scalar("select 1 from cards where did = 1"):
return ""
# parent toggled for collapsing
2018-05-31 08:24:34 +02:00
for parent in self.mw.col.decks.parents(did, nameMap):
2019-12-23 01:34:10 +01:00
if parent["collapsed"]:
buff = ""
return buff
prefix = "-"
2019-12-23 01:34:10 +01:00
if self.mw.col.decks.get(did)["collapsed"]:
prefix = "+"
due += lrn
2019-12-23 01:34:10 +01:00
def indent():
2019-12-23 01:34:10 +01:00
return "&nbsp;" * 6 * depth
if did == self.mw.col.conf["curDeck"]:
klass = "deck current"
else:
2019-12-23 01:34:10 +01:00
klass = "deck"
buf = "<tr class='%s' id='%d'>" % (klass, did)
# deck link
if children:
2019-12-23 01:34:10 +01:00
collapse = (
"<a class=collapse href=# onclick='return pycmd(\"collapse:%d\")'>%s</a>"
% (did, prefix)
)
else:
collapse = "<span class=collapse></span>"
2019-12-23 01:34:10 +01:00
if deck["dyn"]:
extraclass = "filtered"
else:
extraclass = ""
buf += """
<td class=decktd colspan=5>%s%s<a class="deck %s"
2019-12-23 01:34:10 +01:00
href=# onclick="return pycmd('open:%d')">%s</a></td>""" % (
indent(),
collapse,
extraclass,
did,
name,
)
# due counts
def nonzeroColour(cnt, klass):
if not cnt:
klass = "zero-count"
if cnt >= 1000:
cnt = "1000+"
return f'<span class="{klass}">{cnt}</span>'
2019-12-23 01:34:10 +01:00
buf += "<td align=right>%s</td><td align=right>%s</td>" % (
nonzeroColour(due, "review-count"),
nonzeroColour(new, "new-count"),
2019-12-23 01:34:10 +01:00
)
# options
2019-12-23 01:34:10 +01:00
buf += (
"<td align=center class=opts><a onclick='return pycmd(\"opts:%d\");'>"
"<img src='/_anki/imgs/gears.svg' class=gears></a></td></tr>" % did
)
# children
2019-12-23 01:34:10 +01:00
buf += self._renderDeckTree(children, depth + 1)
return buf
def _topLevelDragRow(self):
return "<tr class='top-level-drag-row'><td colspan='6'>&nbsp;</td></tr>"
# Options
##########################################################################
def _showOptions(self, did) -> None:
m = QMenu(self.mw)
a = m.addAction(_("Rename"))
qconnect(a.triggered, lambda b, did=did: self._rename(did))
a = m.addAction(_("Options"))
qconnect(a.triggered, lambda b, did=did: self._options(did))
2014-06-20 02:13:12 +02:00
a = m.addAction(_("Export"))
qconnect(a.triggered, lambda b, did=did: self._export(did))
a = m.addAction(_("Delete"))
qconnect(a.triggered, lambda b, did=did: self._delete(did))
gui_hooks.deck_browser_will_show_options_menu(m, did)
m.exec_(QCursor.pos())
2014-06-20 02:13:12 +02:00
def _export(self, did):
self.mw.onExport(did=did)
def _rename(self, did):
self.mw.checkpoint(_("Rename Deck"))
deck = self.mw.col.decks.get(did)
2019-12-23 01:34:10 +01:00
oldName = deck["name"]
newName = getOnlyText(_("New deck name:"), default=oldName)
2013-06-12 04:12:03 +02:00
newName = newName.replace('"', "")
if not newName or newName == oldName:
return
try:
self.mw.col.decks.rename(deck, newName)
except DeckRenameError as e:
return showWarning(e.description)
self.show()
def _options(self, did):
# select the deck first, because the dyn deck conf assumes the deck
# we're editing is the current one
self.mw.col.decks.select(did)
self.mw.onDeckConf()
def _collapse(self, did):
self.mw.col.decks.collapse(did)
self._renderPage(reuse=True)
def _dragDeckOnto(self, draggedDeckDid, ontoDeckDid):
try:
self.mw.col.decks.renameForDragAndDrop(draggedDeckDid, ontoDeckDid)
except DeckRenameError as e:
return showWarning(e.description)
self.show()
def _delete(self, did):
2019-12-23 01:34:10 +01:00
if str(did) == "1":
return showWarning(_("The default deck can't be deleted."))
self.mw.checkpoint(_("Delete Deck"))
deck = self.mw.col.decks.get(did)
2019-12-23 01:34:10 +01:00
if not deck["dyn"]:
dids = [did] + [r[1] for r in self.mw.col.decks.children(did)]
cnt = self.mw.col.db.scalar(
"select count() from cards where did in {0} or "
2019-12-23 01:34:10 +01:00
"odid in {0}".format(ids2str(dids))
)
if cnt:
extra = ngettext(" It has %d card.", " It has %d cards.", cnt) % cnt
else:
extra = None
2019-12-23 01:34:10 +01:00
if (
deck["dyn"]
or not extra
or askUser(
(_("Are you sure you wish to delete %s?") % deck["name"]) + extra
)
):
self.mw.progress.start(immediate=True)
self.mw.col.decks.rem(did, True)
self.mw.progress.finish()
self.show()
# Top buttons
######################################################################
drawLinks = [
2019-12-23 01:34:10 +01:00
["", "shared", _("Get Shared")],
["", "create", _("Create Deck")],
["Ctrl+I", "import", _("Import File")], # Ctrl+I works from menu
]
def _drawButtons(self):
buf = ""
drawLinks = deepcopy(self.drawLinks)
for b in drawLinks:
if b[0]:
b[0] = _("Shortcut key: %s") % 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=self._linkHandler,
web_context=DeckBrowserBottomBar(self),
)
def _onShared(self):
2019-12-23 01:34:10 +01:00
openLink(aqt.appShared + "decks/")