diff --git a/src/PointScan.test.tsx b/src/PointScan.test.tsx
index 57daaac6..b0a68ff7 100644
--- a/src/PointScan.test.tsx
+++ b/src/PointScan.test.tsx
@@ -11,6 +11,7 @@ import {
defaultPointScanConfig,
type PointScanState,
} from "./scanning/useScanning";
+import { defaultScanPreferences } from "./scanning/preferences";
import { ScanningSection } from "./settings/ScanningSection";
function more() {
const button = screen.getByRole("button", { name: "More options" });
@@ -391,16 +392,88 @@ it("saves the shared panel pointer-avoidance choice from the mouse and keyboard
expect(screen.getByRole("checkbox", { name: "Move away from the pointer" })).toBeChecked();
});
-it("saves the keyboard after-typing choice and preserves it while manual", async () => {
+it("shows what each scanner does after a selection and saves a choice for one", async () => {
mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan" ? initial : { ...initial, config: args.config });
render();
await screen.findByText(initial.message);
- expect(screen.queryByText("After typing")).not.toBeInTheDocument();
+ const pressed = (name: string) => screen.getByRole("button", { name }).getAttribute("aria-pressed") === "true";
+ more();
+ expect(screen.getAllByRole("button", { name: "Usual for each scanner" }).map(button => button.getAttribute("aria-pressed"))).toEqual(["true", "true"]);
+ fireEvent.click(screen.getByRole("button", { name: "Customise menus" }));
+ more();
+ expect(screen.queryByRole("button", { name: "Usual for each scanner" })).not.toBeInTheDocument();
+ expect([pressed("Keep scanning"), pressed("Where I selected")]).toEqual([true, true]);
+ fireEvent.click(screen.getByRole("button", { name: /Back to scanning settings/ }));
+ fireEvent.click(screen.getByRole("button", { name: "Customise point scanning" }));
+ more();
+ expect(pressed("Wait for Select")).toBe(true);
+ expect(screen.queryByText("Start again from")).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Keep scanning" }));
+ await waitFor(() => expect(mocks.invoke).toHaveBeenLastCalledWith("configure_point_scan", { config: { ...defaultPointScanConfig, scanPreferences: { ...defaultScanPreferences, point: { nextScan: "automatic" } } } }));
+ fireEvent.click(screen.getByRole("button", { name: "Use default for after a selection" }));
+ await waitFor(() => expect(pressed("Wait for Select")).toBe(true));
+ fireEvent.click(screen.getByRole("button", { name: /Back to scanning settings/ }));
+ fireEvent.click(screen.getByRole("button", { name: "Customise mouse scanning" }));
+ more();
+ expect([pressed("Keep scanning"), pressed("The beginning")]).toEqual([true, true]);
+ fireEvent.click(screen.getByRole("button", { name: "Where I selected" }));
+ await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.mouse).toEqual({ startFrom: "selection" }));
+});
+
+it("saves a choice for every scanner and shows it in each", async () => {
+ mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan"
+ ? { ...initial, config: { ...initial.config, automatic: false, keyboardWaitAfterTyping: true } } : { ...initial, config: args.config });
+ render();
+ await screen.findByText(initial.message);
+ more();
+ // Point scanning and scanners with their own setting still use it.
+ expect(screen.getByRole("button", { name: "Keep scanning" })).toBeEnabled();
+ expect(screen.getByText(/The keyboard waits, as saved earlier/)).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Keep scanning" }));
+ await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.nextScan).toBe("automatic"));
+ expect(screen.queryByText(/The keyboard waits, as saved earlier/)).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Where I selected" }));
+ await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.startFrom).toBe("selection"));
+ fireEvent.click(screen.getByRole("button", { name: "Customise keyboard" }));
+ more();
+ expect(screen.getByRole("button", { name: "Keep scanning" })).toHaveAttribute("aria-pressed", "true");
+ expect(screen.getByRole("button", { name: "Keep scanning" })).toBeDisabled();
+ expect(screen.getByRole("button", { name: "Where I selected" })).toHaveAttribute("aria-pressed", "true");
+ expect(screen.getByRole("button", { name: "Where I selected" })).toBeEnabled();
+ fireEvent.click(screen.getByRole("button", { name: "The beginning" }));
+ await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({ startFrom: "beginning" }));
+ fireEvent.click(screen.getByRole("button", { name: "Reset scanning overrides" }));
+ await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({}));
+ expect(screen.getByRole("button", { name: "Where I selected" })).toHaveAttribute("aria-pressed", "true");
+});
+
+it("shows a keyboard set back to standard as it will behave", async () => {
+ const saved = { ...initial, config: { ...initial.config, keyboardWaitAfterTyping: true, scanPreferences: { ...defaultScanPreferences, keyboard: { nextScan: "standard" as const } } } };
+ mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan" ? saved : { ...saved, config: args.config });
+ render();
+ await screen.findByText(initial.message);
more();
fireEvent.click(screen.getByRole("button", { name: "Customise keyboard" }));
- expect(screen.getByRole("button", { name: "Continue scanning" })).toHaveAttribute("aria-pressed", "true");
+ more();
+ expect(screen.getByRole("button", { name: "Keep scanning" })).toHaveAttribute("aria-pressed", "true");
+});
+
+it("keeps a saved wait after typing as the keyboard's usual behaviour", async () => {
+ const saved = { ...initial, config: { ...initial.config, keyboardWaitAfterTyping: true } };
+ mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan" ? saved : { ...saved, config: args.config });
+ render();
+ await screen.findByText(initial.message);
+ more();
+ fireEvent.click(screen.getByRole("button", { name: "Customise keyboard" }));
+ more();
+ expect(screen.queryByRole("group", { name: "After typing" })).not.toBeInTheDocument();
+ expect(screen.getByText(/saved earlier as After typing/)).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Wait for Select" })).toHaveAttribute("aria-pressed", "true");
+ fireEvent.click(screen.getByRole("button", { name: "Keep scanning" }));
+ await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({ nextScan: "automatic" }));
+ expect(screen.getByRole("button", { name: "Keep scanning" })).toHaveAttribute("aria-pressed", "true");
fireEvent.click(screen.getByRole("button", { name: "Wait for Select" }));
- await waitFor(() => expect(mocks.invoke).toHaveBeenLastCalledWith("configure_point_scan", { config: { ...defaultPointScanConfig, keyboardWaitAfterTyping: true } }));
+ await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({ nextScan: "wait" }));
fireEvent.click(screen.getByRole("checkbox", { name: "Automatic scanning" }));
await waitFor(() => expect(screen.getByRole("button", { name: "Wait for Select" })).toBeDisabled());
expect(screen.getByRole("button", { name: "Wait for Select" })).toHaveAttribute("aria-pressed", "true");
diff --git a/src/scanning/preferences.ts b/src/scanning/preferences.ts
index 5212a596..c1bcd958 100644
--- a/src/scanning/preferences.ts
+++ b/src/scanning/preferences.ts
@@ -8,16 +8,33 @@ export type ScanOptions = {
pattern: 'grouped' | 'linear';
color: ScannerColor;
thickness: 'thin' | 'standard' | 'thick';
+ /** `standard` is what each scanner has always done. */
+ nextScan: 'standard' | 'automatic' | 'wait';
+ startFrom: 'standard' | 'beginning' | 'selection';
};
-export type ScanPreferences = Pick & Record>;
+export type ScanPreferences = Pick
+ & Partial> & Record>;
export const defaultScanPreferences: ScanPreferences = {
direction: 'forward', passLimit: 3, pattern: 'grouped', thickness: 'standard',
point: {}, menu: {}, keyboard: {}, mouse: {},
};
+/** Wait after typing, saved before the choice existed for every scanner, is still in effect. */
+export function savedKeyboardWait(config: PointScanConfig) {
+ const settings = config.scanPreferences ?? defaultScanPreferences;
+ return config.keyboardWaitAfterTyping && settings.keyboard?.nextScan == null && (settings.nextScan ?? 'standard') === 'standard';
+}
+/** What a scanner does after a selection when nothing has been chosen for it. */
+export function usualAfterSelection(config: PointScanConfig, area: ScanArea): Pick {
+ return {
+ nextScan: area === 'point' || (area === 'keyboard' && savedKeyboardWait(config)) ? 'wait' : 'automatic',
+ startFrom: area === 'menu' ? 'selection' : 'beginning',
+ };
+}
export function sharedOptions(config: PointScanConfig): ScanOptions {
const settings = config.scanPreferences ?? defaultScanPreferences;
return { automatic: config.automatic, intervalMs: config.blockIntervalMs, color: config.scannerColor,
- direction: settings.direction, passLimit: settings.passLimit, pattern: settings.pattern, thickness: settings.thickness };
+ direction: settings.direction, passLimit: settings.passLimit, pattern: settings.pattern, thickness: settings.thickness,
+ nextScan: settings.nextScan ?? 'standard', startFrom: settings.startFrom ?? 'standard' };
}
export function areaOptions(config: PointScanConfig, area: ScanArea): ScanOptions {
const shared = sharedOptions(config);
diff --git a/src/settings/ScannerPreferences.tsx b/src/settings/ScannerPreferences.tsx
index 671fa77b..91bf5372 100644
--- a/src/settings/ScannerPreferences.tsx
+++ b/src/settings/ScannerPreferences.tsx
@@ -2,7 +2,7 @@ import { Button, Input, Select, MoreOptions } from "../ui/controls";
import { useLayoutEffect, useRef, useState, type ReactNode } from 'react';
import { ChevronRight } from 'lucide-react';
import type { ScanningController, ScannerColor, PointScanConfig } from '../scanning/useScanning';
-import { areaOptions, sharedOptions, defaultScanPreferences, type ScanArea, type ScanOptions } from '../scanning/preferences';
+import { areaOptions, sharedOptions, defaultScanPreferences, usualAfterSelection, savedKeyboardWait, type ScanArea, type ScanOptions } from '../scanning/preferences';
import { SettingGroup, Toggle, OptionGroup } from './controls';
export function ScannerPreferences({ controller }: { controller: ScanningController }) {
@@ -45,6 +45,11 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl
const settings = config.scanPreferences ?? defaultScanPreferences;
const shared = sharedOptions(config);
const effective = area === 'shared' ? shared : areaOptions(config, area);
+ // A scanner shows what it will do, so its usual behaviour stands in for "standard".
+ const usual = area === 'shared' ? null : usualAfterSelection(config, area);
+ const nextScan = effective.nextScan === 'standard' && usual ? usual.nextScan : effective.nextScan;
+ const startFrom = effective.startFrom === 'standard' && usual ? usual.startFrom : effective.startFrom;
+ const usualOption = area === 'shared' ? [{ value: 'standard' as const, label: 'Usual for each scanner' }] : [];
const change = (key: K, value: ScanOptions[K] | undefined) => {
if (area !== 'shared') {
const overrides = { ...(settings[area] ?? {}), [key]: value };
@@ -141,10 +146,6 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl
}
{field('automatic', 'Automatic scanning', locked => change('automatic', value)} />)}
- {area === 'keyboard' && legend="After typing" disabled={disabled || !effective.automatic} value={config.keyboardWaitAfterTyping ? 'wait' : 'continue'}
- options={[{ value: 'continue', label: 'Continue scanning' }, { value: 'wait', label: 'Wait for Select' }]}
- onChange={value => update('keyboardWaitAfterTyping', value === 'wait')}
- note={{ summary: effective.automatic ? 'After typing a key or suggestion, wait for Select before scanning again.' : 'Used in automatic keyboard scanning. Your choice is kept while scanning manually.' }} />}
{field('intervalMs', 'Auto scan rate', locked => <>
Auto scan rate
@@ -155,6 +156,17 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl
>)}
+ {field('nextScan', 'After a selection', locked => legend="After a selection" columns={area === 'shared' ? 'three' : undefined} disabled={locked || (area !== 'shared' && area !== 'point' && !effective.automatic)} value={nextScan}
+ options={[...usualOption, { value: 'automatic', label: 'Keep scanning' }, { value: 'wait', label: 'Wait for Select' }]} onChange={value => change('nextScan', value)}
+ note={{ summary: area === 'point' ? 'After a click, the next point scan starts by itself or waits for Select. Scanning still stops at the pass limit.'
+ : area !== 'shared' && !effective.automatic ? 'Used in automatic scanning. Your choice is kept while scanning manually.'
+ : area === 'shared' ? 'After a selection that does something, such as typing a key or clicking, scanning starts again by itself or waits for Select. Usually, scanning waits after a click and starts again after anything else.' + (savedKeyboardWait(config) ? ' The keyboard waits, as saved earlier.' : '') + ' Waiting applies to scanners that scan automatically.'
+ : 'After a selection that does something, such as typing a key or scrolling, scanning starts again by itself or waits for Select.' + (area === 'keyboard' && savedKeyboardWait(config) ? ' Waiting is the default here because it was saved earlier as After typing.' : '') }} />)}
+ {area !== 'point' && field('startFrom', 'Start again from', locked => legend="Start again from" columns={area === 'shared' ? 'three' : undefined} disabled={locked} value={startFrom}
+ options={[...usualOption, { value: 'beginning', label: 'The beginning' }, { value: 'selection', label: 'Where I selected' }]} onChange={value => change('startFrom', value)}
+ note={{ summary: area === 'shared' ? 'Where scanning starts after a selection that does something. Usually, menus stay where you selected, and the keyboard and mouse panel return to the beginning. Point scanning always starts from the beginning.'
+ : area === 'keyboard' ? 'Where scanning starts after typing a key. After a suggestion, or when changing page, it starts from the beginning.'
+ : 'Where scanning starts after a selection that does something. Opening another page or menu starts from its beginning.' }} />)}
{field('direction', 'Initial direction', locked => legend="Initial direction" disabled={locked} value={effective.direction} options={[{ value: 'forward', label: 'Forward' }, { value: 'reverse', label: 'Reverse' }]} onChange={value => change('direction', value)} />)}
{field('passLimit', 'Pass limit', locked => legend="Pass limit" disabled={locked} value={effective.passLimit} options={[1, 2, 3, 5, 0].map(value => ({ value, label: value ? `${value} ${value === 1 ? 'pass' : 'passes'}` : 'Unlimited' }))} onChange={value => change('passLimit', value)}
note={{ summary: 'After this many automatic passes, scanning waits for Select. Unlimited keeps scanning until you pause or stop.' }} />)}