forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into codex/pdf-attach-parity
# Conflicts: # locales/ar-SA/opencadstudio.ftl # locales/bg-BG/opencadstudio.ftl # locales/cs-CZ/opencadstudio.ftl # locales/de-DE/opencadstudio.ftl # locales/el-GR/opencadstudio.ftl # locales/en-US/opencadstudio.ftl # locales/es-ES/opencadstudio.ftl # locales/fi-FI/opencadstudio.ftl # locales/fr-FR/opencadstudio.ftl # locales/hi-IN/opencadstudio.ftl # locales/hu-HU/opencadstudio.ftl # locales/it-IT/opencadstudio.ftl # locales/ja-JP/opencadstudio.ftl # locales/ko-KR/opencadstudio.ftl # locales/nl-NL/opencadstudio.ftl # locales/pl-PL/opencadstudio.ftl # locales/pt-BR/opencadstudio.ftl # locales/ru-RU/opencadstudio.ftl # locales/tr-TR/opencadstudio.ftl # locales/zh-CN/opencadstudio.ftl # locales/zh-TW/opencadstudio.ftl # src/app/control/actions.rs # src/app/mod.rs # src/app/update/mod.rs # src/app/view/modal.rs # src/io/xref.rs # src/ui/window/mod.rs
This commit is contained in:
commit
c1ecb0edaf
77 files changed
+5695
-606
No files matched your search
+35
-1
@@ -7,10 +7,44 @@
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::ui::style::common::muted_style;
|
||||
use iced::widget::{button, container, pick_list, row, text, text_input, Space};
|
||||
use iced::widget::{button, container, pick_list, radio, row, text, text_input, Space};
|
||||
use iced::{Background, Border, Element, Length, Theme};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Standard dialog footer action button (OK, Cancel, Apply, Close, Help, etc.),
|
||||
/// matching the Drafting Settings / Options appearance: 12px text, [6, 18] padding,
|
||||
/// themed primary or secondary style.
|
||||
pub fn dialog_button<'a>(
|
||||
label: impl text::IntoFragment<'a>,
|
||||
on_press: Message,
|
||||
primary: bool,
|
||||
) -> button::Button<'a, Message> {
|
||||
button(text(label).size(12))
|
||||
.on_press(on_press)
|
||||
.padding([6, 18])
|
||||
.style(if primary {
|
||||
button::primary
|
||||
} else {
|
||||
button::secondary
|
||||
})
|
||||
}
|
||||
|
||||
/// Unified radio button helper ensuring consistent 16px size for exact pixel-grid concentric alignment.
|
||||
pub fn form_radio<'a, T>(
|
||||
label: impl Into<String>,
|
||||
value: T,
|
||||
selected: Option<T>,
|
||||
on_click: impl Fn(T) -> Message + 'a,
|
||||
) -> radio::Radio<'a, Message>
|
||||
where
|
||||
T: Copy + Eq + 'a,
|
||||
{
|
||||
radio(label, value, selected, on_click)
|
||||
.size(16)
|
||||
.spacing(6)
|
||||
.text_size(11)
|
||||
}
|
||||
|
||||
/// Width of the label column in a `label : control` row.
|
||||
pub const LABEL_WIDTH: f32 = 92.0;
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ pub fn view_window<'a>(
|
||||
Some(relative),
|
||||
Message::PointStyleSizeRelative,
|
||||
)
|
||||
.size(15)
|
||||
.size(16)
|
||||
.text_size(13),
|
||||
radio(
|
||||
t!("Set Size in Absolute Units"),
|
||||
@@ -176,7 +176,7 @@ pub fn view_window<'a>(
|
||||
Some(relative),
|
||||
Message::PointStyleSizeRelative,
|
||||
)
|
||||
.size(15)
|
||||
.size(16)
|
||||
.text_size(13),
|
||||
]
|
||||
.spacing(6);
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::fmt;
|
||||
|
||||
use acadrust::Handle;
|
||||
use iced::widget::{
|
||||
button, checkbox, column, combo_box, container, mouse_area, pick_list, radio, row, stack, text,
|
||||
button, checkbox, column, combo_box, container, mouse_area, pick_list, row, stack, text,
|
||||
text_editor, text_input, Space,
|
||||
};
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
@@ -14,7 +14,7 @@ use crate::app::Message;
|
||||
use crate::modules::draw::units;
|
||||
use crate::t;
|
||||
use crate::ui::style::common::muted_style;
|
||||
use crate::ui::style::form::{button_style, field_style};
|
||||
use crate::ui::style::form::{button_style, dialog_button, field_style, form_radio};
|
||||
|
||||
static ICON_PICK_POINT: &[u8] = include_bytes!("../../../assets/icons/blocks/pick_point.svg");
|
||||
static ICON_SELECT_OBJECTS: &[u8] =
|
||||
@@ -353,33 +353,24 @@ pub fn view_window<'a>(
|
||||
let btns_row = row![sel_obj_btn, qsel_btn].spacing(5).align_y(iced::Center);
|
||||
|
||||
let radios = column![
|
||||
radio(
|
||||
form_radio(
|
||||
t!("Retain"),
|
||||
BlockObjectMode::Retain,
|
||||
Some(state.object_mode),
|
||||
Message::BlockDefObjectMode,
|
||||
)
|
||||
.size(15)
|
||||
.spacing(5)
|
||||
.text_size(11),
|
||||
radio(
|
||||
),
|
||||
form_radio(
|
||||
t!("Convert to block"),
|
||||
BlockObjectMode::Convert,
|
||||
Some(state.object_mode),
|
||||
Message::BlockDefObjectMode,
|
||||
)
|
||||
.size(15)
|
||||
.spacing(5)
|
||||
.text_size(11),
|
||||
radio(
|
||||
),
|
||||
form_radio(
|
||||
t!("Delete"),
|
||||
BlockObjectMode::Delete,
|
||||
Some(state.object_mode),
|
||||
Message::BlockDefObjectMode,
|
||||
)
|
||||
.size(15)
|
||||
.spacing(5)
|
||||
.text_size(11),
|
||||
),
|
||||
]
|
||||
.spacing(2);
|
||||
|
||||
@@ -580,20 +571,11 @@ pub fn view_window<'a>(
|
||||
// ── Footer Bar ────────────────────────────────────────────────────────────
|
||||
let actions = row![
|
||||
Space::new().width(Fill),
|
||||
button(text(t!("OK")).size(11))
|
||||
.on_press(Message::BlockDefApply)
|
||||
.style(button_style(true))
|
||||
.padding([4, 16]),
|
||||
button(text(t!("Cancel")).size(11))
|
||||
.on_press(Message::CloseModal)
|
||||
.style(button_style(false))
|
||||
.padding([4, 12]),
|
||||
button(text(t!("Help")).size(11))
|
||||
.on_press(Message::BlockDefHelp)
|
||||
.style(button_style(false))
|
||||
.padding([4, 12]),
|
||||
dialog_button(t!("OK"), Message::BlockDefApply, true),
|
||||
dialog_button(t!("Cancel"), Message::CloseModal, false),
|
||||
dialog_button(t!("Help"), Message::BlockDefHelp, false),
|
||||
]
|
||||
.spacing(6)
|
||||
.spacing(8)
|
||||
.align_y(iced::Center);
|
||||
|
||||
// ── Error Banner ──────────────────────────────────────────────────────────
|
||||
@@ -664,15 +646,9 @@ pub fn view_window<'a>(
|
||||
.align_x(iced::alignment::Horizontal::Center),
|
||||
Space::new().height(16),
|
||||
row![
|
||||
button(text(t!("Yes")).size(12))
|
||||
.on_press(Message::BlockDefConfirmRedefine(true))
|
||||
.padding([6, 20])
|
||||
.style(button_style(false)),
|
||||
dialog_button(t!("Yes"), Message::BlockDefConfirmRedefine(true), false),
|
||||
// Default / safe focus on "No"
|
||||
button(text(t!("No")).size(12))
|
||||
.on_press(Message::BlockDefConfirmRedefine(false))
|
||||
.padding([6, 22])
|
||||
.style(button_style(true)),
|
||||
dialog_button(t!("No"), Message::BlockDefConfirmRedefine(false), true),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_y(iced::Center),
|
||||
|
||||
@@ -17,6 +17,7 @@ fn muted_style(theme: &Theme) -> text::Style {
|
||||
pub fn view_window<'a>(
|
||||
fonts: &'a [String],
|
||||
font_source: &'a str,
|
||||
downloading: bool,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let content_width = if matches!(sizing.width, Length::Fill) {
|
||||
@@ -33,16 +34,24 @@ pub fn view_window<'a>(
|
||||
.iter()
|
||||
.map(|name| text(format!(" {name}")).size(12).into())
|
||||
.collect();
|
||||
let download = button(
|
||||
text(if downloading {
|
||||
crate::t!("Downloading...")
|
||||
} else {
|
||||
crate::t!("Download available")
|
||||
})
|
||||
.size(12),
|
||||
)
|
||||
.on_press_maybe((!downloading).then_some(Message::MissingFontsDownload))
|
||||
.style(button::primary)
|
||||
.padding([6, 14]);
|
||||
let actions = row![
|
||||
Space::new().width(content_width),
|
||||
button(text(crate::t!("Skip")).size(12))
|
||||
.on_press(Message::MissingFontsDismiss)
|
||||
.on_press_maybe((!downloading).then_some(Message::MissingFontsDismiss))
|
||||
.style(button::secondary)
|
||||
.padding([6, 14]),
|
||||
button(text(crate::t!("Download")).size(12))
|
||||
.on_press(Message::MissingFontsDownload)
|
||||
.style(button::primary)
|
||||
.padding([6, 14]),
|
||||
download,
|
||||
]
|
||||
.spacing(8);
|
||||
|
||||
@@ -50,7 +59,7 @@ pub fn view_window<'a>(
|
||||
column![
|
||||
text(crate::t!("Missing fonts")).size(20),
|
||||
text(crate::t!(
|
||||
"This drawing uses fonts that are not installed on this machine. Download them from the OpenCADStudio community font repository?"
|
||||
"This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute."
|
||||
))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
|
||||
@@ -25,5 +25,7 @@ pub mod alias_editor;
|
||||
pub mod find_replace;
|
||||
pub mod named_parameters;
|
||||
pub mod pdf_dialogs;
|
||||
pub mod xref_attach;
|
||||
pub mod xref_help;
|
||||
pub mod xref_manager;
|
||||
pub mod wblock;
|
||||
@@ -0,0 +1,492 @@
|
||||
//! Write Block (WBLOCK) dialog window — write a block, selected entities, or entire drawing to an external DWG/DXF file.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use acadrust::Handle;
|
||||
use iced::widget::{
|
||||
button, column, combo_box, container, pick_list, row, text, text_input, Space,
|
||||
};
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::modules::draw::units;
|
||||
use crate::t;
|
||||
use crate::ui::style::form::{button_style, dialog_button, field_style, form_radio};
|
||||
|
||||
static ICON_PICK_POINT: &[u8] = include_bytes!("../../../assets/icons/blocks/pick_point.svg");
|
||||
static ICON_SELECT_OBJECTS: &[u8] =
|
||||
include_bytes!("../../../assets/icons/blocks/select_objects.svg");
|
||||
static ICON_QUICK_SELECT: &[u8] = include_bytes!("../../../assets/icons/blocks/quick_select.svg");
|
||||
static ICON_WARN: &[u8] = include_bytes!("../../../assets/icons/ui/warning_triangle.svg");
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum WblockSourceMode {
|
||||
Block,
|
||||
EntireDrawing,
|
||||
Objects,
|
||||
}
|
||||
|
||||
impl Default for WblockSourceMode {
|
||||
fn default() -> Self {
|
||||
WblockSourceMode::Objects
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum WblockObjectMode {
|
||||
Retain,
|
||||
Convert,
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl Default for WblockObjectMode {
|
||||
fn default() -> Self {
|
||||
WblockObjectMode::Retain
|
||||
}
|
||||
}
|
||||
|
||||
/// Dropdown entry for the Unit picker.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct UnitChoice {
|
||||
pub code: i16,
|
||||
pub label: &'static str,
|
||||
}
|
||||
|
||||
impl fmt::Display for UnitChoice {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let name = crate::i18n::translate(self.label);
|
||||
f.write_str(name.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// The Write Block dialog's state.
|
||||
pub struct WblockState {
|
||||
pub source_mode: WblockSourceMode,
|
||||
pub block_name: String,
|
||||
pub existing_blocks: Vec<String>,
|
||||
pub block_combo: combo_box::State<String>,
|
||||
|
||||
pub base_point_x: String,
|
||||
pub base_point_y: String,
|
||||
pub base_point_z: String,
|
||||
|
||||
pub selected_handles: Vec<Handle>,
|
||||
pub object_mode: WblockObjectMode,
|
||||
|
||||
pub file_path: String,
|
||||
pub unit: i16,
|
||||
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl Clone for WblockState {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
source_mode: self.source_mode,
|
||||
block_name: self.block_name.clone(),
|
||||
existing_blocks: self.existing_blocks.clone(),
|
||||
block_combo: self.block_combo.clone(),
|
||||
base_point_x: self.base_point_x.clone(),
|
||||
base_point_y: self.base_point_y.clone(),
|
||||
base_point_z: self.base_point_z.clone(),
|
||||
selected_handles: self.selected_handles.clone(),
|
||||
object_mode: self.object_mode,
|
||||
file_path: self.file_path.clone(),
|
||||
unit: self.unit,
|
||||
error_message: self.error_message.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WblockState {
|
||||
pub fn new(
|
||||
existing_blocks: Vec<String>,
|
||||
pre_selected: Vec<Handle>,
|
||||
default_unit: i16,
|
||||
default_path: String,
|
||||
) -> Self {
|
||||
let default_block = existing_blocks.first().cloned().unwrap_or_default();
|
||||
Self {
|
||||
source_mode: WblockSourceMode::Objects,
|
||||
block_name: default_block,
|
||||
block_combo: combo_box::State::new(existing_blocks.clone()),
|
||||
existing_blocks,
|
||||
base_point_x: "0.00".to_string(),
|
||||
base_point_y: "0.00".to_string(),
|
||||
base_point_z: "0.00".to_string(),
|
||||
selected_handles: pre_selected,
|
||||
object_mode: WblockObjectMode::Retain,
|
||||
file_path: default_path,
|
||||
unit: default_unit,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_base_point(&self) -> glam::DVec3 {
|
||||
let x = self.base_point_x.trim().parse::<f64>().unwrap_or(0.0);
|
||||
let y = self.base_point_y.trim().parse::<f64>().unwrap_or(0.0);
|
||||
let z = self.base_point_z.trim().parse::<f64>().unwrap_or(0.0);
|
||||
glam::DVec3::new(x, y, z)
|
||||
}
|
||||
}
|
||||
|
||||
fn group<'a>(
|
||||
title: impl Into<String>,
|
||||
body: impl Into<Element<'a, Message>>,
|
||||
height: impl Into<Length>,
|
||||
) -> Element<'a, Message> {
|
||||
container(
|
||||
column![
|
||||
text(title.into())
|
||||
.size(11)
|
||||
.style(|theme: &Theme| text::Style {
|
||||
color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)),
|
||||
}),
|
||||
body.into(),
|
||||
]
|
||||
.spacing(4),
|
||||
)
|
||||
.padding([5, 8])
|
||||
.width(Fill)
|
||||
.height(height)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
color: theme.palette().background.strong.color,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn view_window<'a>(
|
||||
state: &'a WblockState,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let is_objects = state.source_mode == WblockSourceMode::Objects;
|
||||
let is_block = state.source_mode == WblockSourceMode::Block;
|
||||
|
||||
// ── Source: Block Radio & Dropdown ─────────────────────────────────────────
|
||||
let block_radio = form_radio(
|
||||
t!("Block:"),
|
||||
WblockSourceMode::Block,
|
||||
Some(state.source_mode),
|
||||
Message::WblockSourceMode,
|
||||
);
|
||||
|
||||
let block_dropdown: Element<'a, Message> = if is_block {
|
||||
combo_box(
|
||||
&state.block_combo,
|
||||
"",
|
||||
if state.block_name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&state.block_name)
|
||||
},
|
||||
|chosen: String| Message::WblockBlockSelect(chosen),
|
||||
)
|
||||
.on_input(Message::WblockBlockName)
|
||||
.size(11)
|
||||
.padding([2, 5])
|
||||
.width(Fill)
|
||||
.into()
|
||||
} else {
|
||||
crate::ui::read_only::field(&state.block_name, 11.0, Fill)
|
||||
};
|
||||
|
||||
let block_row = row![block_radio, block_dropdown]
|
||||
.spacing(8)
|
||||
.width(Fill)
|
||||
.align_y(iced::Center);
|
||||
|
||||
let entire_drawing_radio = form_radio(
|
||||
t!("Entire drawing"),
|
||||
WblockSourceMode::EntireDrawing,
|
||||
Some(state.source_mode),
|
||||
Message::WblockSourceMode,
|
||||
);
|
||||
|
||||
let objects_radio = form_radio(
|
||||
t!("Objects"),
|
||||
WblockSourceMode::Objects,
|
||||
Some(state.source_mode),
|
||||
Message::WblockSourceMode,
|
||||
);
|
||||
|
||||
// ── Source: Base Point Sub-group ──────────────────────────────────────────
|
||||
let pick_pt_icon = crate::ui::icons::semantic(ICON_PICK_POINT, 14.0);
|
||||
let mut pick_pt_btn = button(
|
||||
row![pick_pt_icon, text(t!("Pick point")).size(11)]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.padding([3, 10]);
|
||||
|
||||
if is_objects {
|
||||
pick_pt_btn = pick_pt_btn
|
||||
.on_press(Message::WblockPickPoint)
|
||||
.style(button_style(false));
|
||||
} else {
|
||||
pick_pt_btn = pick_pt_btn.style(button_style(false));
|
||||
}
|
||||
|
||||
let coord_row = |axis: &'static str, val: &'a str, on_input: fn(String) -> Message| {
|
||||
let input: Element<'a, Message> = if is_objects {
|
||||
text_input("", val)
|
||||
.on_input(on_input)
|
||||
.on_submit(Message::WblockApply)
|
||||
.style(field_style)
|
||||
.size(11)
|
||||
.padding([2, 5])
|
||||
.width(Fill)
|
||||
.into()
|
||||
} else {
|
||||
crate::ui::read_only::field(val, 11.0, Fill)
|
||||
};
|
||||
row![
|
||||
text(axis)
|
||||
.size(11)
|
||||
.style(|theme: &Theme| text::Style {
|
||||
color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)),
|
||||
})
|
||||
.width(Length::Fixed(16.0)),
|
||||
input,
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center)
|
||||
};
|
||||
|
||||
let base_point_group = group(
|
||||
t!("Base point").into_owned(),
|
||||
column![
|
||||
pick_pt_btn,
|
||||
Space::new().height(3),
|
||||
coord_row("X:", &state.base_point_x, Message::WblockBaseX),
|
||||
coord_row("Y:", &state.base_point_y, Message::WblockBaseY),
|
||||
coord_row("Z:", &state.base_point_z, Message::WblockBaseZ),
|
||||
]
|
||||
.spacing(3),
|
||||
Length::Fixed(142.0),
|
||||
);
|
||||
|
||||
// ── Source: Objects Sub-group ─────────────────────────────────────────────
|
||||
let sel_obj_icon = crate::ui::icons::semantic(ICON_SELECT_OBJECTS, 14.0);
|
||||
let mut sel_obj_btn = button(
|
||||
row![sel_obj_icon, text(t!("Select objects")).size(11)]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.padding([3, 8]);
|
||||
|
||||
let qsel_icon = crate::ui::icons::semantic(ICON_QUICK_SELECT, 14.0);
|
||||
let mut qsel_btn = button(qsel_icon).padding([3, 6]);
|
||||
|
||||
if is_objects {
|
||||
sel_obj_btn = sel_obj_btn
|
||||
.on_press(Message::WblockSelectObjects)
|
||||
.style(button_style(false));
|
||||
qsel_btn = qsel_btn
|
||||
.on_press(Message::WblockQuickSelect)
|
||||
.style(button_style(false));
|
||||
} else {
|
||||
sel_obj_btn = sel_obj_btn.style(button_style(false));
|
||||
qsel_btn = qsel_btn.style(button_style(false));
|
||||
}
|
||||
|
||||
let obj_btns_row = row![sel_obj_btn, qsel_btn]
|
||||
.spacing(5)
|
||||
.align_y(iced::Center);
|
||||
|
||||
let object_radios = column![
|
||||
form_radio(
|
||||
t!("Retain"),
|
||||
WblockObjectMode::Retain,
|
||||
if is_objects { Some(state.object_mode) } else { None },
|
||||
Message::WblockObjectMode,
|
||||
),
|
||||
form_radio(
|
||||
t!("Convert to block"),
|
||||
WblockObjectMode::Convert,
|
||||
if is_objects { Some(state.object_mode) } else { None },
|
||||
Message::WblockObjectMode,
|
||||
),
|
||||
form_radio(
|
||||
t!("Delete from drawing"),
|
||||
WblockObjectMode::Delete,
|
||||
if is_objects { Some(state.object_mode) } else { None },
|
||||
Message::WblockObjectMode,
|
||||
),
|
||||
]
|
||||
.spacing(3);
|
||||
|
||||
let count = state.selected_handles.len();
|
||||
let status_row: Element<'a, Message> = if !is_objects {
|
||||
Space::new().height(14).into()
|
||||
} else if count == 0 {
|
||||
row![
|
||||
crate::ui::icons::semantic(ICON_WARN, 13.0),
|
||||
text(t!("No objects selected"))
|
||||
.size(10)
|
||||
.style(|theme: &Theme| text::Style {
|
||||
color: Some(theme.palette().warning.base.color),
|
||||
}),
|
||||
]
|
||||
.spacing(5)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
} else {
|
||||
row![text(crate::tf!("{count} objects selected"))
|
||||
.size(10)
|
||||
.style(|theme: &Theme| text::Style {
|
||||
color: Some(theme.palette().primary.base.color),
|
||||
}),]
|
||||
.spacing(5)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
};
|
||||
|
||||
let objects_subgroup = group(
|
||||
t!("Objects").into_owned(),
|
||||
column![obj_btns_row, Space::new().height(2), object_radios, Space::new().height(2), status_row,]
|
||||
.spacing(2),
|
||||
Length::Fixed(142.0),
|
||||
);
|
||||
|
||||
let sub_panels = row![base_point_group, objects_subgroup]
|
||||
.spacing(8)
|
||||
.width(Fill);
|
||||
|
||||
let source_group = group(
|
||||
t!("Source").into_owned(),
|
||||
column![
|
||||
block_row,
|
||||
entire_drawing_radio,
|
||||
objects_radio,
|
||||
Space::new().height(4),
|
||||
sub_panels,
|
||||
]
|
||||
.spacing(4),
|
||||
Length::Shrink,
|
||||
);
|
||||
|
||||
// ── Destination Group ─────────────────────────────────────────────────────
|
||||
let file_input = text_input("", &state.file_path)
|
||||
.on_input(Message::WblockFilePath)
|
||||
.on_submit(Message::WblockApply)
|
||||
.style(field_style)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill);
|
||||
|
||||
let browse_btn = button(
|
||||
container(text("...").size(11))
|
||||
.width(Fill)
|
||||
.align_x(iced::alignment::Horizontal::Center),
|
||||
)
|
||||
.width(Length::Fixed(28.0))
|
||||
.on_press(Message::WblockBrowsePath)
|
||||
.style(button::secondary)
|
||||
.padding([3, 0]);
|
||||
|
||||
let path_row = row![file_input, browse_btn].spacing(5).align_y(iced::Center);
|
||||
|
||||
let unit_options: Vec<UnitChoice> = units::all()
|
||||
.map(|(code, label)| UnitChoice { code, label })
|
||||
.collect();
|
||||
|
||||
let selected_unit = unit_options
|
||||
.iter()
|
||||
.find(|choice| choice.code == state.unit)
|
||||
.cloned();
|
||||
|
||||
let unit_picker = pick_list(selected_unit, unit_options, |choice| choice.to_string())
|
||||
.on_select(|choice| Message::WblockUnit(choice.code))
|
||||
.text_size(11)
|
||||
.padding([2, 5])
|
||||
.width(Fill);
|
||||
|
||||
let units_row = row![
|
||||
text(t!("Insert units:"))
|
||||
.size(11)
|
||||
.style(|theme: &Theme| text::Style {
|
||||
color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)),
|
||||
}),
|
||||
Space::new().width(8),
|
||||
unit_picker,
|
||||
Space::new().width(Length::Fixed(33.0)),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center);
|
||||
|
||||
let destination_group = group(
|
||||
t!("Destination").into_owned(),
|
||||
column![
|
||||
text(t!("File name and path:"))
|
||||
.size(11)
|
||||
.style(|theme: &Theme| text::Style {
|
||||
color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)),
|
||||
}),
|
||||
path_row,
|
||||
Space::new().height(4),
|
||||
units_row,
|
||||
]
|
||||
.spacing(3),
|
||||
Length::Shrink,
|
||||
);
|
||||
|
||||
// ── Footer Bar ────────────────────────────────────────────────────────────
|
||||
let actions = row![
|
||||
Space::new().width(Fill),
|
||||
dialog_button(t!("OK"), Message::WblockApply, true),
|
||||
dialog_button(t!("Cancel"), Message::CloseModal, false),
|
||||
dialog_button(t!("Help"), Message::WblockHelp, false),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center);
|
||||
|
||||
// ── Error Banner ──────────────────────────────────────────────────────────
|
||||
let mut main_column = column![source_group, destination_group, actions,]
|
||||
.spacing(8)
|
||||
.padding([8, 10])
|
||||
.width(sizing.width);
|
||||
|
||||
if let Some(err) = &state.error_message {
|
||||
let err_banner = container(
|
||||
row![
|
||||
crate::ui::icons::semantic(ICON_WARN, 16.0),
|
||||
text(err.clone())
|
||||
.size(11)
|
||||
.style(|theme: &Theme| text::Style {
|
||||
color: Some(theme.palette().background.strongest.text),
|
||||
}),
|
||||
Space::new().width(Fill),
|
||||
button(text("×").size(12))
|
||||
.on_press(Message::WblockDismissError)
|
||||
.style(button_style(false))
|
||||
.padding([1, 6]),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.padding([6, 10])
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.palette().danger.base.color.scale_alpha(0.18),
|
||||
)),
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
color: theme.palette().danger.base.color,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
main_column = column![err_banner, main_column]
|
||||
.spacing(6)
|
||||
.width(sizing.width)
|
||||
.padding(10);
|
||||
}
|
||||
|
||||
main_column.into()
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
//! Attach External Reference dialog — the options chosen after picking a
|
||||
//! drawing with ATTACH / XATTACH: reference type, scale, insertion point,
|
||||
//! path type and rotation, with the file's preview and block unit.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use iced::widget::{
|
||||
button, column, combo_box, container, image, pick_list, radio, row, text, text_input, Space,
|
||||
};
|
||||
use iced::{Element, Fill, Length, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::io::xref_model::Pathtype;
|
||||
use crate::t;
|
||||
use crate::ui::style::common::muted_style;
|
||||
use crate::ui::style::form::{button_style, field_style};
|
||||
use crate::ui::window::block_definition::{group, labeled_checkbox};
|
||||
|
||||
/// One edit in the dialog.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum XrefAttachMsg {
|
||||
/// Name box: a reference already in the drawing, picked or typed.
|
||||
Name(String),
|
||||
Browse,
|
||||
Overlay(bool),
|
||||
ScaleOnScreen(bool),
|
||||
Scale(usize, String),
|
||||
Uniform(bool),
|
||||
InsertOnScreen(bool),
|
||||
Insert(usize, String),
|
||||
PathType(PathTypeChoice),
|
||||
RotationOnScreen(bool),
|
||||
Rotation(String),
|
||||
Details(bool),
|
||||
Apply,
|
||||
Help,
|
||||
}
|
||||
|
||||
/// Path type choice as the dialog lists it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PathTypeChoice(pub Pathtype);
|
||||
|
||||
impl PathTypeChoice {
|
||||
pub const ALL: [PathTypeChoice; 3] = [
|
||||
PathTypeChoice(Pathtype::Full),
|
||||
PathTypeChoice(Pathtype::Relative),
|
||||
PathTypeChoice(Pathtype::None),
|
||||
];
|
||||
}
|
||||
|
||||
impl fmt::Display for PathTypeChoice {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self.0 {
|
||||
Pathtype::Full => t!("Full path"),
|
||||
Pathtype::Relative => t!("Relative path"),
|
||||
Pathtype::None => t!("No path"),
|
||||
};
|
||||
f.write_str(label.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog's working copy.
|
||||
#[derive(Clone)]
|
||||
pub struct XrefAttachState {
|
||||
/// Absolute path of the drawing to attach.
|
||||
pub path: String,
|
||||
/// Reference name (the file name without extension).
|
||||
pub name: String,
|
||||
/// References already in the drawing: name and saved path.
|
||||
pub existing: Vec<(String, String)>,
|
||||
pub name_combo: combo_box::State<String>,
|
||||
pub overlay: bool,
|
||||
pub scale_on_screen: bool,
|
||||
pub scale: [String; 3],
|
||||
pub uniform: bool,
|
||||
pub insert_on_screen: bool,
|
||||
pub insert: [String; 3],
|
||||
pub path_type: PathTypeChoice,
|
||||
pub rotation_on_screen: bool,
|
||||
pub rotation: String,
|
||||
/// Block unit of the chosen drawing and its factor to the host's unit.
|
||||
pub unit_label: String,
|
||||
pub unit_factor: String,
|
||||
pub found_in: String,
|
||||
pub saved_path: String,
|
||||
pub details: bool,
|
||||
pub preview: Option<image::Handle>,
|
||||
}
|
||||
|
||||
impl XrefAttachState {
|
||||
pub fn new(existing: Vec<(String, String)>) -> Self {
|
||||
let names = existing.iter().map(|(name, _)| name.clone()).collect();
|
||||
Self {
|
||||
path: String::new(),
|
||||
name: String::new(),
|
||||
existing,
|
||||
name_combo: combo_box::State::new(names),
|
||||
overlay: false,
|
||||
scale_on_screen: false,
|
||||
scale: ["1.0000".into(), "1.0000".into(), "1.0000".into()],
|
||||
uniform: false,
|
||||
insert_on_screen: true,
|
||||
insert: ["0.0000".into(), "0.0000".into(), "0.0000".into()],
|
||||
path_type: PathTypeChoice(Pathtype::Relative),
|
||||
rotation_on_screen: false,
|
||||
rotation: "0".into(),
|
||||
unit_label: String::new(),
|
||||
unit_factor: String::new(),
|
||||
found_in: String::new(),
|
||||
saved_path: String::new(),
|
||||
details: false,
|
||||
preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed X/Y/Z scale; a uniform scale takes X for all three.
|
||||
pub fn scale_values(&self) -> Option<[f64; 3]> {
|
||||
let parse = |s: &String| s.trim().parse::<f64>().ok();
|
||||
let x = parse(&self.scale[0])?;
|
||||
if self.uniform {
|
||||
return Some([x, x, x]);
|
||||
}
|
||||
Some([x, parse(&self.scale[1])?, parse(&self.scale[2])?])
|
||||
}
|
||||
|
||||
pub fn insert_point(&self) -> Option<glam::DVec3> {
|
||||
let parse = |s: &String| s.trim().parse::<f64>().ok();
|
||||
Some(glam::DVec3::new(
|
||||
parse(&self.insert[0])?,
|
||||
parse(&self.insert[1])?,
|
||||
parse(&self.insert[2])?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn msg(m: XrefAttachMsg) -> Message {
|
||||
Message::XrefAttach(m)
|
||||
}
|
||||
|
||||
fn number_field<'a>(
|
||||
label: &'a str,
|
||||
value: &'a str,
|
||||
enabled: bool,
|
||||
on_input: impl Fn(String) -> Message + 'a,
|
||||
) -> Element<'a, Message> {
|
||||
let mut field = text_input("", value)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill)
|
||||
.style(field_style);
|
||||
if enabled {
|
||||
field = field.on_input(on_input);
|
||||
}
|
||||
row![
|
||||
text(label).size(11).style(muted_style).width(Length::Fixed(16.0)),
|
||||
field
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn read_only_field<'a>(label: String, value: &'a str, label_width: f32) -> Element<'a, Message> {
|
||||
row![
|
||||
text(label)
|
||||
.size(11)
|
||||
.style(muted_style)
|
||||
.width(Length::Fixed(label_width)),
|
||||
text_input("", value)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill)
|
||||
.style(field_style),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn view_window<'a>(
|
||||
state: &'a XrefAttachState,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
// ── Name ────────────────────────────────────────────────────────────
|
||||
let name_combo = combo_box(
|
||||
&state.name_combo,
|
||||
"",
|
||||
if state.name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&state.name)
|
||||
},
|
||||
|chosen: String| msg(XrefAttachMsg::Name(chosen)),
|
||||
)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill);
|
||||
let name_section = column![
|
||||
text(t!("Name:")).size(11).style(muted_style),
|
||||
row![
|
||||
name_combo,
|
||||
button(text(t!("Browse...")).size(11))
|
||||
.on_press(msg(XrefAttachMsg::Browse))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12])
|
||||
.width(Length::Fixed(96.0)),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(3);
|
||||
|
||||
// ── Preview + reference type ────────────────────────────────────────
|
||||
let preview_body: Element<'a, Message> = match &state.preview {
|
||||
Some(handle) => image(handle.clone()).width(Fill).height(Fill).into(),
|
||||
None => Space::new().width(Fill).height(Fill).into(),
|
||||
};
|
||||
let preview = group(
|
||||
t!("Preview").into_owned(),
|
||||
container(preview_body)
|
||||
.width(Fill)
|
||||
.height(Length::Fixed(132.0))
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(iced::Background::Color(
|
||||
theme.palette().background.base.color,
|
||||
)),
|
||||
border: iced::Border {
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
color: theme.palette().background.strong.color,
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
Length::Shrink,
|
||||
);
|
||||
let reference_type = group(
|
||||
t!("Reference Type").into_owned(),
|
||||
column![
|
||||
radio(
|
||||
t!("Attachment"),
|
||||
false,
|
||||
Some(state.overlay),
|
||||
|v| msg(XrefAttachMsg::Overlay(v))
|
||||
)
|
||||
.size(14)
|
||||
.text_size(11),
|
||||
radio(t!("Overlay"), true, Some(state.overlay), |v| msg(
|
||||
XrefAttachMsg::Overlay(v)
|
||||
))
|
||||
.size(14)
|
||||
.text_size(11),
|
||||
]
|
||||
.spacing(6),
|
||||
Length::Shrink,
|
||||
);
|
||||
let left = column![preview, reference_type]
|
||||
.spacing(6)
|
||||
.width(Length::FillPortion(1));
|
||||
|
||||
// ── Scale + insertion point ─────────────────────────────────────────
|
||||
let scale_enabled = !state.scale_on_screen;
|
||||
let scale = group(
|
||||
t!("Scale").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Specify On-screen"), state.scale_on_screen, |v| msg(
|
||||
XrefAttachMsg::ScaleOnScreen(v)
|
||||
)),
|
||||
number_field("X:", &state.scale[0], scale_enabled, |v| msg(
|
||||
XrefAttachMsg::Scale(0, v)
|
||||
)),
|
||||
number_field(
|
||||
"Y:",
|
||||
&state.scale[1],
|
||||
scale_enabled && !state.uniform,
|
||||
|v| msg(XrefAttachMsg::Scale(1, v))
|
||||
),
|
||||
number_field(
|
||||
"Z:",
|
||||
&state.scale[2],
|
||||
scale_enabled && !state.uniform,
|
||||
|v| msg(XrefAttachMsg::Scale(2, v))
|
||||
),
|
||||
labeled_checkbox(t!("Uniform Scale"), state.uniform, |v| msg(
|
||||
XrefAttachMsg::Uniform(v)
|
||||
)),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let insert_enabled = !state.insert_on_screen;
|
||||
let insertion = group(
|
||||
t!("Insertion point").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Specify On-screen"), state.insert_on_screen, |v| msg(
|
||||
XrefAttachMsg::InsertOnScreen(v)
|
||||
)),
|
||||
number_field("X:", &state.insert[0], insert_enabled, |v| msg(
|
||||
XrefAttachMsg::Insert(0, v)
|
||||
)),
|
||||
number_field("Y:", &state.insert[1], insert_enabled, |v| msg(
|
||||
XrefAttachMsg::Insert(1, v)
|
||||
)),
|
||||
number_field("Z:", &state.insert[2], insert_enabled, |v| msg(
|
||||
XrefAttachMsg::Insert(2, v)
|
||||
)),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let middle = column![scale, insertion]
|
||||
.spacing(6)
|
||||
.width(Length::FillPortion(1));
|
||||
|
||||
// ── Path type, rotation, block unit ─────────────────────────────────
|
||||
let path_type = group(
|
||||
t!("Path type").into_owned(),
|
||||
pick_list(Some(state.path_type), PathTypeChoice::ALL, |choice| choice.to_string())
|
||||
.on_select(|v| msg(XrefAttachMsg::PathType(v)))
|
||||
.text_size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill),
|
||||
Length::Shrink,
|
||||
);
|
||||
let mut angle = text_input("", &state.rotation)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill)
|
||||
.style(field_style);
|
||||
if !state.rotation_on_screen {
|
||||
angle = angle.on_input(|v| msg(XrefAttachMsg::Rotation(v)));
|
||||
}
|
||||
let rotation = group(
|
||||
t!("Rotation").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Specify On-screen"), state.rotation_on_screen, |v| msg(
|
||||
XrefAttachMsg::RotationOnScreen(v)
|
||||
)),
|
||||
row![
|
||||
text(t!("Angle:"))
|
||||
.size(11)
|
||||
.style(muted_style)
|
||||
.width(Length::Fixed(44.0)),
|
||||
angle
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let block_unit = group(
|
||||
t!("Block Unit").into_owned(),
|
||||
column![
|
||||
read_only_field(t!("Unit:").into_owned(), &state.unit_label, 52.0),
|
||||
read_only_field(t!("Factor:").into_owned(), &state.unit_factor, 52.0),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let right = column![path_type, rotation, block_unit]
|
||||
.spacing(6)
|
||||
.width(Length::FillPortion(1));
|
||||
|
||||
let body = row![left, middle, right].spacing(6).width(Fill);
|
||||
|
||||
// ── Details ─────────────────────────────────────────────────────────
|
||||
let details: Element<'a, Message> = if state.details {
|
||||
group(
|
||||
t!("Location").into_owned(),
|
||||
column![
|
||||
read_only_field(t!("Found in:").into_owned(), &state.found_in, 92.0),
|
||||
read_only_field(t!("Saved path:").into_owned(), &state.saved_path, 92.0),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
)
|
||||
} else {
|
||||
Space::new().height(0).into()
|
||||
};
|
||||
|
||||
// ── Footer ──────────────────────────────────────────────────────────
|
||||
let details_label = if state.details {
|
||||
t!("Hide Details")
|
||||
} else {
|
||||
t!("Show Details")
|
||||
};
|
||||
let footer = row![
|
||||
button(text(details_label).size(11))
|
||||
.on_press(msg(XrefAttachMsg::Details(!state.details)))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12]),
|
||||
Space::new().width(Fill),
|
||||
button(text(t!("OK")).size(11))
|
||||
.on_press(msg(XrefAttachMsg::Apply))
|
||||
.style(button_style(true))
|
||||
.padding([4, 16]),
|
||||
button(text(t!("Cancel")).size(11))
|
||||
.on_press(Message::CloseModal)
|
||||
.style(button_style(false))
|
||||
.padding([4, 12]),
|
||||
button(text(t!("Help")).size(11))
|
||||
.on_press(msg(XrefAttachMsg::Help))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12]),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
|
||||
column![name_section, body, details, footer]
|
||||
.spacing(6)
|
||||
.padding([8, 10])
|
||||
.width(sizing.width)
|
||||
.into()
|
||||
}
|
||||
Reference in new issue
Block a user