From 3e45c56f3a10dd4b1610d722cea54ecdd16c163d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 26 Oct 2012 01:58:46 +0900 Subject: [PATCH] improve filtered decks - add a custom study option to the deck overview. it combines the ability to increase the daily limits with the ability to create filtered decks based on presets - removed the presets from the filtered deck dialog. - moved the filter/cram button on the decks/overview screen to the tools menu - filtered decks no longer show their search terms (easily findable by clicking options), and instead show a brief explanation of how they work. - the filter by tags preset now presents a dialog like anki 1.2's active tags dialog. decks will remember their previously selected tags. - the custom study option creates a deck called "Custom Study Session", which will automatically get reused if you custom study in a different deck. you can rename it if you want to keep it. - filtered decks now show in the deck list as blue --- aqt/customstudy.py | 159 +++++++++++++++++++++++++++++++++ aqt/deckbrowser.py | 17 ++-- aqt/dyndeckconf.py | 52 ----------- aqt/main.py | 1 + aqt/overview.py | 51 ++++------- aqt/taglimit.py | 97 ++++++++++++++++++++ designer/customstudy.ui | 191 ++++++++++++++++++++++++++++++++++++++++ designer/dyndconf.ui | 22 ++--- designer/limits.ui | 151 ------------------------------- designer/main.ui | 11 ++- designer/taglimit.ui | 126 ++++++++++++++++++++++++++ 11 files changed, 618 insertions(+), 260 deletions(-) create mode 100644 aqt/customstudy.py create mode 100644 aqt/taglimit.py create mode 100644 designer/customstudy.ui delete mode 100644 designer/limits.ui create mode 100644 designer/taglimit.ui diff --git a/aqt/customstudy.py b/aqt/customstudy.py new file mode 100644 index 000000000..34fba75d8 --- /dev/null +++ b/aqt/customstudy.py @@ -0,0 +1,159 @@ +# Copyright: Damien Elmes +# -*- coding: utf-8 -*- +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from aqt.qt import * +import aqt +from anki.utils import ids2str, isWin, isMac +from aqt.utils import showInfo, showWarning, openHelp, getOnlyText, askUser +from operator import itemgetter +from anki.consts import * + +RADIO_NEW = 1 +RADIO_REV = 2 +RADIO_FORGOT = 3 +RADIO_AHEAD = 4 +RADIO_RANDOM = 5 +RADIO_PREVIEW = 6 +RADIO_TAGS = 7 + +class CustomStudy(QDialog): + def __init__(self, mw): + QDialog.__init__(self, mw) + self.mw = mw + self.deck = self.mw.col.decks.current() + self.form = f = aqt.forms.customstudy.Ui_Dialog() + f.setupUi(self) + self.setWindowModality(Qt.WindowModal) + self.setupSignals() + f.radio1.click() + self.exec_() + + def setupSignals(self): + f = self.form; c = self.connect; s = SIGNAL("clicked()") + c(f.radio1, s, lambda: self.onRadioChange(1)) + c(f.radio2, s, lambda: self.onRadioChange(2)) + c(f.radio3, s, lambda: self.onRadioChange(3)) + c(f.radio4, s, lambda: self.onRadioChange(4)) + c(f.radio5, s, lambda: self.onRadioChange(5)) + c(f.radio6, s, lambda: self.onRadioChange(6)) + c(f.radio7, s, lambda: self.onRadioChange(7)) + + def onRadioChange(self, idx): + f = self.form; sp = f.spin + smin = 1; smax = 9999; sval = 1 + post = _("cards") + tit = "" + spShow = True + def plus(num): + if num == 1000: + num = "1000+" + return ""+str(num)+"" + if idx == RADIO_NEW: + new = self.mw.col.sched.totalNewForCurrentDeck() + self.deck['newToday'] + tit = _("New cards in deck: %s") % plus(new) + pre = _("Increase today's new card limit by") + sval = min(new, self.deck.get('extendNew', 10)) + smax = new + elif idx == RADIO_REV: + rev = self.mw.col.sched.totalRevForCurrentDeck() + tit = _("Reviews due in deck: %s") % plus(rev) + pre = _("Increase today's review limit by") + sval = min(rev, self.deck.get('extendRev', 10)) + elif idx == RADIO_FORGOT: + pre = _("Review cards forgotten in last") + post = _("days") + smax = 30 + elif idx == RADIO_AHEAD: + pre = _("Review ahead by") + post = _("days") + elif idx == RADIO_RANDOM: + pre = _("Select") + post = _("cards randomly from the deck") + sval = 100 + elif idx == RADIO_PREVIEW: + pre = _("Preview new cards added in the last") + post = _("days") + sval = 1 + elif idx == RADIO_TAGS: + tit = _("Press OK to choose tags.") + sval = 100 + spShow = False + pre = post = "" + sp.setShown(spShow) + f.title.setText(tit) + f.title.setShown(not not tit) + f.spin.setMinimum(smin) + f.spin.setMaximum(smax) + f.spin.setValue(sval) + f.preSpin.setText(pre) + f.postSpin.setText(post) + self.radioIdx = idx + + def accept(self): + f = self.form; i = self.radioIdx; spin = f.spin.value() + if i == RADIO_NEW: + self.deck['extendNew'] = spin + self.mw.col.decks.save(self.deck) + self.mw.col.sched.extendLimits(spin, 0) + self.mw.reset() + return QDialog.accept(self) + elif i == RADIO_REV: + self.deck['extendRev'] = spin + self.mw.col.decks.save(self.deck) + self.mw.col.sched.extendLimits(0, spin) + self.mw.reset() + return QDialog.accept(self) + elif i == RADIO_TAGS: + tags = self._getTags() + if not tags: + return + # the rest create a filtered deck + cur = self.mw.col.decks.byName(_("Custom Study Session")) + if cur: + if not cur['dyn']: + showInfo("Please rename the existing Custom Study deck first.") + return QDialog.accept(self) + else: + # safe to empty + self.mw.col.sched.emptyDyn(cur['id']) + # reuse; don't delete as it may have children + dyn = cur + self.mw.col.decks.select(cur['id']) + else: + did = self.mw.col.decks.newDyn(_("Custom Study Session")) + dyn = self.mw.col.decks.get(did) + # and then set various options + if i == RADIO_FORGOT: + dyn['delays'] = [1] + dyn['terms'][0] = ['rated:1:%d' % spin, 9999, DYN_RANDOM] + dyn['resched'] = False + elif i == RADIO_AHEAD: + dyn['delays'] = None + dyn['terms'][0] = ['prop:due<=%d' % spin, 9999, DYN_DUE] + dyn['resched'] = True + elif i == RADIO_RANDOM: + dyn['delays'] = None + dyn['terms'][0] = ['', spin, DYN_RANDOM] + dyn['resched'] = True + elif i == RADIO_PREVIEW: + dyn['delays'] = None + dyn['terms'][0] = ['is:new added:%s'%spin, 9999, DYN_OLDEST] + dyn['resched'] = True + elif i == RADIO_TAGS: + dyn['delays'] = None + dyn['terms'][0] = ["(is:new or is:due) "+tags, 9999, DYN_RANDOM] + dyn['resched'] = True + # add deck limit + dyn['terms'][0][0] = "deck:'%s' %s " % (self.deck['name'], dyn['terms'][0][0]) + # generate cards + if not self.mw.col.sched.rebuildDyn(): + return showWarning(_("No cards matched the criteria you provided.")) + self.mw.moveToState("overview") + QDialog.accept(self) + + def _getTags(self): + from aqt.taglimit import TagLimit + t = TagLimit(self.mw, self) + return t.tags diff --git a/aqt/deckbrowser.py b/aqt/deckbrowser.py index 3eb10386b..756be947e 100644 --- a/aqt/deckbrowser.py +++ b/aqt/deckbrowser.py @@ -44,8 +44,6 @@ class DeckBrowser(object): self._onShared() elif cmd == "import": self.mw.onImport() - elif cmd == "cram": - self.mw.onCram() elif cmd == "create": deck = getOnlyText(_("New deck name:")) if deck: @@ -58,9 +56,8 @@ class DeckBrowser(object): self._collapse(arg) def _keyHandler(self, evt): + # currently does nothing key = unicode(evt.text()) - if key == "f": - self.mw.onCram() def _selDeck(self, did): self.mw.col.decks.select(did) @@ -85,6 +82,7 @@ body { margin: 1em; -webkit-user-select: none; } .count { width: 6em; text-align: right; } .collapse { color: #000; text-decoration:none; display:inline-block; width: 1em; } +.filtered { color: #00a !important; } """ % dict(width=_dragIndicatorBorderWidth) _body = """ @@ -179,6 +177,7 @@ where id > ?""", (self.mw.col.sched.dayCutoff-86400)*1000) def _deckRow(self, node, depth, cnt): 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"): @@ -204,9 +203,14 @@ where id > ?""", (self.mw.col.sched.dayCutoff-86400)*1000) collapse = "%s" % (did, prefix) else: collapse = "" + if deck['dyn']: + extraclass = "filtered" + else: + extraclass = "" buf += """ -%s%s%s"""% ( - indent(), collapse, did, name) + + %s%s%s"""% ( + indent(), collapse, extraclass, did, name) # due counts def nonzeroColour(cnt, colour): if not cnt: @@ -311,7 +315,6 @@ where id > ?""", (self.mw.col.sched.dayCutoff-86400)*1000) ["", "shared", _("Get Shared")], ["", "create", _("Create Deck")], ["Ctrl+I", "import", _("Import File")], - ["F", "cram", _("Filter/Cram")], ] buf = "" for b in links: diff --git a/aqt/dyndeckconf.py b/aqt/dyndeckconf.py index 61611ca6f..b1b1e7924 100644 --- a/aqt/dyndeckconf.py +++ b/aqt/dyndeckconf.py @@ -29,67 +29,15 @@ class DeckConf(QDialog): SIGNAL("helpRequested()"), lambda: openHelp("filtered")) self.setWindowTitle(_("Options for %s") % self.deck['name']) - self.setupExamples() self.setupOrder() self.loadConf() self.show() - if first: - if isMac or isWin: - self.form.examples.showPopup() - else: - mw.progress.timer(200, self.form.examples.showPopup, False) self.exec_() def setupOrder(self): import anki.consts as cs self.form.order.addItems(cs.dynOrderLabels().values()) - def setupExamples(self): - import anki.consts as cs - f = self.form - d = self.dynExamples = cs.dynExamples() - for c, row in enumerate(d): - if not row: - f.examples.insertSeparator(c) - else: - f.examples.addItem(row[0]) - self.connect(f.examples, SIGNAL("activated(int)"), - self.onExample) - # we'll need to reset whenever something is changed - self.ignoreChange = False - def onChange(*args): - if self.ignoreChange: - return - f.examples.setCurrentIndex(0) - c = self.connect - c(f.steps, SIGNAL("textEdited(QString)"), onChange) - c(f.search, SIGNAL("textEdited(QString)"), onChange) - c(f.order, SIGNAL("activated(int)"), onChange) - c(f.limit, SIGNAL("valueChanged(int)"), onChange) - c(f.stepsOn, SIGNAL("stateChanged(int)"), onChange) - c(f.resched, SIGNAL("stateChanged(int)"), onChange) - - def onExample(self, idx): - if idx == 0: - return - p = self.dynExamples[idx][1] - f = self.form - self.ignoreChange = True - search = [p['search']] - if self.search: - search.append(self.search) - f.search.setText(" ".join(search)) - f.order.setCurrentIndex(p['order']) - f.resched.setChecked(p.get("resched", True)) - if p.get("steps"): - f.steps.setText(p['steps']) - f.stepsOn.setChecked(True) - else: - f.steps.setText("1 10") - f.stepsOn.setChecked(False) - f.limit.setValue(1000) - self.ignoreChange = False - def loadConf(self): f = self.form d = self.deck diff --git a/aqt/main.py b/aqt/main.py index e40777cd2..5f0e8bb63 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -745,6 +745,7 @@ and check the statistics for a home deck instead.""")) self.connect(m.actionDonate, s, self.onDonate) self.connect(m.actionFullSync, s, self.onFullSync) self.connect(m.actionStudyDeck, s, self.onStudyDeck) + self.connect(m.actionCreateFiltered, s, self.onCram) self.connect(m.actionEmptyCards, s, self.onEmptyCards) def updateTitleBar(self): diff --git a/aqt/overview.py b/aqt/overview.py index 7b4d8dbed..2189ca9ab 100644 --- a/aqt/overview.py +++ b/aqt/overview.py @@ -55,8 +55,8 @@ class Overview(object): self.mw.moveToState("deckBrowser") elif url == "review": openLink(aqt.appShared+"info/%s?v=%s"%(self.sid, self.sidVer)) - elif url == "limits": - self.onLimits() + elif url == "studymore": + self.onStudyMore() else: openLink(url) @@ -65,17 +65,14 @@ class Overview(object): key = unicode(evt.text()) if key == "o": self.mw.onDeckConf() - if key == "f" and not cram: - deck = self.mw.col.decks.current() - self.mw.onCram("'deck:%s'" % deck['name']) if key == "r" and cram: self.mw.col.sched.rebuildDyn() self.mw.reset() if key == "e" and cram: self.mw.col.sched.emptyDyn(self.mw.col.decks.selected()) self.mw.reset() - if key == "l": - self.onLimits() + if key == "c" and not cram: + self.onStudyMore() # HTML ############################################################ @@ -98,10 +95,14 @@ class Overview(object): def _desc(self, deck): if deck['dyn']: - search, limit, order = deck['terms'][0] - desc = "%s
%s" % ( - _("Search: %s") % search, - _("Order: %s") % dynOrderLabels()[order].lower()) + desc = _("""\ +This is a special deck for studying outside of the normal schedule.""") + desc += " " + _("""\ +Cards will be automatically returned to their original decks after you review \ +them.""") + desc += " " + _("""\ +Deleting this deck from the deck list will return all remaining cards \ +to their original deck.""") else: desc = deck.get("desc", "") if not desc: @@ -190,11 +191,8 @@ text-align: center; links.append(["R", "refresh", _("Rebuild")]) links.append(["E", "empty", _("Empty")]) else: - if not sum(self.mw.col.sched.counts()): - if self.mw.col.sched.newDue() or \ - self.mw.col.sched.revDue(): - links.append(["L", "limits", _("Study More")]) - links.append(["F", "cram", _("Filter/Cram")]) + links.append(["C", "studymore", _("Custom Study")]) + #links.append(["F", "cram", _("Filter/Cram")]) buf = "" for b in links: if b[0]: @@ -209,22 +207,9 @@ text-align: center; self.bottom.web.setFixedHeight(size) self.bottom.web.setLinkHandler(self._linkHandler) - # Today's limits + # Studying more ###################################################################### - def onLimits(self): - d = QDialog(self.mw) - frm = aqt.forms.limits.Ui_Dialog() - frm.setupUi(d) - deck = self.mw.col.decks.current() - frm.newToday.setValue(deck.get('extendNew', 10)) - frm.revToday.setValue(deck.get('extendRev', 50)) - def accept(): - n = deck['extendNew'] = frm.newToday.value() - r = deck['extendRev'] = frm.revToday.value() - self.mw.col.decks.save(deck) - self.mw.col.sched.extendLimits(n, r) - self.mw.reset() - d.connect(frm.buttonBox, SIGNAL("accepted()"), accept) - d.exec_() - + def onStudyMore(self): + import aqt.customstudy + aqt.customstudy.CustomStudy(self.mw) diff --git a/aqt/taglimit.py b/aqt/taglimit.py new file mode 100644 index 000000000..6ade8bee8 --- /dev/null +++ b/aqt/taglimit.py @@ -0,0 +1,97 @@ +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +import aqt +from aqt.qt import * +from aqt.utils import saveGeom, restoreGeom + +class TagLimit(QDialog): + + def __init__(self, mw, parent): + QDialog.__init__(self, parent, Qt.Window) + self.mw = mw + self.parent = parent + self.deck = self.parent.deck + self.dialog = aqt.forms.taglimit.Ui_Dialog() + self.dialog.setupUi(self) + self.rebuildTagList() + restoreGeom(self, "tagLimit") + self.exec_() + + def rebuildTagList(self): + usertags = self.mw.col.tags.all() + yes = self.deck.get("activeTags", []) + no = self.deck.get("inactiveTags", []) + yesHash = {} + noHash = {} + for y in yes: + yesHash[y] = True + for n in no: + noHash[n] = True + groupedTags = [] + usertags.sort() + icon = QIcon(":/icons/Anki_Fact.png") + groupedTags.append([icon, usertags]) + self.tags = [] + for (icon, tags) in groupedTags: + for t in tags: + self.tags.append(t) + item = QListWidgetItem(icon, t.replace("_", " ")) + self.dialog.activeList.addItem(item) + if t in yesHash: + mode = QItemSelectionModel.Select + self.dialog.activeCheck.setChecked(True) + else: + mode = QItemSelectionModel.Deselect + idx = self.dialog.activeList.indexFromItem(item) + self.dialog.activeList.selectionModel().select(idx, mode) + # inactive + item = QListWidgetItem(icon, t.replace("_", " ")) + self.dialog.inactiveList.addItem(item) + if t in noHash: + mode = QItemSelectionModel.Select + else: + mode = QItemSelectionModel.Deselect + idx = self.dialog.inactiveList.indexFromItem(item) + self.dialog.inactiveList.selectionModel().select(idx, mode) + + def reject(self): + self.tags = "" + QDialog.reject(self) + + def accept(self): + self.hide() + n = 0 + # gather yes/no tags + yes = [] + no = [] + for c in range(self.dialog.activeList.count()): + # active + if self.dialog.activeCheck.isChecked(): + item = self.dialog.activeList.item(c) + idx = self.dialog.activeList.indexFromItem(item) + if self.dialog.activeList.selectionModel().isSelected(idx): + yes.append(self.tags[c]) + # inactive + item = self.dialog.inactiveList.item(c) + idx = self.dialog.inactiveList.indexFromItem(item) + if self.dialog.inactiveList.selectionModel().isSelected(idx): + no.append(self.tags[c]) + # save in the deck for future invocations + self.deck['activeTags'] = yes + self.deck['inactiveTags'] = no + self.mw.col.decks.save(self.deck) + # build query string + self.tags = "" + if yes: + arr = [] + for req in yes: + arr.append("tag:'%s'" % req) + self.tags += "(" + " or ".join(arr) + ")" + if no: + arr = [] + for req in no: + arr.append("-tag:'%s'" % req) + self.tags += " " + " ".join(arr) + saveGeom(self, "tagLimit") + QDialog.accept(self) diff --git a/designer/customstudy.ui b/designer/customstudy.ui new file mode 100644 index 000000000..6c08ffde3 --- /dev/null +++ b/designer/customstudy.ui @@ -0,0 +1,191 @@ + + + Dialog + + + + 0 + 0 + 290 + 338 + + + + Custom Study + + + + + + + + Preview new cards + + + + + + + Study a random selection of cards + + + + + + + Increase today's review card limit + + + + + + + Increase today's new card limit + + + + + + + Review forgotten cards + + + + + + + Review ahead + + + + + + + Limit to particular tags + + + + + + + + + + + + + + + ... + + + + + + + + + ... + + + + + + + + + + ... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + radio1 + radio2 + radio3 + radio4 + radio5 + radio6 + radio7 + spin + buttonBox + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/designer/dyndconf.ui b/designer/dyndconf.ui index a910bb37a..41d9bf25e 100644 --- a/designer/dyndconf.ui +++ b/designer/dyndconf.ui @@ -20,21 +20,21 @@ Filter - + Limit to - + Search - + @@ -50,29 +50,19 @@ - + cards selected by - + - + - - - - - - - Preset - - - diff --git a/designer/limits.ui b/designer/limits.ui deleted file mode 100644 index 211a39c6c..000000000 --- a/designer/limits.ui +++ /dev/null @@ -1,151 +0,0 @@ - - - Dialog - - - - 0 - 0 - 428 - 183 - - - - Extend Limits - - - - - - - - 1000 - - - - - - - 1000 - - - - - - - + - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Today's review limit - - - - - - - + - - - - - - - Today's new card limit - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - To review cards before they are due, please use filter/cram. - - - true - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - newToday - revToday - buttonBox - - - - - buttonBox - accepted() - Dialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - Dialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/designer/main.ui b/designer/main.ui index cc2ce83ba..22bb1b21a 100644 --- a/designer/main.ui +++ b/designer/main.ui @@ -104,6 +104,7 @@ + @@ -190,7 +191,7 @@ Browse && Install... - + @@ -248,6 +249,14 @@ Empty Cards... + + + Create Filtered Deck... + + + F + + diff --git a/designer/taglimit.ui b/designer/taglimit.ui new file mode 100644 index 000000000..43e23dc3f --- /dev/null +++ b/designer/taglimit.ui @@ -0,0 +1,126 @@ + + + Dialog + + + + 0 + 0 + 361 + 394 + + + + Selective Study + + + + + + Require one or more of these tags: + + + + + + + false + + + + 0 + 2 + + + + QAbstractItemView::MultiSelection + + + + + + + Select tags to exclude: + + + + + + + true + + + + 0 + 2 + + + + QAbstractItemView::MultiSelection + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 358 + 264 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + activeCheck + toggled(bool) + activeList + setEnabled(bool) + + + 133 + 18 + + + 133 + 85 + + + + +