Let Back close the presets menu instead of the application

The presets menu at the foot of the tool rail is a PopupWindow, and
showing a popup takes focus off the develop view until it closes. The
menu held nothing focusable, so Android's Back gesture, pressed to
dismiss it, found no focus item, went unanswered, and the platform
closed the application. Slint closes a popup on Escape by itself but
not on Back.

The menu now holds a key scope, as the film list does, that closes it
on Back or Escape. The next Back leaves develop for the grid.
This commit is contained in:
2026-09-30 22:11:51 -04:00
parent 6f33517b35
commit 36556729f1
3 changed files with 100 additions and 6 deletions
@@ -0,0 +1,69 @@
// TRACES: FR-UI-5
//! Back closes the presets menu, and the next Back returns to the grid.
//!
//! The menu at the foot of the tool rail is a `PopupWindow`, and showing a
//! popup takes focus off the develop view until it closes. The menu had
//! nothing focusable of its own, so a Back pressed while it was up had no
//! focus item to bubble from, went unanswered, and on Android an unanswered
//! Back is the platform's: it closed the application from develop. Dismissing
//! a menu with the back gesture is the ordinary thing to do, which is why this
//! was seen often.
//!
//! Runs on Slint's testing backend, as `film_list_reaches_every_stock` does.
#![cfg(debug_assertions)]
use std::cell::Cell;
use std::rc::Rc;
use dr_ui::{AppWindow, Develop};
use i_slint_backend_testing::{init_no_event_loop, ElementHandle};
use slint::platform::{Key, PointerEventButton, WindowEvent};
use slint::{ComponentHandle, SharedString};
const MANAGE: &str = "Save or manage…";
fn key(app: &AppWindow, k: Key) {
let text: SharedString = k.into();
app.window()
.dispatch_event(WindowEvent::KeyPressed { text: text.clone() });
app.window()
.dispatch_event(WindowEvent::KeyReleased { text });
}
fn menu_open(app: &AppWindow) -> bool {
ElementHandle::find_by_accessible_label(app, MANAGE)
.next()
.is_some()
}
#[test]
fn back_closes_the_presets_menu_then_leaves_develop() {
init_no_event_loop();
let app = AppWindow::new().expect("the window builds on the testing backend");
app.window().set_size(slint::LogicalSize::new(1600., 1200.));
app.global::<Develop>().set_enabled(true);
// The rail offers presets only with a photograph to apply them to.
app.set_total(3);
let asked = Rc::new(Cell::new(0));
app.on_back_requested({
let asked = asked.clone();
move || {
asked.set(asked.get() + 1);
true
}
});
app.show().unwrap();
ElementHandle::find_by_accessible_label(&app, "Presets")
.next()
.expect("the tool rail's presets entry is on screen")
.mock_single_click(PointerEventButton::Left);
assert!(menu_open(&app), "clicking Presets opens the menu");
key(&app, Key::Back);
assert!(!menu_open(&app), "Back closes the menu");
assert_eq!(asked.get(), 0, "closing the menu is the whole step");
key(&app, Key::Back);
assert_eq!(asked.get(), 1, "the next Back reaches the shell");
}
+25
View File
@@ -55,6 +55,7 @@ import { Icon } from "icons.slint";
import { ViewMode } from "session.slint";
import { PresetRow, PresetOrigin, PresetFolderRow } from "presets.slint";
import { Label, ScrollBar } from "widgets.slint";
import { Keys } from "keys.slint";
// One tool. A struct rather than four parallel arrays so a row cannot be
// half-added — the compiler will not let a new entry omit its icon.
@@ -522,6 +523,30 @@ export component ToolRail inherits Rectangle {
root.height - Theme.gap);
close-policy: PopupClosePolicy.close-on-click-outside;
// Somewhere for Back to start from while the menu is up.
//
// A popup is its own focus tree: showing it takes focus off the
// develop view, and Slint hands it back only when the popup
// closes. With nothing focusable in here, Android's Back gesture
// — the natural way to dismiss a menu — found no focus item,
// went unanswered, and the platform closed the application.
// Slint closes a popup on Escape by itself but knows nothing of
// Back, so both are answered here, as the film list does.
FocusScope {
width: 0px;
height: 0px;
init => { self.focus(); }
// KEYMAP: Develop
key-pressed(event) => {
if (Keys.chord(event) == "Escape" || Keys.chord(event) == "Back") {
preset-menu.close();
return accept;
}
return reject;
}
}
Rectangle {
background: Theme.surface;
border-radius: Theme.radius;