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:
bflatasticandClaude Opus 5.5 committed 2026-10-07 08:36:44 +07:00
1 parent f4181890bd
commit cf7a43d786
18 files changed
+188 -59

No files matched your search

+6 -4
View File
@@ -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));
+4 -1
View File
@@ -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));
}
});
+16 -3
View File
@@ -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);
+4 -3
View File
@@ -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))
}
+26 -3
View File
@@ -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)]
+25 -4
View File
@@ -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);
+1 -1
View File
@@ -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() {
+2 -1
View File
@@ -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 {
+19 -14
View File
@@ -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!(
+10 -7
View File
@@ -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();
});
+6 -3
View File
@@ -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));
}
});
+1 -1
View File
@@ -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 {
+28 -7
View File
@@ -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, &current, names, 300.0) {
if let Some(i) = super::mixed_dropdown(ui, path, &current, 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", &current, &names, 200.0).and_then(|i| names.get(i)) {
if let Some(n) = super::mixed_dropdown(ui, "flattenerPreset", &current, &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());
+25
View File
@@ -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");
}
+2 -2
View File
@@ -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 }))
}
+5 -3
View File
@@ -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);