expand backend Preferences and make undoable

- moved 'default to current deck when adding' into prefs
- move some profile options into the collection config, so they're
undoable and will sync. There is (currently) no automatic migration
from the old profile settings, meaning users will need to set the
options again if they've customized them.
- tidy up preferences.py
- drop the deleteMedia option that was not exposed in the UI
This commit is contained in:
Damien Elmes 2021-03-10 18:20:37 +10:00
parent 24ad7c1f35
commit 6b1dd9ee19
11 changed files with 362 additions and 259 deletions

View File

@ -20,7 +20,7 @@ from bs4 import BeautifulSoup
import aqt import aqt
import aqt.sound import aqt.sound
from anki.cards import Card from anki.cards import Card
from anki.collection import SearchNode from anki.collection import Config, SearchNode
from anki.consts import MODEL_CLOZE from anki.consts import MODEL_CLOZE
from anki.hooks import runFilter from anki.hooks import runFilter
from anki.httpclient import HttpClient from anki.httpclient import HttpClient
@ -781,7 +781,7 @@ class Editor:
filter = f"{tr(TR.EDITING_MEDIA)} ({extension_filter})" filter = f"{tr(TR.EDITING_MEDIA)} ({extension_filter})"
def accept(file: str) -> None: def accept(file: str) -> None:
self.addMedia(file, canDelete=True) self.addMedia(file)
file = getFile( file = getFile(
parent=self.widget, parent=self.widget,
@ -793,24 +793,18 @@ class Editor:
self.parentWindow.activateWindow() self.parentWindow.activateWindow()
def addMedia(self, path: str, canDelete: bool = False) -> None: def addMedia(self, path: str, canDelete: bool = False) -> None:
"""canDelete is a legacy arg and is ignored."""
try: try:
html = self._addMedia(path, canDelete) html = self._addMedia(path)
except Exception as e: except Exception as e:
showWarning(str(e)) showWarning(str(e))
return return
self.web.eval(f"setFormat('inserthtml', {json.dumps(html)});") self.web.eval(f"setFormat('inserthtml', {json.dumps(html)});")
def _addMedia(self, path: str, canDelete: bool = False) -> str: def _addMedia(self, path: str, canDelete: bool = False) -> str:
"Add to media folder and return local img or sound tag." """Add to media folder and return local img or sound tag."""
# copy to media folder # copy to media folder
fname = self.mw.col.media.addFile(path) fname = self.mw.col.media.addFile(path)
# remove original?
if canDelete and self.mw.pm.profile["deleteMedia"]:
if os.path.abspath(fname) != os.path.abspath(path):
try:
os.unlink(path)
except:
pass
# return a local html link # return a local html link
return self.fnameToLink(fname) return self.fnameToLink(fname)
@ -1091,7 +1085,6 @@ class EditorWebView(AnkiWebView):
def __init__(self, parent: QWidget, editor: Editor) -> None: def __init__(self, parent: QWidget, editor: Editor) -> None:
AnkiWebView.__init__(self, title="editor") AnkiWebView.__init__(self, title="editor")
self.editor = editor self.editor = editor
self.strip = self.editor.mw.pm.profile["stripHTML"]
self.setAcceptDrops(True) self.setAcceptDrops(True)
self._markInternal = False self._markInternal = False
clip = self.editor.mw.app.clipboard() clip = self.editor.mw.app.clipboard()
@ -1110,10 +1103,12 @@ class EditorWebView(AnkiWebView):
self.triggerPageAction(QWebEnginePage.Copy) self.triggerPageAction(QWebEnginePage.Copy)
def _wantsExtendedPaste(self) -> bool: def _wantsExtendedPaste(self) -> bool:
extended = not (self.editor.mw.app.queryKeyboardModifiers() & Qt.ShiftModifier) strip_html = self.editor.mw.col.get_config_bool(
if self.editor.mw.pm.profile.get("pasteInvert", False): Config.Bool.PASTE_STRIPS_FORMATTING
extended = not extended )
return extended if self.editor.mw.app.queryKeyboardModifiers() & Qt.ShiftModifier:
strip_html = not strip_html
return strip_html
def _onPaste(self, mode: QClipboard.Mode) -> None: def _onPaste(self, mode: QClipboard.Mode) -> None:
extended = self._wantsExtendedPaste() extended = self._wantsExtendedPaste()
@ -1240,7 +1235,7 @@ class EditorWebView(AnkiWebView):
return None return None
im = QImage(mime.imageData()) im = QImage(mime.imageData())
uname = namedtmp("paste") uname = namedtmp("paste")
if self.editor.mw.pm.profile.get("pastePNG", False): if self.editor.mw.col.get_config_bool(Config.Bool.PASTE_IMAGES_AS_PNG):
ext = ".png" ext = ".png"
im.save(uname + ext, None, 50) im.save(uname + ext, None, 50)
else: else:

View File

@ -80,7 +80,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="pasteInvert"> <widget class="QCheckBox" name="paste_strips_formatting">
<property name="text"> <property name="text">
<string>PREFERENCES_PASTE_WITHOUT_SHIFT_KEY_STRIPS_FORMATTING</string> <string>PREFERENCES_PASTE_WITHOUT_SHIFT_KEY_STRIPS_FORMATTING</string>
</property> </property>
@ -589,7 +589,7 @@
<tabstop>showPlayButtons</tabstop> <tabstop>showPlayButtons</tabstop>
<tabstop>interrupt_audio</tabstop> <tabstop>interrupt_audio</tabstop>
<tabstop>pastePNG</tabstop> <tabstop>pastePNG</tabstop>
<tabstop>pasteInvert</tabstop> <tabstop>paste_strips_formatting</tabstop>
<tabstop>nightMode</tabstop> <tabstop>nightMode</tabstop>
<tabstop>useCurrent</tabstop> <tabstop>useCurrent</tabstop>
<tabstop>recording_driver</tabstop> <tabstop>recording_driver</tabstop>

View File

@ -27,7 +27,7 @@ import aqt.toolbar
import aqt.webview import aqt.webview
from anki import hooks from anki import hooks
from anki._backend import RustBackend as _RustBackend from anki._backend import RustBackend as _RustBackend
from anki.collection import BackendUndo, Checkpoint, Collection, ReviewUndo from anki.collection import BackendUndo, Checkpoint, Collection, Config, ReviewUndo
from anki.decks import Deck from anki.decks import Deck
from anki.hooks import runHook from anki.hooks import runHook
from anki.sound import AVTag, SoundOrVideoTag from anki.sound import AVTag, SoundOrVideoTag
@ -391,8 +391,6 @@ class AnkiQt(QMainWindow):
if not self.loadCollection(): if not self.loadCollection():
return return
self.pm.apply_profile_options()
# show main window # show main window
if self.pm.profile["mainWindowState"]: if self.pm.profile["mainWindowState"]:
restoreGeom(self, "mainWindow") restoreGeom(self, "mainWindow")
@ -467,10 +465,10 @@ class AnkiQt(QMainWindow):
def _add_play_buttons(self, text: str) -> str: def _add_play_buttons(self, text: str) -> str:
"Return card text with play buttons added, or stripped." "Return card text with play buttons added, or stripped."
if self.pm.profile.get("showPlayButtons", True): if self.col.get_config_bool(Config.Bool.HIDE_AUDIO_PLAY_BUTTONS):
return aqt.sound.av_refs_to_play_icons(text)
else:
return anki.sound.strip_av_refs(text) return anki.sound.strip_av_refs(text)
else:
return aqt.sound.av_refs_to_play_icons(text)
def prepare_card_text_for_display(self, text: str) -> str: def prepare_card_text_for_display(self, text: str) -> str:
text = self.col.media.escape_media_filenames(text) text = self.col.media.escape_media_filenames(text)
@ -508,6 +506,7 @@ class AnkiQt(QMainWindow):
try: try:
self.update_undo_actions() self.update_undo_actions()
gui_hooks.collection_did_load(self.col) gui_hooks.collection_did_load(self.col)
self.apply_collection_options()
self.moveToState("deckBrowser") self.moveToState("deckBrowser")
except Exception as e: except Exception as e:
# dump error to stderr so it gets picked up by errors.py # dump error to stderr so it gets picked up by errors.py
@ -572,6 +571,12 @@ class AnkiQt(QMainWindow):
self.col.reopen(after_full_sync=False) self.col.reopen(after_full_sync=False)
self.col.close_for_full_sync() self.col.close_for_full_sync()
def apply_collection_options(self) -> None:
"Setup audio after collection loaded."
aqt.sound.av_player.interrupt_current_audio = self.col.get_config_bool(
Config.Bool.INTERRUPT_AUDIO_WHEN_ANSWERING
)
# Backup and auto-optimize # Backup and auto-optimize
########################################################################## ##########################################################################

View File

@ -1,7 +1,9 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import anki.lang import anki.lang
import aqt import aqt
from anki.consts import newCardSchedulingLabels
from aqt import AnkiQt from aqt import AnkiQt
from aqt.profiles import RecordingDriver, VideoDriver from aqt.profiles import RecordingDriver, VideoDriver
from aqt.qt import * from aqt.qt import *
@ -16,21 +18,6 @@ from aqt.utils import (
) )
def video_driver_name_for_platform(driver: VideoDriver) -> str:
if driver == VideoDriver.ANGLE:
return tr(TR.PREFERENCES_VIDEO_DRIVER_ANGLE)
elif driver == VideoDriver.Software:
if isMac:
return tr(TR.PREFERENCES_VIDEO_DRIVER_SOFTWARE_MAC)
else:
return tr(TR.PREFERENCES_VIDEO_DRIVER_SOFTWARE_OTHER)
else:
if isMac:
return tr(TR.PREFERENCES_VIDEO_DRIVER_OPENGL_MAC)
else:
return tr(TR.PREFERENCES_VIDEO_DRIVER_OPENGL_OTHER)
class Preferences(QDialog): class Preferences(QDialog):
def __init__(self, mw: AnkiQt) -> None: def __init__(self, mw: AnkiQt) -> None:
QDialog.__init__(self, mw, Qt.Window) QDialog.__init__(self, mw, Qt.Window)
@ -45,22 +32,18 @@ class Preferences(QDialog):
self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.PREFERENCES) self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.PREFERENCES)
) )
self.silentlyClose = True self.silentlyClose = True
self.prefs = self.mw.col.get_preferences() self.setup_collection()
self.setupLang() self.setup_profile()
self.setupCollection() self.setup_global()
self.setupNetwork()
self.setupBackup()
self.setupOptions()
self.show() self.show()
def accept(self) -> None: def accept(self) -> None:
# avoid exception if main window is already closed # avoid exception if main window is already closed
if not self.mw.col: if not self.mw.col:
return return
self.updateCollection() self.update_collection()
self.updateNetwork() self.update_profile()
self.updateBackup() self.update_global()
self.updateOptions()
self.mw.pm.save() self.mw.pm.save()
self.mw.reset() self.mw.reset()
self.done(0) self.done(0)
@ -69,16 +52,214 @@ class Preferences(QDialog):
def reject(self) -> None: def reject(self) -> None:
self.accept() self.accept()
# Language # Preferences stored in the collection
###################################################################### ######################################################################
def setupLang(self) -> None: def setup_collection(self) -> None:
self.prefs = self.mw.col.get_preferences()
form = self.form
scheduling = self.prefs.scheduling
form.lrnCutoff.setValue(int(scheduling.learn_ahead_secs / 60.0))
form.newSpread.addItems(list(newCardSchedulingLabels(self.mw.col).values()))
form.newSpread.setCurrentIndex(scheduling.new_review_mix)
form.dayLearnFirst.setChecked(scheduling.day_learn_first)
form.dayOffset.setValue(scheduling.rollover)
if scheduling.scheduler_version < 2:
form.dayLearnFirst.setVisible(False)
form.legacy_timezone.setVisible(False)
else:
form.legacy_timezone.setChecked(not scheduling.new_timezone)
reviewing = self.prefs.reviewing
form.timeLimit.setValue(int(reviewing.time_limit_secs / 60.0))
form.showEstimates.setChecked(reviewing.show_intervals_on_buttons)
form.showProgress.setChecked(reviewing.show_remaining_due_counts)
form.showPlayButtons.setChecked(not reviewing.hide_audio_play_buttons)
form.interrupt_audio.setChecked(reviewing.interrupt_audio_when_answering)
editing = self.prefs.editing
form.useCurrent.setCurrentIndex(
0 if editing.adding_defaults_to_current_deck else 1
)
form.paste_strips_formatting.setChecked(editing.paste_strips_formatting)
form.pastePNG.setChecked(editing.paste_images_as_png)
def update_collection(self) -> None:
form = self.form
scheduling = self.prefs.scheduling
scheduling.new_review_mix = form.newSpread.currentIndex()
scheduling.learn_ahead_secs = form.lrnCutoff.value() * 60
scheduling.day_learn_first = form.dayLearnFirst.isChecked()
scheduling.rollover = form.dayOffset.value()
scheduling.new_timezone = not form.legacy_timezone.isChecked()
reviewing = self.prefs.reviewing
reviewing.show_remaining_due_counts = form.showProgress.isChecked()
reviewing.show_intervals_on_buttons = form.showEstimates.isChecked()
reviewing.time_limit_secs = form.timeLimit.value() * 60
reviewing.hide_audio_play_buttons = not self.form.showPlayButtons.isChecked()
reviewing.interrupt_audio_when_answering = self.form.interrupt_audio.isChecked()
editing = self.prefs.editing
editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex()
editing.paste_images_as_png = self.form.pastePNG.isChecked()
editing.paste_strips_formatting = self.form.paste_strips_formatting.isChecked()
self.mw.col.set_preferences(self.prefs)
self.mw.apply_collection_options()
# Preferences stored in the profile
######################################################################
def setup_profile(self) -> None:
"Setup options stored in the user profile."
self.setup_recording_driver()
self.setup_network()
self.setup_backup()
def update_profile(self) -> None:
self.update_recording_driver()
self.update_network()
self.update_backup()
# Profile: recording driver
######################################################################
def setup_recording_driver(self) -> None:
self._recording_drivers = [
RecordingDriver.QtAudioInput,
RecordingDriver.PyAudio,
]
# The plan is to phase out PyAudio soon, so will hold off on
# making this string translatable for now.
self.form.recording_driver.addItems(
[
f"Voice recording driver: {driver.value}"
for driver in self._recording_drivers
]
)
self.form.recording_driver.setCurrentIndex(
self._recording_drivers.index(self.mw.pm.recording_driver())
)
def update_recording_driver(self) -> None:
new_audio_driver = self._recording_drivers[
self.form.recording_driver.currentIndex()
]
if self.mw.pm.recording_driver() != new_audio_driver:
self.mw.pm.set_recording_driver(new_audio_driver)
if new_audio_driver == RecordingDriver.PyAudio:
showInfo(
"""\
The PyAudio driver will likely be removed in a future update. If you find it works better \
for you than the default driver, please let us know on the Anki forums."""
)
# Profile: network
######################################################################
def setup_network(self) -> None:
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
qconnect(self.form.media_log.clicked, self.on_media_log)
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
self.form.syncMedia.setChecked(self.prof["syncMedia"])
self.form.autoSyncMedia.setChecked(self.mw.pm.auto_sync_media_minutes() != 0)
if not self.prof["syncKey"]:
self._hide_sync_auth_settings()
else:
self.form.syncUser.setText(self.prof.get("syncUser", ""))
qconnect(self.form.syncDeauth.clicked, self.sync_logout)
self.form.syncDeauth.setText(tr(TR.SYNC_LOG_OUT_BUTTON))
def on_media_log(self) -> None:
self.mw.media_syncer.show_sync_log()
def _hide_sync_auth_settings(self) -> None:
self.form.syncDeauth.setVisible(False)
self.form.syncUser.setText("")
self.form.syncLabel.setText(
tr(TR.PREFERENCES_SYNCHRONIZATIONNOT_CURRENTLY_ENABLED_CLICK_THE_SYNC)
)
def sync_logout(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._hide_sync_auth_settings()
def update_network(self) -> None:
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
self.mw.pm.set_auto_sync_media_minutes(
self.form.autoSyncMedia.isChecked() and 15 or 0
)
if self.form.fullSync.isChecked():
self.mw.col.modSchema(check=False)
# Profile: backup
######################################################################
def setup_backup(self) -> None:
self.form.numBackups.setValue(self.prof["numBackups"])
def update_backup(self) -> None:
self.prof["numBackups"] = self.form.numBackups.value()
# Global preferences
######################################################################
def setup_global(self) -> None:
"Setup options global to all profiles."
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
self.form.nightMode.setChecked(self.mw.pm.night_mode())
self.setup_language()
self.setup_video_driver()
self.setupOptions()
def update_global(self) -> None:
restart_required = False
self.update_video_driver()
newScale = self.form.uiScale.value() / 100
if newScale != self.mw.pm.uiScale():
self.mw.pm.setUiScale(newScale)
restart_required = True
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
if restart_required:
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
self.updateOptions()
# legacy - one of Henrik's add-ons is currently wrapping them
def setupOptions(self) -> None:
pass
def updateOptions(self) -> None:
pass
# Global: language
######################################################################
def setup_language(self) -> None:
f = self.form f = self.form
f.lang.addItems([x[0] for x in anki.lang.langs]) f.lang.addItems([x[0] for x in anki.lang.langs])
f.lang.setCurrentIndex(self.langIdx()) f.lang.setCurrentIndex(self.current_lang_index())
qconnect(f.lang.currentIndexChanged, self.onLangIdxChanged) qconnect(f.lang.currentIndexChanged, self.on_language_index_changed)
def langIdx(self) -> int: def current_lang_index(self) -> int:
codes = [x[1] for x in anki.lang.langs] codes = [x[1] for x in anki.lang.langs]
lang = anki.lang.currentLang lang = anki.lang.currentLang
if lang in anki.lang.compatMap: if lang in anki.lang.compatMap:
@ -90,43 +271,16 @@ class Preferences(QDialog):
except: except:
return codes.index("en_US") return codes.index("en_US")
def onLangIdxChanged(self, idx: int) -> None: def on_language_index_changed(self, idx: int) -> None:
code = anki.lang.langs[idx][1] code = anki.lang.langs[idx][1]
self.mw.pm.setLang(code) self.mw.pm.setLang(code)
showInfo( showInfo(
tr(TR.PREFERENCES_PLEASE_RESTART_ANKI_TO_COMPLETE_LANGUAGE), parent=self tr(TR.PREFERENCES_PLEASE_RESTART_ANKI_TO_COMPLETE_LANGUAGE), parent=self
) )
# Collection options # Global: video driver
###################################################################### ######################################################################
def setupCollection(self) -> None:
import anki.consts as c
f = self.form
qc = self.mw.col.conf
self.setup_video_driver()
f.newSpread.addItems(list(c.newCardSchedulingLabels(self.mw.col).values()))
f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True)))
s = self.prefs.scheduling
f.lrnCutoff.setValue(int(s.learn_ahead_secs / 60.0))
f.timeLimit.setValue(int(s.time_limit_secs / 60.0))
f.showEstimates.setChecked(s.show_intervals_on_buttons)
f.showProgress.setChecked(s.show_remaining_due_counts)
f.newSpread.setCurrentIndex(s.new_review_mix)
f.dayLearnFirst.setChecked(s.day_learn_first)
f.dayOffset.setValue(s.rollover)
if s.scheduler_version < 2:
f.dayLearnFirst.setVisible(False)
f.legacy_timezone.setVisible(False)
else:
f.legacy_timezone.setChecked(not s.new_timezone)
def setup_video_driver(self) -> None: def setup_video_driver(self) -> None:
self.video_drivers = VideoDriver.all_for_platform() self.video_drivers = VideoDriver.all_for_platform()
names = [ names = [
@ -144,133 +298,17 @@ class Preferences(QDialog):
self.mw.pm.set_video_driver(new_driver) self.mw.pm.set_video_driver(new_driver)
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU)) showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
def updateCollection(self) -> None:
f = self.form
d = self.mw.col
self.update_video_driver() def video_driver_name_for_platform(driver: VideoDriver) -> str:
if driver == VideoDriver.ANGLE:
qc = d.conf return tr(TR.PREFERENCES_VIDEO_DRIVER_ANGLE)
qc["addToCur"] = not f.useCurrent.currentIndex() elif driver == VideoDriver.Software:
if isMac:
s = self.prefs.scheduling return tr(TR.PREFERENCES_VIDEO_DRIVER_SOFTWARE_MAC)
s.show_remaining_due_counts = f.showProgress.isChecked()
s.show_intervals_on_buttons = f.showEstimates.isChecked()
s.new_review_mix = f.newSpread.currentIndex()
s.time_limit_secs = f.timeLimit.value() * 60
s.learn_ahead_secs = f.lrnCutoff.value() * 60
s.day_learn_first = f.dayLearnFirst.isChecked()
s.rollover = f.dayOffset.value()
s.new_timezone = not f.legacy_timezone.isChecked()
self.mw.col.set_preferences(self.prefs)
# Network
######################################################################
def setupNetwork(self) -> None:
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
qconnect(self.form.media_log.clicked, self.on_media_log)
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
self.form.syncMedia.setChecked(self.prof["syncMedia"])
self.form.autoSyncMedia.setChecked(self.mw.pm.auto_sync_media_minutes() != 0)
if not self.prof["syncKey"]:
self._hideAuth()
else: else:
self.form.syncUser.setText(self.prof.get("syncUser", "")) return tr(TR.PREFERENCES_VIDEO_DRIVER_SOFTWARE_OTHER)
qconnect(self.form.syncDeauth.clicked, self.onSyncDeauth) else:
self.form.syncDeauth.setText(tr(TR.SYNC_LOG_OUT_BUTTON)) if isMac:
return tr(TR.PREFERENCES_VIDEO_DRIVER_OPENGL_MAC)
def on_media_log(self) -> None: else:
self.mw.media_syncer.show_sync_log() return tr(TR.PREFERENCES_VIDEO_DRIVER_OPENGL_OTHER)
def _hideAuth(self) -> None:
self.form.syncDeauth.setVisible(False)
self.form.syncUser.setText("")
self.form.syncLabel.setText(
tr(TR.PREFERENCES_SYNCHRONIZATIONNOT_CURRENTLY_ENABLED_CLICK_THE_SYNC)
)
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) -> None:
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
self.mw.pm.set_auto_sync_media_minutes(
self.form.autoSyncMedia.isChecked() and 15 or 0
)
if self.form.fullSync.isChecked():
self.mw.col.modSchema(check=False)
# Backup
######################################################################
def setupBackup(self) -> None:
self.form.numBackups.setValue(self.prof["numBackups"])
def updateBackup(self) -> None:
self.prof["numBackups"] = self.form.numBackups.value()
# Basic & Advanced Options
######################################################################
def setupOptions(self) -> None:
self.form.pastePNG.setChecked(self.prof.get("pastePNG", False))
self.form.uiScale.setValue(int(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())
self.form.interrupt_audio.setChecked(self.mw.pm.interrupt_audio())
self._recording_drivers = [
RecordingDriver.QtAudioInput,
RecordingDriver.PyAudio,
]
# The plan is to phase out PyAudio soon, so will hold off on
# making this string translatable for now.
self.form.recording_driver.addItems(
[
f"Voice recording driver: {driver.value}"
for driver in self._recording_drivers
]
)
self.form.recording_driver.setCurrentIndex(
self._recording_drivers.index(self.mw.pm.recording_driver())
)
def updateOptions(self) -> None:
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 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())
new_audio_driver = self._recording_drivers[
self.form.recording_driver.currentIndex()
]
if self.mw.pm.recording_driver() != new_audio_driver:
self.mw.pm.set_recording_driver(new_audio_driver)
if new_audio_driver == RecordingDriver.PyAudio:
showInfo(
"""\
The PyAudio driver will likely be removed in a future update. If you find it works better \
for you than the default driver, please let us know on the Anki forums."""
)
if restart_required:
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))

