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.
647 lines
29 KiB
Plaintext
647 lines
29 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 } 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.
|
|
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();
|
|
|
|
/// 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;
|
|
|
|
// 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;
|
|
|
|
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.
|
|
Rectangle {
|
|
x: parent.width - 1px;
|
|
width: 1px;
|
|
background: Theme.rule;
|
|
}
|
|
}
|