mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 13:07:43 -06:00
refactor(lightcraft-ui-egui): split menus.rs into menus/
This commit is contained in:
+8
-2529
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,262 @@
|
||||
use super::*;
|
||||
|
||||
/// The Delete key in an album: a normal album loses the selected photos (`album.removePhotos`),
|
||||
/// which stay in the library and their folder; a smart album keeps them (its rules decide) and
|
||||
/// says so. True when the key was handled here, false outside albums (then it deletes the photos
|
||||
/// from the library). Photo ▸ Delete Photo still deletes from the library everywhere.
|
||||
pub fn delete_in_album(app: &mut LightcraftApp, ctx: &egui::Context) -> bool {
|
||||
let lightcraft_engine::LibrarySource::Album(aid) = app.session.source else { return false };
|
||||
let Some(album) = app.session.catalog.album(aid) else { return false };
|
||||
if album.is_smart() {
|
||||
app.toast(ctx, crate::i18n::tr("Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo"));
|
||||
return true;
|
||||
}
|
||||
if app.session.selected().is_empty() {
|
||||
return true;
|
||||
}
|
||||
match app.run("album.removePhotos", json!({"id": aid.0})) {
|
||||
Ok(r) => {
|
||||
let n = r["removed"].as_u64().unwrap_or(0);
|
||||
let name = r["album"].as_str().unwrap_or_default().to_string();
|
||||
let what = crate::i18n::tr_format!("{n} photo{}", if n == 1 { "" } else { "s" }, n = n);
|
||||
app.toast(ctx, crate::i18n::tr_format!("Removed {what} from {name}", what = what, name = name));
|
||||
}
|
||||
Err(e) => app.toast(ctx, e),
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// With Settings → General → "Confirm before deleting" on, open the confirmation dialog instead
|
||||
/// of deleting; true when it did (the dialog's OK runs `photo.delete`).
|
||||
pub fn confirm_delete(app: &mut LightcraftApp) -> bool {
|
||||
if !app.ui.settings.confirm_delete {
|
||||
return false;
|
||||
}
|
||||
let count = app.session.selected().len();
|
||||
if count == 0 {
|
||||
return false;
|
||||
}
|
||||
app.ui.dialog = Some(Dialog::ConfirmDelete { count });
|
||||
true
|
||||
}
|
||||
|
||||
/// Platform-appropriate label for revealing a file in the system file manager.
|
||||
pub fn reveal_label() -> &'static str {
|
||||
if cfg!(target_os = "macos") {
|
||||
"Show in Finder"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"Show in Explorer"
|
||||
} else {
|
||||
"Show in File Manager"
|
||||
}
|
||||
}
|
||||
|
||||
/// Help ▸ Open Log Folder: reveal the host's log file in the system file manager, so it can be
|
||||
/// attached to a report without hunting for the settings folder (#260).
|
||||
pub(super) fn open_log_folder(app: &mut LightcraftApp) -> Result<Value, String> {
|
||||
let path = app.services.log_file.clone().ok_or("this session keeps no log file")?;
|
||||
let reveal = app.services.reveal.as_mut().ok_or("not available here")?;
|
||||
reveal(&path)?;
|
||||
Ok(json!({"path": path}))
|
||||
}
|
||||
|
||||
/// Reveal the active photo's original in the system file manager.
|
||||
pub(super) fn show_in_finder(app: &mut LightcraftApp) -> Result<Value, String> {
|
||||
let id = app.session.active().ok_or("no photo selected")?;
|
||||
let path = match app.session.catalog.photo(id).map(|p| p.source.clone()) {
|
||||
Some(lightcraft_engine::catalog::Source::File { path }) => path,
|
||||
_ => return Err("this photo has no file (demo scene)".into()),
|
||||
};
|
||||
let reveal = app.services.reveal.as_mut().ok_or("not available here")?;
|
||||
reveal(&path)?;
|
||||
Ok(json!({"path": path}))
|
||||
}
|
||||
|
||||
/// The folder the active photo was imported from, as the sidebar's Folders section counts it (a
|
||||
/// photo straight in a disk's root is in the root). `None` for a demo scene, a photo only browsed
|
||||
/// in Local or in Recently Deleted, and one on the startup disk's root, which has no row to show.
|
||||
fn photo_folder(app: &LightcraftApp, id: Option<lightcraft_catalog::PhotoId>) -> Option<String> {
|
||||
let photo = id.or_else(|| app.session.active()).and_then(|id| app.session.catalog.photo(id)).filter(|p| p.in_library())?;
|
||||
let lightcraft_engine::catalog::Source::File { path } = &photo.source else { return None };
|
||||
let dir = path.rfind(['/', '\\']).and_then(|i| path.get(..i.max(1)))?;
|
||||
(!lightcraft_catalog::folders::is_startup_disk(dir)).then(|| dir.to_string())
|
||||
}
|
||||
|
||||
/// Whether Show in Folders has a row to show for photo `id` (the active one when `None`): its
|
||||
/// folder must be a row of the tree, which stops `MAX_DEPTH` levels down. With no fresh tree to
|
||||
/// look in, the folder alone decides, and the command still checks.
|
||||
pub fn show_in_folders_enabled(app: &LightcraftApp, id: Option<lightcraft_catalog::PhotoId>) -> bool {
|
||||
let Some(dir) = photo_folder(app, id) else { return false };
|
||||
app.caches.folder_tree_if_fresh(&app.session.catalog).is_none_or(|tree| crate::panels::left::folder_row_of(&tree, &dir).is_some())
|
||||
}
|
||||
|
||||
/// Photo ▸ Show in Folders (Lightroom's Go to Folder in Library): show the folder of photo `id`
|
||||
/// (the active one when `None`; the grid's menu passes the photo right-clicked) as a click on its
|
||||
/// row in the sidebar's Folders section does, and bring the row into view: the sidebar, the
|
||||
/// section and the folders above it open, and it scrolls there (`reveal_folder`).
|
||||
pub(super) fn show_in_folders(app: &mut LightcraftApp, id: Option<lightcraft_catalog::PhotoId>) -> Result<Value, String> {
|
||||
let dir = photo_folder(app, id).ok_or("this photo is not in a folder of the library")?;
|
||||
let tree = app.caches.folder_tree(&app.session.catalog);
|
||||
// the row's own spelling (forward slashes): what its click hands `library.source`, and its id
|
||||
let path = crate::panels::left::folder_row_of(&tree, &dir).map(|n| n.path.clone()).ok_or("this photo's folder is not in Folders")?;
|
||||
app.run("library.source", json!({"kind": "libraryFolder", "path": path}))?;
|
||||
app.ui.left_panel = true;
|
||||
app.ui.reveal_folder = Some(path.clone());
|
||||
Ok(json!({"path": path}))
|
||||
}
|
||||
|
||||
/// Photo ▸ Edit In: render a copy of each selected photo with its edits (stacked on its original)
|
||||
/// as Settings ▸ External Editing asks for that editor, then open them there. `p` may override the
|
||||
/// file (`format`, `colorSpace`, `bitDepth`) and the editor (`app`). PhotoCraft is checked first,
|
||||
/// so a missing editor leaves no copy behind. Once the copies exist, an editor that won't open them
|
||||
/// is reported in the result's `failed` (and a toast), not as an error.
|
||||
pub(super) fn edit_in(app: &mut LightcraftApp, ctx: &egui::Context, p: &Value, photocraft: bool) -> Result<Value, String> {
|
||||
// every way in (menu, shortcut, context menu, the control channel) says why nothing opened
|
||||
edit_in_inner(app, ctx, p, photocraft).inspect_err(|e| app.toast(ctx, crate::i18n::tr_format!("Couldn't open the editor: {e}", e = e)))
|
||||
}
|
||||
|
||||
fn edit_in_inner(app: &mut LightcraftApp, ctx: &egui::Context, p: &Value, photocraft: bool) -> Result<Value, String> {
|
||||
let settings = &app.ui.settings;
|
||||
let (file, configured) = if photocraft {
|
||||
let path = settings.photocraft_path.trim();
|
||||
(settings.photocraft_file, if path.is_empty() { crate::state::PHOTOCRAFT.to_string() } else { path.to_string() })
|
||||
} else {
|
||||
(settings.external_file, settings.external_editor.clone())
|
||||
};
|
||||
let editor = p.get("app").and_then(Value::as_str).map_or(configured, str::to_string);
|
||||
if let Some(check) = app.services.check_editor.as_ref() {
|
||||
check(&editor)?;
|
||||
}
|
||||
let mut params = file.params();
|
||||
if let (Some(o), Some(given)) = (params.as_object_mut(), p.as_object()) {
|
||||
for (k, v) in given.iter().filter(|(k, _)| k.as_str() != "app") {
|
||||
o.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
let r = app.session.execute("photo.editExternal", ¶ms).map_err(|e| e.to_string())?;
|
||||
// every selected photo got a copy (#920); each opens in the editor
|
||||
let copies: Vec<(String, Option<u64>)> = r["copies"]
|
||||
.as_array()
|
||||
.map(|c| c.iter().map(|c| (c["path"].as_str().unwrap_or_default().to_string(), c["id"].as_u64())).collect())
|
||||
.unwrap_or_else(|| vec![(r["path"].as_str().unwrap_or_default().to_string(), r["id"].as_u64())]);
|
||||
let mut failed = None;
|
||||
for (path, id) in &copies {
|
||||
if let Some(id) = id {
|
||||
app.ui.external_edits.push(*id);
|
||||
}
|
||||
// the copy is LightCraft's own render, but it goes through the same gate as any other
|
||||
// path handed to the system opener (#2135)
|
||||
match lightcraft_engine::open_path::openable(path, "") {
|
||||
Err(e) => failed = Some(e),
|
||||
Ok(absolute) => {
|
||||
if let Some(Err(e)) = app.services.open_with.as_mut().map(|f| f(&absolute, &editor)) {
|
||||
failed = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut r = r;
|
||||
if let Some(o) = r.as_object_mut() {
|
||||
o.insert("failed".into(), json!(failed));
|
||||
}
|
||||
if let Some(e) = failed {
|
||||
// the copies are in the library already (one undo step): an error would invite a retry
|
||||
// that makes another set, so the result says what failed and the toast says why
|
||||
app.toast(ctx, crate::i18n::tr_format!("Couldn't open the editor: {e}", e = e));
|
||||
return Ok(r);
|
||||
}
|
||||
match copies.as_slice() {
|
||||
[(path, _)] => {
|
||||
let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||||
app.toast(ctx, crate::i18n::tr_format!("{name} opened for editing; it is stacked with the original", name = name));
|
||||
}
|
||||
_ => app.toast(ctx, crate::i18n::tr_format!("{n} photos opened for editing; each is stacked with its original", n = copies.len())),
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Most originals one Open Original hands the editor (each starts it once).
|
||||
const MAX_ORIGINALS: usize = 50;
|
||||
|
||||
/// Photo ▸ Edit In ▸ Open Original in Other Editor: hand each selected photo's original file
|
||||
/// itself (a raw stays a raw: nothing is rendered, copied or added to the library) to the editor
|
||||
/// set in Settings ▸ External Editing, e.g. a raw developer. `p` may give `ids` and the editor
|
||||
/// (`app`). Demo photos and missing files are skipped and listed; when no photo has a file to
|
||||
/// open, it fails saying why.
|
||||
pub(super) fn open_original_in(app: &mut LightcraftApp, ctx: &egui::Context, p: &Value) -> Result<Value, String> {
|
||||
let r = open_originals(app, p);
|
||||
if let Err(e) = &r {
|
||||
app.toast(ctx, e.clone());
|
||||
}
|
||||
let r = r?;
|
||||
let opened = r["opened"].as_array().map_or(0, Vec::len);
|
||||
match (r["failed"].as_str(), r["opened"][0]["path"].as_str()) {
|
||||
(Some(e), _) => app.toast(ctx, crate::i18n::tr_format!("Couldn't open the editor: {e}", e = e)),
|
||||
(None, Some(path)) if opened == 1 => {
|
||||
let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||||
app.toast(ctx, crate::i18n::tr_format!("{name} opened in the other editor", name = name));
|
||||
}
|
||||
_ => app.toast(ctx, crate::i18n::tr_format!("{n} originals opened in the other editor", n = opened)),
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
fn open_originals(app: &mut LightcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let mut ids = app.session.targets(p).map_err(|e| e.to_string())?;
|
||||
ids.dedup();
|
||||
if ids.is_empty() {
|
||||
return Err("no photo selected".into());
|
||||
}
|
||||
if ids.len() > MAX_ORIGINALS {
|
||||
return Err(format!("{} photos selected: open at most {MAX_ORIGINALS} originals at a time", ids.len()));
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
let mut skipped = Vec::new();
|
||||
for id in ids {
|
||||
// the path may come from an imported catalog: only a photo file LightCraft reads is
|
||||
// handed to the opener, never a program (#2135)
|
||||
match app.session.catalog.photo(id).map(|ph| (ph.file_name.clone(), ph.source.clone())) {
|
||||
Some((name, lightcraft_engine::catalog::Source::File { path })) => match lightcraft_engine::open_path::openable(&path, &name) {
|
||||
Ok(absolute) => files.push((id, absolute)),
|
||||
Err(reason) => skipped.push(json!({"id": id.0, "reason": reason})),
|
||||
},
|
||||
Some(_) => skipped.push(json!({"id": id.0, "reason": "a demo photo has no original file"})),
|
||||
None => skipped.push(json!({"id": id.0, "reason": "no such photo"})),
|
||||
}
|
||||
}
|
||||
if files.is_empty() {
|
||||
let why = skipped.first().and_then(|s| s["reason"].as_str()).unwrap_or("no original file");
|
||||
return Err(if skipped.len() == 1 {
|
||||
format!("nothing to open: {why}")
|
||||
} else {
|
||||
format!("nothing to open: none of these photos has its original file ({why})")
|
||||
});
|
||||
}
|
||||
let editor = p.get("app").and_then(Value::as_str).map_or_else(|| app.ui.settings.external_editor.clone(), str::to_string);
|
||||
if let Some(check) = app.services.check_editor.as_ref() {
|
||||
check(&editor)?;
|
||||
}
|
||||
// all at once where the host can: an editor that keeps one window (DxO PhotoLab) loses the
|
||||
// files that arrive while it is still starting
|
||||
if let Some(open_all) = app.services.open_all_with.as_mut() {
|
||||
let paths: Vec<String> = files.iter().map(|(_, path)| path.clone()).collect();
|
||||
open_all(&paths, &editor).map_err(|e| format!("Couldn't open the editor: {e}"))?;
|
||||
let opened: Vec<Value> = files.iter().map(|(id, path)| json!({"id": id.0, "path": path})).collect();
|
||||
return Ok(json!({"opened": opened, "skipped": skipped, "app": editor, "failed": null}));
|
||||
}
|
||||
let open = app.services.open_with.as_mut().ok_or("opening another application isn't available here")?;
|
||||
let mut opened = Vec::new();
|
||||
let mut failed = None;
|
||||
for (id, path) in files {
|
||||
match open(&path, &editor) {
|
||||
Ok(()) => opened.push(json!({"id": id.0, "path": path})),
|
||||
Err(e) => failed = Some(e),
|
||||
}
|
||||
}
|
||||
if opened.is_empty()
|
||||
&& let Some(e) = failed
|
||||
{
|
||||
return Err(format!("Couldn't open the editor: {e}"));
|
||||
}
|
||||
Ok(json!({"opened": opened, "skipped": skipped, "app": editor, "failed": failed}))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,280 @@
|
||||
use super::*;
|
||||
|
||||
/// The "Edit → Language" entries, one per language in [`crate::i18n::Locale::ALL`], so a language
|
||||
/// has a matching menu command (also listed through the control channel). Tests check coverage.
|
||||
pub const LANGUAGE_COMMANDS: &[UiCommand] = &[
|
||||
("app.language.english", crate::i18n::Locale::En.name(), None, "Edit>Language"),
|
||||
// The picker order (`Locale::for_picker`): alphabetical by the language's own name after
|
||||
// English; Cyrillic, Georgian, then CJK names follow the Latin ones. A test keeps them equal.
|
||||
("app.language.indonesian", crate::i18n::Locale::Id.name(), None, "Edit>Language"),
|
||||
("app.language.czech", crate::i18n::Locale::Cs.name(), None, "Edit>Language"),
|
||||
("app.language.german", crate::i18n::Locale::De.name(), None, "Edit>Language"),
|
||||
("app.language.spanish", crate::i18n::Locale::Es.name(), None, "Edit>Language"),
|
||||
("app.language.french", crate::i18n::Locale::Fr.name(), None, "Edit>Language"),
|
||||
("app.language.italian", crate::i18n::Locale::It.name(), None, "Edit>Language"),
|
||||
("app.language.polish", crate::i18n::Locale::Pl.name(), None, "Edit>Language"),
|
||||
("app.language.portuguese", crate::i18n::Locale::PtBr.name(), None, "Edit>Language"),
|
||||
("app.language.finnish", crate::i18n::Locale::Fi.name(), None, "Edit>Language"),
|
||||
("app.language.swedish", crate::i18n::Locale::Sv.name(), None, "Edit>Language"),
|
||||
("app.language.vietnamese", crate::i18n::Locale::Vi.name(), None, "Edit>Language"),
|
||||
("app.language.russian", crate::i18n::Locale::Ru.name(), None, "Edit>Language"),
|
||||
("app.language.ukrainian", crate::i18n::Locale::Uk.name(), None, "Edit>Language"),
|
||||
("app.language.georgian", crate::i18n::Locale::Ka.name(), None, "Edit>Language"),
|
||||
("app.language.japanese", crate::i18n::Locale::Ja.name(), None, "Edit>Language"),
|
||||
("app.language.simplifiedChinese", crate::i18n::Locale::ZhHans.name(), None, "Edit>Language"),
|
||||
("app.language.traditionalChinese", crate::i18n::Locale::ZhHant.name(), None, "Edit>Language"),
|
||||
];
|
||||
|
||||
/// Every UI command: the languages, then everything else. `xtask parity` reads both tables from
|
||||
/// this file, so an id listed in `docs/parity-checklist.md` is checked wherever it is declared.
|
||||
pub fn ui_commands() -> impl Iterator<Item = &'static UiCommand> {
|
||||
LANGUAGE_COMMANDS.iter().chain(UI_COMMANDS)
|
||||
}
|
||||
|
||||
/// The language a Language-menu command selects, if the id is one. The engine and the UI both go
|
||||
/// through here, so the menu, the settings row and the control channel agree on the mapping.
|
||||
pub fn language_from_command(id: &str) -> Option<crate::i18n::Locale> {
|
||||
match id {
|
||||
"app.language.english" => Some(crate::i18n::Locale::En),
|
||||
"app.language.simplifiedChinese" => Some(crate::i18n::Locale::ZhHans),
|
||||
"app.language.traditionalChinese" => Some(crate::i18n::Locale::ZhHant),
|
||||
"app.language.japanese" => Some(crate::i18n::Locale::Ja),
|
||||
"app.language.portuguese" => Some(crate::i18n::Locale::PtBr),
|
||||
"app.language.french" => Some(crate::i18n::Locale::Fr),
|
||||
"app.language.spanish" => Some(crate::i18n::Locale::Es),
|
||||
"app.language.german" => Some(crate::i18n::Locale::De),
|
||||
"app.language.russian" => Some(crate::i18n::Locale::Ru),
|
||||
"app.language.ukrainian" => Some(crate::i18n::Locale::Uk),
|
||||
"app.language.polish" => Some(crate::i18n::Locale::Pl),
|
||||
"app.language.czech" => Some(crate::i18n::Locale::Cs),
|
||||
"app.language.italian" => Some(crate::i18n::Locale::It),
|
||||
"app.language.georgian" => Some(crate::i18n::Locale::Ka),
|
||||
"app.language.swedish" => Some(crate::i18n::Locale::Sv),
|
||||
"app.language.finnish" => Some(crate::i18n::Locale::Fi),
|
||||
"app.language.indonesian" => Some(crate::i18n::Locale::Id),
|
||||
"app.language.vietnamese" => Some(crate::i18n::Locale::Vi),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const UI_COMMANDS: &[UiCommand] = &[
|
||||
("ui.dock", "Arrange Panel", None, ""),
|
||||
("ui.dock.reset", "Reset Panel Layout", None, "Window"),
|
||||
("modelSetup.cancel", "Cancel Pending Photo Action", None, ""),
|
||||
("view.photoGrid", "Photo Grid", None, "View"),
|
||||
("view.squareGrid", "Square Grid", None, "View"),
|
||||
// G: Photo Grid ↔ Square Grid (from other views: the photo grid)
|
||||
("view.gridToggle", "Grid", Some("G"), ""),
|
||||
("library.selectOnlyActive", "Select Only Active Photo", Some("Cmd+Shift+D"), "Edit"),
|
||||
("tool.guidedUpright", "Guided Upright", Some("Shift+G"), "Window>Tools"),
|
||||
("view.detail", "Detail View", Some("D"), "View"),
|
||||
("view.compare", "Compare", Some("Shift+C"), "View"),
|
||||
("view.survey", "Survey", Some("N"), "View"),
|
||||
("view.people", "People", None, "View"),
|
||||
("view.person", "Show Person", None, ""),
|
||||
("view.faceBoxes", "Face Boxes", None, "View"),
|
||||
("view.focusPoint", "Focus Point", Some("Alt+F"), "View"),
|
||||
("view.zoomToFocus", "Zoom to Focus Point", None, "View"),
|
||||
("view.reference", "Reference View", Some("Shift+R"), "View"),
|
||||
("photo.setReference", "Set as Reference Photo", None, ""),
|
||||
("compare.swap", "Swap Compare Photos", None, "View"),
|
||||
("compare.set", "Show in Compare", None, ""),
|
||||
("compare.makeSelect", "Make Candidate the Select", None, "View"),
|
||||
("view.autoAdvance", "Auto Advance", None, "Photo"),
|
||||
("view.filmstrip", "Filmstrip", Some("/"), "View"),
|
||||
("view.leftPanel", "My Photos Panel", Some("Cmd+Shift+L"), "View"),
|
||||
("view.rightPanel", "Right Panel", None, "View"),
|
||||
("view.beforeAfter", "Before/After Side by Side", Some("Y"), "View"),
|
||||
("view.beforeAfterSplit", "Before/After Split", Some("Shift+Y"), "View"),
|
||||
("view.beforeAfterTopBottom", "Before/After Top/Bottom", Some("Alt+Y"), "View"),
|
||||
("view.beforeAfterSplitTopBottom", "Before/After Split Top/Bottom", Some("Alt+Shift+Y"), "View"),
|
||||
("view.showOriginal", "Show Original", Some("\\"), "View"),
|
||||
("view.zoomFit", "Zoom to Fit", Some("Cmd+0"), "View"),
|
||||
("view.zoom100", "Zoom 100%", Some("Cmd+Alt+0"), "View"),
|
||||
("view.zoomToggle", "Toggle Zoom", Some("Z"), "View"),
|
||||
// the ratio a click (and Z / Space) zooms to
|
||||
("view.clickZoom", "Click Zoom Ratio", None, ""),
|
||||
("view.navigate", "Set Image Zoom and Pan", None, ""),
|
||||
("view.zoomIn", "Zoom In", Some("Cmd+="), "View"),
|
||||
("view.zoomOut", "Zoom Out", Some("Cmd+-"), "View"),
|
||||
// Library grids only (Photo Grid, Square Grid, People): the bottom-bar slider's step by key
|
||||
("view.thumbSizeUp", "Increase Grid Thumbnail Size", Some("="), "View"),
|
||||
("view.thumbSizeDown", "Decrease Grid Thumbnail Size", Some("-"), "View"),
|
||||
("view.clipping", "Clipping", Some("J"), "View"),
|
||||
("view.visualizeHdr", "Visualize HDR", None, "View"),
|
||||
// in grids S expands/collapses stacks (the engine command it shadows)
|
||||
("view.softProof", "Soft Proofing", Some("S"), "View"),
|
||||
("view.histogram", "Histogram", Some("Cmd+Shift+H"), "View"),
|
||||
("view.maskOverlay", "Mask Overlay", Some("O"), "View"),
|
||||
// Ctrl+Shift+O / Cmd+Shift+O: every mask's region at once, not just the selected one
|
||||
("view.maskOverlayAll", "All Mask Overlays", Some("Cmd+Shift+O"), "View"),
|
||||
// Shift+O in the Masking panel (elsewhere it cycles the crop overlay)
|
||||
("view.maskOverlayMode", "Cycle Mask Overlay Mode", None, "View"),
|
||||
("view.maskOverlayColor", "Cycle Mask Overlay Color", None, "View"),
|
||||
("view.maskPins", "Mask Pins", None, "View"),
|
||||
// {mode?: auto | always | selected | never}: when spot and mask pins show (steps to the next
|
||||
// mode when omitted; Shift+H, like Classic's H for pins, since H opens Remove). View ▸ Tool
|
||||
// Overlay and the Remove panel list the modes.
|
||||
("view.toolOverlay", "Cycle Tool Overlay", Some("Shift+H"), ""),
|
||||
("view.visualizeSpots", "Visualize Spots", Some("A"), "View"),
|
||||
// A while the Crop panel is open: the aspect lock as a first-class row. Visualize Spots keeps
|
||||
// the key outside the panel; the two are a contextual pair in shortcuts::handle
|
||||
("crop.lockAspect", "Lock Aspect Ratio", Some("A"), ""),
|
||||
("view.cropOverlay", "Cycle Crop Overlay", Some("Shift+O"), "View"),
|
||||
("view.cropOverlayOrientation", "Cycle Crop Overlay Orientation", None, "View"),
|
||||
("view.back", "Back to Grid", Some("Escape"), ""),
|
||||
("tool.done", "Done", Some("Enter"), ""),
|
||||
("view.filterBar", "Filter Bar", Some("Shift+F"), "View"),
|
||||
("local.addRoot", "Add Folder to Local", None, ""),
|
||||
("local.hide", "Remove from Local", None, ""),
|
||||
("local.restoreHidden", "Show Hidden Local Locations", None, ""),
|
||||
// {on?}: the Info panel's Target Photo / Selected Photos choice (on = Selected Photos)
|
||||
("view.infoSelectedPhotos", "Show Metadata for Selected Photos", None, ""),
|
||||
("view.fullScreenPreview", "Full Screen Preview", Some("F"), "View"),
|
||||
("view.lightsOut", "Lights Out", Some("Shift+L"), "View"),
|
||||
("view.enterFullScreen", "Enter Full Screen", Some("Cmd+Shift+F"), "View"),
|
||||
("view.infoOverlay", "Cycle Info Overlay", Some("Cmd+I"), "View"),
|
||||
("view.viewOptions", "View Options…", Some("Cmd+J"), "View"),
|
||||
("view.chooseLayoutImage", "Choose Layout Image…", None, "View"),
|
||||
("view.layoutImage", "Layout Image", Some("Cmd+Alt+O"), "View"),
|
||||
("view.layoutImageOpacity", "Layout Image Opacity", None, ""),
|
||||
("view.layoutImageMatte", "Layout Image Matte", None, ""),
|
||||
("view.navigator", "Navigator", None, "View"),
|
||||
("panel.edit", "Edit Panel", Some("E"), "Window"),
|
||||
("panel.profiles", "Profile Browser", None, "Window"),
|
||||
("panel.crop", "Crop & Rotate", Some("C"), "Window"),
|
||||
("panel.remove", "Remove", Some("H"), "Window"),
|
||||
("panel.masking", "Masking", Some("M"), "Window"),
|
||||
("panel.redeye", "Red Eye", None, "Window"),
|
||||
("panel.presets", "Presets", Some("Shift+P"), "Window"),
|
||||
("panel.info", "Info", Some("I"), "Window"),
|
||||
("panel.keywords", "Keywords", Some("K"), "Window"),
|
||||
// the Keywords panel with the cursor in its Add keyword box
|
||||
("panel.addKeywords", "Add Keywords", Some("Cmd+K"), ""),
|
||||
("panel.versions", "Versions", Some("Shift+V"), "Window"),
|
||||
("panel.activity", "History", None, "Window"),
|
||||
("panel.close", "Close Panel", None, ""),
|
||||
// {solo?}: true opens just that section (⌥-click on its header); otherwise opens or closes it
|
||||
("section.light", "Light", Some("Cmd+1"), "Window>Edit Sections"),
|
||||
("section.color", "Color", Some("Cmd+2"), "Window>Edit Sections"),
|
||||
("section.effects", "Effects", Some("Cmd+3"), "Window>Edit Sections"),
|
||||
("section.detail", "Detail", Some("Cmd+4"), "Window>Edit Sections"),
|
||||
("section.optics", "Optics", Some("Cmd+5"), "Window>Edit Sections"),
|
||||
("section.calibration", "Calibration", None, ""),
|
||||
// {on?}; no params toggles. Opening one Edit section closes the others.
|
||||
("view.soloSections", "Solo Mode", None, "Window>Edit Sections"),
|
||||
("tool.brush", "Brush", Some("B"), "Window>Tools"),
|
||||
("tool.linear", "Linear Gradient", Some("L"), "Window>Tools"),
|
||||
("tool.radial", "Radial Gradient", Some("R"), "Window>Tools"),
|
||||
("tool.wbPicker", "White Balance Selector", Some("W"), "Window>Tools"),
|
||||
("tool.none", "No Tool", None, ""),
|
||||
// brush size / feather of the active brush (Masking brush, Remove tool and its selected spot)
|
||||
("brush.smaller", "Decrease Brush Size", Some("["), "Window>Tools"),
|
||||
("brush.larger", "Increase Brush Size", Some("]"), "Window>Tools"),
|
||||
("brush.featherLess", "Decrease Brush Feather", Some("Shift+["), "Window>Tools"),
|
||||
("brush.featherMore", "Increase Brush Feather", Some("Shift+]"), "Window>Tools"),
|
||||
("dialog.newAlbum", "New Album…", Some("Cmd+N"), "File"),
|
||||
("dialog.newFolder", "New Folder…", Some("Cmd+Shift+N"), "File"),
|
||||
("dialog.smartAlbum", "New Smart Album…", None, "File"),
|
||||
("view.photoCounts", "Photo Counts", None, "View"),
|
||||
("view.sidebarSection", "Show Sidebar Section", None, ""),
|
||||
("view.sidebarMoveSection", "Move Sidebar Section", None, ""),
|
||||
("view.sidebarReset", "Reset Sidebar Sections", None, ""),
|
||||
("view.slideshow", "Slideshow", Some("Cmd+Alt+Enter"), "View"),
|
||||
("view.slideshowOptions", "Slideshow Options…", None, "View"),
|
||||
("dialog.slideshowExport", "Export Slideshow…", None, "View"),
|
||||
("app.slideshowExport", "Export Slideshow", None, ""),
|
||||
// the slideshow's playback controls (Space, ←/→ and Esc while it plays)
|
||||
("slideshow.togglePause", "Pause / Resume Slideshow", None, ""),
|
||||
("slideshow.next", "Next Slide", None, ""),
|
||||
("slideshow.previous", "Previous Slide", None, ""),
|
||||
("slideshow.end", "End Slideshow", None, ""),
|
||||
("view.secondWindow", "Second Window", Some("Cmd+F11"), "Window"),
|
||||
("tool.keywordPainter", "Keyword Painter", None, ""),
|
||||
("view.gridInfo", "Grid Info", None, ""),
|
||||
// {mode?: auto | light | dark}: Appearance Mode (cycles Auto, Light, Dark when omitted; the
|
||||
// top bar's appearance button). View ▸ Appearance lists the modes.
|
||||
("view.appearance", "Next Appearance Mode", None, ""),
|
||||
// {theme: charcoal | midnight | silver | paper}: pick a theme and fix the mode to its family.
|
||||
("view.theme", "Theme", None, ""),
|
||||
("dialog.allMetadata", "All Metadata…", None, "Photo"),
|
||||
("dialog.faceModel", "Add Face Model…", None, ""),
|
||||
("dialog.newSmartAlbum", "New Smart Album from Filter…", Some("Cmd+Alt+N"), "File"),
|
||||
("dialog.createPreset", "Create Preset…", Some("Cmd+Shift+P"), "Photo"),
|
||||
("dialog.autoStack", "Auto-Stack by Capture Time…", None, "Photo>Stack"),
|
||||
("dialog.copySettings", "Choose Edit Settings to Copy…", Some("Cmd+Shift+C"), "Edit"),
|
||||
("dialog.pasteSettings", "Paste Selected Settings…", Some("Cmd+Shift+V"), "Edit"),
|
||||
("view.focusSearch", "Find…", Some("Cmd+F"), "Edit"),
|
||||
("dialog.export", "Export…", None, "File"),
|
||||
("dialog.print", "Print…", Some("Cmd+P"), "File"),
|
||||
("app.print", "Print to PDF", None, ""),
|
||||
("dialog.contactSheet", "Contact Sheet PDF…", None, "File"),
|
||||
("app.contactSheet", "Export Contact Sheet PDF", None, ""),
|
||||
("photo.editInPhotoCraft", "Edit in PhotoCraft", Some("Cmd+Shift+E"), "Photo>Edit In"),
|
||||
("photo.editInExternal", "Edit in Other Editor", Some("Cmd+Alt+E"), "Photo>Edit In"),
|
||||
// {ids?, app?}: the original files themselves (a raw stays a raw: no render, no copy), opened
|
||||
// in the other editor, e.g. a raw developer
|
||||
("photo.openOriginalInExternal", "Open Original in Other Editor (RAW, no copy)", None, "Photo>Edit In"),
|
||||
("dialog.mergeHdr", "HDR…", Some("Ctrl+H"), "Photo>Photo Merge"),
|
||||
("dialog.mergePanorama", "Panorama…", Some("Ctrl+M"), "Photo>Photo Merge"),
|
||||
("dialog.mergeHdrPanorama", "HDR Panorama…", None, "Photo>Photo Merge"),
|
||||
("merge.hdrLast", "HDR with Last Settings", Some("Ctrl+Shift+H"), "Photo>Photo Merge"),
|
||||
("merge.panoramaLast", "Panorama with Last Settings", Some("Ctrl+Shift+M"), "Photo>Photo Merge"),
|
||||
("merge.hdrPanoramaLast", "HDR Panorama with Last Settings", None, "Photo>Photo Merge"),
|
||||
("file.addPhotos", "Import Photos…", Some("Cmd+Shift+I"), "File"),
|
||||
("file.addFolder", "Import from Folder…", None, "File"),
|
||||
// {path?, into?: open | new, name?, location?}: asks where the catalog goes unless `into` (or
|
||||
// Settings) says (#2672, `lightroom_destination`)
|
||||
("file.importLightroom", "Import Lightroom Catalog…", None, "File"),
|
||||
("file.addFromDevice", "Import from Device", None, ""),
|
||||
("file.findMissing", "Find Missing Photos…", None, "File"),
|
||||
("file.backupLibrary", "Back Up Library…", None, "File"),
|
||||
("file.restoreLibrary", "Restore Library from Backup…", None, "File"),
|
||||
("photo.locate", "Locate Missing File…", None, ""),
|
||||
("dialog.saveMetadataPreset", "Save Metadata Preset…", None, ""),
|
||||
("app.quit", "Quit LightCraft", Some("Cmd+Q"), "File"),
|
||||
("file.importPresets", "Import Profiles & Presets…", None, "File"),
|
||||
("file.exportPresets", "Export Presets…", None, "File"),
|
||||
// Masking panel ▸ Effect menu
|
||||
("dialog.saveMaskEffect", "Save Current Settings as New Effect Preset…", None, ""),
|
||||
("dialog.manageMaskEffects", "Manage Effect Presets…", None, ""),
|
||||
("file.exportMaskEffects", "Export Effect Presets…", None, ""),
|
||||
("file.importKeywords", "Import Keywords…", None, "File"),
|
||||
("file.exportKeywords", "Export Keywords…", None, "File"),
|
||||
// Edit panel ▸ Curve ▸ Point Curve dropdown
|
||||
("file.importCurvePresets", "Import Point Curve Presets…", None, ""),
|
||||
("file.exportCurvePresets", "Export Point Curve Presets…", None, ""),
|
||||
("app.settings", "Settings…", Some("Cmd+,"), "Edit"),
|
||||
("app.newLibrary", "New Library…", None, "File"),
|
||||
("app.openLibrary", "Open Library…", Some("Cmd+O"), "File"),
|
||||
("app.recentLibraries", "Recent Libraries", None, ""),
|
||||
("app.clearRecentLibraries", "Clear Recent Libraries", None, ""),
|
||||
// Settings ▸ Display
|
||||
("app.displayProfile", "Display Profile", None, ""),
|
||||
("app.about", "About LightCraft", None, "Help"),
|
||||
("app.checkForUpdates", "Check for Updates…", None, "Help"),
|
||||
("app.systemInfo", "System Info…", None, "Help"),
|
||||
("app.openLogFolder", "Open Log Folder", None, "Help"),
|
||||
// the frame-rate display (`frame_rate`): a debugging aid, never saved
|
||||
("view.frameRate", "Frame Rate", None, "Help>Debug"),
|
||||
("app.whatsNew", "What's New", None, "Help"),
|
||||
("dialog.cull", "Assisted Culling…", None, "Photo"),
|
||||
("app.help", "LightCraft Help", Some("F1"), "Help"),
|
||||
("app.discord", "Join the ArtCraft Discord…", None, "Help"),
|
||||
("app.feedback", "Send Feedback…", None, "Help"),
|
||||
("app.website", "LightCraft Website", None, "Help"),
|
||||
("app.github", "LightCraft on GitHub", None, "Help"),
|
||||
("app.artcraft", "ArtCraft Website", None, "Help"),
|
||||
("app.shortcuts", "Keyboard Shortcuts", Some("Cmd+/"), "Help"),
|
||||
("app.setShortcut", "Set Keyboard Shortcut", None, ""),
|
||||
("app.resetShortcuts", "Reset All Keyboard Shortcuts", None, ""),
|
||||
("app.export", "Export Now", None, ""),
|
||||
("app.showInFinder", "Show in Finder", Some("Cmd+R"), "Photo"),
|
||||
("app.showInFolders", "Show in Folders", None, "Photo"),
|
||||
("app.openSamModelFolder", "Show in File Manager", None, ""),
|
||||
("dialog.rename", "Rename Photos…", Some("F2"), "Photo"),
|
||||
("dialog.labelNames", "Edit Color Label Names…", None, ""),
|
||||
("dialog.captureTime", "Edit Capture Time…", None, "Photo"),
|
||||
("photo.tagFromTracklog", "Auto-Tag from Tracklog…", None, "Photo"),
|
||||
("app.exportPrevious", "Export with Previous", Some("Cmd+Alt+Shift+E"), "File"),
|
||||
];
|
||||
Reference in New Issue
Block a user