mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 04:07:12 -06:00
Merge pull request #4247 from ashokram-si/feat/named-views
View: all 25 saved views in the View menu; Edit Views… lists them
This commit is contained in:
+46
-1
@@ -621,6 +621,29 @@ pub struct SavedView {
|
||||
pub rotation: f64,
|
||||
}
|
||||
|
||||
impl SavedView {
|
||||
/// Most views a document keeps (the View menu lists every one).
|
||||
pub const MAX: usize = 25;
|
||||
/// Longest name, in characters.
|
||||
pub const MAX_NAME: usize = 255;
|
||||
|
||||
/// `name` trimmed and cut to [`Self::MAX_NAME`] characters (`None` when blank).
|
||||
pub fn clean_name(name: &str) -> Option<String> {
|
||||
let name: String = name.trim().chars().take(Self::MAX_NAME).collect();
|
||||
(!name.is_empty()).then_some(name)
|
||||
}
|
||||
|
||||
/// The first "View N" none of `saved` is named.
|
||||
pub fn default_name(saved: &[SavedView]) -> String {
|
||||
(1..=saved.len() + 1).map(|i| format!("View {i}")).find(|n| !saved.iter().any(|x| &x.name == n)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Can the canvas show this view: a finite centre and a finite, positive zoom.
|
||||
pub fn is_usable(&self) -> bool {
|
||||
self.center.x.is_finite() && self.center.y.is_finite() && self.zoom.is_finite() && self.zoom > 0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A saved selection (Select → Save Selection…): the objects that were selected, listed by name at
|
||||
/// the bottom of the Select menu.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -851,7 +874,7 @@ pub struct Document {
|
||||
pub graph_designs: Vec<GraphDesign>,
|
||||
#[serde(default)]
|
||||
pub guides: Vec<Guide>,
|
||||
/// View → New View… (up to 25, like Illustrator).
|
||||
/// View → New View… (at most [`SavedView::MAX`]); saved with the document.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub views: Vec<SavedView>,
|
||||
/// Select → Save Selection… (at most [`SavedSelection::MAX`]); saved with the document.
|
||||
@@ -2134,6 +2157,28 @@ impl Document {
|
||||
self.saved_selections = kept.into();
|
||||
}
|
||||
|
||||
/// Saved views as a file gives them, made safe to list and go to: at most [`SavedView::MAX`],
|
||||
/// each with a clean, unique name, a finite centre and a finite, positive zoom (a rotation
|
||||
/// that isn't finite becomes 0°).
|
||||
pub fn tidy_views(&mut self) {
|
||||
if self.views.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut kept: Vec<SavedView> = vec![];
|
||||
for v in std::mem::take(&mut self.views) {
|
||||
if kept.len() >= SavedView::MAX {
|
||||
break;
|
||||
}
|
||||
let Some(name) = SavedView::clean_name(&v.name) else { continue };
|
||||
if !v.is_usable() || kept.iter().any(|k| k.name == name) {
|
||||
continue;
|
||||
}
|
||||
let rotation = if v.rotation.is_finite() { v.rotation } else { 0.0 };
|
||||
kept.push(SavedView { name, rotation, ..v });
|
||||
}
|
||||
self.views = kept;
|
||||
}
|
||||
|
||||
/// Leave opacity-mask editing: drop the temporary editing layer, a working copy of art the
|
||||
/// mask already holds (the engine syncs it after every edit).
|
||||
pub fn drop_edit_modes(&mut self) {
|
||||
|
||||
+138
-26
@@ -7,9 +7,6 @@ use vectorcraft_geom::Point;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Illustrator keeps up to 25 views per document.
|
||||
pub const MAX_VIEWS: usize = 25;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
@@ -17,7 +14,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New View…",
|
||||
["View"],
|
||||
None,
|
||||
"{name?, center: [x, y], zoom: (1 = 100%), rotation?: deg} save a view (the app fills in the current one) → {name, index}",
|
||||
"{name?, center: [x, y], zoom: (1 = 100%), rotation?: deg} save a view in the document (the app fills in the current one; default \"View N\"; at most 25, names up to 255 characters, each name once) → {name, index}",
|
||||
has_doc,
|
||||
new_view
|
||||
),
|
||||
@@ -26,8 +23,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Edit Views…",
|
||||
["View"],
|
||||
None,
|
||||
"{name, newName?, delete?: bool} rename or delete a saved view; no params → {views}",
|
||||
has_doc,
|
||||
"{name, newName?: rename, delete?: bool} | {edits: [{name, newName?, delete?}…]} rename or delete saved views, all in one undo step; names are those before the edit and must stay unique; no params → {views}",
|
||||
has_views,
|
||||
edit_views
|
||||
),
|
||||
cmd!(query "view.saved.list", "Saved Views", [], None, "{} → {views: [{name, center, zoom, rotation}]}", has_doc, list_views),
|
||||
@@ -66,12 +63,12 @@ fn new_view(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => 0.0,
|
||||
};
|
||||
let st = s.doc()?;
|
||||
if st.doc.views.len() >= MAX_VIEWS {
|
||||
return Err(bad(C, format!("a document keeps at most {MAX_VIEWS} views")));
|
||||
if st.doc.views.len() >= SavedView::MAX {
|
||||
return Err(bad(C, format!("a document keeps at most {} views", SavedView::MAX)));
|
||||
}
|
||||
let name = match str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()) {
|
||||
Some(n) => n.to_string(),
|
||||
None => (1..).map(|i| format!("View {i}")).find(|n| !st.doc.views.iter().any(|v| &v.name == n)).unwrap_or_default(),
|
||||
let name = match str_param(p, "name").and_then(SavedView::clean_name) {
|
||||
Some(n) => n,
|
||||
None => SavedView::default_name(&st.doc.views),
|
||||
};
|
||||
if st.doc.views.iter().any(|v| v.name == name) {
|
||||
return Err(bad(C, format!("a view named `{name}` exists")));
|
||||
@@ -84,26 +81,60 @@ fn new_view(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "name": name, "index": index }))
|
||||
}
|
||||
|
||||
/// Edit Views… needs a document with at least one saved view.
|
||||
fn has_views(s: &Session) -> std::result::Result<(), String> {
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
if st.doc.views.is_empty() { Err("no saved views".into()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
fn edit_views(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "view.saved.edit";
|
||||
let Some(name) = str_param(p, "name") else { return list_views(s, p) };
|
||||
let i = s.doc()?.doc.views.iter().position(|v| v.name == name).ok_or_else(|| bad(C, format!("no view named `{name}`")))?;
|
||||
if bool_or(p, "delete", false) {
|
||||
s.edit("Delete View", |d, _| {
|
||||
d.views.remove(i);
|
||||
let (edits, one) = match p.get("edits").and_then(Value::as_array) {
|
||||
Some(list) => (list.as_slice(), false),
|
||||
None if str_param(p, "name").is_some() => (std::slice::from_ref(p), true),
|
||||
None => return list_views(s, p),
|
||||
};
|
||||
let before = s.doc()?.doc.views.clone();
|
||||
// Edits name the views as they are now, so they don't depend on each other's order.
|
||||
let mut after: Vec<Option<SavedView>> = before.iter().cloned().map(Some).collect();
|
||||
for e in edits {
|
||||
let name = str_param(e, "name").ok_or_else(|| bad(C, "missing name"))?;
|
||||
let i = before.iter().position(|v| v.name == name).ok_or_else(|| bad(C, format!("no view named `{name}`")))?;
|
||||
let Some(slot) = after.get_mut(i) else { continue };
|
||||
if bool_or(e, "delete", false) {
|
||||
*slot = None;
|
||||
} else if let Some(v) = slot {
|
||||
v.name = str_param(e, "newName").and_then(SavedView::clean_name).ok_or_else(|| bad(C, "give newName or delete"))?;
|
||||
}
|
||||
}
|
||||
let after: Vec<SavedView> = after.into_iter().flatten().collect();
|
||||
let mut names = std::collections::HashSet::with_capacity(after.len());
|
||||
for v in &after {
|
||||
if !names.insert(v.name.as_str()) {
|
||||
return Err(bad(C, format!("a view named `{}` exists", v.name)));
|
||||
}
|
||||
}
|
||||
// One rename or delete keeps the name of its step; several are one Edit Views step.
|
||||
let deleted = after.len() < before.len();
|
||||
let label = if !one {
|
||||
"Edit Views"
|
||||
} else if deleted {
|
||||
"Delete View"
|
||||
} else {
|
||||
"Rename View"
|
||||
};
|
||||
let result = match (one, deleted) {
|
||||
(true, true) => json!({ "deleted": str_param(p, "name") }),
|
||||
(true, false) => json!({ "name": str_param(p, "newName").and_then(SavedView::clean_name) }),
|
||||
(false, _) => json!({ "views": after }),
|
||||
};
|
||||
if before != after {
|
||||
s.edit(label, |d, _| {
|
||||
d.views = after;
|
||||
Ok(())
|
||||
})?;
|
||||
return Ok(json!({ "deleted": name }));
|
||||
}
|
||||
let new = str_param(p, "newName").map(str::trim).filter(|n| !n.is_empty()).ok_or_else(|| bad(C, "give newName or delete"))?.to_string();
|
||||
if s.doc()?.doc.views.iter().any(|v| v.name == new) {
|
||||
return Err(bad(C, format!("a view named `{new}` exists")));
|
||||
}
|
||||
s.edit("Rename View", |d, _| {
|
||||
d.views[i].name = new.clone();
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "name": new }))
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -154,4 +185,85 @@ mod tests {
|
||||
let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc);
|
||||
assert_eq!(vectorcraft_format::load(&bytes).unwrap().views[0].name, "Overview");
|
||||
}
|
||||
|
||||
fn names(s: &mut Session) -> Vec<String> {
|
||||
s.doc().unwrap().doc.views.iter().map(|v| v.name.clone()).collect()
|
||||
}
|
||||
|
||||
fn session_with_views(names: &[&str]) -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
for (i, n) in names.iter().enumerate() {
|
||||
s.execute("view.saved.new", &json!({"name": n, "center": [i, i], "zoom": 1 + i})).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Two named views go out with the file and come back with their names, centres, zooms and
|
||||
/// rotations, in order.
|
||||
#[test]
|
||||
fn saved_views_reopen_with_their_names_and_viewports() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("view.saved.new", &json!({"name": "Overview", "center": [306, 396], "zoom": 0.5})).unwrap();
|
||||
s.execute("view.saved.new", &json!({"name": "Logo detail", "center": [40.25, -12.5], "zoom": 8, "rotation": -30})).unwrap();
|
||||
let saved = s.doc().unwrap().doc.views.clone();
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save_file(&s.doc().unwrap().doc)).unwrap();
|
||||
assert_eq!(back.views, saved);
|
||||
assert_eq!((back.views[1].center.x, back.views[1].center.y, back.views[1].zoom, back.views[1].rotation), (40.25, -12.5, 8.0, -30.0));
|
||||
}
|
||||
|
||||
/// Edit Views… applies its renames and deletes together, as one undo step, by the names the
|
||||
/// views had before (so two can swap names); a shared or unknown name changes nothing.
|
||||
#[test]
|
||||
fn edit_views_applies_several_edits_as_one_step() {
|
||||
let mut s = session_with_views(&["A", "B", "C"]);
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
let edits = json!({"edits": [
|
||||
{"name": "A", "newName": "B"},
|
||||
{"name": "B", "newName": " A "},
|
||||
{"name": "C", "delete": true},
|
||||
]});
|
||||
let r = s.execute("view.saved.edit", &edits).unwrap();
|
||||
assert_eq!(r["views"].as_array().map(Vec::len), Some(2));
|
||||
assert_eq!(names(&mut s), ["B", "A"]);
|
||||
// The views keep their viewports under their new names.
|
||||
assert_eq!(s.doc().unwrap().doc.views[0].zoom, 1.0);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(names(&mut s), ["A", "B", "C"]);
|
||||
for bad in [
|
||||
json!({"edits": [{"name": "A", "newName": "C"}]}),
|
||||
json!({"edits": [{"name": "A", "newName": "X"}, {"name": "B", "newName": "X"}]}),
|
||||
json!({"edits": [{"name": "A", "delete": true}, {"name": "Nope", "delete": true}]}),
|
||||
json!({"edits": [{"name": "A", "newName": " "}]}),
|
||||
] {
|
||||
assert!(s.execute("view.saved.edit", &bad).is_err(), "accepted {bad}");
|
||||
assert_eq!(names(&mut s), ["A", "B", "C"]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A document keeps at most 25 views; unnamed ones take the first free "View N", and names are
|
||||
/// capped like other names the user gives.
|
||||
#[test]
|
||||
fn a_document_keeps_at_most_25_views() {
|
||||
let all: Vec<String> = (0..25).map(|i| format!("V{i}")).collect();
|
||||
let mut s = session_with_views(&all.iter().map(String::as_str).collect::<Vec<_>>());
|
||||
assert!(s.execute("view.saved.new", &json!({"name": "One too many", "center": [0, 0], "zoom": 1})).is_err());
|
||||
assert_eq!(s.doc().unwrap().doc.views.len(), 25);
|
||||
s.execute("view.saved.edit", &json!({"name": "V0", "delete": true})).unwrap();
|
||||
assert_eq!(s.execute("view.saved.new", &json!({"center": [0, 0], "zoom": 1})).unwrap()["name"], "View 1");
|
||||
s.execute("view.saved.edit", &json!({"name": "V1", "delete": true})).unwrap();
|
||||
let long = "x".repeat(1000);
|
||||
assert_eq!(s.execute("view.saved.new", &json!({"name": long, "center": [0, 0], "zoom": 1})).unwrap()["name"], "x".repeat(255));
|
||||
}
|
||||
|
||||
/// Edit Views… is off until the document has a view.
|
||||
#[test]
|
||||
fn edit_views_needs_a_saved_view() {
|
||||
let mut s = session_with_views(&[]);
|
||||
assert!(s.execute("view.saved.edit", &json!({})).is_err());
|
||||
s.execute("view.saved.new", &json!({"name": "Only", "center": [0, 0], "zoom": 1})).unwrap();
|
||||
assert_eq!(s.execute("view.saved.edit", &json!({})).unwrap()["views"][0]["name"], "Only");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,6 +597,8 @@ pub fn load_file(bytes: &[u8]) -> Result<NativeFile, FormatError> {
|
||||
doc.setup.keep_in_range();
|
||||
// Untrusted: capped, named and pointing at objects the file has.
|
||||
doc.tidy_saved_selections();
|
||||
// Untrusted: capped, named and with a zoom and centre the canvas can show.
|
||||
doc.tidy_views();
|
||||
doc.tidy_graph_designs();
|
||||
doc.raster_effects.add_around = doc.raster_effects.add_around.clamp(0.0, vectorcraft_doc::rastersettings::MAX_ADD_AROUND);
|
||||
let mut depth_notes = vec![];
|
||||
|
||||
@@ -260,3 +260,43 @@ fn saved_selections_from_a_file_are_tidied() {
|
||||
v["document"]["saved_selections"] = json!([{"name": 5, "objects": "x"}]);
|
||||
assert!(load(&serde_json::to_vec(&v).unwrap()).is_err());
|
||||
}
|
||||
|
||||
/// Saved views (View → New View…) travel with the file, names and viewports intact; older files
|
||||
/// have none, and a hostile one is cut down to what the View menu can list, with names that fit
|
||||
/// and a zoom the canvas can show.
|
||||
#[test]
|
||||
fn saved_views_round_trip_and_from_a_file_are_tidied() {
|
||||
use vectorcraft_doc::SavedView;
|
||||
use vectorcraft_geom::Point;
|
||||
let mut d = rich_doc();
|
||||
let mut v: Value = serde_json::from_slice(&save(&d, false)).unwrap();
|
||||
assert!(v["document"].get("views").is_none(), "nothing written when there are none");
|
||||
assert!(load(&serde_json::to_vec(&v).unwrap()).unwrap().views.is_empty(), "a file from before views opens");
|
||||
d.views = vec![
|
||||
SavedView { name: "Overview".into(), center: Point::new(306.0, 396.0), zoom: 0.5, rotation: 0.0 },
|
||||
SavedView { name: "Logo detail".into(), center: Point::new(40.25, -12.5), zoom: 8.0, rotation: -30.0 },
|
||||
];
|
||||
assert_eq!(load(&save(&d, false)).unwrap().views, d.views);
|
||||
let mut list = vec![
|
||||
json!({"name": format!(" {} ", "x".repeat(1000)), "center": {"x": 1, "y": 2}, "zoom": 2}),
|
||||
json!({"name": " ", "center": {"x": 0, "y": 0}, "zoom": 1}),
|
||||
json!({"name": "Zero", "center": {"x": 0, "y": 0}, "zoom": 0}),
|
||||
json!({"name": "Negative", "center": {"x": 0, "y": 0}, "zoom": -3}),
|
||||
json!({"name": "Dup", "center": {"x": 5, "y": 6}, "zoom": 3, "rotation": 10}),
|
||||
json!({"name": "Dup", "center": {"x": 0, "y": 0}, "zoom": 1}),
|
||||
];
|
||||
list.extend((0..100).map(|i| json!({"name": format!("V{i}"), "center": {"x": i, "y": i}, "zoom": 1})));
|
||||
v["document"]["views"] = json!(list);
|
||||
let back = load(&serde_json::to_vec(&v).unwrap()).unwrap();
|
||||
let views = &back.views;
|
||||
assert_eq!(views.len(), SavedView::MAX);
|
||||
assert_eq!(views[0].name, "x".repeat(SavedView::MAX_NAME));
|
||||
assert_eq!((views[1].name.as_str(), views[1].zoom, views[1].rotation), ("Dup", 3.0, 10.0), "the first of a name stays");
|
||||
assert_eq!(views[2].name, "V0", "blank names, a zero or negative zoom and a repeated name go");
|
||||
assert!(views.iter().all(SavedView::is_usable));
|
||||
// What loads saves and loads again unchanged.
|
||||
assert_eq!(&load(&save(&back, false)).unwrap().views, views);
|
||||
// Junk in the member is a format error, not a crash.
|
||||
v["document"]["views"] = json!([{"name": 5, "center": "x", "zoom": "big"}]);
|
||||
assert!(load(&serde_json::to_vec(&v).unwrap()).is_err());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//! View → Edit Views…: the document's saved views 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
|
||||
//! (`view.saved.edit` `edits`); Cancel leaves them as they were.
|
||||
//!
|
||||
//! Fields: `count` and, for each saved view `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::SavedView;
|
||||
|
||||
use super::{DialogSpec, form};
|
||||
use crate::state::Dialog;
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
|
||||
/// The dialog kind of Edit Views.
|
||||
pub const KIND: &str = "editViews";
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Edit Views").into(), body, confirm, min_width: 420.0, ..DialogSpec::FORM };
|
||||
|
||||
/// Open Edit Views on the active document's saved views.
|
||||
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
let names: Vec<String> = app.session.active().map(|d| d.doc.views.iter().map(|v| v.name.clone()).collect()).unwrap_or_default();
|
||||
if names.is_empty() {
|
||||
return Err("No saved views (View → New View…)".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(SavedView::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 Views"));
|
||||
widgets::list_box(ui, |ui| {
|
||||
egui::ScrollArea::vertical().id_salt("saved-views").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 view, 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 views would share keeps the dialog open, to be changed.
|
||||
let r = if edits.is_empty() { Ok(Value::Null) } else { app.run("view.saved.edit", 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();
|
||||
for n in ["Overview", "Logo", "Text"] {
|
||||
app.run("view.saved.new", 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: &VectorcraftApp) -> Vec<String> {
|
||||
app.session.active().unwrap().doc.views.iter().map(|v| v.name.clone()).collect()
|
||||
}
|
||||
|
||||
#[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!(" Whole page "));
|
||||
set(&mut app, "deleted2", json!(true));
|
||||
assert_eq!(names(&app), ["Overview", "Logo", "Text"]);
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(names(&app), ["Whole page", "Logo"]);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(names(&app), ["Overview", "Logo", "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(&app), ["Overview", "Logo", "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(&app), ["Overview", "Logo", "Text"]);
|
||||
// Deleting the other one frees the name.
|
||||
set(&mut app, "deleted2", json!(true));
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!(names(&app), ["Text", "Logo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_needs_a_saved_view() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ mod document_setup;
|
||||
pub mod dxf_import;
|
||||
pub mod dxf_options;
|
||||
pub mod edit_selection;
|
||||
pub mod edit_views;
|
||||
mod effect;
|
||||
pub mod effect_gallery;
|
||||
pub mod envelope;
|
||||
@@ -367,6 +368,7 @@ registry! {
|
||||
BrushOptions: [brush_options::KIND] => brush_options::SPEC,
|
||||
NewBrush: [new_brush::KIND] => new_brush::SPEC,
|
||||
EditSelection: [edit_selection::KIND] => edit_selection::SPEC,
|
||||
EditViews: [edit_views::KIND] => edit_views::SPEC,
|
||||
PerspectivePlane: [perspective_plane::KIND] => perspective_plane::SPEC,
|
||||
LayerOptions: [layer_options::KIND] => layer_options::SPEC,
|
||||
LayersPanelOptions: [layers_panel_options::KIND] => layers_panel_options::SPEC,
|
||||
|
||||
@@ -2744,7 +2744,22 @@ font-technology Variable Променливи
|
||||
Saved to User Defined: {} Запазено в потребителски: {}
|
||||
Saved View 1 Запазен изглед 1
|
||||
Saved View 10 Запазен изглед 10
|
||||
Saved View 11 Запазен изглед 11
|
||||
Saved View 12 Запазен изглед 12
|
||||
Saved View 13 Запазен изглед 13
|
||||
Saved View 14 Запазен изглед 14
|
||||
Saved View 15 Запазен изглед 15
|
||||
Saved View 16 Запазен изглед 16
|
||||
Saved View 17 Запазен изглед 17
|
||||
Saved View 18 Запазен изглед 18
|
||||
Saved View 19 Запазен изглед 19
|
||||
Saved View 2 Запазен изглед 2
|
||||
Saved View 20 Запазен изглед 20
|
||||
Saved View 21 Запазен изглед 21
|
||||
Saved View 22 Запазен изглед 22
|
||||
Saved View 23 Запазен изглед 23
|
||||
Saved View 24 Запазен изглед 24
|
||||
Saved View 25 Запазен изглед 25
|
||||
Saved View 3 Запазен изглед 3
|
||||
Saved View 4 Запазен изглед 4
|
||||
Saved View 5 Запазен изглед 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -599,6 +599,21 @@
|
||||
Saved View 8 Uložené zobrazení 8
|
||||
Saved View 9 Uložené zobrazení 9
|
||||
Saved View 10 Uložené zobrazení 10
|
||||
Saved View 11 Uložené zobrazení 11
|
||||
Saved View 12 Uložené zobrazení 12
|
||||
Saved View 13 Uložené zobrazení 13
|
||||
Saved View 14 Uložené zobrazení 14
|
||||
Saved View 15 Uložené zobrazení 15
|
||||
Saved View 16 Uložené zobrazení 16
|
||||
Saved View 17 Uložené zobrazení 17
|
||||
Saved View 18 Uložené zobrazení 18
|
||||
Saved View 19 Uložené zobrazení 19
|
||||
Saved View 20 Uložené zobrazení 20
|
||||
Saved View 21 Uložené zobrazení 21
|
||||
Saved View 22 Uložené zobrazení 22
|
||||
Saved View 23 Uložené zobrazení 23
|
||||
Saved View 24 Uložené zobrazení 24
|
||||
Saved View 25 Uložené zobrazení 25
|
||||
Window Okno
|
||||
New Window Nové okno
|
||||
Cascade Do kaskády
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -552,6 +552,21 @@
|
||||
Saved View 8 Gemt visning 8
|
||||
Saved View 9 Gemt visning 9
|
||||
Saved View 10 Gemt visning 10
|
||||
Saved View 11 Gemt visning 11
|
||||
Saved View 12 Gemt visning 12
|
||||
Saved View 13 Gemt visning 13
|
||||
Saved View 14 Gemt visning 14
|
||||
Saved View 15 Gemt visning 15
|
||||
Saved View 16 Gemt visning 16
|
||||
Saved View 17 Gemt visning 17
|
||||
Saved View 18 Gemt visning 18
|
||||
Saved View 19 Gemt visning 19
|
||||
Saved View 20 Gemt visning 20
|
||||
Saved View 21 Gemt visning 21
|
||||
Saved View 22 Gemt visning 22
|
||||
Saved View 23 Gemt visning 23
|
||||
Saved View 24 Gemt visning 24
|
||||
Saved View 25 Gemt visning 25
|
||||
Window Vindue
|
||||
New Window Nyt vindue
|
||||
Cascade Kaskade
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2768,7 +2768,22 @@ mark-style Japanese Japanisch
|
||||
Saved to User Defined: {} In „Benutzerdefiniert“ gespeichert: {}
|
||||
Saved View 1 Gespeicherte Ansicht 1
|
||||
Saved View 10 Gespeicherte Ansicht 10
|
||||
Saved View 11 Gespeicherte Ansicht 11
|
||||
Saved View 12 Gespeicherte Ansicht 12
|
||||
Saved View 13 Gespeicherte Ansicht 13
|
||||
Saved View 14 Gespeicherte Ansicht 14
|
||||
Saved View 15 Gespeicherte Ansicht 15
|
||||
Saved View 16 Gespeicherte Ansicht 16
|
||||
Saved View 17 Gespeicherte Ansicht 17
|
||||
Saved View 18 Gespeicherte Ansicht 18
|
||||
Saved View 19 Gespeicherte Ansicht 19
|
||||
Saved View 2 Gespeicherte Ansicht 2
|
||||
Saved View 20 Gespeicherte Ansicht 20
|
||||
Saved View 21 Gespeicherte Ansicht 21
|
||||
Saved View 22 Gespeicherte Ansicht 22
|
||||
Saved View 23 Gespeicherte Ansicht 23
|
||||
Saved View 24 Gespeicherte Ansicht 24
|
||||
Saved View 25 Gespeicherte Ansicht 25
|
||||
Saved View 3 Gespeicherte Ansicht 3
|
||||
Saved View 4 Gespeicherte Ansicht 4
|
||||
Saved View 5 Gespeicherte Ansicht 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5066,6 +5066,21 @@ effect Points Σημεία
|
||||
Saved View 8 Αποθηκευμένη προβολή 8
|
||||
Saved View 9 Αποθηκευμένη προβολή 9
|
||||
Saved View 10 Αποθηκευμένη προβολή 10
|
||||
Saved View 11 Αποθηκευμένη προβολή 11
|
||||
Saved View 12 Αποθηκευμένη προβολή 12
|
||||
Saved View 13 Αποθηκευμένη προβολή 13
|
||||
Saved View 14 Αποθηκευμένη προβολή 14
|
||||
Saved View 15 Αποθηκευμένη προβολή 15
|
||||
Saved View 16 Αποθηκευμένη προβολή 16
|
||||
Saved View 17 Αποθηκευμένη προβολή 17
|
||||
Saved View 18 Αποθηκευμένη προβολή 18
|
||||
Saved View 19 Αποθηκευμένη προβολή 19
|
||||
Saved View 20 Αποθηκευμένη προβολή 20
|
||||
Saved View 21 Αποθηκευμένη προβολή 21
|
||||
Saved View 22 Αποθηκευμένη προβολή 22
|
||||
Saved View 23 Αποθηκευμένη προβολή 23
|
||||
Saved View 24 Αποθηκευμένη προβολή 24
|
||||
Saved View 25 Αποθηκευμένη προβολή 25
|
||||
Three Point Perspective Preset 1 Προεπιλογή προοπτικής τριών σημείων 1
|
||||
Three Point Perspective Preset 2 Προεπιλογή προοπτικής τριών σημείων 2
|
||||
Three Point Perspective Preset 3 Προεπιλογή προοπτικής τριών σημείων 3
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2069 and column 19.
|
@@ -2799,7 +2799,22 @@ font-technology Variable Variables
|
||||
Saved to User Defined: {} Guardado en Definido por el usuario: {}
|
||||
Saved View 1 Vista guardada 1
|
||||
Saved View 10 Vista guardada 10
|
||||
Saved View 11 Vista guardada 11
|
||||
Saved View 12 Vista guardada 12
|
||||
Saved View 13 Vista guardada 13
|
||||
Saved View 14 Vista guardada 14
|
||||
Saved View 15 Vista guardada 15
|
||||
Saved View 16 Vista guardada 16
|
||||
Saved View 17 Vista guardada 17
|
||||
Saved View 18 Vista guardada 18
|
||||
Saved View 19 Vista guardada 19
|
||||
Saved View 2 Vista guardada 2
|
||||
Saved View 20 Vista guardada 20
|
||||
Saved View 21 Vista guardada 21
|
||||
Saved View 22 Vista guardada 22
|
||||
Saved View 23 Vista guardada 23
|
||||
Saved View 24 Vista guardada 24
|
||||
Saved View 25 Vista guardada 25
|
||||
Saved View 3 Vista guardada 3
|
||||
Saved View 4 Vista guardada 4
|
||||
Saved View 5 Vista guardada 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -590,6 +590,21 @@
|
||||
Saved View 8 Tallennettu näkymä 8
|
||||
Saved View 9 Tallennettu näkymä 9
|
||||
Saved View 10 Tallennettu näkymä 10
|
||||
Saved View 11 Tallennettu näkymä 11
|
||||
Saved View 12 Tallennettu näkymä 12
|
||||
Saved View 13 Tallennettu näkymä 13
|
||||
Saved View 14 Tallennettu näkymä 14
|
||||
Saved View 15 Tallennettu näkymä 15
|
||||
Saved View 16 Tallennettu näkymä 16
|
||||
Saved View 17 Tallennettu näkymä 17
|
||||
Saved View 18 Tallennettu näkymä 18
|
||||
Saved View 19 Tallennettu näkymä 19
|
||||
Saved View 20 Tallennettu näkymä 20
|
||||
Saved View 21 Tallennettu näkymä 21
|
||||
Saved View 22 Tallennettu näkymä 22
|
||||
Saved View 23 Tallennettu näkymä 23
|
||||
Saved View 24 Tallennettu näkymä 24
|
||||
Saved View 25 Tallennettu näkymä 25
|
||||
Window Ikkuna
|
||||
New Window Uusi ikkuna
|
||||
Cascade Porrasta
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2796,7 +2796,22 @@ font-technology Variable Variables
|
||||
Saved to User Defined: {} Enregistré dans Défini par l’utilisateur : {}
|
||||
Saved View 1 Vue enregistrée 1
|
||||
Saved View 10 Vue enregistrée 10
|
||||
Saved View 11 Vue enregistrée 11
|
||||
Saved View 12 Vue enregistrée 12
|
||||
Saved View 13 Vue enregistrée 13
|
||||
Saved View 14 Vue enregistrée 14
|
||||
Saved View 15 Vue enregistrée 15
|
||||
Saved View 16 Vue enregistrée 16
|
||||
Saved View 17 Vue enregistrée 17
|
||||
Saved View 18 Vue enregistrée 18
|
||||
Saved View 19 Vue enregistrée 19
|
||||
Saved View 2 Vue enregistrée 2
|
||||
Saved View 20 Vue enregistrée 20
|
||||
Saved View 21 Vue enregistrée 21
|
||||
Saved View 22 Vue enregistrée 22
|
||||
Saved View 23 Vue enregistrée 23
|
||||
Saved View 24 Vue enregistrée 24
|
||||
Saved View 25 Vue enregistrée 25
|
||||
Saved View 3 Vue enregistrée 3
|
||||
Saved View 4 Vue enregistrée 4
|
||||
Saved View 5 Vue enregistrée 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3382,7 +3382,22 @@ mark-style Japanese Japán
|
||||
Saved to User Defined: {} Mentve a Felhasználói könyvtárak közé: {}
|
||||
Saved View 1 Mentett nézet 1
|
||||
Saved View 10 Mentett nézet 10
|
||||
Saved View 11 Mentett nézet 11
|
||||
Saved View 12 Mentett nézet 12
|
||||
Saved View 13 Mentett nézet 13
|
||||
Saved View 14 Mentett nézet 14
|
||||
Saved View 15 Mentett nézet 15
|
||||
Saved View 16 Mentett nézet 16
|
||||
Saved View 17 Mentett nézet 17
|
||||
Saved View 18 Mentett nézet 18
|
||||
Saved View 19 Mentett nézet 19
|
||||
Saved View 2 Mentett nézet 2
|
||||
Saved View 20 Mentett nézet 20
|
||||
Saved View 21 Mentett nézet 21
|
||||
Saved View 22 Mentett nézet 22
|
||||
Saved View 23 Mentett nézet 23
|
||||
Saved View 24 Mentett nézet 24
|
||||
Saved View 25 Mentett nézet 25
|
||||
Saved View 3 Mentett nézet 3
|
||||
Saved View 4 Mentett nézet 4
|
||||
Saved View 5 Mentett nézet 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -552,6 +552,21 @@
|
||||
Saved View 8 Tampilan tersimpan 8
|
||||
Saved View 9 Tampilan tersimpan 9
|
||||
Saved View 10 Tampilan tersimpan 10
|
||||
Saved View 11 Tampilan tersimpan 11
|
||||
Saved View 12 Tampilan tersimpan 12
|
||||
Saved View 13 Tampilan tersimpan 13
|
||||
Saved View 14 Tampilan tersimpan 14
|
||||
Saved View 15 Tampilan tersimpan 15
|
||||
Saved View 16 Tampilan tersimpan 16
|
||||
Saved View 17 Tampilan tersimpan 17
|
||||
Saved View 18 Tampilan tersimpan 18
|
||||
Saved View 19 Tampilan tersimpan 19
|
||||
Saved View 20 Tampilan tersimpan 20
|
||||
Saved View 21 Tampilan tersimpan 21
|
||||
Saved View 22 Tampilan tersimpan 22
|
||||
Saved View 23 Tampilan tersimpan 23
|
||||
Saved View 24 Tampilan tersimpan 24
|
||||
Saved View 25 Tampilan tersimpan 25
|
||||
Window Jendela
|
||||
New Window Jendela baru
|
||||
Cascade Bertingkat
|
||||
|
||||
|
@@ -2833,7 +2833,22 @@ mark-style Japanese Giapponese
|
||||
Saved to User Defined: {} Salvato in Definito dall'utente: {}
|
||||
Saved View 1 Vista salvata 1
|
||||
Saved View 10 Vista salvata 10
|
||||
Saved View 11 Vista salvata 11
|
||||
Saved View 12 Vista salvata 12
|
||||
Saved View 13 Vista salvata 13
|
||||
Saved View 14 Vista salvata 14
|
||||
Saved View 15 Vista salvata 15
|
||||
Saved View 16 Vista salvata 16
|
||||
Saved View 17 Vista salvata 17
|
||||
Saved View 18 Vista salvata 18
|
||||
Saved View 19 Vista salvata 19
|
||||
Saved View 2 Vista salvata 2
|
||||
Saved View 20 Vista salvata 20
|
||||
Saved View 21 Vista salvata 21
|
||||
Saved View 22 Vista salvata 22
|
||||
Saved View 23 Vista salvata 23
|
||||
Saved View 24 Vista salvata 24
|
||||
Saved View 25 Vista salvata 25
|
||||
Saved View 3 Vista salvata 3
|
||||
Saved View 4 Vista salvata 4
|
||||
Saved View 5 Vista salvata 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -582,6 +582,21 @@ font-technology Variable バリアブル
|
||||
Saved View 8 保存した表示 8
|
||||
Saved View 9 保存した表示 9
|
||||
Saved View 10 保存した表示 10
|
||||
Saved View 11 保存した表示 11
|
||||
Saved View 12 保存した表示 12
|
||||
Saved View 13 保存した表示 13
|
||||
Saved View 14 保存した表示 14
|
||||
Saved View 15 保存した表示 15
|
||||
Saved View 16 保存した表示 16
|
||||
Saved View 17 保存した表示 17
|
||||
Saved View 18 保存した表示 18
|
||||
Saved View 19 保存した表示 19
|
||||
Saved View 20 保存した表示 20
|
||||
Saved View 21 保存した表示 21
|
||||
Saved View 22 保存した表示 22
|
||||
Saved View 23 保存した表示 23
|
||||
Saved View 24 保存した表示 24
|
||||
Saved View 25 保存した表示 25
|
||||
Window ウィンドウ
|
||||
New Window 新規ウィンドウ
|
||||
Cascade 重ねて表示
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -504,6 +504,21 @@
|
||||
Saved View 8 შენახული ხედი 8
|
||||
Saved View 9 შენახული ხედი 9
|
||||
Saved View 10 შენახული ხედი 10
|
||||
Saved View 11 შენახული ხედი 11
|
||||
Saved View 12 შენახული ხედი 12
|
||||
Saved View 13 შენახული ხედი 13
|
||||
Saved View 14 შენახული ხედი 14
|
||||
Saved View 15 შენახული ხედი 15
|
||||
Saved View 16 შენახული ხედი 16
|
||||
Saved View 17 შენახული ხედი 17
|
||||
Saved View 18 შენახული ხედი 18
|
||||
Saved View 19 შენახული ხედი 19
|
||||
Saved View 20 შენახული ხედი 20
|
||||
Saved View 21 შენახული ხედი 21
|
||||
Saved View 22 შენახული ხედი 22
|
||||
Saved View 23 შენახული ხედი 23
|
||||
Saved View 24 შენახული ხედი 24
|
||||
Saved View 25 შენახული ხედი 25
|
||||
Window ფანჯარა
|
||||
New Window ახალი ფანჯარა
|
||||
Cascade კასკადად
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -580,6 +580,21 @@ font-technology Variable 가변
|
||||
Saved View 8 저장된 보기 8
|
||||
Saved View 9 저장된 보기 9
|
||||
Saved View 10 저장된 보기 10
|
||||
Saved View 11 저장된 보기 11
|
||||
Saved View 12 저장된 보기 12
|
||||
Saved View 13 저장된 보기 13
|
||||
Saved View 14 저장된 보기 14
|
||||
Saved View 15 저장된 보기 15
|
||||
Saved View 16 저장된 보기 16
|
||||
Saved View 17 저장된 보기 17
|
||||
Saved View 18 저장된 보기 18
|
||||
Saved View 19 저장된 보기 19
|
||||
Saved View 20 저장된 보기 20
|
||||
Saved View 21 저장된 보기 21
|
||||
Saved View 22 저장된 보기 22
|
||||
Saved View 23 저장된 보기 23
|
||||
Saved View 24 저장된 보기 24
|
||||
Saved View 25 저장된 보기 25
|
||||
Window 윈도우
|
||||
New Window 새 창
|
||||
Cascade 계단식 배열
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -566,6 +566,21 @@
|
||||
Saved View 8 Opgeslagen weergave 8
|
||||
Saved View 9 Opgeslagen weergave 9
|
||||
Saved View 10 Opgeslagen weergave 10
|
||||
Saved View 11 Opgeslagen weergave 11
|
||||
Saved View 12 Opgeslagen weergave 12
|
||||
Saved View 13 Opgeslagen weergave 13
|
||||
Saved View 14 Opgeslagen weergave 14
|
||||
Saved View 15 Opgeslagen weergave 15
|
||||
Saved View 16 Opgeslagen weergave 16
|
||||
Saved View 17 Opgeslagen weergave 17
|
||||
Saved View 18 Opgeslagen weergave 18
|
||||
Saved View 19 Opgeslagen weergave 19
|
||||
Saved View 20 Opgeslagen weergave 20
|
||||
Saved View 21 Opgeslagen weergave 21
|
||||
Saved View 22 Opgeslagen weergave 22
|
||||
Saved View 23 Opgeslagen weergave 23
|
||||
Saved View 24 Opgeslagen weergave 24
|
||||
Saved View 25 Opgeslagen weergave 25
|
||||
Window Venster
|
||||
New Window Nieuw venster
|
||||
Cascade Trapsgewijs schikken
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -557,6 +557,21 @@
|
||||
Saved View 8 Zapisany widok 8
|
||||
Saved View 9 Zapisany widok 9
|
||||
Saved View 10 Zapisany widok 10
|
||||
Saved View 11 Zapisany widok 11
|
||||
Saved View 12 Zapisany widok 12
|
||||
Saved View 13 Zapisany widok 13
|
||||
Saved View 14 Zapisany widok 14
|
||||
Saved View 15 Zapisany widok 15
|
||||
Saved View 16 Zapisany widok 16
|
||||
Saved View 17 Zapisany widok 17
|
||||
Saved View 18 Zapisany widok 18
|
||||
Saved View 19 Zapisany widok 19
|
||||
Saved View 20 Zapisany widok 20
|
||||
Saved View 21 Zapisany widok 21
|
||||
Saved View 22 Zapisany widok 22
|
||||
Saved View 23 Zapisany widok 23
|
||||
Saved View 24 Zapisany widok 24
|
||||
Saved View 25 Zapisany widok 25
|
||||
Window Okno
|
||||
New Window Nowe okno
|
||||
Cascade Kaskadowo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2322,7 +2322,22 @@
|
||||
Saved Selections Seleções Salvas
|
||||
Saved View 1 Visualização Salva 1
|
||||
Saved View 10 Visualização Salva 10
|
||||
Saved View 11 Visualização Salva 11
|
||||
Saved View 12 Visualização Salva 12
|
||||
Saved View 13 Visualização Salva 13
|
||||
Saved View 14 Visualização Salva 14
|
||||
Saved View 15 Visualização Salva 15
|
||||
Saved View 16 Visualização Salva 16
|
||||
Saved View 17 Visualização Salva 17
|
||||
Saved View 18 Visualização Salva 18
|
||||
Saved View 19 Visualização Salva 19
|
||||
Saved View 2 Visualização Salva 2
|
||||
Saved View 20 Visualização Salva 20
|
||||
Saved View 21 Visualização Salva 21
|
||||
Saved View 22 Visualização Salva 22
|
||||
Saved View 23 Visualização Salva 23
|
||||
Saved View 24 Visualização Salva 24
|
||||
Saved View 25 Visualização Salva 25
|
||||
Saved View 3 Visualização Salva 3
|
||||
Saved View 4 Visualização Salva 4
|
||||
Saved View 5 Visualização Salva 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2829,7 +2829,22 @@ mark-style Japanese Японский
|
||||
Saved to User Defined: {} Сохранено в «Пользовательский»: {}
|
||||
Saved View 1 Сохранённый вид 1
|
||||
Saved View 10 Сохранённый вид 10
|
||||
Saved View 11 Сохранённый вид 11
|
||||
Saved View 12 Сохранённый вид 12
|
||||
Saved View 13 Сохранённый вид 13
|
||||
Saved View 14 Сохранённый вид 14
|
||||
Saved View 15 Сохранённый вид 15
|
||||
Saved View 16 Сохранённый вид 16
|
||||
Saved View 17 Сохранённый вид 17
|
||||
Saved View 18 Сохранённый вид 18
|
||||
Saved View 19 Сохранённый вид 19
|
||||
Saved View 2 Сохранённый вид 2
|
||||
Saved View 20 Сохранённый вид 20
|
||||
Saved View 21 Сохранённый вид 21
|
||||
Saved View 22 Сохранённый вид 22
|
||||
Saved View 23 Сохранённый вид 23
|
||||
Saved View 24 Сохранённый вид 24
|
||||
Saved View 25 Сохранённый вид 25
|
||||
Saved View 3 Сохранённый вид 3
|
||||
Saved View 4 Сохранённый вид 4
|
||||
Saved View 5 Сохранённый вид 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2744,7 +2744,22 @@ font-technology Variable Variabla
|
||||
Saved to User Defined: {} Sparat i Användardefinierat: {}
|
||||
Saved View 1 Sparad vy 1
|
||||
Saved View 10 Sparad vy 10
|
||||
Saved View 11 Sparad vy 11
|
||||
Saved View 12 Sparad vy 12
|
||||
Saved View 13 Sparad vy 13
|
||||
Saved View 14 Sparad vy 14
|
||||
Saved View 15 Sparad vy 15
|
||||
Saved View 16 Sparad vy 16
|
||||
Saved View 17 Sparad vy 17
|
||||
Saved View 18 Sparad vy 18
|
||||
Saved View 19 Sparad vy 19
|
||||
Saved View 2 Sparad vy 2
|
||||
Saved View 20 Sparad vy 20
|
||||
Saved View 21 Sparad vy 21
|
||||
Saved View 22 Sparad vy 22
|
||||
Saved View 23 Sparad vy 23
|
||||
Saved View 24 Sparad vy 24
|
||||
Saved View 25 Sparad vy 25
|
||||
Saved View 3 Sparad vy 3
|
||||
Saved View 4 Sparad vy 4
|
||||
Saved View 5 Sparad vy 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2614,7 +2614,22 @@ font-technology Variable Değişken
|
||||
Saved to User Defined: {} Kullanıcı Tanımlı'ya kaydedildi: {}
|
||||
Saved View 1 Kaydedilen Görünüm 1
|
||||
Saved View 10 Kaydedilen Görünüm 10
|
||||
Saved View 11 Kaydedilen Görünüm 11
|
||||
Saved View 12 Kaydedilen Görünüm 12
|
||||
Saved View 13 Kaydedilen Görünüm 13
|
||||
Saved View 14 Kaydedilen Görünüm 14
|
||||
Saved View 15 Kaydedilen Görünüm 15
|
||||
Saved View 16 Kaydedilen Görünüm 16
|
||||
Saved View 17 Kaydedilen Görünüm 17
|
||||
Saved View 18 Kaydedilen Görünüm 18
|
||||
Saved View 19 Kaydedilen Görünüm 19
|
||||
Saved View 2 Kaydedilen Görünüm 2
|
||||
Saved View 20 Kaydedilen Görünüm 20
|
||||
Saved View 21 Kaydedilen Görünüm 21
|
||||
Saved View 22 Kaydedilen Görünüm 22
|
||||
Saved View 23 Kaydedilen Görünüm 23
|
||||
Saved View 24 Kaydedilen Görünüm 24
|
||||
Saved View 25 Kaydedilen Görünüm 25
|
||||
Saved View 3 Kaydedilen Görünüm 3
|
||||
Saved View 4 Kaydedilen Görünüm 4
|
||||
Saved View 5 Kaydedilen Görünüm 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2825,7 +2825,22 @@ mark-style Japanese Японський
|
||||
Saved to User Defined: {} Збережено в «Користувач»: {}
|
||||
Saved View 1 Збережений вигляд 1
|
||||
Saved View 10 Збережений вигляд 10
|
||||
Saved View 11 Збережений вигляд 11
|
||||
Saved View 12 Збережений вигляд 12
|
||||
Saved View 13 Збережений вигляд 13
|
||||
Saved View 14 Збережений вигляд 14
|
||||
Saved View 15 Збережений вигляд 15
|
||||
Saved View 16 Збережений вигляд 16
|
||||
Saved View 17 Збережений вигляд 17
|
||||
Saved View 18 Збережений вигляд 18
|
||||
Saved View 19 Збережений вигляд 19
|
||||
Saved View 2 Збережений вигляд 2
|
||||
Saved View 20 Збережений вигляд 20
|
||||
Saved View 21 Збережений вигляд 21
|
||||
Saved View 22 Збережений вигляд 22
|
||||
Saved View 23 Збережений вигляд 23
|
||||
Saved View 24 Збережений вигляд 24
|
||||
Saved View 25 Збережений вигляд 25
|
||||
Saved View 3 Збережений вигляд 3
|
||||
Saved View 4 Збережений вигляд 4
|
||||
Saved View 5 Збережений вигляд 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 748 and column 40.
|
@@ -3136,7 +3136,22 @@ mark-style Japanese Kiểu Nhật
|
||||
Saved to User Defined: {} Đã lưu vào Tùy chỉnh người dùng: {}
|
||||
Saved View 1 Chế độ xem đã lưu 1
|
||||
Saved View 10 Chế độ xem đã lưu 10
|
||||
Saved View 11 Chế độ xem đã lưu 11
|
||||
Saved View 12 Chế độ xem đã lưu 12
|
||||
Saved View 13 Chế độ xem đã lưu 13
|
||||
Saved View 14 Chế độ xem đã lưu 14
|
||||
Saved View 15 Chế độ xem đã lưu 15
|
||||
Saved View 16 Chế độ xem đã lưu 16
|
||||
Saved View 17 Chế độ xem đã lưu 17
|
||||
Saved View 18 Chế độ xem đã lưu 18
|
||||
Saved View 19 Chế độ xem đã lưu 19
|
||||
Saved View 2 Chế độ xem đã lưu 2
|
||||
Saved View 20 Chế độ xem đã lưu 20
|
||||
Saved View 21 Chế độ xem đã lưu 21
|
||||
Saved View 22 Chế độ xem đã lưu 22
|
||||
Saved View 23 Chế độ xem đã lưu 23
|
||||
Saved View 24 Chế độ xem đã lưu 24
|
||||
Saved View 25 Chế độ xem đã lưu 25
|
||||
Saved View 3 Chế độ xem đã lưu 3
|
||||
Saved View 4 Chế độ xem đã lưu 4
|
||||
Saved View 5 Chế độ xem đã lưu 5
|
||||
|
||||
|
@@ -2672,7 +2672,22 @@ font-script Japanese 汉字
|
||||
Saved to User Defined: {} 已存储至用户自定义:{}
|
||||
Saved View 1 已存视图 1
|
||||
Saved View 10 已存视图 10
|
||||
Saved View 11 已存视图 11
|
||||
Saved View 12 已存视图 12
|
||||
Saved View 13 已存视图 13
|
||||
Saved View 14 已存视图 14
|
||||
Saved View 15 已存视图 15
|
||||
Saved View 16 已存视图 16
|
||||
Saved View 17 已存视图 17
|
||||
Saved View 18 已存视图 18
|
||||
Saved View 19 已存视图 19
|
||||
Saved View 2 已存视图 2
|
||||
Saved View 20 已存视图 20
|
||||
Saved View 21 已存视图 21
|
||||
Saved View 22 已存视图 22
|
||||
Saved View 23 已存视图 23
|
||||
Saved View 24 已存视图 24
|
||||
Saved View 25 已存视图 25
|
||||
Saved View 3 已存视图 3
|
||||
Saved View 4 已存视图 4
|
||||
Saved View 5 已存视图 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3606,7 +3606,22 @@ font-script Japanese 漢字
|
||||
Saved to User Defined: {} 已儲存至使用者自訂:{}
|
||||
Saved View 1 已儲存的檢視 1
|
||||
Saved View 10 已儲存的檢視 10
|
||||
Saved View 11 已儲存的檢視 11
|
||||
Saved View 12 已儲存的檢視 12
|
||||
Saved View 13 已儲存的檢視 13
|
||||
Saved View 14 已儲存的檢視 14
|
||||
Saved View 15 已儲存的檢視 15
|
||||
Saved View 16 已儲存的檢視 16
|
||||
Saved View 17 已儲存的檢視 17
|
||||
Saved View 18 已儲存的檢視 18
|
||||
Saved View 19 已儲存的檢視 19
|
||||
Saved View 2 已儲存的檢視 2
|
||||
Saved View 20 已儲存的檢視 20
|
||||
Saved View 21 已儲存的檢視 21
|
||||
Saved View 22 已儲存的檢視 22
|
||||
Saved View 23 已儲存的檢視 23
|
||||
Saved View 24 已儲存的檢視 24
|
||||
Saved View 25 已儲存的檢視 25
|
||||
Saved View 3 已儲存的檢視 3
|
||||
Saved View 4 已儲存的檢視 4
|
||||
Saved View 5 已儲存的檢視 5
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
+133
-40
@@ -91,6 +91,21 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
("view.goto8", "Saved View 8", "", "{} go to the 8. saved view"),
|
||||
("view.goto9", "Saved View 9", "", "{} go to the 9. saved view"),
|
||||
("view.goto10", "Saved View 10", "", "{} go to the 10. saved view"),
|
||||
("view.goto11", "Saved View 11", "", "{} go to the 11. saved view"),
|
||||
("view.goto12", "Saved View 12", "", "{} go to the 12. saved view"),
|
||||
("view.goto13", "Saved View 13", "", "{} go to the 13. saved view"),
|
||||
("view.goto14", "Saved View 14", "", "{} go to the 14. saved view"),
|
||||
("view.goto15", "Saved View 15", "", "{} go to the 15. saved view"),
|
||||
("view.goto16", "Saved View 16", "", "{} go to the 16. saved view"),
|
||||
("view.goto17", "Saved View 17", "", "{} go to the 17. saved view"),
|
||||
("view.goto18", "Saved View 18", "", "{} go to the 18. saved view"),
|
||||
("view.goto19", "Saved View 19", "", "{} go to the 19. saved view"),
|
||||
("view.goto20", "Saved View 20", "", "{} go to the 20. saved view"),
|
||||
("view.goto21", "Saved View 21", "", "{} go to the 21. saved view"),
|
||||
("view.goto22", "Saved View 22", "", "{} go to the 22. saved view"),
|
||||
("view.goto23", "Saved View 23", "", "{} go to the 23. saved view"),
|
||||
("view.goto24", "Saved View 24", "", "{} go to the 24. saved view"),
|
||||
("view.goto25", "Saved View 25", "", "{} go to the 25. saved view"),
|
||||
("select.recall1", "Saved Selection 1", "", "{} select the 1. saved selection"),
|
||||
("select.recall2", "Saved Selection 2", "", "{} select the 2. saved selection"),
|
||||
("select.recall3", "Saved Selection 3", "", "{} select the 3. saved selection"),
|
||||
@@ -1415,15 +1430,13 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
params["rotation"] = json!(v.rotation);
|
||||
app.session.execute(id, ¶ms).map_err(|e| e.to_string())
|
||||
}
|
||||
id if id.starts_with("view.goto") => {
|
||||
let n: usize = id["view.goto".len()..].parse().unwrap_or(0);
|
||||
let saved = app.session.active().and_then(|d| n.checked_sub(1).and_then(|i| d.doc.views.get(i)).cloned());
|
||||
// A saved view: its zoom, centre and rotation, in the current window.
|
||||
id if saved_view_slot(id).is_some() => {
|
||||
let saved = saved_view(app, id).and_then(|sv| crate::state::View::saved(sv).map(|to| (sv.name.clone(), to)));
|
||||
match (saved, app.view_mut()) {
|
||||
(Some(sv), Some(v)) => {
|
||||
v.center = sv.center;
|
||||
v.zoom = sv.zoom;
|
||||
v.rotation = sv.rotation;
|
||||
Ok(json!({ "name": sv.name }))
|
||||
(Some((name, to)), Some(v)) => {
|
||||
(v.center, v.zoom, v.rotation) = (to.center, to.zoom, to.rotation);
|
||||
Ok(json!({ "name": name }))
|
||||
}
|
||||
_ => Err("no such view".into()),
|
||||
}
|
||||
@@ -2092,10 +2105,7 @@ pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String {
|
||||
let n: usize = id["type.recentFont".len()..].parse().unwrap_or(0);
|
||||
n.checked_sub(1).and_then(|i| app.recent_fonts().get(i)).cloned().unwrap_or_else(|| "—".into())
|
||||
}
|
||||
id if id.starts_with("view.goto") => {
|
||||
let n: usize = id["view.goto".len()..].parse().unwrap_or(0);
|
||||
app.session.active().and_then(|d| n.checked_sub(1).and_then(|i| d.doc.views.get(i)).map(|v| v.name.clone())).unwrap_or_else(|| "—".into())
|
||||
}
|
||||
id if saved_view_slot(id).is_some() => saved_view(app, id).map(|v| v.name.clone()).unwrap_or_else(|| "—".into()),
|
||||
id if saved_selection_slot(id).is_some() => saved_selection_name(app, id).unwrap_or_else(|| "—".into()),
|
||||
"view.artboards" => if v.artboards { "Hide Artboards" } else { "Show Artboards" }.into(),
|
||||
"view.rulers" => if v.rulers { "Hide Rulers" } else { "Show Rulers" }.into(),
|
||||
@@ -2452,6 +2462,45 @@ const SAVED_SELECTION_IDS: [&str; vectorcraft_engine::doc::SavedSelection::MAX]
|
||||
"select.recall25",
|
||||
];
|
||||
|
||||
/// View → saved views: the n-th saved view of the active document.
|
||||
const SAVED_VIEW_IDS: [&str; vectorcraft_engine::doc::SavedView::MAX] = [
|
||||
"view.goto1",
|
||||
"view.goto2",
|
||||
"view.goto3",
|
||||
"view.goto4",
|
||||
"view.goto5",
|
||||
"view.goto6",
|
||||
"view.goto7",
|
||||
"view.goto8",
|
||||
"view.goto9",
|
||||
"view.goto10",
|
||||
"view.goto11",
|
||||
"view.goto12",
|
||||
"view.goto13",
|
||||
"view.goto14",
|
||||
"view.goto15",
|
||||
"view.goto16",
|
||||
"view.goto17",
|
||||
"view.goto18",
|
||||
"view.goto19",
|
||||
"view.goto20",
|
||||
"view.goto21",
|
||||
"view.goto22",
|
||||
"view.goto23",
|
||||
"view.goto24",
|
||||
"view.goto25",
|
||||
];
|
||||
|
||||
/// The 1-based slot a `view.gotoN` id stands for.
|
||||
fn saved_view_slot(id: &str) -> Option<usize> {
|
||||
id.strip_prefix("view.goto")?.parse().ok()
|
||||
}
|
||||
|
||||
/// The saved view a `view.gotoN` slot names (none past the document's last).
|
||||
fn saved_view<'a>(app: &'a VectorcraftApp, id: &str) -> Option<&'a vectorcraft_engine::doc::SavedView> {
|
||||
app.session.active()?.doc.views.get(saved_view_slot(id)?.checked_sub(1)?)
|
||||
}
|
||||
|
||||
/// The 1-based slot a `select.recallN` id stands for.
|
||||
fn saved_selection_slot(id: &str) -> Option<usize> {
|
||||
id.strip_prefix("select.recall")?.parse().ok()
|
||||
@@ -2548,9 +2597,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
||||
id["type.recentFont".len()..].parse::<usize>().is_ok_and(|n| n >= 1 && n <= app.recent_fonts().len()) && app.session.active().is_some()
|
||||
}
|
||||
id if saved_selection_slot(id).is_some() => saved_selection_name(app, id).is_some(),
|
||||
id if id.starts_with("view.goto") => {
|
||||
id["view.goto".len()..].parse::<usize>().is_ok_and(|n| n >= 1 && app.session.active().is_some_and(|d| n <= d.doc.views.len()))
|
||||
}
|
||||
id if saved_view_slot(id).is_some() => saved_view(app, id).is_some(),
|
||||
"effect.dialog" | "ui.recolorDialog" | "ui.effectGalleryDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()),
|
||||
"ui.cropImage" => selected_image(app, |_| true),
|
||||
"effect.applyLast" | "effect.last" => app.last_effect.is_some() && app.session.active().is_some_and(|d| !d.selection.is_empty()),
|
||||
@@ -3099,9 +3146,8 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
v
|
||||
}),
|
||||
("Effect", effect_menu()),
|
||||
(
|
||||
"View",
|
||||
vec![
|
||||
("View", {
|
||||
let mut v = vec![
|
||||
c("Outline", "view.outline"),
|
||||
c("Overprint Preview", "view.overprintPreview"),
|
||||
c("Pixel Preview", "view.pixelPreview"),
|
||||
@@ -3200,18 +3246,10 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
c("New View…", "view.saved.new"),
|
||||
c("Edit Views…", "view.saved.edit"),
|
||||
Sep,
|
||||
c("Saved View 1", "view.goto1"),
|
||||
c("Saved View 2", "view.goto2"),
|
||||
c("Saved View 3", "view.goto3"),
|
||||
c("Saved View 4", "view.goto4"),
|
||||
c("Saved View 5", "view.goto5"),
|
||||
c("Saved View 6", "view.goto6"),
|
||||
c("Saved View 7", "view.goto7"),
|
||||
c("Saved View 8", "view.goto8"),
|
||||
c("Saved View 9", "view.goto9"),
|
||||
c("Saved View 10", "view.goto10"),
|
||||
],
|
||||
),
|
||||
];
|
||||
v.extend(SAVED_VIEW_IDS.iter().map(|id| c("Saved View", id)));
|
||||
v
|
||||
}),
|
||||
(
|
||||
"Window",
|
||||
vec![
|
||||
@@ -3840,20 +3878,16 @@ pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// New View… / Edit Views…: name dialogs.
|
||||
// New View…: a name dialog, starting from the first free "View N".
|
||||
if id == "view.saved.new" && p.as_object().is_none_or(|o| o.is_empty()) {
|
||||
let n = app.session.active().map_or(0, |d| d.doc.views.len()) + 1;
|
||||
let _ = app.run("ui.paramDialog", json!({"command": id, "label": "New View", "params": {"name": format!("View {n}")}}));
|
||||
let name = app.session.active().map(|d| vectorcraft_engine::doc::SavedView::default_name(&d.doc.views)).unwrap_or_default();
|
||||
let _ = app.run("ui.paramDialog", json!({"command": id, "label": "New View", "params": {"name": name}}));
|
||||
return;
|
||||
}
|
||||
// Edit Views…: the saved views in a list, to rename or delete.
|
||||
if id == "view.saved.edit" && p.as_object().is_none_or(|o| o.is_empty()) {
|
||||
let first = app.session.active().and_then(|d| d.doc.views.first().map(|v| v.name.clone()));
|
||||
match first {
|
||||
Some(name) => {
|
||||
let _ = app
|
||||
.run("ui.paramDialog", json!({"command": id, "label": "Edit Views", "params": {"name": name, "newName": "", "delete": false}}));
|
||||
}
|
||||
None => app.status("No saved views (View → New View…)"),
|
||||
if let Err(e) = crate::dialogs::edit_views::open(app) {
|
||||
app.status(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4685,6 +4719,65 @@ mod tests {
|
||||
assert!(!menu_entries(&app).iter().any(|e| e.command.as_deref() == Some("view.goto2")));
|
||||
}
|
||||
|
||||
/// A document's 25 views are all listed at the bottom of the View menu, by name, and the last
|
||||
/// one brings back its zoom, centre and rotation after the view changed.
|
||||
#[test]
|
||||
fn every_saved_view_is_listed_at_the_bottom_of_the_view_menu() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
for i in 1..=25 {
|
||||
{
|
||||
let v = app.view_mut().unwrap();
|
||||
(v.zoom, v.center, v.rotation) = (i as f64 / 4.0, vectorcraft_geom::Point::new(10.0 * i as f64, -5.0 * i as f64), i as f64);
|
||||
}
|
||||
invoke(&mut app, "view.saved.new", json!({}));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
}
|
||||
assert_eq!(app.session.active().unwrap().doc.views.len(), 25);
|
||||
let entries = menu_entries(&app);
|
||||
let at = |id: &str| entries.iter().position(|e| e.command.as_deref() == Some(id));
|
||||
let (edit, first, last) = (at("view.saved.edit").unwrap(), at("view.goto1").unwrap(), at("view.goto25").unwrap());
|
||||
assert!(edit < first && last == first + 24, "listed in order after Edit Views…");
|
||||
assert!(enabled(&app, "view.goto25"));
|
||||
assert_eq!(display_label(&app, "view.goto25", "Saved View"), "View 25");
|
||||
// The document's 26th view is refused, so there is never one the menu leaves out.
|
||||
invoke(&mut app, "view.saved.new", json!({}));
|
||||
assert!(crate::dialogs::confirm(&mut app).is_err());
|
||||
app.ui.dialog = None;
|
||||
{
|
||||
let v = app.view_mut().unwrap();
|
||||
(v.zoom, v.center, v.rotation) = (1.0, vectorcraft_geom::Point::new(0.0, 0.0), 0.0);
|
||||
}
|
||||
assert_eq!(app.run("view.goto25", json!({})).unwrap()["name"], "View 25");
|
||||
let v = app.view().unwrap();
|
||||
assert_eq!((v.zoom, v.center.x, v.center.y, v.rotation), (6.25, 250.0, -125.0, 25.0));
|
||||
}
|
||||
|
||||
/// Edit Views… is off without a saved view and opens its list on the document's views, the
|
||||
/// way Edit Selection… does.
|
||||
#[test]
|
||||
fn edit_views_opens_its_dialog_on_the_saved_views() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
assert!(!enabled(&app, "view.saved.edit"));
|
||||
invoke(&mut app, "view.saved.edit", json!({}));
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert!(app.ui.status.contains("No saved views"), "{}", app.ui.status);
|
||||
app.run("view.saved.new", json!({"name": "Detail"})).unwrap();
|
||||
app.run("view.saved.new", json!({"name": "Page"})).unwrap();
|
||||
assert!(enabled(&app, "view.saved.edit"));
|
||||
invoke(&mut app, "view.saved.edit", json!({}));
|
||||
let d = app.ui.dialog.clone().expect("Edit Views dialog");
|
||||
assert_eq!((d.kind.as_str(), d.str("orig1"), d.str("name1")), ("editViews", "Page".into(), "Page".into()));
|
||||
// Deleting a view takes its slot out of the menu.
|
||||
let mut d = d;
|
||||
d.fields.insert("deleted0".into(), json!(true));
|
||||
app.ui.dialog = Some(d);
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(dynamic_label(&app, "view.goto1", ""), "Page");
|
||||
assert!(shown_state(&app, "view.goto2", &Value::Null).is_none());
|
||||
}
|
||||
|
||||
fn app_with_a_rectangle() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
|
||||
+14
-10
@@ -32,16 +32,20 @@ impl Default for View {
|
||||
impl View {
|
||||
/// The view a document opens at: the one it was saved with, else fitted on first display.
|
||||
pub fn of(st: &vectorcraft_engine::DocState) -> Self {
|
||||
match &st.view {
|
||||
Some(v) if v.zoom.is_finite() && v.zoom > 0.0 && v.center.x.is_finite() && v.center.y.is_finite() => Self {
|
||||
zoom: v.zoom.clamp(0.0313, 640.0),
|
||||
center: v.center,
|
||||
fitted: true,
|
||||
rotation: if v.rotation.is_finite() { v.rotation } else { 0.0 },
|
||||
artboard: 0,
|
||||
},
|
||||
_ => Self::default(),
|
||||
}
|
||||
st.view.as_ref().and_then(Self::saved).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The view `v` saved (the one a document was saved with, or a View → New View… one), within
|
||||
/// the zoom range; `None` when the canvas can't show it (a zoom or centre that isn't finite, a
|
||||
/// zoom of 0 or less).
|
||||
pub fn saved(v: &vectorcraft_engine::doc::SavedView) -> Option<Self> {
|
||||
v.is_usable().then(|| Self {
|
||||
zoom: v.zoom.clamp(0.0313, 640.0),
|
||||
center: v.center,
|
||||
fitted: true,
|
||||
rotation: if v.rotation.is_finite() { v.rotation } else { 0.0 },
|
||||
artboard: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-3
File diff suppressed because one or more lines are too long
+13
@@ -1385,6 +1385,19 @@ names must stay unique. In the desktop app the saved selections are listed at th
|
||||
(`select.recall {name}`). A file's saved selections are checked when it opens: past 25, blank or
|
||||
repeated names, and ids the document doesn't have are dropped.
|
||||
|
||||
## Saved views
|
||||
|
||||
View → New View… keeps a view under a name, in the document: `view.saved.new {name?, center: [x, y], zoom,
|
||||
rotation?}` (the desktop app fills in its current view; default the first free "View N"; at most 25 per document,
|
||||
names up to 255 characters, each name once) → `{name, index}`, one undo step. `view.saved.list` →
|
||||
`{views: [{name, center, zoom, rotation}…]}` in menu order. Edit Views… renames and deletes:
|
||||
`view.saved.edit {name, newName?, delete?}`, or several at once as `{edits: [{name, newName?, delete?}…]}` in one
|
||||
undo step (→ `{views}`), where every `name` is the name before the edit and the names must stay unique; it is
|
||||
enabled while the document has a saved view. In the desktop app the views are listed at the bottom of the View menu
|
||||
(`view.goto1` … `view.goto25`); choosing one sets the current window's zoom, centre and rotation. A file's views are
|
||||
checked when it opens: past 25, blank or repeated names, and views without a finite centre and a positive zoom are
|
||||
dropped.
|
||||
|
||||
## Ruler guides
|
||||
|
||||
Ruler guides are numbered in the order they were made. `guide.add {vertical, pos, artboard?}` makes one (the x of a
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user