Slides: Ctrl+Shift+click selects the range, also in the slide sorter (#1105)

New slide.selectRange {to, from?, add?} selects every slide between the current one (or
`from`) and `to`. Shift+click in the slide list and the slide sorter selects that range;
Ctrl+Shift+click adds it to the slides already selected. The sorter had no range selection
before, and in the list Ctrl+Shift+click used to add only the clicked slide.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Faabyy
2026-10-25 11:48:27 +00:00
co-authored by Claude
parent a0eeb341d2
commit 29bd07e1b8
3 changed files with 88 additions and 9 deletions
+80
View File
@@ -28,6 +28,7 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!(noundo "slide.previous", "Previous Slide", [], Some("PageUp"), "{}", has_slide, prev),
cmd!(noundo "slide.first", "First Slide", [], Some("Home"), "{}", has_slide, first),
cmd!(noundo "slide.last", "Last Slide", [], Some("End"), "{}", has_slide, last),
cmd!(noundo "slide.selectRange", "Select Slide Range", [], None, "{to: index, from?: index (default the current slide), add?: bool (keep the slides already selected)}", has_slide, select_range),
cmd!(noundo "slide.selectSlides", "Select Slides", [], None, "{ids?: [slide id], indices?: [index], add?: bool}", has_slide, select_slides),
cmd!("slide.hide", "Hide Slide", ["Slide Show", "Set Up"], None, "{index?, hidden?: bool}", has_slide, hide),
cmd!("slide.layout", "Layout", ["Home", "Slides"], None, "{layout: name or kind, index?}", has_slide, set_layout),
@@ -372,6 +373,37 @@ fn select_slides(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"selected": s.doc()?.selection.slides}))
}
/// Shift-click in the slide list: select every slide from `from` (default the current one) to `to`.
/// With `add` the slides already selected stay selected (Ctrl+Shift-click).
fn select_range(s: &mut Session, p: &Value) -> Result<Value> {
let to = usize_param(p, "to").ok_or_else(|| bad("slide.selectRange", "missing `to`"))?;
let add = bool_or(p, "add", false);
let (from, count) = {
let st = s.doc()?;
(usize_param(p, "from").unwrap_or(st.selection.slide), st.doc.slides.len())
};
if to >= count || from >= count {
return Err(bad("slide.selectRange", "no such slide"));
}
let (a, b) = (from.min(to), from.max(to));
s.select(|doc, sel| {
let range: Vec<SlideId> = doc.slides.iter().skip(a).take(b - a + 1).map(|x| x.id).collect();
if add {
for id in range {
if !sel.slides.contains(&id) {
sel.slides.push(id);
}
}
} else {
sel.slides = range;
}
sel.slide = to;
sel.shapes.clear();
sel.text = None;
})?;
Ok(json!({"selected": s.doc()?.selection.slides}))
}
fn hide(s: &mut Session, p: &Value) -> Result<Value> {
let targets: Vec<usize> = match opt_usize_param(p, "index", "slide.hide")? {
Some(i) => vec![i],
@@ -697,4 +729,52 @@ mod tests {
let want = [(0, "Level one"), (1, "Level two"), (2, "Level three"), (0, "Level one again")];
assert_eq!(got, want.iter().map(|(l, t)| (*l, t.to_string())).collect::<Vec<_>>());
}
fn deck_of(n: usize) -> Session {
let mut s = Session::with_new();
while s.doc().expect("doc").doc.slides.len() < n {
s.execute("slide.new", &json!({})).expect("new slide");
}
s
}
fn selected_indices(s: &Session) -> Vec<usize> {
let st = s.doc().expect("doc");
let mut v: Vec<usize> = st.selection.slides.iter().filter_map(|id| st.doc.slide_index(*id)).collect();
v.sort_unstable();
v
}
#[test]
fn select_range_covers_every_slide_between() {
let mut s = deck_of(6);
s.execute("slide.go", &json!({"index": 1})).expect("go");
s.execute("slide.selectRange", &json!({"to": 4})).expect("range");
assert_eq!(selected_indices(&s), vec![1, 2, 3, 4]);
assert_eq!(s.doc().expect("doc").selection.slide, 4);
// Backwards works too, and replaces the earlier selection.
s.execute("slide.selectRange", &json!({"from": 5, "to": 3})).expect("range");
assert_eq!(selected_indices(&s), vec![3, 4, 5]);
}
#[test]
fn select_range_add_keeps_the_existing_selection() {
let mut s = deck_of(8);
s.execute("slide.selectSlides", &json!({"indices": [0, 1]})).expect("select");
s.execute("slide.go", &json!({"index": 4})).expect("go");
s.execute("slide.selectSlides", &json!({"indices": [4]})).expect("select");
s.execute("slide.selectRange", &json!({"to": 6, "add": true})).expect("range");
assert_eq!(selected_indices(&s), vec![4, 5, 6]);
s.execute("slide.selectSlides", &json!({"indices": [0]})).expect("select");
s.execute("slide.selectRange", &json!({"from": 3, "to": 5, "add": true})).expect("range");
assert_eq!(selected_indices(&s), vec![0, 3, 4, 5]);
}
#[test]
fn select_range_rejects_out_of_range_slides() {
let mut s = deck_of(3);
assert!(s.execute("slide.selectRange", &json!({"to": 3})).is_err());
assert!(s.execute("slide.selectRange", &json!({"from": 9, "to": 1})).is_err());
assert!(s.execute("slide.selectRange", &json!({})).is_err());
}
}
+4 -2
View File
@@ -229,8 +229,10 @@ fn place_slides(app: &mut SlideApp, ui: &mut Ui, g: &Grid<'_>, budget: &mut u32,
p.text(pos2(img.max.x, img.max.y + 12.0), Align2::RIGHT_CENTER, format!("★ {}", tr.kind), theme::font(10.5), t.text_faint);
}
if resp.clicked() {
let add = ui.input(|inp| inp.modifiers.command);
if add {
let (shift, add) = ui.input(|inp| (inp.modifiers.shift, inp.modifiers.command));
if shift {
let _ = app.run("slide.selectRange", json!({"to": i, "add": add}));
} else if add {
if let Some(id) = id {
let _ = app.run("slide.selectSlides", json!({"ids": [id.0], "add": true}));
}
+4 -7
View File
@@ -158,14 +158,11 @@ fn list(app: &mut SlideApp, ui: &mut Ui) {
if resp.clicked() {
let (shift, cmd) = ui.input(|i| (i.modifiers.shift, i.modifiers.command));
if let Some(id) = id {
if cmd {
if shift {
// Shift: the slides between the current one and this one. Ctrl+Shift keeps what is selected.
let _ = app.run("slide.selectRange", json!({"to": i, "add": cmd}));
} else if cmd {
let _ = app.run("slide.selectSlides", json!({"ids": [id.0], "add": true}));
} else if shift {
let (a, b) = (cur.min(i), cur.max(i));
let ids: Vec<u32> = doc.slides.iter().skip(a).take(b - a + 1).map(|s| s.id.0).collect();
let _ = app.run("slide.selectSlides", json!({"ids": ids}));
let _ = app.run("slide.go", json!({"index": i}));
let _ = app.run("slide.selectSlides", json!({"ids": ids}));
} else {
let _ = app.run("slide.go", json!({"index": i}));
}