diff --git a/crates/engine/src/cmd/slide.rs b/crates/engine/src/cmd/slide.rs index 8002aac9..e54f9249 100644 --- a/crates/engine/src/cmd/slide.rs +++ b/crates/engine/src/cmd/slide.rs @@ -28,6 +28,7 @@ pub fn specs() -> Vec { 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 { 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 { + 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 = 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 { let targets: Vec = 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::>()); } + + 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 { + let st = s.doc().expect("doc"); + let mut v: Vec = 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()); + } } diff --git a/crates/ui-egui/src/sorter.rs b/crates/ui-egui/src/sorter.rs index 3c840860..5a0e30ee 100644 --- a/crates/ui-egui/src/sorter.rs +++ b/crates/ui-egui/src/sorter.rs @@ -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})); } diff --git a/crates/ui-egui/src/thumbs.rs b/crates/ui-egui/src/thumbs.rs index 4e3f4f0b..d1841224 100644 --- a/crates/ui-egui/src/thumbs.rs +++ b/crates/ui-egui/src/thumbs.rs @@ -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 = 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})); }