131d37dca5
AD devs: before updating the sync version to 10, please make sure the new timezone code has been ported over. The core change to the scheduler is in _updateCutoff():9736e4a970/pylib/anki/schedv2.py (L1357)
with the following supporting Python functions:9736e4a970/pylib/anki/schedv2.py (L1410)
_timing_today() is calculated in Rust (lines up to 92, the rest are tests):9736e4a970/rslib/src/sched/cutoff.rs (L20)
The change went through a few iterations before stabilising, so it's probably easier to refer to the above code than the patches that got us to that point.
282 lines
9.9 KiB
Python
282 lines
9.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import datetime
|
|
import time
|
|
|
|
import anki.lang
|
|
import aqt
|
|
from anki.lang import _
|
|
from aqt import AnkiQt
|
|
from aqt.qt import *
|
|
from aqt.theme import theme_manager
|
|
from aqt.utils import TR, askUser, openHelp, showInfo, showWarning, tr
|
|
|
|
|
|
class Preferences(QDialog):
|
|
def __init__(self, mw: AnkiQt):
|
|
QDialog.__init__(self, mw, Qt.Window)
|
|
self.mw = mw
|
|
self.prof = self.mw.pm.profile
|
|
self.form = aqt.forms.preferences.Ui_Preferences()
|
|
self.form.setupUi(self)
|
|
self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False)
|
|
self.form.buttonBox.button(QDialogButtonBox.Close).setAutoDefault(False)
|
|
self.form.buttonBox.helpRequested.connect(lambda: openHelp("profileprefs"))
|
|
self.silentlyClose = True
|
|
self.setupLang()
|
|
self.setupCollection()
|
|
self.setupNetwork()
|
|
self.setupBackup()
|
|
self.setupOptions()
|
|
self.show()
|
|
|
|
def accept(self):
|
|
# avoid exception if main window is already closed
|
|
if not self.mw.col:
|
|
return
|
|
self.updateCollection()
|
|
self.updateNetwork()
|
|
self.updateBackup()
|
|
self.updateOptions()
|
|
self.mw.pm.save()
|
|
self.mw.reset()
|
|
self.done(0)
|
|
aqt.dialogs.markClosed("Preferences")
|
|
|
|
def reject(self):
|
|
self.accept()
|
|
|
|
# Language
|
|
######################################################################
|
|
|
|
def setupLang(self):
|
|
f = self.form
|
|
f.lang.addItems([x[0] for x in anki.lang.langs])
|
|
f.lang.setCurrentIndex(self.langIdx())
|
|
f.lang.currentIndexChanged.connect(self.onLangIdxChanged)
|
|
|
|
def langIdx(self):
|
|
codes = [x[1] for x in anki.lang.langs]
|
|
try:
|
|
return codes.index(anki.lang.currentLang)
|
|
except:
|
|
return codes.index("en_US")
|
|
|
|
def onLangIdxChanged(self, idx):
|
|
code = anki.lang.langs[idx][1]
|
|
self.mw.pm.setLang(code)
|
|
showInfo(_("Please restart Anki to complete language change."), parent=self)
|
|
|
|
# Collection options
|
|
######################################################################
|
|
|
|
def setupCollection(self):
|
|
import anki.consts as c
|
|
|
|
f = self.form
|
|
qc = self.mw.col.conf
|
|
self._setupDayCutoff()
|
|
if isMac:
|
|
f.hwAccel.setVisible(False)
|
|
else:
|
|
f.hwAccel.setChecked(self.mw.pm.glMode() != "software")
|
|
f.lrnCutoff.setValue(qc["collapseTime"] / 60.0)
|
|
f.timeLimit.setValue(qc["timeLim"] / 60.0)
|
|
f.showEstimates.setChecked(qc["estTimes"])
|
|
f.showProgress.setChecked(qc["dueCounts"])
|
|
f.newSpread.addItems(list(c.newCardSchedulingLabels().values()))
|
|
f.newSpread.setCurrentIndex(qc["newSpread"])
|
|
f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True)))
|
|
f.dayLearnFirst.setChecked(qc.get("dayLearnFirst", False))
|
|
if self.mw.col.schedVer() != 2:
|
|
f.dayLearnFirst.setVisible(False)
|
|
f.new_timezone.setVisible(False)
|
|
else:
|
|
f.newSched.setChecked(True)
|
|
f.new_timezone.setChecked(self.mw.col.sched._new_timezone_enabled())
|
|
|
|
def updateCollection(self):
|
|
f = self.form
|
|
d = self.mw.col
|
|
|
|
if not isMac:
|
|
wasAccel = self.mw.pm.glMode() != "software"
|
|
wantAccel = f.hwAccel.isChecked()
|
|
if wasAccel != wantAccel:
|
|
if wantAccel:
|
|
self.mw.pm.setGlMode("auto")
|
|
else:
|
|
self.mw.pm.setGlMode("software")
|
|
showInfo(_("Changes will take effect when you restart Anki."))
|
|
|
|
qc = d.conf
|
|
qc["dueCounts"] = f.showProgress.isChecked()
|
|
qc["estTimes"] = f.showEstimates.isChecked()
|
|
qc["newSpread"] = f.newSpread.currentIndex()
|
|
qc["timeLim"] = f.timeLimit.value() * 60
|
|
qc["collapseTime"] = f.lrnCutoff.value() * 60
|
|
qc["addToCur"] = not f.useCurrent.currentIndex()
|
|
qc["dayLearnFirst"] = f.dayLearnFirst.isChecked()
|
|
self._updateDayCutoff()
|
|
if self.mw.col.schedVer() != 1:
|
|
was_enabled = self.mw.col.sched._new_timezone_enabled()
|
|
is_enabled = f.new_timezone.isChecked()
|
|
if was_enabled != is_enabled:
|
|
if is_enabled:
|
|
self.mw.col.sched.set_creation_offset()
|
|
else:
|
|
self.mw.col.sched.clear_creation_offset()
|
|
self._updateSchedVer(f.newSched.isChecked())
|
|
d.setMod()
|
|
|
|
# Scheduler version
|
|
######################################################################
|
|
|
|
def _updateSchedVer(self, wantNew):
|
|
haveNew = self.mw.col.schedVer() == 2
|
|
|
|
# nothing to do?
|
|
if haveNew == wantNew:
|
|
return
|
|
|
|
if not askUser(
|
|
_(
|
|
"This will reset any cards in learning, clear filtered decks, and change the scheduler version. Proceed?"
|
|
)
|
|
):
|
|
return
|
|
|
|
if wantNew:
|
|
self.mw.col.changeSchedulerVer(2)
|
|
else:
|
|
self.mw.col.changeSchedulerVer(1)
|
|
|
|
# Day cutoff
|
|
######################################################################
|
|
|
|
def _setupDayCutoff(self):
|
|
if self.mw.col.schedVer() == 2:
|
|
self._setupDayCutoffV2()
|
|
else:
|
|
self._setupDayCutoffV1()
|
|
|
|
def _setupDayCutoffV1(self):
|
|
self.startDate = datetime.datetime.fromtimestamp(self.mw.col.crt)
|
|
self.form.dayOffset.setValue(self.startDate.hour)
|
|
|
|
def _setupDayCutoffV2(self):
|
|
self.form.dayOffset.setValue(self.mw.col.conf.get("rollover", 4))
|
|
|
|
def _updateDayCutoff(self):
|
|
if self.mw.col.schedVer() == 2:
|
|
self._updateDayCutoffV2()
|
|
else:
|
|
self._updateDayCutoffV1()
|
|
|
|
def _updateDayCutoffV1(self):
|
|
hrs = self.form.dayOffset.value()
|
|
old = self.startDate
|
|
date = datetime.datetime(old.year, old.month, old.day, hrs)
|
|
self.mw.col.crt = int(time.mktime(date.timetuple()))
|
|
|
|
def _updateDayCutoffV2(self):
|
|
self.mw.col.conf["rollover"] = self.form.dayOffset.value()
|
|
|
|
# Network
|
|
######################################################################
|
|
|
|
def setupNetwork(self):
|
|
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
|
|
self.form.media_log.clicked.connect(self.on_media_log)
|
|
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
|
|
self.form.syncMedia.setChecked(self.prof["syncMedia"])
|
|
if not self.prof["syncKey"]:
|
|
self._hideAuth()
|
|
else:
|
|
self.form.syncUser.setText(self.prof.get("syncUser", ""))
|
|
self.form.syncDeauth.clicked.connect(self.onSyncDeauth)
|
|
|
|
def on_media_log(self):
|
|
self.mw.media_syncer.show_sync_log()
|
|
|
|
def _hideAuth(self):
|
|
self.form.syncDeauth.setVisible(False)
|
|
self.form.syncUser.setText("")
|
|
self.form.syncLabel.setText(
|
|
_(
|
|
"""\
|
|
<b>Synchronization</b><br>
|
|
Not currently enabled; click the sync button in the main window to enable."""
|
|
)
|
|
)
|
|
|
|
def onSyncDeauth(self) -> None:
|
|
if self.mw.media_syncer.is_syncing():
|
|
showWarning("Can't log out while sync in progress.")
|
|
return
|
|
self.prof["syncKey"] = None
|
|
self.mw.col.media.force_resync()
|
|
self._hideAuth()
|
|
|
|
def updateNetwork(self):
|
|
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
|
|
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
|
|
if self.form.fullSync.isChecked():
|
|
self.mw.col.modSchema(check=False)
|
|
self.mw.col.setMod()
|
|
|
|
# Backup
|
|
######################################################################
|
|
|
|
def setupBackup(self):
|
|
self.form.numBackups.setValue(self.prof["numBackups"])
|
|
|
|
def updateBackup(self):
|
|
self.prof["numBackups"] = self.form.numBackups.value()
|
|
|
|
# Basic & Advanced Options
|
|
######################################################################
|
|
|
|
def setupOptions(self):
|
|
self.form.pastePNG.setChecked(self.prof.get("pastePNG", False))
|
|
self.form.uiScale.setValue(self.mw.pm.uiScale() * 100)
|
|
self.form.pasteInvert.setChecked(self.prof.get("pasteInvert", False))
|
|
self.form.showPlayButtons.setChecked(self.prof.get("showPlayButtons", True))
|
|
self.form.nightMode.setChecked(self.mw.pm.night_mode())
|
|
if theme_manager.macos_dark_mode():
|
|
self.form.nightMode.setChecked(True)
|
|
self.form.nightMode.setText(tr(TR.PREFERENCES_DARK_MODE_ACTIVE))
|
|
else:
|
|
self.form.nightMode.setChecked(self.mw.pm.night_mode())
|
|
self.form.interrupt_audio.setChecked(self.mw.pm.interrupt_audio())
|
|
|
|
def updateOptions(self):
|
|
restart_required = False
|
|
|
|
self.prof["pastePNG"] = self.form.pastePNG.isChecked()
|
|
self.prof["pasteInvert"] = self.form.pasteInvert.isChecked()
|
|
newScale = self.form.uiScale.value() / 100
|
|
if newScale != self.mw.pm.uiScale():
|
|
self.mw.pm.setUiScale(newScale)
|
|
restart_required = True
|
|
self.prof["showPlayButtons"] = self.form.showPlayButtons.isChecked()
|
|
|
|
if theme_manager.macos_dark_mode():
|
|
if not self.form.nightMode.isChecked():
|
|
# user is trying to turn it off, but it's forced
|
|
showInfo(
|
|
tr(TR.PREFERENCES_DARK_MODE_DISABLE), textFormat="rich",
|
|
)
|
|
openHelp("profileprefs")
|
|
else:
|
|
if self.mw.pm.night_mode() != self.form.nightMode.isChecked():
|
|
self.mw.pm.set_night_mode(not self.mw.pm.night_mode())
|
|
restart_required = True
|
|
|
|
self.mw.pm.set_interrupt_audio(self.form.interrupt_audio.isChecked())
|
|
|
|
if restart_required:
|
|
showInfo(_("Changes will take effect when you restart Anki."))
|