Ship presets with the application instead of seeding them
The six starter presets were copied into the photographer's own library on a first run and were theirs from then on. That cannot grow into a real collection: a copy is frozen at the release that wrote it, so an improved preset reaches nobody who had the old one, and re-seeding would overwrite a preset someone had tuned. `dr_pipeline::bundled` now holds the shipped presets as `.drpl` files compiled into the binary, in sections — Essentials (the former six) and three sections of film presets, one per measured stock in dr-film, printed on the paper its profile names — and never writes them to the user's file. Every shipped preset is a look (`Reach::Named`), so applying one keeps the corrections a photograph already has. A name links a photographer's copy to a shipped preset. Saving over a shipped name makes their version the one that name applies; it is listed in the shipped section, marked as changed, and deleting it reverts to the shipped one. Renaming it makes it one of their own and the shipped preset reappears. Keyed on the name because that is what the photographer sees and chooses by. Copies an older first run seeded are forgotten on load where they are still exactly as seeded — otherwise all six would list as changed and stay frozen at their old values. A tuned one is kept and now overrides. The sheet lists "Yours" first, then each shipped section, with headings. Shipped rows apply and nothing else; a changed row offers Revert where the photographer's own offer Delete. A dr-ui test checks every shipped film names a stock this build can bake, on that stock's own paper, because dr-pipeline does not link the profile database. The film presets name stocks by id; the measurements behind them are spektrafilm's (CC BY-SA 4.0), attributed in each file as in dr-film.
This commit is contained in:
@@ -14,7 +14,7 @@ import { CollectionsPanel, CollectionRow, Collections, OfflinePrompt, Collection
|
||||
MembershipSheet, MembershipRow } from "collections.slint";
|
||||
import { HistogramPanel, HistogramView, Levels } from "histogram.slint";
|
||||
import { RecoveryPrompt } from "recovery.slint";
|
||||
import { CopySheet, PresetSheet, ScopeChips, ScopeKind } from "presets.slint";
|
||||
import { CopySheet, PresetOrigin, PresetRow, PresetSheet, ScopeChips, ScopeKind } from "presets.slint";
|
||||
import { ExportOptions, ExportSheet } from "export.slint";
|
||||
import { FocusMarks, FocusPanel, Peaking } from "peaking.slint";
|
||||
import { SettingsPage } from "settings.slint";
|
||||
@@ -499,7 +499,7 @@ export component AppWindow inherits Window {
|
||||
// where there is an edit to capture, and applied most often in the grid,
|
||||
// where there is a selection to apply it to.
|
||||
in-out property <bool> presets-open: false;
|
||||
in property <[string]> preset-names;
|
||||
in property <[PresetRow]> preset-rows;
|
||||
/// Whether there is an edit in hand to save, set by whichever view opened
|
||||
/// the sheet. `in-out` because that is where the answer is known.
|
||||
in-out property <bool> preset-can-save: false;
|
||||
@@ -3412,7 +3412,7 @@ in property <bool> panel-visible: true;
|
||||
if root.presets-open: PresetSheet {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
names: root.preset-names;
|
||||
rows: root.preset-rows;
|
||||
can-save: root.preset-can-save;
|
||||
apply-count: root.preset-apply-count;
|
||||
capture-summary: root.preset-capture-summary;
|
||||
|
||||
+58
-26
@@ -16,6 +16,31 @@ export struct ScopeKind {
|
||||
on: bool,
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6
|
||||
// Where a listed preset comes from, which decides what can be done to it.
|
||||
export enum PresetOrigin {
|
||||
// The photographer's own.
|
||||
yours,
|
||||
// Shipped with the application. Applied, never renamed or deleted.
|
||||
shipped,
|
||||
// The photographer's copy under a shipped preset's name, standing in for
|
||||
// it. Deleting it reverts to the shipped one; renaming it makes it theirs.
|
||||
changed,
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6
|
||||
// One row of the preset sheet: a section heading, or a preset.
|
||||
//
|
||||
// One flat list with the headings in it rather than a list of sections, for
|
||||
// the reason the rows are placed by index below: a nested `for` inside a
|
||||
// `Flickable` has no layout to size it, and the arithmetic stays one line.
|
||||
export struct PresetRow {
|
||||
// Non-empty on a heading row, which names no preset.
|
||||
heading: string,
|
||||
name: string,
|
||||
origin: PresetOrigin,
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6 | FR-UI-2
|
||||
// Which kinds of edit travel: a multi-select block of chips that wraps.
|
||||
//
|
||||
@@ -78,8 +103,9 @@ export component ScopeChips inherits Rectangle {
|
||||
// second idiom for the same gesture would be a second thing to learn for no
|
||||
// gain.
|
||||
export component PresetSheet inherits Rectangle {
|
||||
/// The saved names, in the order they are stored.
|
||||
in property <[string]> names;
|
||||
/// The photographer's presets and the shipped ones, sectioned, in the
|
||||
/// order they are listed.
|
||||
in property <[PresetRow]> rows;
|
||||
/// Whether there is an edit in hand to save. False in the library, where
|
||||
/// nothing is open, and with an image that failed to decode.
|
||||
in property <bool> can-save: false;
|
||||
@@ -198,14 +224,21 @@ export component PresetSheet inherits Rectangle {
|
||||
// A floor, so the list is not squeezed out of existence by the
|
||||
// field and the button around it on a short window.
|
||||
min-height: 120px;
|
||||
viewport-height: root.names.length * (Theme.touch-target + 2px);
|
||||
viewport-height: root.rows.length * (Theme.touch-target + 2px);
|
||||
|
||||
for entry[i] in root.names: Rectangle {
|
||||
for entry[i] in root.rows: Rectangle {
|
||||
y: i * (Theme.touch-target + 2px);
|
||||
width: parent.width;
|
||||
height: Theme.touch-target;
|
||||
|
||||
if root.renaming != entry: HorizontalLayout {
|
||||
if entry.heading != "": PanelHeading {
|
||||
text: entry.heading;
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
vertical-alignment: bottom;
|
||||
}
|
||||
|
||||
if entry.heading == "" && root.renaming != entry.name: HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
// The name is the apply button rather than a label
|
||||
@@ -213,33 +246,42 @@ export component PresetSheet inherits Rectangle {
|
||||
// row whose largest target does nothing is a row that
|
||||
// gets pressed by accident and then distrusted.
|
||||
Button {
|
||||
text: entry;
|
||||
text: entry.origin == PresetOrigin.changed
|
||||
? entry.name + " · changed"
|
||||
: entry.name;
|
||||
horizontal-stretch: 1;
|
||||
clicked => { root.apply(entry); }
|
||||
clicked => { root.apply(entry.name); }
|
||||
}
|
||||
|
||||
Button {
|
||||
// A shipped preset is applied and nothing else: it is
|
||||
// not the photographer's to rename or delete, and a
|
||||
// button that did either would be one that lied.
|
||||
// Saving over its name is how it is changed.
|
||||
if entry.origin != PresetOrigin.shipped: Button {
|
||||
text: "Rename";
|
||||
clicked => { root.renaming = entry; }
|
||||
clicked => { root.renaming = entry.name; }
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Delete";
|
||||
clicked => { root.remove(entry); }
|
||||
// "Revert" on a changed one, because that is what
|
||||
// deleting the copy does: the shipped preset is
|
||||
// still there underneath it.
|
||||
if entry.origin != PresetOrigin.shipped: Button {
|
||||
text: entry.origin == PresetOrigin.changed ? "Revert" : "Delete";
|
||||
clicked => { root.remove(entry.name); }
|
||||
}
|
||||
}
|
||||
|
||||
// Renaming in place rather than in a second sheet: a
|
||||
// dialogue over a dialogue is where a user loses track of
|
||||
// which one Escape closes.
|
||||
if root.renaming == entry: HorizontalLayout {
|
||||
if entry.heading == "" && root.renaming == entry.name: HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
rename-field := Field {
|
||||
text: entry;
|
||||
text: entry.name;
|
||||
horizontal-stretch: 1;
|
||||
accepted(text) => {
|
||||
root.rename(entry, text);
|
||||
root.rename(entry.name, text);
|
||||
root.renaming = "";
|
||||
}
|
||||
}
|
||||
@@ -252,20 +294,10 @@ export component PresetSheet inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
if root.names.length == 0: Text {
|
||||
text: root.can-save
|
||||
? "No presets yet. Name the edit above to make the first."
|
||||
: "No presets yet. Open a photograph and save one from the develop panel.";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
wrap: word-wrap;
|
||||
width: parent.width;
|
||||
}
|
||||
|
||||
// Says what applying would do *before* it is done, the same way
|
||||
// the grid's "Paste to 40" does — a count in the label is worth
|
||||
// more than a confirmation asking the same question afterwards.
|
||||
if root.names.length > 0 && root.apply-count > 0: Caption {
|
||||
if root.apply-count > 0: Caption {
|
||||
text: root.apply-count == 1
|
||||
? "Applies to 1 selected photograph"
|
||||
: "Applies to " + root.apply-count + " selected photographs";
|
||||
|
||||
Reference in New Issue
Block a user