View File

@ -89,14 +89,8 @@ profileConf: Dict[str, Any] = dict(
numBackups=50, numBackups=50,
lastOptimize=intTime(), lastOptimize=intTime(),
# editing # editing
fullSearch=False,
searchHistory=[], searchHistory=[],
lastColour="#00f", lastColour="#00f",
stripHTML=True,
pastePNG=False,
# not exposed in gui
deleteMedia=False,
preserveKeyboard=True,
# syncing # syncing
syncKey=None, syncKey=None,
syncMedia=True, syncMedia=True,
@ -104,6 +98,10 @@ profileConf: Dict[str, Any] = dict(
# importing # importing
allowHTML=False, allowHTML=False,
importMode=1, importMode=1,
# these are not used, but Anki 2.1.42 and below
# expect these keys to exist
stripHTML=True,
deleteMedia=False,
) )
@ -617,13 +615,6 @@ create table if not exists profiles
# Profile-specific # Profile-specific
###################################################################### ######################################################################
def interrupt_audio(self) -> bool:
return self.profile.get("interrupt_audio", True)
def set_interrupt_audio(self, val: bool) -> None:
self.profile["interrupt_audio"] = val
aqt.sound.av_player.interrupt_current_audio = val
def set_sync_key(self, val: Optional[str]) -> None: def set_sync_key(self, val: Optional[str]) -> None:
self.profile["syncKey"] = val self.profile["syncKey"] = val
@ -667,8 +658,3 @@ create table if not exists profiles
def set_recording_driver(self, driver: RecordingDriver) -> None: def set_recording_driver(self, driver: RecordingDriver) -> None:
self.profile["recordingDriver"] = driver.value self.profile["recordingDriver"] = driver.value
######################################################################
def apply_profile_options(self) -> None:
aqt.sound.av_player.interrupt_current_audio = self.interrupt_audio()

