File presets in folders that open and close, as collections do
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.
This commit is contained in:
@@ -49,7 +49,9 @@ pub struct Section {
|
||||
/// A stable identifier, for a frontend that remembers which sections a
|
||||
/// photographer folded away. Never shown.
|
||||
pub id: &'static str,
|
||||
/// What the section is called on screen.
|
||||
/// What the section is called on screen, as a category path: `/`
|
||||
/// separates the levels, so `Film/Colour` is a folder inside `Film`. The
|
||||
/// same spelling a photographer's own preset names use for theirs.
|
||||
pub title: &'static str,
|
||||
/// The presets in it, every one reaching only what it names.
|
||||
pub presets: PresetLibrary,
|
||||
@@ -65,17 +67,17 @@ const SECTIONS: &[(&str, &str, &str)] = &[
|
||||
("skies", "Skies", include_str!("../presets/skies.drpl")),
|
||||
(
|
||||
"colour_film",
|
||||
"Colour film",
|
||||
"Film/Colour",
|
||||
include_str!("../presets/colour_film.drpl"),
|
||||
),
|
||||
(
|
||||
"cinema_film",
|
||||
"Cinema film",
|
||||
"Film/Cinema",
|
||||
include_str!("../presets/cinema_film.drpl"),
|
||||
),
|
||||
(
|
||||
"bw_film",
|
||||
"Black and white film",
|
||||
"Film/Black and white",
|
||||
include_str!("../presets/bw_film.drpl"),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -573,6 +573,32 @@ mod tests {
|
||||
assert_eq!(report.failed, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_subfolder_becomes_the_category_of_what_it_holds() {
|
||||
// The folder picked is the root and names nothing; each folder under
|
||||
// it is a level of category, as Lightroom's groups were.
|
||||
let dir = tempdir("categories");
|
||||
std::fs::write(dir.join("Golden Hour.xmp"), ELEMENT_FORM).unwrap();
|
||||
let nested = dir.join("Film").join("Colour");
|
||||
std::fs::create_dir_all(&nested).unwrap();
|
||||
std::fs::write(nested.join("Golden Hour.xmp"), ELEMENT_FORM).unwrap();
|
||||
|
||||
let names: Vec<_> = read_path(&dir).presets.into_iter().map(|p| p.0).collect();
|
||||
assert_eq!(names, ["Film/Colour/Golden Hour", "Golden Hour"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_slash_in_a_displayed_name_is_not_a_category() {
|
||||
let dir = tempdir("slash");
|
||||
std::fs::write(
|
||||
dir.join("p.xmp"),
|
||||
ATTRIBUTE_FORM.replace("Warm Portrait", "Warm / Cool"),
|
||||
)
|
||||
.unwrap();
|
||||
let report = read_path(&dir);
|
||||
assert_eq!(report.presets[0].0, "Warm \u{2215} Cool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_preset_without_a_name_is_called_after_its_file() {
|
||||
// Lightroom writes the name it displays, which is not always the file
|
||||
@@ -661,21 +687,37 @@ pub struct Report {
|
||||
/// A folder because that is the shape a photographer's presets are in — an
|
||||
/// exported Lightroom preset folder, nested one level per group — and asking
|
||||
/// them to import ninety files one at a time would be asking them not to
|
||||
/// bother. Nested folders are walked, which is what makes the group structure
|
||||
/// available to whatever wants it later.
|
||||
/// bother. Nested folders are walked, and each one below `path` becomes a
|
||||
/// category: a preset in `Portraits/` is named `Portraits/Warm skin`, which is
|
||||
/// how the preset menu files it (see `PresetLibrary`'s note on categories).
|
||||
///
|
||||
/// The *name* comes from `crs:Name` where the file carries one and from the
|
||||
/// file stem where it does not. Lightroom writes the name it displays, which
|
||||
/// is not always the file name, and the displayed name is the one the
|
||||
/// photographer will look for.
|
||||
/// photographer will look for. A `/` inside that name would read as a
|
||||
/// category it never had, so it becomes `∕`, which looks the same and
|
||||
/// separates nothing.
|
||||
pub fn read_path(path: &std::path::Path) -> Report {
|
||||
let mut report = Report::default();
|
||||
read_into(path, &mut report);
|
||||
read_into(path, "", &mut report);
|
||||
report.presets.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
report
|
||||
}
|
||||
|
||||
fn read_into(path: &std::path::Path, report: &mut Report) {
|
||||
/// The category a folder below the import root files its presets under.
|
||||
fn category_of(parent: &str, folder: &std::path::Path) -> String {
|
||||
let Some(name) = folder.file_name() else {
|
||||
return parent.to_string();
|
||||
};
|
||||
let name = name.to_string_lossy().replace('/', "\u{2215}");
|
||||
if parent.is_empty() {
|
||||
name
|
||||
} else {
|
||||
format!("{parent}/{name}")
|
||||
}
|
||||
}
|
||||
|
||||
fn read_into(path: &std::path::Path, category: &str, report: &mut Report) {
|
||||
if path.is_dir() {
|
||||
let Ok(entries) = std::fs::read_dir(path) else {
|
||||
log::warn!("preset import: cannot read {}", path.display());
|
||||
@@ -686,7 +728,12 @@ fn read_into(path: &std::path::Path, report: &mut Report) {
|
||||
let mut paths: Vec<std::path::PathBuf> = entries.flatten().map(|e| e.path()).collect();
|
||||
paths.sort();
|
||||
for path in paths {
|
||||
read_into(&path, report);
|
||||
let category = if path.is_dir() {
|
||||
category_of(category, &path)
|
||||
} else {
|
||||
category.to_string()
|
||||
};
|
||||
read_into(&path, &category, report);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -710,6 +757,12 @@ fn read_into(path: &std::path::Path, report: &mut Report) {
|
||||
.unwrap_or_default()
|
||||
});
|
||||
report.unsupported.extend(import.skipped);
|
||||
let name = name.replace('/', "\u{2215}");
|
||||
let name = if category.is_empty() {
|
||||
name
|
||||
} else {
|
||||
format!("{category}/{name}")
|
||||
};
|
||||
report.presets.push((name, import.preset));
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
Reference in New Issue
Block a user