From 6b1dd9ee1968c301cc7d46f24b176712766c3c1d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 10 Mar 2021 18:20:37 +1000 Subject: [PATCH] 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 --- qt/aqt/editor.py | 29 ++- qt/aqt/forms/preferences.ui | 4 +- qt/aqt/main.py | 17 +- qt/aqt/preferences.py | 412 ++++++++++++++++++++---------------- qt/aqt/profiles.py | 22 +- rslib/backend.proto | 25 ++- rslib/src/backend/config.rs | 4 + rslib/src/backend/mod.rs | 2 +- rslib/src/config/bool.rs | 8 +- rslib/src/preferences.rs | 96 +++++++-- rslib/src/undo/ops.rs | 2 + 11 files changed, 362 insertions(+), 259 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 3346dfbc6..ec2d69dd8 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -20,7 +20,7 @@ from bs4 import BeautifulSoup import aqt import aqt.sound from anki.cards import Card -from anki.collection import SearchNode +from anki.collection import Config, SearchNode from anki.consts import MODEL_CLOZE from anki.hooks import runFilter from anki.httpclient import HttpClient @@ -781,7 +781,7 @@ class Editor: filter = f"{tr(TR.EDITING_MEDIA)} ({extension_filter})" def accept(file: str) -> None: - self.addMedia(file, canDelete=True) + self.addMedia(file) file = getFile( parent=self.widget, @@ -793,24 +793,18 @@ class Editor: self.parentWindow.activateWindow() def addMedia(self, path: str, canDelete: bool = False) -> None: + """canDelete is a legacy arg and is ignored.""" try: - html = self._addMedia(path, canDelete) + html = self._addMedia(path) except Exception as e: showWarning(str(e)) return self.web.eval(f"setFormat('inserthtml', {json.dumps(html)});") 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 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 self.fnameToLink(fname) @@ -1091,7 +1085,6 @@ class EditorWebView(AnkiWebView): def __init__(self, parent: QWidget, editor: Editor) -> None: AnkiWebView.__init__(self, title="editor") self.editor = editor - self.strip = self.editor.mw.pm.profile["stripHTML"] self.setAcceptDrops(True) self._markInternal = False clip = self.editor.mw.app.clipboard() @@ -1110,10 +1103,12 @@ class EditorWebView(AnkiWebView): self.triggerPageAction(QWebEnginePage.Copy) def _wantsExtendedPaste(self) -> bool: - extended = not (self.editor.mw.app.queryKeyboardModifiers() & Qt.ShiftModifier) - if self.editor.mw.pm.profile.get("pasteInvert", False): - extended = not extended - return extended + strip_html = self.editor.mw.col.get_config_bool( + Config.Bool.PASTE_STRIPS_FORMATTING + ) + if self.editor.mw.app.queryKeyboardModifiers() & Qt.ShiftModifier: + strip_html = not strip_html + return strip_html def _onPaste(self, mode: QClipboard.Mode) -> None: extended = self._wantsExtendedPaste() @@ -1240,7 +1235,7 @@ class EditorWebView(AnkiWebView): return None im = QImage(mime.imageData()) 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" im.save(uname + ext, None, 50) else: diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 3e5293461..3e950546f 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -80,7 +80,7 @@ - + PREFERENCES_PASTE_WITHOUT_SHIFT_KEY_STRIPS_FORMATTING @@ -589,7 +589,7 @@ showPlayButtons interrupt_audio pastePNG - pasteInvert + paste_strips_formatting nightMode useCurrent recording_driver diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 6a99be3cc..6907c716c 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -27,7 +27,7 @@ import aqt.toolbar import aqt.webview from anki import hooks 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.hooks import runHook from anki.sound import AVTag, SoundOrVideoTag @@ -391,8 +391,6 @@ class AnkiQt(QMainWindow): if not self.loadCollection(): return - self.pm.apply_profile_options() - # show main window if self.pm.profile["mainWindowState"]: restoreGeom(self, "mainWindow") @@ -467,10 +465,10 @@ class AnkiQt(QMainWindow): def _add_play_buttons(self, text: str) -> str: "Return card text with play buttons added, or stripped." - if self.pm.profile.get("showPlayButtons", True): - return aqt.sound.av_refs_to_play_icons(text) - else: + if self.col.get_config_bool(Config.Bool.HIDE_AUDIO_PLAY_BUTTONS): 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: text = self.col.media.escape_media_filenames(text) @@ -508,6 +506,7 @@ class AnkiQt(QMainWindow): try: self.update_undo_actions() gui_hooks.collection_did_load(self.col) + self.apply_collection_options() self.moveToState("deckBrowser") except Exception as e: # 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.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 ########################################################################## diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index ea9726c77..64fe935e9 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -1,7 +1,9 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + import anki.lang import aqt +from anki.consts import newCardSchedulingLabels from aqt import AnkiQt from aqt.profiles import RecordingDriver, VideoDriver 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): def __init__(self, mw: AnkiQt) -> None: QDialog.__init__(self, mw, Qt.Window) @@ -45,22 +32,18 @@ class Preferences(QDialog): self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.PREFERENCES) ) self.silentlyClose = True - self.prefs = self.mw.col.get_preferences() - self.setupLang() - self.setupCollection() - self.setupNetwork() - self.setupBackup() - self.setupOptions() + self.setup_collection() + self.setup_profile() + self.setup_global() self.show() def accept(self) -> None: # avoid exception if main window is already closed if not self.mw.col: return - self.updateCollection() - self.updateNetwork() - self.updateBackup() - self.updateOptions() + self.update_collection() + self.update_profile() + self.update_global() self.mw.pm.save() self.mw.reset() self.done(0) @@ -69,16 +52,214 @@ class Preferences(QDialog): def reject(self) -> None: 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.lang.addItems([x[0] for x in anki.lang.langs]) - f.lang.setCurrentIndex(self.langIdx()) - qconnect(f.lang.currentIndexChanged, self.onLangIdxChanged) + f.lang.setCurrentIndex(self.current_lang_index()) + 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] lang = anki.lang.currentLang if lang in anki.lang.compatMap: @@ -90,43 +271,16 @@ class Preferences(QDialog): except: 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] self.mw.pm.setLang(code) showInfo( 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: self.video_drivers = VideoDriver.all_for_platform() names = [ @@ -144,133 +298,17 @@ class Preferences(QDialog): self.mw.pm.set_video_driver(new_driver) 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() - - qc = d.conf - qc["addToCur"] = not f.useCurrent.currentIndex() - - s = self.prefs.scheduling - 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() +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: - self.form.syncUser.setText(self.prof.get("syncUser", "")) - qconnect(self.form.syncDeauth.clicked, self.onSyncDeauth) - self.form.syncDeauth.setText(tr(TR.SYNC_LOG_OUT_BUTTON)) - - def on_media_log(self) -> None: - self.mw.media_syncer.show_sync_log() - - 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)) + 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) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 50bc6a3a6..200e11f80 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -89,14 +89,8 @@ profileConf: Dict[str, Any] = dict( numBackups=50, lastOptimize=intTime(), # editing - fullSearch=False, searchHistory=[], lastColour="#00f", - stripHTML=True, - pastePNG=False, - # not exposed in gui - deleteMedia=False, - preserveKeyboard=True, # syncing syncKey=None, syncMedia=True, @@ -104,6 +98,10 @@ profileConf: Dict[str, Any] = dict( # importing allowHTML=False, 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 ###################################################################### - 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: self.profile["syncKey"] = val @@ -667,8 +658,3 @@ create table if not exists profiles def set_recording_driver(self, driver: RecordingDriver) -> None: self.profile["recordingDriver"] = driver.value - - ###################################################################### - - def apply_profile_options(self) -> None: - aqt.sound.av_player.interrupt_current_audio = self.interrupt_audio() diff --git a/rslib/backend.proto b/rslib/backend.proto index cbbdf3de1..78e07aeae 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -1036,16 +1036,27 @@ message Preferences { uint32 rollover = 2; uint32 learn_ahead_secs = 3; NewReviewMix new_review_mix = 4; - bool show_remaining_due_counts = 5; - bool show_intervals_on_buttons = 6; - uint32 time_limit_secs = 7; // v2 only - bool new_timezone = 8; - bool day_learn_first = 9; + bool new_timezone = 5; + 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; + Reviewing reviewing = 2; + Editing editing = 3; } message ClozeNumbersInNoteOut { @@ -1275,6 +1286,10 @@ message Config { COLLAPSE_FLAGS = 8; SCHED_2021 = 9; 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; } diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index fcac7d7cc..0deaa12e7 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -22,6 +22,10 @@ impl From for BoolKey { BoolKeyProto::CollapseFlags => BoolKey::CollapseFlags, BoolKeyProto::Sched2021 => BoolKey::Sched2021, BoolKeyProto::AddingDefaultsToCurrentDeck => BoolKey::AddingDefaultsToCurrentDeck, + BoolKeyProto::HideAudioPlayButtons => BoolKey::HideAudioPlayButtons, + BoolKeyProto::InterruptAudioWhenAnswering => BoolKey::InterruptAudioWhenAnswering, + BoolKeyProto::PasteImagesAsPng => BoolKey::PasteImagesAsPng, + BoolKeyProto::PasteStripsFormatting => BoolKey::PasteStripsFormatting, } } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index c8ee24a57..6c8d6c39b 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -1319,7 +1319,7 @@ impl BackendService for Backend { } fn set_preferences(&self, input: pb::Preferences) -> BackendResult { - self.with_col(|col| col.transact(None, |col| col.set_preferences(input))) + self.with_col(|col| col.set_preferences(input)) .map(Into::into) } } diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index 06f13e31e..ebb247d8b 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -18,6 +18,10 @@ pub enum BoolKey { CollapseTags, CollapseToday, FutureDueShowBacklog, + HideAudioPlayButtons, + InterruptAudioWhenAnswering, + PasteImagesAsPng, + PasteStripsFormatting, PreviewBothSides, Sched2021, @@ -50,7 +54,9 @@ impl Collection { } // some keys default to true - BoolKey::AddingDefaultsToCurrentDeck + BoolKey::InterruptAudioWhenAnswering + | BoolKey::ShowIntervalsAboveAnswerButtons + | BoolKey::AddingDefaultsToCurrentDeck | BoolKey::FutureDueShowBacklog | BoolKey::ShowRemainingDueCountsInStudy | BoolKey::CardCountsSeparateInactive diff --git a/rslib/src/preferences.rs b/rslib/src/preferences.rs index b2a59addd..e1b4eb80f 100644 --- a/rslib/src/preferences.rs +++ b/rslib/src/preferences.rs @@ -3,7 +3,9 @@ use crate::{ backend_proto::{ - preferences::scheduling::NewReviewMix as NewRevMixPB, preferences::Scheduling, Preferences, + preferences::scheduling::NewReviewMix as NewRevMixPB, + preferences::{Editing, Reviewing, Scheduling}, + Preferences, }, collection::Collection, config::BoolKey, @@ -14,19 +16,36 @@ use crate::{ impl Collection { pub fn get_preferences(&self) -> Result { 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<()> { - if let Some(sched) = prefs.scheduling { - self.set_collection_scheduling_settings(sched)?; - } + self.transact( + 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(()) } - pub fn get_collection_scheduling_settings(&self) -> Result { + pub fn get_scheduling_preferences(&self) -> Result { Ok(Scheduling { scheduler_version: match self.scheduler_version() { crate::config::SchedulerVersion::V1 => 1, @@ -39,30 +58,15 @@ impl Collection { crate::config::NewReviewMix::ReviewsFirst => NewRevMixPB::ReviewsFirst, crate::config::NewReviewMix::NewFirst => NewRevMixPB::NewFirst, } 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(), day_learn_first: self.get_bool(BoolKey::ShowDayLearningCardsFirst), }) } - pub(crate) fn set_collection_scheduling_settings( - &mut self, - settings: Scheduling, - ) -> Result<()> { + pub(crate) fn set_scheduling_preferences(&mut self, settings: Scheduling) -> Result<()> { let s = settings; 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_new_review_mix(match s.new_review_mix() { @@ -87,4 +91,52 @@ impl Collection { Ok(()) } + + pub fn get_reviewing_preferences(&self) -> Result { + 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 { + 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(()) + } } diff --git a/rslib/src/undo/ops.rs b/rslib/src/undo/ops.rs index dea29e517..028549224 100644 --- a/rslib/src/undo/ops.rs +++ b/rslib/src/undo/ops.rs @@ -14,6 +14,7 @@ pub enum UndoableOpKind { RemoveNote, UpdateTag, UpdateNote, + UpdatePreferences, } impl UndoableOpKind { @@ -34,6 +35,7 @@ impl Collection { UndoableOpKind::RemoveNote => TR::StudyingDeleteNote, UndoableOpKind::UpdateTag => TR::UndoUpdateTag, UndoableOpKind::UpdateNote => TR::UndoUpdateNote, + UndoableOpKind::UpdatePreferences => TR::PreferencesPreferences, }; self.i18n.tr(key).to_string()