View File

@ -1036,16 +1036,27 @@ message Preferences {
uint32 rollover = 2; uint32 rollover = 2;
uint32 learn_ahead_secs = 3; uint32 learn_ahead_secs = 3;
NewReviewMix new_review_mix = 4; NewReviewMix new_review_mix = 4;
bool show_remaining_due_counts = 5;
bool show_intervals_on_buttons = 6;
uint32 time_limit_secs = 7;
// v2 only // v2 only
bool new_timezone = 8; bool new_timezone = 5;
bool day_learn_first = 9; bool day_learn_first = 6;
}
message Reviewing {
bool hide_audio_play_buttons = 1;
bool interrupt_audio_when_answering = 2;
bool show_remaining_due_counts = 3;
bool show_intervals_on_buttons = 4;
uint32 time_limit_secs = 5;
}
message Editing {
bool adding_defaults_to_current_deck = 1;
bool paste_images_as_png = 2;
bool paste_strips_formatting = 3;
} }
Scheduling scheduling = 1; Scheduling scheduling = 1;
Reviewing reviewing = 2;
Editing editing = 3;
} }
message ClozeNumbersInNoteOut { message ClozeNumbersInNoteOut {
@ -1275,6 +1286,10 @@ message Config {
COLLAPSE_FLAGS = 8; COLLAPSE_FLAGS = 8;
SCHED_2021 = 9; SCHED_2021 = 9;
ADDING_DEFAULTS_TO_CURRENT_DECK = 10; ADDING_DEFAULTS_TO_CURRENT_DECK = 10;
HIDE_AUDIO_PLAY_BUTTONS = 11;
INTERRUPT_AUDIO_WHEN_ANSWERING = 12;
PASTE_IMAGES_AS_PNG = 13;
PASTE_STRIPS_FORMATTING = 14;
} }
Key key = 1; Key key = 1;
} }

