mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
i18n: dialogs show user, file and system names as they are
Lists and headings that mix our labels with names no longer send the names through the catalog, so a preset, printer or plug-in called "Regular" or "Black" keeps its name in every language. - dialogs::shown_names / mixed_dropdown: translate only a list's built-in entries and show the rest with widgets::dropdown_names (same indices). - Artboard lists (Save for Office, Save for Web, save options) and plug-in choices: dropdown_names. - Mixed lists translate only their built-in entries: Recolor's Limit to Library, Save for Web, Print, Save PDF and Export for Screens presets, printers (Default Printer, PDF File), colour profiles and flattener presets (profile_pick, flatten::preset_dropdown), perspective presets, New Document's preset cards and Size, and the shortcut set. - Plug-in dialogs and plug-in effects are headed by the plug-in's name as it is; plug-in parameter labels are not translated. - The confirm dialog shows its message and detail as given, and the shared frame no longer translates headings again: each heading is translated by its spec. File > Revert translates its template before filling in the document's name. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f4181890bd
commit
cf7a43d786
18 files changed
+188
-59
No files matched your search
@@ -2,7 +2,8 @@
|
||||
//! as deleting swatches.
|
||||
//!
|
||||
//! Fields: `message` (the question, shown as the heading), `detail` (an optional line under it),
|
||||
//! `__command` and `__params` (the command OK runs).
|
||||
//! `__command` and `__params` (the command OK runs). Both texts are shown as given: callers
|
||||
//! translate their template before filling in a name, so the name is never translated.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -15,11 +16,11 @@ use crate::theme::Tokens;
|
||||
pub const KIND: &str = "confirm";
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
heading: |d| tl!(&d.str("message")).to_string(),
|
||||
heading: |d| d.str("message"),
|
||||
body: |_, ui, d| {
|
||||
let detail = d.str("detail");
|
||||
if !detail.is_empty() {
|
||||
ui.label(egui::RichText::new(tl!(&detail)).color(Tokens::get(ui.ctx()).text_dim));
|
||||
ui.label(egui::RichText::new(detail).color(Tokens::get(ui.ctx()).text_dim));
|
||||
}
|
||||
false
|
||||
},
|
||||
@@ -28,7 +29,8 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
..DialogSpec::FORM
|
||||
};
|
||||
|
||||
/// Ask `message` (with an optional `detail` line); OK runs `command` with `params`.
|
||||
/// Ask `message` (with an optional `detail` line), both in the UI language; OK runs `command`
|
||||
/// with `params`.
|
||||
pub fn ask(app: &mut VectorcraftApp, message: &str, detail: &str, command: &str, params: Value) {
|
||||
let fields = json!({"message": message, "detail": detail, "__command": command, "__params": params});
|
||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
|
||||
@@ -20,9 +20,12 @@ use crate::{VectorcraftApp, widgets};
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| heading_label(d), body, confirm, preview: true, ..DialogSpec::FORM };
|
||||
|
||||
/// The effect's name (`__label`, English) in the UI language: the catalog may carry it with or
|
||||
/// without its trailing ellipsis.
|
||||
/// without its trailing ellipsis. A plug-in effect's name is shown as it is.
|
||||
fn heading_label(d: &Dialog) -> String {
|
||||
let label = d.str("__label");
|
||||
if vectorcraft_plugins::effect::plugin_id(&d.str("__effect")).is_some() {
|
||||
return label;
|
||||
}
|
||||
let dotted = format!("{label}…");
|
||||
let shown = tl!(&dotted);
|
||||
if shown != dotted { shown.trim_end_matches('…').to_string() } else { tl!(&label).to_string() }
|
||||
|
||||
@@ -558,7 +558,9 @@ fn format_settings(ui: &mut egui::Ui, d: &mut Dialog, id: &str) {
|
||||
d.fields.get("__presets").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(tl!("PDF Preset:")).color(t.text_dim));
|
||||
if let Some(p) = widgets::dropdown(ui, "efs-pdf-preset", &s.str("preset"), &labels, 220.0).and_then(|i| labels.get(i)) {
|
||||
// The built-in presets are ours (translated); the saved ones are names.
|
||||
let builtin = |k: usize| labels.get(k).is_some_and(|n| super::save_pdf::is_builtin_preset(n));
|
||||
if let Some(p) = super::mixed_dropdown(ui, "efs-pdf-preset", &s.str("preset"), &labels, 220.0, builtin).and_then(|i| labels.get(i)) {
|
||||
s.fields.insert("preset".into(), json!(p));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -153,7 +153,8 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
}
|
||||
|
||||
/// The flattener preset dropdown: `presets` by name, showing `name` while `options` are that
|
||||
/// preset's, else [Custom]. Returns the preset chosen.
|
||||
/// preset's, else [Custom]. The built-in presets are ours (translated); the saved ones are names.
|
||||
/// Returns the preset chosen.
|
||||
pub(crate) fn preset_dropdown<'a>(
|
||||
ui: &mut egui::Ui,
|
||||
id: &str,
|
||||
@@ -162,9 +163,14 @@ pub(crate) fn preset_dropdown<'a>(
|
||||
options: Option<&FlattenOptions>,
|
||||
width: f32,
|
||||
) -> Option<&'a FlattenerPreset> {
|
||||
let shown = presets.iter().find(|p| p.name == name && Some(&p.options) == options).map_or(CUSTOM, |p| p.name.as_str());
|
||||
let shown = presets.iter().find(|p| p.name == name && Some(&p.options) == options).map_or(tl!(CUSTOM), |p| p.name.as_str());
|
||||
let names: Vec<&str> = presets.iter().map(|p| p.name.as_str()).collect();
|
||||
widgets::dropdown(ui, id, shown, &names, width).map(|i| &presets[i])
|
||||
super::mixed_dropdown(ui, id, shown, &names, width, |k| names.get(k).is_some_and(|n| is_builtin(n))).and_then(|i| presets.get(i))
|
||||
}
|
||||
|
||||
/// Is `name` a built-in flattener preset's?
|
||||
fn is_builtin(name: &str) -> bool {
|
||||
FlattenOptions::PRESETS.iter().any(|id| FlattenOptions::preset_label(id) == Some(name))
|
||||
}
|
||||
|
||||
/// The width of the option value fields.
|
||||
@@ -230,6 +236,13 @@ mod tests {
|
||||
use super::*;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
/// The built-in presets are told from saved ones by name (only theirs are translated).
|
||||
#[test]
|
||||
fn built_in_presets_are_known_by_name() {
|
||||
assert!(FlattenOptions::builtin_presets().iter().all(|p| is_builtin(&p.name)));
|
||||
assert!(!is_builtin("Regular") && !is_builtin("high"));
|
||||
}
|
||||
|
||||
fn frame(app: &mut VectorcraftApp) {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
|
||||
@@ -207,14 +207,15 @@ pub(super) fn param_fields(ui: &mut egui::Ui, d: &mut Dialog, is_length: &dyn Fn
|
||||
|
||||
/// Editor for a plug-in's parameters from its schema (plug-in filter and effect dialogs): numbers
|
||||
/// within their range, whole numbers, checkboxes and dropdowns, in declaration order. Returns
|
||||
/// true when a value changed.
|
||||
/// true when a value changed. The labels (from the parameter keys) and choices are the plug-in's
|
||||
/// text, shown as they are.
|
||||
pub(super) fn schema_fields(ui: &mut egui::Ui, d: &mut Dialog, specs: &[(String, vectorcraft_plugins::ParamSpec)]) -> bool {
|
||||
use vectorcraft_plugins::ParamSpec;
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut changed = false;
|
||||
egui::Grid::new("plugin-grid").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
for (k, spec) in specs {
|
||||
ui.label(egui::RichText::new(humanized(k)).color(t.text));
|
||||
ui.label(egui::RichText::new(humanize(k)).color(t.text));
|
||||
let cur = d.fields.get(k).cloned().unwrap_or_else(|| spec.default_value());
|
||||
let new = match spec {
|
||||
ParamSpec::Number { min, max, .. } => {
|
||||
@@ -232,7 +233,7 @@ pub(super) fn schema_fields(ui: &mut egui::Ui, d: &mut Dialog, specs: &[(String,
|
||||
}
|
||||
ParamSpec::Choice { options, .. } => {
|
||||
let labels: Vec<&str> = options.iter().map(String::as_str).collect();
|
||||
crate::widgets::dropdown(ui, ("plugin-choice", k), cur.as_str().unwrap_or_default(), &labels, 140.0)
|
||||
crate::widgets::dropdown_names(ui, ("plugin-choice", k), cur.as_str().unwrap_or_default(), &labels, 140.0)
|
||||
.and_then(|i| options.get(i))
|
||||
.map(|o| json!(o))
|
||||
}
|
||||
|
||||
@@ -108,7 +108,8 @@ const EDGE_GAP: f32 = 8.0;
|
||||
pub(crate) struct DialogSpec {
|
||||
/// Draws its own window instead of the shared frame (the frame fields below are then unused).
|
||||
pub window: Option<fn(&mut VectorcraftApp, &egui::Context)>,
|
||||
/// The heading (and window title).
|
||||
/// The heading (and window title), in the UI language: the spec translates its own text and
|
||||
/// leaves names in it (a document's, a plug-in's) as they are.
|
||||
pub heading: fn(&Dialog) -> String,
|
||||
/// Draws the fields. Returns true to close the dialog as Cancel would.
|
||||
pub body: fn(&mut VectorcraftApp, &mut egui::Ui, &mut Dialog) -> bool,
|
||||
@@ -131,7 +132,7 @@ impl DialogSpec {
|
||||
/// A text field per value; OK just closes. Also the fallback for unregistered kinds.
|
||||
pub const FORM: Self = Self {
|
||||
window: None,
|
||||
heading: |_| "Dialog".into(),
|
||||
heading: |_| tl!("Dialog").into(),
|
||||
body: |app, ui, d| {
|
||||
form::grid(ui, d, app.session.general_unit());
|
||||
false
|
||||
@@ -340,7 +341,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let room = (ctx.content_rect().width() - 2.0 * (f32::from(MARGIN) + EDGE_GAP)).max(EDGE_GAP);
|
||||
ui.set_min_width(spec.min_width.min(room));
|
||||
ui.set_max_width(spec.max_width.map_or(room, |w| w.min(room)));
|
||||
ui.label(egui::RichText::new(tl!(heading.as_str())).font(theme::semibold(16.0)).color(t.text));
|
||||
ui.label(egui::RichText::new(heading.as_str()).font(theme::semibold(16.0)).color(t.text));
|
||||
ui.add_space(12.0);
|
||||
cancel = (spec.body)(app, ui, &mut d);
|
||||
ui.add_space(16.0);
|
||||
@@ -379,6 +380,28 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a dropdown shows for `names`, a list mixing built-in labels (`builtin(index)`: translated
|
||||
/// into `lang`) with names the user saved or a file or the system supplied (shown as they are).
|
||||
pub(crate) fn shown_names<'a>(lang: crate::i18n::Lang, names: &[&'a str], builtin: impl Fn(usize) -> bool) -> Vec<&'a str> {
|
||||
names.iter().enumerate().map(|(k, n)| if builtin(k) { crate::i18n::tr(lang, n) } else { *n }).collect()
|
||||
}
|
||||
|
||||
/// [`widgets::dropdown_names`] over such a mixed list ([`shown_names`] in the UI language), showing
|
||||
/// `current` as its entry reads; a `current` that isn't among `names` is shown as given (a caller
|
||||
/// translates its own [Custom]). Returns the index chosen in `names`.
|
||||
pub(crate) fn mixed_dropdown(
|
||||
ui: &mut egui::Ui,
|
||||
id: impl std::hash::Hash + std::fmt::Debug,
|
||||
current: &str,
|
||||
names: &[&str],
|
||||
width: f32,
|
||||
builtin: impl Fn(usize) -> bool,
|
||||
) -> Option<usize> {
|
||||
let shown = shown_names(crate::i18n::current(), names, builtin);
|
||||
let current = names.iter().position(|n| *n == current).and_then(|i| shown.get(i).copied()).unwrap_or(current);
|
||||
widgets::dropdown_names(ui, id, current, &shown, width)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -252,10 +252,22 @@ fn presets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Is `name` a built-in preset's? Those are ours (translated); Recent and Saved hold the names of
|
||||
/// the user's documents and presets, shown as they are.
|
||||
fn is_builtin_preset(name: &str) -> bool {
|
||||
newdoc::CATEGORIES.iter().any(|(_, _, _, presets)| presets.iter().any(|(n, ..)| *n == name))
|
||||
}
|
||||
|
||||
/// A preset's name as shown: a built-in one translated, the user's as it is.
|
||||
fn preset_name(name: &str) -> &str {
|
||||
if is_builtin_preset(name) { tl!(name) } else { name }
|
||||
}
|
||||
|
||||
/// A preset card: a page-proportioned thumbnail, the name and the size (New Document, the Home
|
||||
/// screen). Returns the card's response.
|
||||
pub fn preset_card(ui: &mut egui::Ui, s: &DocSettings, selected: bool) -> egui::Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let label = preset_name(&s.name);
|
||||
let (r, resp) = ui.allocate_exact_size(egui::vec2(146.0, 150.0), egui::Sense::click());
|
||||
let p = ui.painter();
|
||||
p.rect_filled(r, egui::CornerRadius::same(6), if resp.hovered() || selected { t.hover } else { t.panel_darker });
|
||||
@@ -271,7 +283,7 @@ pub fn preset_card(ui: &mut egui::Ui, s: &DocSettings, selected: bool) -> egui::
|
||||
p.rect_filled(page, 0.0, egui::Color32::WHITE);
|
||||
// The name on up to two lines, then the size.
|
||||
let mut job = egui::text::LayoutJob::single_section(
|
||||
tl!(&s.name).to_string(),
|
||||
label.to_string(),
|
||||
egui::TextFormat { font_id: theme::semibold(11.5), color: t.text_strong, ..Default::default() },
|
||||
);
|
||||
job.halign = egui::Align::Center;
|
||||
@@ -280,7 +292,7 @@ pub fn preset_card(ui: &mut egui::Ui, s: &DocSettings, selected: bool) -> egui::
|
||||
let name_h = name.size().y;
|
||||
p.galley(egui::pos2(r.center().x, r.top() + 94.0), name, t.text_strong);
|
||||
p.text(egui::pos2(r.center().x, r.top() + 98.0 + name_h), egui::Align2::CENTER_TOP, s.size_label(), egui::FontId::proportional(11.0), t.text_dim);
|
||||
resp.on_hover_text(tl!(&s.name))
|
||||
resp.on_hover_text(label)
|
||||
}
|
||||
|
||||
/// The Preset Details column.
|
||||
@@ -535,8 +547,9 @@ fn show_more(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let size_names: Vec<&str> = sizes.iter().map(|s| s.name.as_str()).collect();
|
||||
let preset = d.str("preset");
|
||||
widgets::label_row(ui, tl!("Size:"), L, |ui| {
|
||||
let shown = if preset.is_empty() { "Custom" } else { preset.as_str() };
|
||||
if let Some(s) = widgets::dropdown(ui, "newdoc-size", shown, &size_names, 220.0).and_then(|i| sizes.get(i)) {
|
||||
// The profile's sizes are built in; the preset may be one the user saved.
|
||||
let shown = if preset.is_empty() { tl!("Custom") } else { preset_name(&preset) };
|
||||
if let Some(s) = super::mixed_dropdown(ui, "newdoc-size", shown, &size_names, 220.0, |_| true).and_then(|i| sizes.get(i)) {
|
||||
apply(&mut d, s);
|
||||
}
|
||||
});
|
||||
@@ -589,6 +602,14 @@ mod tests {
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
|
||||
/// Cards translate the built-in presets' names only: Recent and Saved show the user's names.
|
||||
#[test]
|
||||
fn only_built_in_preset_names_are_translated() {
|
||||
assert!(super::is_builtin_preset("Letter") && super::is_builtin_preset("Phone 390×844"));
|
||||
assert!(!super::is_builtin_preset("Black") && !super::is_builtin_preset("Untitled-1"));
|
||||
assert_eq!(super::preset_name("Black"), "Black");
|
||||
}
|
||||
|
||||
fn frame(app: &mut VectorcraftApp) -> egui::FullOutput {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
|
||||
@@ -39,7 +39,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
if doc.artboards.len() > 1 {
|
||||
label(ui, tl!("Artboard:"));
|
||||
let names: Vec<&str> = doc.artboards.iter().map(|a| a.name.as_str()).collect();
|
||||
if let Some(i) = widgets::dropdown(ui, "office-artboard", names.get(board).copied().unwrap_or_default(), &names, 160.0) {
|
||||
if let Some(i) = widgets::dropdown_names(ui, "office-artboard", names.get(board).copied().unwrap_or_default(), &names, 160.0) {
|
||||
d.fields.insert("artboard".into(), json!(i));
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
use vectorcraft_tools::distort::perspective::define::is_builtin;
|
||||
use vectorcraft_tools::distort::perspective::{GridDefinition, Rgb};
|
||||
|
||||
use super::{DialogSpec, form};
|
||||
@@ -219,8 +220,11 @@ fn name_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
let shown = current.map_or(CUSTOM, |p| p.name.as_str());
|
||||
let names: Vec<&str> = std::iter::once(CUSTOM).chain(presets.iter().map(|p| p.name.as_str())).collect();
|
||||
// [Custom] and the built-in presets are ours (translated); the saved ones are names.
|
||||
let builtin = |k: usize| k == 0 || names.get(k).is_some_and(|n| is_builtin(n));
|
||||
widgets::label_row(ui, tl!("Preset:"), LABEL, |ui| {
|
||||
if let Some(p) = widgets::dropdown(ui, "persp-preset", shown, &names, 200.0).and_then(|i| i.checked_sub(1)).and_then(|i| presets.get(i)) {
|
||||
let picked = super::mixed_dropdown(ui, "persp-preset", shown, &names, 200.0, builtin);
|
||||
if let Some(p) = picked.and_then(|i| i.checked_sub(1)).and_then(|i| presets.get(i)) {
|
||||
let mut fields = json!(p);
|
||||
fields[SHOWN] = fields["units"].clone();
|
||||
if let Some(o) = fields.as_object() {
|
||||
|
||||
@@ -23,8 +23,9 @@ const CMD: &str = "plugin.run";
|
||||
/// Object › Plug-ins' header while no filter plug-in is installed.
|
||||
pub const NO_FILTERS: &str = "No filter plug-ins installed";
|
||||
|
||||
/// Headed by the plug-in's name, as it is (plug-in text is never translated).
|
||||
pub(super) const SPEC: DialogSpec =
|
||||
DialogSpec { heading: |d| tl!(&d.str("__label")).to_string(), body, confirm, preview: true, max_width: Some(420.0), ..DialogSpec::FORM };
|
||||
DialogSpec { heading: |d| d.str("__label"), body, confirm, preview: true, max_width: Some(420.0), ..DialogSpec::FORM };
|
||||
|
||||
/// A menu label for plug-in `name`: with an ellipsis when it opens a dialog.
|
||||
fn label(name: &str, has_params: bool) -> &'static str {
|
||||
|
||||
@@ -264,8 +264,9 @@ fn preset_rows(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let asking = d.fields.contains_key(SAVE_AS);
|
||||
let mut ask = false;
|
||||
row_with(ui, tl!("Print Preset:"), TOP_LABEL_WIDTH, |ui| {
|
||||
picked =
|
||||
widgets::dropdown(ui, "print-preset", if same { &name } else { CUSTOM }, &names, 300.0).and_then(|i| names.get(i)).map(|n| n.to_string());
|
||||
// [Default] is ours (translated); the saved presets are names.
|
||||
let current = if same { name.as_str() } else { tl!(CUSTOM) };
|
||||
picked = super::mixed_dropdown(ui, "print-preset", current, &names, 300.0, |k| k == 0).and_then(|i| names.get(i)).map(|n| n.to_string());
|
||||
let save = ui.add_enabled_ui(!asking, |ui| widgets::flat_button(ui, tl!("Save Preset…"), 96.0)).inner;
|
||||
ask = save.on_hover_text(tl!("Save these settings as a print preset")).clicked();
|
||||
});
|
||||
@@ -317,23 +318,25 @@ fn printer_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
entries.push(Destination::Default);
|
||||
}
|
||||
entries.push(Destination::File);
|
||||
let label = |e: &Destination| match e {
|
||||
Destination::Printer(name) => name.clone(),
|
||||
Destination::Default => DEFAULT_PRINTER.into(),
|
||||
Destination::File => PDF_FILE.into(),
|
||||
};
|
||||
let labels: Vec<String> = entries.iter().map(label).collect();
|
||||
// The system's printers by their names; the default printer and PDF File are ours.
|
||||
let options: Vec<&str> = entries
|
||||
.iter()
|
||||
.map(|e| match e {
|
||||
Destination::Printer(name) => name.as_str(),
|
||||
Destination::Default => tl!(DEFAULT_PRINTER),
|
||||
Destination::File => tl!(PDF_FILE),
|
||||
})
|
||||
.collect();
|
||||
let printer = d.str("printer");
|
||||
let current = if d.bool("toFile") {
|
||||
PDF_FILE
|
||||
tl!(PDF_FILE)
|
||||
} else if printer.is_empty() {
|
||||
DEFAULT_PRINTER
|
||||
tl!(DEFAULT_PRINTER)
|
||||
} else {
|
||||
printer.as_str()
|
||||
};
|
||||
row_with(ui, tl!("Printer:"), TOP_LABEL_WIDTH, |ui| {
|
||||
let options: Vec<&str> = labels.iter().map(String::as_str).collect();
|
||||
match widgets::dropdown(ui, "print-printer", current, &options, 300.0).and_then(|i| entries.get(i)) {
|
||||
match widgets::dropdown_names(ui, "print-printer", current, &options, 300.0).and_then(|i| entries.get(i)) {
|
||||
Some(Destination::Printer(name)) => {
|
||||
d.fields.insert("printer".into(), json!(name));
|
||||
d.fields.insert("toFile".into(), json!(false));
|
||||
@@ -733,7 +736,7 @@ fn graphics(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
fn color(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let profiles = vectorcraft_color::cms::profiles();
|
||||
let names: Vec<&str> = std::iter::once(SAME_AS_SOURCE).chain(profiles.iter().map(|p| p.name.as_str())).collect();
|
||||
row(ui, tl!("Printer profile:"), |ui| super::save_pdf::profile_pick(ui, d, "color.profile", &names, true));
|
||||
row(ui, tl!("Printer profile:"), |ui| super::save_pdf::profile_pick(ui, d, "color.profile", &names, 1, true));
|
||||
note(ui, tl!("Composite colours are converted to it; separations separate with it when it is a CMYK profile."));
|
||||
row(ui, tl!("Rendering intent:"), |ui| {
|
||||
let current = serde_json::from_value::<Intent>(get(d, "color.intent").clone()).unwrap_or_default();
|
||||
@@ -760,7 +763,9 @@ fn advanced(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
let presets = app.session.flattener_presets();
|
||||
let names: Vec<&str> = std::iter::once(KEEP_TRANSPARENCY).chain(presets.iter().map(|p| p.name.as_str())).collect();
|
||||
row(ui, tl!("Preset:"), |ui| super::save_pdf::profile_pick(ui, d, "advanced.flattenerPreset", &names, true));
|
||||
// None and the built-in presets are ours; the saved presets are names.
|
||||
let builtins = names.len().saturating_sub(app.session.prefs.flattener_presets.len());
|
||||
row(ui, tl!("Preset:"), |ui| super::save_pdf::profile_pick(ui, d, "advanced.flattenerPreset", &names, builtins, true));
|
||||
note(
|
||||
ui,
|
||||
tl!(
|
||||
|
||||
@@ -28,7 +28,7 @@ use vectorcraft_engine::cmd::swatchlib;
|
||||
|
||||
use super::{DialogSpec, form};
|
||||
use crate::panels::c32;
|
||||
use crate::panels::swatches::{colour_libraries, limit_key, limit_name};
|
||||
use crate::panels::swatches::{DOCUMENT_SWATCHES, colour_libraries, limit_key};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
@@ -433,15 +433,18 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label(egui::RichText::new(tl!("Limit to Library:")).color(t.text_dim));
|
||||
// None, the document's swatches, then the colour libraries.
|
||||
// None, the document's swatches, then the colour libraries: the built-in ones (at the top
|
||||
// level of the library menu) translated, those the user saved or loaded by their names.
|
||||
let libs = colour_libraries(app);
|
||||
let keys: Vec<&str> = ["", swatchlib::DOCUMENT_SWATCHES].into_iter().chain(libs.iter().map(|l| l.id.as_str())).collect();
|
||||
let labels: Vec<String> = keys.iter().map(|k| limit_name(app, k).unwrap_or_else(|| tl!("None").into())).collect();
|
||||
let names: Vec<&str> = ["None", DOCUMENT_SWATCHES].into_iter().chain(libs.iter().map(|l| l.name.as_str())).collect();
|
||||
let builtin = |k: usize| k.checked_sub(2).is_none_or(|i| libs.get(i).is_some_and(|l| l.submenu.is_none()));
|
||||
let shown = super::shown_names(crate::i18n::current(), &names, builtin);
|
||||
let cur = d.str("limitTo");
|
||||
let name = keys.iter().position(|k| *k == cur).map_or(tl!("None"), |i| labels[i].as_str());
|
||||
let labels: Vec<&str> = labels.iter().map(String::as_str).collect();
|
||||
if let Some(i) = widgets::dropdown(ui, "recolor-library", name, &labels, 180.0) {
|
||||
d.fields.insert("limitTo".into(), json!(keys[i]));
|
||||
let at = keys.iter().position(|k| *k == cur).unwrap_or(0);
|
||||
let current = shown.get(at).copied().unwrap_or_default();
|
||||
if let Some(k) = widgets::dropdown_names(ui, "recolor-library", current, &shown, 180.0).and_then(|i| keys.get(i)) {
|
||||
d.fields.insert("limitTo".into(), json!(k));
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
@@ -570,10 +570,13 @@ fn presets_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, s: O
|
||||
let saved = app.session.prefs.web_export_presets.len();
|
||||
let current = s.and_then(|s| presets.iter().rposition(|p| p.settings == *s));
|
||||
let names: Vec<&str> = presets.iter().map(|p| p.name.as_str()).collect();
|
||||
// The built-in presets come first (translated); the saved ones are names.
|
||||
let builtins = presets.len().saturating_sub(saved);
|
||||
let labels = super::shown_names(crate::i18n::current(), &names, |k| k < builtins);
|
||||
let mut run: Option<(&str, Value)> = None;
|
||||
widgets::label_row(ui, tl!("Preset:"), LABEL_W, |ui| {
|
||||
let shown = current.and_then(|i| names.get(i).copied()).unwrap_or(tl!("[Unnamed]"));
|
||||
if let Some(i) = widgets::dropdown(ui, "sfw-preset", shown, &names, FIELD_W - 60.0)
|
||||
let shown = current.and_then(|i| labels.get(i).copied()).unwrap_or(tl!("[Unnamed]"));
|
||||
if let Some(i) = widgets::dropdown_names(ui, "sfw-preset", shown, &labels, FIELD_W - 60.0)
|
||||
&& let Some(p) = presets.get(i)
|
||||
{
|
||||
d.fields.extend(settings_fields(&p.settings));
|
||||
@@ -828,7 +831,7 @@ fn image_size(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, s: Option
|
||||
let names: Vec<&str> = boards.iter().map(|a| a.name.as_str()).collect();
|
||||
let at = s.and_then(|s| s.artboard).unwrap_or(0).min(names.len() - 1);
|
||||
widgets::label_row(ui, tl!("Artboard:"), LABEL_W, |ui| {
|
||||
if let Some(i) = widgets::dropdown(ui, "sfw-artboard", names.get(at).copied().unwrap_or_default(), &names, FIELD_W) {
|
||||
if let Some(i) = widgets::dropdown_names(ui, "sfw-artboard", names.get(at).copied().unwrap_or_default(), &names, FIELD_W) {
|
||||
d.fields.insert("artboard".into(), json!(i));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -164,7 +164,7 @@ fn option_row(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, o: &Forma
|
||||
if let Some(boards) = artboards {
|
||||
let i = value.as_u64().unwrap_or(0) as usize;
|
||||
let names: Vec<&str> = boards.iter().map(|a| a.name.as_str()).collect();
|
||||
if let Some(i) = widgets::dropdown(ui, ("save-option", o.name), names.get(i).copied().unwrap_or(""), &names, 254.0) {
|
||||
if let Some(i) = widgets::dropdown_names(ui, ("save-option", o.name), names.get(i).copied().unwrap_or(""), &names, 254.0) {
|
||||
d.fields.insert(o.name.into(), json!(i));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -50,6 +50,13 @@ pub(super) const SECTIONS: [&str; 7] = ["General", "Compression", "Marks and Ble
|
||||
/// The default settings as JSON: what a field an agent left out reads as.
|
||||
static DEFAULTS: LazyLock<Value> = LazyLock::new(|| serde_json::to_value(PdfSettings::default()).unwrap_or_default());
|
||||
|
||||
/// Is `name` a built-in PDF preset's? Those are ours (translated where listed); the saved ones are
|
||||
/// names.
|
||||
pub(super) fn is_builtin_preset(name: &str) -> bool {
|
||||
static NAMES: LazyLock<Vec<String>> = LazyLock::new(|| vectorcraft_pdf::builtin_presets().into_iter().map(|p| p.name).collect());
|
||||
NAMES.iter().any(|n| n == name)
|
||||
}
|
||||
|
||||
/// Trim mark weights offered (pt).
|
||||
const WEIGHTS: [f64; 3] = [0.125, 0.25, 0.5];
|
||||
const WEIGHT_LABELS: [&str; 3] = ["0.125 pt", "0.25 pt", "0.5 pt"];
|
||||
@@ -394,7 +401,11 @@ fn preset_rows(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
row_with(ui, tl!("Preset:"), TOP_LABEL_WIDTH, |ui| {
|
||||
let presets = pdf::presets(&app.session);
|
||||
let names: Vec<&str> = presets.iter().map(String::as_str).collect();
|
||||
let chosen = widgets::dropdown(ui, "pdf-preset", &d.str("preset"), &names, 300.0).and_then(|i| names.get(i)).map(|p| p.to_string());
|
||||
// The built-in presets come first (translated); the saved ones are names.
|
||||
let builtins = names.len().saturating_sub(app.session.prefs.pdf_presets.len());
|
||||
let chosen = super::mixed_dropdown(ui, "pdf-preset", &d.str("preset"), &names, 300.0, |k| k < builtins)
|
||||
.and_then(|i| names.get(i))
|
||||
.map(|p| p.to_string());
|
||||
if let Some(name) = chosen {
|
||||
apply_preset(app, d, &name);
|
||||
}
|
||||
@@ -560,12 +571,13 @@ pub(super) fn marks_and_bleeds(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut
|
||||
}
|
||||
|
||||
/// A dropdown of names such as colour profiles or flattener presets (`names`, the first one
|
||||
/// standing for "" at `path`), showing the name at `path` (one that isn't among them too).
|
||||
pub(super) fn profile_pick(ui: &mut egui::Ui, d: &mut Dialog, path: &str, names: &[&str], enabled: bool) {
|
||||
/// standing for "" at `path`), showing the name at `path` (one that isn't among them too). The
|
||||
/// first `builtins` entries are ours (translated); the others (profiles, saved presets) are names.
|
||||
pub(super) fn profile_pick(ui: &mut egui::Ui, d: &mut Dialog, path: &str, names: &[&str], builtins: usize, enabled: bool) {
|
||||
let blank = names.first().copied().unwrap_or_default();
|
||||
let current = get(d, path).as_str().filter(|s| !s.is_empty()).unwrap_or(blank).to_string();
|
||||
ui.add_enabled_ui(enabled, |ui| {
|
||||
if let Some(i) = widgets::dropdown(ui, path, ¤t, names, 300.0) {
|
||||
if let Some(i) = super::mixed_dropdown(ui, path, ¤t, names, 300.0, |k| k < builtins) {
|
||||
set(d, path, json!(if i == 0 { "" } else { names.get(i).copied().unwrap_or_default() }));
|
||||
}
|
||||
});
|
||||
@@ -581,7 +593,7 @@ fn output(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
// PDF/X-1a converts to CMYK even without a conversion.
|
||||
let converting = get(d, "output.conversion") != ColorConversion::None.id() || standard.cmyk_only();
|
||||
row(ui, tl!("Destination:"), |ui| profile_pick(ui, d, "output.destination", &names(DOCUMENT_PROFILE), converting));
|
||||
row(ui, tl!("Destination:"), |ui| profile_pick(ui, d, "output.destination", &names(DOCUMENT_PROFILE), 1, converting));
|
||||
row(ui, tl!("Profile inclusion:"), |ui| {
|
||||
// A standard decides whether colours are tagged.
|
||||
ui.add_enabled_ui(standard == Standard::None, |ui| pick::<ProfileInclusion>(ui, d, "output.profiles", 300.0, |_| true));
|
||||
@@ -595,7 +607,7 @@ fn output(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
// PDF/A files carry their own output intent; PDF/X files always have one.
|
||||
let own = standard != Standard::PdfA2b;
|
||||
let blank = if standard.is_pdfx() { PDFX_INTENT } else { NO_PROFILE };
|
||||
row(ui, tl!("Output intent profile:"), |ui| profile_pick(ui, d, "output.outputIntent", &names(blank), own));
|
||||
row(ui, tl!("Output intent profile:"), |ui| profile_pick(ui, d, "output.outputIntent", &names(blank), 1, own));
|
||||
for (p, label) in [
|
||||
("output.outputCondition", tl!("Output condition:")),
|
||||
("output.outputConditionId", tl!("Condition identifier:")),
|
||||
@@ -627,11 +639,13 @@ fn advanced(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
row(ui, tl!("Flattener preset:"), |ui| {
|
||||
let presets = app.session.flattener_presets();
|
||||
let names: Vec<&str> = presets.iter().map(|p| p.name.as_str()).collect();
|
||||
// The built-in presets come first (translated); the saved ones are names.
|
||||
let builtins = names.len().saturating_sub(app.session.prefs.flattener_presets.len());
|
||||
let name = get(d, "flattenerPreset").as_str().map(str::trim).filter(|n| !n.is_empty()).unwrap_or(pdf::DEFAULT_FLATTENER);
|
||||
// A built-in preset named by id (`high`) shows as its name.
|
||||
let current = FlattenOptions::preset_label(&name.to_ascii_lowercase()).unwrap_or(name).to_string();
|
||||
ui.add_enabled_ui(flat, |ui| {
|
||||
if let Some(n) = widgets::dropdown(ui, "flattenerPreset", ¤t, &names, 200.0).and_then(|i| names.get(i)) {
|
||||
if let Some(n) = super::mixed_dropdown(ui, "flattenerPreset", ¤t, &names, 200.0, |k| k < builtins).and_then(|i| names.get(i)) {
|
||||
set(d, "flattenerPreset", json!(n));
|
||||
}
|
||||
});
|
||||
@@ -801,6 +815,13 @@ mod tests {
|
||||
|
||||
type Log = Rc<RefCell<Vec<(String, Vec<u8>)>>>;
|
||||
|
||||
/// The built-in presets are told from saved ones by name (only theirs are translated).
|
||||
#[test]
|
||||
fn built_in_presets_are_known_by_name() {
|
||||
assert!(vectorcraft_pdf::builtin_presets().iter().all(|p| is_builtin_preset(&p.name)));
|
||||
assert!(is_builtin_preset(pdf::DEFAULT_PRESET) && !is_builtin_preset("Default") && !is_builtin_preset("My Preset"));
|
||||
}
|
||||
|
||||
/// An app with two artboards whose writer and URL opener record what they get.
|
||||
fn app() -> (VectorcraftApp, Log, Log) {
|
||||
let (written, opened) = (Log::default(), Log::default());
|
||||
|
||||
@@ -273,3 +273,28 @@ fn a_form_dialog_is_as_wide_as_its_fields_not_the_window() {
|
||||
let narrow = dialog_rect(&mut app, 360.0);
|
||||
assert!(narrow.width() <= 360.0, "{:.0} wide in a 360-point window", narrow.width());
|
||||
}
|
||||
|
||||
/// A list mixing built-in labels with names translates only the built-in entries: names that
|
||||
/// happen to be catalog keys ("Black", "Regular") are shown as they are.
|
||||
#[test]
|
||||
fn mixed_lists_translate_only_their_built_in_entries() {
|
||||
use crate::i18n::{Lang, tr};
|
||||
let zh = Lang::from_code("zh-hant").unwrap();
|
||||
for key in ["None", "Black", "Regular", "Default"] {
|
||||
assert_ne!(tr(zh, key), key, "{key} must be a catalog key for this test");
|
||||
}
|
||||
let names = ["None", "Black", "Regular", "Default"];
|
||||
assert_eq!(shown_names(zh, &names, |k| k == 0 || k == 3), [tr(zh, "None"), "Black", "Regular", tr(zh, "Default")]);
|
||||
assert_eq!(shown_names(zh, &names, |_| false), names);
|
||||
assert_eq!(shown_names(Lang::EN, &names, |_| true), names);
|
||||
}
|
||||
|
||||
/// Confirmations and plug-in dialogs are headed by the text they are given (callers translate
|
||||
/// their own templates; a plug-in's name is its own).
|
||||
#[test]
|
||||
fn given_headings_are_shown_as_they_are() {
|
||||
let d = Dialog::new(confirm::KIND, json!({"message": "Delete “Black”?", "detail": "Regular"}));
|
||||
assert_eq!((spec(&d.kind).heading)(&d), "Delete “Black”?");
|
||||
let d = Dialog::new(plugin::KIND, json!({"__label": "Black", "__plugin": "x"}));
|
||||
assert_eq!((spec(&d.kind).heading)(&d), "Black");
|
||||
}
|
||||
@@ -529,8 +529,8 @@ pub fn ask_revert(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
let c = vectorcraft_engine::find_command("file.revert").ok_or("no revert command")?;
|
||||
(c.enabled)(&app.session)?;
|
||||
let name = app.session.active().map(|d| d.title()).unwrap_or_default();
|
||||
let message = format!("Revert to the saved version of “{name}”?");
|
||||
dialogs::confirm::ask(app, &message, "Changes made since it was last saved will be lost.", "file.revert", json!({ "confirmed": true }));
|
||||
let message = crate::i18n::fmt(tl!("Revert to the saved version of “{name}”?"), &[("name", &name)]);
|
||||
dialogs::confirm::ask(app, &message, tl!("Changes made since it was last saved will be lost."), "file.revert", json!({ "confirmed": true }));
|
||||
Ok(json!({ "pending": dialogs::confirm::KIND }))
|
||||
}
|
||||
|
||||
|
||||
@@ -538,11 +538,13 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
if !PRESETS.contains(&cur.as_str()) {
|
||||
opts.push(CUSTOM);
|
||||
}
|
||||
if let Some(i) = widgets::dropdown(ui, "kbset", &cur, &opts, 200.0)
|
||||
&& let Some(p) = preset(opts[i])
|
||||
// The sets listed are ours (translated); a set read from an imported file shows
|
||||
// its name as it is.
|
||||
if let Some(&name) = crate::dialogs::mixed_dropdown(ui, "kbset", &cur, &opts, 200.0, |_| true).and_then(|i| opts.get(i))
|
||||
&& let Some(p) = preset(name)
|
||||
{
|
||||
ov = p;
|
||||
d.fields.insert("set".into(), json!(opts[i]));
|
||||
d.fields.insert("set".into(), json!(name));
|
||||
d.fields.insert("__message".into(), json!(""));
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
|
||||
Reference in new issue
Block a user