The info panel gains a line under the lens: "Adobe Standard · in the file", the .dcp it came from, "· off" when the photographer switched it off, or "No camera profile · matrix only" — the ordinary case for a CR2, worded as a fact rather than a failure. A DNG whose embedded profile may be copied, for a body with no installed profile, also gets "Use this profile for every Canon EOS 6D →", which saves it into the profiles directory; the body's CR2s render through it from their next decode. The profiles directory is <data>/profiles, read at start-up on desktop and Android before anything decodes. The library open path never set the lens line; it now sets both. Labels: Camera Profile, Use Profile, Look Amount.
482 lines
20 KiB
Plaintext
482 lines
20 KiB
Plaintext
// The develop view's own chrome: the strip across the top of the window and
|
||
// the capture-metadata block in the column beside the canvas.
|
||
//
|
||
// Split out of `app.slint`, where they sat above `AppWindow` and were read as
|
||
// part of the application shell. They are not: neither is instantiated
|
||
// anywhere but the develop view, and the shell's job — choosing which of the
|
||
// five screens is up — is easier to read without two unrelated components in
|
||
// front of it.
|
||
|
||
import { Theme } from "theme.slint";
|
||
import { Develop } from "session.slint";
|
||
import { Transfer } from "adjust.slint";
|
||
import { Button, IconButton, PanelHeading, Label, Value, Caption, Panel } from "widgets.slint";
|
||
import { Labels, LabelMark, LabelPicker } from "labels.slint";
|
||
import { StarStrip } from "library.slint";
|
||
|
||
// Status strip — surfaces the GPU backend and adapter, which matters during
|
||
// v0.1 because assumption A1 is exactly "does this compositing path work on
|
||
// this hardware". Seeing the adapter at a glance makes vendor differences
|
||
// obvious during the S1/S2 spikes.
|
||
export component StatusBar inherits Rectangle {
|
||
in property <string> adapter;
|
||
in property <string> backend;
|
||
in property <string> layout-class;
|
||
in property <int> fps;
|
||
/// FR-UI-1: the layout class, so the instrumentation can stand down on a
|
||
/// strip too narrow to carry both it and the controls.
|
||
in property <bool> expanded: true;
|
||
in property <string> filename;
|
||
in property <string> position;
|
||
/// Only offered where there is a library to go back to — with files named
|
||
/// on the command line there is no grid behind this view.
|
||
in property <bool> can-return-to-library: false;
|
||
/// Whether the develop column is currently shown, for the toggle's label.
|
||
in property <bool> panel-visible: true;
|
||
/// What the export button says. Rust owns it because the answer depends
|
||
/// on settings this component does not see — the format, and whether the
|
||
/// destination is this device or the server.
|
||
in property <string> export-label: "Export";
|
||
in property <bool> export-busy: false;
|
||
/// What the last export did. Sits beside the button rather than in a
|
||
/// dialogue: an export that succeeded needs no acknowledging, and one
|
||
/// that failed needs its reason where the retry is.
|
||
in property <string> export-status;
|
||
/// Whether the edit can be stepped either way (FR-DEV-5).
|
||
in property <bool> can-undo: false;
|
||
in property <bool> can-redo: false;
|
||
/// TRACES: FR-CAT-5
|
||
/// The open photograph's colour label code, 0 for none, -1 where there
|
||
/// is no library to hold one (files named on the command line).
|
||
in property <int> colour-label: -1;
|
||
/// Whether the label choices are open in the strip.
|
||
property <bool> picking-label: false;
|
||
callback label-picked(int);
|
||
/// TRACES: FR-UI-5 | FR-CULL-4
|
||
/// The open photograph's stars, or -1 where there is no library to hold
|
||
/// them, and its flag: 0 none, 1 pick, 2 reject.
|
||
in property <int> rating: -1;
|
||
in property <int> flag: 0;
|
||
callback rated(int);
|
||
callback flagged(int);
|
||
|
||
callback back-to-library();
|
||
callback open-people();
|
||
callback open-settings();
|
||
/// TRACES: FR-UI-4
|
||
/// Open the gesture reference, which the window draws.
|
||
callback open-help();
|
||
callback toggle-panel();
|
||
callback export-image();
|
||
callback undo();
|
||
callback redo();
|
||
|
||
// 44px and `surface`, the same bar the library and settings draw.
|
||
//
|
||
// It was 28px — the control height — which was a reasonable size for a
|
||
// readout and the wrong size for the thing it actually is: this strip
|
||
// carries the only way out of develop, and it was half the height of the
|
||
// identical control on the two screens either side of it. Three screens
|
||
// whose headers do not agree read as three applications, and the one place
|
||
// the user needs to find a way back was the one drawn smallest.
|
||
//
|
||
// Controls keep their own 28px and are centred in it, exactly as the
|
||
// library header centres the same buttons: a `Button` sets a fixed height,
|
||
// which a HorizontalLayout parks at the top of the row rather than
|
||
// centring for it.
|
||
height: 44px;
|
||
background: Theme.surface;
|
||
|
||
// Scrolls rather than overflowing. A layout given less width than its
|
||
// children need does not shrink them — it overruns the edge and reports
|
||
// the oversized minimum to whatever contains it. Collapsing the
|
||
// instrumentation below buys back about 200 logical pixels, which is
|
||
// enough for a tablet; it is not enough for a phone, and nothing here
|
||
// should be unreachable merely because the window is narrow.
|
||
Flickable {
|
||
width: 100%;
|
||
height: 100%;
|
||
viewport-height: self.height;
|
||
viewport-width: max(self.width, strip.preferred-width);
|
||
|
||
// Related controls sit closer together than unrelated ones: Pick and
|
||
// 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.
|
||
strip := HorizontalLayout {
|
||
width: parent.viewport-width;
|
||
height: parent.viewport-height;
|
||
padding-left: Theme.gap;
|
||
padding-right: Theme.gap;
|
||
spacing: Theme.gap;
|
||
alignment: start;
|
||
|
||
// Kept instantiated with `visible` rather than wrapped in an `if`:
|
||
// the strip is inside the layout that `expanded` feeds, and adding a
|
||
// conditional child here is the shape that has already caused binding
|
||
// loops in this file (see the panel below).
|
||
Button {
|
||
text: "‹ Library";
|
||
y: (parent.height - self.height) / 2;
|
||
visible: root.can-return-to-library;
|
||
clicked => { root.back-to-library(); }
|
||
}
|
||
|
||
// The third screen, reachable from the second. Beside the way back
|
||
// because it is the same kind of move — leaving this photograph for
|
||
// somewhere else in the library — and because a mode you can only
|
||
// reach from one of the other two is not a peer of them, whatever the
|
||
// navigation model says.
|
||
Button {
|
||
text: "Identity";
|
||
y: (parent.height - self.height) / 2;
|
||
visible: root.can-return-to-library;
|
||
clicked => { root.open-people(); }
|
||
}
|
||
|
||
Value { text: root.filename; compact: true; overflow: elide; }
|
||
|
||
Caption { text: root.position; }
|
||
|
||
// TRACES: FR-CAT-5 | FR-UI-4 | NFR-A11Y-3
|
||
// The label, shown and set here. The button says the label's name
|
||
// beside its mark, so the strip reads "Label: Green" in words; with
|
||
// none it offers "Label". Pressing it opens the six choices in the
|
||
// strip, which close on the one chosen.
|
||
Button {
|
||
text: root.colour-label > 0 ? "Label: " + Labels.names[root.colour-label] : "Label";
|
||
active: root.picking-label;
|
||
visible: root.colour-label >= 0;
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.picking-label = !root.picking-label; }
|
||
}
|
||
|
||
if root.colour-label > 0: LabelMark {
|
||
code: root.colour-label;
|
||
y: (parent.height - self.height) / 2;
|
||
}
|
||
|
||
if root.picking-label && root.colour-label >= 0: LabelPicker {
|
||
current: root.colour-label;
|
||
chosen(code) => {
|
||
root.label-picked(code);
|
||
root.picking-label = false;
|
||
}
|
||
}
|
||
|
||
// TRACES: FR-UI-5 | FR-CULL-4 | NFR-A11Y-3
|
||
// The judgement, shown and set on the photograph that is open — the
|
||
// pointer and touch half of the rating and flag keys, which a tablet
|
||
// has no number row for (FR-UI-5, amended 2026-09-19). The grid's own
|
||
// stars, so a rating looks the same in both places; the star already
|
||
// set clears it, as there.
|
||
StarStrip {
|
||
rating: max(0, root.rating);
|
||
show-empty: true;
|
||
can-trash: false;
|
||
visible: root.rating >= 0;
|
||
y: (parent.height - self.height) / 2;
|
||
rate(n) => { root.rated(n); }
|
||
}
|
||
|
||
HorizontalLayout {
|
||
spacing: Theme.gap-sm;
|
||
|
||
// Pressed again, a flag comes off: two buttons that each toggle,
|
||
// rather than a third for "none" that is only ever pressed to undo
|
||
// one of the other two.
|
||
Button {
|
||
text: root.flag == 1 ? "Picked" : "Pick";
|
||
active: root.flag == 1;
|
||
visible: root.rating >= 0;
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.flagged(root.flag == 1 ? 0 : 1); }
|
||
}
|
||
|
||
Button {
|
||
text: root.flag == 2 ? "Rejected" : "Reject";
|
||
active: root.flag == 2;
|
||
visible: root.rating >= 0;
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.flagged(root.flag == 2 ? 0 : 2); }
|
||
}
|
||
}
|
||
|
||
Rectangle { horizontal-stretch: 1; }
|
||
|
||
// --- instrumentation lives in Settings now ------------------------
|
||
//
|
||
// The render backend, the layout class and the frame rate are
|
||
// developer readouts. They cost about 200 logical pixels of a strip
|
||
// that also carries the only way out of develop, the undo pair, the
|
||
// panel toggle and the export button — and a `HorizontalLayout` given
|
||
// less width than its children's minimums does not shrink them, it
|
||
// runs off the end.
|
||
//
|
||
// A tablet in portrait is 768 logical pixels and was already below the
|
||
// breakpoint that hid them, so it kept its header. Landscape is 1200
|
||
// and is not, so the readouts came back and the header ran off the
|
||
// right-hand edge there instead — a breakpoint that hides a problem at
|
||
// one size and not another is a workaround, not a fix.
|
||
//
|
||
// The backend, the frame rate and the layout class used to live here.
|
||
// They are diagnostics — read once when something looks wrong, and
|
||
// never again — and as permanent furniture in a 44px strip they cost
|
||
// three slots that the controls beside them actually needed. On a
|
||
// tablet in landscape the header ran off the right-hand edge, and this
|
||
// was a third of the reason.
|
||
//
|
||
// They are in Settings under ABOUT now, next to the version, which is
|
||
// where someone goes when they have a bug to report rather than a
|
||
// photograph to edit.
|
||
|
||
// Undo and redo, in the strip rather than in the develop column: the
|
||
// panel can be put away, and the one control that takes back a
|
||
// mis-drag must not go away with it. Disabled rather than hidden, so
|
||
// the pair keeps its place and the keyboard shortcut has something
|
||
// visible to correspond to.
|
||
HorizontalLayout {
|
||
spacing: Theme.gap-sm;
|
||
|
||
Button {
|
||
text: "Undo";
|
||
enabled: root.can-undo;
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.undo(); }
|
||
}
|
||
|
||
Button {
|
||
text: "Redo";
|
||
enabled: root.can-redo;
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.redo(); }
|
||
}
|
||
}
|
||
|
||
// TRACES: FR-DEV-6
|
||
// Copy and paste, in the strip beside undo for the reason undo is
|
||
// here: they act on the whole edit, not on any one panel, and the
|
||
// column can be put away. They sat in the column under a "SETTINGS"
|
||
// heading, which read as application settings and hid them behind a
|
||
// scroll and behind the panel toggle.
|
||
//
|
||
// The paste button names what would be pasted — "3 adjustments", and
|
||
// whether the crop is staying behind — because "Paste" alone asks the
|
||
// user to remember what they copied.
|
||
HorizontalLayout {
|
||
spacing: Theme.gap-sm;
|
||
|
||
Button {
|
||
text: "Copy";
|
||
enabled: Develop.enabled;
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { Transfer.copy(); }
|
||
}
|
||
|
||
// Enabled on `armed` rather than on a non-empty summary, so pasting a
|
||
// neutral copy — which clears this image — stays available.
|
||
Button {
|
||
text: !Transfer.armed ? "Paste"
|
||
: "Paste " + Transfer.summary
|
||
+ (Transfer.framing-withheld ? ", no crop" : "");
|
||
enabled: Develop.enabled && Transfer.armed;
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { Transfer.paste(); }
|
||
}
|
||
|
||
// 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
|
||
// screen that is mostly photograph, and the whole point of opening an
|
||
// image is to look at it — so being able to put the instruments away
|
||
// and bring them back is worth a control of its own. The glyph points
|
||
// the way the panel will move.
|
||
Button {
|
||
text: root.panel-visible ? "Hide panel ›" : "‹ Panel";
|
||
active: root.panel-visible;
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.toggle-panel(); }
|
||
}
|
||
|
||
// Only while there is something to say. Empty, it still took a slot
|
||
// and the spacing either side of it, and this strip has no pixels to
|
||
// spare: at 1600 wide they are what kept Settings on screen.
|
||
if root.export-status != "": Caption {
|
||
text: root.export-status;
|
||
vertical-alignment: center;
|
||
overflow: elide;
|
||
}
|
||
|
||
// The export itself, beside the settings that shape it. This is the
|
||
// screen where a photograph is finished, so it is the screen where
|
||
// one is asked for — the grid can export a selection later, but a
|
||
// single finished frame is exported from in front of it.
|
||
Button {
|
||
text: root.export-busy ? "Exporting…" : root.export-label;
|
||
enabled: !root.export-busy;
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.export-image(); }
|
||
}
|
||
|
||
HorizontalLayout {
|
||
spacing: Theme.gap-sm;
|
||
|
||
// TRACES: FR-UI-4
|
||
// The pointer and touch way to the reference F1 opens. Beside
|
||
// Settings, as Help is in the library header, because it is about the
|
||
// application rather than about the photograph.
|
||
//
|
||
// A square "?" rather than a word: this strip is already full at
|
||
// 1600 pixels, and a labelled button pushed Settings off its end.
|
||
// Screen readers still hear "Help".
|
||
IconButton {
|
||
icon: "help";
|
||
label: "Help";
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.open-help(); }
|
||
}
|
||
|
||
// Reachable from develop as well as from the grid: export defaults are
|
||
// most likely to be wanted with a finished photograph on screen, which
|
||
// is exactly where this bar is and the library header is not.
|
||
Button {
|
||
text: "Settings";
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.open-settings(); }
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
y: parent.height - 1px;
|
||
height: 1px;
|
||
background: Theme.rule;
|
||
}
|
||
}
|
||
|
||
/// What the file says about how the photograph was taken.
|
||
///
|
||
/// A global rather than four properties on the panel, for the reason
|
||
/// `session.slint` gives at length: the panel is drawn twice — once in the
|
||
/// develop column and once in the portrait dock — and four lines of forwarding
|
||
/// copied into the second composition is four lines that can fall behind the
|
||
/// first without anything failing.
|
||
///
|
||
/// Nothing here is a control. It is the one panel in the column that only
|
||
/// reports, which is why this global carries no callback at all.
|
||
export global Capture {
|
||
in property <string> camera;
|
||
/// TRACES: FR-DEV-3
|
||
/// The lens, and whether it matched a correction profile.
|
||
///
|
||
/// Here rather than beside the optical sliders, and the reason is what the
|
||
/// line is *for*. `dr-lens` states the rule — an automatic correction that
|
||
/// silently did nothing is worse than one the user can see is unavailable
|
||
/// — and the fact it reports is a fact about the file: which lens took
|
||
/// this photograph, and whether the database has heard of it. That is the
|
||
/// same kind of thing as the body and the exposure, and it wants reading
|
||
/// once on opening rather than hunting for under a group filter.
|
||
in property <string> lens;
|
||
/// TRACES: FR-DEV-3e
|
||
/// The camera profile the photograph renders through, and where it was
|
||
/// found — or that there is none (D20). A fact about the file and the
|
||
/// installed profiles, read once on opening, as the lens line is.
|
||
in property <string> profile;
|
||
in property <string> exposure;
|
||
in property <string> dimensions;
|
||
}
|
||
|
||
/// TRACES: FR-DEV-3e
|
||
/// The one action the info panel offers: saving the camera profile embedded
|
||
/// in this file for every photograph from the same body (D20).
|
||
///
|
||
/// A global of its own because `Capture` only reports and carries no
|
||
/// callback. `offer` is the wording, naming the body; empty when there is
|
||
/// nothing to offer, which is every file but a DNG whose profile may be
|
||
/// copied and is not installed yet.
|
||
export global ProfileOffer {
|
||
in property <string> offer;
|
||
callback adopt();
|
||
}
|
||
|
||
// The offer as a line of text that acts, rather than a button: a button
|
||
// does not wrap, and a label naming a camera is long enough to widen the
|
||
// whole column.
|
||
component OfferLine inherits Rectangle {
|
||
in property <string> text;
|
||
callback clicked();
|
||
accessible-role: button;
|
||
accessible-label: root.text;
|
||
accessible-action-default => { root.clicked(); }
|
||
height: line.preferred-height;
|
||
|
||
line := Caption {
|
||
width: 100%;
|
||
text: root.text + " →";
|
||
wrap: word-wrap;
|
||
emphasised: touch.has-hover;
|
||
}
|
||
touch := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.clicked(); }
|
||
}
|
||
}
|
||
|
||
// Capture metadata. Read-only; the adjustment controls live in AdjustPanel,
|
||
// which is generated from pipeline capabilities rather than written here.
|
||
//
|
||
// Flat, like everything else in this column. This was briefly a collapsible
|
||
// section so that every panel could be put away; the sidebar as a whole now
|
||
// closes from the status strip instead, which is the control that was actually
|
||
// wanted, and the per-group lids only stood between the user and the controls.
|
||
export component InfoPanel inherits Rectangle {
|
||
background: transparent;
|
||
height: panel.preferred-height;
|
||
|
||
panel := Panel {
|
||
// Flat: this abuts the adjust panel below it and the rule between
|
||
// them is drawn by the column that stacks the two.
|
||
flat: true;
|
||
width: 100%;
|
||
|
||
PanelHeading { text: "IMAGE"; }
|
||
|
||
Value {
|
||
text: Capture.camera == "" ? "—" : Capture.camera;
|
||
placeholder: Capture.camera == "";
|
||
wrap: word-wrap;
|
||
}
|
||
|
||
Label { text: Capture.exposure; }
|
||
|
||
// Dim, like the dimensions below it: this is something the file says,
|
||
// not something the photographer chose. An empty string draws nothing
|
||
// — a session built from pixels with no header has no lens to report,
|
||
// and an empty row is honest where "Unknown" would be noise.
|
||
Caption {
|
||
text: Capture.lens;
|
||
visible: Capture.lens != "";
|
||
wrap: word-wrap;
|
||
}
|
||
|
||
Caption {
|
||
text: Capture.profile;
|
||
visible: Capture.profile != "";
|
||
wrap: word-wrap;
|
||
}
|
||
|
||
if ProfileOffer.offer != "": OfferLine {
|
||
text: ProfileOffer.offer;
|
||
clicked => { ProfileOffer.adopt(); }
|
||
}
|
||
|
||
Caption { text: Capture.dimensions; }
|
||
}
|
||
}
|