diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index c6458cc91..c81deaa6e 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -789,8 +789,17 @@ class Collection: except KeyError: return default - def set_config(self, key: str, val: Any) -> OpChanges: - return self._backend.set_config_json(key=key, value_json=to_json_bytes(val)) + def set_config(self, key: str, val: Any, *, undoable: bool = False) -> OpChanges: + """Set a single config variable to any JSON-serializable value. The config + is currently sent on every sync, so please don't store more than a few + kilobytes in it. + + By default, no undo entry will be created, but the existing undo history + will be preserved. Set `undoable=True` to allow the change to be undone; + see undo code for how you can merge multiple undo entries.""" + return self._backend.set_config_json( + key=key, value_json=to_json_bytes(val), undoable=undoable + ) def remove_config(self, key: str) -> OpChanges: return self.conf.remove(key) @@ -802,14 +811,18 @@ class Collection: def get_config_bool(self, key: Config.Bool.Key.V) -> bool: return self._backend.get_config_bool(key) - def set_config_bool(self, key: Config.Bool.Key.V, value: bool) -> OpChanges: - return self._backend.set_config_bool(key=key, value=value) + def set_config_bool( + self, key: Config.Bool.Key.V, value: bool, *, undoable: bool = False + ) -> OpChanges: + return self._backend.set_config_bool(key=key, value=value, undoable=undoable) def get_config_string(self, key: Config.String.Key.V) -> str: return self._backend.get_config_string(key) - def set_config_string(self, key: Config.String.Key.V, value: str) -> OpChanges: - return self._backend.set_config_string(key=key, value=value) + def set_config_string( + self, key: Config.String.Key.V, value: str, undoable: bool = False + ) -> OpChanges: + return self._backend.set_config_string(key=key, value=value, undoable=undoable) # Stats ########################################################################## diff --git a/pylib/anki/config.py b/pylib/anki/config.py index 624a95a35..af71eeaf5 100644 --- a/pylib/anki/config.py +++ b/pylib/anki/config.py @@ -45,7 +45,10 @@ class ConfigManager: def set(self, key: str, val: Any) -> None: self.col._backend.set_config_json_no_undo( - key=key, value_json=to_json_bytes(val) + key=key, + value_json=to_json_bytes(val), + # this argument is ignored + undoable=True, ) def remove(self, key: str) -> OpChanges: diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index aa97827b8..6c5e30468 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -180,7 +180,8 @@ class Table: SearchContext(search=last_search, browser=self.browser) ) self.col.set_config_bool( - Config.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, self.is_notes_mode() + Config.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, + self.is_notes_mode(), ) self._restore_header() self._restore_selection(self._toggled_selection) diff --git a/rslib/backend.proto b/rslib/backend.proto index 3fc24c0b6..1bed8f2f7 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -988,6 +988,7 @@ message RenameTagsIn { message SetConfigJsonIn { string key = 1; bytes value_json = 2; + bool undoable = 3; } message StockNotetype { @@ -1441,11 +1442,13 @@ message Config { message SetConfigBoolIn { Config.Bool.Key key = 1; bool value = 2; + bool undoable = 3; } message SetConfigStringIn { Config.String.Key key = 1; string value = 2; + bool undoable = 3; } message RenderMarkdownIn { diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index 49525c160..c5b3467b6 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -65,7 +65,7 @@ impl ConfigService for Backend { fn set_config_json(&self, input: pb::SetConfigJsonIn) -> Result { self.with_col(|col| { let val: Value = serde_json::from_slice(&input.value_json)?; - col.set_config_json(input.key.as_str(), &val) + col.set_config_json(input.key.as_str(), &val, input.undoable) }) .map(Into::into) } @@ -100,7 +100,7 @@ impl ConfigService for Backend { } fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> Result { - self.with_col(|col| col.set_config_bool(input.key().into(), input.value)) + self.with_col(|col| col.set_config_bool(input.key().into(), input.value, input.undoable)) .map(Into::into) } @@ -113,7 +113,7 @@ impl ConfigService for Backend { } fn set_config_string(&self, input: pb::SetConfigStringIn) -> Result { - self.with_col(|col| col.set_config_string(input.key().into(), &input.value)) + self.with_col(|col| col.set_config_string(input.key().into(), &input.value, input.undoable)) .map(Into::into) } diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index d78380f6d..b97c83184 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -70,9 +70,18 @@ impl Collection { } } - pub fn set_config_bool(&mut self, key: BoolKey, value: bool) -> Result> { + pub fn set_config_bool( + &mut self, + key: BoolKey, + value: bool, + undoable: bool, + ) -> Result> { self.transact(Op::UpdateConfig, |col| { - col.set_config(key, &value).map(|_| ()) + col.set_config(key, &value)?; + if !undoable { + col.clear_current_undo_step_changes(); + } + Ok(()) }) } } diff --git a/rslib/src/config/mod.rs b/rslib/src/config/mod.rs index 690bf9df5..3ac207965 100644 --- a/rslib/src/config/mod.rs +++ b/rslib/src/config/mod.rs @@ -70,8 +70,19 @@ pub enum SchedulerVersion { } impl Collection { - pub fn set_config_json(&mut self, key: &str, val: &T) -> Result> { - self.transact(Op::UpdateConfig, |col| col.set_config(key, val).map(|_| ())) + pub fn set_config_json( + &mut self, + key: &str, + val: &T, + undoable: bool, + ) -> Result> { + self.transact(Op::UpdateConfig, |col| { + col.set_config(key, val)?; + if !undoable { + col.clear_current_undo_step_changes(); + } + Ok(()) + }) } pub fn remove_config(&mut self, key: &str) -> Result> { diff --git a/rslib/src/config/string.rs b/rslib/src/config/string.rs index 05b2e6587..553b5255e 100644 --- a/rslib/src/config/string.rs +++ b/rslib/src/config/string.rs @@ -23,9 +23,18 @@ impl Collection { .unwrap_or_else(|| default.to_string()) } - pub fn set_config_string(&mut self, key: StringKey, val: &str) -> Result> { + pub fn set_config_string( + &mut self, + key: StringKey, + val: &str, + undoable: bool, + ) -> Result> { self.transact(Op::UpdateConfig, |col| { - col.set_config_string_inner(key, val).map(|_| ()) + col.set_config_string_inner(key, val)?; + if !undoable { + col.clear_current_undo_step_changes(); + } + Ok(()) }) } } diff --git a/rslib/src/undo/mod.rs b/rslib/src/undo/mod.rs index f54a43c39..1c503915d 100644 --- a/rslib/src/undo/mod.rs +++ b/rslib/src/undo/mod.rs @@ -182,6 +182,12 @@ impl UndoManager { self.end_step(); self.counter } + + fn clear_current_changes(&mut self) { + if let Some(op) = &mut self.current_step { + op.changes.clear(); + } + } } impl Collection { @@ -255,6 +261,12 @@ impl Collection { self.state.undo.save(item.into()); } + /// Forget any recorded changes in the current transaction, allowing + /// a minor change like a config update to bypass undo. + pub(crate) fn clear_current_undo_step_changes(&mut self) { + self.state.undo.clear_current_changes() + } + pub(crate) fn current_undo_op(&self) -> Option<&UndoableOp> { self.state.undo.current_op() }