2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
2020-04-20 05:15:23 +02:00
|
|
|
|
2013-10-18 03:38:29 +02:00
|
|
|
from operator import itemgetter
|
2020-07-24 04:32:57 +02:00
|
|
|
from typing import Any, Dict
|
|
|
|
|
|
|
|
from PyQt5.QtWidgets import QLineEdit
|
2020-04-05 05:08:15 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt
|
2019-12-20 10:19:03 +01:00
|
|
|
from anki.consts import NEW_CARDS_RANDOM
|
2019-03-04 02:58:34 +01:00
|
|
|
from anki.lang import _, ngettext
|
2020-02-24 13:42:30 +01:00
|
|
|
from aqt import gui_hooks
|
2019-12-20 10:19:03 +01:00
|
|
|
from aqt.qt import *
|
2019-12-23 01:34:10 +01:00
|
|
|
from aqt.utils import (
|
|
|
|
askUser,
|
|
|
|
getOnlyText,
|
|
|
|
openHelp,
|
|
|
|
restoreGeom,
|
|
|
|
saveGeom,
|
|
|
|
showInfo,
|
|
|
|
showWarning,
|
|
|
|
tooltip,
|
|
|
|
)
|
2019-12-20 10:19:03 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
class DeckConf(QDialog):
|
2020-04-20 05:15:23 +02:00
|
|
|
def __init__(self, mw: aqt.AnkiQt, deck: Dict):
|
2012-12-21 08:51:59 +01:00
|
|
|
QDialog.__init__(self, mw)
|
|
|
|
self.mw = mw
|
|
|
|
self.deck = deck
|
2019-12-23 01:34:10 +01:00
|
|
|
self.childDids = [d[1] for d in self.mw.col.decks.children(self.deck["id"])]
|
2013-05-18 11:17:58 +02:00
|
|
|
self._origNewOrder = None
|
2012-12-21 08:51:59 +01:00
|
|
|
self.form = aqt.forms.dconf.Ui_Dialog()
|
|
|
|
self.form.setupUi(self)
|
2020-02-24 15:29:23 +01:00
|
|
|
gui_hooks.deck_conf_did_setup_ui_form(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.checkpoint(_("Options"))
|
|
|
|
self.setupCombos()
|
|
|
|
self.setupConfs()
|
|
|
|
self.setWindowModality(Qt.WindowModal)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self.form.buttonBox.helpRequested, lambda: openHelp("deckoptions"))
|
|
|
|
qconnect(self.form.confOpts.clicked, self.confOpts)
|
|
|
|
qconnect(
|
|
|
|
self.form.buttonBox.button(QDialogButtonBox.RestoreDefaults).clicked,
|
|
|
|
self.onRestore,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
|
|
|
self.setWindowTitle(_("Options for %s") % self.deck["name"])
|
2013-05-23 04:09:54 +02:00
|
|
|
# qt doesn't size properly with altered fonts otherwise
|
2014-06-18 20:47:45 +02:00
|
|
|
restoreGeom(self, "deckconf", adjustSize=True)
|
2020-02-24 13:42:30 +01:00
|
|
|
gui_hooks.deck_conf_will_show(self)
|
2013-05-23 04:09:54 +02:00
|
|
|
self.show()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.exec_()
|
2014-06-18 20:47:45 +02:00
|
|
|
saveGeom(self, "deckconf")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def setupCombos(self):
|
|
|
|
import anki.consts as cs
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
f = self.form
|
2016-05-12 06:45:35 +02:00
|
|
|
f.newOrder.addItems(list(cs.newCardOrderLabels().values()))
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(f.newOrder.currentIndexChanged, self.onNewOrderChanged)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Conf list
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def setupConfs(self):
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self.form.dconf.currentIndexChanged, self.onConfChange)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.conf = None
|
|
|
|
self.loadConfs()
|
|
|
|
|
|
|
|
def loadConfs(self):
|
2019-12-23 01:34:10 +01:00
|
|
|
current = self.deck["conf"]
|
2012-12-21 08:51:59 +01:00
|
|
|
self.confList = self.mw.col.decks.allConf()
|
2019-12-23 01:34:10 +01:00
|
|
|
self.confList.sort(key=itemgetter("name"))
|
2012-12-21 08:51:59 +01:00
|
|
|
startOn = 0
|
|
|
|
self.ignoreConfChange = True
|
|
|
|
self.form.dconf.clear()
|
|
|
|
for idx, conf in enumerate(self.confList):
|
2019-12-23 01:34:10 +01:00
|
|
|
self.form.dconf.addItem(conf["name"])
|
|
|
|
if str(conf["id"]) == str(current):
|
2012-12-21 08:51:59 +01:00
|
|
|
startOn = idx
|
|
|
|
self.ignoreConfChange = False
|
|
|
|
self.form.dconf.setCurrentIndex(startOn)
|
2013-05-18 11:17:58 +02:00
|
|
|
if self._origNewOrder is None:
|
2019-12-23 01:34:10 +01:00
|
|
|
self._origNewOrder = self.confList[startOn]["new"]["order"]
|
2012-12-21 08:51:59 +01:00
|
|
|
self.onConfChange(startOn)
|
|
|
|
|
|
|
|
def confOpts(self):
|
|
|
|
m = QMenu(self.mw)
|
|
|
|
a = m.addAction(_("Add"))
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(a.triggered, self.addGroup)
|
2012-12-21 08:51:59 +01:00
|
|
|
a = m.addAction(_("Delete"))
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(a.triggered, self.remGroup)
|
2012-12-21 08:51:59 +01:00
|
|
|
a = m.addAction(_("Rename"))
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(a.triggered, self.renameGroup)
|
2012-12-21 08:51:59 +01:00
|
|
|
a = m.addAction(_("Set for all subdecks"))
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(a.triggered, self.setChildren)
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.childDids:
|
|
|
|
a.setEnabled(False)
|
|
|
|
m.exec_(QCursor.pos())
|
|
|
|
|
|
|
|
def onConfChange(self, idx):
|
|
|
|
if self.ignoreConfChange:
|
|
|
|
return
|
|
|
|
if self.conf:
|
|
|
|
self.saveConf()
|
|
|
|
conf = self.confList[idx]
|
2019-12-23 01:34:10 +01:00
|
|
|
self.deck["conf"] = conf["id"]
|
2020-05-17 06:37:59 +02:00
|
|
|
self.mw.col.decks.save(self.deck)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.loadConf()
|
2020-05-17 06:37:59 +02:00
|
|
|
cnt = len(self.mw.col.decks.didsForConf(conf))
|
2013-05-16 09:50:48 +02:00
|
|
|
if cnt > 1:
|
2019-12-23 01:34:10 +01:00
|
|
|
txt = _(
|
|
|
|
"Your changes will affect multiple decks. If you wish to "
|
|
|
|
"change only the current deck, please add a new options group first."
|
|
|
|
)
|
2013-05-16 09:50:48 +02:00
|
|
|
else:
|
|
|
|
txt = ""
|
|
|
|
self.form.count.setText(txt)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-04-20 05:15:23 +02:00
|
|
|
def addGroup(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
name = getOnlyText(_("New options group name:"))
|
|
|
|
if not name:
|
|
|
|
return
|
2020-05-22 21:24:05 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# first, save currently entered data to current conf
|
|
|
|
self.saveConf()
|
|
|
|
# then clone the conf
|
2020-04-20 05:15:23 +02:00
|
|
|
id = self.mw.col.decks.add_config_returning_id(name, clone_from=self.conf)
|
2020-05-22 21:24:05 +02:00
|
|
|
gui_hooks.deck_conf_did_add_config(self, self.deck, self.conf, name, id)
|
2012-12-21 08:51:59 +01:00
|
|
|
# set the deck to the new conf
|
2019-12-23 01:34:10 +01:00
|
|
|
self.deck["conf"] = id
|
2012-12-21 08:51:59 +01:00
|
|
|
# then reload the conf list
|
|
|
|
self.loadConfs()
|
|
|
|
|
2020-04-20 05:23:05 +02:00
|
|
|
def remGroup(self) -> None:
|
2019-12-23 01:34:10 +01:00
|
|
|
if int(self.conf["id"]) == 1:
|
2012-12-21 08:51:59 +01:00
|
|
|
showInfo(_("The default configuration can't be removed."), self)
|
|
|
|
else:
|
2020-05-22 21:24:05 +02:00
|
|
|
gui_hooks.deck_conf_will_remove_config(self, self.deck, self.conf)
|
2020-04-20 05:23:05 +02:00
|
|
|
self.mw.col.modSchema(check=True)
|
2020-04-03 05:54:52 +02:00
|
|
|
self.mw.col.decks.remove_config(self.conf["id"])
|
2020-04-30 00:10:29 +02:00
|
|
|
self.conf = None
|
2019-12-23 01:34:10 +01:00
|
|
|
self.deck["conf"] = 1
|
2012-12-21 08:51:59 +01:00
|
|
|
self.loadConfs()
|
|
|
|
|
2020-04-20 05:23:05 +02:00
|
|
|
def renameGroup(self) -> None:
|
2019-12-23 01:34:10 +01:00
|
|
|
old = self.conf["name"]
|
2012-12-21 08:51:59 +01:00
|
|
|
name = getOnlyText(_("New name:"), default=old)
|
|
|
|
if not name or name == old:
|
|
|
|
return
|
2020-05-22 21:24:05 +02:00
|
|
|
|
|
|
|
gui_hooks.deck_conf_will_rename_config(self, self.deck, self.conf, name)
|
2019-12-23 01:34:10 +01:00
|
|
|
self.conf["name"] = name
|
2020-04-20 05:17:55 +02:00
|
|
|
self.saveConf()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.loadConfs()
|
|
|
|
|
|
|
|
def setChildren(self):
|
|
|
|
if not askUser(
|
2019-12-23 01:34:10 +01:00
|
|
|
_("Set all decks below %s to this option group?") % self.deck["name"]
|
|
|
|
):
|
2012-12-21 08:51:59 +01:00
|
|
|
return
|
|
|
|
for did in self.childDids:
|
|
|
|
deck = self.mw.col.decks.get(did)
|
2019-12-23 01:34:10 +01:00
|
|
|
if deck["dyn"]:
|
2013-01-14 21:39:44 +01:00
|
|
|
continue
|
2019-12-23 01:34:10 +01:00
|
|
|
deck["conf"] = self.deck["conf"]
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.col.decks.save(deck)
|
2019-12-23 01:34:10 +01:00
|
|
|
tooltip(
|
|
|
|
ngettext("%d deck updated.", "%d decks updated.", len(self.childDids))
|
|
|
|
% len(self.childDids)
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Loading
|
|
|
|
##################################################
|
|
|
|
|
|
|
|
def listToUser(self, l):
|
2020-04-05 05:08:15 +02:00
|
|
|
def num_to_user(n: Union[int, float]):
|
|
|
|
if n == round(n):
|
|
|
|
return str(int(n))
|
|
|
|
else:
|
|
|
|
return str(n)
|
|
|
|
|
|
|
|
return " ".join(map(num_to_user, l))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def parentLimText(self, type="new"):
|
|
|
|
# top level?
|
2019-12-23 01:34:10 +01:00
|
|
|
if "::" not in self.deck["name"]:
|
2012-12-21 08:51:59 +01:00
|
|
|
return ""
|
|
|
|
lim = -1
|
2019-12-23 01:34:10 +01:00
|
|
|
for d in self.mw.col.decks.parents(self.deck["id"]):
|
|
|
|
c = self.mw.col.decks.confForDid(d["id"])
|
|
|
|
x = c[type]["perDay"]
|
2012-12-21 08:51:59 +01:00
|
|
|
if lim == -1:
|
|
|
|
lim = x
|
|
|
|
else:
|
|
|
|
lim = min(x, lim)
|
|
|
|
return _("(parent limit: %d)") % lim
|
|
|
|
|
|
|
|
def loadConf(self):
|
2019-12-23 01:34:10 +01:00
|
|
|
self.conf = self.mw.col.decks.confForDid(self.deck["id"])
|
2012-12-21 08:51:59 +01:00
|
|
|
# new
|
2019-12-23 01:34:10 +01:00
|
|
|
c = self.conf["new"]
|
2012-12-21 08:51:59 +01:00
|
|
|
f = self.form
|
2019-12-23 01:34:10 +01:00
|
|
|
f.lrnSteps.setText(self.listToUser(c["delays"]))
|
|
|
|
f.lrnGradInt.setValue(c["ints"][0])
|
|
|
|
f.lrnEasyInt.setValue(c["ints"][1])
|
|
|
|
f.lrnFactor.setValue(c["initialFactor"] / 10.0)
|
|
|
|
f.newOrder.setCurrentIndex(c["order"])
|
|
|
|
f.newPerDay.setValue(c["perDay"])
|
rework sibling handling and change bury semantics
First, burying changes:
- unburying now happens on day rollover, or when manually unburying from
overview screen
- burying is not performed when returning to deck list, or when closing
collection, so burying now must mark cards as modified to ensure sync
consistent
- because they're no longer temporary to a session, make sure we exclude them
in filtered decks in -is:suspended
Sibling spacing changes:
- core behaviour now based on automatically burying related cards when we
answer a card
- applies to reviews, optionally to new cards, and never to cards in the
learning queue (partly because we can't suspend/bury cards in that queue at
the moment)
- this means spacing works consistently in filtered decks now, works on
reviews even when user is late to review, and provides better separation of
new cards
- if burying new cards disabled, we just discard them from the current queue.
an option to set due=ord*space+due would be nicer, but would require
changing a lot of code and is more appropriate for a future major version
change. discarding from queue suffers from the same issue as the new card
cycling in that queue rebuilds may cause cards to be shown close together,
so the default burying behaviour is preferable
- refer to them as 'related cards' rather than 'siblings'
These changes don't require any changes to the database format, so they
should hopefully coexist with older clients without issue.
2013-08-10 08:54:33 +02:00
|
|
|
f.bury.setChecked(c.get("bury", True))
|
2019-12-23 01:34:10 +01:00
|
|
|
f.newplim.setText(self.parentLimText("new"))
|
2012-12-21 08:51:59 +01:00
|
|
|
# rev
|
2019-12-23 01:34:10 +01:00
|
|
|
c = self.conf["rev"]
|
|
|
|
f.revPerDay.setValue(c["perDay"])
|
|
|
|
f.easyBonus.setValue(c["ease4"] * 100)
|
|
|
|
f.fi1.setValue(c["ivlFct"] * 100)
|
|
|
|
f.maxIvl.setValue(c["maxIvl"])
|
|
|
|
f.revplim.setText(self.parentLimText("rev"))
|
2013-10-18 03:38:29 +02:00
|
|
|
f.buryRev.setChecked(c.get("bury", True))
|
2019-12-23 01:34:10 +01:00
|
|
|
f.hardFactor.setValue(int(c.get("hardFactor", 1.2) * 100))
|
2018-07-11 13:23:22 +02:00
|
|
|
if self.mw.col.schedVer() == 1:
|
|
|
|
f.hardFactor.setVisible(False)
|
|
|
|
f.hardFactorLabel.setVisible(False)
|
2012-12-21 08:51:59 +01:00
|
|
|
# lapse
|
2019-12-23 01:34:10 +01:00
|
|
|
c = self.conf["lapse"]
|
|
|
|
f.lapSteps.setText(self.listToUser(c["delays"]))
|
|
|
|
f.lapMult.setValue(c["mult"] * 100)
|
|
|
|
f.lapMinInt.setValue(c["minInt"])
|
|
|
|
f.leechThreshold.setValue(c["leechFails"])
|
|
|
|
f.leechAction.setCurrentIndex(c["leechAction"])
|
2012-12-21 08:51:59 +01:00
|
|
|
# general
|
|
|
|
c = self.conf
|
2019-12-23 01:34:10 +01:00
|
|
|
f.maxTaken.setValue(c["maxTaken"])
|
|
|
|
f.showTimer.setChecked(c.get("timer", 0))
|
|
|
|
f.autoplaySounds.setChecked(c["autoplay"])
|
|
|
|
f.replayQuestion.setChecked(c.get("replayq", True))
|
2012-12-21 08:51:59 +01:00
|
|
|
# description
|
2019-12-23 01:34:10 +01:00
|
|
|
f.desc.setPlainText(self.deck["desc"])
|
2020-02-24 15:47:48 +01:00
|
|
|
gui_hooks.deck_conf_did_load_config(self, self.deck, self.conf)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def onRestore(self):
|
|
|
|
self.mw.progress.start()
|
|
|
|
self.mw.col.decks.restoreToDefault(self.conf)
|
|
|
|
self.mw.progress.finish()
|
|
|
|
self.loadConf()
|
|
|
|
|
|
|
|
# New order
|
|
|
|
##################################################
|
|
|
|
|
|
|
|
def onNewOrderChanged(self, new):
|
2019-12-23 01:34:10 +01:00
|
|
|
old = self.conf["new"]["order"]
|
2012-12-21 08:51:59 +01:00
|
|
|
if old == new:
|
|
|
|
return
|
2019-12-23 01:34:10 +01:00
|
|
|
self.conf["new"]["order"] = new
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.progress.start()
|
|
|
|
self.mw.col.sched.resortConf(self.conf)
|
|
|
|
self.mw.progress.finish()
|
|
|
|
|
|
|
|
# Saving
|
|
|
|
##################################################
|
|
|
|
|
2020-07-24 04:32:57 +02:00
|
|
|
def updateList(self, conf: Any, key: str, w: QLineEdit, minSize: int = 1) -> None:
|
2016-05-12 06:45:35 +02:00
|
|
|
items = str(w.text()).split(" ")
|
2012-12-21 08:51:59 +01:00
|
|
|
ret = []
|
2020-07-24 04:32:57 +02:00
|
|
|
for item in items:
|
|
|
|
if not item:
|
2012-12-21 08:51:59 +01:00
|
|
|
continue
|
|
|
|
try:
|
2020-07-24 04:32:57 +02:00
|
|
|
i = float(item)
|
2012-12-21 08:51:59 +01:00
|
|
|
assert i > 0
|
|
|
|
if i == int(i):
|
|
|
|
i = int(i)
|
|
|
|
ret.append(i)
|
|
|
|
except:
|
|
|
|
# invalid, don't update
|
|
|
|
showWarning(_("Steps must be numbers."))
|
|
|
|
return
|
|
|
|
if len(ret) < minSize:
|
|
|
|
showWarning(_("At least one step is required."))
|
|
|
|
return
|
|
|
|
conf[key] = ret
|
|
|
|
|
|
|
|
def saveConf(self):
|
|
|
|
# new
|
2019-12-23 01:34:10 +01:00
|
|
|
c = self.conf["new"]
|
2012-12-21 08:51:59 +01:00
|
|
|
f = self.form
|
2019-12-23 01:34:10 +01:00
|
|
|
self.updateList(c, "delays", f.lrnSteps)
|
|
|
|
c["ints"][0] = f.lrnGradInt.value()
|
|
|
|
c["ints"][1] = f.lrnEasyInt.value()
|
|
|
|
c["initialFactor"] = f.lrnFactor.value() * 10
|
|
|
|
c["order"] = f.newOrder.currentIndex()
|
|
|
|
c["perDay"] = f.newPerDay.value()
|
|
|
|
c["bury"] = f.bury.isChecked()
|
|
|
|
if self._origNewOrder != c["order"]:
|
2013-05-18 11:17:58 +02:00
|
|
|
# order of current deck has changed, so have to resort
|
2019-12-23 01:34:10 +01:00
|
|
|
if c["order"] == NEW_CARDS_RANDOM:
|
|
|
|
self.mw.col.sched.randomizeCards(self.deck["id"])
|
2013-05-18 11:17:58 +02:00
|
|
|
else:
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw.col.sched.orderCards(self.deck["id"])
|
2012-12-21 08:51:59 +01:00
|
|
|
# rev
|
2019-12-23 01:34:10 +01:00
|
|
|
c = self.conf["rev"]
|
|
|
|
c["perDay"] = f.revPerDay.value()
|
|
|
|
c["ease4"] = f.easyBonus.value() / 100.0
|
|
|
|
c["ivlFct"] = f.fi1.value() / 100.0
|
|
|
|
c["maxIvl"] = f.maxIvl.value()
|
|
|
|
c["bury"] = f.buryRev.isChecked()
|
|
|
|
c["hardFactor"] = f.hardFactor.value() / 100.0
|
2012-12-21 08:51:59 +01:00
|
|
|
# lapse
|
2019-12-23 01:34:10 +01:00
|
|
|
c = self.conf["lapse"]
|
|
|
|
self.updateList(c, "delays", f.lapSteps, minSize=0)
|
|
|
|
c["mult"] = f.lapMult.value() / 100.0
|
|
|
|
c["minInt"] = f.lapMinInt.value()
|
|
|
|
c["leechFails"] = f.leechThreshold.value()
|
|
|
|
c["leechAction"] = f.leechAction.currentIndex()
|
2012-12-21 08:51:59 +01:00
|
|
|
# general
|
|
|
|
c = self.conf
|
2019-12-23 01:34:10 +01:00
|
|
|
c["maxTaken"] = f.maxTaken.value()
|
|
|
|
c["timer"] = f.showTimer.isChecked() and 1 or 0
|
|
|
|
c["autoplay"] = f.autoplaySounds.isChecked()
|
|
|
|
c["replayq"] = f.replayQuestion.isChecked()
|
2012-12-21 08:51:59 +01:00
|
|
|
# description
|
2019-12-23 01:34:10 +01:00
|
|
|
self.deck["desc"] = f.desc.toPlainText()
|
2020-02-24 13:42:30 +01:00
|
|
|
gui_hooks.deck_conf_will_save_config(self, self.deck, self.conf)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.col.decks.save(self.deck)
|
|
|
|
self.mw.col.decks.save(self.conf)
|
|
|
|
|
|
|
|
def reject(self):
|
|
|
|
self.accept()
|
|
|
|
|
|
|
|
def accept(self):
|
|
|
|
self.saveConf()
|
|
|
|
self.mw.reset()
|
|
|
|
QDialog.accept(self)
|