Issue #2980: Clear Color Label can be given a keyboard shortcut

Setting a colour label is photo.label with the colour as a parameter, and the
keyboard shortcuts editor binds a key to a command, not to a parameter, so
only 6-9 had keys and clearing a label could not be given one. photo.clearLabel
is the same as photo.label with none, as a command of its own. The Photo >
Set Color Label > None row runs it and shows its key.
This commit is contained in:
x-labb
2026-10-30 21:03:50 +01:00
parent 8aabf61449
commit cc8a9f3d3d
11 changed files with 249 additions and 7 deletions
+4
View File
@@ -29,6 +29,10 @@ mod flag_cycle_tests;
#[path = "library/tests_rated_out.rs"]
mod rated_out_tests;
#[cfg(test)]
#[path = "library/tests_clear_label.rs"]
mod clear_label_tests;
#[cfg(test)]
#[path = "library/tests_enable_filters.rs"]
mod enable_filters_tests;
+10
View File
@@ -575,6 +575,16 @@ pub fn specs() -> Vec<CommandSpec> {
cull(s, p, |s| for_targets(s, p, "Set Color Label", |id| Some(Op::SetLabel { id, label: l })))
}
),
// `photo.label` takes the colour as a parameter, so a key can't be given to "none" alone
cmd!(
"photo.clearLabel",
"Clear Color Label",
["Photo", "Set Color Label"],
None,
"{ids?, advance?: bool} — `photo.label` with `none`; without `ids`, the selection moves on when this takes the active photo out of the view",
has_selection,
|s, p| { cull(s, p, |s| for_targets(s, p, "Set Color Label", |id| Some(Op::SetLabel { id, label: None }))) }
),
// ---- orientation
cmd!("photo.rotateLeft", "Rotate Left", ["Photo"], Some("Cmd+["), "{ids?}", has_selection, |s, p| rotate(s, p, false)),
cmd!("photo.rotateRight", "Rotate Right", ["Photo"], Some("Cmd+]"), "{ids?}", has_selection, |s, p| rotate(s, p, true)),
@@ -0,0 +1,70 @@
//! `photo.clearLabel`: the colour label taken off, as a command that can have a key of its own (#2980).
use lightcraft_catalog::{ColorLabel, Op, Photo, PhotoId, Source};
use serde_json::json;
use crate::{Result, Session};
fn session() -> Result<Session> {
let mut s = Session::new();
for (id, label) in [(1, Some(ColorLabel::Red)), (2, Some(ColorLabel::Purple)), (3, Some(ColorLabel::Blue)), (4, None)] {
let mut photo = Photo::new(
PhotoId(id),
Source::File { path: format!("/lightcraft-clear-label/{id}.jpg") },
&format!("{id}.jpg"),
"JPEG",
40,
30,
"2026-10-25",
);
photo.label = label;
s.catalog.apply(Op::AddPhoto { photo: Box::new(photo) })?;
}
s.execute("library.sort", &json!({"key": "fileName", "ascending": true}))?;
Ok(s)
}
fn labels(s: &Session) -> Vec<Option<ColorLabel>> {
(1..=4).map(|id| s.catalog.photo(PhotoId(id)).unwrap().label).collect()
}
#[test]
fn the_selected_photos_lose_their_label_in_one_undo_step() {
let mut s = session().unwrap();
s.execute("library.select", &json!({"ids": [1, 2], "active": 2})).unwrap();
s.execute("photo.clearLabel", &json!({})).unwrap();
assert_eq!(labels(&s), [None, None, Some(ColorLabel::Blue), None]);
assert_eq!(s.undo.len(), 1);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(labels(&s), [Some(ColorLabel::Red), Some(ColorLabel::Purple), Some(ColorLabel::Blue), None]);
}
#[test]
fn it_does_what_photo_label_none_does() {
let mut a = session().unwrap();
let mut b = session().unwrap();
for s in [&mut a, &mut b] {
s.execute("library.select", &json!({"ids": [3]})).unwrap();
}
a.execute("photo.clearLabel", &json!({})).unwrap();
b.execute("photo.label", &json!({"label": "none"})).unwrap();
assert_eq!(labels(&a), labels(&b));
assert_eq!(a.undo.len(), b.undo.len());
}
#[test]
fn ids_name_the_photos_and_an_unlabelled_or_unknown_photo_is_handled() {
let mut s = session().unwrap();
s.execute("library.select", &json!({"ids": [4]})).unwrap();
s.execute("photo.clearLabel", &json!({"ids": [1]})).unwrap();
assert_eq!(labels(&s), [None, Some(ColorLabel::Purple), Some(ColorLabel::Blue), None]);
// a photo without a label is left as it is, and a photo that does not exist is an error
s.execute("photo.clearLabel", &json!({"ids": [4]})).unwrap();
assert!(s.execute("photo.clearLabel", &json!({"ids": [999]})).is_err());
}
#[test]
fn it_has_no_key_of_its_own_and_is_listed_for_the_keyboard_shortcuts_editor() {
let spec = crate::command_specs().iter().find(|c| c.id == "photo.clearLabel").expect("registered");
assert_eq!(spec.shortcut, None, "the 6-9 keys are Lightroom's; none is given to clearing");
assert_eq!(spec.label, "Clear Color Label");
}
@@ -65,6 +65,7 @@ fn flags_and_labels_that_leave_the_view_go_on_to_the_next_photo_too() {
(json!({"flag": "pick"}), "photo.cycleFlag", json!({})),
(json!({"flag": "pick"}), "photo.decreaseFlag", json!({})),
(json!({"label": "red"}), "photo.label", json!({"label": "none"})),
(json!({"label": "red"}), "photo.clearLabel", json!({})),
(json!({}), "photo.increaseRating", json!({})),
] {
let mut s = one_star().unwrap();
+1 -1
View File
@@ -251,7 +251,7 @@ impl LightcraftApp {
self.ui.toast = Some((message, now + TOAST_SECONDS, None));
}
}
if r.is_ok() && id == "photo.label" {
if r.is_ok() && matches!(id, "photo.label" | "photo.clearLabel") {
let label = params.get("label").and_then(Value::as_str).and_then(lightcraft_catalog::ColorLabel::parse);
let text = match label {
Some(label) => {
+4 -1
View File
@@ -343,6 +343,7 @@ const HIDDEN: &[&str] = &[
"photo.reject",
"photo.unflag",
"photo.label",
"photo.clearLabel",
"library.sort",
"library.shuffle",
"album.addPhotos",
@@ -548,7 +549,8 @@ pub(crate) fn expanded(app: &LightcraftApp, name: &str) -> Option<Vec<MenuNode>>
})
.collect();
v.push(MenuNode::Separator);
v.push(item("photo.label", json!({"label": "none"}), "None", None, has, Some(active.as_ref().is_some_and(|p| p.label.is_none()))));
let clear_key = crate::shortcuts::shortcut_of(&app.ui.settings.keymap, "photo.clearLabel");
v.push(item("photo.clearLabel", Value::Null, "None", clear_key, has, Some(active.as_ref().is_some_and(|p| p.label.is_none()))));
v.push(MenuNode::Separator);
// label sets: each a checkable item; Edit… names them
let sets = lightcraft_engine::cmd::manage::label_sets_json(&app.session);
@@ -923,6 +925,7 @@ pub fn run_item(app: &mut LightcraftApp, id: &str, params: Value) -> Result<Valu
"photo.rate"
| "photo.flag"
| "photo.label"
| "photo.clearLabel"
| "photo.pick"
| "photo.reject"
| "photo.unflag"
+5 -1
View File
@@ -584,7 +584,7 @@ fn fire_rating_key(app: &mut LightcraftApp, ctx: &egui::Context, f: &str) -> boo
crate::i18n::tr_format!("Rated {}", "★".repeat(n.parse().unwrap_or(0)))
};
app.toast(ctx, label);
} else if matches!(f, "photo.decreaseRating" | "photo.increaseRating") {
} else if matches!(f, "photo.decreaseRating" | "photo.increaseRating" | "photo.clearLabel") {
cull(app, f, json!({}), false);
} else if let Some(l) = f.strip_prefix("label:") {
cull(app, "photo.label", json!({"label": l}), false);
@@ -796,6 +796,10 @@ mod tests;
#[path = "shortcuts/tests_relative_ratings.rs"]
mod relative_rating_tests;
#[cfg(test)]
#[path = "shortcuts/tests_clear_label.rs"]
mod clear_label_tests;
#[cfg(test)]
#[path = "shortcuts/tests_flag_cycle.rs"]
mod flag_cycle_tests;
@@ -0,0 +1,138 @@
//! Clear Color Label (`photo.clearLabel`) is a command of its own, so the keyboard shortcuts editor can give it a key (#2980).
use egui::{Key, Modifiers};
use lightcraft_catalog::{ColorLabel, Op, Photo, PhotoId, Source};
use serde_json::json;
use crate::LightcraftApp;
use crate::menubar::MenuNode;
use crate::state::ViewMode;
const KEY: &str = "Alt+Shift+L";
fn app() -> lightcraft_engine::Result<LightcraftApp> {
let mut s = lightcraft_engine::Session::new();
for id in 1..=3 {
let mut photo = Photo::new(
PhotoId(id),
Source::File { path: format!("/lightcraft-clear-label-keys/{id}.jpg") },
&format!("{id}.jpg"),
"JPEG",
40,
30,
"2026-10-25",
);
photo.label = Some(ColorLabel::Green);
s.catalog.apply(Op::AddPhoto { photo: Box::new(photo) })?;
}
s.execute("library.sort", &json!({"key": "fileName", "ascending": true}))?;
s.execute("library.select", &json!({"ids": [2]}))?;
let mut app = LightcraftApp::new(s, crate::Services { png: None, ..Default::default() });
super::assign(&mut app.ui.settings.keymap, "photo.clearLabel", Some(KEY)).unwrap();
Ok(app)
}
fn labels(app: &LightcraftApp) -> Vec<Option<ColorLabel>> {
(1..=3).map(|id| app.session.catalog.photo(PhotoId(id)).unwrap().label).collect()
}
fn frame(app: &mut LightcraftApp, ctx: &egui::Context, key: Option<(Key, Modifiers)>, text: Option<&mut String>) {
let events =
key.map(|(key, modifiers)| egui::Event::Key { key, physical_key: Some(key), pressed: true, repeat: false, modifiers }).into_iter().collect();
let mut text = text;
let mut out = ctx.run_ui(
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))), ..Default::default() },
|ui| {
if let Some(text) = text.as_deref_mut() {
ui.add(egui::TextEdit::singleline(text).id(egui::Id::unique("clear-label-search"))).request_focus();
}
super::handle(app, ui.ctx());
},
);
out.textures_delta.clear();
}
fn press(app: &mut LightcraftApp, ctx: &egui::Context) {
frame(app, ctx, Some((Key::L, Modifiers::ALT | Modifiers::SHIFT)), None);
}
#[test]
fn clearing_a_label_has_no_key_until_one_is_given() {
assert_eq!(super::shortcut_of(&Default::default(), "photo.clearLabel"), None);
assert!(super::find_bindable("photo.clearLabel").is_some(), "the shortcuts editor lists it");
let mut app = app().unwrap();
assert_eq!(super::shortcut_of(&app.ui.settings.keymap, "photo.clearLabel"), Some(KEY));
// the number keys keep Lightroom's meaning: 6-9 set a label, nothing is taken from them
app.ui.view = ViewMode::PhotoGrid;
let ctx = egui::Context::default();
frame(&mut app, &ctx, Some((Key::Num6, Modifiers::NONE)), None);
assert_eq!(labels(&app)[1], Some(ColorLabel::Red));
}
#[test]
fn the_key_clears_the_selected_photos_label_in_grid_and_loupe() {
for view in [ViewMode::PhotoGrid, ViewMode::SquareGrid, ViewMode::Detail] {
let mut app = app().unwrap();
app.ui.view = view;
let ctx = egui::Context::default();
press(&mut app, &ctx);
assert_eq!(labels(&app), [Some(ColorLabel::Green), None, Some(ColorLabel::Green)], "{view:?}");
assert_eq!(app.session.undo.len(), 1);
assert_eq!(app.ui.toast.as_ref().map(|t| t.0.as_str()), Some("Color label cleared"));
app.session.execute("edit.undo", &json!({})).unwrap();
assert_eq!(labels(&app)[1], Some(ColorLabel::Green));
}
}
#[test]
fn the_key_does_nothing_while_a_text_field_has_focus() {
let mut app = app().unwrap();
app.ui.view = ViewMode::PhotoGrid;
let ctx = egui::Context::default();
let mut text = String::new();
frame(&mut app, &ctx, None, Some(&mut text));
frame(&mut app, &ctx, Some((Key::L, Modifiers::ALT | Modifiers::SHIFT)), Some(&mut text));
assert!(ctx.egui_wants_keyboard_input());
assert_eq!(labels(&app), [Some(ColorLabel::Green); 3]);
assert!(app.session.undo.is_empty());
}
#[test]
fn compare_clears_only_the_active_photo_and_auto_advance_moves_on() {
let mut app = app().unwrap();
app.ui.view = ViewMode::Compare;
let ctx = egui::Context::default();
app.session.execute("library.select", &json!({"ids": [1, 2], "active": 2})).unwrap();
press(&mut app, &ctx);
assert_eq!(labels(&app), [Some(ColorLabel::Green), None, Some(ColorLabel::Green)]);
app.ui.view = ViewMode::PhotoGrid;
app.ui.auto_advance = true;
app.session.execute("library.select", &json!({"ids": [1]})).unwrap();
press(&mut app, &ctx);
assert_eq!(labels(&app), [None, None, Some(ColorLabel::Green)]);
assert_eq!(app.session.active(), Some(PhotoId(2)));
}
fn find<'a>(nodes: &'a [MenuNode], id: &str) -> Option<&'a MenuNode> {
nodes.iter().find_map(|n| match n {
MenuNode::Item { id: i, .. } if i == id => Some(n),
MenuNode::Submenu { children, .. } => find(children, id),
_ => None,
})
}
#[test]
fn the_menus_none_row_is_the_command_and_shows_its_key() {
let mut app = app().unwrap();
let bar = crate::menubar::menu_bar(&app);
let all: Vec<MenuNode> = bar.into_iter().flat_map(|(_, v)| v).collect();
let Some(MenuNode::Item { shortcut, label, .. }) = find(&all, "photo.clearLabel") else {
panic!("Photo ▸ Set Color Label ▸ None is Clear Color Label")
};
assert_eq!(label, "None");
assert_eq!(shortcut.as_deref(), Some(KEY));
// and the row behaves like the key: Compare clears only the active photo
app.ui.view = ViewMode::Compare;
app.session.execute("library.select", &json!({"ids": [1, 2], "active": 2})).unwrap();
crate::menubar::run_item(&mut app, "photo.clearLabel", serde_json::Value::Null).unwrap();
assert_eq!(labels(&app), [Some(ColorLabel::Green), None, Some(ColorLabel::Green)]);
}
+3 -1
View File
@@ -29,7 +29,9 @@ the photo, its edits or the last edit Undo step, and it works in any view that h
It can be reassigned or disabled in Help → Keyboard Shortcuts; text fields and dialogs keep the key.
Purple and clearing a colour label are available in Photo → Set Color Label and the photo context
menu. Purple has no default number key, matching Lightroom Classic. Rejection marks a photo for
menu. Purple has no default number key, matching Lightroom Classic. Clear Color Label has none either,
but it is a command of its own: give it a key in Help → Keyboard Shortcuts and the key clears the label of
the selected photos, as the Photo → Set Color Label → None row does (which then shows the key). Rejection marks a photo for
culling; it does not delete the original or remove it from the library.
Setting a colour label shows the same brief bottom toast as a rating, using the colour or your
custom label name. Clearing it shows a confirmation too, whether applied by a key, menu or swatch.
+4 -3
View File
@@ -102,7 +102,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
| LR-LIB-TARGET | Target album | P2 | ✅ | `cmd:album.setTarget`, `cmd:album.toggleTarget` | see LRC-LIB-COLLECTIONS |
| LR-LIB-RATING | Star ratings | P0 | ✅ | `cmd:photo.rate` (`advance`), `cmd:photo.decreaseRating`, `cmd:photo.increaseRating`, `crates/ui-egui/src/shortcuts.rs`, `crates/ui-egui/src/widgets/rating.rs` | one shared star row in the bottom bar, Info panel and filter bar (hover previews, ←/→ on a focused star) and one painter for badges (grid cells, filmstrip, Compare caption); menu rows with star text (Photo ▸ Set Rating, Select By Rating, the grid context menu) are separate menu items, unchanged; 0 clears; 1–5 set; Shift+0–5 applies and advances once, including with Auto Advance. A rating, flag or label that takes the active photo out of the view (filter bar, Picks, smart album) makes the photo after it active (the one before at the end), and Shift / Auto Advance doesn't step past it (#2312). Default grid brackets adjust each photo by one, clamped to 0–5; one batch undo and AutoWrite. Batch grid and undo/redo regressions; macOS native ownership corrected (PR #261) |
| LR-LIB-FLAG | Pick / reject flags | P0 | ✅ | `cmd:photo.pick`, `cmd:photo.reject`, `cmd:photo.unflag`, `cmd:photo.flag` | P pick, X reject, U clear; Shift+P picks and advances in both grids (Presets elsewhere); Shift+X/U and legacy Shift+Z advance; no flag cycle; rejection does not delete the file; a pick shows as a filled flag on thumbnails, in Compare, in the loupe and on the Pick button, a reject outlined (#1013) |
| LR-LIB-LABEL | Colour labels | P1 | ✅ | `cmd:photo.label`, `cmd:label.setNames`, `cmd:label.names`, `cmd:dialog.labelNames`, keys 6–9 in `crates/ui-egui/src/shortcuts.rs` | Photo menu and grid context menu (coloured, named), Info-panel swatches, label dot plus translucent colour on square-grid/filmstrip surrounds and photo-grid footers (photo pixels unchanged); colour-label toasts have pale matching backgrounds; setting/clearing through keys (including Shift), menus or Info swatches shows the same bottom toast as ratings, with the colour/custom name; editable label names (undoable, journaled); no purple key (as in Lightroom), no custom extra labels |
| LR-LIB-LABEL | Colour labels | P1 | ✅ | `cmd:photo.label`, `cmd:photo.clearLabel`, `cmd:label.setNames`, `cmd:label.names`, `cmd:dialog.labelNames`, keys 6–9 in `crates/ui-egui/src/shortcuts.rs` | Photo menu and grid context menu (coloured, named), Info-panel swatches, label dot plus translucent colour on square-grid/filmstrip surrounds and photo-grid footers (photo pixels unchanged); colour-label toasts have pale matching backgrounds; setting/clearing through keys (including Shift), menus or Info swatches shows the same bottom toast as ratings, with the colour/custom name; editable label names (undoable, journaled); no purple key (as in Lightroom), Clear Color Label (`photo.clearLabel`) has no key until one is given in Help → Keyboard Shortcuts, no custom extra labels |
| LR-LIB-KEYWORD | Keywords | P0 | ✅ | `cmd:panel.keywords`, `cmd:photo.setMeta` (`addKeywords`/`removeKeywords`), `cmd:keyword.list`, `cmd:keyword.rename`, `cmd:keyword.delete`, `cmd:keyword.merge`, `cmd:keyword.suggest`, `crates/catalog/src/keywords.rs`, `crates/ui-egui/src/panels/left.rs` | left-panel keyword tree with counts (click filters, children included; context menu rename / merge / delete / add / remove); library-wide ops are one undo step and replay from the op log; suggestions (co-occurring / most used / completions) in the Keywords panel; keywords reorganize by drag and drop in the Keyword List (LRC-LIB-KEYWORDS) |
| LR-LIB-PEOPLE | People / faces | P2 | 🟡 | `cmd:faces.detect`, `cmd:view.people`, `cmd:view.faceBoxes`, `cmd:photo.removeRegion`, `cmd:photo.setRegion`, `cmd:library.filter`, `crates/ui-egui/src/panels/people.rs`, `crates/catalog/src/query.rs` (`people`), `crates/engine/src/media.rs` (`face_job`), `crates/meta/src/xmp.rs` | MVP: named face regions read from XMP (MWG-RS, read-only, see `docs/xmp-interop.md`; clipped to the photo; Lightroom's `Rotation` of ±π/2 and π handled, mirrored orientations taken as written; boxes follow Rotate Left/Right and flips; a sidecar stating `mwg-rs:Regions`, even empty, replaces the photo's on re-read, one without leaves them) become cards in the People view (View ▸ People, or the People button in the toolbar): a close-up of the person's largest face, name and photo count; a click filters the grid (`person` in `library.filter`, `person:` search token, filter chip). Face boxes in the loupe can be switched off (View ▸ Face Boxes, toolbar) resized with eight drag handles and removed with the × shown on hover (catalog-only and undoable, one undo step per drag; the sidecar is never rewritten for either); no moving a box by dragging it yet. Photo ▸ Detect Faces (`cmd:faces.detect`) finds faces with the YuNet detector (pure Rust, about 200 ms a photo, matches OpenCV's own YuNet on a 45-photo test set; the 232 KB model is a one-time download offered in the same step, never part of LightCraft) and adds them as unnamed boxes; names are suggested from the faces already named (recognition models are opt-in downloads, run on the CPU in a background thread; a face is named only when you confirm it); a person's page shows their cropped faces and the unnamed faces that look like them (LRC-LIB-PEOPLE), with a user-triggered ↑ action or targeted A key to name all shown suggestions at or above one score in one undo step; with recognition on, a background scan finds faces in photos that have none and embeds every face (one to three worker threads, raws through their embedded preview); an Unnamed faces section orders faces by likeness and names a selected group at once; no separated clusters, no manual tagging, no writing regions back |
| LR-LIB-STACK | Stacks | P1 | ✅ | `cmd:stack.group`, `cmd:stack.ungroup`, `cmd:stack.toggle`, `cmd:stack.setTop`, `cmd:stack.remove`, `cmd:stack.auto`, `cmd:photo.delete`, `crates/catalog/src/stacks.rs` | grid/filmstrip count badges (a click expands / collapses the stack, selected or not), expand/collapse (a click or double-click on a grid badge toggles the stack once, #2233), auto-stack by capture time; delete / remove folder drop the photo from the stack so `i/n` updates (#1493); no visual-similarity auto-stack |
@@ -557,7 +557,7 @@ visible menu bar yet: the menu model is only exposed through the control channel
| MENU-PHOTO-REMOVEFROMALBUM | Remove from album | P0 | ✅ | `cmd:album.removePhotos` | Photo ▸ Remove from Album, shown in a normal album only, carries the ⌫ shortcut there (Delete Photo then has none) (issue #1805) |
| MENU-PHOTO-RATE | Rate submenu | P0 | ✅ | `cmd:photo.rate` | |
| MENU-PHOTO-FLAG | Flag submenu | P0 | ✅ | `cmd:photo.flag` | |
| MENU-PHOTO-LABEL | Colour label submenu | P1 | ✅ | `cmd:photo.label`, `cmd:dialog.labelNames` | shows custom names; Edit Label Names… |
| MENU-PHOTO-LABEL | Colour label submenu | P1 | ✅ | `cmd:photo.label`, `cmd:photo.clearLabel`, `cmd:dialog.labelNames` | shows custom names; None is Clear Color Label and shows the key given to it; Edit Label Names… |
| MENU-PHOTO-ROTATE | Rotate left / right | P0 | ✅ | `cmd:photo.rotateLeft`, `cmd:photo.rotateRight` | |
| MENU-PHOTO-FLIP | Flip horizontal / vertical | P0 | ✅ | `cmd:photo.flipHorizontal`, `cmd:photo.flipVertical` | |
| MENU-PHOTO-CREATEVERSION | Create Version… | P1 | ✅ | `cmd:version.create` | no name prompt |
@@ -655,7 +655,7 @@ From `06-shortcuts.md` part 1. Evidence is our binding; conflicts are explained
| KEY-UNFLAG | Unflag — U | P0 | ✅ | `cmd:photo.unflag` | |
| KEY-REJECT | Reject — X | P0 | ✅ | `cmd:photo.reject` | swaps crop aspect while cropping |
| KEY-RATING | Ratings — 0…5 | P0 | ✅ | `crates/ui-egui/src/shortcuts.rs` → `cmd:photo.rate` | 0 clears, 1–5 set; Shift applies and advances; native macOS plain keys stay with egui; grid multi-selection and undo/redo tested |
| KEY-LABELS | Labels — 6…9 | P1 | ✅ | `crates/ui-egui/src/shortcuts.rs` → `cmd:photo.label` | 6 red, 7 yellow, 8 green, 9 blue; Shift+6–9 labels and advances; purple/none available from menus |
| KEY-LABELS | Labels — 6…9 | P1 | ✅ | `crates/ui-egui/src/shortcuts.rs` → `cmd:photo.label` | 6 red, 7 yellow, 8 green, 9 blue; Shift+6–9 labels and advances; purple/none available from menus; Clear Color Label can be given a key in Help → Keyboard Shortcuts (#2980) |
| KEY-MASKING | Masking — M | P0 | ✅ | `cmd:panel.masking` | |
| KEY-ERASE | Erase while held — ⌥ | P0 | ✅ | `crates/ui-egui/src/panels/detail.rs`, `crates/ui-egui/src/panels/masking.rs` | ⌥ while painting flips Add ↔ Erase for that stroke; Add/Erase buttons too |
| KEY-RATEADVANCE | Rate and advance — ⇧0…5 | P1 | ✅ | `crates/ui-egui/src/shortcuts.rs` → `cmd:photo.rate` (`advance`) | |
@@ -799,6 +799,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
| Date | Change | Summary |
|---|---|---|
| 2026-10-27 | patch | LR-LIB-LABEL, MENU-PHOTO-LABEL, KEY-LABELS: Clear Color Label (`photo.clearLabel`) is a command of its own, so Help → Keyboard Shortcuts can give it a key (#2980); the Photo ▸ Set Color Label ▸ None row runs it; status unchanged |
| 2026-10-25 | patch | LR-BEHAV-TITLEBAR: the top bar's title-bar gestures come from craft-window's `title_bar`: a double-click follows the system setting, and a press on a button, the search field or a panel floating over the bar never moves the window; status counts unchanged |
| 2026-10-24 | minor | KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 (`keyword.nextSet`, `keyword.previousSet`) go to the next / previous keyword set; status counts unchanged (KEYC-METADATA stays 🟡) |
| 2026-10-24 | minor | KEYC-METADATA: ⌘K / Ctrl+K (`panel.addKeywords`, Add Keywords) shows the Keywords panel, opens Keywording and puts the cursor in the Add keyword box; status counts unchanged (KEYC-METADATA stays 🟡) |
+9
View File
@@ -1,5 +1,14 @@
# What's new in LightCraft
## 2026-10-30
### A keyboard shortcut for Clear Color Label
- Clear Color Label is now a command of its own in Help ▸ Keyboard Shortcuts, so it can have a key like Set Rating or
Set Flag; before, only the red, yellow, green and blue labels (6–9) had keys and clearing a label was menu-only. It has
no key until you give it one. Photo ▸ Set Color Label ▸ None runs it and shows its key. The key clears the label of
the selected photos as one undo step, only the active photo in Compare and Survey, and moves on with Auto Advance,
as the other label keys do.
## 2026-10-27
### The Edit menu groups Select Only Active Photo and Paste Settings from Previous with their kin