Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src-tauri/src/point_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
107 changes: 106 additions & 1 deletion src-tauri/src/scan_preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -49,6 +70,8 @@ pub struct Overrides {
pub pattern: Option<Pattern>,
pub color: Option<ScannerColor>,
pub thickness: Option<Thickness>,
pub next_scan: Option<NextScan>,
pub start_from: Option<StartFrom>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
Expand All @@ -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,
Expand All @@ -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(),
Expand All @@ -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 {
Expand Down Expand Up @@ -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<Preferences, D::Error> {
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;
Expand All @@ -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();
Expand Down
Loading