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
8 changes: 7 additions & 1 deletion app/src-tauri/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ impl Role {
}
}

pub fn masks_the_menu(self) -> bool {
self == Role::Plain
}

fn spare(self) -> &'static [&'static str] {
match self {
Role::Panel => SPARE,
Expand Down Expand Up @@ -116,7 +120,9 @@ pub fn bind<R: Runtime>(app: &AppHandle<R>, role: Role, said: &str) -> Result<()
app.global_shortcut()
.on_shortcut(wanted, move |_app, _shortcut, event| {
if event.state() == ShortcutState::Pressed {
unmenued();
if role.masks_the_menu() {
unmenued();
}
role.answer(&handle);
}
})
Expand Down
10 changes: 10 additions & 0 deletions app/src-tauri/src/keys_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,13 @@ fn settings_that_cannot_be_read_take_no_combination_beyond_the_panel() {
assert_eq!(unread.plain_shortcut, "");
assert_eq!(unread.forms_shortcut, "");
}

#[test]
fn only_the_shortcut_that_opens_no_window_keeps_the_menu_closed_by_hand() {
assert!(Role::Plain.masks_the_menu());
assert!(
!Role::Panel.masks_the_menu(),
"a window of ours takes the keys, and a typed mask would cost it the front"
);
assert!(!Role::Forms.masks_the_menu());
}
2 changes: 1 addition & 1 deletion app/src-tauri/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ fn written(app: &tauri::AppHandle, config: Config) -> Result<Config, String> {
};
crate::note::note(&format!("the new shortcut could not be taken: {why}"));
let mut back = landed;
let before = was.unwrap_or_else(crate::keys::unread);
for role in &moved {
let before = was.clone().unwrap_or_else(crate::keys::unread);
role.set(&mut back, role.of(&before).to_owned());
}
if let Err(twice) = rebound(app, &moved, &back) {
Expand Down
33 changes: 33 additions & 0 deletions app/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,39 @@ export function nearLimit(usedBytes: number, quotaMb: number) {
return quotaMb > 0 && usedBytes * 10 >= quotaMb * A_MEGABYTE * 7;
}

const ALIASES: Record<string, string> = {
control: "ctrl",
option: "alt",
command: "cmd",
super: "cmd",
meta: "cmd",
};

const EITHER = ["cmdorctrl", "commandorcontrol", "cmdorcontrol", "commandorctrl"];

function canonical(key: string, mac: boolean): string {
const bare = /^(key|digit)(.)$/.exec(key)?.[2] ?? key;
if (EITHER.includes(bare)) {
return mac ? "cmd" : "ctrl";
}
return ALIASES[bare] ?? bare;
}

function keysOf(shortcut: string, mac: boolean): string[] {
return shortcut
.split("+")
.map((one) => one.trim().toLowerCase())
.filter((one) => one.length > 0)
.map((one) => canonical(one, mac))
.sort((one, other) => one.localeCompare(other));
}

export function sameKeys(one: string, other: string, mac = onMac()): boolean {
const mine = keysOf(one, mac);
const theirs = keysOf(other, mac);
return mine.length > 0 && mine.join("+") === theirs.join("+");
}

export function combination(press: {
ctrlKey: boolean;
altKey: boolean;
Expand Down
17 changes: 16 additions & 1 deletion app/src/tests/keys.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { combination } from "../core";
import { combination, sameKeys } from "../core";

function press(code: string, held: Partial<Record<"ctrl" | "alt" | "shift" | "meta", boolean>>) {
return {
Expand Down Expand Up @@ -45,3 +45,18 @@ describe("the combination the user presses", () => {
expect(combination(press("AltLeft", { ctrl: true, alt: true }))).toBeNull();
});
});

describe("two combinations", () => {
it("are the same however they are spelled or ordered", () => {
expect(sameKeys("Shift+Alt+V", "Alt+Shift+V")).toBe(true);
expect(sameKeys("Control+Alt+P", "ctrl + alt + p")).toBe(true);
expect(sameKeys("Option+Super+1", "Alt+Cmd+1")).toBe(true);
expect(sameKeys("Ctrl+Alt+V", "Ctrl+Alt+Shift+V")).toBe(false);
expect(sameKeys("", "")).toBe(false);
expect(sameKeys("Ctrl+Alt+KeyV", "Ctrl+Alt+V")).toBe(true);
expect(sameKeys("Ctrl+Digit1", "Ctrl+1")).toBe(true);
expect(sameKeys("CommandOrControl+Alt+V", "Ctrl+Alt+V", false)).toBe(true);
expect(sameKeys("CmdOrCtrl+Alt+V", "Cmd+Alt+V", true)).toBe(true);
expect(sameKeys("CmdOrCtrl+Alt+V", "Cmd+Alt+V", false)).toBe(false);
});
});
14 changes: 11 additions & 3 deletions app/src/ui/Keys.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react";
import { asKeys, combination, type Kept, onMac, type Role, useKeys } from "../core";
import { asKeys, combination, type Kept, onMac, type Role, sameKeys, useKeys } from "../core";
import { panelKeys, t } from "../locales";
import { Band, Line } from "./Bits";

Expand Down Expand Up @@ -78,6 +78,13 @@ export default function Keys({
);
}

function refusalOf(system: boolean, repeated: boolean): "keysSystem" | "keysTwice" | null {
if (system) {
return "keysSystem";
}
return repeated ? "keysTwice" : null;
}

function Shortcut({
which: role,
says,
Expand All @@ -101,8 +108,9 @@ function Shortcut({
const taken = !asking && !off && keys !== null && !keys.bound;
const take = (one: string) => {
const others = (Object.keys(FIELD) as Role[]).filter((other) => other !== role);
const repeated = one !== "" && others.some((other) => kept[FIELD[other]] === one);
const why = SYSTEM.includes(one) ? "keysSystem" : repeated ? "keysTwice" : null;
const repeated = others.some((other) => sameKeys(one, kept[FIELD[other]] ?? ""));
const system = SYSTEM.some((reserved) => sameKeys(one, reserved));
const why = refusalOf(system, repeated);
setRefused(why);
if (why === null) {
void change({ [field]: one }).finally(() => recheck(one));
Expand Down
3 changes: 3 additions & 0 deletions crates/cp-panel/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,9 @@ impl App {
}),
self.state.borrow().ahead.clone(),
);
if !quick.draw() {
note("the paste as card is drawn on its first use instead");
}
let plain = quick.clone();
panel.on_plain_asked(move || plain.plain());
let forms = quick.clone();
Expand Down
53 changes: 46 additions & 7 deletions crates/cp-panel/src/here.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

pub use platform::{
Dragged, Listening, THUMBNAILS_FILES, Watching, activated, ahead_now, capture_insisting,
content_of, data_dir, drag_out, dress, forward, front, ground, hand_back, host, in_front,
keep_awake, media_of, ocr_available, on_foreground, open_link, open_path, ours_up_front,
paste_into, pointer, read_stuck, rebuilds_whole, sequence, system_is_light, text_in,
thumb_of_file, thumbs_dir, to_clipboard, towards, unreadable, utc_offset_at, watch_start,
worn_sequence,
content_of, data_dir, drag_out, dress, forward, front, ground, hand_back, holds_front, host,
in_front, keep_awake, media_of, ocr_available, on_foreground, open_link, open_path,
ours_up_front, paste_into, pointer, read_stuck, rebuilds_whole, screen_of, sequence,
system_is_light, text_in, thumb_of_file, thumbs_dir, to_clipboard, towards, unreadable,
utc_offset_at, watch_start, worn_sequence,
};

#[derive(Debug, Clone, Copy, PartialEq)]
Expand Down Expand Up @@ -56,6 +56,32 @@
cp_win_sys::frontmost::foreground().map_or(0, |window| window.0 as isize)
}

pub fn screen_of(handle: RawWindowHandle) -> Option<super::Pointer> {
let RawWindowHandle::Win32(win32) = handle else {
return None;
};
let spot = cp_win_sys::pointer::spot_of(win32.hwnd.get())?;
Some(super::Pointer {
at: (f64::from(spot.x), f64::from(spot.y)),
area: crate::placing::Area {
left: f64::from(spot.left),
top: f64::from(spot.top),
right: f64::from(spot.right),
bottom: f64::from(spot.bottom),
},
physical: true,
scale: spot.scale,
})

Check warning on line 74 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace platform::screen_of -> Option<super::Pointer> with None
}

pub fn holds_front(handle: RawWindowHandle) -> bool {
let RawWindowHandle::Win32(win32) = handle else {
return true;
};
cp_win_sys::frontmost::target_at(win32.hwnd.get())
.is_some_and(|ours| cp_win_sys::frontmost::foreground() == Some(ours.window))

Check warning on line 82 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace == with != in platform::holds_front

Check warning on line 82 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace platform::holds_front -> bool with false

Check warning on line 82 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace platform::holds_front -> bool with true
}

pub fn worn_sequence() -> Option<i64> {
let sequence = cp_win_sys::clipboard::sequence()?;
let ours = cp_win_sys::clipboard::holds(cp_win::restore::WORN);
Expand Down Expand Up @@ -222,11 +248,16 @@
}

pub fn forward(handle: RawWindowHandle) {
let RawWindowHandle::Win32(win32) = handle else {
return;
};
if let Some(target) = cp_win_sys::frontmost::target_at(win32.hwnd.get()) {
cp_win_sys::frontmost::bring_forward(target.window);
let Some(target) = cp_win_sys::frontmost::target_at(win32.hwnd.get()) else {
return;
};
if !cp_win_sys::frontmost::pull_forward(target.window) {

Check warning on line 257 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

delete ! in platform::forward
crate::note::note(
"Windows would not bring our window forward, so its keys stay behind",
);
}

Check warning on line 261 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace platform::forward with ()
}

Expand Down Expand Up @@ -316,6 +347,14 @@
0
}

pub fn holds_front(_handle: RawWindowHandle) -> bool {
true

Check warning on line 351 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace platform::holds_front -> bool with false
}

pub fn screen_of(_handle: RawWindowHandle) -> Option<super::Pointer> {
None

Check warning on line 355 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace platform::screen_of -> Option<super::Pointer> with Some(Default::default())
}

pub fn worn_sequence() -> Option<i64> {
None
}
Expand Down
7 changes: 7 additions & 0 deletions crates/cp-panel/src/placing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ pub fn beside(pointer: (f64, f64), area: Area, size: (f64, f64), unit: f64) -> (
)
}

pub fn kept_within(at: (f64, f64), size: (f64, f64), area: Area) -> (f64, f64) {
(
at.0.min(area.right - size.0).max(area.left),
at.1.min(area.bottom - size.1).max(area.top),
)
}

#[cfg(test)]
#[path = "placing_test.rs"]
mod tests;
26 changes: 26 additions & 0 deletions crates/cp-panel/src/placing_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,29 @@ fn a_panel_taller_than_the_screen_starts_at_its_top() {
};
assert_eq!(beside((500.0, 200.0), short, PANEL, 1.0).1, 0.0);
}

#[test]
fn a_window_that_grew_is_pushed_back_inside_without_moving_otherwise() {
let area = Area {
left: 0.0,
top: 0.0,
right: 1000.0,
bottom: 800.0,
};
assert_eq!(
kept_within((100.0, 200.0), (300.0, 200.0), area),
(100.0, 200.0)
);
assert_eq!(
kept_within((100.0, 700.0), (300.0, 200.0), area),
(100.0, 600.0)
);
assert_eq!(
kept_within((900.0, 50.0), (300.0, 200.0), area),
(700.0, 50.0)
);
assert_eq!(
kept_within((-20.0, -10.0), (300.0, 200.0), area),
(0.0, 0.0)
);
}
Loading
Loading