Files
OneCAD-Draft/src/ui/window/annotation_data.rs
T
Hakan SevenandClaude Opus 5.5 f537aff75c feat: node graph overlay; move to opencadcodec/opencadkernel/opencadgraph
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.

A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:34:23 +03:00

657 lines
30 KiB
Rust

//! Dialogs used by the Annotate tab's table and drawing-data tools.
//!
//! The working state lives outside the drawing until the user presses OK or
//! Finish. That keeps Cancel lossless and lets the wizard validate one page
//! before advancing to the next.
use std::collections::BTreeSet;
use std::fmt;
use codec::types::Handle;
use iced::widget::{button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space};
use iced::{Border, Element, Fill, Length, Theme};
use crate::app::Message;
use crate::t;
use crate::ui::style::form::{dialog_button, dialog_button_styled_opt, form_radio};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TableSource {
#[default]
Empty,
DataLink,
DataExtraction,
}
impl fmt::Display for TableSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Empty => t!("Empty table").into_owned(),
Self::DataLink => t!("Data link").into_owned(),
Self::DataExtraction => t!("Data extraction").into_owned(),
};
f.write_str(&label)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TableInsertion {
#[default]
Point,
Window,
}
impl fmt::Display for TableInsertion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Point => t!("Specify insertion point").into_owned(),
Self::Window => t!("Specify window").into_owned(),
};
f.write_str(&label)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataLinkChoice {
pub handle: Handle,
pub name: String,
pub path: String,
pub valid: bool,
}
impl fmt::Display for DataLinkChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.valid {
write!(f, "🔗 {}", self.name)
} else {
write!(f, "⚠ {}", self.name)
}
}
}
#[derive(Debug, Clone)]
pub struct TableInsertState {
pub styles: Vec<String>,
pub style: String,
pub source: TableSource,
pub links: Vec<DataLinkChoice>,
pub link: Option<DataLinkChoice>,
pub insertion: TableInsertion,
pub columns: String,
pub data_rows: String,
pub column_width: String,
pub row_height: String,
pub preview: bool,
pub first_row_style: String,
pub second_row_style: String,
pub other_row_style: String,
pub error: String,
}
impl Default for TableInsertState {
fn default() -> Self {
Self {
styles: vec!["Standard".into()],
style: "Standard".into(),
source: TableSource::Empty,
links: Vec::new(),
link: None,
insertion: TableInsertion::Point,
columns: "3".into(),
data_rows: "4".into(),
column_width: "2.0".into(),
row_height: "0.5".into(),
preview: true,
first_row_style: "Title".into(),
second_row_style: "Header".into(),
other_row_style: "Data".into(),
error: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub enum TableInsertField {
Style(String),
Source(TableSource),
Link(DataLinkChoice),
Insertion(TableInsertion),
Columns(String),
DataRows(String),
ColumnWidth(String),
RowHeight(String),
Preview(bool),
FirstRowStyle(String),
SecondRowStyle(String),
OtherRowStyle(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LinkPathType {
#[default]
Full,
Relative,
FileName,
}
impl fmt::Display for LinkPathType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Full => t!("Full path").into_owned(),
Self::Relative => t!("Relative path").into_owned(),
Self::FileName => t!("No path").into_owned(),
};
f.write_str(&label)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LinkRange {
#[default]
EntireSheet,
NamedRange,
CellRange,
}
impl fmt::Display for LinkRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::EntireSheet => t!("Entire sheet").into_owned(),
Self::NamedRange => t!("Named range").into_owned(),
Self::CellRange => t!("Cell range").into_owned(),
};
f.write_str(&label)
}
}
#[derive(Debug, Clone)]
pub struct DataLinkManagerState {
pub links: Vec<DataLinkChoice>,
pub selected: Option<DataLinkChoice>,
pub editing: bool,
pub editing_handle: Option<Handle>,
pub name: String,
pub path: String,
pub path_type: LinkPathType,
pub range_kind: LinkRange,
pub range: String,
pub sheets: Vec<String>,
pub sheet: String,
pub named_ranges: Vec<String>,
pub allow_write: bool,
pub use_source_formatting: bool,
pub update_source_formatting: bool,
pub insert_table: bool,
pub more_options: bool,
pub preview: Vec<Vec<String>>,
pub status: String,
}
impl Default for DataLinkManagerState {
fn default() -> Self {
Self {
links: Vec::new(),
selected: None,
editing: false,
editing_handle: None,
name: String::new(),
path: String::new(),
path_type: LinkPathType::Full,
range_kind: LinkRange::EntireSheet,
range: String::new(),
sheets: Vec::new(),
sheet: String::new(),
named_ranges: Vec::new(),
allow_write: false,
use_source_formatting: true,
update_source_formatting: true,
insert_table: false,
more_options: false,
preview: Vec::new(),
status: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub enum DataLinkField {
Name(String),
Path(String),
PathType(LinkPathType),
RangeKind(LinkRange),
Range(String),
Sheet(String),
AllowWrite(bool),
UseSourceFormatting(bool),
UpdateSourceFormatting(bool),
InsertTable(bool),
MoreOptions(bool),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExtractionPage {
#[default]
Begin,
Source,
Objects,
Properties,
Refine,
Output,
TableStyle,
Finish,
}
impl ExtractionPage {
pub const fn number(self) -> usize {
match self {
Self::Begin => 1,
Self::Source => 2,
Self::Objects => 3,
Self::Properties => 4,
Self::Refine => 5,
Self::Output => 6,
Self::TableStyle => 7,
Self::Finish => 8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExtractionBegin {
#[default]
New,
Template,
Edit,
}
impl fmt::Display for ExtractionBegin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::New => t!("Create a new data extraction").into_owned(),
Self::Template => t!("Use a previous extraction as a template").into_owned(),
Self::Edit => t!("Edit an existing data extraction").into_owned(),
};
f.write_str(&label)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExtractionSource {
#[default]
CurrentDrawing,
CurrentSelection,
DrawingsAndFolders,
}
impl fmt::Display for ExtractionSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::CurrentDrawing => t!("Current drawing").into_owned(),
Self::CurrentSelection => t!("Selected objects in current drawing").into_owned(),
Self::DrawingsAndFolders => t!("Drawings / folders").into_owned(),
};
f.write_str(&label)
}
}
#[derive(Debug, Clone)]
pub struct ExtractionObject {
pub name: String,
pub checked: bool,
pub count: usize,
}
#[derive(Debug, Clone)]
pub struct ExtractionProperty {
pub key: String,
pub name: String,
pub category: String,
pub checked: bool,
}
#[derive(Debug, Clone)]
pub struct DataExtractionState {
pub page: ExtractionPage,
pub begin: ExtractionBegin,
pub settings_path: String,
pub source: ExtractionSource,
pub include_current: bool,
pub include_subfolders: bool,
pub source_files: Vec<String>,
pub selection_handles: Vec<Handle>,
pub objects: Vec<ExtractionObject>,
pub properties: Vec<ExtractionProperty>,
pub categories: BTreeSet<String>,
pub combine_identical: bool,
pub show_count: bool,
pub show_name: bool,
pub preview: Vec<Vec<String>>,
pub output_table: bool,
pub output_file: bool,
pub output_path: String,
pub table_styles: Vec<String>,
pub table_style: String,
pub table_title: String,
pub error: String,
}
impl Default for DataExtractionState {
fn default() -> Self {
Self {
page: ExtractionPage::Begin,
begin: ExtractionBegin::New,
settings_path: String::new(),
source: ExtractionSource::CurrentDrawing,
include_current: true,
include_subfolders: true,
source_files: Vec::new(),
selection_handles: Vec::new(),
objects: Vec::new(),
properties: vec![
ExtractionProperty { key: "type".into(), name: "Type".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "handle".into(), name: "Handle".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "layer".into(), name: "Layer".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "color".into(), name: "Color".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "linetype".into(), name: "Linetype".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "details".into(), name: "Details".into(), category: "Geometry".into(), checked: true },
],
categories: ["General".to_string(), "Geometry".to_string()].into_iter().collect(),
combine_identical: false,
show_count: true,
show_name: true,
preview: Vec::new(),
output_table: true,
output_file: false,
output_path: String::new(),
table_styles: vec!["Standard".into()],
table_style: "Standard".into(),
table_title: "Data Extraction".into(),
error: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub enum DataExtractionField {
Begin(ExtractionBegin),
SettingsPath(String),
Source(ExtractionSource),
IncludeCurrent(bool),
IncludeSubfolders(bool),
Object(usize, bool),
Property(usize, bool),
Category(String, bool),
CombineIdentical(bool),
ShowCount(bool),
ShowName(bool),
OutputTable(bool),
OutputFile(bool),
OutputPath(String),
TableStyle(String),
TableTitle(String),
}
fn muted(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.62)),
}
}
fn group<'a>(title: impl Into<String>, body: Element<'a, Message>) -> Element<'a, Message> {
container(column![text(title.into()).size(11).style(muted), body].spacing(7))
.padding(9)
.width(Fill)
.style(|theme: &Theme| container::Style {
border: Border { width: 1.0, radius: 4.0.into(), color: theme.palette().background.strong.color },
..Default::default()
})
.into()
}
fn labeled_input<'a>(label: impl Into<String>, value: &'a str, msg: fn(String) -> Message) -> Element<'a, Message> {
row![text(label.into()).size(11).style(muted).width(118), text_input("", value).on_input(msg).padding([4, 7]).size(12).width(Fill)]
.spacing(8).align_y(iced::Center).into()
}
fn cell_style_picker<'a>(
label: impl Into<String>,
selected: &'a str,
msg: fn(String) -> Message,
) -> Element<'a, Message> {
row![
text(label.into()).size(11).style(muted).width(118),
pick_list(
Some(selected.to_string()),
vec!["Title".to_string(), "Header".to_string(), "Data".to_string()],
|value| value.clone(),
)
.on_select(msg)
.padding([3, 6])
.text_size(12)
.width(Fill),
]
.spacing(8)
.align_y(iced::Center)
.into()
}
fn radio_button<'a>(selected: bool, label: impl Into<String>, msg: Message) -> Element<'a, Message> {
form_radio(label, true, selected.then_some(true), move |_| msg.clone()).into()
}
fn preview_grid<'a>(rows: &'a [Vec<String>]) -> Element<'a, Message> {
if rows.is_empty() {
return text(t!("No preview available.")).size(11).style(muted).into();
}
let lines = rows.iter().take(8).map(|row| row.join(" | ")).collect::<Vec<_>>().join("\n");
container(text(lines).size(11)).padding(6).width(Fill).into()
}
pub fn table_insert_view(state: &TableInsertState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
let style = pick_list(Some(state.style.clone()), state.styles.clone(), |value| value.clone()).on_select(Message::TableInsertStyle).padding([3, 6]).text_size(12).width(Fill);
let source = column![
radio_button(state.source == TableSource::Empty, t!("Start from empty table"), Message::TableInsertField(TableInsertField::Source(TableSource::Empty))),
radio_button(state.source == TableSource::DataLink, t!("Start from data link"), Message::TableInsertField(TableInsertField::Source(TableSource::DataLink))),
radio_button(state.source == TableSource::DataExtraction, t!("Start from data extraction"), Message::TableInsertField(TableInsertField::Source(TableSource::DataExtraction))),
].spacing(1);
let link_picker: Element<'_, Message> = if state.source == TableSource::DataLink {
row![
text(t!("Data link")).size(11).style(muted).width(118),
pick_list(state.link.clone(), state.links.clone(), |value| value.to_string()).on_select(|value| Message::TableInsertField(TableInsertField::Link(value))).padding([3, 6]).text_size(12).width(Fill),
button(text(t!("Manage…")).size(11)).on_press(Message::DataLinkManagerOpen).padding([4, 8]),
].spacing(8).align_y(iced::Center).into()
} else { Space::new().height(0).into() };
let insertion = column![
radio_button(state.insertion == TableInsertion::Point, t!("Specify insertion point"), Message::TableInsertField(TableInsertField::Insertion(TableInsertion::Point))),
radio_button(state.insertion == TableInsertion::Window, t!("Specify window"), Message::TableInsertField(TableInsertField::Insertion(TableInsertion::Window))),
];
let settings = column![
labeled_input(t!("Columns"), &state.columns, |v| Message::TableInsertField(TableInsertField::Columns(v))),
labeled_input(t!("Column width"), &state.column_width, |v| Message::TableInsertField(TableInsertField::ColumnWidth(v))),
labeled_input(t!("Data rows"), &state.data_rows, |v| Message::TableInsertField(TableInsertField::DataRows(v))),
labeled_input(t!("Row height"), &state.row_height, |v| Message::TableInsertField(TableInsertField::RowHeight(v))),
].spacing(5);
let cell_styles = column![
cell_style_picker(t!("First row"), &state.first_row_style, |v| Message::TableInsertField(TableInsertField::FirstRowStyle(v))),
cell_style_picker(t!("Second row"), &state.second_row_style, |v| Message::TableInsertField(TableInsertField::SecondRowStyle(v))),
cell_style_picker(t!("All other rows"), &state.other_row_style, |v| Message::TableInsertField(TableInsertField::OtherRowStyle(v))),
].spacing(5);
column![
group(t!("Table style"), row![style].into()),
group(t!("Insert options"), column![source, link_picker].spacing(4).into()),
row![group(t!("Insertion behavior"), insertion.into()), group(t!("Column & row settings"), settings.into())].spacing(8),
group(t!("Set cell styles"), cell_styles.into()),
checkbox(state.preview).label(t!("Preview")).on_toggle(|v| Message::TableInsertField(TableInsertField::Preview(v))).size(14),
text(&state.error).size(11),
row![
Space::new().width(Fill),
dialog_button(t!("Cancel"), Message::CloseModal, false),
dialog_button(t!("OK"), Message::TableInsertApply, true),
].spacing(8),
].spacing(8).padding(10).width(sizing.width).height(sizing.height).into()
}
pub fn data_link_view(state: &DataLinkManagerState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
let mut links = column![text(t!("Spreadsheet Links")).size(11).style(muted)].spacing(2);
links = links.push(button(text(t!("Create a New Data Link")).size(12)).on_press(Message::DataLinkNew).padding([5, 7]).style(button::text).width(Fill));
for link in &state.links {
let selected = state.selected.as_ref().is_some_and(|v| v.handle == link.handle);
let label = if selected { format!("● {link}") } else { format!(" {link}") };
links = links.push(button(text(label).size(12)).on_press(Message::DataLinkSelect(link.handle)).padding([5, 7]).style(button::text).width(Fill));
}
let tree = container(scrollable(links).height(Fill)).padding(6).width(Length::Fixed(220.0)).height(Fill);
let right: Element<'_, Message> = if state.editing {
let mut editor = column![
labeled_input(t!("Data link name"), &state.name, |v| Message::DataLinkField(DataLinkField::Name(v))),
row![text(t!("File")).size(11).style(muted).width(118), text_input("", &state.path).on_input(|v| Message::DataLinkField(DataLinkField::Path(v))).padding([4, 7]).size(12).width(Fill), button(text("…")).on_press(Message::DataLinkBrowse).padding([4, 9])].spacing(6).align_y(iced::Center),
row![text(t!("Path type")).size(11).style(muted).width(118), pick_list(Some(state.path_type), vec![LinkPathType::Full, LinkPathType::Relative, LinkPathType::FileName], |value| value.to_string()).on_select(|value| Message::DataLinkField(DataLinkField::PathType(value))).width(Fill).padding([3, 6])].spacing(8),
row![text(t!("Link option")).size(11).style(muted).width(118), pick_list(Some(state.range_kind), vec![LinkRange::EntireSheet, LinkRange::NamedRange, LinkRange::CellRange], |value| value.to_string()).on_select(|value| Message::DataLinkField(DataLinkField::RangeKind(value))).width(Fill).padding([3, 6])].spacing(8),
].spacing(6);
if !state.sheets.is_empty() && state.range_kind != LinkRange::NamedRange {
editor = editor.push(row![
text(t!("Sheet")).size(11).style(muted).width(118),
pick_list(Some(state.sheet.clone()), state.sheets.clone(), |value| value.clone())
.on_select(|value| Message::DataLinkField(DataLinkField::Sheet(value)))
.width(Fill)
.padding([3, 6]),
].spacing(8));
}
if state.range_kind == LinkRange::NamedRange && !state.named_ranges.is_empty() {
editor = editor.push(row![
text(t!("Named range")).size(11).style(muted).width(118),
pick_list(Some(state.range.clone()), state.named_ranges.clone(), |value| value.clone())
.on_select(|value| Message::DataLinkField(DataLinkField::Range(value)))
.width(Fill)
.padding([3, 6]),
].spacing(8));
} else if state.range_kind != LinkRange::EntireSheet {
editor = editor.push(labeled_input(t!("Range"), &state.range, |v| Message::DataLinkField(DataLinkField::Range(v))));
}
editor = editor.push(checkbox(state.more_options).label(t!("More options")).on_toggle(|v| Message::DataLinkField(DataLinkField::MoreOptions(v))).size(14));
if state.more_options {
editor = editor
.push(checkbox(state.allow_write).label(t!("Allow writing to source file")).on_toggle(|v| Message::DataLinkField(DataLinkField::AllowWrite(v))).size(14))
.push(checkbox(state.use_source_formatting).label(t!("Use source formatting")).on_toggle(|v| Message::DataLinkField(DataLinkField::UseSourceFormatting(v))).size(14))
.push(checkbox(state.update_source_formatting).label(t!("Keep table updated to source formatting")).on_toggle(|v| Message::DataLinkField(DataLinkField::UpdateSourceFormatting(v))).size(14));
}
editor = editor.push(checkbox(state.insert_table).label(t!("Insert table after saving link")).on_toggle(|v| Message::DataLinkField(DataLinkField::InsertTable(v))).size(14));
group(t!("Link settings"), editor.into())
} else if let Some(link) = &state.selected {
column![
group(t!("Details"), column![text(format!("{}: {}", t!("Name"), link.name)).size(11), text(format!("{}: {}", t!("File"), link.path)).size(11), text(if link.valid { t!("Status: Linked") } else { t!("Status: Source not found") }).size(11)].spacing(5).into()),
group(t!("Preview"), preview_grid(&state.preview)),
].spacing(8).into()
} else {
group(t!("Details"), text(t!("Select a data link or create a new one.")).size(11).style(muted).into())
};
let actions = if state.editing {
row![
Space::new().width(Fill),
dialog_button(t!("Cancel"), Message::DataLinkEditCancel, false),
dialog_button(t!("OK"), Message::DataLinkSave, true),
]
.spacing(8)
} else {
let enabled = state.selected.is_some();
let edit = dialog_button_styled_opt(t!("Edit"), enabled.then_some(Message::DataLinkEdit), button::secondary);
let delete = dialog_button_styled_opt(t!("Delete"), enabled.then_some(Message::DataLinkDelete), button::secondary);
let insert = dialog_button_styled_opt(t!("Insert Table"), enabled.then_some(Message::DataLinkInsert), button::primary);
let close = dialog_button(t!("Close"), Message::DataLinkClose, false);
row![edit, delete, Space::new().width(Fill), insert, close].spacing(8)
};
column![row![tree, container(right).width(Fill).height(Fill)].spacing(8).height(Fill), text(&state.status).size(11), actions]
.spacing(7).padding(10).width(sizing.width).height(sizing.height).into()
}
fn wizard_nav(state: &DataExtractionState) -> iced::widget::Row<'_, Message> {
let back = dialog_button_styled_opt(
t!("Back"),
(state.page != ExtractionPage::Begin).then_some(Message::DataExtractionBack),
button::secondary,
);
let cancel = dialog_button(t!("Cancel"), Message::CloseModal, false);
let next = dialog_button_styled_opt(
if state.page == ExtractionPage::Finish {
t!("Finish")
} else {
t!("Next")
},
state.error.is_empty().then_some(if state.page == ExtractionPage::Finish {
Message::DataExtractionFinish
} else {
Message::DataExtractionNext
}),
button::primary,
);
row![Space::new().width(Fill), back, cancel, next].spacing(8)
}
pub fn data_extraction_view(state: &DataExtractionState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
let add_folder = button(text(t!("Add folder…")).size(12)).padding([5, 10]);
#[cfg(not(target_arch = "wasm32"))]
let add_folder = add_folder.on_press(Message::DataExtractionAddFolder);
let body: Element<'_, Message> = match state.page {
ExtractionPage::Begin => group(t!("Begin"), column![
radio_button(state.begin == ExtractionBegin::New, t!("Create a new data extraction"), Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::New))),
radio_button(state.begin == ExtractionBegin::Template, t!("Use a previous extraction as a template"), Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::Template))),
radio_button(state.begin == ExtractionBegin::Edit, t!("Edit an existing data extraction"), Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::Edit))),
row![text(t!("Extraction settings")).size(11).style(muted).width(118), text_input("", &state.settings_path).on_input(|v| Message::DataExtractionField(DataExtractionField::SettingsPath(v))).padding([4, 7]).size(12).width(Fill), button(text("…")).on_press(Message::DataExtractionBrowseSettings).padding([4, 9])].spacing(6).align_y(iced::Center),
].spacing(5).into()),
ExtractionPage::Source => group(t!("Define data source"), column![
radio_button(state.source == ExtractionSource::CurrentDrawing, t!("Current drawing"), Message::DataExtractionField(DataExtractionField::Source(ExtractionSource::CurrentDrawing))),
radio_button(state.source == ExtractionSource::CurrentSelection, t!("Select objects in the current drawing"), Message::DataExtractionField(DataExtractionField::Source(ExtractionSource::CurrentSelection))),
radio_button(state.source == ExtractionSource::DrawingsAndFolders, t!("Drawings / folders"), Message::DataExtractionField(DataExtractionField::Source(ExtractionSource::DrawingsAndFolders))),
checkbox(state.include_current).label(t!("Include current drawing")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::IncludeCurrent(v))).size(14),
checkbox(state.include_subfolders).label(t!("Include subfolders")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::IncludeSubfolders(v))).size(14),
row![
button(text(t!("Add drawings…")).size(12)).on_press(Message::DataExtractionAddDrawings).padding([5, 10]),
add_folder,
button(text(t!("Clear")).size(12)).on_press(Message::DataExtractionClearSources).padding([5, 10]),
].spacing(6),
text(state.source_files.join("\n")).size(10),
].spacing(6).into()),
ExtractionPage::Objects => {
let mut list = column![].spacing(3);
for (i, item) in state.objects.iter().enumerate() {
list = list.push(checkbox(item.checked).label(format!("{} ({})", item.name, item.count)).on_toggle(move |v| Message::DataExtractionField(DataExtractionField::Object(i, v))).size(14));
}
group(t!("Select objects"), scrollable(list).height(Fill).into())
}
ExtractionPage::Properties => {
let mut categories = row![].spacing(10);
for category in ["General", "Geometry"] {
let checked = state.categories.contains(category);
let key = category.to_string();
categories = categories.push(checkbox(checked).label(category).on_toggle(move |v| Message::DataExtractionField(DataExtractionField::Category(key.clone(), v))).size(14));
}
let mut list = column![categories].spacing(4);
for (i, item) in state.properties.iter().enumerate().filter(|(_, p)| state.categories.contains(&p.category)) {
list = list.push(checkbox(item.checked).label(format!("{} · {}", item.name, item.category)).on_toggle(move |v| Message::DataExtractionField(DataExtractionField::Property(i, v))).size(14));
}
group(t!("Select properties"), scrollable(list).height(Fill).into())
}
ExtractionPage::Refine => group(t!("Refine data"), column![
row![checkbox(state.combine_identical).label(t!("Combine identical rows")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::CombineIdentical(v))).size(14), checkbox(state.show_count).label(t!("Show count column")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::ShowCount(v))).size(14), checkbox(state.show_name).label(t!("Show name column")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::ShowName(v))).size(14)].spacing(12),
scrollable(preview_grid(&state.preview)).height(Fill),
].spacing(8).into()),
ExtractionPage::Output => group(t!("Choose output"), column![
checkbox(state.output_table).label(t!("Insert data extraction table into drawing")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::OutputTable(v))).size(14),
checkbox(state.output_file).label(t!("Output data to external file")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::OutputFile(v))).size(14),
row![text(t!("Output file")).size(11).style(muted).width(118), text_input("", &state.output_path).on_input(|v| Message::DataExtractionField(DataExtractionField::OutputPath(v))).padding([4, 7]).size(12).width(Fill), button(text("…")).on_press(Message::DataExtractionBrowseOutput).padding([4, 9])].spacing(6).align_y(iced::Center),
].spacing(7).into()),
ExtractionPage::TableStyle => group(t!("Table style"), column![
row![text(t!("Style")).size(11).style(muted).width(118), pick_list(Some(state.table_style.clone()), state.table_styles.clone(), |value| value.clone()).on_select(|value| Message::DataExtractionField(DataExtractionField::TableStyle(value))).padding([3, 6]).width(Fill)].spacing(8),
labeled_input(t!("Title"), &state.table_title, |v| Message::DataExtractionField(DataExtractionField::TableTitle(v))),
preview_grid(&state.preview),
].spacing(7).into()),
ExtractionPage::Finish => group(t!("Finish"), column![text(t!("The selected drawing data is ready to be extracted.")).size(12), text(format!("{}: {}", t!("Rows"), state.preview.len().saturating_sub(1))).size(11), text(format!("{}: {}{}", t!("Output"), if state.output_table { t!("Drawing table") } else { "".into() }, if state.output_file { format!(" {}", t!("External file")) } else { String::new() })).size(11)].spacing(7).into()),
};
column![
text(format!("{} {}/8", t!("Data Extraction"), state.page.number())).size(13),
container(body).height(Fill).width(Fill),
text(&state.error).size(11),
wizard_nav(state),
].spacing(8).padding(10).width(sizing.width).height(sizing.height).into()
}