mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
Allow toolbar tool shortcuts to be remapped (#4194)
Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
10 files changed
+221
-8
No files matched your search
@@ -199,6 +199,7 @@ mod tool_cursor;
|
||||
pub mod tool_feedback;
|
||||
pub(crate) mod tool_memory;
|
||||
mod tool_reset;
|
||||
mod tool_shortcuts;
|
||||
pub mod transform_fx;
|
||||
pub mod transform_tex;
|
||||
pub mod transform_tool;
|
||||
|
||||
@@ -161,6 +161,10 @@ pub fn invoke(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, params: Va
|
||||
|
||||
/// [`invoke`] without the unsaved-changes prompt, for once the user has already answered it.
|
||||
pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, params: Value) -> Result<Value, String> {
|
||||
if let Some(tool) = crate::tool_shortcuts::tool(id) {
|
||||
app.ui.tool = tool;
|
||||
return Ok(json!({"tool": tool}));
|
||||
}
|
||||
if id == "ui.symmetryTransform" {
|
||||
return crate::symmetry_ui::begin(app).map(|_| Value::Null);
|
||||
}
|
||||
@@ -660,6 +664,9 @@ fn open_path(app: &mut PhotocraftApp, path: &str) -> Result<Value, String> {
|
||||
}
|
||||
|
||||
pub fn is_enabled(app: &PhotocraftApp, id: &str) -> bool {
|
||||
if crate::tool_shortcuts::tool(id).is_some() {
|
||||
return true;
|
||||
}
|
||||
if id == "ui.symmetryTransform" {
|
||||
return crate::symmetry_ui::can_transform(app);
|
||||
}
|
||||
@@ -890,6 +897,7 @@ pub struct MenuItem {
|
||||
pub fn is_live(id: &str) -> bool {
|
||||
static UI_IDS: std::sync::OnceLock<std::collections::HashSet<&'static str>> = std::sync::OnceLock::new();
|
||||
photocraft_engine::commands::find(id).is_some()
|
||||
|| crate::tool_shortcuts::tool(id).is_some()
|
||||
|| UI_IDS.get_or_init(|| UI_COMMANDS.iter().map(|c| c.0).collect()).contains(id)
|
||||
|| panel_alias(id).is_some()
|
||||
|| workspace_name(id).is_some()
|
||||
|
||||
@@ -157,8 +157,10 @@ pub fn toolbar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
let key = egui::Id::new(("tool-slot", *slot_index));
|
||||
let tool = slot_tool(ui, app.ui.tool, slot, key);
|
||||
let sel = slot.contains(&app.ui.tool);
|
||||
let tip =
|
||||
if tool.key() == '\0' { tl!(tool.label()).to_string() } else { format!("{} ({})", tl!(tool.label()), tool.key()) };
|
||||
let tip = match crate::tool_shortcuts::shortcut(app, tool) {
|
||||
Some(sc) => format!("{} ({})", tl!(tool.label()), crate::shortcuts::pretty(&sc)),
|
||||
None => tl!(tool.label()).to_string(),
|
||||
};
|
||||
let resp =
|
||||
icons::button_with_icon_size_tip(ui, icons::tool_icon(tool), bx, toolbar_icon_size(bx, t.pro), sel, &tip, tool_tips);
|
||||
if slot.len() > 1 {
|
||||
@@ -247,11 +249,11 @@ pub fn toolbar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
egui::FontId::proportional(12.5),
|
||||
t.text,
|
||||
);
|
||||
if item.key() != '\0' {
|
||||
if let Some(sc) = crate::tool_shortcuts::shortcut(app, item) {
|
||||
ui.painter().text(
|
||||
pos2(r.right() - 8.0, r.center().y),
|
||||
Align2::RIGHT_CENTER,
|
||||
item.key().to_string(),
|
||||
crate::shortcuts::pretty(&sc),
|
||||
egui::FontId::proportional(12.0),
|
||||
t.text_dim,
|
||||
);
|
||||
|
||||
@@ -765,6 +765,11 @@ pub fn shortcut_items(app: &PhotocraftApp) -> Vec<(String, String, Vec<String>,
|
||||
let at = out.iter().rposition(|i| i.2.first().map(String::as_str) == Some("Layer")).map_or(out.len(), |i| i + 1);
|
||||
out.splice(at..at, layer);
|
||||
out.extend(tools);
|
||||
out.extend(
|
||||
crate::state::Tool::ALL
|
||||
.into_iter()
|
||||
.map(|tool| (crate::tool_shortcuts::id(tool), tool.label().to_string(), vec!["Tools".into()], crate::tool_shortcuts::default(tool))),
|
||||
);
|
||||
for (id, label, def) in prefs::TEMPORARY_TOOLS {
|
||||
if seen.insert(id.to_string()) {
|
||||
out.push((id.to_string(), label.to_string(), vec!["Tools".into(), "Temporary".into()], Some(def.to_string())));
|
||||
@@ -1670,6 +1675,9 @@ fn shortcuts_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String
|
||||
let mut last_top = String::new();
|
||||
egui::Grid::new("shortcut-grid").num_columns(3).spacing([12.0, 3.0]).striped(true).show(ui, |ui| {
|
||||
for (id, label, path, def) in &items {
|
||||
if tab == 1 && crate::tool_shortcuts::tool(id).is_some() {
|
||||
continue;
|
||||
}
|
||||
let cur = eff(&overrides, id, def);
|
||||
let (shown_path, shown_label) = shown(id, label, path);
|
||||
// Match what is shown and the English name (commands are documented in English).
|
||||
|
||||
@@ -32,6 +32,94 @@ fn field(h: &Harness<'_, PhotocraftApp>, id: u64, name: &str) -> Value {
|
||||
h.state().ui.dialogs.iter().find(|d| d.id == id).unwrap().fields.get(name).cloned().unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolbar_shortcut_can_be_captured_and_restored_on_restart() {
|
||||
let (mut h, id) = dialog(1.0, "tools.select.pencil", "Pencil Tool");
|
||||
h.get_by_role_and_label(egui::accesskit::Role::Button, "B").click();
|
||||
h.run_steps(2);
|
||||
h.key_press(Key::F6);
|
||||
h.run_steps(2);
|
||||
assert_eq!(field(&h, id, "overrides")["tools.select.pencil"], json!("F6"));
|
||||
crate::dialogs::confirm(h.state_mut(), id).unwrap();
|
||||
h.run_steps(2);
|
||||
h.key_press(Key::F6);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().ui.tool, crate::state::Tool::Pencil);
|
||||
|
||||
let saved = h.state().session.prefs_to_json();
|
||||
let mut session = photocraft_engine::Session::new();
|
||||
session.load_prefs_json(&saved).unwrap();
|
||||
let mut restarted = PhotocraftApp::new(session, crate::Services::default());
|
||||
let ctx = egui::Context::default();
|
||||
ctx.begin_pass(egui::RawInput {
|
||||
events: vec![Event::Key { key: Key::F6, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }],
|
||||
..Default::default()
|
||||
});
|
||||
crate::shortcuts::handle(&mut restarted, &ctx);
|
||||
ctx.end_pass().textures_delta.clear();
|
||||
assert_eq!(restarted.ui.tool, crate::state::Tool::Pencil);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolbar_shortcut_overrides_remove_old_keys_and_skip_removed_group_members() {
|
||||
use crate::state::Tool;
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
let ctx = egui::Context::default();
|
||||
let press = |app: &mut PhotocraftApp, key, modifiers| {
|
||||
ctx.begin_pass(egui::RawInput {
|
||||
// A tap: press and release, so the next press isn't a held key's repeat.
|
||||
events: vec![
|
||||
Event::ModifiersChanged(modifiers),
|
||||
Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers },
|
||||
Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers },
|
||||
],
|
||||
..Default::default()
|
||||
});
|
||||
crate::shortcuts::handle(app, &ctx);
|
||||
ctx.end_pass().textures_delta.clear();
|
||||
};
|
||||
app.run("edit.keyboardShortcuts", json!({"set": {"tools.select.move": "F6", "tools.select.pencil": ""}, "allowUnknown": true})).unwrap();
|
||||
app.ui.tool = Tool::Zoom;
|
||||
press(&mut app, Key::V, Modifiers::NONE);
|
||||
assert_eq!(app.ui.tool, Tool::Artboard, "the old key only selects unchanged tools");
|
||||
press(&mut app, Key::F6, Modifiers::NONE);
|
||||
assert_eq!(app.ui.tool, Tool::Move);
|
||||
|
||||
app.ui.tool = Tool::Pencil;
|
||||
press(&mut app, Key::V, Modifiers::NONE);
|
||||
press(&mut app, Key::B, Modifiers::NONE);
|
||||
assert_eq!(app.ui.tool, Tool::Brush, "a removed tool cannot return through group memory");
|
||||
press(&mut app, Key::B, Modifiers::SHIFT);
|
||||
assert_eq!(app.ui.tool, Tool::MixerBrush, "Shift cycling skips a removed tool");
|
||||
|
||||
app.run("edit.keyboardShortcuts", json!({"reset": true})).unwrap();
|
||||
app.ui.tool = Tool::Brush;
|
||||
press(&mut app, Key::B, Modifiers::SHIFT);
|
||||
assert_eq!(app.ui.tool, Tool::Pencil, "reset restores the default group");
|
||||
app.session.edit_prefs(|p| p.tools.use_shift_key_for_tool_switch = false);
|
||||
press(&mut app, Key::B, Modifiers::NONE);
|
||||
assert_eq!(app.ui.tool, Tool::MixerBrush, "plain-key cycling still follows the preference");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolbar_shortcuts_participate_in_conflict_resolution() {
|
||||
let (mut h, id) = dialog(1.0, "edit.search", "search…");
|
||||
capture(&mut h, &crate::shortcuts::pretty("Cmd+F"), Key::ControlLeft, Modifiers::COMMAND, Key::B);
|
||||
// Remove Ctrl from the captured binding: a plain B belongs to the Brush group by default.
|
||||
h.state_mut().ui.dialog_mut(id).unwrap().fields["overrides"]["edit.search"] = json!("B");
|
||||
crate::dialogs::confirm(h.state_mut(), id).unwrap();
|
||||
for tool in ["brush", "pencil", "mixerBrush", "colorReplacement"] {
|
||||
assert_eq!(h.state().session.prefs().shortcuts.get(&format!("tools.select.{tool}")).map(String::as_str), Some(""));
|
||||
}
|
||||
let ctx = h.ctx.clone();
|
||||
h.state_mut().ui.tool = crate::state::Tool::Move;
|
||||
key(&mut h, Key::B, true, Modifiers::NONE);
|
||||
h.run_steps(2);
|
||||
assert!(h.state().ui.palette_open, "B now runs Search");
|
||||
assert_eq!(h.state().ui.tool, crate::state::Tool::Move);
|
||||
assert_eq!(crate::shortcut_dispatch::take_log(&ctx).last().map(|i| i.0.as_str()), Some("edit.search"));
|
||||
}
|
||||
|
||||
/// Click the selected command's shortcut button, then press `modifier` and `k` like a keyboard:
|
||||
/// the modifier key goes down first (its own key event), then the key with the modifier held.
|
||||
fn capture(h: &mut Harness<'_, PhotocraftApp>, button: &str, modifier: Key, held: Modifiers, k: Key) {
|
||||
|
||||
@@ -302,6 +302,7 @@ fn label(id: &str) -> String {
|
||||
.map(|c| c.1)
|
||||
.or_else(|| crate::menus::UI_COMMANDS.iter().find(|c| c.0 == id).map(|c| c.1))
|
||||
.or_else(|| photocraft_engine::commands::find(id).map(|c| c.label))
|
||||
.or_else(|| crate::tool_shortcuts::tool(id).map(|tool| tool.label()))
|
||||
.unwrap_or(id)
|
||||
.trim_end_matches('…')
|
||||
.to_string()
|
||||
|
||||
@@ -108,6 +108,7 @@ pub fn default_shortcut(id: &str) -> Option<String> {
|
||||
.or_else(|| crate::menu_catalog::CATALOG.iter().find(|c| c.3 == id).and_then(|c| c.2))
|
||||
.or_else(|| photocraft_engine::prefs::TEMPORARY_TOOLS.iter().find(|t| t.0 == id).map(|t| t.2))
|
||||
.map(str::to_string)
|
||||
.or_else(|| crate::tool_shortcuts::tool(id).and_then(crate::tool_shortcuts::default))
|
||||
}
|
||||
|
||||
/// With ⇧ held the OS reports the shifted character as the logical key (US layout): ⇧; is `:`.
|
||||
@@ -580,15 +581,18 @@ pub fn handle(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
ctx.input_mut(|i| i.events.retain(|e| !matches!(e, egui::Event::Key { key, pressed: true, repeat: true, .. } if crate::spring_tools::is_tool_key(*key))));
|
||||
let shift_switch = app.session.prefs().tools.use_shift_key_for_tool_switch;
|
||||
for t in Tool::ALL {
|
||||
if !crate::tool_shortcuts::uses_default(app, t) {
|
||||
continue;
|
||||
}
|
||||
let Some(k) = crate::spring_tools::key_of(t.key()) else { continue };
|
||||
let cycle_shift = shift_switch && ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, k));
|
||||
if cycle_shift || pressed(k) {
|
||||
let before = app.ui.tool;
|
||||
let group: Vec<Tool> = Tool::ALL.iter().copied().filter(|x| x.key() == t.key()).collect();
|
||||
let group: Vec<Tool> = Tool::ALL.iter().copied().filter(|x| x.key() == t.key() && crate::tool_shortcuts::uses_default(app, *x)).collect();
|
||||
app.ui.tool = match group.iter().position(|x| *x == app.ui.tool) {
|
||||
Some(i) if shift_switch && !cycle_shift => group[i],
|
||||
Some(i) => group[(i + 1) % group.len()],
|
||||
None => app.ui.group_tool(t.key()).unwrap_or(group[0]),
|
||||
None => app.ui.group_tool(t.key()).filter(|last| group.contains(last)).unwrap_or(group[0]),
|
||||
};
|
||||
// `consume_key` ignores extra ⇧ and ⌥: a ⇧/⌥ switch is never spring-loaded.
|
||||
let (plain, now) = ctx.input(|i| (!cycle_shift && !i.modifiers.shift && !i.modifiers.alt, i.time));
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//! Toolbar tools in Edit › Keyboard Shortcuts (#4086). Explicit assignments select an
|
||||
//! individual tool; unchanged tools keep Photoshop's shared keys and Shift cycling.
|
||||
|
||||
use crate::{PhotocraftApp, state::Tool};
|
||||
|
||||
pub(crate) fn id(tool: Tool) -> String {
|
||||
let mut name = format!("{tool:?}");
|
||||
if let Some(first) = name.get_mut(..1) {
|
||||
first.make_ascii_lowercase();
|
||||
}
|
||||
format!("tools.select.{name}")
|
||||
}
|
||||
|
||||
pub(crate) fn tool(id: &str) -> Option<Tool> {
|
||||
let name = id.strip_prefix("tools.select.")?;
|
||||
let tool = Tool::from_name(name)?;
|
||||
(id == self::id(tool)).then_some(tool)
|
||||
}
|
||||
|
||||
pub(crate) fn default(tool: Tool) -> Option<String> {
|
||||
(tool.key() != '\0').then(|| tool.key().to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn shortcut(app: &PhotocraftApp, tool: Tool) -> Option<String> {
|
||||
crate::shortcuts::effective_shortcut(app, &id(tool), default(tool).as_deref())
|
||||
}
|
||||
|
||||
pub(crate) fn uses_default(app: &PhotocraftApp, tool: Tool) -> bool {
|
||||
!app.session.prefs().shortcuts.contains_key(&id(tool))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn every_toolbar_tool_has_a_unique_live_shortcut_row() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
let items = crate::prefs_ui::shortcut_items(&app);
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
for t in Tool::ALL {
|
||||
let id = id(t);
|
||||
assert!(ids.insert(id.clone()));
|
||||
assert_eq!(tool(&id), Some(t));
|
||||
assert!(crate::menus::is_live(&id));
|
||||
assert!(crate::menus::is_enabled(&app, &id));
|
||||
let rows: Vec<_> = items.iter().filter(|i| i.0 == id).collect();
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0].1, t.label());
|
||||
assert_eq!(rows[0].2, ["Tools"]);
|
||||
assert_eq!(rows[0].3, default(t));
|
||||
assert_eq!(crate::shortcuts::default_shortcut(&id), default(t));
|
||||
crate::menus::invoke(&mut app, &ctx, &id, json!({})).unwrap();
|
||||
assert_eq!(app.ui.tool, t);
|
||||
}
|
||||
for id in ["tools.select.", "tools.select.missing", "tools.select.Brush"] {
|
||||
assert!(!crate::menus::is_live(id));
|
||||
assert!(crate::menus::invoke(&mut app, &ctx, id, json!({})).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolbar_hints_follow_assignments_removal_and_reset() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
assert_eq!(shortcut(&app, Tool::Pencil).as_deref(), Some("B"));
|
||||
assert_eq!(shortcut(&app, Tool::SingleRowMarquee), None);
|
||||
app.run("prefs.set", json!({"path": "shortcuts.tools.select.pencil", "value": "F6"})).unwrap();
|
||||
assert_eq!(shortcut(&app, Tool::Pencil).as_deref(), Some("F6"));
|
||||
app.run("prefs.set", json!({"path": "shortcuts.tools.select.pencil", "value": ""})).unwrap();
|
||||
assert_eq!(shortcut(&app, Tool::Pencil), None);
|
||||
app.run("prefs.reset", json!({"path": "shortcuts.tools.select.pencil"})).unwrap();
|
||||
assert_eq!(shortcut(&app, Tool::Pencil).as_deref(), Some("B"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_remapped_tool_letter_does_not_steal_text_input() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
app.run("prefs.set", json!({"path": "shortcuts.tools.select.pencil", "value": "Q"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().build_ui_state(
|
||||
|ui, app: &mut PhotocraftApp| {
|
||||
ui.text_edit_singleline(&mut app.ui.status).request_focus();
|
||||
crate::shortcuts::handle(app, ui.ctx());
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
assert!(h.ctx.text_edit_focused());
|
||||
h.key_press(egui::Key::Q);
|
||||
h.event(egui::Event::Text("q".into()));
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().ui.tool, Tool::Brush);
|
||||
assert_eq!(h.state().ui.status, "q");
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
# Where PhotoCraft falls short of Photoshop
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed) · **Target:** Adobe Photoshop 2026
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed) · **Target:** Adobe Photoshop 2026
|
||||
|
||||
Every known shortfall against Photoshop 2026 (27.11.0), one entry each, ranked by user impact
|
||||
on the way to beta. This is the work list agents pick from: take the highest-ranked entry that
|
||||
@@ -170,6 +170,8 @@ what an agent can't finish alone.
|
||||
- **Estimate:** 60–110 h.
|
||||
|
||||
### G9. Panels, preferences and workspace
|
||||
- **Progress (2026-10-12):** toolbar tool shortcuts can be reassigned, removed and reset (#4086),
|
||||
with saved bindings, matching toolbar hints and regression coverage (`ui-egui/src/tool_shortcuts.rs`).
|
||||
- **Missing:** 27 of 156 preferences do nothing (scorecard prefs audit); panel groups can't
|
||||
float or tear off into OS windows (UI-217-5, #1828, #1213, #2305); Color panel lacks wheel and
|
||||
slider modes (UI-217-17, #2569); more themes (UI-217-11, #2661); layer clipboard
|
||||
@@ -240,6 +242,7 @@ what an agent can't finish alone.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | G9: toolbar tool shortcut editing (#4086); the remaining workspace gaps stay open |
|
||||
| 2026-10-12 | minor | G1: #2465 (CS2 `PlLd`-only smart objects) marked fixed |
|
||||
| 2026-10-12 | minor | G5: spring-loaded tool letters done (#2672) |
|
||||
| 2026-10-12 | minor | G10: DPX read and write (#1049); Cineon remains open |
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# UI parity: tools, handles, modifiers and feel
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (⇧-dragged guides land on ruler ticks; layer and selection keys; tool keys; spring-loaded tool letters; type formatting keys; Pen Auto Add/Delete, #4144) · **Target:** Adobe Photoshop 2026
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (toolbar tool shortcuts can be reassigned; ⇧-dragged guides land on ruler ticks; layer and selection keys; tool keys; spring-loaded tool letters; type formatting keys; Pen Auto Add/Delete, #4144) · **Target:** Adobe Photoshop 2026
|
||||
|
||||
How PhotoCraft's direct manipulation compares with Photoshop 2026 (27.11.0): the toolbox, the
|
||||
handles and modifier keys of each gesture, nudging, snapping, numeric precision, navigation,
|
||||
@@ -183,6 +183,7 @@ Triangle and Curvature Pen (minus legacy Rounded Rectangle and Targeted Adjustme
|
||||
| Shortcut audit | 214 → 0 failures (2026-10-07) | measured then; not re-run here |
|
||||
| `.kys` import | done (`engine/src/kys.rs`) | |
|
||||
| Shortcut glyphs per platform | done (UI-217-19) | |
|
||||
| Toolbar tool shortcut editing | done (#4086): each tool has a row under Tools; assignments persist, removed tools leave the default key group, and unchanged tools keep Shift cycling. Toolbar and flyout hints follow the binding | `tool_shortcuts.rs`, `shortcut_capture_tests.rs` |
|
||||
| Brush size/hardness keys on non-US layouts | done (#3109): `[` `]` need AltGr on many layouts, so Photoshop uses other keys there (German: Ö / #, ⇧ for hardness). `layout_keys.rs` has a data table of such layouts (`LAYOUTS`, add rows to support one more) and a fallback for all others: the keys at the US `[` / `]` positions act as `[` / `]` when they don't type brackets. Off once a real bracket is typed; Edit › Keyboard Shortcuts overrides still apply | |
|
||||
| Layer and selection keys: ⌘, toggles visibility, ⌘\ targets the mask, / locks transparency, ⌘⌥2 loads luminosity, ⌘⌥⌫ fills from history, Tab / ⇧Tab rename the next layer, ⌥-click a thumbnail fits the layer | done (2026-10-11) | |
|
||||
| Tool keys: ← / → turn the brush tip (1°, ⇧ 15°), ⇧-click with Type starts new text over old, Space+F / ⇧Space+F cycle the pasteboard colour | done (2026-10-11) | |
|
||||
@@ -224,3 +225,4 @@ Paragraph; the rest open as in-app floating windows.
|
||||
| 2026-10-11 | minor | Spring-loaded tool letters done (#2672): holding any tool letter (Z included) past 300 ms, or using the canvas while it is down, makes the tool temporary until release |
|
||||
| 2026-10-11 | minor | Type formatting keys done: ⇧⌘K, U, B, I and /, ⌥↑/↓, ⇧⌥↑/↓, ⌥←/→ over a selection and ⌥⇧⌘H format the selected type while editing, and no longer reach Color Settings, Desaturate, Auto Color or Inverse |
|
||||
| 2026-10-12 | minor | Pen Auto Add/Delete on the active path, with its options-bar checkbox, hover feedback and + / − cursor badge (#4144) |
|
||||
| 2026-10-12 | minor | Toolbar tool shortcuts can be reassigned, removed and reset in Keyboard Shortcuts (#4086); capture, restart, group cycling, conflict and text-focus regressions |
|
||||
Reference in new issue
Block a user