Skip to content

Commit 24c636f

Browse files
enaboappsOwenMcGirrclaude
authored
Add after-selection scan preferences to the settings model (#936)
* Add after-selection scan preferences to the settings model Next scan and Start from have a shared value and an override for each scanner. Standard keeps what each scanner does today. A saved wait after typing becomes the keyboard's choice, and a value this version does not know falls back alone. Closes #930 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Keep a null shared choice from resetting scan preferences Review follow-up: a null shared value is dropped like an unknown one, and the keyboard's own standard choice now stands over a saved wait after typing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent b4eacd1 commit 24c636f

2 files changed

Lines changed: 174 additions & 1 deletion

File tree

‎src-tauri/src/point_scan.rs‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ impl Config {
103103
.resolved(crate::scan_preferences::Area::Keyboard)
104104
.automatic;
105105
}
106+
// Saved before the choice existed for every scanner. It stands
107+
// until the keyboard is given a choice of its own, or every scanner
108+
// is given one other than standard.
109+
if area == crate::scan_preferences::Area::Keyboard
110+
&& self.keyboard_wait_after_typing
111+
&& self.scan_preferences.keyboard.next_scan.is_none()
112+
&& resolved.next_scan == crate::scan_preferences::NextScan::Standard
113+
{
114+
resolved.next_scan = crate::scan_preferences::NextScan::Wait;
115+
}
106116
resolved
107117
}
108118
pub fn point(&self) -> PointSettings {
@@ -616,6 +626,64 @@ mod tests {
616626
assert!(!restored.word_prediction);
617627
}
618628
#[test]
629+
fn saved_wait_after_typing_becomes_the_keyboard_choice() {
630+
use crate::scan_preferences::{Area, NextScan, StartFrom};
631+
let saved: Config = serde_json::from_str(r#"{"keyboardWaitAfterTyping":true}"#).unwrap();
632+
assert_eq!(saved.resolved(Area::Keyboard).next_scan, NextScan::Wait);
633+
assert_eq!(
634+
saved.resolved(Area::Keyboard).start_from,
635+
StartFrom::Standard
636+
);
637+
let standard: Config = serde_json::from_str(
638+
r#"{"keyboardWaitAfterTyping":true,"scanPreferences":{"keyboard":{"nextScan":"standard"}}}"#,
639+
)
640+
.unwrap();
641+
assert_eq!(
642+
standard.resolved(Area::Keyboard).next_scan,
643+
NextScan::Standard
644+
);
645+
for area in [Area::Point, Area::Menu, Area::Mouse] {
646+
assert_eq!(saved.resolved(area).next_scan, NextScan::Standard);
647+
}
648+
// A choice made since then is kept.
649+
let chosen: Config = serde_json::from_str(
650+
r#"{"keyboardWaitAfterTyping":true,"scanPreferences":{"keyboard":{"nextScan":"automatic"}}}"#,
651+
)
652+
.unwrap();
653+
assert_eq!(
654+
chosen.resolved(Area::Keyboard).next_scan,
655+
NextScan::Automatic
656+
);
657+
let shared: Config = serde_json::from_str(
658+
r#"{"keyboardWaitAfterTyping":true,"scanPreferences":{"nextScan":"automatic"}}"#,
659+
)
660+
.unwrap();
661+
assert_eq!(
662+
shared.resolved(Area::Keyboard).next_scan,
663+
NextScan::Automatic
664+
);
665+
}
666+
#[test]
667+
fn an_unknown_choice_keeps_every_other_scan_setting() {
668+
let saved: Config = serde_json::from_str(
669+
r#"{"gridSize":7,"scanPreferences":{"passLimit":5,"nextScan":"sometimes"}}"#,
670+
)
671+
.unwrap();
672+
saved.validate().unwrap();
673+
assert_eq!(saved.grid_size, 7);
674+
assert_eq!(saved.scan_preferences.pass_limit, 5);
675+
}
676+
#[test]
677+
fn after_selection_choices_round_trip_through_the_saved_file() {
678+
use crate::scan_preferences::{NextScan, StartFrom};
679+
let mut config = Config::default();
680+
config.scan_preferences.next_scan = NextScan::Automatic;
681+
config.scan_preferences.point.start_from = Some(StartFrom::Selection);
682+
let restored: Config =
683+
serde_json::from_value(serde_json::to_value(&config).unwrap()).unwrap();
684+
assert_eq!(restored, config);
685+
}
686+
#[test]
619687
fn enhanced_prediction_defaults_off_for_saved_settings_and_round_trips() {
620688
let c: Config = serde_json::from_str(r#"{"wordPrediction":true}"#).unwrap();
621689
assert!(!c.enhanced_word_prediction);

‎src-tauri/src/scan_preferences.rs‎

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ impl Thickness {
3232
}
3333
}
3434
}
35+
/// Whether scanning moves on by itself after a selection that performed
36+
/// an action.
37+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
38+
#[serde(rename_all = "camelCase")]
39+
pub enum NextScan {
40+
/// What the scanner has always done.
41+
#[default]
42+
Standard,
43+
Automatic,
44+
Wait,
45+
}
46+
/// Where scanning starts after a selection that performed an action.
47+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
48+
#[serde(rename_all = "camelCase")]
49+
pub enum StartFrom {
50+
/// What the scanner has always done.
51+
#[default]
52+
Standard,
53+
Beginning,
54+
Selection,
55+
}
3556
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3657
pub enum Area {
3758
Point,
@@ -49,6 +70,8 @@ pub struct Overrides {
4970
pub pattern: Option<Pattern>,
5071
pub color: Option<ScannerColor>,
5172
pub thickness: Option<Thickness>,
73+
pub next_scan: Option<NextScan>,
74+
pub start_from: Option<StartFrom>,
5275
}
5376
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5477
#[serde(rename_all = "camelCase", default)]
@@ -57,6 +80,8 @@ pub struct Preferences {
5780
pub pass_limit: usize,
5881
pub pattern: Pattern,
5982
pub thickness: Thickness,
83+
pub next_scan: NextScan,
84+
pub start_from: StartFrom,
6085
pub point: Overrides,
6186
pub menu: Overrides,
6287
pub keyboard: Overrides,
@@ -69,6 +94,8 @@ impl Default for Preferences {
6994
pass_limit: 3,
7095
pattern: Pattern::Grouped,
7196
thickness: Thickness::Standard,
97+
next_scan: NextScan::Standard,
98+
start_from: StartFrom::Standard,
7299
point: Default::default(),
73100
menu: Default::default(),
74101
keyboard: Default::default(),
@@ -85,6 +112,8 @@ pub struct Resolved {
85112
pub pattern: Pattern,
86113
pub color: ScannerColor,
87114
pub thickness: Thickness,
115+
pub next_scan: NextScan,
116+
pub start_from: StartFrom,
88117
}
89118
impl Default for Resolved {
90119
fn default() -> Self {
@@ -134,13 +163,22 @@ impl Preferences {
134163
},
135164
color: local.color.unwrap_or(color),
136165
thickness: local.thickness.unwrap_or(self.thickness),
166+
next_scan: local.next_scan.unwrap_or(self.next_scan),
167+
start_from: local.start_from.unwrap_or(self.start_from),
137168
}
138169
}
139170
}
140171
pub fn deserialize_preferences<'de, D: serde::Deserializer<'de>>(
141172
deserializer: D,
142173
) -> Result<Preferences, D::Error> {
143-
let value = serde_json::Value::deserialize(deserializer)?;
174+
let mut value = serde_json::Value::deserialize(deserializer)?;
175+
// A choice this version does not know must not cost the other settings.
176+
forget_unknown_choices(&mut value, false);
177+
for area in ["point", "menu", "keyboard", "mouse"] {
178+
if let Some(local) = value.get_mut(area) {
179+
forget_unknown_choices(local, true);
180+
}
181+
}
144182
let mut preferences: Preferences = serde_json::from_value(value).unwrap_or_default();
145183
if !valid_passes(preferences.pass_limit) {
146184
preferences.pass_limit = 3;
@@ -156,10 +194,77 @@ pub fn deserialize_preferences<'de, D: serde::Deserializer<'de>>(
156194
}
157195
Ok(preferences)
158196
}
197+
/// Only a scanner's own choice may be absent, which is saved as null.
198+
fn forget_unknown_choices(value: &mut serde_json::Value, optional: bool) {
199+
let Some(fields) = value.as_object_mut() else {
200+
return;
201+
};
202+
fields.retain(|name, choice| match name.as_str() {
203+
"nextScan" => (optional && choice.is_null()) || NextScan::deserialize(&*choice).is_ok(),
204+
"startFrom" => (optional && choice.is_null()) || StartFrom::deserialize(&*choice).is_ok(),
205+
_ => true,
206+
});
207+
}
159208

160209
#[cfg(test)]
161210
mod tests {
162211
use super::*;
212+
fn read(value: serde_json::Value) -> Preferences {
213+
deserialize_preferences(value).unwrap()
214+
}
215+
#[test]
216+
fn after_selection_choices_default_to_standard_and_round_trip() {
217+
let saved = read(serde_json::json!({"direction":"reverse"}));
218+
assert_eq!(saved.next_scan, NextScan::Standard);
219+
assert_eq!(saved.start_from, StartFrom::Standard);
220+
for area in [Area::Point, Area::Menu, Area::Keyboard, Area::Mouse] {
221+
let resolved = saved.resolve(area, true, 1000, ScannerColor::Blue);
222+
assert_eq!(resolved.next_scan, NextScan::Standard);
223+
assert_eq!(resolved.start_from, StartFrom::Standard);
224+
}
225+
let mut chosen = Preferences {
226+
next_scan: NextScan::Wait,
227+
start_from: StartFrom::Selection,
228+
..Default::default()
229+
};
230+
chosen.menu.next_scan = Some(NextScan::Automatic);
231+
chosen.mouse.start_from = Some(StartFrom::Beginning);
232+
let stored = serde_json::to_value(&chosen).unwrap();
233+
assert_eq!(stored["nextScan"], "wait");
234+
assert_eq!(stored["startFrom"], "selection");
235+
assert_eq!(stored["menu"]["nextScan"], "automatic");
236+
assert_eq!(stored["mouse"]["startFrom"], "beginning");
237+
assert_eq!(read(stored), chosen);
238+
let menu = chosen.resolve(Area::Menu, true, 1000, ScannerColor::Blue);
239+
assert_eq!(menu.next_scan, NextScan::Automatic);
240+
assert_eq!(menu.start_from, StartFrom::Selection);
241+
let mouse = chosen.resolve(Area::Mouse, true, 1000, ScannerColor::Blue);
242+
assert_eq!(mouse.next_scan, NextScan::Wait);
243+
assert_eq!(mouse.start_from, StartFrom::Beginning);
244+
}
245+
#[test]
246+
fn an_unknown_choice_falls_back_alone() {
247+
let saved = read(serde_json::json!({
248+
"direction": "reverse",
249+
"passLimit": 5,
250+
"nextScan": null,
251+
"startFrom": 7,
252+
"keyboard": {"intervalMs": 250, "nextScan": "later", "startFrom": "selection"},
253+
"menu": {"nextScan": "wait", "startFrom": null},
254+
}));
255+
assert_eq!(saved.direction, Direction::Reverse);
256+
assert_eq!(saved.pass_limit, 5);
257+
assert_eq!(saved.next_scan, NextScan::Standard);
258+
assert_eq!(saved.start_from, StartFrom::Standard);
259+
assert_eq!(saved.keyboard.interval_ms, Some(250));
260+
assert_eq!(saved.keyboard.next_scan, None);
261+
assert_eq!(saved.keyboard.start_from, Some(StartFrom::Selection));
262+
assert_eq!(saved.menu.next_scan, Some(NextScan::Wait));
263+
assert_eq!(saved.menu.start_from, None);
264+
let saved = read(serde_json::json!({"passLimit": 5, "nextScan": "sometimes"}));
265+
assert_eq!(saved.pass_limit, 5);
266+
assert_eq!(saved.next_scan, NextScan::Standard);
267+
}
163268
#[test]
164269
fn areas_inherit_and_override_independently() {
165270
let mut settings = Preferences::default();

0 commit comments

Comments
 (0)