//! 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 acadrust::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; #[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, pub style: String, pub source: TableSource, pub links: Vec, pub link: Option, 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, pub selected: Option, pub editing: bool, pub editing_handle: Option, pub name: String, pub path: String, pub path_type: LinkPathType, pub range_kind: LinkRange, pub range: String, pub sheets: Vec, pub sheet: String, pub named_ranges: Vec, 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>, 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, pub selection_handles: Vec, pub objects: Vec, pub properties: Vec, pub categories: BTreeSet, pub combine_identical: bool, pub show_count: bool, pub show_name: bool, pub preview: Vec>, pub output_table: bool, pub output_file: bool, pub output_path: String, pub table_styles: Vec, 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, 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, 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, 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, msg: Message) -> Element<'a, Message> { let mark = if selected { "◉" } else { "○" }; button(row![text(mark).size(13), text(label.into()).size(12)].spacing(6)) .on_press(msg).padding([4, 7]).style(button::text).into() } fn preview_grid<'a>(rows: &'a [Vec]) -> 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::>().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), button(text(t!("Cancel")).size(12)).on_press(Message::CloseModal).padding([5, 12]), button(text(t!("OK")).size(12)).on_press(Message::TableInsertApply).style(button::primary).padding([5, 12])].spacing(7), ].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), button(text(t!("Cancel")).size(12)).on_press(Message::DataLinkEditCancel).padding([5, 11]), button(text(t!("OK")).size(12)).on_press(Message::DataLinkSave).style(button::primary).padding([5, 11])].spacing(7) } else { let enabled = state.selected.is_some(); let edit = button(text(t!("Edit")).size(12)).padding([5, 11]); let delete = button(text(t!("Delete")).size(12)).padding([5, 11]); let insert = button(text(t!("Insert Table")).size(12)).padding([5, 11]); row![if enabled { edit.on_press(Message::DataLinkEdit) } else { edit }, if enabled { delete.on_press(Message::DataLinkDelete) } else { delete }, Space::new().width(Fill), if enabled { insert.on_press(Message::DataLinkInsert) } else { insert }, button(text(t!("Close")).size(12)).on_press(Message::DataLinkClose).padding([5, 11])].spacing(7) }; 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 = button(text(t!("Back")).size(12)).padding([5, 12]); let next = button(text(if state.page == ExtractionPage::Finish { t!("Finish") } else { t!("Next") }).size(12)).padding([5, 12]).style(button::primary); row![Space::new().width(Fill), if state.page == ExtractionPage::Begin { back } else { back.on_press(Message::DataExtractionBack) }, button(text(t!("Cancel")).size(12)).on_press(Message::CloseModal).padding([5, 12]), if state.error.is_empty() { next.on_press(if state.page == ExtractionPage::Finish { Message::DataExtractionFinish } else { Message::DataExtractionNext }) } else { next }].spacing(7) } pub fn data_extraction_view(state: &DataExtractionState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> { 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]), button(text(t!("Add folder…")).size(12)).on_press(Message::DataExtractionAddFolder).padding([5, 10]), 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() }