mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 11:47:21 -06:00
View: all 25 saved views in the View menu; Edit Views… lists them
A document keeps up to 25 saved views, but the View menu listed only the
first 10, so views 11 to 25 could be saved and never chosen. All 25 are
listed now, by name, and choosing one changes the current window to its
zoom, centre and rotation (within the canvas's zoom range).
Edit Views… used to open the generic parameter dialog on the first view,
where the name of the view to change had to be typed. It now opens a list
of the document's views, like Edit Selection…: pick one to rename it or
delete it; OK applies every change as one undo step and Cancel leaves the
views as they were. view.saved.edit takes {edits: [{name, newName?,
delete?}…]} for that, the one-view form still works, and the command is
off while the document has no saved view.
A file's views are untrusted: on open at most 25 are kept, names are
trimmed, capped at 255 characters and unique, and views without a finite
centre and a positive zoom are dropped. New View… starts from the first
free "View N" and caps names the same way.
Behaviour from the public help page "Use multiple windows and views":
https://helpx.adobe.com/illustrator/using/viewing-artwork.html
docs/gaps.md (G7) and docs/target-app-parity.md take New View/Edit Views
off the missing list; docs/mcp.md documents the view.saved commands; the
catalogs get the Saved View 11-25 rows in the pattern of 1-10.
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