Slides: Ctrl+A in the slide pane selects every slide (#188)

The UI remembers whether the last press landed in the thumbnail pane (the sorter always counts) and passes scope slides to edit.selectAll in that case.
This commit is contained in:
Isaac Levine
2026-10-11 22:53:29 -04:00
parent 96abeba71b
commit be019fd615
3 changed files with 61 additions and 1 deletions
+51
View File
@@ -209,6 +209,8 @@ pub struct SlideApp {
pub(crate) notes_buf: (u64, usize, String),
/// Thumbnail pane drag (slide index, current drop index).
pub(crate) thumb_drag: Option<(usize, usize)>,
/// The slide thumbnail pane was the last thing clicked, so ⌘A selects slides, not shapes.
pub(crate) slides_focused: bool,
}
impl SlideApp {
@@ -244,6 +246,7 @@ impl SlideApp {
quit_confirmed: false,
notes_buf: (0, usize::MAX, String::new()),
thumb_drag: None,
slides_focused: false,
}
}
@@ -365,6 +368,12 @@ impl SlideApp {
Ok(Value::Null)
}
/// Whether ⌘A should act on the slide list: always in the sorter, in Normal view once the
/// thumbnail pane has been clicked.
pub(crate) fn slides_list_focused(&self) -> bool {
self.ui.view == ViewMode::Sorter || (self.ui.view == ViewMode::Normal && self.slides_focused)
}
pub fn start_show(&mut self, from: usize, reading: bool) {
self.media.stop_owned(media::Owner::Editor);
if let Some(d) = self.session.active() {
@@ -938,4 +947,46 @@ mod tests {
app.run("app.home", json!({})).unwrap();
assert!(!app.home_open() && app.home.is_none(), "with nothing open the start screen shows anyway");
}
/// #188: Ctrl/Cmd+A with the slide thumbnail pane focused selects every slide; with the
/// canvas focused it still selects shapes.
#[test]
fn select_all_follows_the_focused_pane() {
let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
SlideApp::new(Session::with_new(), Services::default()),
);
harness.run_steps(2);
for _ in 0..2 {
harness.state_mut().run("slide.new", json!({})).unwrap();
}
harness.state_mut().run("slide.go", json!({"index": 0})).unwrap();
harness.run_steps(3);
let click = |h: &mut egui_kittest::Harness<'_, SlideApp>, pos: egui::Pos2| {
h.event(egui::Event::PointerMoved(pos));
for pressed in [true, false] {
h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE });
}
h.run_steps(2);
};
// Press in the thumbnail pane, then Cmd+A.
let w = harness.state().ui.thumbs_width;
click(&mut harness, egui::pos2(w / 2.0, 300.0));
assert!(harness.state().slides_focused);
harness.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
harness.run_steps(2);
let st = harness.state().session.active().unwrap();
assert_eq!(st.selection.slides.len(), 3, "every slide is selected");
// A press on the canvas moves focus away: Cmd+A selects shapes again.
click(&mut harness, egui::pos2(700.0, 400.0));
assert!(!harness.state().slides_focused);
harness.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
harness.run_steps(2);
let st = harness.state().session.active().unwrap();
assert!(!st.selection.shapes.is_empty(), "canvas select all picks the shapes");
}
}
+5
View File
@@ -10,6 +10,11 @@ use crate::theme::{self, Tokens};
/// Shortcuts the UI handles itself (views, show, zoom, palette, file dialogs). True when consumed.
pub fn ui_shortcut(app: &mut SlideApp, key: egui::Key, m: Mods) -> bool {
use egui::Key::*;
if key == A && m.cmd && !m.shift && !m.alt && app.slides_list_focused() {
// The engine can't see which pane has focus, so the UI says so through the scope param.
let _ = app.run("edit.selectAll", json!({"scope": "slides"}));
return true;
}
let id = match (key, m.cmd, m.shift, m.alt) {
(F5, _, false, _) => {
app.start_show(0, false);
+5 -1
View File
@@ -11,7 +11,7 @@ use crate::theme::{self, Tokens};
pub fn show(app: &mut SlideApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let in_master = app.session.active().is_some_and(|d| d.selection.target != Target::Slides);
egui::Panel::left("thumbs")
let panel = egui::Panel::left("thumbs")
.resizable(true)
.default_size(app.ui.thumbs_width)
.size_range(150.0..=420.0)
@@ -28,6 +28,10 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
ui.add_space(20.0);
});
});
// Focus follows the last press: inside the pane keeps it, anywhere else (canvas, ribbon) drops it.
if let Some(pos) = ui.input(|i| if i.pointer.primary_pressed() { i.pointer.interact_pos() } else { None }) {
app.slides_focused = panel.response.rect.contains(pos);
}
}
fn list(app: &mut SlideApp, ui: &mut Ui) {