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()