Placed documents: Place's Link tooltip, Break Link in the Links panel
This commit is contained in:
parent
2418921760
commit
5e7af3f83c
File diff suppressed because one or more lines are too long
|
|
@ -3,7 +3,9 @@
|
|||
//! object) or loads the place cursor with several ([`crate::place::confirm`]).
|
||||
//!
|
||||
//! Fields: `files` (`[{path} | {name}]`), `link`, `template`, `replace`, and `__replace` (one file
|
||||
//! and one selected object: Replace applies), `__info` (a line per file).
|
||||
//! and one selected object: Replace applies), `__documents` and `__others` (VectorCraft documents
|
||||
//! from files are picked, other files are: what Link's tooltip explains), `__info` (a line per
|
||||
//! file).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -72,12 +74,32 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
|||
for (key, label, tip) in OPTIONS {
|
||||
let on = d.bool(key);
|
||||
let enabled = key != "replace" || d.bool("__replace");
|
||||
let resp = ui.scope(|ui| widgets::check(ui, tl!(label), on && enabled, enabled));
|
||||
if resp.inner {
|
||||
let tip = if key == "link" { link_tip(d) } else { tl!(tip).to_string() };
|
||||
if widgets::check_tip(ui, tl!(label), on && enabled, enabled, &tip) {
|
||||
d.fields.insert(key.into(), json!(!on));
|
||||
}
|
||||
resp.response.on_hover_text(tl!(tip));
|
||||
}
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
/// Link's tooltip: what it does for the files picked (images, VectorCraft documents, or both), a
|
||||
/// line each.
|
||||
pub(crate) fn link_tip(d: &Dialog) -> String {
|
||||
let lines = match (d.bool("__documents"), d.bool("__others")) {
|
||||
(true, false) => {
|
||||
vec![
|
||||
tl!("On: one locked object showing the document, updated when it changes."),
|
||||
tl!("Off: an editable copy of its art, embedded in this document."),
|
||||
]
|
||||
}
|
||||
(true, true) => {
|
||||
vec![
|
||||
tl!("Images: link to the file instead of only embedding it."),
|
||||
tl!("VectorCraft documents: one locked object, updated when the file changes."),
|
||||
]
|
||||
}
|
||||
_ => vec![tl!("Keep a link to an image file instead of only embedding it")],
|
||||
};
|
||||
lines.join("\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3593,3 +3593,8 @@ axis Both Ambos
|
|||
Zoom Zoom
|
||||
Bring All to Front Traer todo al frente
|
||||
Settings Ajustes
|
||||
Break Link Romper vínculo
|
||||
On: one locked object showing the document, updated when it changes. Activado: un objeto bloqueado que muestra el documento y se actualiza cuando este cambia.
|
||||
Off: an editable copy of its art, embedded in this document. Desactivado: una copia editable de su ilustración, incrustada en este documento.
|
||||
Images: link to the file instead of only embedding it. Imágenes: vincular al archivo en lugar de solo incrustarlo.
|
||||
VectorCraft documents: one locked object, updated when the file changes. Documentos de VectorCraft: un objeto bloqueado que se actualiza cuando cambia el archivo.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3593,3 +3593,8 @@ axis Both Entrambi
|
|||
Zoom Zoom
|
||||
Bring All to Front Porta tutto in primo piano
|
||||
Settings Impostazioni
|
||||
Break Link Interrompi collegamento
|
||||
On: one locked object showing the document, updated when it changes. Attivato: un unico oggetto bloccato che mostra il documento, aggiornato quando cambia.
|
||||
Off: an editable copy of its art, embedded in this document. Disattivato: una copia modificabile della sua grafica, incorporata in questo documento.
|
||||
Images: link to the file instead of only embedding it. Immagini: collega il file invece di incorporarlo soltanto.
|
||||
VectorCraft documents: one locked object, updated when the file changes. Documenti VectorCraft: un unico oggetto bloccato, aggiornato quando il file cambia.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3055,3 +3055,8 @@ axis Both 両方
|
|||
Bring All to Front すべてを手前に移動
|
||||
Settings 環境設定
|
||||
theme Appearance 外観
|
||||
Break Link リンクを解除
|
||||
On: one locked object showing the document, updated when it changes. オン:ドキュメントを表示するロックされた 1 つのオブジェクト。ドキュメントの変更時に更新されます。
|
||||
Off: an editable copy of its art, embedded in this document. オフ:アートの編集可能なコピーをこのドキュメントに埋め込みます。
|
||||
Images: link to the file instead of only embedding it. 画像:埋め込むだけでなく、ファイルにリンクします。
|
||||
VectorCraft documents: one locked object, updated when the file changes. VectorCraft ドキュメント:ロックされた 1 つのオブジェクト。ファイルの変更時に更新されます。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2643,3 +2643,8 @@
|
|||
Zoom Zoom
|
||||
Bring All to Front Trazer Todas para a Frente
|
||||
Settings Configurações
|
||||
Break Link Quebrar Vínculo
|
||||
On: one locked object showing the document, updated when it changes. Ativado: um objeto bloqueado que mostra o documento, atualizado quando ele muda.
|
||||
Off: an editable copy of its art, embedded in this document. Desativado: uma cópia editável da arte, incorporada neste documento.
|
||||
Images: link to the file instead of only embedding it. Imagens: vincular ao arquivo em vez de apenas incorporá-lo.
|
||||
VectorCraft documents: one locked object, updated when the file changes. Documentos do VectorCraft: um objeto bloqueado, atualizado quando o arquivo muda.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3310,3 +3310,8 @@ axis Both 两者
|
|||
Hide VectorCraft 隐藏 VectorCraft
|
||||
Bring All to Front 前置全部窗口
|
||||
Settings 设置
|
||||
Break Link 断开链接
|
||||
On: one locked object showing the document, updated when it changes. 开启:以一个锁定对象显示文档,文档更改时随之更新。
|
||||
Off: an editable copy of its art, embedded in this document. 关闭:将其图稿的可编辑副本嵌入此文档。
|
||||
Images: link to the file instead of only embedding it. 图像:链接到文件,而不只是嵌入。
|
||||
VectorCraft documents: one locked object, updated when the file changes. VectorCraft 文档:一个锁定对象,文件更改时随之更新。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3085,3 +3085,8 @@ axis Both 兩者
|
|||
Zoom 縮放
|
||||
Bring All to Front 將全部移至最前
|
||||
Settings 設定
|
||||
Break Link 中斷連結
|
||||
On: one locked object showing the document, updated when it changes. 開啟:以單一鎖定物件顯示文件,文件變更時隨之更新。
|
||||
Off: an editable copy of its art, embedded in this document. 關閉:將其圖稿的可編輯副本內嵌在此文件中。
|
||||
Images: link to the file instead of only embedding it. 影像:連結至檔案,而不只是嵌入。
|
||||
VectorCraft documents: one locked object, updated when the file changes. VectorCraft 文件:單一鎖定物件,檔案變更時隨之更新。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -560,7 +560,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
|||
"links.editOriginal",
|
||||
"Edit Original",
|
||||
"",
|
||||
"{id?} open the file of linked image `id` (default: the selected linked image) in the system's default app for its type (Links panel, Edit › Edit Original) → {path}; edits saved there show after links.update",
|
||||
"{id?} open the file of linked image `id` (default: the selected linked image) in the system's default app for its type (Links panel, Edit › Edit Original); a placed document's file opens here in a new tab, and saving it updates the documents placing it → {path}; edits saved elsewhere show after links.update",
|
||||
),
|
||||
(
|
||||
"links.reveal",
|
||||
|
|
@ -1837,8 +1837,8 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
|||
"ui.spotColors" => app.session.active().is_some(),
|
||||
"ui.menuDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()),
|
||||
"ui.dxfOptionsDialog" => app.session.active().is_some(),
|
||||
"links.editOriginal" | "links.reveal" => selected_image(app, |im| im.link.is_some()),
|
||||
"ui.placementOptionsDialog" => selected_image(app, |_| true),
|
||||
"links.editOriginal" | "links.reveal" => selected_image(app, |im| im.link.is_some()) || selected_placed(app),
|
||||
"ui.placementOptionsDialog" => selected_image(app, |_| true) || selected_placed(app),
|
||||
"ui.packageDialog" | "docInfo.save" => app.session.active().is_some(),
|
||||
"ui.epsOptionsDialog" => app.session.active().is_some(),
|
||||
"file.saveForWeb" | "file.saveForWeb.browser" => app.session.active().is_some(),
|
||||
|
|
@ -1855,6 +1855,13 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// Is a placed document selected?
|
||||
fn selected_placed(app: &VectorcraftApp) -> bool {
|
||||
app.session.active().is_some_and(|st| {
|
||||
st.selection.objects.iter().any(|id| st.doc.node(*id).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::PlacedDocument(_))))
|
||||
})
|
||||
}
|
||||
|
||||
/// Is an image `keep` accepts selected?
|
||||
fn selected_image(app: &VectorcraftApp, keep: impl Fn(&vectorcraft_doc::ImageObject) -> bool) -> bool {
|
||||
app.session.active().is_some_and(|st| {
|
||||
|
|
|
|||
|
|
@ -83,10 +83,12 @@ fn cached(app: &mut VectorcraftApp, ctx: &egui::Context, o: &Options) -> Option<
|
|||
Some(c)
|
||||
}
|
||||
|
||||
/// The ids of the selected image objects.
|
||||
/// The ids of the selected image objects and placed documents.
|
||||
fn selected_images(app: &VectorcraftApp) -> Vec<u64> {
|
||||
let Some(st) = app.session.active() else { return vec![] };
|
||||
let image = |id: &NodeId| st.doc.node(*id).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Image(_)));
|
||||
let image = |id: &NodeId| {
|
||||
st.doc.node(*id).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Image(_) | vectorcraft_doc::NodeKind::PlacedDocument(_)))
|
||||
};
|
||||
st.selection.objects.iter().filter(|id| image(id)).map(|id| id.0).collect()
|
||||
}
|
||||
|
||||
|
|
@ -170,6 +172,11 @@ pub(crate) fn open_file(app: &mut VectorcraftApp, p: &Value, reveal: bool) -> Re
|
|||
if info["status"] == "missing" {
|
||||
return Err(format!("{path} can't be found: relink it first"));
|
||||
}
|
||||
// A placed document's file opens here, in a new tab: saving it updates this document.
|
||||
if !reveal && info["document"] == true {
|
||||
app.run("document.open", json!({ "path": path }))?;
|
||||
return Ok(json!({ "path": path }));
|
||||
}
|
||||
if reveal { crate::io::reveal_path(app, &path) } else { crate::io::open_in_app(app, &path) }?;
|
||||
Ok(json!({ "path": path }))
|
||||
}
|
||||
|
|
@ -293,11 +300,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
None => {}
|
||||
}
|
||||
if o.info {
|
||||
link_info(ui, cache.info.as_ref().map(|(_, i)| i));
|
||||
let units = app.session.general_unit();
|
||||
link_info(ui, cache.info.as_ref().map(|(_, i)| i), units);
|
||||
}
|
||||
let sel = selected_rows(&cache.list, &selected);
|
||||
let modified = ids(&sel, |r| status_is(r, "modified") || r["preview"] == true && !status_is(r, "missing"));
|
||||
let can_edit = sel.iter().any(|r| r["linked"] == true && !status_is(r, "missing"));
|
||||
let linked = ids(&sel, |r| r["linked"] == true);
|
||||
widgets::bottom_bar(ui, |ui| {
|
||||
let info = !o.info;
|
||||
if widgets::icon_button(ui, if o.info { "chevron-down" } else { "chevron-right" }, tl!("Show Link Info"), false, 24.0).clicked() {
|
||||
|
|
@ -319,10 +328,24 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
if widgets::icon_button_enabled(ui, "link", tl!("Relink…"), false, !sel.is_empty() && can_pick(app), 24.0).clicked() {
|
||||
relink(app, ids(&sel, |_| true));
|
||||
}
|
||||
if widgets::icon_button_enabled(ui, "link-2-off", tl!("Break Link"), false, !linked.is_empty(), 24.0).clicked() {
|
||||
break_link(app, linked.clone());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Break Link (`links.embed`) of `ids`: images keep their files' pixels, placed documents become
|
||||
/// editable copies of their art.
|
||||
fn break_link(app: &mut VectorcraftApp, ids: Vec<u64>) {
|
||||
let r = app.run("links.embed", json!({ "ids": ids }));
|
||||
report(app, r, |v| {
|
||||
let missing = v["missing"].as_array().map_or(0, Vec::len);
|
||||
let n = v["embedded"].as_array().map_or(0, Vec::len);
|
||||
if missing == 0 { format!("Broke {n} link(s)") } else { format!("Broke {n} link(s); {missing} can't be read: relink or update them first") }
|
||||
});
|
||||
}
|
||||
|
||||
/// Pick-a-file actions need a file dialog (the desktop app).
|
||||
fn can_pick(app: &VectorcraftApp) -> bool {
|
||||
app.services.pick_open.is_some()
|
||||
|
|
@ -344,7 +367,7 @@ pub(crate) fn date_label(ms: u64) -> String {
|
|||
}
|
||||
|
||||
/// The Link Info section: the details of `info` (`links.info`), or a hint.
|
||||
fn link_info(ui: &mut Ui, info: Option<&Value>) {
|
||||
fn link_info(ui: &mut Ui, info: Option<&Value>, units: vectorcraft_doc::Unit) {
|
||||
widgets::divider(ui);
|
||||
let Some(i) = info else {
|
||||
widgets::dim_label(ui, tl!("Select an image to see its Link Info."));
|
||||
|
|
@ -365,9 +388,19 @@ fn link_info(ui: &mut Ui, info: Option<&Value>) {
|
|||
if let Some(l) = i["location"].as_str() {
|
||||
row(ui, tl!("Location"), l.into());
|
||||
}
|
||||
row(ui, tl!("PPI"), pair("ppi"));
|
||||
row(ui, tl!("Effective PPI"), pair("effectivePpi"));
|
||||
row(ui, tl!("Dimensions"), format!("{} × {} px", i["pixelWidth"], i["pixelHeight"]));
|
||||
// A placed document has no resolution: its rows are left out.
|
||||
for (label, k) in [(tl!("PPI"), "ppi"), (tl!("Effective PPI"), "effectivePpi")] {
|
||||
if i.get(k).is_some() {
|
||||
row(ui, label, pair(k));
|
||||
}
|
||||
}
|
||||
// Pixels, or a placed document's artboard size.
|
||||
let dims = match (i["pixelWidth"].as_u64(), i["pageWidth"].as_f64(), i["pageHeight"].as_f64()) {
|
||||
(Some(w), _, _) => format!("{w} × {} px", i["pixelHeight"].as_u64().unwrap_or(0)),
|
||||
(None, Some(w), Some(h)) => format!("{} × {}", units.format(w), units.format(h)),
|
||||
_ => String::new(),
|
||||
};
|
||||
row(ui, tl!("Dimensions"), dims);
|
||||
let scale: Vec<f64> = i["scale"].as_array().into_iter().flatten().filter_map(Value::as_f64).collect();
|
||||
if let [x, y] = scale[..] {
|
||||
row(ui, tl!("Scale"), format!("H: {x:.1}% V: {y:.1}%"));
|
||||
|
|
|
|||
|
|
@ -160,11 +160,20 @@ fn open_dialog(app: &mut VectorcraftApp, files: Vec<Value>) {
|
|||
"template": false,
|
||||
"replace": false,
|
||||
"__replace": files.len() == 1 && one_object,
|
||||
"__documents": files.iter().any(is_vectorcraft_file),
|
||||
"__others": !files.iter().all(is_vectorcraft_file),
|
||||
"__info": info,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
/// Is dialog file entry `file` a VectorCraft document on disk (what Link places as one locked
|
||||
/// object)?
|
||||
fn is_vectorcraft_file(file: &Value) -> bool {
|
||||
let native = ["vectorcraft", vectorcraft_format::LEGACY_EXTENSION, "vctemplate"];
|
||||
file.get("path").and_then(Value::as_str).is_some_and(|p| native.contains(&fileio::extension(p).as_str()))
|
||||
}
|
||||
|
||||
/// The engine params naming `file` (a dialog file entry).
|
||||
fn source(app: &VectorcraftApp, file: &Value) -> Option<Value> {
|
||||
if let Some(path) = file.get("path").and_then(Value::as_str) {
|
||||
|
|
|
|||
|
|
@ -361,3 +361,40 @@ fn the_control_bars_mask_clips_the_image_and_selects_the_clipping_path() {
|
|||
let ids: Vec<u64> = group.children().unwrap().iter().map(|c| c.id.0).collect();
|
||||
assert_eq!((ids[1], st.selection.objects.iter().map(|i| i.0).collect::<Vec<_>>()), (img, vec![ids[0]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_vectorcraft_document_places_linked_and_edit_original_opens_it() {
|
||||
let mut app = app();
|
||||
// A one-artboard document with a rectangle.
|
||||
let mut src = Session::new();
|
||||
src.execute("file.new", &json!({"width": 100, "height": 50})).unwrap();
|
||||
src.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 50})).unwrap();
|
||||
let bytes = vectorcraft_format::save_file(&src.active().unwrap().doc);
|
||||
let path = temp_file("badge.vectorcraft", &bytes);
|
||||
place_picked(&mut app, &path);
|
||||
let d = app.ui.dialog.as_ref().expect("the Place dialog");
|
||||
assert!(d.bool("link") && d.bool("__documents") && !d.bool("__others"), "Link on, for a document");
|
||||
// Link's tooltip says what it does for a document.
|
||||
assert!(crate::dialogs::place::link_tip(d).contains("editable copy"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let st = app.session.active().unwrap();
|
||||
let id = st.selection.objects[0];
|
||||
let vectorcraft_doc::NodeKind::PlacedDocument(p) = &st.doc.node(id).unwrap().kind else { panic!("not a placed document") };
|
||||
assert_eq!(p.link.path, path);
|
||||
// Edit Original opens the document in a new tab.
|
||||
let tabs = app.session.documents().len();
|
||||
app.run("links.editOriginal", json!({})).unwrap();
|
||||
assert_eq!(app.session.documents().len(), tabs + 1);
|
||||
assert_eq!(app.session.active().unwrap().path.as_deref(), Some(path.as_str()));
|
||||
// A PNG keeps the image tooltip; a PNG and a document, both.
|
||||
let pic = temp_file("pic.png", &png(10, 10, 72.0));
|
||||
place_picked(&mut app, &pic);
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert!(!d.bool("__documents") && crate::dialogs::place::link_tip(d).contains("image file"));
|
||||
app.ui.dialog = None;
|
||||
let both = vec![pic.clone(), path.clone()];
|
||||
app.services.pick_open_multi = Some(Box::new(move || both.clone()));
|
||||
app.run("file.place", json!({})).unwrap();
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert!(crate::dialogs::place::link_tip(d).contains("VectorCraft documents"));
|
||||
}
|
||||
|
|
|
|||
15
docs/mcp.md
15
docs/mcp.md
|
|
@ -1424,8 +1424,11 @@ passed straight back to `file.new`. Print presets and sizes without `units` star
|
|||
|
||||
`file.place` puts another file's art into the active document as one undo step without touching the clipboard:
|
||||
a raster image at 100% of its physical size (the resolution its file declares, else 72 ppi; linked to its `path`
|
||||
unless `link: false`), an SVG as one group, a PDF/.ai page or a native document's artboard (`page`, `crop`) as one
|
||||
clipped group, with the images, symbols, patterns and swatches it uses. `at` centres it, `rect` fits it, `replace`
|
||||
unless `link: false`), an SVG as one group, a PDF/.ai page as one clipped group, with the images, symbols, patterns
|
||||
and swatches it uses. A VectorCraft document read from `path` is a placed document: one locked object showing its
|
||||
artboard `page` (or, with `crop: "bounding"`, its art's bounds), linked to the file and read again when it changes
|
||||
(see Linked images), and vectors in every output; with `link: false` (or from `dataBase64`) it is an editable copy
|
||||
of its art, as one clipped group. `at` centres it, `rect` fits it, `replace`
|
||||
swaps the selected object (keeping its place and transform), `template` puts it on a new template layer.
|
||||
`file.place.info` describes a file without placing it and `image.info` reports a placed image's link, colour mode and
|
||||
effective ppi. `file.place.queue` loads the place cursor (the `place` tool) with several files: headless, drive it
|
||||
|
|
@ -1534,6 +1537,14 @@ in `updatedLinks`), each as `{name, path, ids}`. `links.check` reports every lin
|
|||
another file (or each at the file of its name in a folder); images keep their bounds, one undo step each. Without a
|
||||
file system (the web), linked images show their previews.
|
||||
|
||||
Placed documents (a `.vectorcraft` file placed linked) work the same way: the document keeps the file's bytes and a
|
||||
preview (JPEG or PNG, 1 px per point, at most 1024 px a side), and a save writes the preview (Include Linked Files:
|
||||
the file); output reads the file again, or uses the preview with a warning when it is gone or changed.
|
||||
`links.list` and `links.info` give them `document: true` and the artboard's `pageWidth` and `pageHeight` (pt).
|
||||
Saving the file in the app updates the open documents that place it. `links.embed` (the Links panel's Break Link)
|
||||
turns one into the editable copy placing without link gives, its symbols, patterns, swatches and images joining
|
||||
the document; so does `object.expand`.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"links.check","params":{}}}
|
||||
{"name":"run_command","arguments":{"command":"links.relink","params":{"ids":[12],"path":"/new/photo.png"}}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue