diff --git a/src-tauri/src/point_scan.rs b/src-tauri/src/point_scan.rs index 87f97782..9d48bc25 100644 --- a/src-tauri/src/point_scan.rs +++ b/src-tauri/src/point_scan.rs @@ -103,6 +103,16 @@ impl Config { .resolved(crate::scan_preferences::Area::Keyboard) .automatic; } + // Saved before the choice existed for every scanner. It stands + // until the keyboard is given a choice of its own, or every scanner + // is given one other than standard. + if area == crate::scan_preferences::Area::Keyboard + && self.keyboard_wait_after_typing + && self.scan_preferences.keyboard.next_scan.is_none() + && resolved.next_scan == crate::scan_preferences::NextScan::Standard + { + resolved.next_scan = crate::scan_preferences::NextScan::Wait; + } resolved } pub fn point(&self) -> PointSettings { @@ -616,6 +626,64 @@ mod tests { assert!(!restored.word_prediction); } #[test] + fn saved_wait_after_typing_becomes_the_keyboard_choice() { + use crate::scan_preferences::{Area, NextScan, StartFrom}; + let saved: Config = serde_json::from_str(r#"{"keyboardWaitAfterTyping":true}"#).unwrap(); + assert_eq!(saved.resolved(Area::Keyboard).next_scan, NextScan::Wait); + assert_eq!( + saved.resolved(Area::Keyboard).start_from, + StartFrom::Standard + ); + let standard: Config = serde_json::from_str( + r#"{"keyboardWaitAfterTyping":true,"scanPreferences":{"keyboard":{"nextScan":"standard"}}}"#, + ) + .unwrap(); + assert_eq!( + standard.resolved(Area::Keyboard).next_scan, + NextScan::Standard + ); + for area in [Area::Point, Area::Menu, Area::Mouse] { + assert_eq!(saved.resolved(area).next_scan, NextScan::Standard); + } + // A choice made since then is kept. + let chosen: Config = serde_json::from_str( + r#"{"keyboardWaitAfterTyping":true,"scanPreferences":{"keyboard":{"nextScan":"automatic"}}}"#, + ) + .unwrap(); + assert_eq!( + chosen.resolved(Area::Keyboard).next_scan, + NextScan::Automatic + ); + let shared: Config = serde_json::from_str( + r#"{"keyboardWaitAfterTyping":true,"scanPreferences":{"nextScan":"automatic"}}"#, + ) + .unwrap(); + assert_eq!( + shared.resolved(Area::Keyboard).next_scan, + NextScan::Automatic + ); + } + #[test] + fn an_unknown_choice_keeps_every_other_scan_setting() { + let saved: Config = serde_json::from_str( + r#"{"gridSize":7,"scanPreferences":{"passLimit":5,"nextScan":"sometimes"}}"#, + ) + .unwrap(); + saved.validate().unwrap(); + assert_eq!(saved.grid_size, 7); + assert_eq!(saved.scan_preferences.pass_limit, 5); + } + #[test] + fn after_selection_choices_round_trip_through_the_saved_file() { + use crate::scan_preferences::{NextScan, StartFrom}; + let mut config = Config::default(); + config.scan_preferences.next_scan = NextScan::Automatic; + config.scan_preferences.point.start_from = Some(StartFrom::Selection); + let restored: Config = + serde_json::from_value(serde_json::to_value(&config).unwrap()).unwrap(); + assert_eq!(restored, config); + } + #[test] fn enhanced_prediction_defaults_off_for_saved_settings_and_round_trips() { let c: Config = serde_json::from_str(r#"{"wordPrediction":true}"#).unwrap(); assert!(!c.enhanced_word_prediction); diff --git a/src-tauri/src/scan_preferences.rs b/src-tauri/src/scan_preferences.rs index 20cea2b8..c37895a9 100644 --- a/src-tauri/src/scan_preferences.rs +++ b/src-tauri/src/scan_preferences.rs @@ -32,6 +32,27 @@ impl Thickness { } } } +/// Whether scanning moves on by itself after a selection that performed +/// an action. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum NextScan { + /// What the scanner has always done. + #[default] + Standard, + Automatic, + Wait, +} +/// Where scanning starts after a selection that performed an action. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum StartFrom { + /// What the scanner has always done. + #[default] + Standard, + Beginning, + Selection, +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Area { Point, @@ -49,6 +70,8 @@ pub struct Overrides { pub pattern: Option, pub color: Option, pub thickness: Option, + pub next_scan: Option, + pub start_from: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] @@ -57,6 +80,8 @@ pub struct Preferences { pub pass_limit: usize, pub pattern: Pattern, pub thickness: Thickness, + pub next_scan: NextScan, + pub start_from: StartFrom, pub point: Overrides, pub menu: Overrides, pub keyboard: Overrides, @@ -69,6 +94,8 @@ impl Default for Preferences { pass_limit: 3, pattern: Pattern::Grouped, thickness: Thickness::Standard, + next_scan: NextScan::Standard, + start_from: StartFrom::Standard, point: Default::default(), menu: Default::default(), keyboard: Default::default(), @@ -85,6 +112,8 @@ pub struct Resolved { pub pattern: Pattern, pub color: ScannerColor, pub thickness: Thickness, + pub next_scan: NextScan, + pub start_from: StartFrom, } impl Default for Resolved { fn default() -> Self { @@ -134,13 +163,22 @@ impl Preferences { }, color: local.color.unwrap_or(color), thickness: local.thickness.unwrap_or(self.thickness), + next_scan: local.next_scan.unwrap_or(self.next_scan), + start_from: local.start_from.unwrap_or(self.start_from), } } } pub fn deserialize_preferences<'de, D: serde::Deserializer<'de>>( deserializer: D, ) -> Result { - let value = serde_json::Value::deserialize(deserializer)?; + let mut value = serde_json::Value::deserialize(deserializer)?; + // A choice this version does not know must not cost the other settings. + forget_unknown_choices(&mut value, false); + for area in ["point", "menu", "keyboard", "mouse"] { + if let Some(local) = value.get_mut(area) { + forget_unknown_choices(local, true); + } + } let mut preferences: Preferences = serde_json::from_value(value).unwrap_or_default(); if !valid_passes(preferences.pass_limit) { preferences.pass_limit = 3; @@ -156,10 +194,77 @@ pub fn deserialize_preferences<'de, D: serde::Deserializer<'de>>( } Ok(preferences) } +/// Only a scanner's own choice may be absent, which is saved as null. +fn forget_unknown_choices(value: &mut serde_json::Value, optional: bool) { + let Some(fields) = value.as_object_mut() else { + return; + }; + fields.retain(|name, choice| match name.as_str() { + "nextScan" => (optional && choice.is_null()) || NextScan::deserialize(&*choice).is_ok(), + "startFrom" => (optional && choice.is_null()) || StartFrom::deserialize(&*choice).is_ok(), + _ => true, + }); +} #[cfg(test)] mod tests { use super::*; + fn read(value: serde_json::Value) -> Preferences { + deserialize_preferences(value).unwrap() + } + #[test] + fn after_selection_choices_default_to_standard_and_round_trip() { + let saved = read(serde_json::json!({"direction":"reverse"})); + assert_eq!(saved.next_scan, NextScan::Standard); + assert_eq!(saved.start_from, StartFrom::Standard); + for area in [Area::Point, Area::Menu, Area::Keyboard, Area::Mouse] { + let resolved = saved.resolve(area, true, 1000, ScannerColor::Blue); + assert_eq!(resolved.next_scan, NextScan::Standard); + assert_eq!(resolved.start_from, StartFrom::Standard); + } + let mut chosen = Preferences { + next_scan: NextScan::Wait, + start_from: StartFrom::Selection, + ..Default::default() + }; + chosen.menu.next_scan = Some(NextScan::Automatic); + chosen.mouse.start_from = Some(StartFrom::Beginning); + let stored = serde_json::to_value(&chosen).unwrap(); + assert_eq!(stored["nextScan"], "wait"); + assert_eq!(stored["startFrom"], "selection"); + assert_eq!(stored["menu"]["nextScan"], "automatic"); + assert_eq!(stored["mouse"]["startFrom"], "beginning"); + assert_eq!(read(stored), chosen); + let menu = chosen.resolve(Area::Menu, true, 1000, ScannerColor::Blue); + assert_eq!(menu.next_scan, NextScan::Automatic); + assert_eq!(menu.start_from, StartFrom::Selection); + let mouse = chosen.resolve(Area::Mouse, true, 1000, ScannerColor::Blue); + assert_eq!(mouse.next_scan, NextScan::Wait); + assert_eq!(mouse.start_from, StartFrom::Beginning); + } + #[test] + fn an_unknown_choice_falls_back_alone() { + let saved = read(serde_json::json!({ + "direction": "reverse", + "passLimit": 5, + "nextScan": null, + "startFrom": 7, + "keyboard": {"intervalMs": 250, "nextScan": "later", "startFrom": "selection"}, + "menu": {"nextScan": "wait", "startFrom": null}, + })); + assert_eq!(saved.direction, Direction::Reverse); + assert_eq!(saved.pass_limit, 5); + assert_eq!(saved.next_scan, NextScan::Standard); + assert_eq!(saved.start_from, StartFrom::Standard); + assert_eq!(saved.keyboard.interval_ms, Some(250)); + assert_eq!(saved.keyboard.next_scan, None); + assert_eq!(saved.keyboard.start_from, Some(StartFrom::Selection)); + assert_eq!(saved.menu.next_scan, Some(NextScan::Wait)); + assert_eq!(saved.menu.start_from, None); + let saved = read(serde_json::json!({"passLimit": 5, "nextScan": "sometimes"})); + assert_eq!(saved.pass_limit, 5); + assert_eq!(saved.next_scan, NextScan::Standard); + } #[test] fn areas_inherit_and_override_independently() { let mut settings = Preferences::default();