From 549effe5cb52ab8c8fbe3e334aed207426362e2d Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Wed, 23 Sep 2026 14:27:18 +0100 Subject: [PATCH 1/4] Present Point and Mouse as scanning modes --- src-tauri/src/point_scan.rs | 14 ++++++ src-tauri/src/point_scan_runtime.rs | 33 ++++++++++++- src-tauri/src/point_workflow.rs | 72 ++++++++++++++++++++++++++--- src-tauri/src/scan_executor.rs | 4 +- src-tauri/src/scan_mouse.rs | 2 +- src-tauri/src/scanning.rs | 7 ++- src-tauri/src/scanning_runtime.rs | 24 +++++++++- src-tauri/src/switch_gestures.rs | 2 + src-tauri/src/switches.rs | 2 + src/App.test.tsx | 2 +- src/App.tsx | 6 +-- src/PointScan.test.tsx | 24 ++++++++-- src/scanning/useScanning.ts | 2 + src/scanning/useSwitches.ts | 1 + src/settings/ScannerPreferences.tsx | 22 ++++++--- src/settings/ScanningSection.tsx | 5 +- src/settings/settings.test.tsx | 3 +- 17 files changed, 193 insertions(+), 32 deletions(-) diff --git a/src-tauri/src/point_scan.rs b/src-tauri/src/point_scan.rs index 612ba17c..0d48eafe 100644 --- a/src-tauri/src/point_scan.rs +++ b/src-tauri/src/point_scan.rs @@ -11,10 +11,18 @@ pub enum Mode { Line, Grid, } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ControlMode { + #[default] + Point, + Mouse, +} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct Config { + pub control_mode: ControlMode, #[serde(deserialize_with = "crate::scan_preferences::deserialize_preferences")] pub scan_preferences: crate::scan_preferences::Preferences, pub word_prediction: bool, @@ -35,6 +43,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { + control_mode: ControlMode::Point, scan_preferences: Default::default(), word_prediction: true, keyboard_wait_after_typing: false, @@ -91,6 +100,7 @@ impl Config { } pub fn point(&self) -> PointSettings { PointSettings { + control_mode: self.control_mode, scan: self.resolved(crate::scan_preferences::Area::Point), menu_scan: self.resolved(crate::scan_preferences::Area::Menu), keyboard_scan: self.resolved(crate::scan_preferences::Area::Keyboard), @@ -115,6 +125,7 @@ impl Config { } #[derive(Clone)] pub struct PointSettings { + pub control_mode: ControlMode, pub scan: crate::scan_preferences::Resolved, pub menu_scan: crate::scan_preferences::Resolved, pub keyboard_scan: crate::scan_preferences::Resolved, @@ -284,6 +295,7 @@ impl Engine { | Action::Stop | Action::Cancel | Action::OpenKeyboard + | Action::OpenPoint | Action::OpenMouse => {} } None @@ -1006,6 +1018,7 @@ mod tests { json["autoSelectEnabled"] = serde_json::json!(false); json["autoSelectDelayMs"] = serde_json::json!(1000); json["scannerColor"] = serde_json::json!("blue"); + json["controlMode"] = serde_json::json!("point"); json["wordPrediction"] = serde_json::json!(true); json["keyboardWaitAfterTyping"] = serde_json::json!(false); json["scanPreferences"] = @@ -1031,6 +1044,7 @@ mod tests { fn legacy_empty_config_and_validation() { let c: Config = serde_json::from_str("{}").unwrap(); assert_eq!(c, Config::default()); + assert_eq!(c.control_mode, ControlMode::Point); assert!(Config { select_key: "Escape".into(), ..c.clone() diff --git a/src-tauri/src/point_scan_runtime.rs b/src-tauri/src/point_scan_runtime.rs index 04b07b7c..f15ae985 100644 --- a/src-tauri/src/point_scan_runtime.rs +++ b/src-tauri/src/point_scan_runtime.rs @@ -41,6 +41,14 @@ impl Adapter for PointScan { type Environment = Environment; const EVENT: &'static str = "point-scan-changed"; const FILE: &'static str = "point-scan.json"; + fn sync_mode(config: &mut Config, technique: &Workflow) -> bool { + let next = technique.control_mode(); + if config.control_mode == next { + return false; + } + config.control_mode = next; + true + } fn cursor_feedback(technique: &Workflow) -> Option { technique.mouse_feedback() } @@ -91,7 +99,7 @@ impl Adapter for PointScan { crate::prediction::record(stroke, result.is_ok(), scope); return result.map(|()| None); } - Request::OpenKeyboard | Request::OpenMouse => { + Request::OpenKeyboard | Request::OpenMouse | Request::OpenPoint => { crate::prediction::stop(); crate::scan_executor::activate(request) } @@ -282,6 +290,29 @@ fn validate_display(app: &AppHandle, display: Option<&Environment>) -> Result<() #[cfg(test)] mod tests { use super::*; + use crate::scanning::Technique; + + #[test] + fn saving_mode_change_keeps_other_settings_and_reloads() { + let mut config = Config { + speed: 4, + ..Config::default() + }; + let screen = Rect { + x: 0.0, + y: 0.0, + width: 1280.0, + height: 720.0, + }; + let mut workflow = Workflow::new(config.point(), screen, 1.0).unwrap(); + workflow.handle(crate::scanning::Action::OpenMouse); + assert!(PointScan::sync_mode(&mut config, &workflow)); + assert!(!PointScan::sync_mode(&mut config, &workflow)); + let restored: Config = + serde_json::from_slice(&serde_json::to_vec(&config).unwrap()).unwrap(); + assert_eq!(restored.control_mode, crate::point_scan::ControlMode::Mouse); + assert_eq!(restored.speed, 4); + } #[test] fn typing_preserves_visuals_but_pointer_execution_hides_them() { diff --git a/src-tauri/src/point_workflow.rs b/src-tauri/src/point_workflow.rs index ccf74aaa..c56cbbae 100644 --- a/src-tauri/src/point_workflow.rs +++ b/src-tauri/src/point_workflow.rs @@ -14,7 +14,7 @@ pub enum Request { }, OpenKeyboard, OpenMouse, - CloseMouse, + OpenPoint, Keyboard(crate::scan_keyboard::Stroke), MouseMove { dx: i32, @@ -207,6 +207,9 @@ impl Workflow { pub fn mouse_open(&self) -> bool { matches!(self.stage, Stage::Mouse | Stage::MouseMoving) } + pub fn control_mode(&self) -> crate::point_scan::ControlMode { + self.point.config.control_mode + } pub fn set_mouse_area(&mut self, area: Rect, screen: Rect, scale: f64, displays: usize) { self.mouse_area = area; self.point.screen = screen; @@ -272,6 +275,7 @@ impl Workflow { } } fn open_mouse(&mut self) -> Option { + self.point.config.control_mode = crate::point_scan::ControlMode::Mouse; self.stage = Stage::Mouse; self.move_repeat = None; self.mouse = crate::scan_mouse::MousePanel::new( @@ -286,6 +290,13 @@ impl Workflow { self.return_to_mouse = false; Some(Request::OpenMouse) } + fn open_point(&mut self) -> Option { + self.point.config.control_mode = crate::point_scan::ControlMode::Point; + self.reset(); + self.stage = Stage::Point; + self.point.start(); + Some(Request::OpenPoint) + } fn mouse_key(&mut self, key: crate::scan_mouse::Key) -> Option { use crate::scan_mouse::Key; self.mouse.choose(key); @@ -345,10 +356,7 @@ impl Workflow { .with_wait_after_typing(self.point.config.keyboard_wait_after_typing); Some(Request::OpenKeyboard) } - Key::Close => { - self.reset(); - Some(Request::CloseMouse) - } + Key::Close => self.open_point(), Key::More | Key::Movement | Key::Dock => None, } } @@ -495,9 +503,14 @@ impl Technique for Workflow { } } fn start(&mut self) { + let mode = self.point.config.control_mode; self.reset(); - self.stage = Stage::Point; - self.point.start(); + if mode == crate::point_scan::ControlMode::Mouse { + let _ = self.open_mouse(); + } else { + self.stage = Stage::Point; + self.point.start(); + } } fn reset(&mut self) { self.keyboard = crate::scan_keyboard::Keyboard::configured( @@ -547,6 +560,9 @@ impl Technique for Workflow { self.stage != Stage::Executing } fn handle(&mut self, action: Action) -> Option { + if action == Action::OpenPoint { + return self.open_point(); + } if action == Action::OpenMouse { if self.mouse_open() { return None; @@ -828,6 +844,48 @@ mod tests { use super::*; use crate::{point_scan::Config, scanning::Session}; + #[test] + fn select_resumes_saved_mode_and_switching_updates_it() { + let screen = Rect { + x: 0.0, + y: 0.0, + width: 1280.0, + height: 720.0, + }; + let config = Config { + control_mode: crate::point_scan::ControlMode::Mouse, + ..Config::default() + }; + let mut session = Session::new(Workflow::new(config.point(), screen, 1.0).unwrap(), true); + assert_eq!(session.action(Action::Select), None); + assert_eq!( + session.technique.phase(), + Phase::Workflow(WorkflowPhase::Mouse) + ); + assert_eq!( + session.technique.control_mode(), + crate::point_scan::ControlMode::Mouse + ); + assert_eq!(session.action(Action::OpenPoint), Some(Request::OpenPoint)); + assert_eq!( + session.technique.control_mode(), + crate::point_scan::ControlMode::Point + ); + assert!(matches!(session.technique.phase(), Phase::Point(_))); + assert_eq!(session.action(Action::OpenMouse), Some(Request::OpenMouse)); + assert_eq!( + session.technique.mouse_key(crate::scan_mouse::Key::Close), + Some(Request::OpenPoint) + ); + assert_eq!( + session.technique.control_mode(), + crate::point_scan::ControlMode::Point + ); + assert_eq!(session.action(Action::Stop), None); + assert_eq!(session.action(Action::Select), None); + assert!(matches!(session.technique.phase(), Phase::Point(_))); + } + #[test] fn mouse_motion_stops_on_press_and_returns_to_first_row() { let screen = Rect { diff --git a/src-tauri/src/scan_executor.rs b/src-tauri/src/scan_executor.rs index ec05df4f..8b74ca7c 100644 --- a/src-tauri/src/scan_executor.rs +++ b/src-tauri/src/scan_executor.rs @@ -16,7 +16,7 @@ pub fn execute( } match request { Request::Prediction { .. } => Err("Prediction requires the scan controller.".into()), - Request::OpenKeyboard | Request::OpenMouse | Request::CloseMouse => input.release_all(), + Request::OpenKeyboard | Request::OpenMouse | Request::OpenPoint => input.release_all(), Request::MouseMove { dx, dy } => input.move_pointer_pixels(dx, dy).map(|_| ()), Request::MouseMoveAbsolute { x, y } => input.move_pointer_pixels_absolute(x, y).map(|_| ()), Request::MouseClick { right, count } => { @@ -436,7 +436,7 @@ mod tests { .unwrap(); execute(&mut input, Request::MouseScroll { dy: 5 }, true).unwrap(); execute(&mut input, Request::DragStart((10, 20)), true).unwrap(); - execute(&mut input, Request::CloseMouse, true).unwrap(); + execute(&mut input, Request::OpenPoint, true).unwrap(); assert!(!input.has_active_drag()); assert!(input.injector.events.contains(&"relative 4 -2".into())); assert!(input.injector.events.contains(&"move 40 50".into())); diff --git a/src-tauri/src/scan_mouse.rs b/src-tauri/src/scan_mouse.rs index 338d9bef..5c2cd962 100644 --- a/src-tauri/src/scan_mouse.rs +++ b/src-tauri/src/scan_mouse.rs @@ -153,7 +153,7 @@ impl MousePanel { Movement => "Movement".into(), Keyboard => "Keyboard".into(), Dock => if self.top { "Dock bottom" } else { "Dock top" }.into(), - Close => "Close mouse".into(), + Close => "Switch to Point".into(), } } pub fn frame(&self, screen: Rect, units: f64, color: ScannerColor) -> Frame { diff --git a/src-tauri/src/scanning.rs b/src-tauri/src/scanning.rs index 3318ee07..b2bbb612 100644 --- a/src-tauri/src/scanning.rs +++ b/src-tauri/src/scanning.rs @@ -15,12 +15,14 @@ pub enum Action { Stop, Cancel, OpenKeyboard, + OpenPoint, OpenMouse, } impl Action { pub fn label(self) -> &'static str { match self { Self::OpenKeyboard => "Open keyboard", + Self::OpenPoint => "Open point", Self::OpenMouse => "Open mouse", Self::Select => "Select", Self::Next => "Next", @@ -386,7 +388,10 @@ impl Session { self.reset(); return None; } - if matches!(action, Action::OpenKeyboard | Action::OpenMouse) { + if matches!( + action, + Action::OpenKeyboard | Action::OpenPoint | Action::OpenMouse + ) { let selection = self.technique.handle(action); if selection.is_some() { self.active = true; diff --git a/src-tauri/src/scanning_runtime.rs b/src-tauri/src/scanning_runtime.rs index eb0140fd..e46d89bf 100644 --- a/src-tauri/src/scanning_runtime.rs +++ b/src-tauri/src/scanning_runtime.rs @@ -19,6 +19,9 @@ pub trait Adapter: Send + Sync + 'static { fn cursor_feedback(_technique: &Self::Technique) -> Option { None } + fn sync_mode(_config: &mut Self::Config, _technique: &Self::Technique) -> bool { + false + } fn cursor_action_feedback( _request: &::Selection, ) -> Option { @@ -426,8 +429,10 @@ fn switch(app: &AppHandle, action: Action, input_generation: u64, re hide_prompt(); let result = (|| -> Result<(), String> { let mut d = c.data.lock().unwrap_or_else(|p| p.into_inner()); - if matches!(action, Action::OpenKeyboard | Action::OpenMouse) - || (d.engine.as_ref().is_none_or(|e| !e.active()) && action == Action::Select) + if matches!( + action, + Action::OpenKeyboard | Action::OpenPoint | Action::OpenMouse + ) || (d.engine.as_ref().is_none_or(|e| !e.active()) && action == Action::Select) { let (engine, display) = A::create(app, d.config.clone())?; d.engine = Some(Session::new(engine, A::switches(&d.config).automatic)); @@ -446,6 +451,21 @@ fn switch(app: &AppHandle, action: Action, input_generation: u64, re } A::validate_environment(app, d.display.as_ref())?; let point = d.engine.as_mut().and_then(|e| e.action(action)); + if let Some(mode) = d.engine.as_ref().map(|e| &e.technique) { + let mut config = d.config.clone(); + if A::sync_mode(&mut config, mode) { + let path = config_path::(app)?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + std::fs::write( + &path, + serde_json::to_vec_pretty(&config).map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string())?; + d.config = config; + } + } d.last_tick = Instant::now(); let display = d.display.clone(); drop(d); diff --git a/src-tauri/src/switch_gestures.rs b/src-tauri/src/switch_gestures.rs index a2cb6300..e85a8c65 100644 --- a/src-tauri/src/switch_gestures.rs +++ b/src-tauri/src/switch_gestures.rs @@ -34,6 +34,7 @@ impl Gestures { let mut binding = binding.clone(); if countdown && !binding.hold_actions.contains(&Action::OpenKeyboard) + && !binding.hold_actions.contains(&Action::OpenPoint) && !binding.hold_actions.contains(&Action::OpenMouse) && !matches!( binding.press_action, @@ -41,6 +42,7 @@ impl Gestures { | Action::Pause | Action::Cancel | Action::OpenKeyboard + | Action::OpenPoint | Action::OpenMouse ) { diff --git a/src-tauri/src/switches.rs b/src-tauri/src/switches.rs index fe621d94..d642ca3e 100644 --- a/src-tauri/src/switches.rs +++ b/src-tauri/src/switches.rs @@ -133,10 +133,12 @@ mod tests { assert_eq!(serde_json::to_value(&settings).unwrap(), legacy); settings.bindings[0].hold_actions.push(Action::OpenKeyboard); settings.bindings[0].hold_actions.push(Action::OpenMouse); + settings.bindings[0].hold_actions.push(Action::OpenPoint); settings.validate_actions(false).unwrap(); let updated = serde_json::to_value(&settings).unwrap(); assert_eq!(updated["bindings"][0]["holdActions"][5], "openKeyboard"); assert_eq!(updated["bindings"][0]["holdActions"][6], "openMouse"); + assert_eq!(updated["bindings"][0]["holdActions"][7], "openPoint"); assert_eq!( serde_json::from_value::(updated).unwrap(), settings diff --git a/src/App.test.tsx b/src/App.test.tsx index 0a467130..8a9434ce 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -65,7 +65,7 @@ describe("Switchify PC shell", () => { render(); await screen.findByRole("heading", { name: "Ready" }); expect(screen.getByText("1 saved · Select assigned")).toBeInTheDocument(); - expect(screen.getByText("Manual scanning · Line only")).toBeInTheDocument(); + expect(screen.getByText("Select starts Point scanning · Manual")).toBeInTheDocument(); scanning.state = { ...scanning.state!, paused: true }; act(() => receive?.({ ...structuredClone(browserState), bluetooth: "connected" })); expect(screen.getByText("Local scanning is paused while a mobile device is connected.")).toBeInTheDocument(); diff --git a/src/App.tsx b/src/App.tsx index 2433f617..1083db53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -75,7 +75,7 @@ function HomeView({ state, switches, scanning, navigate, onDisconnect, onAccessi

Input access

{state.accessibility === "required" && }

Saved switches

{saved.length} saved · {hasSelect ? "Select assigned" : "Select action needed"}

-

Scanning

{areaOptions(scanning.config, "point").automatic ? "Automatic scanning" : "Manual scanning"} · {scanning.config.mode === "line" ? "Line only" : "Grid then line"}

+

Scanning

Select starts {scanning.config.controlMode === "mouse" ? "Mouse scanning" : "Point scanning"} · {areaOptions(scanning.config, scanning.config.controlMode).automatic ? "Automatic" : "Manual"}

Mobile connection

{state.bluetooth === "connected" ? state.connectedDeviceName ?? bluetoothLabels.connected : bluetoothLabels[state.bluetooth]}

{state.bluetooth === "connected" && }
; @@ -407,7 +407,7 @@ function SetupGuide({ state, switches, suspended, busy, error, skip, finish, acc

Input access

{state.accessibility === "required" && }
} {step === 1 && } - {step === 2 &&

Use your switches

Focus the application you want to use, then press and release Select to start scanning. Select chooses a point and opens the action menu.

Automatic scanning moves the highlight for you. Use Auto scan rate for grid and menu timing, and Line speed for scanning lines. Holding a switch freezes movement; release runs the action shown.

Escape disables switch control. Assign Stop scanning or Pause / resume in Switches. Holding any switch for {switches.state ? switches.state.escapeHoldMs / 1000 : 4}s disables switch control.

Open Scanning after setup to adjust movement. Mobile connection is optional.

} + {step === 2 &&

Use your switches

Focus the application you want to use, then press and release Select. It starts the last mode you used. Point scanning chooses a screen location and an action; Mouse scanning moves a visible pointer ring and offers mouse controls.

Automatic scanning moves the highlight for you. Use Auto scan rate for grid and panel timing, and Line speed for point scanning lines. Holding a switch freezes movement; release runs the action shown.

Escape disables switch control. Assign Stop scanning or Pause / resume in Switches. Holding any switch for {switches.state ? switches.state.escapeHoldMs / 1000 : 4}s disables switch control.

Open Scanning after setup to adjust movement and switch between Point and Mouse. Mobile connection is optional.

} {step === 3 &&

Choose startup behavior

Switchify can start quietly when you sign in, ready for your switches.

} {step === 4 &&
} @@ -778,7 +778,7 @@ export function App() {

{updateNotice}

{view === "home" && void perform(api.disconnectAll)} onAccessibility={() => void perform(() => api.checkAccessibility(true))} />} {view === "switches" &&

Switches

} - {view === "scanning" &&

Scanning

} + {view === "scanning" &&

Scanning

Choose how your switches control the pointer.

} {(view === "mobile" || view === "devices" || view === "profiles") &&

Mobile

{view === "mobile" && void perform(api.disconnectAll)} />} {view === "devices" && void perform(() => api.forgetDevice(id))} />} diff --git a/src/PointScan.test.tsx b/src/PointScan.test.tsx index f0fa9f79..c3d16998 100644 --- a/src/PointScan.test.tsx +++ b/src/PointScan.test.tsx @@ -405,11 +405,12 @@ it.each([100, 250, 750, 10000])("adjusts and persists the shared interval from % })); }); -it("keeps method and speed visible while advanced choices stay collapsed", async () => { +it("shows both modes before their settings and keeps advanced panels collapsed", async () => { render(); await screen.findByText(initial.message); - expect(screen.getByRole("group", { name: "Method" })).toBeVisible(); - expect(screen.getByRole("group", { name: "Line speed" })).toBeVisible(); + expect(screen.getByRole("button", { name: "Customise point scanning" })).toBeVisible(); + expect(screen.getByRole("button", { name: "Customise mouse" })).toBeVisible(); + expect(screen.queryByRole("group", { name: "Method" })).toBeNull(); expect(screen.getByRole("checkbox", { name: "Automatic scanning" })).toBeVisible(); expect(screen.queryByRole("radio", { name: "Blue" })).toBeNull(); expect(screen.queryByRole("button", { name: "Customise keyboard" })).toBeNull(); @@ -417,5 +418,22 @@ it("keeps method and speed visible while advanced choices stay collapsed", async expect(screen.getByRole("radio", { name: "Blue" })).toBeChecked(); expect(screen.getByRole("button", { name: "Customise keyboard" })).toBeVisible(); fireEvent.click(screen.getByRole("button", { name: "More options" })); + fireEvent.click(screen.getByRole("button", { name: "Customise point scanning" })); + expect(screen.getByRole("group", { name: "Method" })).toBeVisible(); + expect(screen.getByRole("group", { name: "Line speed" })).toBeVisible(); expect(mocks.invoke.mock.calls.some(([command]) => command === "configure_point_scan")).toBe(false); }); + +it("explains the saved Mouse mode and keeps mode cards keyboard accessible", async () => { + mocks.invoke.mockImplementation((command) => command === "get_point_scan" ? Promise.resolve({ ...initial, config: { ...defaultPointScanConfig, controlMode: "mouse" } }) : Promise.resolve(initial)); + render(); + await screen.findByText(initial.message); + expect(screen.getByText("Mouse scanning", { selector: "strong" })).toBeInTheDocument(); + const mouse = screen.getByRole("button", { name: "Customise mouse" }); + mouse.focus(); + fireEvent.keyDown(mouse, { key: "Enter" }); + fireEvent.click(mouse); + expect(screen.getByRole("heading", { name: "Mouse", level: 2 })).toHaveFocus(); + fireEvent.click(screen.getByRole("button", { name: "Back to scanning settings" })); + expect(screen.getByRole("button", { name: "Customise mouse" })).toHaveFocus(); +}); diff --git a/src/scanning/useScanning.ts b/src/scanning/useScanning.ts index e725d633..94ac4e3b 100644 --- a/src/scanning/useScanning.ts +++ b/src/scanning/useScanning.ts @@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; export type ScannerColor = "red" | "green" | "blue" | "yellow" | "white"; export type PointScanConfig = { + controlMode: "point" | "mouse"; scanPreferences?: import("./preferences").ScanPreferences; wordPrediction: boolean; keyboardWaitAfterTyping: boolean; @@ -29,6 +30,7 @@ export type PointScanState = { supported: boolean; }; export const defaultPointScanConfig: PointScanConfig = { + controlMode: "point", wordPrediction: true, keyboardWaitAfterTyping: false, scannerColor: "blue", diff --git a/src/scanning/useSwitches.ts b/src/scanning/useSwitches.ts index b8229e07..18b50f5a 100644 --- a/src/scanning/useSwitches.ts +++ b/src/scanning/useSwitches.ts @@ -4,6 +4,7 @@ import { listen } from "@tauri-apps/api/event"; export const actions = { select: "Select", openKeyboard: "Open keyboard", + openPoint: "Open point", openMouse: "Open mouse", next: "Next", back: "Previous", diff --git a/src/settings/ScannerPreferences.tsx b/src/settings/ScannerPreferences.tsx index e727c8bf..f7065830 100644 --- a/src/settings/ScannerPreferences.tsx +++ b/src/settings/ScannerPreferences.tsx @@ -34,6 +34,12 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl setArea(next); }; const names = { point: 'Point scanning', menu: 'Menus', keyboard: 'Keyboard', mouse: 'Mouse' }; + const descriptions = { + point: 'Choose a place on the screen with a moving line or grid, then choose what to do there.', + mouse: 'Move the pointer with direction controls and a visible ring. Click, drag, scroll, or open the keyboard from the mouse panel.', + menu: 'Choose actions after selecting a point.', + keyboard: 'Choose keys and word suggestions when the scanning keyboard is open.', + }; const disabled = !state?.supported; const settings = config.scanPreferences ?? defaultScanPreferences; const shared = sharedOptions(config); @@ -57,11 +63,13 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl
; }; return
- {area === 'shared' ?

Scanning

:
+ {area === 'shared' ?

Two ways to control your PC

Select starts the last mode you used: {config.controlMode === 'mouse' ? 'Mouse scanning' : 'Point scanning'}. Use Open Point or Open Mouse on a switch to change modes. You can also switch from a scanned panel.

:
-

{names[area]}

Change any value to customise it. Other settings keep following your defaults.

+

{names[area]}

{descriptions[area]} Change any value to customise it; other settings follow the shared defaults.

} - {(area === 'point' || area === 'shared') && <> + {area === 'shared' &&
{(['point', 'mouse'] as const).map(key => )}
} + {area === 'mouse' &&

The ring stays visible while Mouse is open. Select a direction to move; with Repeat mouse movement on, the next switch press stops movement. Pointer speed and repeat controls are in Settings → Controls. Switch to Point returns to screen selection.

} + {area === 'point' && <> {area === 'shared' &&
-

Customise an area

Give an area different settings, or keep using your defaults.

-
{(['point', 'menu', 'keyboard', 'mouse'] as const).map(key => { +

Advanced panel settings

Menus and Keyboard can use different scan settings from the shared defaults.

+
{(['menu', 'keyboard'] as const).map(key => { const options = areaOptions(config, key); - const count = Object.entries(settings[key] ?? {}).filter(([name, value]) => value != null && !(key === 'point' && name === 'pattern')).length; + const count = Object.values(settings[key] ?? {}).filter(value => value != null).length; return