View File

@ -22,6 +22,10 @@ impl From<BoolKeyProto> for BoolKey {
BoolKeyProto::CollapseFlags => BoolKey::CollapseFlags, BoolKeyProto::CollapseFlags => BoolKey::CollapseFlags,
BoolKeyProto::Sched2021 => BoolKey::Sched2021, BoolKeyProto::Sched2021 => BoolKey::Sched2021,
BoolKeyProto::AddingDefaultsToCurrentDeck => BoolKey::AddingDefaultsToCurrentDeck, BoolKeyProto::AddingDefaultsToCurrentDeck => BoolKey::AddingDefaultsToCurrentDeck,
BoolKeyProto::HideAudioPlayButtons => BoolKey::HideAudioPlayButtons,
BoolKeyProto::InterruptAudioWhenAnswering => BoolKey::InterruptAudioWhenAnswering,
BoolKeyProto::PasteImagesAsPng => BoolKey::PasteImagesAsPng,
BoolKeyProto::PasteStripsFormatting => BoolKey::PasteStripsFormatting,
} }
} }
} }

View File

@ -1319,7 +1319,7 @@ impl BackendService for Backend {
} }
fn set_preferences(&self, input: pb::Preferences) -> BackendResult<Empty> { fn set_preferences(&self, input: pb::Preferences) -> BackendResult<Empty> {
self.with_col(|col| col.transact(None, |col| col.set_preferences(input))) self.with_col(|col| col.set_preferences(input))
.map(Into::into) .map(Into::into)
} }
} }

