mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 21:56:24 +00:00
ui.dialog.set stored any field name it was given, so a misspelled field (`discrad` for `discard`) was kept without effect and the dialog then ran with its old value. It now answers such a field with an error that lists the dialog's fields, and leaves the dialog unchanged. A dialog accepts the fields it holds, `__` fields, `discard` with a discard button, `preview` with a live preview, the fields it reads before a control adds them (each spec's `inputs`, for example Copy and the object options of the Transform dialogs, Move's `distance` and `angle`, the reference point, `path` of the export options dialogs, `searchFolder`, Brush Options' `tips`, the Justification and Hyphenation values that open blank, the Corners `kind` and `radius` of corners that differ, the Gradient Stop popover's `color`, `opacity`, `location` and `spread`, and a new artboard's `color` and display marks), and the fields it passes to the command it runs: every parameter of the generic `command` dialog's command, and the `textureImage` that Load Texture… sets in the Glass and Texturizer effects. Edit Selection now opens holding `deleted<i>` for each of its rows, so setting a row number it does not have returns an error, and Transparency Flattener Presets opens holding the selected preset's name and options instead of adding them on its first frame.
186 lines
7.3 KiB
Rust
186 lines
7.3 KiB
Rust
//! Select → Edit Selection…: the document's saved selections in a list, with the chosen one's name
|
|
//! to edit and Delete. Nothing changes until OK, which applies every rename and delete as one
|
|
//! undo step (`select.editSaved` `edits`); Cancel leaves them as they were.
|
|
//!
|
|
//! Fields: `count` and, for each saved selection `i`, `orig<i>` (its name when the dialog opened),
|
|
//! `name<i>` (as edited) and `deleted<i>`; `selected` (the row picked).
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_engine::doc::SavedSelection;
|
|
|
|
use super::{DialogSpec, form};
|
|
use crate::state::Dialog;
|
|
use crate::{VectorcraftApp, widgets};
|
|
|
|
/// The dialog kind of Edit Selection.
|
|
pub const KIND: &str = "editSelection";
|
|
|
|
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Edit Selection").into(), body, confirm, min_width: 420.0, ..DialogSpec::FORM };
|
|
|
|
/// Open Edit Selection on the active document's saved selections.
|
|
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
|
let names = app.run("select.savedList", json!({}))?;
|
|
let names: Vec<&str> = names.as_array().map(|a| a.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
|
|
if names.is_empty() {
|
|
return Err("No saved selections (Select → Save Selection…)".into());
|
|
}
|
|
let mut fields = json!({ "count": names.len(), "selected": 0 });
|
|
for (i, n) in names.iter().enumerate() {
|
|
fields[format!("orig{i}")] = json!(n);
|
|
fields[format!("name{i}")] = json!(n);
|
|
fields[format!("deleted{i}")] = json!(false);
|
|
}
|
|
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
fn count(d: &Dialog) -> usize {
|
|
d.fields.get("count").and_then(Value::as_u64).and_then(|n| usize::try_from(n).ok()).unwrap_or(0).min(SavedSelection::MAX)
|
|
}
|
|
|
|
/// The rows still in the list (not deleted).
|
|
fn rows(d: &Dialog) -> Vec<usize> {
|
|
(0..count(d)).filter(|i| !d.bool(&format!("deleted{i}"))).collect()
|
|
}
|
|
|
|
fn selected(d: &Dialog, rows: &[usize]) -> Option<usize> {
|
|
let want = d.fields.get("selected").and_then(Value::as_u64).and_then(|n| usize::try_from(n).ok());
|
|
want.filter(|w| rows.contains(w)).or_else(|| rows.first().copied())
|
|
}
|
|
|
|
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
|
let rows = rows(d);
|
|
let current = selected(d, &rows);
|
|
let mut pick = None;
|
|
let mut delete = false;
|
|
ui.horizontal_top(|ui| {
|
|
ui.vertical(|ui| {
|
|
ui.set_width(200.0);
|
|
widgets::dim_label(ui, tl!("Saved Selections"));
|
|
widgets::list_box(ui, |ui| {
|
|
egui::ScrollArea::vertical().id_salt("saved-selections").min_scrolled_height(180.0).max_height(180.0).show(ui, |ui| {
|
|
ui.set_width(ui.available_width());
|
|
ui.set_min_height(180.0);
|
|
for &i in &rows {
|
|
if ui.selectable_label(Some(i) == current, d.str(&format!("name{i}"))).clicked() {
|
|
pick = Some(i);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
ui.add_space(16.0);
|
|
ui.vertical(|ui| {
|
|
ui.set_width(190.0);
|
|
if let Some(i) = current {
|
|
widgets::dim_label(ui, tl!("Name:"));
|
|
form::text(ui, d, &format!("name{i}"), 190.0);
|
|
ui.add_space(10.0);
|
|
delete = widgets::flat_button(ui, tl!("Delete"), 70.0).clicked();
|
|
}
|
|
});
|
|
});
|
|
if let Some(i) = pick {
|
|
d.fields.insert("selected".into(), json!(i));
|
|
}
|
|
if let (true, Some(i)) = (delete, current) {
|
|
d.fields.insert(format!("deleted{i}"), json!(true));
|
|
// The row below takes its place (the one above, for the last).
|
|
let next = rows.iter().copied().find(|r| *r > i).or_else(|| rows.iter().copied().rfind(|r| *r < i));
|
|
d.fields.insert("selected".into(), json!(next.unwrap_or(0)));
|
|
}
|
|
false
|
|
}
|
|
|
|
/// The edits OK applies: each deleted selection, and each one whose name was changed.
|
|
fn edits(d: &Dialog) -> Vec<Value> {
|
|
(0..count(d))
|
|
.filter_map(|i| {
|
|
let (orig, name) = (d.str(&format!("orig{i}")), d.str(&format!("name{i}")));
|
|
if d.bool(&format!("deleted{i}")) {
|
|
Some(json!({ "name": orig, "delete": true }))
|
|
} else if !name.trim().is_empty() && name.trim() != orig {
|
|
Some(json!({ "name": orig, "newName": name.trim() }))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
|
let edits = edits(d);
|
|
// A name two selections would share keeps the dialog open, to be changed.
|
|
let r = if edits.is_empty() { Ok(Value::Null) } else { app.run("select.editSaved", json!({ "edits": edits })) };
|
|
if r.is_ok() {
|
|
app.ui.dialog = None;
|
|
}
|
|
r
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::json;
|
|
|
|
use crate::VectorcraftApp;
|
|
|
|
fn app() -> VectorcraftApp {
|
|
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
|
app.run("file.new", json!({})).unwrap();
|
|
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
|
for n in ["Logo", "Icons", "Text"] {
|
|
app.run("select.save", json!({ "name": n })).unwrap();
|
|
}
|
|
app
|
|
}
|
|
|
|
fn set(app: &mut VectorcraftApp, k: &str, v: serde_json::Value) {
|
|
app.ui.dialog.as_mut().unwrap().fields.insert(k.into(), v);
|
|
}
|
|
|
|
fn names(app: &mut VectorcraftApp) -> serde_json::Value {
|
|
app.run("select.savedList", json!({})).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn nothing_changes_until_ok_and_all_edits_are_one_undo_step() {
|
|
let mut app = app();
|
|
super::open(&mut app).unwrap();
|
|
// Rename the first and delete the last.
|
|
set(&mut app, "name0", json!(" Brand "));
|
|
set(&mut app, "deleted2", json!(true));
|
|
assert_eq!(names(&mut app), json!(["Logo", "Icons", "Text"]));
|
|
super::super::confirm(&mut app).unwrap();
|
|
assert!(app.ui.dialog.is_none());
|
|
assert_eq!(names(&mut app), json!(["Brand", "Icons"]));
|
|
app.run("edit.undo", json!({})).unwrap();
|
|
assert_eq!(names(&mut app), json!(["Logo", "Icons", "Text"]));
|
|
}
|
|
|
|
#[test]
|
|
fn cancel_leaves_them_and_a_shared_name_keeps_the_dialog_open() {
|
|
let mut app = app();
|
|
super::open(&mut app).unwrap();
|
|
set(&mut app, "name0", json!("Gone"));
|
|
super::super::cancel(&mut app);
|
|
assert_eq!(names(&mut app), json!(["Logo", "Icons", "Text"]));
|
|
super::open(&mut app).unwrap();
|
|
set(&mut app, "name0", json!("Text"));
|
|
assert!(super::super::confirm(&mut app).is_err());
|
|
assert!(app.ui.dialog.is_some());
|
|
assert_eq!(names(&mut app), json!(["Logo", "Icons", "Text"]));
|
|
// Deleting the other one frees the name.
|
|
set(&mut app, "deleted2", json!(true));
|
|
super::super::confirm(&mut app).unwrap();
|
|
assert_eq!(names(&mut app), json!(["Text", "Icons"]));
|
|
}
|
|
|
|
#[test]
|
|
fn it_needs_a_saved_selection() {
|
|
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
|
app.run("file.new", json!({})).unwrap();
|
|
assert!(super::open(&mut app).is_err());
|
|
assert!(app.ui.dialog.is_none());
|
|
}
|
|
}
|