Rate under the pointer, filter a star range, and name Help as help
Benchmarks / CPU and I/O (per commit) (push) Successful in 1m53s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 2m50s
Build and test / Layer separation (push) Successful in 33s
Traceability / Requirement traces (push) Successful in 40s
🐳 Android image / Build and push (push) Successful in 4s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 4s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Successful in 14m19s
Build and test / Windows (x86_64, cross) (push) Successful in 17m50s
Build and test / Publish the release (push) Skipped
Benchmarks / CPU and I/O (per commit) (push) Successful in 1m53s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 2m50s
Build and test / Layer separation (push) Successful in 33s
Traceability / Requirement traces (push) Successful in 40s
🐳 Android image / Build and push (push) Successful in 4s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 4s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Successful in 14m19s
Build and test / Windows (x86_64, cross) (push) Successful in 17m50s
Build and test / Publish the release (push) Skipped
Rating keys in the grid follow darktable's rule: with the pointer over a photograph outside the selection, 0-5, P, X and U judge that photograph alone; over one inside it, the whole selection, as before; off the grid, the selection. The hover is cleared when the grid scrolls, so a key after a wheel turn cannot judge whatever used to be under the pointer. Holding F and tapping digits filters by stars: one digit for exactly that many, two for everything between them, F alone to show every rating again. The filter gains a ceiling to do it (`max_rating`, one BETWEEN in the query). The place record carries it, and a record from an older build reads as having none. The star chips light across a capped range and the bar says "2-3★ only" beside them. The grid also takes Ctrl+E and Ctrl+Shift+E for the selection, Ctrl+V to paste onto it and Ctrl+A to select all. The "Gestures" button is now "Help", its sheet "Controls and shortcuts", and F1 opens it.
This commit is contained in:
@@ -36,6 +36,10 @@ pub(super) fn wire_filter_ratings_and_people(window: &AppWindow, ctl: &Rc<Librar
|
||||
.on_library_filter_min_rating_changed(move |n| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
ctl.filter.borrow_mut().min_rating = n.clamp(0, 5) as u8;
|
||||
// The chips say "n or more", so a chip replaces a range
|
||||
// rather than narrowing it from below.
|
||||
ctl.filter.borrow_mut().max_rating = None;
|
||||
w.global::<Library>().set_library_filter_max_rating(5);
|
||||
// Stars and "unrated" are contradictory terms — asking for four
|
||||
// stars *and* nothing judged matches nothing at all, which reads
|
||||
// as a broken filter rather than an impossible question.
|
||||
@@ -50,6 +54,38 @@ pub(super) fn wire_filter_ratings_and_people(window: &AppWindow, ctl: &Rc<Librar
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-UI-5
|
||||
// A star range from the keyboard: hold F and tap one digit for exactly
|
||||
// that many stars, two for everything between them. Five as the ceiling
|
||||
// is no ceiling, so "F, 2, 5" and the "2+" chip are the same filter and
|
||||
// the bar shows them the same way.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_filter_rating_range(move |low, high| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let (low, high) = (low.clamp(0, 5), high.clamp(0, 5));
|
||||
let (low, high) = (low.min(high), low.max(high));
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.min_rating = low as u8;
|
||||
f.max_rating = (high < 5).then_some(high as u8);
|
||||
// As for the chips: stars and "nothing judged" cannot
|
||||
// both hold once a star is asked for.
|
||||
if low > 0 {
|
||||
f.unjudged = false;
|
||||
}
|
||||
}
|
||||
let lib = w.global::<Library>();
|
||||
lib.set_library_filter_min_rating(low);
|
||||
lib.set_library_filter_max_rating(high);
|
||||
lib.set_library_filter_unjudged(ctl.filter.borrow().unjudged);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
@@ -63,12 +99,14 @@ pub(super) fn wire_filter_ratings_and_people(window: &AppWindow, ctl: &Rc<Librar
|
||||
// See above: the two cannot both hold.
|
||||
if on {
|
||||
f.min_rating = 0;
|
||||
f.max_rating = None;
|
||||
f.flag = None;
|
||||
}
|
||||
}
|
||||
w.global::<Library>().set_library_filter_unjudged(on);
|
||||
if on {
|
||||
w.global::<Library>().set_library_filter_min_rating(0);
|
||||
w.global::<Library>().set_library_filter_max_rating(5);
|
||||
w.global::<Library>().set_library_filter_flag(0);
|
||||
}
|
||||
refilter(&w, &ctl);
|
||||
|
||||
@@ -931,16 +931,23 @@ pub(super) fn wire_ratings_and_flags(
|
||||
}
|
||||
|
||||
// A rating or flag key. Applies to the whole selection, which is what
|
||||
// makes judging a run of frames one keystroke rather than forty.
|
||||
// makes judging a run of frames one keystroke rather than forty — unless
|
||||
// the pointer is over a photograph outside it, which then takes the key
|
||||
// alone (darktable's rule; see `judged_images`).
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let coll_for_keys = coll_ctl.clone();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_judged(move |rating, flag| {
|
||||
.on_library_judged(move |rating, flag, hovered| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let chosen = coll_for_keys.selected();
|
||||
let offset = w.global::<Library>().get_library_offset().max(0) as usize;
|
||||
let under_pointer = usize::try_from(hovered)
|
||||
.ok()
|
||||
.and_then(|ordinal| ordinal.checked_sub(offset))
|
||||
.and_then(|row| ctl.visible_ids().get(row).copied());
|
||||
let chosen = judged_images(coll_for_keys.selected(), under_pointer);
|
||||
|
||||
// Exactly one axis is meant per keystroke; the other arrives as
|
||||
// -1 so a star press cannot disturb a flag or the reverse.
|
||||
@@ -953,6 +960,24 @@ pub(super) fn wire_ratings_and_flags(
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-UI-5
|
||||
/// What a judgement key applies to: the photograph under the pointer when it
|
||||
/// is not part of the selection, and the selection otherwise.
|
||||
///
|
||||
/// Hovering one of the selected frames rates all of them, so the pointer
|
||||
/// resting on the grid never shrinks a forty-frame judgement to one. With
|
||||
/// the pointer off the grid, or over a cell the grid has since scrolled away
|
||||
/// from, it is the selection as it always was.
|
||||
fn judged_images(
|
||||
selection: Vec<dr_types::ImageId>,
|
||||
under_pointer: Option<dr_types::ImageId>,
|
||||
) -> Vec<dr_types::ImageId> {
|
||||
match under_pointer {
|
||||
Some(id) if !selection.contains(&id) => vec![id],
|
||||
_ => selection,
|
||||
}
|
||||
}
|
||||
|
||||
/// --- keywords (FR-CAT-5, FR-CAT-6) ------------------------------------
|
||||
///
|
||||
/// Three callbacks and no state of their own: the sheet's open/shut is local
|
||||
@@ -1003,6 +1028,27 @@ pub(super) fn wire_keywords(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// --- what a judgement key reaches (FR-UI-5) ----------------------------
|
||||
|
||||
#[test]
|
||||
fn a_hovered_photograph_outside_the_selection_is_judged_alone() {
|
||||
let (a, b, c) = (dr_types::ImageId(1), dr_types::ImageId(2), dr_types::ImageId(3));
|
||||
assert_eq!(judged_images(vec![a, b], Some(c)), vec![c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hovering_a_selected_photograph_judges_the_whole_selection() {
|
||||
let (a, b) = (dr_types::ImageId(1), dr_types::ImageId(2));
|
||||
assert_eq!(judged_images(vec![a, b], Some(b)), vec![a, b]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_nothing_hovered_the_selection_is_judged() {
|
||||
let (a, b) = (dr_types::ImageId(1), dr_types::ImageId(2));
|
||||
assert_eq!(judged_images(vec![a, b], None), vec![a, b]);
|
||||
assert_eq!(judged_images(vec![], None), vec![]);
|
||||
}
|
||||
|
||||
// --- what the status line says about a keyword (FR-CAT-5) -------------
|
||||
//
|
||||
// Split out from the callback for the same reason `decide_drop` is: the
|
||||
|
||||
@@ -903,6 +903,9 @@ fn push_filter(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_filter_min_rating(filter.min_rating as i32);
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_filter_max_rating(filter.max_rating.map_or(5, i32::from));
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_filter_unjudged(filter.unjudged);
|
||||
|
||||
Reference in New Issue
Block a user