mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Add a Precise Cursors command and fix the misleading Caps Lock comment
General > Use Precise Cursors already worked and is saved with the other preferences. This adds a `view.preciseCursors` command that toggles that same preference, so it can be given a shortcut in Edit > Keyboard Shortcuts. It has no default shortcut and no View menu item. The comment in canvas.rs claimed Caps Lock gave precise cursors; nothing reads Caps Lock (egui has no state for it), so it now describes what the code does. Tests: for the tools whose own pointer the preference replaces (Pen, the anchor tools, Curvature, Mesh, Eyedropper, Slice, Blend, Shape Builder) the pointer is their own when off and a crosshair when on; every state of the Pen's pointer turns into a crosshair; the preference round-trips through to_json/apply_prefs; the command toggles it, updates the saved UI state, is listed in the shortcut editor without a key and can be assigned one; switching it creates no undo step and leaves the document saved; with system cursors the glyph fallback is the system crosshair either way. The new label is translated in the languages that claim complete menus. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
3ea4802a53
commit
66a0f457e9
18 files changed
+235
-1
No files matched your search
@@ -373,6 +373,134 @@ fn use_precise_cursors_makes_drawing_cursors_crosshairs() {
|
||||
assert_eq!(cursor(&mut s, "eyedropper"), Cursor::Eyedropper);
|
||||
}
|
||||
|
||||
/// Use Precise Cursors changes the pointer of the tools that have one of their own: off, each shows its own
|
||||
/// pointer; on, a crosshair. Every other drawing tool shows a crosshair either way, and the Type tool and the
|
||||
/// Scissors over no path keep theirs.
|
||||
#[test]
|
||||
fn use_precise_cursors_swaps_the_tools_own_pointers_for_a_crosshair() {
|
||||
use vectorcraft_tools::{Cursor, Mods};
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let at = vectorcraft_geom::Point::new(300.0, 300.0);
|
||||
let cursor = |s: &mut Session, tool: &str| {
|
||||
s.select_tool(tool, ViewInfo::default()).unwrap();
|
||||
s.cursor(at, Mods::default(), ViewInfo::default())
|
||||
};
|
||||
// Off: the pointer of each tool whose own pointer the preference replaces.
|
||||
let own = [
|
||||
("pen", Cursor::Pen),
|
||||
("addAnchor", Cursor::PenAdd),
|
||||
("deleteAnchor", Cursor::PenDelete),
|
||||
("anchorPoint", Cursor::Pen),
|
||||
("curvature", Cursor::Pen),
|
||||
("mesh", Cursor::PenAdd),
|
||||
("eyedropper", Cursor::Eyedropper),
|
||||
("slice", Cursor::Slice),
|
||||
("blend", Cursor::Blend),
|
||||
("shapeBuilder", Cursor::ShapeBuilder),
|
||||
];
|
||||
for (tool, c) in own {
|
||||
assert_eq!(cursor(&mut s, tool), c, "{tool} off");
|
||||
}
|
||||
// The tools that show a crosshair whatever the preference says.
|
||||
let always = [
|
||||
"lineSegment",
|
||||
"arc",
|
||||
"spiral",
|
||||
"rectangularGrid",
|
||||
"polarGrid",
|
||||
"rectangle",
|
||||
"roundedRectangle",
|
||||
"ellipse",
|
||||
"polygon",
|
||||
"star",
|
||||
"flare",
|
||||
"paintbrush",
|
||||
"blobBrush",
|
||||
"shaper",
|
||||
"pencil",
|
||||
"smooth",
|
||||
"pathEraser",
|
||||
"join",
|
||||
"eraser",
|
||||
"knife",
|
||||
"mirrorCut",
|
||||
"lineCut",
|
||||
"rectCut",
|
||||
"gradient",
|
||||
];
|
||||
for tool in always {
|
||||
assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} off");
|
||||
}
|
||||
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
|
||||
for (tool, _) in own {
|
||||
assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on");
|
||||
}
|
||||
for tool in always {
|
||||
assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on");
|
||||
}
|
||||
assert_eq!(cursor(&mut s, "type"), Cursor::Text);
|
||||
assert_eq!(cursor(&mut s, "scissors"), Cursor::NotAllowed);
|
||||
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": false})).unwrap();
|
||||
for (tool, c) in own {
|
||||
assert_eq!(cursor(&mut s, tool), c, "{tool} off again");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every state of the Pen's pointer, and the other drawing pointers, turn into a crosshair; the rest stay.
|
||||
#[test]
|
||||
fn precise_cursors_replace_every_pen_state() {
|
||||
use vectorcraft_tools::Cursor;
|
||||
for c in [
|
||||
Cursor::Pen,
|
||||
Cursor::PenAdd,
|
||||
Cursor::PenDelete,
|
||||
Cursor::PenClose,
|
||||
Cursor::PenContinue,
|
||||
Cursor::PenJoin,
|
||||
Cursor::PenConvert,
|
||||
Cursor::Eyedropper,
|
||||
Cursor::Slice,
|
||||
Cursor::Blend,
|
||||
Cursor::BlendObject,
|
||||
Cursor::BlendAnchor,
|
||||
Cursor::ShapeBuilder,
|
||||
Cursor::ShapeBuilderErase,
|
||||
] {
|
||||
assert_eq!(c.precise(), Cursor::Crosshair, "{c:?}");
|
||||
}
|
||||
for c in [Cursor::Arrow, Cursor::Move, Cursor::Text, Cursor::Hand, Cursor::NotAllowed, Cursor::SliceSelect] {
|
||||
assert_eq!(c.precise(), c, "{c:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Switching the preference is not an edit: no undo step, no unsaved changes.
|
||||
#[test]
|
||||
fn use_precise_cursors_is_not_an_edit() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let before = (s.active().unwrap().history.undo.len(), s.active().unwrap().history.redo.len());
|
||||
assert!(!s.active().unwrap().is_dirty());
|
||||
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
|
||||
assert!(s.prefs.use_precise_cursors);
|
||||
let st = s.active().unwrap();
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), before, "no undo step");
|
||||
assert!(!st.is_dirty(), "the document stays saved");
|
||||
}
|
||||
|
||||
/// The preference is saved with the others and reads back as it was.
|
||||
#[test]
|
||||
fn use_precise_cursors_is_saved_and_restored() {
|
||||
let mut s = Session::new();
|
||||
assert!(!s.prefs.use_precise_cursors, "off by default");
|
||||
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
|
||||
let saved = s.prefs.to_json();
|
||||
assert_eq!(saved["usePreciseCursors"], json!(true));
|
||||
let mut t = Session::new();
|
||||
t.apply_prefs(serde_json::from_value(saved).unwrap());
|
||||
assert!(t.prefs.use_precise_cursors);
|
||||
}
|
||||
|
||||
// ---------- #394, second batch ----------
|
||||
|
||||
fn new_doc() -> Session {
|
||||
|
||||
@@ -484,7 +484,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
{
|
||||
crate::place::paint_cursor(app, ui.ctx(), &painter, hp);
|
||||
}
|
||||
// Caps Lock gives precise (crosshair) cursors, like Illustrator; env opt-out for system cursors.
|
||||
// The glyph cursors are drawn here; General › Use Precise Cursors (the session turns the drawing tools' pointers
|
||||
// into crosshairs, `Cursor::precise`) needs no handling. VECTORCRAFT_SYSTEM_CURSORS opts out of the glyphs.
|
||||
let custom = std::env::var_os("VECTORCRAFT_SYSTEM_CURSORS").is_none();
|
||||
// User Interface › Scale Cursor Proportional to UI: the glyphs grow with UI Scaling;
|
||||
// off, they keep their size.
|
||||
@@ -2209,6 +2210,32 @@ mod tests {
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_geom::Shape as _;
|
||||
|
||||
/// With VECTORCRAFT_SYSTEM_CURSORS set the glyphs aren't drawn and the system's cursors stand in: those of
|
||||
/// the Pen and the other drawing tools are the system crosshair already, so Use Precise Cursors makes no
|
||||
/// difference to them there.
|
||||
#[test]
|
||||
fn system_cursors_show_the_same_crosshair_with_precise_cursors_on_or_off() {
|
||||
for c in [
|
||||
Cursor::Pen,
|
||||
Cursor::PenAdd,
|
||||
Cursor::PenDelete,
|
||||
Cursor::PenClose,
|
||||
Cursor::PenContinue,
|
||||
Cursor::PenJoin,
|
||||
Cursor::PenConvert,
|
||||
Cursor::Eyedropper,
|
||||
Cursor::Slice,
|
||||
Cursor::Blend,
|
||||
Cursor::BlendObject,
|
||||
Cursor::BlendAnchor,
|
||||
Cursor::ShapeBuilder,
|
||||
Cursor::ShapeBuilderErase,
|
||||
] {
|
||||
assert_eq!(cursor_icon(c), egui::CursorIcon::Crosshair, "{c:?}");
|
||||
assert_eq!(cursor_icon(c), cursor_icon(c.precise()), "{c:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #826: in a turned view each ruler still has ticks along its whole length; at 0° the top one
|
||||
/// reads x where the canvas has it.
|
||||
#[test]
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
Three Point Perspective Tříbodová perspektiva
|
||||
Save Grid as Preset… Uložit mřížku jako přednastavení…
|
||||
Snap to Pixel Přichytávat k obrazovým bodům
|
||||
Precise Cursors Přesné kurzory
|
||||
Snap to Point Přichytávat k bodům
|
||||
Snap to Glyph Přichytávat ke glyfům
|
||||
New View… Nové zobrazení…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2724,6 +2724,7 @@ effect Points Punkte
|
||||
Snap to Glyph An Glyphe ausrichten
|
||||
Snap to Grid Am Raster ausrichten
|
||||
Snap to Pixel An Pixel ausrichten
|
||||
Precise Cursors Präzise Cursor
|
||||
Snap to Point An Punkt ausrichten
|
||||
Snapping Tolerance Einrasttoleranz
|
||||
Social Square Post 1080×1080 Quadratischer Social-Media-Beitrag 1080×1080
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2774,6 +2774,7 @@
|
||||
Snap to Glyph Ajustar a glifo
|
||||
Snap to Grid Ajustar a cuadrícula
|
||||
Snap to Pixel Ajustar a píxel
|
||||
Precise Cursors Cursores precisos
|
||||
Snap to Point Ajustar a punto
|
||||
Snapping Tolerance Tolerancia de ajuste
|
||||
Social Square Post 1080×1080 Publicación cuadrada para redes 1080×1080
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2771,6 +2771,7 @@
|
||||
Snap to Glyph Magnétisme du glyphe
|
||||
Snap to Grid Magnétisme de la grille
|
||||
Snap to Pixel Magnétisme du pixel
|
||||
Precise Cursors Pointeurs précis
|
||||
Snap to Point Magnétisme du point
|
||||
Snapping Tolerance Tolérance du magnétisme
|
||||
Social Square Post 1080×1080 Publication carrée pour réseaux sociaux 1080×1080
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3358,6 +3358,7 @@
|
||||
Snap to Glyph Illesztés glifához
|
||||
Snap to Grid Illesztés a rácshoz
|
||||
Snap to Pixel Illesztés pixelhez
|
||||
Precise Cursors Pontos kurzorok
|
||||
Snap to Point Illesztés pontra
|
||||
Snapping Tolerance Illesztési tűrés
|
||||
Social Közösségi média
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2774,6 +2774,7 @@
|
||||
Snap to Glyph Aggancia a glifo
|
||||
Snap to Grid Aggancia alla griglia
|
||||
Snap to Pixel Aggancia ai pixel
|
||||
Precise Cursors Cursori di precisione
|
||||
Snap to Point Aggancia al punto
|
||||
Snapping Tolerance Tolleranza di aggancio
|
||||
Social Square Post 1080×1080 Post quadrato per social 1080×1080
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -514,6 +514,7 @@
|
||||
Three Point Perspective 3 点遠近法
|
||||
Save Grid as Preset… グリッドをプリセットとして保存…
|
||||
Snap to Pixel ピクセルにスナップ
|
||||
Precise Cursors 精密カーソル
|
||||
Snap to Point ポイントにスナップ
|
||||
Snap to Glyph グリフにスナップ
|
||||
New View… 新規表示…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -512,6 +512,7 @@
|
||||
Three Point Perspective 3점 투시
|
||||
Save Grid as Preset… 격자를 프리셋으로 저장…
|
||||
Snap to Pixel 픽셀에 스냅
|
||||
Precise Cursors 정밀 커서
|
||||
Snap to Point 포인트에 스냅
|
||||
Snap to Glyph 글리프에 스냅
|
||||
New View… 새 보기…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -489,6 +489,7 @@
|
||||
Three Point Perspective Perspektywa trzypunktowa
|
||||
Save Grid as Preset… Zapisz siatkę jako ustawienie…
|
||||
Snap to Pixel Przyciągaj do pikseli
|
||||
Precise Cursors Precyzyjne kursory
|
||||
Snap to Point Przyciągaj do punktów
|
||||
Snap to Glyph Przyciągaj do glifów
|
||||
New View… Nowy widok…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2306,6 +2306,7 @@
|
||||
Snap to Glyph Ajustar ao Glifo
|
||||
Snap to Grid Ajustar à Grade
|
||||
Snap to Pixel Ajustar ao Pixel
|
||||
Precise Cursors Cursores Precisos
|
||||
Snap to Point Ajustar ao Ponto
|
||||
Snapping Tolerance Tolerância de Ajuste
|
||||
Soft Light Luz Suave
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2770,6 +2770,7 @@
|
||||
Snap to Glyph Привязать к глифу
|
||||
Snap to Grid Привязать к сетке
|
||||
Snap to Pixel Привязать к пикселю
|
||||
Precise Cursors Точные курсоры
|
||||
Snap to Point Привязать к точке
|
||||
Snapping Tolerance Допуск привязки
|
||||
Social Square Post 1080×1080 Квадратный пост для соцсетей 1080×1080
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2767,6 +2767,7 @@ effect Points Точки
|
||||
Snap to Glyph Прив’язати до гліфу
|
||||
Snap to Grid Прив’язати до сітки
|
||||
Snap to Pixel Прив’язати до пікселя
|
||||
Precise Cursors Точні курсори
|
||||
Snap to Point Прив’язати до точки
|
||||
Snapping Tolerance Допуск прив’язки
|
||||
Social Square Post 1080×1080 Квадратний пост для соцмереж 1080×1080
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 713 and column 40.
|
@@ -3075,6 +3075,7 @@
|
||||
Snap to Glyph Hút vào glyph
|
||||
Snap to Grid Hút vào lưới
|
||||
Snap to Pixel Hút vào pixel
|
||||
Precise Cursors Con trỏ chính xác
|
||||
Snap to Point Hút vào điểm
|
||||
Snapping Tolerance Dung sai hút
|
||||
Social Square Post 1080×1080 Bài đăng vuông mạng xã hội 1080×1080
|
||||
|
||||
|
@@ -2661,6 +2661,7 @@
|
||||
Snap to Glyph 靠齐字符
|
||||
Snap to Grid 对齐网格
|
||||
Snap to Pixel 对齐像素
|
||||
Precise Cursors 精确光标
|
||||
Snap to Point 对齐点
|
||||
Snapping Tolerance 吸附容差
|
||||
Social Square Post 1080×1080 社交方形帖子 1080×1080
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2752,6 +2752,7 @@
|
||||
Snap to Glyph 靠齊字符
|
||||
Snap to Grid 靠齊格點
|
||||
Snap to Pixel 靠齊像素
|
||||
Precise Cursors 精確游標
|
||||
Snap to Point 靠齊控制點
|
||||
Snapping Tolerance 靠齊容許度
|
||||
Social Square Post 1080×1080 社群正方形貼文 1080×1080
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -186,6 +186,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
("view.trimView", "Trim View", "", "{} toggle: hide everything outside the artboards"),
|
||||
("view.cornerWidget", "Hide Corner Widget", "", "{} toggle the live corner widgets"),
|
||||
("view.snapToPixel", "Snap to Pixel", "", "{} toggle: drawing and moving land on whole pixels"),
|
||||
(
|
||||
"view.preciseCursors",
|
||||
"Precise Cursors",
|
||||
"",
|
||||
"{} toggle General › Use Precise Cursors: the drawing tools' pointers become a crosshair → true when on",
|
||||
),
|
||||
("view.textThreads", "Hide Text Threads", "Cmd+Shift+Y", "{} toggle the thread lines between threaded text frames"),
|
||||
("view.gradientAnnotator", "Hide Gradient Annotator", "Cmd+Alt+G", "{} toggle the Gradient tool's annotator"),
|
||||
("type.hiddenCharacters", "Show Hidden Characters", "Cmd+Alt+I", "{} toggle markers for spaces, paragraph ends and story ends"),
|
||||
@@ -1053,6 +1059,15 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
None => Err("no such saved selection".into()),
|
||||
},
|
||||
"view.snapToPixel" => flag(&mut app.ui.view.snap_to_pixel),
|
||||
// The preference itself (General › Use Precise Cursors), so the command, the dialog and the saved
|
||||
// preferences share one value.
|
||||
"view.preciseCursors" => {
|
||||
let on = !app.session.prefs.use_precise_cursors;
|
||||
app.run("prefs.set", json!({"key": "usePreciseCursors", "value": on})).map(|_| {
|
||||
crate::prefs_dialog::snapshot(app);
|
||||
json!(on)
|
||||
})
|
||||
}
|
||||
"view.textThreads" => flag(&mut app.ui.view.text_threads),
|
||||
"type.hiddenCharacters" => flag(&mut app.ui.view.hidden_chars),
|
||||
"type.bold" => crate::panels::character::toggle_face(app, Face::Bold),
|
||||
@@ -3993,6 +4008,55 @@ mod tests {
|
||||
assert_eq!(dynamic_label(&app, "view.cornerWidget", ""), "Show Corner Widget");
|
||||
}
|
||||
|
||||
/// View › Precise Cursors has no menu item or default key, but the command (assignable in Keyboard Shortcuts)
|
||||
/// writes General › Use Precise Cursors, the value the dialog and the saved preferences read.
|
||||
#[test]
|
||||
fn precise_cursors_command_toggles_the_preference() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
assert!(!app.session.prefs.use_precise_cursors);
|
||||
assert_eq!(app.run("view.preciseCursors", json!({})).unwrap(), json!(true));
|
||||
assert!(app.session.prefs.use_precise_cursors);
|
||||
assert_eq!(app.ui.engine_prefs["usePreciseCursors"], json!(true), "saved with the UI state");
|
||||
assert!(UI_COMMANDS.iter().any(|c| c.0 == "view.preciseCursors" && c.2.is_empty()), "assignable, no default key");
|
||||
assert_eq!(app.run("view.preciseCursors", json!({})).unwrap(), json!(false));
|
||||
assert!(!app.session.prefs.use_precise_cursors);
|
||||
}
|
||||
|
||||
/// The command is listed in Edit › Keyboard Shortcuts without a key and takes one the user types; with the
|
||||
/// key it is the command that runs.
|
||||
#[test]
|
||||
fn precise_cursors_command_is_listed_and_assignable_in_the_shortcut_editor() {
|
||||
use crate::shortcut_editor as ed;
|
||||
let e = ed::entry("view.preciseCursors").expect("listed in the editor");
|
||||
assert_eq!((e.label.as_str(), e.group.as_str(), e.is_tool), ("Precise Cursors", "View", false));
|
||||
assert_eq!(ed::default_of("view.preciseCursors"), None, "no default key");
|
||||
let mut overrides = std::collections::BTreeMap::new();
|
||||
assert_eq!(ed::effective_in(&overrides, "view.preciseCursors"), None);
|
||||
assert_eq!(ed::assign(&mut overrides, "view.preciseCursors", Some("Cmd+Alt+Shift+U"), false), Ok(vec![]));
|
||||
let chord = ed::effective_in(&overrides, "view.preciseCursors").expect("assigned");
|
||||
assert_eq!(ed::normalize(&chord), ed::normalize("Cmd+Alt+Shift+U"));
|
||||
// A key another command holds is refused, so the new command can't take it over silently.
|
||||
assert!(ed::assign(&mut overrides, "view.preciseCursors", Some("Cmd+Alt+Shift+P"), false).is_err());
|
||||
assert_eq!(ed::assign(&mut overrides, "view.preciseCursors", None, false), Ok(vec![]));
|
||||
assert_eq!(ed::effective_in(&overrides, "view.preciseCursors"), None, "cleared again");
|
||||
}
|
||||
|
||||
/// The command is a preference, not an edit: no undo step, no unsaved changes, with a document open.
|
||||
#[test]
|
||||
fn precise_cursors_command_is_not_an_edit() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let steps = |app: &VectorcraftApp| {
|
||||
let st = app.session.active().unwrap();
|
||||
(st.history.undo.len(), st.history.redo.len(), st.is_dirty())
|
||||
};
|
||||
let before = steps(&app);
|
||||
assert!(!before.2);
|
||||
app.run("view.preciseCursors", json!({})).unwrap();
|
||||
assert!(app.session.prefs.use_precise_cursors);
|
||||
assert_eq!(steps(&app), before);
|
||||
}
|
||||
|
||||
/// Selection & Anchor Display › Zoom to Selection (#394): Zoom In and Zoom Out bring the
|
||||
/// selection to the middle; off, they keep the view's centre.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user