mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
.kys import: tool keys and sets saved on Windows (K3)
Photoshop's <tool> rows now import: each matches our tool by name (our tool names are Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own becomes its override, while its own letter keeps the tool cycling with the others (#5036). A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that would also mean Control on a Mac. The import reports `windows`.
This commit is contained in:
1 parent
c63c91a206
commit
2924e5c4ee
5 files changed
+204
-13
No files matched your search
+140
-6
@@ -12,9 +12,11 @@
|
||||
//! candidate (the app lists them in Photoshop's menu order). A row's first shortcut becomes the
|
||||
//! command's main key, unless one of its keys is already the command's default (Zoom In lists
|
||||
//! `Cmd++` before `Cmd+=`); its other keys become the command's alternates (#3665: Copy is
|
||||
//! `Cmd+C` and `F3`, and importing it must not leave `Cmd+C` unbound). Tool keys are not
|
||||
//! imported yet (#5036). [`write`] saves a set the same way, with our command id on each row
|
||||
//! (`pc-id`, which Photoshop ignores), so PhotoCraft reads its own sets back exactly.
|
||||
//! `Cmd+C` and `F3`, and importing it must not leave `Cmd+C` unbound). Tool keys match our
|
||||
//! tools by name ([`plan_tools`]; the app knows the tools). A set saved on Windows spells the
|
||||
//! primary modifier Ctrl where a Mac set says Cmd ([`KysSet::windows`], [`to_ours`]).
|
||||
//! [`write`] saves a set the same way, with our command id on each row (`pc-id`, which
|
||||
//! Photoshop ignores), so PhotoCraft reads its own sets back exactly.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
@@ -40,6 +42,11 @@ pub struct KysSet {
|
||||
pub name: Option<String>,
|
||||
pub commands: Vec<KysCommand>,
|
||||
pub tool_keys: usize,
|
||||
/// The `<tool>` rows with a key: the tool's name, its key, our id in sets PhotoCraft wrote.
|
||||
pub tools: Vec<KysCommand>,
|
||||
/// Saved by Photoshop on Windows: no key spells ⌘ or Opt, and Ctrl is the primary modifier,
|
||||
/// so `Ctrl` here is our `Cmd` (a Mac set's `Control` stays the Control key). See [`to_ours`].
|
||||
pub windows: bool,
|
||||
}
|
||||
|
||||
/// Parse a `.kys` file (or Photoshop's live `Keyboard Shortcuts.psp`, the same XML). Rows
|
||||
@@ -70,13 +77,58 @@ pub fn parse(text: &str) -> Result<KysSet, String> {
|
||||
set.commands.push(KysCommand { name: name.to_string(), shortcuts, id });
|
||||
}
|
||||
}
|
||||
"tool" if node.text().is_some_and(|t| !t.trim().is_empty()) => set.tool_keys += 1,
|
||||
"tool" => {
|
||||
let key = node.text().map(str::trim).filter(|t| !t.is_empty());
|
||||
let name = node.attribute("name").map(str::trim).filter(|n| !n.is_empty());
|
||||
if let Some(key) = key {
|
||||
set.tool_keys += 1;
|
||||
if let Some(name) = name {
|
||||
let id = node.attribute(PC_ID).map(str::trim).filter(|i| !i.is_empty()).map(str::to_string);
|
||||
set.tools.push(KysCommand { name: name.to_string(), shortcuts: vec![key.to_string()], id });
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
set.windows = windows_set(&set);
|
||||
Ok(set)
|
||||
}
|
||||
|
||||
/// The modifier words of `sc` (`Ctrl+Alt+K` → Ctrl, Alt), whatever the key is.
|
||||
fn modifiers(sc: &str) -> impl Iterator<Item = String> + '_ {
|
||||
let mods = sc.strip_suffix("++").unwrap_or_else(|| sc.rsplit_once('+').map_or("", |(m, _)| m));
|
||||
mods.split('+').filter(|m| !m.is_empty()).map(|m| m.trim().to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// Photoshop on Windows writes Ctrl for the primary modifier, where a Mac set writes Cmd (and
|
||||
/// Control for the Control key): a set with no ⌘ or Opt anywhere but some Ctrl is a Windows set.
|
||||
fn windows_set(set: &KysSet) -> bool {
|
||||
let keys = || set.commands.iter().flat_map(|c| c.shortcuts.iter());
|
||||
let mac = keys().flat_map(|k| modifiers(k).collect::<Vec<_>>()).any(|m| matches!(m.as_str(), "cmd" | "command" | "opt" | "option"));
|
||||
let ctrl = keys().flat_map(|k| modifiers(k).collect::<Vec<_>>()).any(|m| matches!(m.as_str(), "ctrl" | "control"));
|
||||
!mac && ctrl
|
||||
}
|
||||
|
||||
/// A key of `set` in our notation, normalised: on a Windows set, Ctrl (or Control) is our `Cmd`
|
||||
/// (Ctrl on Windows and Linux, ⌘ on the Mac), so `Ctrl+L` is the `Cmd+L` default rather than a
|
||||
/// literal Control key.
|
||||
pub fn to_ours(set: &KysSet, sc: &str) -> Option<String> {
|
||||
if !set.windows {
|
||||
return normalize_shortcut(sc);
|
||||
}
|
||||
let (mods, key) = match sc.strip_suffix("++") {
|
||||
Some(m) => (m, "+"),
|
||||
None => sc.rsplit_once('+').unwrap_or(("", sc)),
|
||||
};
|
||||
let mods: Vec<&str> = mods
|
||||
.split('+')
|
||||
.filter(|m| !m.is_empty())
|
||||
.map(|m| if matches!(m.trim().to_ascii_lowercase().as_str(), "ctrl" | "control") { "Cmd" } else { m })
|
||||
.collect();
|
||||
normalize_shortcut(&mods.into_iter().chain(std::iter::once(key)).collect::<Vec<_>>().join("+"))
|
||||
}
|
||||
|
||||
/// What an import would change.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Plan {
|
||||
@@ -137,12 +189,12 @@ pub fn plan(candidates: impl IntoIterator<Item = Candidate>, set: &KysSet) -> Pl
|
||||
plan.unknown.push(row.name.clone());
|
||||
continue;
|
||||
};
|
||||
let Some(sc) = row.shortcuts.first().and_then(|s| normalize_shortcut(s)) else {
|
||||
let Some(sc) = row.shortcuts.first().and_then(|s| to_ours(set, s)) else {
|
||||
plan.unreadable.push(row.name.clone());
|
||||
continue;
|
||||
};
|
||||
plan.alternates += row.shortcuts.len().saturating_sub(1);
|
||||
let imported: Vec<String> = row.shortcuts.iter().filter_map(|s| normalize_shortcut(s)).collect();
|
||||
let imported: Vec<String> = row.shortcuts.iter().filter_map(|s| to_ours(set, s)).collect();
|
||||
let pick = candidates
|
||||
.iter()
|
||||
.find(|(_, d)| d.as_deref().and_then(normalize_shortcut).is_some_and(|d| imported.contains(&d)))
|
||||
@@ -166,6 +218,30 @@ pub fn plan(candidates: impl IntoIterator<Item = Candidate>, set: &KysSet) -> Pl
|
||||
plan
|
||||
}
|
||||
|
||||
/// The tool keys of `set` matched to `tools` (our tool id and Photoshop's name for it, which is
|
||||
/// ours too: "Move Tool"): tool id → key (a single letter, upper case), and the names with no
|
||||
/// tool here. A row PhotoCraft wrote names the tool by id. A key that isn't one letter is skipped.
|
||||
pub fn plan_tools<'a>(tools: impl IntoIterator<Item = (&'a str, &'a str)>, set: &KysSet) -> (BTreeMap<String, String>, Vec<String>) {
|
||||
let tools: Vec<(&str, &str)> = tools.into_iter().collect();
|
||||
let mut keys = BTreeMap::new();
|
||||
let mut unknown = Vec::new();
|
||||
for row in &set.tools {
|
||||
let found =
|
||||
row.id.as_deref().and_then(|id| tools.iter().find(|(t, _)| *t == id)).or_else(|| tools.iter().find(|(_, name)| key(name) == key(&row.name)));
|
||||
let Some((id, _)) = found else {
|
||||
unknown.push(row.name.clone());
|
||||
continue;
|
||||
};
|
||||
let mut letters = row.shortcuts.first().map(|k| k.trim()).unwrap_or_default().chars();
|
||||
if let (Some(c), None) = (letters.next(), letters.next())
|
||||
&& c.is_ascii_alphanumeric()
|
||||
{
|
||||
keys.insert((*id).to_string(), c.to_ascii_uppercase().to_string());
|
||||
}
|
||||
}
|
||||
(keys, unknown)
|
||||
}
|
||||
|
||||
/// The attribute naming our command id on the rows PhotoCraft writes (Photoshop ignores it).
|
||||
pub const PC_ID: &str = "pc-id";
|
||||
|
||||
@@ -321,4 +397,62 @@ mod tests {
|
||||
plain.commands[1].id = None;
|
||||
assert!(plan(cands(), &plain).set.contains_key("y.levels"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_windows_set_means_cmd_by_ctrl_and_a_mac_set_keeps_control() {
|
||||
let win = "<photoshop-keyboard-shortcuts version=\"4\">
|
||||
\t<command name=\"Levels...\"><shortcut>Ctrl+L</shortcut></command>
|
||||
\t<command name=\"Gaussian Blur...\"><shortcut>Ctrl+Alt+Shift+G</shortcut></command>
|
||||
\t<command name=\"Zoom In\"><shortcut>Ctrl++</shortcut><shortcut>Ctrl+=</shortcut></command>
|
||||
</photoshop-keyboard-shortcuts>";
|
||||
let set = parse(win).unwrap();
|
||||
assert!(set.windows);
|
||||
assert_eq!(to_ours(&set, "Ctrl+L").as_deref(), Some("Cmd+L"));
|
||||
assert_eq!(to_ours(&set, "Ctrl+Alt+Shift+G").as_deref(), normalize_shortcut("Cmd+Alt+Shift+G").as_deref());
|
||||
assert_eq!(to_ours(&set, "Ctrl++").as_deref(), Some("Cmd++"));
|
||||
assert_eq!(to_ours(&set, "F3").as_deref(), Some("F3"));
|
||||
let cands = vec![Candidate { id: "image.levels".into(), label: "Levels…".into(), default: Some("Cmd+L".into()) }];
|
||||
assert_eq!(plan(cands, &set).set.get("image.levels").map(String::as_str), Some("Cmd+L"), "the default, not a Control override");
|
||||
// A Mac set spells ⌘ Cmd: its Control is the Control key.
|
||||
let mac = parse(
|
||||
"<photoshop-keyboard-shortcuts version=\"4\"><command name=\"Levels...\"><shortcut>Cmd+L</shortcut></command><command name=\"X\"><shortcut>Control+Opt+Cmd+G</shortcut></command><command name=\"Y\"><shortcut>Control+K</shortcut></command></photoshop-keyboard-shortcuts>",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!mac.windows);
|
||||
assert_eq!(to_ours(&mac, "Control+K"), normalize_shortcut("Ctrl+K"));
|
||||
// No modifiers at all: nothing to tell, nothing to change.
|
||||
let plain = parse("<photoshop-keyboard-shortcuts><command name=\"Copy\"><shortcut>F3</shortcut></command></photoshop-keyboard-shortcuts>").unwrap();
|
||||
assert!(!plain.windows);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_keys_match_by_name_or_id_and_take_one_letter() {
|
||||
let xml = "<photoshop-keyboard-shortcuts version=\"4\">
|
||||
\t<tool name=\"Move Tool\" type=\"1\">V</tool>
|
||||
\t<tool name=\"pencil tool\" type=\"1\">n</tool>
|
||||
\t<tool name=\"Renamed\" pc-id=\"tools.select.brush\">B</tool>
|
||||
\t<tool name=\"Imaginary Tool\">Q</tool>
|
||||
\t<tool name=\"Lasso Tool\">Shift+L</tool>
|
||||
\t<tool name=\"Default Colors\"></tool>
|
||||
\t<tool>Z</tool>
|
||||
</photoshop-keyboard-shortcuts>";
|
||||
let set = parse(xml).unwrap();
|
||||
assert_eq!((set.tool_keys, set.tools.len()), (6, 5), "a key without a name counts but can't be matched");
|
||||
let tools = [
|
||||
("tools.select.move", "Move Tool"),
|
||||
("tools.select.pencil", "Pencil Tool"),
|
||||
("tools.select.brush", "Brush Tool"),
|
||||
("tools.select.lasso", "Lasso Tool"),
|
||||
];
|
||||
let (keys, unknown) = plan_tools(tools, &set);
|
||||
assert_eq!(keys.get("tools.select.move").map(String::as_str), Some("V"));
|
||||
assert_eq!(keys.get("tools.select.pencil").map(String::as_str), Some("N"), "case-insensitive, upper-cased");
|
||||
assert_eq!(keys.get("tools.select.brush").map(String::as_str), Some("B"), "by pc-id");
|
||||
assert!(!keys.contains_key("tools.select.lasso"), "a tool key is one letter");
|
||||
assert_eq!(unknown, vec!["Imaginary Tool".to_string()]);
|
||||
// Our own export reads back by id.
|
||||
let out = write("T", &[Entry { id: "tools.select.pencil".into(), label: "Pencil Tool".into(), tool: true, keys: vec!["F".into()] }]);
|
||||
let (keys, _) = plan_tools(tools, &parse(&out).unwrap());
|
||||
assert_eq!(keys.get("tools.select.pencil").map(String::as_str), Some("F"));
|
||||
}
|
||||
}
|
||||
@@ -1892,6 +1892,7 @@ fn keyboard_shortcuts(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"alternates": plan.alternates,
|
||||
"alternateKeys": plan.alternate_keys,
|
||||
"toolKeys": set.tool_keys,
|
||||
"windows": set.windows,
|
||||
}));
|
||||
let mut with_import = p.clone();
|
||||
with_import["set"] = Value::Object(merged);
|
||||
|
||||
@@ -32,12 +32,20 @@ pub fn plan(app: &PhotocraftApp, set: &KysSet) -> Plan {
|
||||
photocraft_engine::kys::plan(menu.chain(specs), set)
|
||||
}
|
||||
|
||||
/// The set's tool keys matched to our tools by name (Photoshop's names are ours): tool
|
||||
/// shortcut id (`tools.select.move`) → letter, and the tools PhotoCraft doesn't have.
|
||||
pub fn plan_tools(set: &KysSet) -> (BTreeMap<String, String>, Vec<String>) {
|
||||
let tools: Vec<(String, &str)> = crate::state::Tool::ALL.into_iter().map(|t| (crate::tool_shortcuts::id(t), t.label())).collect();
|
||||
photocraft_engine::kys::plan_tools(tools.iter().map(|(id, name)| (id.as_str(), *name)), set)
|
||||
}
|
||||
|
||||
/// Put the plan into the open Keyboard Shortcuts dialog (opening one when none is), as
|
||||
/// overrides the OK button applies: a key equal to the command's default clears its override.
|
||||
/// Returns the dialog id and the plan as JSON.
|
||||
pub fn import_text(app: &mut PhotocraftApp, file_name: &str, text: &str) -> Result<Value, String> {
|
||||
let set = parse(text).map_err(|e| format!("{file_name}: {e}"))?;
|
||||
let plan = plan(app, &set);
|
||||
let (tool_keys, unknown_tools) = plan_tools(&set);
|
||||
let id = match app.ui.dialogs.iter().find(|d| d.fields.get("__prefsui").and_then(Value::as_str) == Some("shortcuts")) {
|
||||
Some(d) => d.id,
|
||||
None => crate::prefs_ui::open_shortcuts(app, 0),
|
||||
@@ -52,6 +60,17 @@ pub fn import_text(app: &mut PhotocraftApp, file_name: &str, text: &str) -> Resu
|
||||
overrides.insert(cmd.clone(), sc.clone());
|
||||
}
|
||||
}
|
||||
// Tool keys (#5036): a letter other than the tool's own becomes its override; its own
|
||||
// letter clears one, so the tool keeps cycling with the others that share it.
|
||||
for tool in crate::state::Tool::ALL {
|
||||
let id = crate::tool_shortcuts::id(tool);
|
||||
let Some(key) = tool_keys.get(&id) else { continue };
|
||||
if crate::tool_shortcuts::default(tool).is_some_and(|d| d.eq_ignore_ascii_case(key)) {
|
||||
overrides.remove(&id);
|
||||
} else {
|
||||
overrides.insert(id, key.clone());
|
||||
}
|
||||
}
|
||||
// A row's other keys are the command's further keys (#3665): Copy's F3 beside ⌘C.
|
||||
let mut alternates: BTreeMap<String, Vec<String>> = f.get("alternates").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
|
||||
for (cmd, keys) in &plan.alternate_keys {
|
||||
@@ -63,10 +82,10 @@ pub fn import_text(app: &mut PhotocraftApp, file_name: &str, text: &str) -> Resu
|
||||
}
|
||||
}
|
||||
let shown = std::path::Path::new(file_name).file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| file_name.to_string());
|
||||
let n = plan.set.len().to_string();
|
||||
let n = (plan.set.len() + tool_keys.len()).to_string();
|
||||
let mut message = crate::i18n::fmt(tl!("Imported {n} shortcuts from {file}. Click OK to keep them."), &[("n", &n), ("file", &shown)]);
|
||||
if !plan.unknown.is_empty() {
|
||||
let u = plan.unknown.len().to_string();
|
||||
if !plan.unknown.is_empty() || !unknown_tools.is_empty() {
|
||||
let u = (plan.unknown.len() + unknown_tools.len()).to_string();
|
||||
message.push(' ');
|
||||
message.push_str(&crate::i18n::fmt(tl!("{n} commands are not in PhotoCraft."), &[("n", &u)]));
|
||||
}
|
||||
@@ -87,6 +106,9 @@ pub fn import_text(app: &mut PhotocraftApp, file_name: &str, text: &str) -> Resu
|
||||
"alternates": plan.alternates,
|
||||
"alternateKeys": plan.alternate_keys,
|
||||
"toolKeys": set.tool_keys,
|
||||
"tools": tool_keys,
|
||||
"unknownTools": unknown_tools,
|
||||
"windows": set.windows,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -104,8 +104,12 @@ fn rows_match_our_commands_by_label_with_photoshop_tie_breaks() {
|
||||
#[test]
|
||||
fn an_unreadable_shortcut_is_reported_not_imported() {
|
||||
let app = app();
|
||||
let set =
|
||||
super::KysSet { name: None, commands: vec![KysCommand { name: "Undo".into(), shortcuts: vec!["Cmd+Shift+Ctrl".into()], id: None }], tool_keys: 0 };
|
||||
let set = super::KysSet {
|
||||
name: None,
|
||||
commands: vec![KysCommand { name: "Undo".into(), shortcuts: vec!["Cmd+Shift+Ctrl".into()], id: None }],
|
||||
tool_keys: 0,
|
||||
..Default::default()
|
||||
};
|
||||
let p = plan(&app, &set);
|
||||
assert!(p.set.is_empty());
|
||||
assert_eq!(p.unreadable, vec!["Undo".to_string()]);
|
||||
@@ -144,7 +148,9 @@ fn import_button_fills_the_dialog_and_ok_applies_the_set() {
|
||||
assert!(overrides.get("select.all").is_none(), "already the default: {overrides}");
|
||||
assert!(overrides.get("view.zoomIn").is_none(), "Cmd+= is among the keys, so the default stays: {overrides}");
|
||||
let message = field(&h, "message");
|
||||
assert_eq!(message, json!("Imported 9 shortcuts from texcuts.kys. Click OK to keep them. 2 commands are not in PhotoCraft."));
|
||||
// 9 commands and the Move Tool's V (its own letter: no override).
|
||||
assert_eq!(message, json!("Imported 10 shortcuts from texcuts.kys. Click OK to keep them. 2 commands are not in PhotoCraft."));
|
||||
assert!(overrides.get("tools.select.move").is_none(), "{overrides}");
|
||||
assert_eq!(field(&h, "capture"), json!(false));
|
||||
assert_eq!(field(&h, "messageWarning"), json!(false), "an import clears the earlier shortcut conflict warning");
|
||||
// Nothing is applied until OK.
|
||||
@@ -321,3 +327,31 @@ fn an_oversized_file_is_refused() {
|
||||
assert!(e.contains("larger than 4 MiB"), "{e}");
|
||||
assert!(app.ui.dialogs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_windows_set_with_tool_keys_imports_as_photoshop_meant_it() {
|
||||
// Photoshop on Windows writes Ctrl for the primary modifier; the tool rows give letters.
|
||||
let set = "<photoshop-keyboard-shortcuts version=\"4\">
|
||||
\t<command kind=\"static\" name=\"Copy\" id=\"103\"><shortcut>Ctrl+C</shortcut><shortcut>F3</shortcut></command>
|
||||
\t<command kind=\"static\" name=\"Levels...\" id=\"1801\"><shortcut>Ctrl+Shift+Alt+L</shortcut></command>
|
||||
\t<tool name=\"Move Tool\" type=\"1\">V</tool>
|
||||
\t<tool name=\"Pencil Tool\" type=\"1\">N</tool>
|
||||
\t<tool name=\"Imaginary Tool\" type=\"1\">Q</tool>
|
||||
</photoshop-keyboard-shortcuts>";
|
||||
let mut app = app();
|
||||
let r = super::import_text(&mut app, "win.kys", set).unwrap();
|
||||
assert_eq!((r["windows"].as_bool(), r["unknownTools"].clone()), (Some(true), json!(["Imaginary Tool"])));
|
||||
assert_eq!(r["tools"], json!({"tools.select.move": "V", "tools.select.pencil": "N"}));
|
||||
let id = r["dialog"].as_u64().unwrap();
|
||||
let f = &app.ui.dialog_mut(id).unwrap().fields;
|
||||
assert!(f["overrides"].get("edit.copy").is_none(), "Ctrl+C on Windows is the default ⌘C: {}", f["overrides"]);
|
||||
assert!(f["overrides"].get("tools.select.move").is_none(), "Move's own V keeps it cycling");
|
||||
assert_eq!(f["overrides"]["tools.select.pencil"], json!("N"));
|
||||
let levels = f["overrides"].as_object().unwrap().iter().find(|(id, _)| id.contains("levels")).map(|(_, v)| v.clone());
|
||||
assert_eq!(
|
||||
levels.as_ref().and_then(Value::as_str).and_then(photocraft_engine::prefs::normalize_shortcut),
|
||||
photocraft_engine::prefs::normalize_shortcut("Cmd+Alt+Shift+L")
|
||||
);
|
||||
crate::dialogs::confirm(&mut app, id).unwrap();
|
||||
assert_eq!(crate::tool_shortcuts::shortcut(&app, crate::state::Tool::Pencil).as_deref(), Some("N"));
|
||||
}
|
||||
@@ -227,7 +227,7 @@ the same commands work in the app, the CLI and headless MCP. Paths are dotted ca
|
||||
| `prefs.set` | `{"path":"cursors.painting","value":"precise"}` or `{"values":{"unitsAndRulers.rulers":"cm","guidesGridAndSlices.gridColor":"#ff8800"}}`. Values are validated (choices, ranges, `#rrggbb` colours, shortcut syntax); a batch applies all or nothing. A section path takes an object and merges it key by key |
|
||||
| `prefs.reset` | `{"path":"performance"}` (a section or key); no path resets everything. An individual `shortcuts.<id>` or `menus.colors.<id>` reset removes the override and returns `null`, including when it is already absent |
|
||||
| `edit.preferences.<section>` | the section's values; in the app (no params) it opens the Preferences dialog on that section |
|
||||
| `edit.keyboardShortcuts` | `{"set":{"edit.fill":"Cmd+Shift+F"},"reset":true\|["id",…],"removeConflicts":true,"filter":"blur","list":false,"importKys":"<.kys XML>"}`: returns overrides, matching commands and conflicts. A shortcut moved to another command is removed from its old owner unless `removeConflicts` is false. `""` removes a shortcut, `null` restores the default. The held temporary tools are bindable too: `tools.temporary.hand` (Space; also repositions a selection being drawn), `tools.temporary.zoomIn` (Cmd+Space), `tools.temporary.zoomOut` (Alt+Space; Alt added to a held Zoom In, Cmd+Alt+Space, zooms out too), `tools.temporary.erase` (`` ` ``; with the Brush or Pencil, a stroke started while it is held is in Clear mode, `paint.stroke`/`paint.pencil` `"mode": "clear"`, erasing with that tool's brush); they list with `"hold": true`. ⌘ (Ctrl off the Mac) held alone is the Move tool with the painting, retouching and other non-selection tools, as in Photoshop; it is a modifier, not a binding. `importKys` applies a Photoshop `.kys` set (the file's XML text, at most 4 MiB): rows are matched to commands by menu label, a key equal to the default restores it, keys also given in `set` win, tool keys are not imported, and the result gains `import` (`name`, `imported`, `set`, `unknown`, `unreadable`, `alternates`, `toolKeys`). In the app it opens Keyboard Shortcuts and Menus, whose Import Shortcuts… button loads a `.kys` set into the dialog (opening or dropping a `.kys`, or Photoshop's live `Keyboard Shortcuts.psp`, does the same). On a desktop with Photoshop installed, the first launch that has no shortcut overrides yet asks whether to import the newest install's live set (Import / Don't Import; `ui.inspect` shows it as `kysOffer`); the answer is recorded in the preferences as `dialogs.photoshopShortcuts`, so it is asked once. **Sets** (Edit › Keyboard Shortcuts › Set): `useSet` loads a saved set's bindings (`""` or `"PhotoCraft Defaults"`: the defaults) before the call's other changes; then `saveSet` saves the bindings as they end up under a name and uses it, `deleteSet` removes one (the bindings stay), `sets` replaces every saved set (`{"<name>": {"shortcuts": {…}, "alternateShortcuts": {…}}}`, the dialog's OK) and `activeSet` names the set in use without loading it. Names are trimmed, 1–64 characters, at most 64 sets. Every reply has `sets`, `activeSet` (`""` for the defaults), `defaultsName` and `modified` (the bindings differ from the set in use). `exportKys: true` adds `kys`, the bindings of the engine's commands as a `.kys` file (Photoshop's macOS key spelling; each row carries our id as `pc-id`, so the file imports back exactly); `summarize: true` (or `{"mac": false}`) adds `html`, Photoshop's Summarize page. The app's dialog exports and summarizes the shell's commands and tools as well. A bad parameter anywhere in the call changes nothing |
|
||||
| `edit.keyboardShortcuts` | `{"set":{"edit.fill":"Cmd+Shift+F"},"reset":true\|["id",…],"removeConflicts":true,"filter":"blur","list":false,"importKys":"<.kys XML>"}`: returns overrides, matching commands and conflicts. A shortcut moved to another command is removed from its old owner unless `removeConflicts` is false. `""` removes a shortcut, `null` restores the default. The held temporary tools are bindable too: `tools.temporary.hand` (Space; also repositions a selection being drawn), `tools.temporary.zoomIn` (Cmd+Space), `tools.temporary.zoomOut` (Alt+Space; Alt added to a held Zoom In, Cmd+Alt+Space, zooms out too), `tools.temporary.erase` (`` ` ``; with the Brush or Pencil, a stroke started while it is held is in Clear mode, `paint.stroke`/`paint.pencil` `"mode": "clear"`, erasing with that tool's brush); they list with `"hold": true`. ⌘ (Ctrl off the Mac) held alone is the Move tool with the painting, retouching and other non-selection tools, as in Photoshop; it is a modifier, not a binding. `importKys` applies a Photoshop `.kys` set (the file's XML text, at most 4 MiB): rows are matched to commands by our id (`pc-id`, in sets PhotoCraft exported) or else by menu label, a key equal to the default restores it, keys also given in `set` win, and the result gains `import` (`name`, `imported`, `set`, `unknown`, `unreadable`, `alternates`, `toolKeys`, `windows`). A set saved by Photoshop on Windows (no ⌘ or Opt anywhere, Ctrl used) reads Ctrl as the primary modifier, so `Ctrl+L` is the `Cmd+L` default (`windows: true`). Tool keys are matched to tools by name in the app's dialog import (a tool's own letter keeps its Shift cycling); this command doesn't know the tools and leaves them. In the app it opens Keyboard Shortcuts and Menus, whose Import Shortcuts… button loads a `.kys` set into the dialog (opening or dropping a `.kys`, or Photoshop's live `Keyboard Shortcuts.psp`, does the same). On a desktop with Photoshop installed, the first launch that has no shortcut overrides yet asks whether to import the newest install's live set (Import / Don't Import; `ui.inspect` shows it as `kysOffer`); the answer is recorded in the preferences as `dialogs.photoshopShortcuts`, so it is asked once. **Sets** (Edit › Keyboard Shortcuts › Set): `useSet` loads a saved set's bindings (`""` or `"PhotoCraft Defaults"`: the defaults) before the call's other changes; then `saveSet` saves the bindings as they end up under a name and uses it, `deleteSet` removes one (the bindings stay), `sets` replaces every saved set (`{"<name>": {"shortcuts": {…}, "alternateShortcuts": {…}}}`, the dialog's OK) and `activeSet` names the set in use without loading it. Names are trimmed, 1–64 characters, at most 64 sets. Every reply has `sets`, `activeSet` (`""` for the defaults), `defaultsName` and `modified` (the bindings differ from the set in use). `exportKys: true` adds `kys`, the bindings of the engine's commands as a `.kys` file (Photoshop's macOS key spelling; each row carries our id as `pc-id`, so the file imports back exactly); `summarize: true` (or `{"mac": false}`) adds `html`, Photoshop's Summarize page. The app's dialog exports and summarizes the shell's commands and tools as well. A bad parameter anywhere in the call changes nothing |
|
||||
| `edit.menus` / `edit.toolbar` | `{"hide":["edit.fade"],"show":[…],"color":{"edit.fill":"red"},"reset":false}` / `{"hidden":["Sponge"],"order":[…]}` |
|
||||
| `edit.colorSettings` | `{"workingRgb":"display-p3","workingCmyk":"coated-cmyk","workingGray":"sgray","policyRgb":"preserve\|convert\|off",…,"askOnMismatch":true,"intent":"perceptual","bpc":true}`; honoured when opening files (`file.openAs`, the app's File › Open) and by Image › Mode and "working" profile specs. `color.profileMismatch {"action":"preserve\|convert\|discard\|assignWorking"}` answers the mismatch prompt |
|
||||
|
||||
|
||||
Reference in new issue
Block a user