fix(annotate): complete table and data workflows

This commit is contained in:
ramox81 committed 2026-09-14 14:54:55 +03:00
1 parent 6505b27818
commit a35c996f9f
17 files changed
+2481 -145

No files matched your search

Generated
+99 -1
View File
@@ -13,6 +13,7 @@ dependencies = [
"bytemuck",
"cadkernel",
"cadkernel-constraints",
"calamine",
"clap",
"console_error_panic_hook",
"cosmic-text 0.15.0",
@@ -587,6 +588,16 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "atoi_simd"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cdb3708a128e559a30fb830e8a77a5022ee6902806925c216658652b452a44"
dependencies = [
"debug_unsafe",
"rustversion",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -912,6 +923,23 @@ dependencies = [
"nalgebra",
]
[[package]]
name = "calamine"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fa68281b1a76b54a62156474adb06bb380a67e07dd60656e3217152b42183f3"
dependencies = [
"atoi_simd",
"byteorder",
"codepage",
"encoding_rs",
"fast-float2",
"log",
"quick-xml 0.41.0",
"serde",
"zip",
]
[[package]]
name = "calloop"
version = "0.13.0"
@@ -1058,6 +1086,15 @@ dependencies = [
"error-code",
]
[[package]]
name = "codepage"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdff162541cd8b79de82e2edcc7eff3a8c2a6dc3d75152636028f96d93de3b26"
dependencies = [
"encoding_rs",
]
[[package]]
name = "codespan-reporting"
version = "0.12.0"
@@ -1342,6 +1379,12 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
name = "debug_unsafe"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eed2c4702fa172d1ce21078faa7c5203e69f5394d48cc436d25928394a867a2"
[[package]]
name = "defmt"
version = "1.1.1"
@@ -1671,6 +1714,12 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "fast-float2"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e8948ce679d00a02a94739ea185595dca7118ed04feb991127e443bd3d761f"
[[package]]
name = "fastrand"
version = "2.4.1"
@@ -1721,6 +1770,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
"zlib-rs",
]
[[package]]
@@ -4504,6 +4554,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]]
name = "quote"
version = "1.0.46"
@@ -5934,6 +5994,12 @@ dependencies = [
"rustc-hash 2.1.3",
]
[[package]]
name = "typed-path"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
[[package]]
name = "typeid"
version = "1.0.3"
@@ -6364,7 +6430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
dependencies = [
"proc-macro2",
"quick-xml",
"quick-xml 0.39.4",
"quote",
]
@@ -7329,12 +7395,44 @@ dependencies = [
"syn 3.0.3",
]
[[package]]
name = "zip"
version = "8.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
dependencies = [
"crc32fast",
"flate2",
"indexmap",
"memchr",
"typed-path",
"zopfli",
]
[[package]]
name = "zlib-rs"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
[[package]]
name = "zune-core"
version = "0.5.1"
+1
View File
@@ -19,6 +19,7 @@ winresource = "0.1"
[dependencies]
serde_json = "1"
serde = { version = "1", features = ["derive"] }
calamine = "0.36.1"
base64 = "0.22"
semver = "1"
url = "2"
File diff suppressed because it is too large. Load diff
+5 -2
View File
@@ -2474,6 +2474,7 @@ impl OpenCADStudio {
CmdResult::CommitManyAndEditText {
entities,
edit_index,
open_editor,
} => {
let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = entities
@@ -2527,8 +2528,10 @@ impl OpenCADStudio {
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
if let Some(h) = edit_handle {
return self.begin_text_edit(h);
if open_editor {
if let Some(h) = edit_handle {
return self.begin_text_edit(h);
}
}
}
CmdResult::CreateBlock {
+1 -26
View File
@@ -668,32 +668,7 @@ impl OpenCADStudio {
}
"TABLE" => {
use crate::modules::annotate::table_cmd::TableCommand;
let name = self.tabs[i]
.scene
.document
.header
.current_table_style_name
.clone();
let style = self.tabs[i]
.scene
.document
.objects
.iter()
.find_map(|(handle, object)| match object {
acadrust::objects::ObjectType::TableStyle(style)
if style.name.eq_ignore_ascii_case(&name) =>
{
Some((*handle, style.clone()))
}
_ => None,
});
let multiplier = self.tabs[i].scene.creation_annotation_multiplier();
let cmd = style.as_ref().map_or_else(TableCommand::new, |(handle, style)| {
TableCommand::with_style(*handle, style, multiplier)
});
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
self.open_table_insert();
}
"TABLEDIT" => {
+29 -30
View File
@@ -1585,10 +1585,7 @@ impl OpenCADStudio {
// DATALINK <path.csv> — create a persistent linked table.
"DATALINK" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new("DATALINK", "DATALINK path to the .csv file:");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
self.open_data_link_manager(false);
}
cmd if cmd.starts_with("DATALINK ") => {
let path = cmd.trim_start_matches("DATALINK").trim();
@@ -1690,20 +1687,7 @@ impl OpenCADStudio {
let Some(link_handle) = link_handle else {
continue;
};
let path = match self.tabs[i].scene.document.objects.get(&link_handle) {
Some(acadrust::objects::ObjectType::ClassObject(object)) => {
match &object.data {
acadrust::objects::ClassObjectData::DataLink(link) => {
Some(link.connection_string.clone())
}
_ => None,
}
}
_ => None,
};
if let Some(path) = path {
jobs.push((*handle, link_handle, path));
}
jobs.push((*handle, link_handle));
}
if jobs.is_empty() {
self.command_line
@@ -1712,28 +1696,43 @@ impl OpenCADStudio {
}
if write_back {
let mut written = 0usize;
for (table_handle, _, path) in &jobs {
let mut errors = Vec::new();
for (table_handle, link_handle) in &jobs {
let Some(acadrust::EntityType::Table(table)) =
self.tabs[i].scene.document.get_entity(*table_handle)
else {
continue;
};
let csv = table_to_csv(table);
if std::fs::write(path, csv).is_ok() {
written += 1;
match crate::app::annotation_data::data_link_write_path(
&self.tabs[i].scene.document,
*link_handle,
)
.and_then(|path| std::fs::write(path, csv).map_err(|error| error.to_string()))
{
Ok(()) => written += 1,
Err(error) => errors.push(error),
}
}
self.command_line.push_output(
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
);
if written > 0 {
self.command_line.push_output(
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
);
}
for error in errors {
self.command_line.push_error(&error);
}
return Some(Task::none());
}
let updates: Vec<_> = jobs
.into_iter()
.filter_map(|(table_handle, link_handle, path)| {
std::fs::read_to_string(&path)
.ok()
.map(|text| (table_handle, link_handle, parse_csv_table(&text)))
.filter_map(|(table_handle, link_handle)| {
crate::app::annotation_data::read_data_link(
&self.tabs[i].scene.document,
link_handle,
)
.ok()
.map(|rows| (table_handle, link_handle, rows))
})
.filter(|(_, _, rows)| !rows.is_empty())
.collect();
@@ -1888,7 +1887,7 @@ fn parse_landxml_cgpoints(xml: &str) -> Vec<[f64; 3]> {
out
}
fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
pub(crate) fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
let mut rows = Vec::new();
let mut row = Vec::new();
let mut field = String::new();
@@ -1928,7 +1927,7 @@ fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
rows
}
fn table_to_csv(table: &acadrust::entities::Table) -> String {
pub(crate) fn table_to_csv(table: &acadrust::entities::Table) -> String {
fn escape(value: &str) -> String {
if value.contains([',', '"', '\r', '\n']) {
format!("\"{}\"", value.replace('"', "\"\""))
+1 -78
View File
@@ -1198,8 +1198,7 @@ impl OpenCADStudio {
}
"DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => {
let csv = build_data_extraction_csv(&self.tabs[i].scene.document);
return Some(Task::done(Message::DataExtractionSave(csv)));
self.open_data_extraction();
}
// ── Find / Replace ────────────────────────────────────────────────
@@ -1935,82 +1934,6 @@ fn z_axis_normal(normal: acadrust::types::Vector3) -> bool {
normal.x == 0.0 && normal.y == 0.0 && normal.z != 0.0
}
// ── DATAEXTRACTION ─────────────────────────────────────────────────────────
/// Build a CSV string with one row per entity in model space.
/// Columns: Type, Handle, Layer, Color, Linetype, ExtraInfo
fn build_data_extraction_csv(doc: &acadrust::CadDocument) -> String {
use acadrust::EntityType;
let mut out = String::from("Type,Handle,Layer,Color,Linetype,ExtraInfo\n");
let ms_handle = doc.header.model_space_block_handle;
for e in doc.entities() {
// Skip Block/EndBlock sentinels and paper-space entities.
if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) {
continue;
}
if !ms_handle.is_null() && e.common().owner_handle != ms_handle {
continue;
}
let type_name = crate::entities::names::dxf_name(e);
let handle = format!("{:X}", e.common().handle.value());
let layer = csv_escape(&e.common().layer);
let color = format!("{}", e.common().color);
let lt = csv_escape(&e.common().linetype);
let extra = csv_escape(&entity_extra_info(e));
out.push_str(&format!(
"{type_name},{handle},{layer},{color},{lt},{extra}\n"
));
}
out
}
/// Return a short geometry summary for CSV ExtraInfo column.
fn entity_extra_info(entity: &acadrust::EntityType) -> String {
use acadrust::EntityType;
match entity {
EntityType::Line(e) => format!(
"({:.3},{:.3})-({:.3},{:.3})",
e.start.x, e.start.y, e.end.x, e.end.y
),
EntityType::Circle(e) => {
format!("C({:.3},{:.3}) R={:.3}", e.center.x, e.center.y, e.radius)
}
EntityType::Arc(e) => format!(
"C({:.3},{:.3}) R={:.3} {:.1}°-{:.1}°",
e.center.x,
e.center.y,
e.radius,
e.start_angle.to_degrees(),
e.end_angle.to_degrees()
),
EntityType::Text(e) => e.value.clone(),
EntityType::MText(e) => e.value.chars().take(60).collect(),
EntityType::Insert(e) => format!(
"BLK={} @({:.3},{:.3})",
e.block_name, e.insert_point.x, e.insert_point.y
),
EntityType::LwPolyline(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Polyline(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Polyline2D(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Polyline3D(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Hatch(e) => format!("PAT={}", e.pattern.name),
EntityType::Dimension(e) => format!("{:.3}", e.base().actual_measurement),
EntityType::Spline(e) => crate::tf!("{} ctrl pts", e.control_points.len()).into_owned(),
_ => String::new(),
}
}
/// Escape a string for a CSV field (wrap in quotes if it contains comma/quote/newline).
fn csv_escape(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}
// ── CAL — arithmetic expression evaluator ──────────────────────────────────
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
// decimal numbers. Self-contained (no external dependency).
+1 -1
View File
@@ -6,7 +6,7 @@ use std::path::PathBuf;
mod blocks;
mod dim;
mod display;
pub(crate) mod display;
mod draw;
mod fileops;
mod inquiry;
+43
View File
@@ -29,6 +29,7 @@ mod startup;
mod style_ops;
mod text_inline;
mod tolerance_dialog;
mod annotation_data;
mod update;
mod view;
mod visibility;
@@ -1092,6 +1093,12 @@ pub(super) struct OpenCADStudio {
/// without Apply, mirroring the style managers' staging.
scale_stage: Option<crate::app::style_ops::ScaleStage>,
// ── Annotation table/data dialogs ────────────────────────────────────
table_insert: crate::ui::window::annotation_data::TableInsertState,
data_link_manager: crate::ui::window::annotation_data::DataLinkManagerState,
data_link_parent_table: bool,
data_extraction: crate::ui::window::annotation_data::DataExtractionState,
// ── Plot Style Panel ──────────────────────────────────────────────────
/// Selected ACI index in the panel (1-255).
plotstyle_panel_aci: u8,
@@ -1747,6 +1754,9 @@ pub enum ModalKind {
/// Add / remove the annotation scales a single selected object has a
/// per-object representation for.
AnnoObjectScale,
InsertTable,
DataLinkManager,
DataExtraction,
/// The scene is drawn by a software rasterizer, or not at all: what that
/// means and what usually fixes it. Queued once per verdict; the status
/// bar's ⚠ pill reopens it.
@@ -3412,6 +3422,35 @@ pub enum Message {
/// Background extraction/write completion.
WblockWriteFinished(String, std::path::PathBuf, Result<(), String>),
// ── DATAEXTRACTION ────────────────────────────────────────────────────
TableInsertStyle(String),
TableInsertField(crate::ui::window::annotation_data::TableInsertField),
TableInsertApply,
DataLinkManagerOpen,
DataLinkNew,
DataLinkSelect(acadrust::types::Handle),
DataLinkEdit,
DataLinkEditCancel,
DataLinkField(crate::ui::window::annotation_data::DataLinkField),
DataLinkBrowse,
DataLinkBrowseResult(Option<std::path::PathBuf>),
DataLinkSave,
DataLinkDelete,
DataLinkInsert,
DataLinkClose,
DataExtractionOpen,
DataExtractionField(crate::ui::window::annotation_data::DataExtractionField),
DataExtractionBack,
DataExtractionNext,
DataExtractionBrowseSettings,
DataExtractionBrowseSettingsResult(Option<std::path::PathBuf>),
DataExtractionAddDrawings,
DataExtractionAddDrawingsResult(Vec<std::path::PathBuf>),
DataExtractionAddFolder,
DataExtractionAddFolderResult(Option<std::path::PathBuf>),
DataExtractionClearSources,
DataExtractionBrowseOutput,
DataExtractionBrowseOutputResult(Option<std::path::PathBuf>),
DataExtractionFinish,
/// Save the pre-built CSV string to a file chosen by the user.
DataExtractionSave(String),
/// Path chosen (or None = cancelled).
@@ -3780,6 +3819,10 @@ impl OpenCADStudio {
anno_object_scale_target: None,
scale_rename_buf: String::new(),
scale_stage: None,
table_insert: Default::default(),
data_link_manager: Default::default(),
data_link_parent_table: false,
data_extraction: Default::default(),
layout_manager_rename_buf: String::new(),
plotstyle_panel_aci: 1,
ps_color_buf: String::new(),
+53
View File
@@ -1070,6 +1070,59 @@ impl OpenCADStudio {
Task::none()
}
Message::TableInsertStyle(value) => self.on_table_insert_style(value),
Message::TableInsertField(field) => self.on_table_insert_field(field),
Message::TableInsertApply => self.on_table_insert_apply(),
Message::DataLinkManagerOpen => {
let from_table = self.active_modal == Some(super::ModalKind::InsertTable);
self.open_data_link_manager(from_table);
Task::none()
}
Message::DataLinkNew => self.on_data_link_new(),
Message::DataLinkSelect(handle) => self.on_data_link_select(handle),
Message::DataLinkEdit => self.on_data_link_edit(),
Message::DataLinkEditCancel => {
self.data_link_manager.editing = false;
self.data_link_manager.editing_handle = None;
self.data_link_manager.status.clear();
Task::none()
}
Message::DataLinkField(field) => self.on_data_link_field(field),
Message::DataLinkBrowse => self.on_data_link_browse(),
Message::DataLinkBrowseResult(path) => self.on_data_link_browse_result(path),
Message::DataLinkSave => self.on_data_link_save(),
Message::DataLinkDelete => self.on_data_link_delete(),
Message::DataLinkInsert => self.on_data_link_insert(),
Message::DataLinkClose => self.on_data_link_close(),
Message::DataExtractionOpen => {
self.open_data_extraction();
Task::none()
}
Message::DataExtractionField(field) => self.on_data_extraction_field(field),
Message::DataExtractionBack => self.on_data_extraction_back(),
Message::DataExtractionNext => self.on_data_extraction_next(),
Message::DataExtractionBrowseSettings => self.on_data_extraction_browse_settings(),
Message::DataExtractionBrowseSettingsResult(path) => {
self.on_data_extraction_browse_settings_result(path)
}
Message::DataExtractionAddDrawings => self.on_data_extraction_add_drawings(),
Message::DataExtractionAddDrawingsResult(paths) => {
self.on_data_extraction_add_drawings_result(paths)
}
Message::DataExtractionAddFolder => self.on_data_extraction_add_folder(),
Message::DataExtractionAddFolderResult(path) => {
self.on_data_extraction_add_folder_result(path)
}
Message::DataExtractionClearSources => {
self.data_extraction.source_files.clear();
Task::none()
}
Message::DataExtractionBrowseOutput => self.on_data_extraction_browse_output(),
Message::DataExtractionBrowseOutputResult(path) => {
self.on_data_extraction_browse_output_result(path)
}
Message::DataExtractionFinish => self.on_data_extraction_finish(),
Message::DataExtractionSave(csv) => {
let csv_clone = csv.clone();
Task::perform(
+21
View File
@@ -31,6 +31,9 @@ impl OpenCADStudio {
Some(K::LayoutManager) => crate::tr!("modal", "layout-manager"),
Some(K::ScaleManager) => crate::tr!("modal", "scale-manager"),
Some(K::AnnoObjectScale) => crate::tr!("modal", "annotation-object-scale"),
Some(K::InsertTable) => crate::t!("Insert Table").into_owned(),
Some(K::DataLinkManager) => crate::t!("Data Link Manager").into_owned(),
Some(K::DataExtraction) => crate::t!("Data Extraction Wizard").into_owned(),
Some(K::Plotstyle) => crate::tr!("modal", "plot-style-editor"),
Some(K::TextStyle) => crate::tr!("modal", "text-style-manager"),
Some(K::MlStyle) => crate::tr!("modal", "multiline-style-manager"),
@@ -581,6 +584,24 @@ impl OpenCADStudio {
},
)
}
super::super::ModalKind::InsertTable => sized_flow(
ex,
620,
650,
|flow| crate::ui::window::annotation_data::table_insert_view(&self.table_insert, flow),
),
super::super::ModalKind::DataLinkManager => sized_flow(
ex,
760,
560,
|flow| crate::ui::window::annotation_data::data_link_view(&self.data_link_manager, flow),
),
super::super::ModalKind::DataExtraction => sized_flow(
ex,
780,
570,
|flow| crate::ui::window::annotation_data::data_extraction_view(&self.data_extraction, flow),
),
super::super::ModalKind::Plotstyle => sized_flow(
ex,
780,
+1
View File
@@ -1384,6 +1384,7 @@ pub enum CmdResult {
CommitManyAndEditText {
entities: Vec<EntityType>,
edit_index: usize,
open_editor: bool,
},
/// Create a block definition from existing entities and insert one reference.
CreateBlock {
+44 -2
View File
@@ -22,6 +22,7 @@ pub fn tool() -> ToolDef {
pub struct DataLinkPlaceCommand {
table: Table,
plane: WorkingPlane,
command_name: &'static str,
}
impl DataLinkPlaceCommand {
@@ -30,6 +31,43 @@ impl DataLinkPlaceCommand {
Self {
table,
plane: WorkingPlane::default(),
command_name: "DATALINK",
}
}
/// Place a table that references a data-link object already stored in the
/// drawing. Every populated cell is locked and points at the same link so
/// update and save/reopen keep one stable relationship.
pub fn existing(mut table: Table, link: acadrust::Handle) -> Self {
use acadrust::entities::table::CellStateFlags;
let rows = table.row_count() as i32;
let columns = table.column_count() as i32;
for row in &mut table.rows {
for cell in &mut row.cells {
cell.has_linked_data = true;
cell.data_link_handle = Some(link);
cell.data_link_rows = rows;
cell.data_link_columns = columns;
cell.state.insert(
CellStateFlags::LINKED
| CellStateFlags::CONTENT_LOCKED
| CellStateFlags::FORMAT_LOCKED,
);
}
}
Self {
table,
plane: WorkingPlane::default(),
command_name: "DATALINK",
}
}
/// Place a prepared, unlinked table (used by the extraction wizard).
pub fn unlinked(table: Table) -> Self {
Self {
table,
plane: WorkingPlane::default(),
command_name: "DATAEXTRACTION",
}
}
@@ -105,11 +143,15 @@ impl CadCommand for DataLinkPlaceCommand {
}
fn name(&self) -> &'static str {
"DATALINK"
self.command_name
}
fn prompt(&self) -> String {
t!("DATALINK Specify insertion point:").into_owned()
if self.command_name == "DATALINK" {
t!("DATALINK Specify insertion point:").into_owned()
} else {
t!("DATAEXTRACTION Specify insertion point:").into_owned()
}
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
+58 -5
View File
@@ -35,6 +35,8 @@ pub fn tool() -> ToolDef {
enum Step {
Points,
Format,
Annotation,
AnnotationOptions,
}
pub struct LeaderCommand {
@@ -50,6 +52,7 @@ pub struct LeaderCommand {
gap: f64,
arrow_size: f64,
annotative: bool,
annotation_lines: Vec<String>,
}
impl LeaderCommand {
@@ -76,10 +79,11 @@ impl LeaderCommand {
gap: defaults.gap,
arrow_size: defaults.arrow_size,
annotative: defaults.annotative,
annotation_lines: Vec::new(),
}
}
fn finish(&self) -> CmdResult {
fn finish(&self, annotation: Option<&str>, open_editor: bool) -> CmdResult {
if self.verts.len() < 2 {
return CmdResult::Cancel;
}
@@ -134,8 +138,13 @@ impl LeaderCommand {
// when another representation becomes active.
let mtext_height = displayed_height;
if annotation.is_none() {
leader.creation_type = LeaderCreationType::NoAnnotation;
return CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Leader(leader)));
}
let mtext = build_mtext(
"",
annotation.unwrap_or_default(),
anchor,
mtext_height,
attach,
@@ -150,6 +159,7 @@ impl LeaderCommand {
self.plane.place_entity(EntityType::MText(mtext)),
],
edit_index: 1,
open_editor,
}
}
}
@@ -168,6 +178,16 @@ impl CadCommand for LeaderCommand {
return t!("LEADER Enter leader formatting option [Spline/Straight/Arrow/None] <exit>:")
.into_owned();
}
if self.step == Step::AnnotationOptions {
return t!("LEADER Enter an annotation option [None/Mtext]:").into_owned();
}
if self.step == Step::Annotation {
return if self.annotation_lines.is_empty() {
t!("LEADER Enter first line of annotation text or <options>:").into_owned()
} else {
t!("LEADER Enter next line of annotation text:").into_owned()
};
}
match self.verts.len() {
0 => t!("LEADER Specify leader start point:").into_owned(),
1 => t!("LEADER Specify next point:").into_owned(),
@@ -185,6 +205,12 @@ impl CadCommand for LeaderCommand {
CmdOption::new("None", "NONE"),
];
}
if self.step == Step::AnnotationOptions {
return vec![
CmdOption::new("None", "NONE"),
CmdOption::new("Mtext", "MTEXT"),
];
}
if self.verts.len() >= 2 {
vec![
CmdOption::new("Annotation", "ANNOTATION"),
@@ -197,8 +223,10 @@ impl CadCommand for LeaderCommand {
}
fn input_kind(&self) -> InputKind {
if self.step == Step::Format {
if matches!(self.step, Step::Format | Step::AnnotationOptions) {
InputKind::SingleToken
} else if self.step == Step::Annotation {
InputKind::FreeText
} else {
InputKind::Point
}
@@ -210,6 +238,17 @@ impl CadCommand for LeaderCommand {
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().to_ascii_uppercase();
if self.step == Step::Annotation {
self.annotation_lines.push(text.to_string());
return Some(CmdResult::NeedPoint);
}
if self.step == Step::AnnotationOptions {
return match keyword.as_str() {
"N" | "NONE" => Some(self.finish(None, false)),
"M" | "MTEXT" => Some(self.finish(Some(""), true)),
_ => None,
};
}
if self.step == Step::Format {
match keyword.as_str() {
"S" | "SPLINE" => self.path_type = LeaderPathType::Spline,
@@ -225,7 +264,10 @@ impl CadCommand for LeaderCommand {
return None;
}
match keyword.as_str() {
"A" | "ANNOTATION" => Some(self.finish()),
"A" | "ANNOTATION" => {
self.step = Step::Annotation;
Some(CmdResult::NeedPoint)
}
"F" | "FORMAT" => {
self.step = Step::Format;
Some(CmdResult::NeedPoint)
@@ -250,8 +292,19 @@ impl CadCommand for LeaderCommand {
if self.step == Step::Format {
self.step = Step::Points;
CmdResult::NeedPoint
} else if self.step == Step::Annotation {
if self.annotation_lines.is_empty() {
self.step = Step::AnnotationOptions;
CmdResult::NeedPoint
} else {
let text = self.annotation_lines.join("\n");
self.finish(Some(&text), false)
}
} else if self.step == Step::AnnotationOptions {
CmdResult::NeedPoint
} else {
self.finish()
self.step = Step::Annotation;
CmdResult::NeedPoint
}
}
+99
View File
@@ -4,6 +4,7 @@
// data rows. The command remembers its previous settings and supports either a
// fixed insertion point or a two-corner sizing window.
use acadrust::entities::table::{CellStyle, CellStylePropertyFlags};
use acadrust::entities::TableBuilder;
use acadrust::types::Vector3;
use acadrust::EntityType;
@@ -101,9 +102,47 @@ pub struct TableCommand {
preview_scale: f64,
title_row: bool,
header_row: bool,
row_style_overrides: Option<[CellStyle; 3]>,
preview_enabled: bool,
plane: WorkingPlane,
}
fn cell_style_from_row(style: &acadrust::objects::RowCellStyle) -> CellStyle {
let mut result = CellStyle::new();
result.property_flags = CellStylePropertyFlags::DATA_TYPE
| CellStylePropertyFlags::DATA_FORMAT
| CellStylePropertyFlags::ALIGNMENT
| CellStylePropertyFlags::CONTENT_COLOR
| CellStylePropertyFlags::TEXT_STYLE
| CellStylePropertyFlags::TEXT_HEIGHT
| CellStylePropertyFlags::BACKGROUND_COLOR;
result.content_color = style.text_color;
result.text_style_handle = style.text_style_handle;
result.text_style_name = style.text_style_name.clone();
result.text_height = style.text_height;
result.alignment = style.alignment as i32;
result.background_color = style.fill_color;
result.fill_enabled = style.fill_enabled;
result.value_data_type = style.data_type;
result.value_unit_type = style.unit_type;
result.value_format = style.format_string.clone();
result
}
fn selected_row_style(
table_style: &acadrust::objects::TableStyle,
name: &str,
) -> CellStyle {
let style = if name.eq_ignore_ascii_case("Title") {
&table_style.title_row_style
} else if name.eq_ignore_ascii_case("Header") {
&table_style.header_row_style
} else {
&table_style.data_row_style
};
cell_style_from_row(style)
}
impl TableCommand {
pub fn new() -> Self {
Self {
@@ -114,6 +153,8 @@ impl TableCommand {
preview_scale: 1.0,
title_row: true,
header_row: true,
row_style_overrides: None,
preview_enabled: true,
plane: WorkingPlane::default(),
}
}
@@ -141,10 +182,60 @@ impl TableCommand {
},
title_row: !style.title_suppressed,
header_row: !style.header_suppressed,
row_style_overrides: None,
preview_enabled: true,
plane: WorkingPlane::default(),
}
}
/// Build the insertion command from the values accepted by the Insert
/// Table dialog. The dialog owns validation, so the command starts at the
/// point/window placement step instead of asking for the same values again.
pub fn configured(
style: Option<(acadrust::Handle, &acadrust::objects::TableStyle)>,
annotation_multiplier: f64,
columns: usize,
data_rows: usize,
column_width: f64,
row_height: f64,
use_window: bool,
preview_enabled: bool,
row_style_names: [&str; 3],
) -> Self {
let mut command = style.map_or_else(Self::new, |(handle, style)| {
Self::with_style(handle, style, annotation_multiplier)
});
let insertion_mode = if use_window {
InsertionMode::Window
} else {
InsertionMode::Point
};
if let Ok(mut saved) = saved_defaults().lock() {
*saved = TableDefaults {
columns,
data_rows,
column_width,
row_height,
insertion_mode,
};
}
command.step = if use_window {
Step::WindowFirst { columns, data_rows }
} else {
Step::Insertion {
columns,
data_rows,
column_width,
row_height,
}
};
command.preview_enabled = preview_enabled;
command.row_style_overrides = style.map(|(_, table_style)| {
row_style_names.map(|name| selected_row_style(table_style, name))
});
command
}
fn defaults(&self) -> TableDefaults {
let mut defaults = saved_defaults().lock().map(|value| *value).unwrap_or_default();
if (defaults.column_width - DEFAULT_COL_WIDTH).abs() < 1.0e-9 {
@@ -175,6 +266,11 @@ impl TableCommand {
.column_width(column_width)
.build();
table.table_style_handle = self.style_handle;
if let Some(styles) = &self.row_style_overrides {
for (row_index, row) in table.rows.iter_mut().enumerate() {
row.style = Some(styles[row_index.min(2)].clone());
}
}
self.plane.place_entity(EntityType::Table(table))
}
@@ -447,6 +543,9 @@ impl CadCommand for TableCommand {
}
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
if !self.preview_enabled {
return None;
}
match self.step {
Step::Insertion {
columns,
+626
View File
@@ -0,0 +1,626 @@
//! 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<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> {
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<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), 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()
}
+1
View File
@@ -16,6 +16,7 @@ pub mod open_progress;
pub mod recovery;
pub mod options;
pub mod attribute_editor;
pub mod annotation_data;
pub mod alias_editor;
pub mod find_replace;
pub mod named_parameters;