View File

@ -18,6 +18,10 @@ pub enum BoolKey {
CollapseTags, CollapseTags,
CollapseToday, CollapseToday,
FutureDueShowBacklog, FutureDueShowBacklog,
HideAudioPlayButtons,
InterruptAudioWhenAnswering,
PasteImagesAsPng,
PasteStripsFormatting,
PreviewBothSides, PreviewBothSides,
Sched2021, Sched2021,
@ -50,7 +54,9 @@ impl Collection {
} }
// some keys default to true // some keys default to true
BoolKey::AddingDefaultsToCurrentDeck BoolKey::InterruptAudioWhenAnswering
| BoolKey::ShowIntervalsAboveAnswerButtons
| BoolKey::AddingDefaultsToCurrentDeck
| BoolKey::FutureDueShowBacklog | BoolKey::FutureDueShowBacklog
| BoolKey::ShowRemainingDueCountsInStudy | BoolKey::ShowRemainingDueCountsInStudy
| BoolKey::CardCountsSeparateInactive | BoolKey::CardCountsSeparateInactive

View File

@ -3,7 +3,9 @@
use crate::{ use crate::{
backend_proto::{ backend_proto::{
preferences::scheduling::NewReviewMix as NewRevMixPB, preferences::Scheduling, Preferences, preferences::scheduling::NewReviewMix as NewRevMixPB,
preferences::{Editing, Reviewing, Scheduling},
Preferences,
}, },
collection::Collection, collection::Collection,
config::BoolKey, config::BoolKey,
@ -14,19 +16,36 @@ use crate::{
impl Collection { impl Collection {
pub fn get_preferences(&self) -> Result<Preferences> { pub fn get_preferences(&self) -> Result<Preferences> {
Ok(Preferences { Ok(Preferences {
scheduling: Some(self.get_collection_scheduling_settings()?), scheduling: Some(self.get_scheduling_preferences()?),
reviewing: Some(self.get_reviewing_preferences()?),
editing: Some(self.get_editing_preferences()?),
}) })
} }
pub fn set_preferences(&mut self, prefs: Preferences) -> Result<()> { pub fn set_preferences(&mut self, prefs: Preferences) -> Result<()> {
if let Some(sched) = prefs.scheduling { self.transact(
self.set_collection_scheduling_settings(sched)?; Some(crate::undo::UndoableOpKind::UpdatePreferences),
|col| col.set_preferences_inner(prefs),
)
} }
fn set_preferences_inner(
&mut self,
prefs: Preferences,
) -> Result<(), crate::prelude::AnkiError> {
if let Some(sched) = prefs.scheduling {
self.set_scheduling_preferences(sched)?;
}
if let Some(reviewing) = prefs.reviewing {
self.set_reviewing_preferences(reviewing)?;
}
if let Some(editing) = prefs.editing {
self.set_editing_preferences(editing)?;
}
Ok(()) Ok(())
} }
pub fn get_collection_scheduling_settings(&self) -> Result<Scheduling> { pub fn get_scheduling_preferences(&self) -> Result<Scheduling> {
Ok(Scheduling { Ok(Scheduling {
scheduler_version: match self.scheduler_version() { scheduler_version: match self.scheduler_version() {
crate::config::SchedulerVersion::V1 => 1, crate::config::SchedulerVersion::V1 => 1,
@ -39,30 +58,15 @@ impl Collection {
crate::config::NewReviewMix::ReviewsFirst => NewRevMixPB::ReviewsFirst, crate::config::NewReviewMix::ReviewsFirst => NewRevMixPB::ReviewsFirst,
crate::config::NewReviewMix::NewFirst => NewRevMixPB::NewFirst, crate::config::NewReviewMix::NewFirst => NewRevMixPB::NewFirst,
} as i32, } as i32,
show_remaining_due_counts: self.get_bool(BoolKey::ShowRemainingDueCountsInStudy),
show_intervals_on_buttons: self.get_bool(BoolKey::ShowIntervalsAboveAnswerButtons),
time_limit_secs: self.get_answer_time_limit_secs(),
new_timezone: self.get_creation_utc_offset().is_some(), new_timezone: self.get_creation_utc_offset().is_some(),
day_learn_first: self.get_bool(BoolKey::ShowDayLearningCardsFirst), day_learn_first: self.get_bool(BoolKey::ShowDayLearningCardsFirst),
}) })
} }
pub(crate) fn set_collection_scheduling_settings( pub(crate) fn set_scheduling_preferences(&mut self, settings: Scheduling) -> Result<()> {
&mut self,
settings: Scheduling,
) -> Result<()> {
let s = settings; let s = settings;
self.set_bool(BoolKey::ShowDayLearningCardsFirst, s.day_learn_first)?; self.set_bool(BoolKey::ShowDayLearningCardsFirst, s.day_learn_first)?;
self.set_bool(
BoolKey::ShowRemainingDueCountsInStudy,
s.show_remaining_due_counts,
)?;
self.set_bool(
BoolKey::ShowIntervalsAboveAnswerButtons,
s.show_intervals_on_buttons,
)?;
self.set_answer_time_limit_secs(s.time_limit_secs)?;
self.set_learn_ahead_secs(s.learn_ahead_secs)?; self.set_learn_ahead_secs(s.learn_ahead_secs)?;
self.set_new_review_mix(match s.new_review_mix() { self.set_new_review_mix(match s.new_review_mix() {
@ -87,4 +91,52 @@ impl Collection {
Ok(()) Ok(())
} }
pub fn get_reviewing_preferences(&self) -> Result<Reviewing> {
Ok(Reviewing {
hide_audio_play_buttons: self.get_bool(BoolKey::HideAudioPlayButtons),
interrupt_audio_when_answering: self.get_bool(BoolKey::InterruptAudioWhenAnswering),
show_remaining_due_counts: self.get_bool(BoolKey::ShowRemainingDueCountsInStudy),
show_intervals_on_buttons: self.get_bool(BoolKey::ShowIntervalsAboveAnswerButtons),
time_limit_secs: self.get_answer_time_limit_secs(),
})
}
pub(crate) fn set_reviewing_preferences(&mut self, settings: Reviewing) -> Result<()> {
let s = settings;
self.set_bool(BoolKey::HideAudioPlayButtons, s.hide_audio_play_buttons)?;
self.set_bool(
BoolKey::InterruptAudioWhenAnswering,
s.interrupt_audio_when_answering,
)?;
self.set_bool(
BoolKey::ShowRemainingDueCountsInStudy,
s.show_remaining_due_counts,
)?;
self.set_bool(
BoolKey::ShowIntervalsAboveAnswerButtons,
s.show_intervals_on_buttons,
)?;
self.set_answer_time_limit_secs(s.time_limit_secs)?;
Ok(())
}
pub fn get_editing_preferences(&self) -> Result<Editing> {
Ok(Editing {
adding_defaults_to_current_deck: self.get_bool(BoolKey::AddingDefaultsToCurrentDeck),
paste_images_as_png: self.get_bool(BoolKey::PasteImagesAsPng),
paste_strips_formatting: self.get_bool(BoolKey::PasteStripsFormatting),
})
}
pub(crate) fn set_editing_preferences(&mut self, settings: Editing) -> Result<()> {
let s = settings;
self.set_bool(
BoolKey::AddingDefaultsToCurrentDeck,
s.adding_defaults_to_current_deck,
)?;
self.set_bool(BoolKey::PasteImagesAsPng, s.paste_images_as_png)?;
self.set_bool(BoolKey::PasteStripsFormatting, s.paste_strips_formatting)?;
Ok(())
}
} }

View File

@ -14,6 +14,7 @@ pub enum UndoableOpKind {
RemoveNote, RemoveNote,
UpdateTag, UpdateTag,
UpdateNote, UpdateNote,
UpdatePreferences,
} }
impl UndoableOpKind { impl UndoableOpKind {
@ -34,6 +35,7 @@ impl Collection {
UndoableOpKind::RemoveNote => TR::StudyingDeleteNote, UndoableOpKind::RemoveNote => TR::StudyingDeleteNote,
UndoableOpKind::UpdateTag => TR::UndoUpdateTag, UndoableOpKind::UpdateTag => TR::UndoUpdateTag,
UndoableOpKind::UpdateNote => TR::UndoUpdateNote, UndoableOpKind::UpdateNote => TR::UndoUpdateNote,
UndoableOpKind::UpdatePreferences => TR::PreferencesPreferences,
}; };
self.i18n.tr(key).to_string() self.i18n.tr(key).to_string()