Open presets from a menu at the foot of the tool rail

Presets were a "Presets…" button in develop's top bar that opened a
sheet over the photograph, so applying one was two clicks with the
picture covered. They are now an entry pinned to the bottom of the tool
rail, apart from the tools because a preset arms nothing, and it opens a
menu beside the rail: the same sectioned rows the sheet lists, one click
to apply to the open photograph. The menu's last row, "Save or manage…",
opens the sheet, which keeps saving, renaming, reverting and importing,
since those take a name or a path.

The menu is a PopupWindow for the film list's reasons, with the list in
a clipped box of its own; the Flickable alone let its last row draw over
the manage row. Choosing from it zeroes preset-apply-count before
applying, since the grid may have left its selection count there.

The manual says where presets are now and gains a picture of the open
menu. The scenes reach the sheet through the menu and aim the manage
row from the rail's entry, as the film list's rows are aimed, because
the automation reports a popup's elements in the popup's coordinates.
This commit is contained in:
2026-09-27 16:08:33 -04:00
parent e22128c16b
commit f52c4cb8b6
13 changed files with 366 additions and 81 deletions
+11
View File
@@ -1785,6 +1785,17 @@ in property <bool> panel-visible: true;
tabs: Adjustments.tabs;
active-tab: Adjustments.active-tab;
group-picked(i) => { Adjustments.tab-picked(i); }
// TRACES: FR-DEV-6
// The rail's presets menu applies to the photograph in
// front of it, so the count the apply handler reads is
// set to say so — the grid may have left its own there.
presets: root.preset-rows;
preset-picked(name) => {
root.preset-apply-count = 0;
root.apply-preset(name);
}
manage-presets => { Transfer.open-presets(); }
}
// The canvas: compute output composited directly. No CPU
+4 -8
View File
@@ -100,7 +100,7 @@ export component StatusBar inherits Rectangle {
viewport-width: max(self.width, strip.preferred-width);
// Related controls sit closer together than unrelated ones: Pick and
// Reject, Undo and Redo, Copy, Paste and Presets, Help and Settings are
// Reject, Undo and Redo, Copy and Paste, Help and Settings are
// each a group at `gap-sm`, with `gap` between groups. It reads as the
// pairs it is, and it is also what keeps Settings on screen at 1600
// pixels wide once Help joined the strip.
@@ -285,13 +285,9 @@ export component StatusBar inherits Rectangle {
clicked => { Transfer.paste(); }
}
// The saved half: this edit, kept under a name.
Button {
text: "Presets…";
enabled: Develop.enabled;
y: (parent.height - self.height) / 2;
clicked => { Transfer.open-presets(); }
}
// The saved half — this edit, kept under a name — is at the foot
// of the tool rail now (`toolrail.slint`), as a menu beside the
// photograph rather than a sheet over it.
}
// Show or hide the develop column. On a tablet the panel is 280px of a
+13
View File
@@ -386,4 +386,17 @@ export component Icon inherits Rectangle {
commands: "M 4.6 14.8 A 5 5 0 1 0 14.6 14.8 A 5 5 0 1 0 4.6 14.8 Z "
+ "M 11.8 9 A 4 4 0 1 0 19.8 9 A 4 4 0 1 0 11.8 9 Z";
}
// TRACES: FR-DEV-6
// Presets: a stack of prints, the front one whole and two edges behind
// it. Not a tool — it arms nothing on the canvas — so it is the one
// drawing in the rail with no frame broken or marked: a saved edit is a
// finished photograph, several times over.
if root.name == "presets": Ink {
stroke: root.ink;
stroke-width: root.weight;
commands: "M 3.4 9.4 L 16.6 9.4 L 16.6 20.6 L 3.4 20.6 Z "
+ "M 6.2 6.4 L 19.4 6.4 L 19.4 17.6 "
+ "M 9 3.4 L 20.6 3.4 L 20.6 14.6";
}
}
+229 -1
View File
@@ -53,6 +53,8 @@
import { Theme } from "theme.slint";
import { Icon } from "icons.slint";
import { ViewMode } from "session.slint";
import { PresetRow, PresetOrigin } from "presets.slint";
import { PanelHeading, Label, ScrollBar } from "widgets.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.
@@ -117,6 +119,25 @@ export component ToolRail inherits Rectangle {
/// reason to want to.
callback group-picked(int);
/// TRACES: FR-DEV-6
/// The presets, sectioned — the same rows the preset sheet lists — and
/// what choosing one from the rail's menu asks for.
///
/// **A menu at the foot of the rail, not a tool in its table.** A tool
/// arms something on the canvas and stays lit while it is held; a preset
/// is applied and is over. Drawing it as a fifth tool would light a tile
/// for a state that does not exist. It sits apart, pinned to the bottom
/// edge so a rail long with groups cannot scroll it away, and it opens a
/// list beside the photograph it is about to change — one click to apply,
/// where the sheet in the strip was two and covered the picture.
///
/// Saving, renaming and importing stay in the sheet (`presets.slint`):
/// they take a name or a path, and a menu is the wrong place to type.
/// The menu's last row opens it.
in property <[PresetRow]> presets;
callback preset-picked(string);
callback manage-presets();
/// The groups this rail actually draws.
///
/// A conditional model rather than an `if` wrapped around the repeater:
@@ -191,6 +212,16 @@ export component ToolRail inherits Rectangle {
// construction — a tool is `rail-entry-height` and a group is a touch
// target, both stated a few lines below — so this is exact rather than an
// estimate, and it depends on nothing that depends on it.
// The presets entry's share of the rail, taken off the bottom of the
// scroller above it rather than added to its content, so it never
// scrolls.
private property <length> foot-height: Theme.rail-entry-height + 1px;
// One row of the presets menu. Shorter than a touch target for the reason
// the film list gives: a row's neighbours are other rows.
private property <length> menu-row-height: 32px;
private property <length> menu-width: 240px;
private property <length> content-height:
Theme.gap-sm
+ root.tools.length * Theme.rail-entry-height
@@ -200,7 +231,7 @@ export component ToolRail inherits Rectangle {
Flickable {
width: 100%;
height: 100%;
height: parent.height - root.foot-height;
viewport-width: self.width;
viewport-height: max(self.height, root.content-height);
@@ -407,6 +438,203 @@ export component ToolRail inherits Rectangle {
}
}
// TRACES: FR-DEV-6
// The presets, pinned to the foot of the rail. A rule above it says it is
// not one of the tools, and it lights only while its menu is open.
Rectangle {
x: Theme.gap-sm;
y: parent.height - root.foot-height;
width: parent.width - 2 * Theme.gap-sm;
height: 1px;
background: Theme.rule;
}
presets-entry := TouchArea {
y: parent.height - Theme.rail-entry-height;
height: Theme.rail-entry-height;
mouse-cursor: pointer;
clicked => { preset-menu.show(); }
accessible-role: button;
accessible-label: "Presets";
accessible-action-default => { preset-menu.show(); }
property <bool> lit: preset-menu.is-open;
Rectangle {
x: Theme.gap-sm / 2;
width: parent.width - Theme.gap-sm;
height: parent.height - 2px;
y: 1px;
border-radius: Theme.radius;
background: presets-entry.lit || presets-entry.pressed
? Theme.pressed
: (presets-entry.has-hover ? Theme.hover : transparent);
}
VerticalLayout {
alignment: center;
spacing: 3px;
HorizontalLayout {
alignment: center;
Icon {
name: "presets";
size: 20px;
ink: presets-entry.lit || presets-entry.has-hover ? Theme.ink : Theme.ink-dim;
}
}
Text {
text: "Presets";
font-size: Theme.text-sm;
color: presets-entry.lit || presets-entry.has-hover ? Theme.ink : Theme.ink-faint;
horizontal-alignment: center;
overflow: elide;
}
}
// Beside the rail, its foot level with the entry's, so it grows up
// over the photograph from where it was opened. Held to the rail's
// height, which is the frame's: a long list scrolls rather than
// running off the top of the window.
//
// A PopupWindow for the film list's reasons (`adjust.slint`): it is
// its own tree, so it scrolls by wheel and drag whatever is under it,
// and it closes on a choice, on Escape, or on a press outside it.
preset-menu := PopupWindow {
x: parent.width;
y: parent.height - self.height;
width: root.menu-width;
height: min(
(root.presets.length + 1) * root.menu-row-height + 1px,
root.height - Theme.gap);
close-policy: PopupClosePolicy.close-on-click-outside;
Rectangle {
background: Theme.surface;
border-radius: Theme.radius;
border-width: 1px;
border-color: Theme.rule;
clip: true;
// The list, clipped by a box of its own: the Flickable alone
// let its last row draw over "Save or manage…" below it.
Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height - root.menu-row-height - 1px;
clip: true;
// Counted, not measured, for the reason `content-height`
// above gives: every row is `menu-row-height`.
menu-flick := Flickable {
width: parent.width;
height: parent.height;
viewport-width: self.width;
viewport-height: root.presets.length * root.menu-row-height;
for entry[i] in root.presets: Rectangle {
y: i * root.menu-row-height;
width: parent.width;
height: root.menu-row-height;
if entry.heading != "": PanelHeading {
x: Theme.gap;
width: parent.width - 2 * Theme.gap;
height: parent.height;
text: entry.heading;
vertical-alignment: bottom;
}
if entry.heading == "": row := TouchArea {
mouse-cursor: pointer;
clicked => {
root.preset-picked(entry.name);
preset-menu.close();
}
accessible-role: button;
accessible-label: entry.name;
accessible-action-default => {
root.preset-picked(entry.name);
preset-menu.close();
}
Rectangle {
border-radius: Theme.radius;
background: row.pressed ? Theme.pressed
: (row.has-hover ? Theme.hover : transparent);
HorizontalLayout {
padding-left: Theme.gap;
padding-right: Theme.gap;
Label {
text: entry.origin == PresetOrigin.changed
? entry.name + " · changed"
: entry.name;
emphasised: row.has-hover;
vertical-alignment: center;
overflow: elide;
}
}
}
}
}
}
ScrollBar {
x: parent.width - self.width;
y: 0px;
height: menu-flick.height;
viewport-y <=> menu-flick.viewport-y;
viewport-height: menu-flick.viewport-height;
visible-height: menu-flick.height;
}
}
Rectangle {
y: menu-flick.height;
height: 1px;
background: Theme.rule;
}
// Last, next to where the pointer came from: saving the edit
// in hand is the other thing someone opening this wants.
manage := TouchArea {
y: parent.height - root.menu-row-height;
height: root.menu-row-height;
mouse-cursor: pointer;
clicked => {
preset-menu.close();
root.manage-presets();
}
accessible-role: button;
accessible-label: "Save or manage…";
accessible-action-default => {
preset-menu.close();
root.manage-presets();
}
Rectangle {
background: manage.pressed ? Theme.pressed
: (manage.has-hover ? Theme.hover : transparent);
HorizontalLayout {
padding-left: Theme.gap;
padding-right: Theme.gap;
Label {
text: "Save or manage…";
emphasised: manage.has-hover;
vertical-alignment: center;
}
}
}
}
}
}
}
// The rail's own edge. Drawn here rather than by whatever contains it, so
// the rail is a complete thing wherever it is put — and on the right,
// where it meets the canvas.