forked from 1b-agent/opencadstudio-upstream
feat(locale): localize full interface
This commit is contained in:
1 parent
b90b3e3ecf
commit
3e6693b93f
205 files changed
+32369
-3810
No files matched your search
+10
-6
@@ -1,8 +1,12 @@
|
||||
use crate::app::Message;
|
||||
use iced::widget::{button, column, container, row, text};
|
||||
use iced::{Background, Element, Theme};
|
||||
use crate::t;
|
||||
|
||||
fn info_row<'a>(label: &'static str, value: String) -> Element<'a, Message> {
|
||||
fn info_row<'a>(
|
||||
label: std::borrow::Cow<'static, str>,
|
||||
value: String,
|
||||
) -> Element<'a, Message> {
|
||||
row![
|
||||
text(label)
|
||||
.size(11)
|
||||
@@ -30,7 +34,7 @@ pub fn view_window<'a>() -> Element<'a, Message> {
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().primary.base.color),
|
||||
}),
|
||||
text("CAD application for Architecture & Engineering")
|
||||
text(t!("CAD application for Architecture & Engineering"))
|
||||
.size(11)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
|
||||
@@ -49,16 +53,16 @@ pub fn view_window<'a>() -> Element<'a, Message> {
|
||||
|
||||
let info_block = container(
|
||||
column![
|
||||
info_row("Version", format!("v{}", version)),
|
||||
info_row("Platform", os.to_string()),
|
||||
info_row("Arch", arch.to_string()),
|
||||
info_row(t!("Version"), format!("v{}", version)),
|
||||
info_row(t!("Platform"), os.to_string()),
|
||||
info_row(t!("Arch"), arch.to_string()),
|
||||
]
|
||||
.spacing(2)
|
||||
.padding([12, 16]),
|
||||
)
|
||||
.style(container::bordered_box);
|
||||
|
||||
let copy_btn = button(text("Copy Info").size(11))
|
||||
let copy_btn = button(text(t!("Copy Info")).size(11))
|
||||
.on_press(Message::AboutCopyInfo)
|
||||
.style(button::primary)
|
||||
.padding([6, 16]);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use crate::app::Message;
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||
use iced::{Background, Element, Length, Theme};
|
||||
use crate::t;
|
||||
|
||||
/// Which column of an alias row a text edit targets.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -29,11 +30,10 @@ pub fn view_window(
|
||||
rows: &[(String, String)],
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'_, Message> {
|
||||
let title = text("Command Aliases").size(15);
|
||||
let hint = text(
|
||||
"Type an alias and the command it runs (e.g. L → LINE). \
|
||||
Apply to save to ocad.pgp; closing discards unapplied edits.",
|
||||
)
|
||||
let title = text(t!("Command Aliases")).size(15);
|
||||
let hint = text(t!(
|
||||
"Type an alias and the command it runs (e.g. L → LINE). Apply to save to ocad.pgp; closing discards unapplied edits."
|
||||
))
|
||||
.size(11)
|
||||
.style(muted_style);
|
||||
|
||||
@@ -44,8 +44,8 @@ pub fn view_window(
|
||||
|
||||
let head = container(
|
||||
row![
|
||||
container(text("Alias").size(11).style(muted_style)).width(Length::Fixed(120.0)),
|
||||
container(text("Command").size(11).style(muted_style)).width(sizing.width),
|
||||
container(text(t!("Alias")).size(11).style(muted_style)).width(Length::Fixed(120.0)),
|
||||
container(text(t!("Command")).size(11).style(muted_style)).width(sizing.width),
|
||||
Space::new().width(Length::Fixed(30.0)),
|
||||
]
|
||||
.spacing(8),
|
||||
@@ -54,12 +54,12 @@ pub fn view_window(
|
||||
|
||||
let mut list = column![].spacing(3);
|
||||
for (idx, (alias, cmd)) in rows.iter().enumerate() {
|
||||
let alias_box = text_input("alias", alias)
|
||||
let alias_box = text_input(t!("alias").as_ref(), alias)
|
||||
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v })
|
||||
.size(13)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fixed(120.0));
|
||||
let cmd_box = text_input("command", cmd)
|
||||
let cmd_box = text_input(t!("command").as_ref(), cmd)
|
||||
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v })
|
||||
.size(13)
|
||||
.padding([3, 6])
|
||||
@@ -75,13 +75,13 @@ pub fn view_window(
|
||||
);
|
||||
}
|
||||
|
||||
let add = button(text("+ Add alias").size(12))
|
||||
let add = button(text(t!("+ Add alias")).size(12))
|
||||
.on_press(Message::AliasEditorAdd)
|
||||
.padding([4, 10])
|
||||
.style(button::secondary);
|
||||
|
||||
// Apply — primary action; commits the rows to ocad.pgp and stays open.
|
||||
let apply = button(text("Apply").size(12))
|
||||
let apply = button(text(t!("Apply")).size(12))
|
||||
.on_press(Message::AliasEditorApply)
|
||||
.padding([4, 16])
|
||||
.style(button::primary);
|
||||
|
||||
@@ -20,6 +20,9 @@ use iced::widget::{
|
||||
button, checkbox, column, container, row, scrollable, text, text_input, Space,
|
||||
};
|
||||
use iced::{Background, Border, Element, Length, Theme};
|
||||
use crate::t;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
use crate::ui::properties::{lw_options, LwItem};
|
||||
|
||||
@@ -59,7 +62,8 @@ pub struct AttrRow {
|
||||
}
|
||||
|
||||
/// Combined justification options (horizontal × vertical), matching the
|
||||
/// standard text-justification list.
|
||||
/// standard text-justification list. The stored labels are the English source
|
||||
/// texts; `justify_label` / `justify_from_label` translate both directions.
|
||||
pub const JUSTIFY: &[(&str, HorizontalAlignment, VerticalAlignment)] = &[
|
||||
("Left", HorizontalAlignment::Left, VerticalAlignment::Baseline),
|
||||
("Center", HorizontalAlignment::Center, VerticalAlignment::Baseline),
|
||||
@@ -78,13 +82,15 @@ pub const JUSTIFY: &[(&str, HorizontalAlignment, VerticalAlignment)] = &[
|
||||
("Bottom Right", HorizontalAlignment::Right, VerticalAlignment::Bottom),
|
||||
];
|
||||
|
||||
/// The justification label for an (h, v) pair (defaults to "Left").
|
||||
pub fn justify_label(h: HorizontalAlignment, v: VerticalAlignment) -> &'static str {
|
||||
/// The justification label for an (h, v) pair (defaults to "Left"). Kept in
|
||||
/// English (like the colour names) — the labels are standard CAD terms and
|
||||
/// their keys would collide with ribbon tool labels such as "Right".
|
||||
pub fn justify_label(h: HorizontalAlignment, v: VerticalAlignment) -> Cow<'static, str> {
|
||||
JUSTIFY
|
||||
.iter()
|
||||
.find(|(_, jh, jv)| *jh == h && *jv == v)
|
||||
.map(|(l, _, _)| *l)
|
||||
.unwrap_or("Left")
|
||||
.map(|(l, _, _)| Cow::Borrowed(*l))
|
||||
.unwrap_or(Cow::Borrowed("Left"))
|
||||
}
|
||||
|
||||
/// Resolve a justification label back to its (h, v) pair.
|
||||
@@ -171,7 +177,7 @@ fn hdivider<'a>(width: Length) -> Element<'a, Message> {
|
||||
|
||||
/// One `label : widget` row with a fixed-width label column.
|
||||
fn field_row<'a>(
|
||||
label: &'a str,
|
||||
label: Cow<'static, str>,
|
||||
widget: Element<'a, Message>,
|
||||
width: Length,
|
||||
) -> Element<'a, Message> {
|
||||
@@ -187,7 +193,7 @@ fn field_row<'a>(
|
||||
|
||||
/// A labelled editable text field for one of the selected row's string buffers.
|
||||
fn edit_field<'a>(
|
||||
label: &'a str,
|
||||
label: Cow<'static, str>,
|
||||
value: &'a str,
|
||||
on_input: impl Fn(String) -> Message + 'a,
|
||||
width: Length,
|
||||
@@ -204,7 +210,7 @@ fn edit_field<'a>(
|
||||
|
||||
/// A labelled pick_list of owned string options.
|
||||
fn pick_field<'a>(
|
||||
label: &'a str,
|
||||
label: Cow<'static, str>,
|
||||
options: Vec<String>,
|
||||
selected: Option<String>,
|
||||
on_select: impl Fn(String) -> Message + 'a,
|
||||
@@ -218,7 +224,47 @@ fn pick_field<'a>(
|
||||
field_row(label, pl.into(), width)
|
||||
}
|
||||
|
||||
fn tab_button<'a>(label: &'a str, this: AttrTab, active: AttrTab) -> Element<'a, Message> {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct LocalizedChoice {
|
||||
raw: String,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl LocalizedChoice {
|
||||
fn new(raw: String) -> Self {
|
||||
let label = raw
|
||||
.strip_prefix("Color ")
|
||||
.map(|number| crate::tf!("Color {}", number).into_owned())
|
||||
.unwrap_or_else(|| crate::i18n::translate(&raw).into_owned());
|
||||
Self { raw, label }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LocalizedChoice {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.label)
|
||||
}
|
||||
}
|
||||
|
||||
/// A translated pick list which keeps the stored and emitted value unchanged.
|
||||
fn localized_pick_field<'a>(
|
||||
label: Cow<'static, str>,
|
||||
options: Vec<String>,
|
||||
selected: Option<String>,
|
||||
on_select: impl Fn(String) -> Message + 'a,
|
||||
width: Length,
|
||||
) -> Element<'a, Message> {
|
||||
let options = options.into_iter().map(LocalizedChoice::new).collect::<Vec<_>>();
|
||||
let selected = selected.map(LocalizedChoice::new);
|
||||
let pl = iced::widget::pick_list(selected, options, |value| value.to_string())
|
||||
.on_select(move |choice: LocalizedChoice| on_select(choice.raw))
|
||||
.text_size(13)
|
||||
.padding([3, 6])
|
||||
.width(width);
|
||||
field_row(label, pl.into(), width)
|
||||
}
|
||||
|
||||
fn tab_button<'a>(label: Cow<'static, str>, this: AttrTab, active: AttrTab) -> Element<'a, Message> {
|
||||
let is_active = this == active;
|
||||
button(text(label).size(11))
|
||||
.padding([4, 12])
|
||||
@@ -264,13 +310,13 @@ pub fn view_window<'a>(
|
||||
let height = sizing.height;
|
||||
// ── Top toolbar: block name on the left, Apply on the right ───────────
|
||||
// Mirrors the style-manager windows (actions left, primary action right).
|
||||
let apply = button(text("Apply").size(11))
|
||||
let apply = button(text(t!("Apply")).size(11))
|
||||
.padding([4, 14])
|
||||
.on_press(Message::AttrEditorApply)
|
||||
.style(button::primary);
|
||||
let toolbar = container(
|
||||
row![
|
||||
text(format!("Block: {block}")).size(12).style(muted_style),
|
||||
text(t!("Block: %{block}", block = block)).size(12).style(muted_style),
|
||||
Space::new().width(width),
|
||||
apply,
|
||||
]
|
||||
@@ -286,14 +332,14 @@ pub fn view_window<'a>(
|
||||
.padding([5, 8]);
|
||||
|
||||
let tabs = row![
|
||||
tab_button("Attribute", AttrTab::Attribute, tab),
|
||||
tab_button("Text Options", AttrTab::TextOptions, tab),
|
||||
tab_button("Properties", AttrTab::Properties, tab),
|
||||
tab_button(t!("Attribute"), AttrTab::Attribute, tab),
|
||||
tab_button(t!("Text Options"), AttrTab::TextOptions, tab),
|
||||
tab_button(t!("Properties"), AttrTab::Properties, tab),
|
||||
]
|
||||
.spacing(2);
|
||||
|
||||
let body: Element<'_, Message> = if rows.is_empty() {
|
||||
text("This block has no attributes.").size(13).style(muted_style).into()
|
||||
text(t!("This block has no attributes.")).size(13).style(muted_style).into()
|
||||
} else {
|
||||
match tab {
|
||||
AttrTab::Attribute => attribute_tab(rows, selected, width, height),
|
||||
@@ -337,9 +383,9 @@ fn attribute_tab<'a>(
|
||||
height: Length,
|
||||
) -> Element<'a, Message> {
|
||||
let head = row![
|
||||
container(text("Tag").size(11).style(muted_style)).width(130),
|
||||
container(text("Prompt").size(11).style(muted_style)).width(width),
|
||||
container(text("Value").size(11).style(muted_style)).width(140),
|
||||
container(text(t!("Tag")).size(11).style(muted_style)).width(130),
|
||||
container(text(t!("Prompt")).size(11).style(muted_style)).width(width),
|
||||
container(text(t!("Value")).size(11).style(muted_style)).width(140),
|
||||
]
|
||||
.spacing(6);
|
||||
|
||||
@@ -389,7 +435,7 @@ fn attribute_tab<'a>(
|
||||
head,
|
||||
scrollable(list).width(width).height(height),
|
||||
Space::new().height(8),
|
||||
field_row("Value:", value_box.into(), width),
|
||||
field_row(t!("Value:"), value_box.into(), width),
|
||||
]
|
||||
.spacing(6)
|
||||
.width(width)
|
||||
@@ -409,24 +455,24 @@ fn text_options_tab<'a>(
|
||||
} else {
|
||||
Some(r.text_style.clone())
|
||||
};
|
||||
let justify_opts: Vec<String> = JUSTIFY.iter().map(|(l, _, _)| l.to_string()).collect();
|
||||
let justify_opts: Vec<String> = JUSTIFY.iter().map(|(l, _, _)| (*l).to_string()).collect();
|
||||
let justify_sel = Some(justify_label(r.h_align, r.v_align).to_string());
|
||||
|
||||
column![
|
||||
pick_field("Text Style", styles, style_sel, |s| {
|
||||
pick_field(t!("Text Style"), styles, style_sel, |s| {
|
||||
Message::AttrEditorTextStyle(s)
|
||||
}, width),
|
||||
pick_field("Justification", justify_opts, justify_sel, |s| {
|
||||
localized_pick_field(t!("Justification"), justify_opts, justify_sel, |s| {
|
||||
Message::AttrEditorJustify(s)
|
||||
}, width),
|
||||
edit_field("Height", &r.height, Message::AttrEditorHeight, width),
|
||||
edit_field("Rotation", &r.rotation, Message::AttrEditorRotation, width),
|
||||
edit_field("Width Factor", &r.width_factor, Message::AttrEditorWidth, width),
|
||||
edit_field("Oblique Angle", &r.oblique, Message::AttrEditorOblique, width),
|
||||
edit_field(t!("Height"), &r.height, Message::AttrEditorHeight, width),
|
||||
edit_field(t!("Rotation"), &r.rotation, Message::AttrEditorRotation, width),
|
||||
edit_field(t!("Width Factor"), &r.width_factor, Message::AttrEditorWidth, width),
|
||||
edit_field(t!("Oblique Angle"), &r.oblique, Message::AttrEditorOblique, width),
|
||||
field_row(
|
||||
"",
|
||||
Cow::Borrowed(""),
|
||||
checkbox(r.backwards)
|
||||
.label("Backwards")
|
||||
.label(t!("Backwards"))
|
||||
.on_toggle(Message::AttrEditorBackwards)
|
||||
.size(15)
|
||||
.text_size(12)
|
||||
@@ -434,9 +480,9 @@ fn text_options_tab<'a>(
|
||||
width,
|
||||
),
|
||||
field_row(
|
||||
"",
|
||||
Cow::Borrowed(""),
|
||||
checkbox(r.upside_down)
|
||||
.label("Upside down")
|
||||
.label(t!("Upside down"))
|
||||
.on_toggle(Message::AttrEditorUpsideDown)
|
||||
.size(15)
|
||||
.text_size(12)
|
||||
@@ -483,12 +529,12 @@ fn properties_tab<'a>(
|
||||
.width(width);
|
||||
|
||||
column![
|
||||
pick_field("Layer", layers, layer_sel, Message::AttrEditorLayer, width),
|
||||
pick_field("Linetype", linetypes, lt_sel, Message::AttrEditorLinetype, width),
|
||||
pick_field("Color", color_opts, color_sel, |s| {
|
||||
pick_field(t!("Layer"), layers, layer_sel, Message::AttrEditorLayer, width),
|
||||
localized_pick_field(t!("Linetype"), linetypes, lt_sel, Message::AttrEditorLinetype, width),
|
||||
localized_pick_field(t!("Color"), color_opts, color_sel, |s| {
|
||||
Message::AttrEditorColor(s)
|
||||
}, width),
|
||||
field_row("Lineweight", lw.into(), width),
|
||||
field_row(t!("Lineweight"), lw.into(), width),
|
||||
]
|
||||
.spacing(8)
|
||||
.width(width)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::app::Message;
|
||||
use crate::t;
|
||||
use iced::widget::{button, column, row, text, text_input, Space};
|
||||
use iced::{Element, Fill, Length, Shrink};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub const FIND_INPUT_ID: &str = "find-replace-search";
|
||||
|
||||
@@ -15,19 +17,19 @@ pub fn view_window<'a>(
|
||||
} else {
|
||||
Shrink
|
||||
};
|
||||
let find_input = text_input("Text to find", search)
|
||||
let find_input = text_input("", search)
|
||||
.id(iced::widget::Id::new(FIND_INPUT_ID))
|
||||
.on_input(Message::FindReplaceSearchChanged)
|
||||
.on_submit(Message::FindReplaceNext)
|
||||
.padding([6, 8])
|
||||
.size(13);
|
||||
let replacement_input = text_input("Replacement text", replacement)
|
||||
let replacement_input = text_input("", replacement)
|
||||
.on_input(Message::FindReplaceReplacementChanged)
|
||||
.padding([6, 8])
|
||||
.size(13);
|
||||
|
||||
let enabled = !search.trim().is_empty();
|
||||
let action = |label: &'static str, message: Message| {
|
||||
let action = |label: Cow<'static, str>, message: Message| {
|
||||
let button = button(text(label).size(12)).padding([6, 12]);
|
||||
if enabled {
|
||||
button.on_press(message)
|
||||
@@ -38,29 +40,29 @@ pub fn view_window<'a>(
|
||||
|
||||
column![
|
||||
row![
|
||||
text("Find:").size(12).width(90),
|
||||
text(t!("Find:")).size(12).width(90),
|
||||
find_input.width(field_width),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
row![
|
||||
text("Replace with:").size(12).width(90),
|
||||
text(t!("Replace with:")).size(12).width(90),
|
||||
replacement_input.width(field_width),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
text("Searches Text, MText, Attribute Definitions, and block attribute values.")
|
||||
text(t!("Searches Text, MText, Attribute Definitions, and block attribute values."))
|
||||
.size(11),
|
||||
text(status).size(11),
|
||||
row![
|
||||
Space::new().width(field_width),
|
||||
button(text("Close").size(12))
|
||||
button(text(t!("Close")).size(12))
|
||||
.on_press(Message::CloseModal)
|
||||
.padding([6, 12])
|
||||
.style(button::secondary),
|
||||
action("Replace", Message::FindReplaceOne).style(button::secondary),
|
||||
action("Replace All", Message::FindReplaceAll).style(button::danger),
|
||||
action("Find Next", Message::FindReplaceNext).style(button::primary),
|
||||
action(t!("Replace"), Message::FindReplaceOne).style(button::secondary),
|
||||
action(t!("Replace All"), Message::FindReplaceAll).style(button::danger),
|
||||
action(t!("Find Next"), Message::FindReplaceNext).style(button::primary),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
|
||||
@@ -7,6 +7,8 @@ use iced::widget::{
|
||||
button, checkbox, column, container, row, scrollable, text, text_input, Space,
|
||||
};
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
use crate::t;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
@@ -15,7 +17,7 @@ struct TransparencyItem(Option<acadrust::types::Transparency>);
|
||||
impl fmt::Display for TransparencyItem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
None => write!(f, "Not set"),
|
||||
None => write!(f, "{}", t!("Not set")),
|
||||
Some(value) => write!(f, "{}%", (value.as_percent() * 100.0).round() as u8),
|
||||
}
|
||||
}
|
||||
@@ -96,7 +98,7 @@ fn mask_summary(mask: LayerStateMask) -> String {
|
||||
];
|
||||
properties
|
||||
.into_iter()
|
||||
.filter_map(|(flag, label)| mask.contains(flag).then_some(label))
|
||||
.filter_map(|(flag, label)| mask.contains(flag).then(|| t!(label)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
@@ -126,7 +128,7 @@ pub fn view_window<'a>(
|
||||
let is_selected =
|
||||
selected.is_some_and(|selected| state.name.eq_ignore_ascii_case(selected));
|
||||
let subtitle = if state.description.is_empty() {
|
||||
format!("{} layers", state.layers.len())
|
||||
t!("%{n} layers", n = state.layers.len()).to_string()
|
||||
} else {
|
||||
state.description.clone()
|
||||
};
|
||||
@@ -148,13 +150,13 @@ pub fn view_window<'a>(
|
||||
let empty: Element<'_, Message> = container(
|
||||
column![
|
||||
text(if states.is_empty() {
|
||||
"No layer states in this drawing"
|
||||
t!("No layer states in this drawing")
|
||||
} else {
|
||||
"No matching layer states"
|
||||
t!("No matching layer states")
|
||||
})
|
||||
.size(11)
|
||||
.style(muted),
|
||||
text("Choose New to capture the current layer settings.")
|
||||
text(t!("Choose New to capture the current layer settings."))
|
||||
.size(10)
|
||||
.style(muted),
|
||||
]
|
||||
@@ -173,7 +175,7 @@ pub fn view_window<'a>(
|
||||
|
||||
let left = container(
|
||||
column![
|
||||
text_input("Search layer states…", filter)
|
||||
text_input(t!("Search layer states…").as_ref(), filter)
|
||||
.on_input(Message::LayerStateManagerFilter)
|
||||
.size(11)
|
||||
.padding([5, 8]),
|
||||
@@ -204,14 +206,14 @@ pub fn view_window<'a>(
|
||||
|
||||
let details = if let Some(state) = selected_state {
|
||||
column![
|
||||
text("Saved state details").size(13),
|
||||
text(t!("Saved state details")).size(13),
|
||||
row![
|
||||
text("Layers").size(10).style(muted).width(92),
|
||||
text(t!("Layers")).size(10).style(muted).width(92),
|
||||
text(state.layers.len().to_string()).size(11),
|
||||
]
|
||||
.spacing(8),
|
||||
row![
|
||||
text("Current layer").size(10).style(muted).width(92),
|
||||
text(t!("Current layer")).size(10).style(muted).width(92),
|
||||
text(if state.current_layer.is_empty() {
|
||||
"—".to_string()
|
||||
} else {
|
||||
@@ -220,14 +222,14 @@ pub fn view_window<'a>(
|
||||
.size(11),
|
||||
]
|
||||
.spacing(8),
|
||||
text("Restored properties").size(10).style(muted),
|
||||
text(t!("Restored properties")).size(10).style(muted),
|
||||
text(mask_summary(state.mask)).size(11),
|
||||
]
|
||||
.spacing(7)
|
||||
} else {
|
||||
column![
|
||||
text("New layer state").size(13),
|
||||
text("Save captures the current settings of every layer in the drawing.")
|
||||
text(t!("New layer state")).size(13),
|
||||
text(t!("Save captures the current settings of every layer in the drawing."))
|
||||
.size(11)
|
||||
.style(muted),
|
||||
]
|
||||
@@ -235,31 +237,31 @@ pub fn view_window<'a>(
|
||||
};
|
||||
|
||||
let restore = if selected_state.is_some() {
|
||||
button(text("Restore").size(11))
|
||||
button(text(t!("Restore")).size(11))
|
||||
.on_press(Message::LayerStateManagerRestore)
|
||||
.style(button_style(true))
|
||||
} else {
|
||||
button(text("Restore").size(11)).style(button_style(true))
|
||||
button(text(t!("Restore")).size(11)).style(button_style(true))
|
||||
};
|
||||
let delete = if selected_state.is_some() {
|
||||
button(text("Delete").size(11))
|
||||
button(text(t!("Delete")).size(11))
|
||||
.on_press(Message::LayerStateManagerDelete)
|
||||
.style(button::danger)
|
||||
} else {
|
||||
button(text("Delete").size(11)).style(button::danger)
|
||||
button(text(t!("Delete")).size(11)).style(button::danger)
|
||||
};
|
||||
let edit = if selected_state.is_some() {
|
||||
button(text("Edit").size(11))
|
||||
button(text(t!("Edit")).size(11))
|
||||
.on_press(Message::LayerStateManagerEdit)
|
||||
.style(button_style(false))
|
||||
} else {
|
||||
button(text("Edit").size(11)).style(button_style(false))
|
||||
button(text(t!("Edit")).size(11)).style(button_style(false))
|
||||
};
|
||||
|
||||
let right = container(
|
||||
column![
|
||||
row![
|
||||
button(text("New").size(11))
|
||||
button(text(t!("New")).size(11))
|
||||
.on_press(Message::LayerStateManagerNew)
|
||||
.style(button_style(false))
|
||||
.padding([5, 12]),
|
||||
@@ -271,27 +273,27 @@ pub fn view_window<'a>(
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
divider(sizing.width),
|
||||
text("Name").size(10).style(muted),
|
||||
text_input("Layer state name", name)
|
||||
text(t!("Name")).size(10).style(muted),
|
||||
text_input(t!("Layer state name").as_ref(), name)
|
||||
.on_input(Message::LayerStateManagerName)
|
||||
.on_submit(Message::LayerStateManagerSave)
|
||||
.size(11)
|
||||
.padding([5, 8]),
|
||||
text("Description").size(10).style(muted),
|
||||
text_input("Optional description", description)
|
||||
text(t!("Description")).size(10).style(muted),
|
||||
text_input(t!("Optional description").as_ref(), description)
|
||||
.on_input(Message::LayerStateManagerDescription)
|
||||
.size(11)
|
||||
.padding([5, 8]),
|
||||
Space::new().height(8),
|
||||
details,
|
||||
Space::new().height(sizing.height),
|
||||
text("Layer states are stored inside the drawing and remain available after reopening it.")
|
||||
text(t!("Layer states are stored inside the drawing and remain available after reopening it."))
|
||||
.size(10)
|
||||
.style(muted),
|
||||
button(text(if selected_state.is_some() {
|
||||
"Update from Drawing"
|
||||
t!("Update from Drawing")
|
||||
} else {
|
||||
"Save New State"
|
||||
t!("Save New State")
|
||||
})
|
||||
.size(11))
|
||||
.on_press(Message::LayerStateManagerSave)
|
||||
@@ -328,7 +330,7 @@ fn mask_for(property: LayerStateProperty) -> LayerStateMask {
|
||||
|
||||
fn mask_button<'a>(
|
||||
state: &LayerState,
|
||||
label: &'a str,
|
||||
label: Cow<'static, str>,
|
||||
property: LayerStateProperty,
|
||||
) -> Element<'a, Message> {
|
||||
let enabled = state.mask.contains(mask_for(property));
|
||||
@@ -365,17 +367,17 @@ fn bool_cell<'a>(
|
||||
fn editor_header<'a>() -> Element<'a, Message> {
|
||||
container(
|
||||
row![
|
||||
text("Layer").size(10).style(muted).width(Length::Fixed(170.0)),
|
||||
text("On").size(10).style(muted).width(Length::Fixed(44.0)),
|
||||
text("Freeze").size(10).style(muted).width(Length::Fixed(54.0)),
|
||||
text("Lock").size(10).style(muted).width(Length::Fixed(44.0)),
|
||||
text("Plot").size(10).style(muted).width(Length::Fixed(44.0)),
|
||||
text("New VP").size(10).style(muted).width(Length::Fixed(54.0)),
|
||||
text("Color").size(10).style(muted).width(Length::Fixed(135.0)),
|
||||
text("Linetype").size(10).style(muted).width(Length::Fixed(150.0)),
|
||||
text("Lineweight").size(10).style(muted).width(Length::Fixed(115.0)),
|
||||
text("Plot style").size(10).style(muted).width(Length::Fixed(135.0)),
|
||||
text("Transparency").size(10).style(muted).width(Length::Fixed(105.0)),
|
||||
text(t!("Layer")).size(10).style(muted).width(Length::Fixed(170.0)),
|
||||
text(t!("On")).size(10).style(muted).width(Length::Fixed(44.0)),
|
||||
text(t!("Freeze")).size(10).style(muted).width(Length::Fixed(54.0)),
|
||||
text(t!("Lock")).size(10).style(muted).width(Length::Fixed(44.0)),
|
||||
text(t!("Plot")).size(10).style(muted).width(Length::Fixed(44.0)),
|
||||
text(t!("New VP")).size(10).style(muted).width(Length::Fixed(54.0)),
|
||||
text(t!("Color")).size(10).style(muted).width(Length::Fixed(135.0)),
|
||||
text(t!("Linetype")).size(10).style(muted).width(Length::Fixed(150.0)),
|
||||
text(t!("Lineweight")).size(10).style(muted).width(Length::Fixed(115.0)),
|
||||
text(t!("Plot style")).size(10).style(muted).width(Length::Fixed(135.0)),
|
||||
text(t!("Transparency")).size(10).style(muted).width(Length::Fixed(105.0)),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center),
|
||||
@@ -455,7 +457,7 @@ fn editor_layer_row<'a>(
|
||||
.text_size(11)
|
||||
.padding([3, 5])
|
||||
.width(Length::Fixed(115.0)),
|
||||
text_input("Default", &layer.plot_style)
|
||||
text_input(t!("Default").as_ref(), &layer.plot_style)
|
||||
.on_input(move |value| Message::LayerStateEditorLayerPlotStyle(index, value))
|
||||
.size(11)
|
||||
.padding([3, 5])
|
||||
@@ -492,16 +494,16 @@ pub fn view_editor<'a>(
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let properties = [
|
||||
("On / Off", LayerStateProperty::On),
|
||||
("Freeze", LayerStateProperty::Frozen),
|
||||
("Lock", LayerStateProperty::Locked),
|
||||
("Plot", LayerStateProperty::Plot),
|
||||
("New VP", LayerStateProperty::NewViewport),
|
||||
("Color", LayerStateProperty::Color),
|
||||
("Linetype", LayerStateProperty::LineType),
|
||||
("Lineweight", LayerStateProperty::LineWeight),
|
||||
("Plot style", LayerStateProperty::PlotStyle),
|
||||
("Transparency", LayerStateProperty::Transparency),
|
||||
(t!("On / Off"), LayerStateProperty::On),
|
||||
(t!("Freeze"), LayerStateProperty::Frozen),
|
||||
(t!("Lock"), LayerStateProperty::Locked),
|
||||
(t!("Plot"), LayerStateProperty::Plot),
|
||||
(t!("New VP"), LayerStateProperty::NewViewport),
|
||||
(t!("Color"), LayerStateProperty::Color),
|
||||
(t!("Linetype"), LayerStateProperty::LineType),
|
||||
(t!("Lineweight"), LayerStateProperty::LineWeight),
|
||||
(t!("Plot style"), LayerStateProperty::PlotStyle),
|
||||
(t!("Transparency"), LayerStateProperty::Transparency),
|
||||
];
|
||||
let mask_controls = properties
|
||||
.into_iter()
|
||||
@@ -534,14 +536,14 @@ pub fn view_editor<'a>(
|
||||
container(
|
||||
column![
|
||||
row![
|
||||
text("Name").size(10).style(muted),
|
||||
text_input("Layer state name", &state.name)
|
||||
text(t!("Name")).size(10).style(muted),
|
||||
text_input(t!("Layer state name").as_ref(), &state.name)
|
||||
.on_input(Message::LayerStateEditorName)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fixed(220.0)),
|
||||
text("Description").size(10).style(muted),
|
||||
text_input("Optional description", &state.description)
|
||||
text(t!("Description")).size(10).style(muted),
|
||||
text_input(t!("Optional description").as_ref(), &state.description)
|
||||
.on_input(Message::LayerStateEditorDescription)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
@@ -550,11 +552,11 @@ pub fn view_editor<'a>(
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
row![
|
||||
text(format!("{} saved layers", state.layers.len()))
|
||||
text(t!("%{n} saved layers", n = state.layers.len()))
|
||||
.size(10)
|
||||
.style(muted),
|
||||
Space::new().width(sizing.width),
|
||||
text("Current layer").size(10).style(muted),
|
||||
text(t!("Current layer")).size(10).style(muted),
|
||||
iced::widget::pick_list(
|
||||
Some(state.current_layer.clone()),
|
||||
layer_names,
|
||||
@@ -568,12 +570,12 @@ pub fn view_editor<'a>(
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
divider(sizing.width),
|
||||
text("Properties restored by this state").size(10).style(muted),
|
||||
text(t!("Properties restored by this state")).size(10).style(muted),
|
||||
mask_controls,
|
||||
row![
|
||||
text("Saved layer values").size(12),
|
||||
text(t!("Saved layer values")).size(12),
|
||||
Space::new().width(sizing.width),
|
||||
text_input("Search layers…", filter)
|
||||
text_input(t!("Search layers…").as_ref(), filter)
|
||||
.on_input(Message::LayerStateEditorFilter)
|
||||
.size(11)
|
||||
.padding([4, 7])
|
||||
@@ -595,15 +597,15 @@ pub fn view_editor<'a>(
|
||||
..Default::default()
|
||||
}),
|
||||
row![
|
||||
text("Changes affect the saved state only; the drawing is unchanged until Restore.")
|
||||
text(t!("Changes affect the saved state only; the drawing is unchanged until Restore."))
|
||||
.size(10)
|
||||
.style(muted),
|
||||
Space::new().width(sizing.width),
|
||||
button(text("Cancel").size(11))
|
||||
button(text(t!("Cancel")).size(11))
|
||||
.on_press(Message::LayerStateEditorCancel)
|
||||
.style(button_style(false))
|
||||
.padding([5, 12]),
|
||||
button(text("Save Changes").size(11))
|
||||
button(text(t!("Save Changes")).size(11))
|
||||
.on_press(Message::LayerStateEditorSave)
|
||||
.style(button_style(true))
|
||||
.padding([5, 12]),
|
||||
|
||||
+20
-18
@@ -13,6 +13,8 @@ use iced::widget::{
|
||||
};
|
||||
use iced::Padding;
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
|
||||
use crate::t;
|
||||
use std::borrow::Cow;
|
||||
|
||||
// ── Per-viewport column descriptor ───────────────────────────────────────────
|
||||
|
||||
@@ -330,22 +332,22 @@ impl LayerPanel {
|
||||
// ── Toolbar ───────────────────────────────────────────────────────
|
||||
let toolbar = container(
|
||||
row![
|
||||
toolbar_btn(crate::ui::icons::PLUS, "New", Message::LayerNew),
|
||||
toolbar_btn(crate::ui::icons::PLUS, t!("New"), Message::LayerNew),
|
||||
toolbar_btn_cond(
|
||||
crate::ui::icons::TRASH,
|
||||
"Delete",
|
||||
t!("Delete"),
|
||||
Message::LayerDelete,
|
||||
has_sel && !sel_is_zero,
|
||||
),
|
||||
toolbar_btn_cond(
|
||||
crate::ui::icons::CHECK,
|
||||
"Set Current",
|
||||
t!("Set Current"),
|
||||
Message::LayerSetCurrent,
|
||||
has_sel,
|
||||
),
|
||||
iced::widget::Space::new().width(sizing.width),
|
||||
// Search box: filters rows by name as the user types (#343).
|
||||
text_input("Search…", &self.filter)
|
||||
text_input(t!("Search…").as_ref(), &self.filter)
|
||||
.on_input(Message::LayerManagerFilterChanged)
|
||||
.size(FONT_SZ)
|
||||
.padding([3, 6])
|
||||
@@ -368,8 +370,8 @@ impl LayerPanel {
|
||||
let sc = self.sort_col;
|
||||
let sa = self.sort_asc;
|
||||
let mut header_row = row![
|
||||
text("Status").size(10).style(muted_style).width(50),
|
||||
sortable_header("Name", LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa),
|
||||
text(t!("Status")).size(10).style(muted_style).width(50),
|
||||
sortable_header(t!("Name"), LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa),
|
||||
// Draggable divider: adjusts the Name column width (#359).
|
||||
iced::widget::mouse_area(
|
||||
container(iced::widget::Space::new().width(2).height(14)).style(
|
||||
@@ -383,14 +385,14 @@ impl LayerPanel {
|
||||
)
|
||||
.on_press(Message::LayerNameColGrab)
|
||||
.interaction(iced::mouse::Interaction::ResizingHorizontally),
|
||||
sortable_header("On", LayerSortCol::On, Length::Fixed(COL_ICON), sc, sa),
|
||||
sortable_header("Freeze", LayerSortCol::Freeze, Length::Fixed(COL_ICON), sc, sa),
|
||||
sortable_header("Lock", LayerSortCol::Lock, Length::Fixed(COL_ICON), sc, sa),
|
||||
sortable_header("Color", LayerSortCol::Color, Length::Fixed(COL_COLOR), sc, sa),
|
||||
sortable_header("Linetype", LayerSortCol::Linetype, Length::Fixed(COL_LT), sc, sa),
|
||||
sortable_header("Lineweight", LayerSortCol::Lineweight, Length::Fixed(COL_LW), sc, sa),
|
||||
sortable_header(t!("On"), LayerSortCol::On, Length::Fixed(COL_ICON), sc, sa),
|
||||
sortable_header(t!("Freeze"), LayerSortCol::Freeze, Length::Fixed(COL_ICON), sc, sa),
|
||||
sortable_header(t!("Lock"), LayerSortCol::Lock, Length::Fixed(COL_ICON), sc, sa),
|
||||
sortable_header(t!("Color"), LayerSortCol::Color, Length::Fixed(COL_COLOR), sc, sa),
|
||||
sortable_header(t!("Linetype"), LayerSortCol::Linetype, Length::Fixed(COL_LT), sc, sa),
|
||||
sortable_header(t!("Lineweight"), LayerSortCol::Lineweight, Length::Fixed(COL_LW), sc, sa),
|
||||
sortable_header(
|
||||
"Transparency",
|
||||
t!("Transparency"),
|
||||
LayerSortCol::Transparency,
|
||||
Length::Fixed(COL_TRANS),
|
||||
sc,
|
||||
@@ -535,7 +537,7 @@ fn color_sort_key(c: AcadColor) -> u32 {
|
||||
/// A clickable column header that sorts the table by `col`. Shows an up/down
|
||||
/// SVG arrow when it is the active sort column (#133).
|
||||
fn sortable_header<'a>(
|
||||
label: &'a str,
|
||||
label: Cow<'static, str>,
|
||||
col: LayerSortCol,
|
||||
width: Length,
|
||||
active: Option<LayerSortCol>,
|
||||
@@ -566,7 +568,7 @@ fn sortable_header<'a>(
|
||||
|
||||
// ── Toolbar buttons ───────────────────────────────────────────────────────
|
||||
|
||||
fn toolbar_btn<'a>(icon: &'static [u8], label: &'a str, msg: Message) -> Element<'a, Message> {
|
||||
fn toolbar_btn<'a>(icon: &'static [u8], label: Cow<'static, str>, msg: Message) -> Element<'a, Message> {
|
||||
button(
|
||||
row![
|
||||
crate::ui::icons::themed(icon, 12.0),
|
||||
@@ -601,7 +603,7 @@ fn toolbar_btn<'a>(icon: &'static [u8], label: &'a str, msg: Message) -> Element
|
||||
|
||||
fn toolbar_btn_cond<'a>(
|
||||
icon: &'static [u8],
|
||||
label: &'a str,
|
||||
label: Cow<'static, str>,
|
||||
msg: Message,
|
||||
enabled: bool,
|
||||
) -> Element<'a, Message> {
|
||||
@@ -791,7 +793,7 @@ fn layer_row<'a>(
|
||||
let lt_cell: Element<'_, Message> = if let Some(state) = lt_combo {
|
||||
combo_box(
|
||||
state,
|
||||
"linetype",
|
||||
t!("linetype").as_ref(),
|
||||
Some(&cur_lt_item),
|
||||
|item: LinetypeItem| Message::LayerLinetypeSet(item.name),
|
||||
)
|
||||
@@ -816,7 +818,7 @@ fn layer_row<'a>(
|
||||
// Lineweight cell
|
||||
let cur_lw_item = LwItem(layer.lineweight);
|
||||
let lw_cell: Element<'_, Message> = if let Some(state) = lw_combo_state {
|
||||
combo_box(state, "lineweight", Some(&cur_lw_item), |item: LwItem| {
|
||||
combo_box(state, t!("lineweight").as_ref(), Some(&cur_lw_item), |item: LwItem| {
|
||||
Message::LayerLineweightSet(item.0)
|
||||
})
|
||||
.size(FONT_SZ)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use crate::app::Message;
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||
use iced::{Background, Element, Theme};
|
||||
use crate::t;
|
||||
|
||||
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
|
||||
iced::widget::text::Style {
|
||||
@@ -74,11 +75,11 @@ pub fn view_window<'a>(
|
||||
// ── Toolbar ───────────────────────────────────────────────────────────
|
||||
let toolbar = container(
|
||||
row![
|
||||
button(text("New Layout").size(11))
|
||||
button(text(t!("New Layout")).size(11))
|
||||
.on_press(Message::LayoutManagerNew)
|
||||
.style(btn_s(false))
|
||||
.padding([4, 10]),
|
||||
button(text("Delete").size(11))
|
||||
button(text(t!("Delete")).size(11))
|
||||
.on_press(Message::LayoutManagerDelete)
|
||||
.style(move |theme: &Theme, status| {
|
||||
if is_model {
|
||||
@@ -92,7 +93,7 @@ pub fn view_window<'a>(
|
||||
button(
|
||||
row![
|
||||
crate::ui::icons::themed_arrow_left(9.0),
|
||||
text("Move Left").size(11),
|
||||
text(t!("Move Left")).size(11),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center),
|
||||
@@ -102,7 +103,7 @@ pub fn view_window<'a>(
|
||||
.padding([4, 8]),
|
||||
button(
|
||||
row![
|
||||
text("Move Right").size(11),
|
||||
text(t!("Move Right")).size(11),
|
||||
crate::ui::icons::themed_arrow_right(9.0),
|
||||
]
|
||||
.spacing(4)
|
||||
@@ -111,7 +112,7 @@ pub fn view_window<'a>(
|
||||
.on_press(Message::LayoutManagerMoveRight)
|
||||
.style(btn_s(false))
|
||||
.padding([4, 8]),
|
||||
button(text("Set Current").size(11))
|
||||
button(text(t!("Set Current")).size(11))
|
||||
.on_press(Message::LayoutManagerSetCurrent)
|
||||
.style(btn_s(true))
|
||||
.padding([4, 10]),
|
||||
@@ -151,7 +152,7 @@ pub fn view_window<'a>(
|
||||
|
||||
let layout_list = container(
|
||||
column![
|
||||
text("Layouts").size(10).style(muted_style),
|
||||
text(t!("Layouts")).size(10).style(muted_style),
|
||||
container(scrollable(column(list_items).spacing(2)).height(sizing.height))
|
||||
.style(container::bordered_box)
|
||||
.width(sizing.width)
|
||||
@@ -174,24 +175,24 @@ pub fn view_window<'a>(
|
||||
let details = container(
|
||||
column![
|
||||
text(if is_model {
|
||||
"Model Space"
|
||||
t!("Model Space")
|
||||
} else {
|
||||
"Paper Space Layout"
|
||||
t!("Paper Space Layout")
|
||||
})
|
||||
.size(13),
|
||||
Space::new().height(8),
|
||||
row![
|
||||
text("Name:").size(11).style(muted_style).width(80),
|
||||
text(t!("Name:")).size(11).style(muted_style).width(80),
|
||||
text(selected).size(11),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
row![
|
||||
text("Status:").size(11).style(muted_style).width(80),
|
||||
text(t!("Status:")).size(11).style(muted_style).width(80),
|
||||
text(if selected == current.as_str() {
|
||||
"Active"
|
||||
t!("Active")
|
||||
} else {
|
||||
"Inactive"
|
||||
t!("Inactive")
|
||||
})
|
||||
.size(11)
|
||||
.style(move |theme: &Theme| {
|
||||
@@ -205,14 +206,14 @@ pub fn view_window<'a>(
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
Space::new().height(16),
|
||||
text("Rename").size(10).style(muted_style),
|
||||
text(t!("Rename")).size(10).style(muted_style),
|
||||
row![
|
||||
text_input("New name…", rename_buf)
|
||||
text_input(t!("New name…").as_ref(), rename_buf)
|
||||
.on_input(Message::LayoutManagerRenameBuf)
|
||||
.on_submit(Message::LayoutManagerRenameCommit)
|
||||
.size(11)
|
||||
.padding([4, 8]),
|
||||
button(text("OK").size(11))
|
||||
button(text(t!("OK")).size(11))
|
||||
.on_press(Message::LayoutManagerRenameCommit)
|
||||
.style(btn_s(true))
|
||||
.padding([4, 10]),
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
use iced::time::Instant;
|
||||
use iced::widget::{button, column, container, row, stack, text, Space};
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
use crate::t;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::app::{
|
||||
@@ -17,14 +19,14 @@ const CARD_WIDTH: f32 = 420.0;
|
||||
const BAR_TRACK_WIDTH: f32 = 380.0;
|
||||
const BAR_TRACK_HEIGHT: f32 = 6.0;
|
||||
|
||||
fn phase_label(phase: u8) -> &'static str {
|
||||
fn phase_label(phase: u8) -> Cow<'static, str> {
|
||||
match phase {
|
||||
OPEN_PHASE_READING => "Reading file…",
|
||||
OPEN_PHASE_PARSING => "Parsing entities…",
|
||||
OPEN_PHASE_XREF => "Loading references…",
|
||||
OPEN_PHASE_CACHING => "Building scene caches…",
|
||||
OPEN_PHASE_FINALIZING => "Finalizing…",
|
||||
_ => "Working…",
|
||||
OPEN_PHASE_READING => t!("Reading file…"),
|
||||
OPEN_PHASE_PARSING => t!("Parsing entities…"),
|
||||
OPEN_PHASE_XREF => t!("Loading references…"),
|
||||
OPEN_PHASE_CACHING => t!("Building scene caches…"),
|
||||
OPEN_PHASE_FINALIZING => t!("Finalizing…"),
|
||||
_ => t!("Working…"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +103,7 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
|
||||
.into();
|
||||
|
||||
// ── Card body ────────────────────────────────────────────────────────
|
||||
let title = text("Opening file").size(15);
|
||||
let title = text(t!("Opening file")).size(15);
|
||||
|
||||
let name_line = text(format!(
|
||||
"{} ({})",
|
||||
@@ -123,7 +125,7 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
|
||||
color: Some(theme.palette().primary.base.color),
|
||||
});
|
||||
|
||||
let cancel_btn: Element<'_, Message> = button(text("Cancel").size(12))
|
||||
let cancel_btn: Element<'_, Message> = button(text(t!("Cancel")).size(12))
|
||||
.on_press(Message::OpenCancel)
|
||||
.style(button::danger)
|
||||
.padding([4, 14])
|
||||
|
||||
@@ -4,6 +4,22 @@ use iced::widget::{
|
||||
button, column, container, row, scrollable, text, text_input, Space,
|
||||
};
|
||||
use iced::{Background, Border, Element, Theme};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ThemeChoice(String);
|
||||
|
||||
impl fmt::Display for ThemeChoice {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self.0.as_str() {
|
||||
"Light" => crate::t!("Light"),
|
||||
"Dark" => crate::t!("Dark"),
|
||||
"Custom" => crate::t!("Custom"),
|
||||
_ => std::borrow::Cow::Borrowed(self.0.as_str()),
|
||||
};
|
||||
f.write_str(label.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_window<'a>(
|
||||
default_save_format: &'a str,
|
||||
@@ -21,8 +37,9 @@ pub fn view_window<'a>(
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.chain(std::iter::once("Custom".to_string()))
|
||||
.map(ThemeChoice)
|
||||
.collect::<Vec<_>>();
|
||||
let selected_theme = Some(ui_theme.name.clone());
|
||||
let selected_theme = Some(ThemeChoice(ui_theme.name.clone()));
|
||||
|
||||
let palette = ui_theme.palette.to_iced();
|
||||
let colors = [
|
||||
@@ -110,7 +127,7 @@ pub fn view_window<'a>(
|
||||
theme_options,
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.on_select(Message::OptionsThemeChanged)
|
||||
.on_select(|choice| Message::OptionsThemeChanged(choice.0))
|
||||
.width(sizing.width),
|
||||
]
|
||||
.spacing(12)
|
||||
|
||||
+139
-83
@@ -11,6 +11,9 @@ use iced::widget::{
|
||||
Space,
|
||||
};
|
||||
use iced::{Background, Border, Element, Fit, Length, Theme};
|
||||
use crate::t;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
/// Sentinel entries in the printer dropdown (not real printer names).
|
||||
pub const OUT_DEFAULT: &str = "System default printer";
|
||||
@@ -22,6 +25,35 @@ pub const SETUP_NONE: &str = "<none>";
|
||||
pub const SETUP_PREV: &str = "<previous>";
|
||||
pub const STYLE_NONE: &str = "<none>";
|
||||
|
||||
/// A plot dropdown value keeps its persisted/raw value separate from the
|
||||
/// localized label shown by Iced. Printer names, scale names, paper sizes, and
|
||||
/// style-table file names remain verbatim; built-in choices use the catalog.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct PlotChoice {
|
||||
raw: String,
|
||||
localized: bool,
|
||||
}
|
||||
|
||||
impl PlotChoice {
|
||||
fn raw(value: impl Into<String>) -> Self {
|
||||
Self { raw: value.into(), localized: false }
|
||||
}
|
||||
|
||||
fn localized(value: impl Into<String>) -> Self {
|
||||
Self { raw: value.into(), localized: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PlotChoice {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.localized {
|
||||
formatter.write_str(crate::i18n::translate(&self.raw).as_ref())
|
||||
} else {
|
||||
formatter.write_str(&self.raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One of the many boolean plot options (folded into a single message so the
|
||||
/// dialog needn't carry a variant per checkbox).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -280,7 +312,7 @@ fn hdivider<'a>(width: Length) -> Element<'a, Message> {
|
||||
.into()
|
||||
}
|
||||
|
||||
fn section_label<'a>(s: &'static str) -> Element<'a, Message> {
|
||||
fn section_label<'a>(s: Cow<'static, str>) -> Element<'a, Message> {
|
||||
text(s).size(11).style(muted_style).into()
|
||||
}
|
||||
|
||||
@@ -314,7 +346,12 @@ fn setup_row<'a>(
|
||||
.into();
|
||||
}
|
||||
let is_selected = name == selected;
|
||||
let cell = container(text(name.to_string()).size(11))
|
||||
let display_name = if name == SETUP_NONE || name == SETUP_PREV {
|
||||
crate::i18n::translate(name)
|
||||
} else {
|
||||
Cow::Owned(name.to_string())
|
||||
};
|
||||
let cell = container(text(display_name).size(11))
|
||||
.padding([4, 8])
|
||||
.width(Fit)
|
||||
.style(move |theme: &Theme| {
|
||||
@@ -334,14 +371,14 @@ fn setup_row<'a>(
|
||||
/// A `label : dropdown` row. `ctor` turns the picked string into a dialog
|
||||
/// message.
|
||||
fn drop_row<'a>(
|
||||
label: &'a str,
|
||||
options: Vec<String>,
|
||||
selected: Option<String>,
|
||||
label: Cow<'static, str>,
|
||||
options: Vec<PlotChoice>,
|
||||
selected: Option<PlotChoice>,
|
||||
ctor: fn(String) -> PlotDlgMsg,
|
||||
width: Length,
|
||||
) -> Element<'a, Message> {
|
||||
let pl = iced::widget::pick_list(selected, options, |value| value.to_string())
|
||||
.on_select(move |s| Message::PlotDlg(ctor(s)))
|
||||
.on_select(move |choice| Message::PlotDlg(ctor(choice.raw)))
|
||||
.text_size(12)
|
||||
.padding([3, 6])
|
||||
.width(width);
|
||||
@@ -352,9 +389,9 @@ fn drop_row<'a>(
|
||||
}
|
||||
|
||||
fn drop_row_enabled<'a>(
|
||||
label: &'a str,
|
||||
options: Vec<String>,
|
||||
selected: Option<String>,
|
||||
label: Cow<'static, str>,
|
||||
options: Vec<PlotChoice>,
|
||||
selected: Option<PlotChoice>,
|
||||
ctor: fn(String) -> PlotDlgMsg,
|
||||
width: Length,
|
||||
enabled: bool,
|
||||
@@ -364,7 +401,7 @@ fn drop_row_enabled<'a>(
|
||||
}
|
||||
row![
|
||||
text(label).size(11).style(muted_style).width(92),
|
||||
container(text(selected.unwrap_or_default()).size(12).style(muted_style))
|
||||
container(text(selected.map(|choice| choice.to_string()).unwrap_or_default()).size(12).style(muted_style))
|
||||
.padding([4, 7])
|
||||
.width(width),
|
||||
]
|
||||
@@ -375,7 +412,7 @@ fn drop_row_enabled<'a>(
|
||||
|
||||
/// A `label : text field` row.
|
||||
fn field_row<'a>(
|
||||
label: &'a str,
|
||||
label: Cow<'static, str>,
|
||||
value: &'a str,
|
||||
ctor: fn(String) -> PlotDlgMsg,
|
||||
width: u16,
|
||||
@@ -394,7 +431,7 @@ fn field_row<'a>(
|
||||
}
|
||||
|
||||
fn field_row_enabled<'a>(
|
||||
label: &'a str,
|
||||
label: Cow<'static, str>,
|
||||
value: &'a str,
|
||||
ctor: fn(String) -> PlotDlgMsg,
|
||||
width: u16,
|
||||
@@ -415,7 +452,7 @@ fn field_row_enabled<'a>(
|
||||
}
|
||||
|
||||
/// A single option checkbox bound to a `PlotFlag`.
|
||||
fn check<'a>(label: &'a str, on: bool, flag: PlotFlag) -> Element<'a, Message> {
|
||||
fn check<'a>(label: Cow<'static, str>, on: bool, flag: PlotFlag) -> Element<'a, Message> {
|
||||
checkbox(on)
|
||||
.label(label)
|
||||
.on_toggle(move |_| Message::PlotDlg(PlotDlgMsg::Flag(flag)))
|
||||
@@ -425,7 +462,7 @@ fn check<'a>(label: &'a str, on: bool, flag: PlotFlag) -> Element<'a, Message> {
|
||||
}
|
||||
|
||||
fn check_enabled<'a>(
|
||||
label: &'a str,
|
||||
label: Cow<'static, str>,
|
||||
on: bool,
|
||||
flag: PlotFlag,
|
||||
enabled: bool,
|
||||
@@ -447,8 +484,8 @@ fn panel<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||
.into()
|
||||
}
|
||||
|
||||
fn strs(items: &[&str]) -> Vec<String> {
|
||||
items.iter().map(|s| s.to_string()).collect()
|
||||
fn choices(items: &[&str]) -> Vec<PlotChoice> {
|
||||
items.iter().map(|&value| PlotChoice::localized(value)).collect()
|
||||
}
|
||||
|
||||
pub fn view_window(
|
||||
@@ -457,7 +494,7 @@ pub fn view_window(
|
||||
) -> Element<'_, Message> {
|
||||
let width = sizing.width;
|
||||
let height = sizing.height;
|
||||
let action = if s.to_file { "Export PDF" } else { "Print" };
|
||||
let action = if s.to_file { t!("Export PDF") } else { t!("Print") };
|
||||
let is_special = s.selected_setup == SETUP_NONE || s.selected_setup == SETUP_PREV;
|
||||
let sel_is_layout = s.selected_setup.len() >= 2
|
||||
&& s.selected_setup.starts_with('*')
|
||||
@@ -478,7 +515,7 @@ pub fn view_window(
|
||||
.map(|name| setup_row(name, &s.selected_setup, renaming, rename_buf))
|
||||
.collect();
|
||||
let list_body: Element<'_, Message> = if rows.is_empty() {
|
||||
container(text("(no page setups)").size(11).style(muted_style))
|
||||
container(text(t!("(no page setups)")).size(11).style(muted_style))
|
||||
.padding([6, 8])
|
||||
.into()
|
||||
} else {
|
||||
@@ -494,7 +531,7 @@ pub fn view_window(
|
||||
.size(11)
|
||||
.padding([4, 8])
|
||||
.width(Length::Fill),
|
||||
button(text("Save").size(11))
|
||||
button(text(t!("Save")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::NameCommit))
|
||||
.style(btn(true))
|
||||
.padding([4, 8]),
|
||||
@@ -510,7 +547,7 @@ pub fn view_window(
|
||||
};
|
||||
let list_panel = container(
|
||||
column![
|
||||
text("Page setups").size(10).style(muted_style),
|
||||
text(t!("Page setups")).size(10).style(muted_style),
|
||||
new_name,
|
||||
container(list_body)
|
||||
.style(|theme: &Theme| {
|
||||
@@ -541,20 +578,20 @@ pub fn view_window(
|
||||
left: 12.0,
|
||||
});
|
||||
|
||||
let mut copy_button = button(text("Copy").size(11))
|
||||
let mut copy_button = button(text(t!("Copy")).size(11))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]);
|
||||
if can_copy {
|
||||
copy_button = copy_button.on_press(Message::PlotDlg(PlotDlgMsg::CopySetup));
|
||||
}
|
||||
let mut delete_button = button(text("Delete").size(11))
|
||||
let mut delete_button = button(text(t!("Delete")).size(11))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]);
|
||||
if is_named {
|
||||
delete_button = delete_button.on_press(Message::PlotDlg(PlotDlgMsg::DeleteSetup));
|
||||
}
|
||||
let left_bar = row![
|
||||
button(text("New").size(11))
|
||||
button(text(t!("New")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::NewSetup))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]),
|
||||
@@ -564,17 +601,20 @@ pub fn view_window(
|
||||
.spacing(4);
|
||||
|
||||
// ── Printer / plotter ─────────────────────────────────────────────────
|
||||
let mut printer_opts = vec![OUT_DEFAULT.to_string()];
|
||||
printer_opts.extend(s.printers.iter().cloned());
|
||||
printer_opts.push(OUT_PDF.to_string());
|
||||
let mut printer_opts = vec![PlotChoice::localized(OUT_DEFAULT)];
|
||||
printer_opts.extend(s.printers.iter().cloned().map(PlotChoice::raw));
|
||||
printer_opts.push(PlotChoice::localized(OUT_PDF));
|
||||
let printer_sel = if s.to_file {
|
||||
Some(OUT_PDF.to_string())
|
||||
Some(PlotChoice::localized(OUT_PDF))
|
||||
} else {
|
||||
Some(s.printer.clone().unwrap_or_else(|| OUT_DEFAULT.to_string()))
|
||||
Some(match &s.printer {
|
||||
Some(printer) => PlotChoice::raw(printer.clone()),
|
||||
None => PlotChoice::localized(OUT_DEFAULT),
|
||||
})
|
||||
};
|
||||
let paper_opts: Vec<String> = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect();
|
||||
let paper_note: Element<'_, Message> = if s.area == "Layout" {
|
||||
text("Layout plots the current sheet using the selected paper size.")
|
||||
text(t!("Layout plots the current sheet using the selected paper size."))
|
||||
.size(10)
|
||||
.style(muted_style)
|
||||
.width(width)
|
||||
@@ -583,9 +623,9 @@ pub fn view_window(
|
||||
Space::new().height(0).into()
|
||||
};
|
||||
let mut output_row = row![
|
||||
text("Output").size(11).style(muted_style).width(92),
|
||||
text(t!("Output")).size(11).style(muted_style).width(92),
|
||||
iced::widget::pick_list(printer_sel, printer_opts, |value| value.to_string())
|
||||
.on_select(|value| Message::PlotDlg(PlotDlgMsg::Printer(value)))
|
||||
.on_select(|choice| Message::PlotDlg(PlotDlgMsg::Printer(choice.raw)))
|
||||
.text_size(12)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fill),
|
||||
@@ -594,7 +634,7 @@ pub fn view_window(
|
||||
.align_y(iced::Center);
|
||||
if !s.to_file {
|
||||
output_row = output_row.push(
|
||||
button(text("Properties…").size(11))
|
||||
button(text(t!("Properties…")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::PrinterProperties))
|
||||
.style(btn(false))
|
||||
.padding([4, 8]),
|
||||
@@ -603,11 +643,11 @@ pub fn view_window(
|
||||
let copies_row: Element<'_, Message> = if s.to_file {
|
||||
Space::new().height(0).into()
|
||||
} else {
|
||||
field_row("Copies", &s.copies, PlotDlgMsg::Copies, 60)
|
||||
field_row(t!("Copies"), &s.copies, PlotDlgMsg::Copies, 60)
|
||||
};
|
||||
let printer_panel = panel(
|
||||
column![
|
||||
section_label("Printer / plotter"),
|
||||
section_label(t!("Printer / plotter")),
|
||||
output_row,
|
||||
copies_row,
|
||||
]
|
||||
@@ -616,19 +656,29 @@ pub fn view_window(
|
||||
|
||||
// ── Paper, area, offset, scale ────────────────────────────────────────
|
||||
let paper_panel = panel(column![
|
||||
section_label("Paper"),
|
||||
drop_row("Size", paper_opts, Some(s.paper.clone()), PlotDlgMsg::Paper, width),
|
||||
section_label(t!("Paper")),
|
||||
drop_row(
|
||||
t!("Size"),
|
||||
paper_opts.into_iter().map(PlotChoice::raw).collect(),
|
||||
Some(PlotChoice::raw(s.paper.clone())),
|
||||
PlotDlgMsg::Paper,
|
||||
width,
|
||||
),
|
||||
paper_note,
|
||||
].spacing(7));
|
||||
|
||||
let mut area_options = strs(&["Extents", "Display", "Window"]);
|
||||
let mut area_options = choices(&["Extents", "Display", "Window"]);
|
||||
if s.paper_space {
|
||||
area_options.insert(0, "Layout".to_string());
|
||||
area_options.insert(0, PlotChoice::localized("Layout"));
|
||||
}
|
||||
let mut area_row = row![
|
||||
text("What to plot").size(11).style(muted_style).width(92),
|
||||
iced::widget::pick_list(Some(s.area.clone()), area_options, |value| value.to_string())
|
||||
.on_select(|value| Message::PlotDlg(PlotDlgMsg::Area(value)))
|
||||
text(t!("What to plot")).size(11).style(muted_style).width(92),
|
||||
iced::widget::pick_list(
|
||||
Some(PlotChoice::localized(s.area.clone())),
|
||||
area_options,
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.on_select(|choice| Message::PlotDlg(PlotDlgMsg::Area(choice.raw)))
|
||||
.text_size(12)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fill),
|
||||
@@ -637,7 +687,7 @@ pub fn view_window(
|
||||
.align_y(iced::Center);
|
||||
if s.area == "Window" {
|
||||
area_row = area_row.push(
|
||||
button(text("Pick…").size(11))
|
||||
button(text(t!("Pick…")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::PickWindow))
|
||||
.style(btn(false))
|
||||
.padding([4, 10]),
|
||||
@@ -645,35 +695,41 @@ pub fn view_window(
|
||||
}
|
||||
let common_area = s.area != "Layout";
|
||||
let area_panel = panel(column![
|
||||
section_label("Plot area"),
|
||||
section_label(t!("Plot area")),
|
||||
area_row,
|
||||
section_label("Plot offset"),
|
||||
section_label(t!("Plot offset")),
|
||||
column![
|
||||
field_row_enabled("X (mm)", &s.offset_x, PlotDlgMsg::OffsetX, 70, common_area && !s.center),
|
||||
field_row_enabled("Y (mm)", &s.offset_y, PlotDlgMsg::OffsetY, 70, common_area && !s.center),
|
||||
field_row_enabled(t!("X (mm)"), &s.offset_x, PlotDlgMsg::OffsetX, 70, common_area && !s.center),
|
||||
field_row_enabled(t!("Y (mm)"), &s.offset_y, PlotDlgMsg::OffsetY, 70, common_area && !s.center),
|
||||
]
|
||||
.spacing(7),
|
||||
check_enabled("Center the plot", s.center, PlotFlag::Center, common_area),
|
||||
check_enabled(t!("Center the plot"), s.center, PlotFlag::Center, common_area),
|
||||
].spacing(7));
|
||||
let scale_options = s.scales.iter().map(|(name, _)| name.clone()).collect();
|
||||
let scale_options = s
|
||||
.scales
|
||||
.iter()
|
||||
.map(|(name, _)| PlotChoice::raw(name.clone()))
|
||||
.collect();
|
||||
let scale_panel = panel(column![
|
||||
section_label("Plot scale"),
|
||||
section_label(t!("Plot scale")),
|
||||
check_enabled(
|
||||
"Fit to paper",
|
||||
t!("Fit to paper"),
|
||||
s.fit_to_paper,
|
||||
PlotFlag::FitToPaper,
|
||||
common_area,
|
||||
),
|
||||
// "Scale" intentionally stays untranslated: it would clash with the
|
||||
// ribbon's zoom tool label under the same lookup key.
|
||||
drop_row_enabled(
|
||||
"Scale",
|
||||
Cow::Borrowed("Scale"),
|
||||
scale_options,
|
||||
Some(s.scale.clone()),
|
||||
Some(PlotChoice::raw(s.scale.clone())),
|
||||
PlotDlgMsg::Scale,
|
||||
width,
|
||||
common_area && !s.fit_to_paper,
|
||||
),
|
||||
check_enabled(
|
||||
"Scale lineweights",
|
||||
t!("Scale lineweights"),
|
||||
s.scale_lw && !s.fit_to_paper,
|
||||
PlotFlag::ScaleLw,
|
||||
!s.fit_to_paper,
|
||||
@@ -681,35 +737,35 @@ pub fn view_window(
|
||||
].spacing(7));
|
||||
|
||||
// ── Style and shaded viewport settings ───────────────────────────────
|
||||
let mut style_options = vec![STYLE_NONE.to_string()];
|
||||
style_options.extend(s.plot_styles.iter().cloned());
|
||||
let mut style_options = vec![PlotChoice::localized(STYLE_NONE)];
|
||||
style_options.extend(s.plot_styles.iter().cloned().map(PlotChoice::raw));
|
||||
if !s.style_name.is_empty()
|
||||
&& !style_options
|
||||
.iter()
|
||||
.any(|name| name.eq_ignore_ascii_case(&s.style_name))
|
||||
.any(|choice| choice.raw.eq_ignore_ascii_case(&s.style_name))
|
||||
{
|
||||
style_options.push(s.style_name.clone());
|
||||
style_options.push(PlotChoice::raw(s.style_name.clone()));
|
||||
}
|
||||
let style_selected = if s.style_name.is_empty() {
|
||||
STYLE_NONE.to_string()
|
||||
PlotChoice::localized(STYLE_NONE)
|
||||
} else {
|
||||
s.style_name.clone()
|
||||
PlotChoice::raw(s.style_name.clone())
|
||||
};
|
||||
let style_panel = panel(column![
|
||||
section_label("Plot style table (pen assignments)"),
|
||||
section_label(t!("Plot style table (pen assignments)")),
|
||||
drop_row(
|
||||
"Table",
|
||||
t!("Table"),
|
||||
style_options,
|
||||
Some(style_selected),
|
||||
PlotDlgMsg::Style,
|
||||
width,
|
||||
),
|
||||
row![
|
||||
button(text("Load…").size(11))
|
||||
button(text(t!("Load…")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::LoadStyle))
|
||||
.style(btn(false))
|
||||
.padding([4, 10]),
|
||||
button(text("Save…").size(11))
|
||||
button(text(t!("Save…")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::SaveStyle))
|
||||
.style(btn(false))
|
||||
.padding([4, 10]),
|
||||
@@ -719,10 +775,10 @@ pub fn view_window(
|
||||
].spacing(7));
|
||||
|
||||
let shaded_panel = panel(column![
|
||||
section_label("Shaded viewport options"),
|
||||
section_label(t!("Shaded viewport options")),
|
||||
drop_row(
|
||||
"Shade plot",
|
||||
strs(&[
|
||||
t!("Shade plot"),
|
||||
choices(&[
|
||||
"As displayed",
|
||||
"2D Wireframe",
|
||||
"3D Wireframe",
|
||||
@@ -732,14 +788,14 @@ pub fn view_window(
|
||||
"Flat Shaded + Edges",
|
||||
"Gouraud Shaded + Edges",
|
||||
]),
|
||||
Some(s.shade.clone()),
|
||||
Some(PlotChoice::localized(s.shade.clone())),
|
||||
PlotDlgMsg::Shade,
|
||||
width,
|
||||
),
|
||||
drop_row(
|
||||
"Quality",
|
||||
strs(&["Low", "Normal", "High"]),
|
||||
Some(s.quality.clone()),
|
||||
t!("Quality"),
|
||||
choices(&["Low", "Normal", "High"]),
|
||||
Some(PlotChoice::localized(s.quality.clone())),
|
||||
PlotDlgMsg::Quality,
|
||||
width,
|
||||
),
|
||||
@@ -748,7 +804,7 @@ pub fn view_window(
|
||||
// ── Output options and orientation ────────────────────────────────────
|
||||
let paper_order_option: Element<'_, Message> = if s.paper_space {
|
||||
check(
|
||||
"Paper space last",
|
||||
t!("Paper space last"),
|
||||
s.paperspace_last,
|
||||
PlotFlag::PaperspaceLast,
|
||||
)
|
||||
@@ -756,19 +812,19 @@ pub fn view_window(
|
||||
Space::new().height(0).into()
|
||||
};
|
||||
let options_panel = panel(column![
|
||||
section_label("Plot options"),
|
||||
section_label(t!("Plot options")),
|
||||
row![
|
||||
column![
|
||||
check("Plot in background", s.background, PlotFlag::Background),
|
||||
check("Object lineweights", s.lineweights, PlotFlag::Lineweights),
|
||||
check("Plot transparency", s.transparency, PlotFlag::Transparency),
|
||||
check(t!("Plot in background"), s.background, PlotFlag::Background),
|
||||
check(t!("Object lineweights"), s.lineweights, PlotFlag::Lineweights),
|
||||
check(t!("Plot transparency"), s.transparency, PlotFlag::Transparency),
|
||||
]
|
||||
.spacing(6)
|
||||
.width(width),
|
||||
column![
|
||||
paper_order_option,
|
||||
check("Merge overlapping lines", s.merge_lines, PlotFlag::MergeLines),
|
||||
check("Plot stamp", s.stamp, PlotFlag::Stamp),
|
||||
check(t!("Merge overlapping lines"), s.merge_lines, PlotFlag::MergeLines),
|
||||
check(t!("Plot stamp"), s.stamp, PlotFlag::Stamp),
|
||||
]
|
||||
.spacing(6)
|
||||
.width(width),
|
||||
@@ -778,13 +834,13 @@ pub fn view_window(
|
||||
|
||||
let orientation_panel = panel(column![
|
||||
drop_row(
|
||||
"Orientation",
|
||||
strs(&["Portrait", "Landscape"]),
|
||||
Some(s.orientation.clone()),
|
||||
t!("Orientation"),
|
||||
choices(&["Portrait", "Landscape"]),
|
||||
Some(PlotChoice::localized(s.orientation.clone())),
|
||||
PlotDlgMsg::Orientation,
|
||||
width,
|
||||
),
|
||||
check("Plot upside-down", s.upside_down, PlotFlag::UpsideDown),
|
||||
check(t!("Plot upside-down"), s.upside_down, PlotFlag::UpsideDown),
|
||||
].spacing(7));
|
||||
|
||||
let left = column![
|
||||
@@ -820,12 +876,12 @@ pub fn view_window(
|
||||
row![
|
||||
left_bar,
|
||||
Space::new().width(width),
|
||||
button(text("Set current").size(11))
|
||||
button(text(t!("Set current")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::SetCurrent))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]),
|
||||
Space::new().width(6),
|
||||
button(text("Preview").size(11))
|
||||
button(text(t!("Preview")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::Preview))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]),
|
||||
|
||||
@@ -12,7 +12,9 @@ use iced::widget::{
|
||||
button, column, container, markdown, row, rule, scrollable, text, text_input, Space,
|
||||
};
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
use crate::t;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Empty lane kept at the right edge of scroll content so the vertical
|
||||
/// scrollbar never covers card controls.
|
||||
@@ -79,9 +81,9 @@ fn badge<'a>(label: String) -> Element<'a, Message> {
|
||||
fn toggle_button<'a>(id: &str, disabled: bool) -> Element<'a, Message> {
|
||||
// Label shows the action the click performs.
|
||||
let label = if disabled {
|
||||
"Enable"
|
||||
t!("Enable")
|
||||
} else {
|
||||
"Disable"
|
||||
t!("Disable")
|
||||
};
|
||||
let want_enabled = disabled; // clicking flips the state
|
||||
let id_owned = id.to_string();
|
||||
@@ -101,8 +103,8 @@ enum StatusKind {
|
||||
}
|
||||
|
||||
/// Coloured status pill for a discovered external package.
|
||||
fn status_badge<'a>(label: &str, kind: StatusKind) -> Element<'a, Message> {
|
||||
container(text(label.to_string()).size(11))
|
||||
fn status_badge<'a>(label: Cow<'static, str>, kind: StatusKind) -> Element<'a, Message> {
|
||||
container(text(label).size(11))
|
||||
.padding([2, 8])
|
||||
.style(move |theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
@@ -219,17 +221,17 @@ fn external_card<'a>(
|
||||
let failed_old_api =
|
||||
load_error.is_some() && p.api_version != ocs_plugin_api::API_VERSION;
|
||||
let (status, kind) = if loaded && disabled {
|
||||
("Disabled", StatusKind::Muted)
|
||||
(t!("Disabled"), StatusKind::Muted)
|
||||
} else if loaded {
|
||||
("Loaded", StatusKind::Success)
|
||||
(t!("Loaded"), StatusKind::Success)
|
||||
} else if !p.api_compatible() || failed_old_api {
|
||||
("API incompatible", StatusKind::Danger)
|
||||
(t!("API incompatible"), StatusKind::Danger)
|
||||
} else if load_error.is_some() {
|
||||
("Load failed", StatusKind::Danger)
|
||||
(t!("Load failed"), StatusKind::Danger)
|
||||
} else if !p.lib_present {
|
||||
("No library", StatusKind::Warning)
|
||||
(t!("No library"), StatusKind::Warning)
|
||||
} else {
|
||||
("Restart to load", StatusKind::Warning)
|
||||
(t!("Restart to load"), StatusKind::Warning)
|
||||
};
|
||||
let header = row![
|
||||
text(p.name.clone()).size(15),
|
||||
@@ -249,21 +251,23 @@ fn external_card<'a>(
|
||||
}
|
||||
if !p.command_prefixes.is_empty() {
|
||||
info_body = info_body.push(
|
||||
text(format!("Commands: {}", p.command_prefixes.join(", ")))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
text(t!(
|
||||
"Commands: %{cmds}",
|
||||
cmds = p.command_prefixes.join(", ")
|
||||
))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
);
|
||||
}
|
||||
if let Some(error) = load_error {
|
||||
let detail = if failed_old_api {
|
||||
format!(
|
||||
"Plugin API {} is incompatible with this build's API {} ribbon ABI. \
|
||||
Update or rebuild the plugin.",
|
||||
p.api_version,
|
||||
ocs_plugin_api::API_VERSION,
|
||||
t!(
|
||||
"Plugin API %{plugin} is incompatible with this build's API %{build} ribbon ABI. Update or rebuild the plugin.",
|
||||
plugin = p.api_version,
|
||||
build = ocs_plugin_api::API_VERSION,
|
||||
)
|
||||
} else {
|
||||
format!("Load failed: {error}")
|
||||
t!("Load failed: %{error}", error = error)
|
||||
};
|
||||
info_body = info_body.push(
|
||||
text(detail)
|
||||
@@ -288,9 +292,8 @@ fn external_card<'a>(
|
||||
|
||||
let mut actions = row![Space::new().width(Fill)].align_y(iced::Center);
|
||||
if let (Some(repo), Some(tag)) = (repository, update_tag) {
|
||||
let label = format!("Update to {tag}");
|
||||
actions = actions.push(pill_button(
|
||||
&label,
|
||||
t!("Update to %{tag}", tag = tag),
|
||||
Message::PluginUpdate(repo, tag),
|
||||
button::primary,
|
||||
));
|
||||
@@ -302,7 +305,7 @@ fn external_card<'a>(
|
||||
actions = actions.push(Space::new().width(6));
|
||||
}
|
||||
actions = actions.push(pill_button(
|
||||
"Uninstall",
|
||||
t!("Uninstall"),
|
||||
Message::PluginUninstall(p.id.clone()),
|
||||
button::danger,
|
||||
));
|
||||
@@ -314,11 +317,11 @@ fn external_card<'a>(
|
||||
}
|
||||
|
||||
fn pill_button<'a>(
|
||||
label: &str,
|
||||
label: impl Into<Cow<'static, str>>,
|
||||
msg: Message,
|
||||
style: fn(&Theme, button::Status) -> button::Style,
|
||||
) -> Element<'a, Message> {
|
||||
button(text(label.to_string()).size(12))
|
||||
button(text(label.into()).size(12))
|
||||
.padding([4, 12])
|
||||
.on_press(msg)
|
||||
.style(style)
|
||||
@@ -357,7 +360,7 @@ fn install_controls<'a>(
|
||||
.map(|release| release.api_version)
|
||||
});
|
||||
let picker: Element<'_, Message> = if tags.is_empty() {
|
||||
text("no releases").size(11).style(muted_style).into()
|
||||
text(t!("no releases")).size(11).style(muted_style).into()
|
||||
} else {
|
||||
let r = repo_s.clone();
|
||||
iced::widget::pick_list(selected, tags, |value| value.to_string())
|
||||
@@ -368,13 +371,13 @@ fn install_controls<'a>(
|
||||
let action = match selected_api {
|
||||
Some(api_version) if api_version == ocs_plugin_api::API_VERSION => {
|
||||
pill_button(
|
||||
"Install",
|
||||
t!("Install"),
|
||||
Message::PluginInstall(repo_s.clone()),
|
||||
button::success,
|
||||
)
|
||||
}
|
||||
Some(_) => status_badge("Incompatible", StatusKind::Danger),
|
||||
None => status_badge("Unavailable", StatusKind::Muted),
|
||||
Some(_) => status_badge(t!("Incompatible"), StatusKind::Danger),
|
||||
None => status_badge(t!("Unavailable"), StatusKind::Muted),
|
||||
};
|
||||
let mut controls = row![picker, Space::new().width(8), action]
|
||||
.align_y(iced::Center)
|
||||
@@ -424,14 +427,14 @@ fn repository_display_name(repository: &str) -> String {
|
||||
|
||||
fn add_repository_card<'a>(m: &MarketView) -> Element<'a, Message> {
|
||||
let form = row![
|
||||
text_input("GitHub URL or owner/repository", m.input)
|
||||
text_input(t!("GitHub URL or owner/repository").as_ref(), m.input)
|
||||
.on_input(Message::PluginRepoInput)
|
||||
.on_submit(Message::PluginRepoAdd)
|
||||
.size(13)
|
||||
.width(Fill),
|
||||
Space::new().width(8),
|
||||
pill_button(
|
||||
"Add repository",
|
||||
t!("Add repository"),
|
||||
Message::PluginRepoAdd,
|
||||
button::primary,
|
||||
),
|
||||
@@ -440,11 +443,10 @@ fn add_repository_card<'a>(m: &MarketView) -> Element<'a, Message> {
|
||||
|
||||
market_card(
|
||||
column![
|
||||
text("Add from GitHub").size(14),
|
||||
text(
|
||||
"Paste a public repository URL. Compatible releases and the README \
|
||||
are detected automatically.",
|
||||
)
|
||||
text(t!("Add from GitHub")).size(14),
|
||||
text(t!(
|
||||
"Paste a public repository URL. Compatible releases and the README are detected automatically."
|
||||
))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
Space::new().height(3),
|
||||
@@ -455,28 +457,31 @@ fn add_repository_card<'a>(m: &MarketView) -> Element<'a, Message> {
|
||||
)
|
||||
}
|
||||
|
||||
fn registry_error_message(error: &str) -> (&'static str, &'static str) {
|
||||
fn registry_error_message(error: &str) -> (Cow<'static, str>, Cow<'static, str>) {
|
||||
let error = error.to_ascii_lowercase();
|
||||
if error.contains("certificate")
|
||||
|| error.contains("unknownissuer")
|
||||
|| error.contains("unknown issuer")
|
||||
{
|
||||
(
|
||||
"Unable to verify the server certificate",
|
||||
"Open CAD Studio could not trust the certificate presented for the plugin registry. \
|
||||
Check your system certificate and proxy settings, then retry.",
|
||||
t!("Unable to verify the server certificate"),
|
||||
t!(
|
||||
"Open CAD Studio could not trust the certificate presented for the plugin registry. Check your system certificate and proxy settings, then retry."
|
||||
),
|
||||
)
|
||||
} else if error.contains("timed out") || error.contains("timeout") {
|
||||
(
|
||||
"Plugin registry request timed out",
|
||||
"Check your internet or proxy connection, then retry. Manually added repositories \
|
||||
remain available.",
|
||||
t!("Plugin registry request timed out"),
|
||||
t!(
|
||||
"Check your internet or proxy connection, then retry. Manually added repositories remain available."
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"Unable to load the plugin registry",
|
||||
"Check your internet or proxy connection, then retry. Manually added repositories \
|
||||
remain available.",
|
||||
t!("Unable to load the plugin registry"),
|
||||
t!(
|
||||
"Check your internet or proxy connection, then retry. Manually added repositories remain available."
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -486,23 +491,23 @@ fn registry_notice<'a>(m: &MarketView) -> Option<Element<'a, Message>> {
|
||||
let (title, message) = registry_error_message(error);
|
||||
let actions = row![
|
||||
pill_button(
|
||||
"Retry",
|
||||
t!("Retry"),
|
||||
Message::PluginRegistryRetry,
|
||||
button::primary,
|
||||
),
|
||||
Space::new().width(6),
|
||||
pill_button(
|
||||
if m.registry_error_details_open {
|
||||
"Hide details"
|
||||
t!("Hide details")
|
||||
} else {
|
||||
"Show details"
|
||||
t!("Show details")
|
||||
},
|
||||
Message::PluginRegistryErrorDetailsToggle,
|
||||
button::secondary,
|
||||
),
|
||||
Space::new().width(6),
|
||||
pill_button(
|
||||
"Copy details",
|
||||
t!("Copy details"),
|
||||
Message::PluginRegistryCopyDiagnostics,
|
||||
button::secondary,
|
||||
),
|
||||
@@ -545,8 +550,8 @@ fn registry_notice<'a>(m: &MarketView) -> Option<Element<'a, Message>> {
|
||||
(m.registry_loading && m.registry.is_empty()).then(|| {
|
||||
container(
|
||||
column![
|
||||
text("Loading plugin catalog…").size(13),
|
||||
text("Connecting securely using your system certificate settings.")
|
||||
text(t!("Loading plugin catalog…")).size(13),
|
||||
text(t!("Connecting securely using your system certificate settings."))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
]
|
||||
@@ -563,7 +568,7 @@ fn marketplace_section<'a>(
|
||||
m: &MarketView,
|
||||
externals: &[ExternalPlugin],
|
||||
) -> Element<'a, Message> {
|
||||
let mut col = column![text("Available plugins").size(13).style(primary_style)].spacing(6);
|
||||
let mut col = column![text(t!("Available plugins")).size(13).style(primary_style)].spacing(6);
|
||||
let mut visible = 0usize;
|
||||
if let Some(notice) = registry_notice(m) {
|
||||
col = col.push(notice);
|
||||
@@ -617,7 +622,7 @@ fn marketplace_section<'a>(
|
||||
let info = button(
|
||||
column![
|
||||
text(display_name).size(14),
|
||||
text("Added from GitHub").size(11).style(muted_style),
|
||||
text(t!("Added from GitHub")).size(11).style(muted_style),
|
||||
]
|
||||
.spacing(4)
|
||||
.width(Fill),
|
||||
@@ -642,9 +647,9 @@ fn marketplace_section<'a>(
|
||||
&& !(m.registry_loading && m.registry.is_empty())
|
||||
{
|
||||
let message = if m.search.trim().is_empty() {
|
||||
"No additional plugins are available."
|
||||
t!("No additional plugins are available.")
|
||||
} else {
|
||||
"No available plugins match your search."
|
||||
t!("No available plugins match your search.")
|
||||
};
|
||||
col = col.push(text(message).size(12).style(muted_style));
|
||||
}
|
||||
@@ -684,8 +689,8 @@ fn readme_panel<'a>(
|
||||
let Some(repo) = market.selected_repo else {
|
||||
return container(
|
||||
column![
|
||||
text("Plugin details").size(16),
|
||||
text("Select a plugin to read its GitHub README.")
|
||||
text(t!("Plugin details")).size(16),
|
||||
text(t!("Select a plugin to read its GitHub README."))
|
||||
.size(12)
|
||||
.style(muted_style),
|
||||
]
|
||||
@@ -712,7 +717,7 @@ fn readme_panel<'a>(
|
||||
.spacing(3)
|
||||
.width(width),
|
||||
pill_button(
|
||||
"View on GitHub",
|
||||
t!("View on GitHub"),
|
||||
Message::OpenUrl(format!("https://github.com/{repo}")),
|
||||
button::secondary,
|
||||
),
|
||||
@@ -722,8 +727,8 @@ fn readme_panel<'a>(
|
||||
let content: Element<'a, Message> = if market.readme_loading.contains(repo) {
|
||||
container(
|
||||
column![
|
||||
text("Loading README…").size(13),
|
||||
text("Fetching the default branch from GitHub.")
|
||||
text(t!("Loading README…")).size(13),
|
||||
text(t!("Fetching the default branch from GitHub."))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
]
|
||||
@@ -748,11 +753,11 @@ fn readme_panel<'a>(
|
||||
}
|
||||
Some(Err(error)) => container(
|
||||
column![
|
||||
text("README could not be loaded").size(14),
|
||||
text(t!("README could not be loaded")).size(14),
|
||||
text(error.clone()).size(11).style(muted_style),
|
||||
Space::new().height(4),
|
||||
pill_button(
|
||||
"Retry",
|
||||
t!("Retry"),
|
||||
Message::PluginReadmeSelect(repo.to_string()),
|
||||
button::secondary,
|
||||
),
|
||||
@@ -765,7 +770,7 @@ fn readme_panel<'a>(
|
||||
.height(height)
|
||||
.into(),
|
||||
None => container(
|
||||
text("Select the plugin again to load its README.")
|
||||
text(t!("Select the plugin again to load its README."))
|
||||
.size(12)
|
||||
.style(muted_style),
|
||||
)
|
||||
@@ -811,15 +816,16 @@ pub fn view_window<'a>(
|
||||
) -> Element<'a, Message> {
|
||||
let width = sizing.width;
|
||||
let height = sizing.height;
|
||||
let title = text("Plugins").size(20);
|
||||
let subtitle = text("Browse, install, and manage add-ons. Select one to view its README.")
|
||||
.size(12)
|
||||
.style(muted_style);
|
||||
let title = text(t!("Plugins")).size(20);
|
||||
let subtitle =
|
||||
text(t!("Browse, install, and manage add-ons. Select one to view its README."))
|
||||
.size(12)
|
||||
.style(muted_style);
|
||||
|
||||
let mut list = column![].spacing(10);
|
||||
// Installed external packages (from the plugins folder).
|
||||
if externals.is_empty() {
|
||||
list = list.push(text("No plugins installed yet.").size(13).style(muted_style));
|
||||
list = list.push(text(t!("No plugins installed yet.")).size(13).style(muted_style));
|
||||
} else {
|
||||
let mut visible_installed = 0usize;
|
||||
for p in externals {
|
||||
@@ -828,7 +834,7 @@ pub fn view_window<'a>(
|
||||
continue;
|
||||
}
|
||||
if visible_installed == 0 {
|
||||
list = list.push(text("Installed").size(13).style(primary_style));
|
||||
list = list.push(text(t!("Installed")).size(13).style(primary_style));
|
||||
}
|
||||
visible_installed += 1;
|
||||
let selected = repository.as_deref() == market.selected_repo;
|
||||
@@ -857,7 +863,7 @@ pub fn view_window<'a>(
|
||||
}
|
||||
if visible_installed == 0 && !market.search.trim().is_empty() {
|
||||
list = list.push(
|
||||
text("No installed plugins match your search.")
|
||||
text(t!("No installed plugins match your search."))
|
||||
.size(12)
|
||||
.style(muted_style),
|
||||
);
|
||||
@@ -875,7 +881,7 @@ pub fn view_window<'a>(
|
||||
let catalog = scrollable(container(list.width(Fill)).padding(gutter))
|
||||
.height(height)
|
||||
.width(Fill);
|
||||
let search = text_input("Search plugins…", market.search)
|
||||
let search = text_input(t!("Search plugins…").as_ref(), market.search)
|
||||
.on_input(Message::PluginSearchInput)
|
||||
.size(13)
|
||||
.padding([7, 10])
|
||||
@@ -916,7 +922,7 @@ pub fn view_window<'a>(
|
||||
/// Browser replacement for the native Plugin Manager.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn view_web_notice<'a>() -> Element<'a, Message> {
|
||||
let download = button(text("Download desktop app").size(13))
|
||||
let download = button(text(t!("Download desktop app")).size(13))
|
||||
.on_press(Message::OpenUrl(DESKTOP_DOWNLOAD_URL.to_string()))
|
||||
.padding([9, 18])
|
||||
.style(|theme: &Theme, status| {
|
||||
@@ -944,14 +950,13 @@ pub fn view_web_notice<'a>() -> Element<'a, Message> {
|
||||
let notice = container(
|
||||
column![
|
||||
icon,
|
||||
text("Plugins are available in the desktop app")
|
||||
text(t!("Plugins are available in the desktop app"))
|
||||
.size(20)
|
||||
.width(Length::Fit)
|
||||
.align_x(iced::alignment::Horizontal::Center),
|
||||
text(
|
||||
"Open CAD Studio plugins are native packages and cannot run inside a browser. \
|
||||
Download the desktop app to browse, install, and use plugins.",
|
||||
)
|
||||
text(t!(
|
||||
"Open CAD Studio plugins are native packages and cannot run inside a browser. Download the desktop app to browse, install, and use plugins."
|
||||
))
|
||||
.size(13)
|
||||
.width(Length::Fit)
|
||||
.align_x(iced::alignment::Horizontal::Center)
|
||||
@@ -998,6 +1003,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn certificate_errors_get_user_friendly_copy() {
|
||||
// The registry message is localised; pin English so the assertion
|
||||
// does not depend on the ambient process locale.
|
||||
crate::i18n::set_language(crate::i18n::Language::EnUs)
|
||||
.expect("English locale must be available");
|
||||
let (title, message) =
|
||||
registry_error_message("io: invalid peer certificate: UnknownIssuer");
|
||||
|
||||
|
||||
+33
-31
@@ -3,6 +3,7 @@
|
||||
use crate::app::Message;
|
||||
use iced::widget::{column, container, row, scrollable, text, Space};
|
||||
use iced::{Background, Element, Theme};
|
||||
use crate::t;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Display name of the primary accelerator modifier on this platform.
|
||||
@@ -41,7 +42,7 @@ fn hdivider<'a>(width: iced::Length) -> Element<'a, Message> {
|
||||
|
||||
fn shortcut_row<'a>(
|
||||
key: impl Into<Cow<'static, str>>,
|
||||
action: &'static str,
|
||||
action: Cow<'static, str>,
|
||||
) -> Element<'a, Message> {
|
||||
row![
|
||||
text(key.into())
|
||||
@@ -75,7 +76,7 @@ pub fn view_window<'a>(
|
||||
// ── Toolbar ───────────────────────────────────────────────────────────
|
||||
let toolbar = container(
|
||||
row![
|
||||
text("Type SHORTCUTS SET <key> <cmd> to add custom shortcuts.")
|
||||
text(t!("Type SHORTCUTS SET <key> <cmd> to add custom shortcuts."))
|
||||
.size(10)
|
||||
.style(muted_style),
|
||||
]
|
||||
@@ -92,41 +93,42 @@ pub fn view_window<'a>(
|
||||
|
||||
// ── Shortcut entries ──────────────────────────────────────────────────
|
||||
let mut rows: Vec<Element<'_, Message>> = vec![
|
||||
section("── Function Keys ──────────────────────────────────────"),
|
||||
shortcut_row("F3", "Toggle Object Snap"),
|
||||
shortcut_row("F7", "Toggle Grid"),
|
||||
shortcut_row("F8", "Toggle Ortho"),
|
||||
shortcut_row("F9", "Toggle Grid Snap"),
|
||||
shortcut_row("F10", "Toggle Polar Tracking"),
|
||||
shortcut_row("F11", "Toggle Object Snap Tracking"),
|
||||
shortcut_row("F12", "Toggle Dynamic Input"),
|
||||
section(format!(
|
||||
"── {MOD} Shortcuts ──────────────────────────────────────"
|
||||
section(t!("── Function Keys ──────────────────────────────────────")),
|
||||
shortcut_row("F3", t!("Toggle Object Snap")),
|
||||
shortcut_row("F7", t!("Toggle Grid")),
|
||||
shortcut_row("F8", t!("Toggle Ortho")),
|
||||
shortcut_row("F9", t!("Toggle Grid Snap")),
|
||||
shortcut_row("F10", t!("Toggle Polar Tracking")),
|
||||
shortcut_row("F11", t!("Toggle Object Snap Tracking")),
|
||||
shortcut_row("F12", t!("Toggle Dynamic Input")),
|
||||
section(t!(
|
||||
"── %{MOD} Shortcuts ──────────────────────────────────────",
|
||||
MOD = MOD
|
||||
)),
|
||||
shortcut_row(format!("{MOD}+N"), "New Drawing"),
|
||||
shortcut_row(format!("{MOD}+O"), "Open File"),
|
||||
shortcut_row(format!("{MOD}+S"), "Save"),
|
||||
shortcut_row(format!("{MOD}+Shift+S"), "Save As"),
|
||||
shortcut_row(format!("{MOD}+Z"), "Undo"),
|
||||
shortcut_row(format!("{MOD}+Shift+Z / {MOD}+Y"), "Redo"),
|
||||
shortcut_row(format!("{MOD}+F / {MOD}+H"), "Find and Replace"),
|
||||
shortcut_row(format!("{MOD}+C"), "Copy to Clipboard"),
|
||||
shortcut_row(format!("{MOD}+X"), "Cut to Clipboard"),
|
||||
shortcut_row(format!("{MOD}+V"), "Paste from Clipboard"),
|
||||
section("── Other Keys ──────────────────────────────────────────"),
|
||||
shortcut_row("Enter / Space", "Finalize command / Repeat last"),
|
||||
shortcut_row("Escape", "Cancel active command"),
|
||||
shortcut_row("Delete", "Delete selected entities"),
|
||||
shortcut_row("↑ / ↓", "Command history navigation"),
|
||||
shortcut_row(format!("{MOD}+N"), t!("New Drawing")),
|
||||
shortcut_row(format!("{MOD}+O"), t!("Open File")),
|
||||
shortcut_row(format!("{MOD}+S"), t!("Save")),
|
||||
shortcut_row(format!("{MOD}+Shift+S"), t!("Save As")),
|
||||
shortcut_row(format!("{MOD}+Z"), t!("Undo")),
|
||||
shortcut_row(format!("{MOD}+Shift+Z / {MOD}+Y"), t!("Redo")),
|
||||
shortcut_row(format!("{MOD}+F / {MOD}+H"), t!("Find and Replace")),
|
||||
shortcut_row(format!("{MOD}+C"), t!("Copy to Clipboard")),
|
||||
shortcut_row(format!("{MOD}+X"), t!("Cut to Clipboard")),
|
||||
shortcut_row(format!("{MOD}+V"), t!("Paste from Clipboard")),
|
||||
section(t!("── Other Keys ──────────────────────────────────────────")),
|
||||
shortcut_row("Enter / Space", t!("Finalize command / Repeat last")),
|
||||
shortcut_row("Escape", t!("Cancel active command")),
|
||||
shortcut_row("Delete", t!("Delete selected entities")),
|
||||
shortcut_row("↑ / ↓", t!("Command history navigation")),
|
||||
];
|
||||
|
||||
// Custom overrides section
|
||||
rows.push(section(
|
||||
"── Custom Overrides (SHORTCUTS SET) ──────────────────",
|
||||
t!("── Custom Overrides (SHORTCUTS SET) ──────────────────"),
|
||||
));
|
||||
if overrides.is_empty() {
|
||||
rows.push(
|
||||
text(" (none — use: SHORTCUTS SET <key> <command>)")
|
||||
text(t!(" (none — use: SHORTCUTS SET <key> <command>)"))
|
||||
.size(11)
|
||||
.style(muted_style)
|
||||
.into(),
|
||||
@@ -160,8 +162,8 @@ pub fn view_window<'a>(
|
||||
// ── Header row with accent ────────────────────────────────────────────
|
||||
let header = container(
|
||||
row![
|
||||
text("Key").size(10).style(primary_style).width(160),
|
||||
text("Action").size(10).style(primary_style),
|
||||
text(t!("Key")).size(10).style(primary_style).width(160),
|
||||
text(t!("Action")).size(10).style(primary_style),
|
||||
]
|
||||
.spacing(8)
|
||||
.padding([4, 16]),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::app::Message;
|
||||
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
||||
use iced::{Background, Border, Element, Theme};
|
||||
use crate::t;
|
||||
use std::borrow::Cow;
|
||||
|
||||
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
|
||||
iced::widget::text::Style {
|
||||
@@ -18,7 +20,7 @@ fn primary_style(theme: &Theme) -> iced::widget::text::Style {
|
||||
/// flag tints the border + label with the accent colour, making the new
|
||||
/// version the visual anchor of the row.
|
||||
fn version_card<'a>(
|
||||
label: &'static str,
|
||||
label: Cow<'static, str>,
|
||||
value: String,
|
||||
highlight: bool,
|
||||
width: iced::Length,
|
||||
@@ -132,8 +134,8 @@ pub fn view_window<'a>(
|
||||
) -> Element<'a, Message> {
|
||||
let header = container(
|
||||
column![
|
||||
text("New Release Available").size(20).style(primary_style),
|
||||
text("A newer Open CAD Studio version is published on GitHub.")
|
||||
text(t!("New Release Available")).size(20).style(primary_style),
|
||||
text(t!("A newer Open CAD Studio version is published on GitHub."))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
]
|
||||
@@ -153,12 +155,12 @@ pub fn view_window<'a>(
|
||||
// one accent-tinted to draw the eye. Replaces the previous label/value
|
||||
// row layout. The arrow between them is purely decorative.
|
||||
let installed = version_card(
|
||||
"Installed",
|
||||
t!("Installed"),
|
||||
format!("v{}", env!("CARGO_PKG_VERSION")),
|
||||
false,
|
||||
sizing.width,
|
||||
);
|
||||
let latest_card = version_card("Latest", format!("v{}", latest), true, sizing.width);
|
||||
let latest_card = version_card(t!("Latest"), format!("v{}", latest), true, sizing.width);
|
||||
let arrow = container(crate::ui::icons::themed_secondary(
|
||||
crate::ui::icons::ARROW_LONG_RIGHT,
|
||||
20.0,
|
||||
@@ -172,12 +174,12 @@ pub fn view_window<'a>(
|
||||
.align_y(iced::Center)
|
||||
.width(sizing.width);
|
||||
|
||||
let later_btn = button(text("Later").size(11))
|
||||
let later_btn = button(text(t!("Later")).size(11))
|
||||
.on_press(Message::UpdateNoticeClose)
|
||||
.style(button::secondary)
|
||||
.padding([6, 16]);
|
||||
|
||||
let open_btn = button(text("Open Release Page").size(11))
|
||||
let open_btn = button(text(t!("Open Release Page")).size(11))
|
||||
.on_press(Message::UpdateNoticeOpenRelease)
|
||||
.style(button::primary)
|
||||
.padding([6, 16]);
|
||||
@@ -195,7 +197,7 @@ pub fn view_window<'a>(
|
||||
// Release notes panel. Rendered as a light-markdown column inside a
|
||||
// bordered scrollable so long bodies stay contained and don't
|
||||
// explode the window. Empty body → "No release notes provided."
|
||||
let notes_heading = container(text("What's new").size(11).style(muted_style))
|
||||
let notes_heading = container(text(t!("What's new")).size(11).style(muted_style))
|
||||
.padding(iced::Padding {
|
||||
top: 10.0,
|
||||
right: 0.0,
|
||||
@@ -204,7 +206,7 @@ pub fn view_window<'a>(
|
||||
});
|
||||
|
||||
let notes_body: Element<'a, Message> = if body.trim().is_empty() {
|
||||
text("No release notes provided.")
|
||||
text(t!("No release notes provided."))
|
||||
.size(11)
|
||||
.style(muted_style)
|
||||
.into()
|
||||
|
||||
Reference in new issue
Block a user