The presets menu and sheet listed every preset under flat section headings, seventy rows to scroll past. They now list folders, closed until opened, with how many presets each holds; opening one shows what is inside it, folders and presets indented beneath. A category is spelled in the name: "Portraits/Warm skin" is Warm skin in a Portraits folder under Yours. The file format does not change, so an older build lists the whole path as the name; renaming a preset is how it moves, and saving or renaming into a folder opens the way to it. A Lightroom import names what it reads after the folders below the one chosen, and a "/" in a displayed name becomes "∕" so it files nothing. The shipped film sections become Film › Colour, Cinema and Black and white. The tree is built and flattened in Rust (PresetTree), each row carrying its depth, and which folders are open is remembered for the session. A PopupWindow keeps the size it was shown at, so a folder opened in the menu pushed its contents under "Save or manage…"; the menu is shown again after each toggle to take its new height. That is a function on the rail because Slint 1.17 generates Rust that does not compile for a popup's close() reached from inside the popup. The sheet's list takes a preferred height of up to 400px, since a Flickable reports next to nothing and an opened folder showed three rows. The manual describes the folders and naming. Its pictures show the menu with Film › Colour open, and a black-and-white stock applied from Film › Black and white; the scenes aim popup rows from the rail's entry, and pick the menu's "Film" over the develop column's film chooser.
670 lines
30 KiB
Plaintext
670 lines
30 KiB
Plaintext
// The develop view's tool rail: which tool the photographer is holding, and —
|
|
// where the interface is driven by a finger — which group of adjustments they
|
|
// are looking at.
|
|
//
|
|
// **Two sections, two sources, one rule between them.**
|
|
//
|
|
// The tools are the array literal in `tools` below. A tool is a row in it — an
|
|
// icon name, a word, and the `ViewMode` it arms — and adding one is that row
|
|
// plus a drawing in `icons.slint` plus a variant on the enum. Nothing in
|
|
// `app.slint` is touched and there is no second list that could fall out of
|
|
// step with it.
|
|
//
|
|
// The groups are not written here at all, and must not be: they are whatever
|
|
// the operation set declares itself to be about, resolved in Rust and handed
|
|
// over as `tabs` (FR-DEV-3a). This file names no group, exactly as
|
|
// `GroupStrip` names none — it takes the same model, because the two controls
|
|
// answer the same question and only one of them is on screen at a time.
|
|
//
|
|
// **Why a rail is the right shape for a finger and the wrong one for a mouse.**
|
|
// See `groups-in-rail` below, and D-N6 in `docs/dev/ui-navigation.md` for the
|
|
// decision it reverses and the half of that decision that still stands.
|
|
//
|
|
// **Why it left the chip strip.** These four used to be chips at the top of
|
|
// the develop column, sharing a row with the adjustment groups — two kinds of
|
|
// state in one strip, told apart by the shape of their highlight. Three things
|
|
// were wrong with that, and only the third is about tidiness:
|
|
//
|
|
// 1. The column can be put away. The strip went with it, so the way *out* of
|
|
// crop went away with the way in, and the fix was a second "Done
|
|
// Cropping" button floating over the canvas — one control duplicated
|
|
// because the first one was reachable only sometimes.
|
|
// 2. The strip sized the column. Its chips are generated from the operation
|
|
// set, so the widest thing in the develop sidebar was a row nobody had
|
|
// chosen the contents of, and a richer operation set silently took width
|
|
// from the photograph.
|
|
// 3. A mode and a filter are not the same kind of thing. "Crop" changes what
|
|
// a click on the photograph does; "Light" changes which sliders are on
|
|
// screen. Putting them in one row and distinguishing them by underline
|
|
// versus fill asks the eye to carry a distinction the layout could just
|
|
// make.
|
|
//
|
|
// The rail is always up, so (1) is gone; it is a fixed width that no operation
|
|
// set can influence, so (2) is gone; and it is somewhere else entirely, so (3)
|
|
// is gone. What is left in the column is a row of group filters and nothing
|
|
// else — see `GroupStrip` in `adjust.slint`.
|
|
//
|
|
// **Down the left, not the right.** The develop column is on the right and
|
|
// holds the *consequences* of a choice — the sliders the tool exposes. The
|
|
// choice itself goes on the far side, so the eye's path across the window is
|
|
// tool, photograph, adjustment, in that order, and the rail never moves when
|
|
// the column opens and closes beside it.
|
|
|
|
import { Theme } from "theme.slint";
|
|
import { Icon } from "icons.slint";
|
|
import { ViewMode } from "session.slint";
|
|
import { PresetRow, PresetOrigin, PresetFolderRow } from "presets.slint";
|
|
import { 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.
|
|
struct Tool {
|
|
/// A name from `icons.slint`'s vocabulary. A typo here draws nothing,
|
|
/// which is loud: the entry becomes a label with a hole above it.
|
|
icon: string,
|
|
/// What it is called. One word — see `rail-width` in `style.yaml`.
|
|
label: string,
|
|
/// What arming it puts the canvas into.
|
|
mode: ViewMode,
|
|
}
|
|
|
|
export component ToolRail inherits Rectangle {
|
|
/// Which tool is held. Owned by Rust, like every other piece of session
|
|
/// state: the rail asks for a mode and is told what the mode became, so a
|
|
/// change made anywhere else — the keyboard, the back gesture, the button
|
|
/// over the canvas — lights the same entry.
|
|
in property <ViewMode> mode: ViewMode.photo;
|
|
/// Whether there is a photograph to point a tool at. The rail collapses
|
|
/// rather than greying out: an empty develop view has no tools, and four
|
|
/// dead icons beside a blank canvas suggest otherwise.
|
|
in property <bool> enabled: true;
|
|
|
|
/// The adjustment groups, already resolved — the same model `GroupStrip`
|
|
/// takes, because it is the same list answering the same question.
|
|
///
|
|
/// Empty unless this rail is carrying them. Nothing here names a group:
|
|
/// the strings arrive from whatever the operations declared themselves to
|
|
/// be about (FR-DEV-3a).
|
|
in property <[string]> tabs;
|
|
/// Index into `tabs`, or -1 for "everything".
|
|
in property <int> active-tab: -1;
|
|
/// TRACES: FR-UI-1 | FR-UI-7
|
|
/// Whether the groups live in this rail or in the strip above the develop
|
|
/// column.
|
|
///
|
|
/// **The one property that makes this rail two different controls**, and
|
|
/// it is set from how the interface is being *driven* rather than from
|
|
/// which binary is running — see `input_class` in `lib.rs`.
|
|
///
|
|
/// With a mouse, a horizontal run of words above the column is a tab bar,
|
|
/// which is what a pointer is good at: it is one gesture to a target the
|
|
/// eye has already found, and the strip costs a row of a column that has
|
|
/// plenty of height. With a finger it is the wrong control twice over. The
|
|
/// strip pans when the operation set is rich, so a group can be off the
|
|
/// end of a row with nothing saying so; and it sits at the top of a
|
|
/// column, which on a tablet held in two hands is the furthest point from
|
|
/// either thumb.
|
|
///
|
|
/// Down the rail the same list is a column of finger-sized targets, all of
|
|
/// them visible at once, on the edge of the screen a hand is already at.
|
|
in property <bool> groups-in-rail: false;
|
|
|
|
callback picked(ViewMode);
|
|
/// A group was chosen: an index into `tabs`, or -1 for "everything".
|
|
///
|
|
/// Deliberately the same signature `GroupStrip` emits, and routed to the
|
|
/// same callback in `app.slint`. The two controls are alternatives, not
|
|
/// peers — only one is on screen at a time — and giving them one contract
|
|
/// means Rust cannot tell which of them the user pressed, and has no
|
|
/// 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();
|
|
/// A category folder in the menu was opened or closed, by its key.
|
|
callback folder-toggled(string);
|
|
|
|
/// The groups this rail actually draws.
|
|
///
|
|
/// A conditional model rather than an `if` wrapped around the repeater:
|
|
/// Slint has no way to nest one inside the other, and putting the
|
|
/// condition on the model keeps the entries as direct children of the
|
|
/// layout below — which is the shape that matters here. A nested layout
|
|
/// under-reports its height and its siblings get drawn on top of each
|
|
/// other; `app.slint`'s develop column carries the same note.
|
|
private property <[string]> rail-tabs: root.groups-in-rail ? root.tabs : [];
|
|
|
|
// **The table.** Add a row to get a tool.
|
|
//
|
|
// Order is the order they appear, and it is not arbitrary: `photo` first
|
|
// because it is the resting state and the way back from everywhere else,
|
|
// then the three that arm a gesture on the canvas, roughly in the order a
|
|
// photograph is worked — frame it, then adjust parts of it, then clean it
|
|
// up.
|
|
//
|
|
// `MaskSource::Brush` is the next one to land here. The core already has
|
|
// the stroke calls; what is missing is the canvas interaction, and when it
|
|
// arrives this file's share of the work is one line.
|
|
private property <[Tool]> tools: [
|
|
{ icon: "photo", label: "Photo", mode: ViewMode.photo },
|
|
// "Compose", not "Crop", and the panel it opens says the same. The
|
|
// tool arms a crop gesture, but what it is *for* is deciding the
|
|
// frame — straightening, the quarter turns and the flips are in that
|
|
// panel too, and perspective will be. Naming the tool after one of its
|
|
// controls was what made cropping look like a category of its own.
|
|
//
|
|
// `ViewMode.crop` keeps its name: it identifies a canvas interaction,
|
|
// which is exactly what it still is.
|
|
{ icon: "crop", label: "Compose", mode: ViewMode.crop },
|
|
{ icon: "mask", label: "Local", mode: ViewMode.local },
|
|
{ icon: "repair", label: "Repair", mode: ViewMode.spots },
|
|
];
|
|
|
|
// TRACES: FR-UI-1
|
|
// Fixed, and the point of the exercise. This rail flanks the photograph,
|
|
// so its width is taken out of the picture — and a rail measured from its
|
|
// contents would hand that decision to whichever tool label happens to be
|
|
// longest. `style.yaml` carries the number and the reasoning.
|
|
width: root.enabled ? Theme.rail-width : 0px;
|
|
visible: root.enabled;
|
|
background: Theme.surface;
|
|
clip: true;
|
|
|
|
// **A Flickable now, and this comment used to argue the opposite.** It
|
|
// said the rail held four entries written in this file, on an axis with
|
|
// room for fourteen, and that a rail which scrolls is the answer only if
|
|
// that stops being true. It has stopped being true: with `groups-in-rail`
|
|
// the list is four tools plus one entry per group the operation set
|
|
// declares, and an operation set is exactly the "something the user's data
|
|
// decides" the old note excluded this control from.
|
|
//
|
|
// Four tools, "All" and today's five groups is ten entries — comfortable
|
|
// on any supported screen. The point is not today's count but that the
|
|
// count is no longer written here, and a rail that overflows loses its
|
|
// last entries silently, on the one control the develop view is navigated
|
|
// by.
|
|
// **How tall the entries actually are, counted rather than measured.**
|
|
//
|
|
// This used to read `max(self.height, layout.preferred-height)`, with the
|
|
// layout below taking its height from the viewport in turn. That is a
|
|
// cycle — the viewport asks the layout how tall it wants to be, and the
|
|
// layout has already been told — and Slint settles it by handing back the
|
|
// height it was given. The viewport then never exceeded the visible area,
|
|
// so there was nothing to scroll and everything past the fold was clipped
|
|
// in silence: at a 680px window the rail stopped after "All" and the five
|
|
// groups below it could not be reached by any means.
|
|
//
|
|
// Counting works because every entry in this rail is a fixed height by
|
|
// 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;
|
|
|
|
// Show the presets menu again, so it takes the height its rows now want.
|
|
// A function on the rail rather than the two calls in the row that needs
|
|
// them: Slint 1.17 generates Rust that does not compile for a popup's
|
|
// `close()` reached from an element nested inside that popup.
|
|
function remeasure-menu() {
|
|
preset-menu.close();
|
|
preset-menu.show();
|
|
}
|
|
|
|
// 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
|
|
+ (root.groups-in-rail
|
|
? Theme.gap + Theme.touch-target * (1 + root.rail-tabs.length)
|
|
: 0px);
|
|
|
|
Flickable {
|
|
width: 100%;
|
|
height: parent.height - root.foot-height;
|
|
viewport-width: self.width;
|
|
viewport-height: max(self.height, root.content-height);
|
|
|
|
layout := VerticalLayout {
|
|
height: parent.viewport-height;
|
|
padding-top: Theme.gap-sm;
|
|
spacing: 0px;
|
|
alignment: start;
|
|
|
|
for tool in root.tools: entry := TouchArea {
|
|
height: Theme.rail-entry-height;
|
|
mouse-cursor: pointer;
|
|
|
|
property <bool> on: root.mode == tool.mode;
|
|
|
|
// Pressing the tool you are holding puts it down, exactly as the
|
|
// chips did: the same control both directions. `photo` is the
|
|
// exception — it *is* putting the tool down, so pressing it while
|
|
// it is lit is a no-op rather than a toggle into itself.
|
|
clicked => {
|
|
root.picked(entry.on ? ViewMode.photo : tool.mode);
|
|
}
|
|
|
|
// **The word below is drawn; this is what makes it a control.**
|
|
// Without these the rail reaches a screen reader as four pieces of
|
|
// static text — the labels get through, because a `Text` announces
|
|
// itself, and nothing says any of them can be pressed. That is the
|
|
// develop view's primary navigation reduced to a caption.
|
|
//
|
|
// `checkable` unconditionally, unlike `Button`'s: a rail entry is
|
|
// always a held-or-not state, so an unheld one should say "not
|
|
// pressed" rather than pass for an ordinary button. The action
|
|
// repeats the click handler rather than calling it, because a
|
|
// `TouchArea`'s `clicked` is raised by the pointer and cannot be
|
|
// raised from here.
|
|
accessible-role: button;
|
|
accessible-label: tool.label;
|
|
accessible-checkable: true;
|
|
accessible-checked: entry.on;
|
|
accessible-action-default => {
|
|
root.picked(entry.on ? ViewMode.photo : tool.mode);
|
|
}
|
|
|
|
// The lit tile, and the only marker there is. Inset from the
|
|
// rail's edges so the run of four reads as four things rather than
|
|
// as one striped column.
|
|
//
|
|
// A bright bar against the outer edge was tried alongside it, on
|
|
// the theory that a rail is scanned from the side and needs
|
|
// something unambiguous. It is not needed and it is not
|
|
// unambiguous: `active-dim` is near-white and `hover` is a shade
|
|
// above the surface, so the held tool and a tool under the pointer
|
|
// are not two similar greys — they are opposite ends of the
|
|
// palette. The bar sat against the lit tile and merged with it.
|
|
Rectangle {
|
|
x: Theme.gap-sm / 2;
|
|
width: parent.width - Theme.gap-sm;
|
|
height: parent.height - 2px;
|
|
y: 1px;
|
|
border-radius: Theme.radius;
|
|
background: entry.on
|
|
? Theme.active-dim
|
|
: (entry.has-hover ? Theme.hover : transparent);
|
|
}
|
|
|
|
VerticalLayout {
|
|
alignment: center;
|
|
spacing: 3px;
|
|
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
Icon {
|
|
name: tool.icon;
|
|
size: 20px;
|
|
// Dark on the lit tile, which is near-white: the same
|
|
// inversion `Button`'s primary state makes, and the
|
|
// same one the chips made before this.
|
|
ink: entry.on
|
|
? Theme.ground
|
|
: (entry.has-hover ? Theme.ink : Theme.ink-dim);
|
|
}
|
|
}
|
|
|
|
// Named, not just drawn. An icon-only rail is a quiz — these
|
|
// four are conventional enough to guess and not conventional
|
|
// enough to be sure of, and "sure" is what a tool that changes
|
|
// what a click does has to be. The word is `text-sm` and dim,
|
|
// so it reads as the icon's caption rather than as a button in
|
|
// its own right.
|
|
Text {
|
|
text: tool.label;
|
|
font-size: Theme.text-sm;
|
|
color: entry.on
|
|
? Theme.ground
|
|
: (entry.has-hover ? Theme.ink : Theme.ink-faint);
|
|
horizontal-alignment: center;
|
|
// Elided rather than wrapped: a two-line label would make
|
|
// this entry taller than the three beside it, and a rail
|
|
// whose rows are different heights reads as a list of
|
|
// unrelated things.
|
|
overflow: elide;
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The seam between the two kinds of entry.**
|
|
//
|
|
// A rule and a gap, because above it are things that change what a
|
|
// click on the photograph *does* and below it are things that change
|
|
// which sliders are on screen. `ui-navigation.md` §N1 made that
|
|
// distinction by drawing a mode and a group differently in one strip;
|
|
// it holds here by separating them, which is the cheaper signal when
|
|
// the axis is vertical and there is a whole rail's width to draw a
|
|
// line across.
|
|
if root.groups-in-rail: Rectangle {
|
|
height: Theme.gap;
|
|
background: transparent;
|
|
|
|
Rectangle {
|
|
x: Theme.gap-sm;
|
|
width: parent.width - 2 * Theme.gap-sm;
|
|
height: 1px;
|
|
y: (parent.height - 1px) / 2;
|
|
background: Theme.rule;
|
|
}
|
|
}
|
|
|
|
// "All", and it is not decoration. A group filter that cannot be
|
|
// cleared is a way to make controls unreachable, and this is the only
|
|
// entry in the run below that is not generated.
|
|
if root.groups-in-rail: all := TouchArea {
|
|
height: Theme.touch-target;
|
|
mouse-cursor: pointer;
|
|
clicked => { root.group-picked(-1); }
|
|
|
|
accessible-role: button;
|
|
accessible-label: "All adjustments";
|
|
accessible-checkable: true;
|
|
accessible-checked: root.active-tab == -1;
|
|
accessible-action-default => { root.group-picked(-1); }
|
|
|
|
Rectangle {
|
|
x: 0;
|
|
width: 2px;
|
|
height: parent.height;
|
|
background: root.active-tab == -1 ? Theme.ink : transparent;
|
|
}
|
|
|
|
Text {
|
|
text: "All";
|
|
font-size: Theme.text-sm;
|
|
color: root.active-tab == -1
|
|
? Theme.ink
|
|
: (all.has-hover ? Theme.ink : Theme.ink-faint);
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
// **A bar down the leading edge, not a filled tile.**
|
|
//
|
|
// The tools above fill with `active-dim` and invert their ink; these
|
|
// do not, and the difference is the one §N1 insisted on — a mode and a
|
|
// filter are not the same kind of state, and a reader should not have
|
|
// to remember which section a lit entry was in to know which they are
|
|
// looking at. An underline is what said so when the groups were a
|
|
// horizontal strip; turned ninety degrees, that is a bar down the
|
|
// edge.
|
|
for tab[i] in root.rail-tabs: group := TouchArea {
|
|
height: Theme.touch-target;
|
|
mouse-cursor: pointer;
|
|
clicked => { root.group-picked(i); }
|
|
|
|
accessible-role: button;
|
|
accessible-label: tab;
|
|
accessible-checkable: true;
|
|
accessible-checked: root.active-tab == i;
|
|
accessible-action-default => { root.group-picked(i); }
|
|
|
|
Rectangle {
|
|
x: 0;
|
|
width: 2px;
|
|
height: parent.height;
|
|
background: root.active-tab == i ? Theme.ink : transparent;
|
|
}
|
|
|
|
Text {
|
|
text: tab;
|
|
font-size: Theme.text-sm;
|
|
color: root.active-tab == i
|
|
? Theme.ink
|
|
: (group.has-hover ? Theme.ink : Theme.ink-faint);
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
width: 100%;
|
|
height: 100%;
|
|
// A group's name comes from the operation set and this rail is
|
|
// a mandated width, so a long one has to give somewhere.
|
|
overflow: elide;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Opening a folder rebuilds the rows, and so the
|
|
// height the menu wants. A PopupWindow takes its
|
|
// geometry once, when shown — a folder opened in
|
|
// place stayed the old size and pushed what it
|
|
// held under "Save or manage…" — so the menu is
|
|
// shown again, measured afresh, its foot still on
|
|
// the entry and growing upward.
|
|
if entry.folder: PresetFolderRow {
|
|
entry: entry;
|
|
width: parent.width;
|
|
height: parent.height;
|
|
clicked => {
|
|
root.folder-toggled(entry.key);
|
|
root.remeasure-menu();
|
|
}
|
|
}
|
|
|
|
if !entry.folder: 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 {
|
|
// Past the folder's arrow, so a preset
|
|
// lines up with its folder's name.
|
|
padding-left: Theme.gap-sm + 14px + Theme.gap-sm
|
|
+ (entry.depth - 1) * Theme.indent;
|
|
padding-right: Theme.gap;
|
|
Label {
|
|
text: entry.origin == PresetOrigin.changed
|
|
? entry.label + " · changed"
|
|
: entry.label;
|
|
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.
|
|
Rectangle {
|
|
x: parent.width - 1px;
|
|
width: 1px;
|
|
background: Theme.rule;
|
|
}
|
|
}
|