From 87211f4654aadb2ec5df743063d194796f72a60e Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:56:54 +0300 Subject: [PATCH] FIELD: Field dialog, FIELD command and the ATTDEF field buttons A Field dialog with the fields the application evaluates and the reference format choices, FIELD placing a field as multi-line text, the attribute definition dialogs' field buttons, field attachment on commit, system variable / file size / regional date providers for field evaluation, and the codec patch with the field authoring API. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 6 +- locales/ar-SA/opencadstudio.ftl | 30 ++ locales/bg-BG/opencadstudio.ftl | 30 ++ locales/cs-CZ/opencadstudio.ftl | 30 ++ locales/de-DE/opencadstudio.ftl | 30 ++ locales/el-GR/opencadstudio.ftl | 30 ++ locales/en-US/opencadstudio.ftl | 30 ++ locales/es-ES/opencadstudio.ftl | 30 ++ locales/fi-FI/opencadstudio.ftl | 30 ++ locales/fr-FR/opencadstudio.ftl | 30 ++ locales/hi-IN/opencadstudio.ftl | 30 ++ locales/hu-HU/opencadstudio.ftl | 30 ++ locales/it-IT/opencadstudio.ftl | 30 ++ locales/ja-JP/opencadstudio.ftl | 30 ++ locales/ko-KR/opencadstudio.ftl | 30 ++ locales/nl-NL/opencadstudio.ftl | 30 ++ locales/pl-PL/opencadstudio.ftl | 30 ++ locales/pt-BR/opencadstudio.ftl | 30 ++ locales/ru-RU/opencadstudio.ftl | 30 ++ locales/tr-TR/opencadstudio.ftl | 30 ++ locales/zh-CN/opencadstudio.ftl | 30 ++ locales/zh-TW/opencadstudio.ftl | 30 ++ src/app/command_driver/entity_commit.rs | 19 +- src/app/commands/attdef.rs | 35 +- src/app/commands/field.rs | 206 ++++++++++ src/app/commands/mod.rs | 4 + src/app/control/actions.rs | 35 ++ src/app/mod.rs | 5 + src/app/update/mod.rs | 5 + src/app/view/modal.rs | 5 + src/command.rs | 6 + src/entities/field.rs | 181 ++++++++- src/locale_catalog.rs | 30 ++ src/modules/annotate/field_cmd.rs | 190 +++++++++ src/modules/annotate/mod.rs | 1 + src/modules/draw/draw/attdef.rs | 7 + src/ui/window/attdef_dialog.rs | 14 +- src/ui/window/field_dialog.rs | 491 ++++++++++++++++++++++++ src/ui/window/mod.rs | 1 + 40 files changed, 1859 insertions(+), 14 deletions(-) create mode 100644 src/app/commands/field.rs create mode 100644 src/modules/annotate/field_cmd.rs create mode 100644 src/ui/window/field_dialog.rs diff --git a/Cargo.lock b/Cargo.lock index a2ce9f354..6996f8143 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=d28fd71#d28fd710268e7301798703d3ca9c5698a1af91ed" +source = "git+https://github.com/ramox81/cadcodec.git?rev=22a5c3e#22a5c3e07bf9eda6b7937e951906f8aa53fdeaf3" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 5ced7c8f8..c9b2a9433 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ naga = { version = "27", features = ["wgsl-in"] } [target.'cfg(target_os = "windows")'.dependencies] libloading = "0.8" -windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Security", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing", "Win32_Graphics_Gdi", "Win32_Storage_Xps"] } +windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Security", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing", "Win32_Graphics_Gdi", "Win32_Storage_Xps", "Win32_Globalization"] } [target.'cfg(target_os = "linux")'.dependencies] ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] } @@ -90,6 +90,10 @@ objc2-app-kit = { version = "0.2", features = ["NSApplication", "NSResponder", " block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } +# Field authoring API and field format codes (opencadcodec PR pending). +[patch."https://github.com/HakanSeven12/opencadcodec.git"] +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "22a5c3e" } + [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 90ca7ab4d..e6f68ac6a 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -4053,6 +4053,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = إنشاء .boolean = عمليات منطقية diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 2db9fb1be..e4fbebcc8 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -4040,6 +4040,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Създаване .boolean = Булеви операции diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 568d18c19..2899fe9ad 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -4038,6 +4038,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Vytvořit .boolean = Booleovské operace diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 854583d3c..16cbf3cb4 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -4039,6 +4039,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Erstellen .boolean = Boolesche Operationen diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 1bc916265..ec8a36752 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -4041,6 +4041,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Δημιουργία .boolean = Δυαδική πράξη diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index fc3a39030..f0e040b93 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -4042,6 +4042,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Create .boolean = Boolean diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 8154c0646..d304992f1 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -4041,6 +4041,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Crear .boolean = Booleanas diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index c49419d28..a0b803861 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -4040,6 +4040,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Luo .boolean = Boolen operaatiot diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 8a393e26d..63e30ebc5 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -4041,6 +4041,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Créer .boolean = Opérations booléennes diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index ff357f061..e73aeaaca 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -4029,6 +4029,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = बनाएँ .boolean = बूलियन संक्रियाएँ diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 053a20ff4..661f31f9a 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -4040,6 +4040,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Létrehozás .boolean = Logikai műveletek diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 541650845..076e47305 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -4040,6 +4040,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Crea .boolean = Operazioni booleane diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 0a7379a95..d6d39f90e 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -4029,6 +4029,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = 作成 .boolean = ブール演算 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 6103f015b..2d0268da0 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -4040,6 +4040,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = 생성 .boolean = 불리언 연산 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 79975ad46..3d132c8dd 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -4039,6 +4039,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Maken .boolean = Booleaanse bewerkingen diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index f1c5ceae0..de75a9ba0 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -4040,6 +4040,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Utwórz .boolean = Operacje boolowskie diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 1c3ddf058..e6747180f 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -4041,6 +4041,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Criar .boolean = Operações booleanas diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 96391bb95..6b65c9a04 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -4042,6 +4042,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = Создание .boolean = Булевы операции diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index e7d0a2914..aacafb9bd 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -4001,6 +4001,36 @@ insert = .attdef-tag-spaces = Etiket boşluk içeremez. .attdef-positive = Değer pozitif ve sıfırdan farklı olmalı. .attdef-angle = Geçerli bir sayısal açı veya ikinci nokta gerekir. + .field-title = Alan + .field-category = Kategori + .field-date-format = Tarih biçimi + .field-examples = Örnekler + .field-file-extension = Dosya uzantısını göster + .field-filename-only = Yalnız dosya adı + .field-path-only = Yalnız yol + .field-path-filename = Yol ve dosya adı + .field-bytes = Bayt + .field-kilobytes = Kilobayt + .field-megabytes = Megabayt + .field-system-variable = Sistem değişkeni + .field-diesel = Diesel ifadesi + .field-named-type = Adlandırılmış nesne türü + .field-object-type = Nesne türü + .field-select-object = Nesne seç + .field-expression = Alan ifadesi + .field-help = Bir alan ekler: çizimden, tarihten veya başka bir kaynaktan güncellenen metin. + .field-cat-date = Tarih ve Saat + .field-cat-document = Belge + .field-cat-other = Diğer + .field-case-upper = Büyük harf + .field-case-lower = Küçük harf + .field-case-first = İlk harf büyük + .field-case-title = Başlık + .field-named-dimstyle = Ölçü stili + .field-named-tablestyle = Tablo stili + .field-named-textstyle = Metin stili + .field-prop-endpoint = Bitiş noktası + .field-prop-startpoint = Başlangıç noktası model = .create = Oluştur .boolean = Boole işlemleri diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 8a71f831c..c36d3ea01 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -4024,6 +4024,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = 创建 .boolean = 布尔运算 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index ec5f8dc6c..609a3d99a 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -4036,6 +4036,36 @@ insert = .attdef-tag-spaces = The tag may not contain spaces. .attdef-positive = Value must be positive and nonzero. .attdef-angle = Requires valid numeric angle or second point. + .field-title = Field + .field-category = Category + .field-date-format = Date format + .field-examples = Examples + .field-file-extension = Display file extension + .field-filename-only = Filename only + .field-path-only = Path only + .field-path-filename = Path and filename + .field-bytes = Bytes + .field-kilobytes = Kilobytes + .field-megabytes = Megabytes + .field-system-variable = System variable + .field-diesel = Diesel expression + .field-named-type = Named object type + .field-object-type = Object type + .field-select-object = Select object + .field-expression = Field expression + .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-cat-date = Date & Time + .field-cat-document = Document + .field-cat-other = Other + .field-case-upper = Uppercase + .field-case-lower = Lowercase + .field-case-first = First capital + .field-case-title = Title case + .field-named-dimstyle = Dimstyle + .field-named-tablestyle = Tablestyle + .field-named-textstyle = Textstyle + .field-prop-endpoint = EndPoint + .field-prop-startpoint = StartPoint model = .create = 建立 .boolean = 布林運算 diff --git a/src/app/command_driver/entity_commit.rs b/src/app/command_driver/entity_commit.rs index 13b3c1ddf..8aa9f3672 100644 --- a/src/app/command_driver/entity_commit.rs +++ b/src/app/command_driver/entity_commit.rs @@ -226,8 +226,17 @@ impl OpenCADStudio { self.tabs[i].scene.document.header.text_height = attribute.height; } } + // A field the committed text shows (FIELD, an ATTDEF Default from the + // Field dialog) is attached once the entity has its handle; the field + // objects are not part of the add delta, so the step is a snapshot. + let text_field = self.tabs[i].active_cmd.as_ref().and_then(|cmd| cmd.text_field()); let label = self.history_label_from_active_cmd(i, "ENTITY"); - let delta_safe = self.delta_add_safe(i, &entity); + let delta_safe = text_field.is_none() && self.delta_add_safe(i, &entity); + let field_value = match &entity { + codec::EntityType::MText(m) => m.value.clone(), + codec::EntityType::AttributeDefinition(a) => a.default_value.clone(), + _ => String::new(), + }; let pending = self.begin_undo(i, label, 1, delta_safe); let is_associative_dimension = matches!( entity, @@ -247,6 +256,14 @@ impl OpenCADStudio { .attach_dimension_association(handle, sources); } } + if let (Some(handle), Some(field)) = (committed, text_field) { + crate::entities::field::set_text_field( + &mut self.tabs[i].scene.document, + handle, + Some(field), + &field_value, + ); + } if let Some(handle) = committed { self.apply_continuous_constraints(i, &[handle]); } diff --git a/src/app/commands/attdef.rs b/src/app/commands/attdef.rs index ff5dabea1..9e4064d31 100644 --- a/src/app/commands/attdef.rs +++ b/src/app/commands/attdef.rs @@ -54,6 +54,7 @@ impl OpenCADStudio { if let Some(state) = self.attdef_dialog.as_mut() { if let Some(value) = value { state.default = value; + state.field = None; } self.active_modal = Some(crate::app::ModalKind::AttDef); } @@ -134,6 +135,7 @@ impl OpenCADStudio { align_below: false, can_align_below: session.last.is_some(), error: None, + field: None, }); drop(session); self.tabs[i].active_cmd = None; @@ -154,6 +156,8 @@ impl OpenCADStudio { default: a.default_value.clone(), constant: a.flags.constant, error: None, + field: None, + field_changed: false, }); self.active_modal = Some(crate::app::ModalKind::AttDefEdit); } @@ -164,7 +168,15 @@ impl OpenCADStudio { match m { AttdefDialogMsg::EditTag(v) => edit.tag = v, AttdefDialogMsg::EditPrompt(v) => edit.prompt = v, - AttdefDialogMsg::EditDefault(v) => edit.default = v, + AttdefDialogMsg::EditDefault(v) => { + edit.default = v; + edit.field = None; + edit.field_changed = true; + } + AttdefDialogMsg::EditInsertField => { + self.active_modal = None; + self.open_field_dialog(crate::ui::window::field_dialog::FieldTarget::AttdefEdit); + } AttdefDialogMsg::Help => self.command_line.push_info( crate::t!("Changes the tag, prompt and default value of an attribute definition.") .as_ref(), @@ -189,7 +201,15 @@ impl OpenCADStudio { AttdefDialogMsg::Annotative(v) => state.annotative = v, AttdefDialogMsg::Tag(v) => state.tag = v, AttdefDialogMsg::Prompt(v) => state.prompt = v, - AttdefDialogMsg::Default(v) => state.default = v, + AttdefDialogMsg::Default(v) => { + state.default = v; + state.field = None; + } + AttdefDialogMsg::InsertField => { + self.active_modal = None; + self.open_field_dialog(crate::ui::window::field_dialog::FieldTarget::AttdefDefault); + return Task::none(); + } AttdefDialogMsg::Justify(c) => state.justify = c.0, AttdefDialogMsg::Style(v) => state.style = v, AttdefDialogMsg::Height(v) => state.height = v, @@ -279,6 +299,7 @@ impl OpenCADStudio { oblique_angle: defaults.oblique_angle, boundary_width: width, line_spacing: 1.0, + field: state.field.clone(), }; self.attdef_dialog = None; self.close_active_modal(); @@ -336,7 +357,7 @@ impl OpenCADStudio { self.tabs[i].scene.document.get_entity(state.handle), Some(codec::EntityType::AttributeDefinition(a)) if a.tag != attdef::normalize_tag(&state.tag) || a.prompt != state.prompt || a.default_value != state.default - ); + ) || state.field_changed; if changed && !self.reject_locked_edit(i, state.handle) { self.push_undo_snapshot(i, "DDEDIT"); if let Some(codec::EntityType::AttributeDefinition(a)) = @@ -348,6 +369,14 @@ impl OpenCADStudio { } a.default_value = state.default.clone(); } + if state.field_changed { + crate::entities::field::set_text_field( + &mut self.tabs[i].scene.document, + state.handle, + state.field.clone(), + &state.default, + ); + } self.tabs[i] .scene .bump_entities(&[(state.handle, crate::scene::ChangeKind::Modified)]); diff --git a/src/app/commands/field.rs b/src/app/commands/field.rs new file mode 100644 index 000000000..7fd9001ee --- /dev/null +++ b/src/app/commands/field.rs @@ -0,0 +1,206 @@ +//! FIELD and the Field dialog behind the ƒ buttons of the attribute +//! definition dialogs. + +use crate::app::{Message, OpenCADStudio}; +use crate::command::CadCommand; +use crate::modules::annotate::field_cmd::{FieldObjectPickCommand, FieldPlaceCommand}; +use crate::ui::window::field_dialog::{ + fields_of, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget, DATE_FORMATS, + NAMED_TYPES, +}; +use iced::Task; + +impl OpenCADStudio { + pub(super) fn dispatch_field(&mut self, cmd: &str, i: usize) -> Option> { + match cmd { + "FIELD" => { + self.open_field_dialog(FieldTarget::NewText); + Some(Task::none()) + } + cmd if cmd == "_FIELD_OBJECT" || cmd.starts_with("_FIELD_OBJECT ") => { + let handle = cmd + .split_whitespace() + .nth(1) + .and_then(|h| u64::from_str_radix(h, 16).ok()) + .map(codec::Handle::new); + if let Some(state) = self.field_dialog.as_mut() { + if let Some(entity) = handle.and_then(|h| self.tabs[i].scene.document.get_entity(h)) { + let props = object_properties(entity); + state.object = Some((entity.common().handle, crate::t!(crate::entities::names::ui_name(entity)).into_owned())); + state.object_prop = props.first().copied(); + state.object_props = props; + } + self.active_modal = Some(crate::app::ModalKind::Field); + self.refresh_field_preview(); + } + Some(Task::none()) + } + _ => None, + } + } + + /// Open the Field dialog for `target`. + pub(in crate::app) fn open_field_dialog(&mut self, target: FieldTarget) { + let mut state = FieldDialogState::new(target); + state.examples = DATE_FORMATS + .iter() + .map(|f| self.field_value(&format!("\\AcVar Date \\f \"{f}\""), &[])) + .collect(); + self.field_dialog = Some(state); + self.fill_named_objects(); + self.refresh_field_preview(); + self.active_modal = Some(crate::app::ModalKind::Field); + } + + fn field_value(&self, code: &str, objects: &[codec::Handle]) -> String { + crate::entities::field::evaluate(&self.tabs[self.active_tab].scene.document, code, objects, None) + .unwrap_or_else(|| "----".to_string()) + } + + fn refresh_field_preview(&mut self) { + let Some(state) = self.field_dialog.as_ref() else { + return; + }; + let (code, objects) = state.code(); + let preview = if state.complete() { self.field_value(&code, &objects) } else { String::new() }; + if let Some(state) = self.field_dialog.as_mut() { + state.preview = preview; + } + } + + /// The names of the chosen named-object type, sorted. + fn fill_named_objects(&mut self) { + let Some(state) = self.field_dialog.as_mut() else { + return; + }; + let document = &self.tabs[self.active_tab].scene.document; + let mut names: Vec<(String, codec::Handle)> = match NAMED_TYPES[state.named_type] { + "Block" => document + .block_records + .iter() + .filter(|b| !b.name.starts_with('*')) + .map(|b| (b.name.clone(), b.handle)) + .collect(), + "Dimstyle" => document.dim_styles.iter().map(|s| (s.name.clone(), s.handle)).collect(), + "Layer" => document.layers.iter().map(|l| (l.name.clone(), l.handle)).collect(), + "Linetype" => document.line_types.iter().map(|l| (l.name.clone(), l.handle)).collect(), + "Textstyle" => document + .text_styles + .iter() + .filter(|s| !s.name.is_empty()) + .map(|s| (s.name.clone(), s.handle)) + .collect(), + "View" => document.views.iter().map(|v| (v.name.clone(), v.handle)).collect(), + _ => Vec::new(), + }; + names.sort_by_key(|(n, _)| n.to_lowercase()); + state.named_names = names; + state.named = None; + } + + pub(in crate::app) fn on_field_dialog(&mut self, m: FieldDialogMsg) -> Task { + let i = self.active_tab; + let Some(state) = self.field_dialog.as_mut() else { + return Task::none(); + }; + match m { + FieldDialogMsg::Category(c) => { + state.category = c; + let names = fields_of(c); + if !names.contains(&state.name) { + state.name = names.first().copied().unwrap_or("Date"); + } + } + FieldDialogMsg::Name(n) => { + state.name = n; + state.text_case = 0; + } + FieldDialogMsg::DateFormat(f) => state.date_format = f, + FieldDialogMsg::DateExample(k) => { + if let Some(f) = DATE_FORMATS.get(k) { + state.date_format = f.to_string(); + } + } + FieldDialogMsg::TextCase(k) => state.text_case = k, + FieldDialogMsg::FileParts(b) => state.file_parts = b, + FieldDialogMsg::FileExtension(v) => state.file_extension = v, + FieldDialogMsg::SizeUnit(k) => state.size_unit = k, + FieldDialogMsg::SysVar(v) => state.sysvar = v, + FieldDialogMsg::Diesel(v) => state.diesel = v, + FieldDialogMsg::NamedType(k) => { + state.named_type = k; + self.fill_named_objects(); + } + FieldDialogMsg::Named(k) => state.named = Some(k), + FieldDialogMsg::ObjectProp(p) => state.object_prop = Some(p), + FieldDialogMsg::SelectObject => { + // The dialog waits while one object is picked. + self.active_modal = None; + let command = FieldObjectPickCommand; + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + return Task::none(); + } + FieldDialogMsg::Help => self.command_line.push_info( + crate::t!("Inserts a field: text that updates from the drawing, the date or another source.").as_ref(), + ), + FieldDialogMsg::Ok => return self.field_dialog_ok(i), + } + self.refresh_field_preview(); + Task::none() + } + + fn field_dialog_ok(&mut self, i: usize) -> Task { + let Some(state) = self.field_dialog.take() else { + return Task::none(); + }; + let (code, objects) = state.code(); + let value = state.preview.clone(); + let field = Some((code, objects)); + match state.target { + FieldTarget::NewText => { + self.close_active_modal(); + let defaults = + crate::scene::creation_style::current_text_defaults(&self.tabs[i].scene.document); + let command = FieldPlaceCommand::new( + value, + field.expect("set above"), + defaults.style_name, + defaults.height, + ); + self.reset_command_start_state(i); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + self.push_ucs_to_cmd(i); + return self.focus_cmd_input(); + } + FieldTarget::AttdefDefault => { + if let Some(dialog) = self.attdef_dialog.as_mut() { + dialog.default = value; + dialog.field = field; + } + self.active_modal = Some(crate::app::ModalKind::AttDef); + } + FieldTarget::AttdefEdit => { + if let Some(edit) = self.attdef_edit.as_mut() { + edit.default = value; + edit.field = field; + edit.field_changed = true; + } + self.active_modal = Some(crate::app::ModalKind::AttDefEdit); + } + } + Task::none() + } + + /// The dialogs a Field dialog was opened from come back when it closes. + pub(in crate::app) fn close_field_dialog(&mut self) { + let target = self.field_dialog.take().map(|s| s.target); + self.close_active_modal(); + self.active_modal = match target { + Some(FieldTarget::AttdefDefault) if self.attdef_dialog.is_some() => Some(crate::app::ModalKind::AttDef), + Some(FieldTarget::AttdefEdit) if self.attdef_edit.is_some() => Some(crate::app::ModalKind::AttDefEdit), + _ => None, + }; + } +} diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 6c0027791..5a0ce5a8a 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -5,6 +5,7 @@ use iced::Task; use std::path::PathBuf; mod attdef; +mod field; mod blocks; mod xref_attach; mod dim; @@ -282,6 +283,9 @@ impl OpenCADStudio { if let Some(t) = self.dispatch_attdef(cmd, i) { return Some(t); } + if let Some(t) = self.dispatch_field(cmd, i) { + return Some(t); + } if let Some(t) = self.dispatch_draw(cmd, i) { return Some(t); } diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index b037ca839..136c621f3 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -80,6 +80,7 @@ pub(super) const NAMES: &[&str] = &[ "ribbon_dropdown", "dialog_ok", "attdef_dialog", + "field_dialog", "close_document", "toggle_properties", "toggle_layers", @@ -870,6 +871,8 @@ impl OpenCADStudio { "align_below" => M::AlignBelow(on), "edit_value" => M::EditValue, "dismiss" => M::DismissError, + "insert_field" if editing => M::EditInsertField, + "insert_field" => M::InsertField, "x" => M::Coord(0, v.into()), "y" => M::Coord(1, v.into()), "z" => M::Coord(2, v.into()), @@ -887,6 +890,35 @@ impl OpenCADStudio { _ => return Err(failure("invalid_value", "Unknown attdef dialog field")), }) } + // One field of the open Field dialog: `category=`, `name=Date`, + // `date=yyyy-MM-dd`, `case=0..4`, `file=1|2|3`, `ext=0|1`, `size=0..2`, + // `sysvar=dimscale`, `diesel=…`, `named_type=`, `named=`, + // `prop=Area`, `select_object`. + "field_dialog" => { + use crate::ui::window::field_dialog::{self as fd, FieldDialogMsg as F}; + let value = string(req, "value")?; + let (key, v) = value.split_once('=').unwrap_or((value, "")); + let index = || v.parse::().map_err(|_| failure("invalid_value", "index expected")); + let known = |list: &[&'static str]| { + list.iter().copied().find(|n| n.eq_ignore_ascii_case(v)).ok_or_else(|| failure("invalid_value", "unknown name")) + }; + Message::FieldDialog(match key { + "category" => F::Category(index()?), + "name" => F::Name(known(&fd::fields_of(0))?), + "date" => F::DateFormat(v.into()), + "case" => F::TextCase(index()?), + "file" => F::FileParts(v.parse::().map_err(|_| failure("invalid_value", "1|2|3"))?), + "ext" => F::FileExtension(v == "1"), + "size" => F::SizeUnit(index()?), + "sysvar" => F::SysVar(v.into()), + "diesel" => F::Diesel(v.into()), + "named_type" => F::NamedType(index()?), + "named" => F::Named(index()?), + "prop" => F::ObjectProp(known(&["Area", "Center", "Circumference", "Diameter", "EndPoint", "Length", "Radius", "StartPoint"])?), + "select_object" => F::SelectObject, + _ => return Err(failure("invalid_value", "Unknown field dialog key")), + }) + } "ribbon_dropdown" => Message::ToggleRibbonDropdown(string(req, "value")?.into()), "pdf_dialog_ok" => { use crate::ui::window::pdf_dialogs::PdfDialogMsg; @@ -913,6 +945,9 @@ impl OpenCADStudio { Some(crate::app::ModalKind::AttDefEdit) => Message::AttdefDialog( crate::ui::window::attdef_dialog::AttdefDialogMsg::EditOk, ), + Some(crate::app::ModalKind::Field) => Message::FieldDialog( + crate::ui::window::field_dialog::FieldDialogMsg::Ok, + ), _ => return Err(failure("no_dialog", "No dialog with an OK button is open")), }, "close_document" => Message::TabClose(self.tabs[self.active_tab].id), diff --git a/src/app/mod.rs b/src/app/mod.rs index d24cbb9a5..9c3e0776b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -501,6 +501,8 @@ pub(super) struct OpenCADStudio { /// ATTDEF dialog and Edit Attribute Definition dialog; `None` while closed. attdef_dialog: Option, attdef_edit: Option, + /// Field dialog working copy; `None` while closed. + field_dialog: Option, point_cloud_color_map: Option, pc_section: Option, underlay_layers: Option, @@ -1922,6 +1924,7 @@ pub enum ModalKind { GeometricTolerance, AttDef, AttDefEdit, + Field, DraftingSettings, AutoConstrainSettings, LayerStateEditor, @@ -3870,6 +3873,7 @@ pub enum Message { /// An edit in one of the PDF dialogs. PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg), AttdefDialog(crate::ui::window::attdef_dialog::AttdefDialogMsg), + FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg), /// A click or search in the Point Cloud Manager. PcManager(crate::ui::window::pc_manager::PcManagerMsg), PdfImportPickResult(Result<(std::path::PathBuf, std::sync::Arc>), String>), @@ -4065,6 +4069,7 @@ impl OpenCADStudio { point_cloud_attach: None, attdef_dialog: None, attdef_edit: None, + field_dialog: None, point_cloud_color_map: None, pc_section: None, underlay_layers: None, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 8bfeab79a..cd3f93700 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1813,6 +1813,7 @@ impl OpenCADStudio { Message::PdfDialog(message) => self.update_pdf_dialog(message), Message::AttdefDialog(message) => self.on_attdef_dialog(message), + Message::FieldDialog(message) => self.on_field_dialog(message), Message::PcManager(message) => self.update_pc_manager(message), Message::RibbonSelectTab(idx) => { self.ribbon.select(idx); @@ -8503,6 +8504,10 @@ impl OpenCADStudio { } Message::CloseModal => { + if self.active_modal == Some(super::ModalKind::Field) { + self.close_field_dialog(); + return Task::none(); + } if matches!( self.active_modal, Some(super::ModalKind::AttDef | super::ModalKind::AttDefEdit) diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 0b2a697b9..44c6aeb9f 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -40,6 +40,7 @@ impl OpenCADStudio { Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(), Some(K::AttDef) => crate::t!("Attribute Definition").into_owned(), Some(K::AttDefEdit) => crate::t!("Edit Attribute Definition").into_owned(), + Some(K::Field) => crate::t!("Field").into_owned(), Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(), Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(), Some(K::LayerStateEditor) => crate::tr!("modal", "edit-layer-state"), @@ -578,6 +579,10 @@ impl OpenCADStudio { let state = self.attdef_dialog.as_ref()?; sized_flow(ex, 720, 520, |flow| crate::ui::window::attdef_dialog::view(state, flow)) } + super::super::ModalKind::Field => { + let state = self.field_dialog.as_ref()?; + sized_flow(ex, 760, 660, |flow| crate::ui::window::field_dialog::view(state, flow)) + } super::super::ModalKind::AttDefEdit => { let state = self.attdef_edit.as_ref()?; sized_flow(ex, 460, 220, |flow| crate::ui::window::attdef_dialog::view_edit(state, flow)) diff --git a/src/command.rs b/src/command.rs index 427c15a1b..f8a5abfe2 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2628,6 +2628,12 @@ pub trait CadCommand: Send { /// Resume the command with collected rich text. fn on_editor_text(&mut self, _value: String) {} + /// A field (code and referenced objects) to attach to the entity this + /// command commits. + fn text_field(&self) -> Option<(String, Vec)> { + None + } + fn on_editor_display_height(&mut self, _height: f64) {} /// Called when the user clicks and `needs_entity_pick()` is true. diff --git a/src/entities/field.rs b/src/entities/field.rs index f41638bbd..9cd0cf4c7 100644 --- a/src/entities/field.rs +++ b/src/entities/field.rs @@ -11,10 +11,11 @@ use codec::fields::FieldContext; use codec::types::Handle; use codec::CadDocument; -/// OCS's environment provider for field evaluation. -struct OcsFieldContext; +/// OCS's environment provider for field evaluation. With the document at +/// hand it also answers the system variables the engine asks the host for. +struct OcsFieldContext<'a>(Option<&'a CadDocument>); -impl FieldContext for OcsFieldContext { +impl FieldContext for OcsFieldContext<'_> { fn now_julian(&self) -> f64 { // Unix epoch is Julian Day 2440587.5. epoch_secs() as f64 / 86_400.0 + 2_440_587.5 @@ -34,6 +35,24 @@ impl FieldContext for OcsFieldContext { None } + fn getvar(&self, name: &str) -> Option { + self.0.and_then(|document| sysvar(document, name)) + } + + fn file_size(&self) -> Option { + #[cfg(not(target_arch = "wasm32"))] + { + let path = self.0?.source_path.as_deref()?; + return std::fs::metadata(path).ok().map(|m| m.len()); + } + #[cfg(target_arch = "wasm32")] + None + } + + fn date_locale(&self) -> codec::fields::DateLocale { + os_date_locale() + } + fn getenv(&self, name: &str) -> Option { #[cfg(not(target_arch = "wasm32"))] { @@ -50,7 +69,7 @@ impl FieldContext for OcsFieldContext { /// Re-evaluate the field hosted by entity `host` (usually an MTEXT), or `None` /// to keep the cached text. Thin wrapper over the library engine. pub fn resolve(document: &CadDocument, host: Handle) -> Option { - codec::fields::resolve(document, host, &OcsFieldContext) + codec::fields::resolve(document, host, &OcsFieldContext(Some(document))) } pub fn resolve_handle( @@ -58,7 +77,87 @@ pub fn resolve_handle( field: Handle, host: Handle, ) -> Option { - codec::fields::resolve_handle(document, field, host, &OcsFieldContext) + codec::fields::resolve_handle(document, field, host, &OcsFieldContext(Some(document))) +} + +/// Evaluate one field code (`\AcVar Date \f "yyyy-MM-dd"`, or the `%<…>%` +/// form) for a preview; `None` when it cannot be evaluated. +pub fn evaluate( + document: &CadDocument, + code: &str, + objects: &[Handle], + host: Option, +) -> Option { + codec::fields::evaluate_code(document, code, objects, host, &OcsFieldContext(Some(document))) +} + +/// The system variables the Field dialog lists, in its order (lower case). +pub const SYSVARS: &[&str] = &[ + "aunits", "auprec", "celtscale", "chamfera", "chamferb", "chamferc", "chamferd", + "clayer", "dimscale", "dimtxt", "dwgname", "dwgprefix", "elevation", "filletrad", + "insbase", "insunits", "limmax", "limmin", "ltscale", "lunits", "luprec", + "measurement", "mirrtext", "pdmode", "pdsize", "textsize", "textstyle", "thickness", + "useri1", "useri2", "useri3", "useri4", "useri5", "userr1", "userr2", "userr3", + "userr4", "userr5", +]; + +fn num(v: f64) -> String { + let t = format!("{v:.4}"); + t.trim_end_matches('0').trim_end_matches('.').to_string() +} + +/// A system variable's value as a field shows it. +pub fn sysvar(document: &CadDocument, name: &str) -> Option { + let h = &document.header; + let path = document.source_path.as_deref().unwrap_or(""); + let (folder, file) = match path.rfind(['/', '\\']) { + Some(i) => (&path[..=i], &path[i + 1..]), + None => ("", path), + }; + Some(match name.trim().to_ascii_lowercase().as_str() { + "aunits" => h.angular_unit_format.to_string(), + "auprec" => h.angular_unit_precision.to_string(), + "celtscale" => num(h.current_entity_linetype_scale), + "chamfera" => num(h.chamfer_distance_a), + "chamferb" => num(h.chamfer_distance_b), + "chamferc" => num(h.chamfer_length), + "chamferd" => num(h.chamfer_angle), + "clayer" => h.current_layer_name.clone(), + "dimscale" => num(h.dim_scale), + "dimtxt" => num(h.dim_text_height), + "dwgname" => if file.is_empty() { "Drawing1.dwg".to_string() } else { file.to_string() }, + "dwgprefix" => folder.to_string(), + "elevation" => num(h.elevation), + "filletrad" => num(h.fillet_radius), + "insbase" => { + let p = h.model_space_insertion_base; + format!("{},{},{}", num(p.x), num(p.y), num(p.z)) + } + "insunits" => h.insertion_units.to_string(), + "limmax" => format!("{},{}", num(h.model_space_limits_max.x), num(h.model_space_limits_max.y)), + "limmin" => format!("{},{}", num(h.model_space_limits_min.x), num(h.model_space_limits_min.y)), + "ltscale" => num(h.linetype_scale), + "lunits" => h.linear_unit_format.to_string(), + "luprec" => h.linear_unit_precision.to_string(), + "measurement" => h.measurement.to_string(), + "mirrtext" => i16::from(h.mirror_text).to_string(), + "pdmode" => h.point_display_mode.to_string(), + "pdsize" => num(h.point_display_size), + "textsize" => num(h.text_height), + "textstyle" => h.current_text_style_name.clone(), + "thickness" => num(h.thickness), + "useri1" => h.user_int1.to_string(), + "useri2" => h.user_int2.to_string(), + "useri3" => h.user_int3.to_string(), + "useri4" => h.user_int4.to_string(), + "useri5" => h.user_int5.to_string(), + "userr1" => num(h.user_real1), + "userr2" => num(h.user_real2), + "userr3" => num(h.user_real3), + "userr4" => num(h.user_real4), + "userr5" => num(h.user_real5), + _ => return None, + }) } /// Seconds since the Unix epoch. Native uses the system clock; wasm uses the JS @@ -77,3 +176,75 @@ fn epoch_secs() -> i64 { (js_sys::Date::now() / 1000.0) as i64 } } + +/// Attach `field` (its code and referenced objects) to `host`, showing +/// `value`; `None` removes the host's field. +pub fn set_text_field( + document: &mut CadDocument, + host: Handle, + field: Option<(String, Vec)>, + value: &str, +) { + match field { + Some((code, objects)) => { + let mut child = codec::fields::NewField::new(code, value); + child.objects = objects; + document.set_text_field(host, "%<\\_FldIdx 0>%", vec![child]); + } + None => { + document.remove_text_field(host); + } + } +} + +/// Month and day names and the regional date / time pictures, as date +/// fields show them: the operating system's regional settings. +fn os_date_locale() -> codec::fields::DateLocale { + #[cfg(target_os = "windows")] + { + use windows_sys::Win32::Globalization::GetLocaleInfoEx; + let info = |kind: u32| -> Option { + let mut buffer = [0u16; 128]; + // A null locale name is the user's default locale. + let len = unsafe { GetLocaleInfoEx(std::ptr::null(), kind, buffer.as_mut_ptr(), buffer.len() as i32) }; + (len > 1).then(|| String::from_utf16_lossy(&buffer[..len as usize - 1])) + }; + let mut locale = codec::fields::DateLocale::default(); + for k in 0..12u32 { + if let Some(v) = info(0x38 + k) { + locale.months[k as usize] = v; // LOCALE_SMONTHNAME1.. + } + if let Some(v) = info(0x44 + k) { + locale.months_abbr[k as usize] = v; // LOCALE_SABBREVMONTHNAME1.. + } + } + // LOCALE_SDAYNAME1 is Monday; the codec lists Sunday first. + for k in 0..7u32 { + let slot = ((k + 1) % 7) as usize; + if let Some(v) = info(0x2A + k) { + locale.days[slot] = v; + } + if let Some(v) = info(0x31 + k) { + locale.days_abbr[slot] = v; + } + } + if let Some(v) = info(0x28) { + locale.am = v; // LOCALE_S1159 + } + if let Some(v) = info(0x29) { + locale.pm = v; // LOCALE_S2359 + } + if let Some(v) = info(0x1F) { + locale.short_date = v; // LOCALE_SSHORTDATE + } + if let Some(v) = info(0x20) { + locale.long_date = v; // LOCALE_SLONGDATE + } + if let Some(v) = info(0x1003) { + locale.long_time = v; // LOCALE_STIMEFORMAT + } + locale + } + #[cfg(not(target_os = "windows"))] + codec::fields::DateLocale::default() +} diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 7a4ba7dc3..74a88413f 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -184,6 +184,36 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "The tag may not contain spaces." => Some(("insert", "attdef-tag-spaces")), "Value must be positive and nonzero." => Some(("insert", "attdef-positive")), "Requires valid numeric angle or second point." => Some(("insert", "attdef-angle")), + "Field" => Some(("insert", "field-title")), + "Category" => Some(("insert", "field-category")), + "Date format" => Some(("insert", "field-date-format")), + "Examples" => Some(("insert", "field-examples")), + "Display file extension" => Some(("insert", "field-file-extension")), + "Filename only" => Some(("insert", "field-filename-only")), + "Path only" => Some(("insert", "field-path-only")), + "Path and filename" => Some(("insert", "field-path-filename")), + "Bytes" => Some(("insert", "field-bytes")), + "Kilobytes" => Some(("insert", "field-kilobytes")), + "Megabytes" => Some(("insert", "field-megabytes")), + "System variable" => Some(("insert", "field-system-variable")), + "Diesel expression" => Some(("insert", "field-diesel")), + "Named object type" => Some(("insert", "field-named-type")), + "Object type" => Some(("insert", "field-object-type")), + "Select object" => Some(("insert", "field-select-object")), + "Field expression" => Some(("insert", "field-expression")), + "Inserts a field: text that updates from the drawing, the date or another source." => Some(("insert", "field-help")), + "Date & Time" => Some(("insert", "field-cat-date")), + "Document" => Some(("insert", "field-cat-document")), + "Other" => Some(("insert", "field-cat-other")), + "Uppercase" => Some(("insert", "field-case-upper")), + "Lowercase" => Some(("insert", "field-case-lower")), + "First capital" => Some(("insert", "field-case-first")), + "Title case" => Some(("insert", "field-case-title")), + "Dimstyle" => Some(("insert", "field-named-dimstyle")), + "Tablestyle" => Some(("insert", "field-named-tablestyle")), + "Textstyle" => Some(("insert", "field-named-textstyle")), + "EndPoint" => Some(("insert", "field-prop-endpoint")), + "StartPoint" => Some(("insert", "field-prop-startpoint")), "Stylization" => Some(("properties", "stylization")), "Color scheme" => Some(("properties", "color-scheme")), "Scan Colors" => Some(("properties", "scan-colors")), diff --git a/src/modules/annotate/field_cmd.rs b/src/modules/annotate/field_cmd.rs new file mode 100644 index 000000000..80d4f691e --- /dev/null +++ b/src/modules/annotate/field_cmd.rs @@ -0,0 +1,190 @@ +// FIELD — places the field chosen in the Field dialog as a multi-line text, +// and the object pick behind the dialog's "Select object" button. + +use codec::entities::mtext::AttachmentPoint; +use codec::entities::MText; +use codec::types::{Handle, Vector3}; +use codec::EntityType; +use glam::DVec3; + +use crate::command::{CadCommand, CmdResult, InputKind, WorkingPlane}; + +/// A field to attach to the entity a command commits: the field code and the +/// objects it refers to. +pub type TextField = (String, Vec); + +#[derive(Clone, Copy, PartialEq)] +enum Step { + Point, + Height, + Justify, +} + +/// `Specify start point or [Height/Justify]:` → an MTEXT holding the field. +pub struct FieldPlaceCommand { + step: Step, + value: String, + field: TextField, + style: String, + height: f64, + attachment: AttachmentPoint, + plane: WorkingPlane, +} + +impl FieldPlaceCommand { + pub fn new(value: String, field: TextField, style: String, height: f64) -> Self { + Self { + step: Step::Point, + value, + field, + style, + height, + attachment: AttachmentPoint::TopLeft, + plane: WorkingPlane::default(), + } + } +} + +fn short(v: f64) -> String { + let t = format!("{v:.4}"); + t.trim_end_matches('0').trim_end_matches('.').to_string() +} + +impl CadCommand for FieldPlaceCommand { + fn name(&self) -> &'static str { + "FIELD" + } + + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + + fn prompt(&self) -> String { + match self.step { + Step::Point => "Specify start point or [Height/Justify]:".into(), + Step::Height => format!("Specify height <{}>:", short(self.height)), + Step::Justify => "Enter justification [TL/TC/TR/ML/MC/MR/BL/BC/BR] :".into(), + } + } + + fn input_kind(&self) -> InputKind { + match self.step { + Step::Justify => InputKind::SingleToken, + _ => InputKind::Point, + } + } + + fn point_step_accepts_keywords(&self) -> bool { + self.step == Step::Point + } + + fn on_text_input(&mut self, text: &str) -> Option { + let word = text.trim().trim_start_matches('_').to_ascii_uppercase(); + match self.step { + Step::Point => match word.as_str() { + "H" | "HEIGHT" => self.step = Step::Height, + "J" | "JUSTIFY" => self.step = Step::Justify, + "" => return Some(CmdResult::ReportError("Point or option keyword required.".into())), + _ => return Some(CmdResult::ReportError("Point or option keyword required.".into())), + }, + Step::Height => { + if word.is_empty() { + self.step = Step::Point; + return Some(CmdResult::NeedPoint); + } + match word.parse::() { + Ok(v) if v > 0.0 => { + self.height = v; + self.step = Step::Point; + } + Ok(_) => return Some(CmdResult::ReportError("Value must be positive and nonzero.".into())), + Err(_) => return Some(CmdResult::ReportError("Requires numeric distance or two points.".into())), + } + } + Step::Justify => { + self.attachment = match word.as_str() { + "" | "TL" => AttachmentPoint::TopLeft, + "TC" => AttachmentPoint::TopCenter, + "TR" => AttachmentPoint::TopRight, + "ML" => AttachmentPoint::MiddleLeft, + "MC" => AttachmentPoint::MiddleCenter, + "MR" => AttachmentPoint::MiddleRight, + "BL" => AttachmentPoint::BottomLeft, + "BC" => AttachmentPoint::BottomCenter, + "BR" => AttachmentPoint::BottomRight, + _ => return Some(CmdResult::ReportError("Invalid option keyword.".into())), + }; + self.step = Step::Point; + } + } + Some(CmdResult::NeedPoint) + } + + fn on_point(&mut self, pt: DVec3) -> CmdResult { + if self.step == Step::Height { + return CmdResult::NeedPoint; + } + let p = self.plane.to_local(pt); + let mut mtext = MText::new(); + mtext.value = self.value.clone(); + mtext.insertion_point = Vector3::new(p.x, p.y, p.z); + mtext.height = self.height; + mtext.style = self.style.clone(); + mtext.attachment_point = self.attachment; + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::MText(mtext))) + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + Step::Point => CmdResult::ReportError("Point or option keyword required.".into()), + _ => self.on_text_input("").unwrap_or(CmdResult::NeedPoint), + } + } + + fn text_field(&self) -> Option { + Some(self.field.clone()) + } +} + +/// The Field dialog's "Select object": one object is picked and handed back +/// (`_FIELD_OBJECT `); Esc returns without one. +pub struct FieldObjectPickCommand; + +impl CadCommand for FieldObjectPickCommand { + fn name(&self) -> &'static str { + "FIELD" + } + + fn prompt(&self) -> String { + "Select object:".into() + } + + fn needs_entity_pick(&self) -> bool { + true + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { + if handle.is_null() { + return CmdResult::NeedPoint; + } + CmdResult::Dispatch(format!("_FIELD_OBJECT {:X}", handle.value())) + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Dispatch("_FIELD_OBJECT".into()) + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Dispatch("_FIELD_OBJECT".into()) + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["FIELD"] }); diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index c9e783fc2..5a882d148 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -7,6 +7,7 @@ pub mod angular_dim; pub mod data_extract; pub mod data_link; pub mod ddedit; +pub mod field_cmd; pub mod diameter_dim; pub mod dim_baseline; pub mod dim_continue; diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index f2d1e0366..73eb457d5 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -218,6 +218,8 @@ pub struct AttdefSpec { /// Multi-line boundary width (0 = none). pub boundary_width: f64, pub line_spacing: f64, + /// A field the default value comes from: its code and referenced objects. + pub field: Option<(String, Vec)>, } impl AttdefSpec { @@ -447,6 +449,7 @@ impl AttdefCommand { oblique_angle, boundary_width: 0.0, line_spacing: 1.0, + field: None, }, lines: Vec::new(), styles, @@ -952,6 +955,10 @@ impl CadCommand for AttdefPlaceCommand { fn on_mouse_move(&mut self, pt: DVec3) -> Option { Some(cross_preview(pt, self.plane)) } + + fn text_field(&self) -> Option<(String, Vec)> { + self.spec.field.clone() + } } /// A small cross at the cursor while a placement point is asked. diff --git a/src/ui/window/attdef_dialog.rs b/src/ui/window/attdef_dialog.rs index c74c81f08..baf5702aa 100644 --- a/src/ui/window/attdef_dialog.rs +++ b/src/ui/window/attdef_dialog.rs @@ -34,6 +34,9 @@ pub enum AttdefDialogMsg { PickHeight, PickRotation, PickWidth, + /// The ƒ button: pick a field for the Default value. + InsertField, + EditInsertField, /// Edit the default value in the multi-line text editor. EditValue, DismissError, @@ -76,6 +79,8 @@ pub struct AttdefDialogState { pub can_align_below: bool, /// Validation message shown above the buttons. pub error: Option, + /// The field the Default value shows, when one was inserted. + pub field: Option<(String, Vec)>, } /// The Edit Attribute Definition dialog's fields. @@ -87,6 +92,10 @@ pub struct AttdefEditState { pub default: String, pub constant: bool, pub error: Option, + pub field: Option<(String, Vec)>, + /// The Default was retyped or a field inserted: the stored field is + /// replaced on Apply. + pub field_changed: bool, } fn msg(m: AttdefDialogMsg) -> Message { @@ -239,8 +248,7 @@ pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSiz Some(AttdefDialogMsg::EditValue), )); } - // No field picker in this application yet. - value.push(pick("ƒ", t!("Insert field").into_owned(), None)) + value.push(pick("ƒ", t!("Insert field").into_owned(), Some(AttdefDialogMsg::InsertField))) } .spacing(6) .into(), @@ -372,7 +380,7 @@ pub fn view_edit<'a>(state: &'a AttdefEditState, sizing: crate::ui::modal::Modal true, AttdefDialogMsg::EditDefault ), - pick("ƒ", t!("Insert field").into_owned(), None), + pick("ƒ", t!("Insert field").into_owned(), Some(AttdefDialogMsg::EditInsertField)), ] .spacing(6) .align_y(iced::Center), diff --git a/src/ui/window/field_dialog.rs b/src/ui/window/field_dialog.rs new file mode 100644 index 000000000..640238b74 --- /dev/null +++ b/src/ui/window/field_dialog.rs @@ -0,0 +1,491 @@ +//! Field dialog (FIELD and the ƒ buttons of the attribute definition +//! dialogs): a category and field list on the left, the chosen field's +//! format on the right, a live preview and the field expression below. + +use iced::widget::{button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space}; +use iced::{Element, Fill, Length}; + +use crate::app::Message; +use crate::t; +use crate::ui::style::common::muted_style; +use crate::ui::style::form::{button_style, dialog_button, field_style}; +use crate::ui::window::pdf_dialogs::{accent_text, card_style}; + +/// Where the chosen field goes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FieldTarget { + /// FIELD: a new multi-line text placed on screen. + NewText, + /// The Default of the Attribute Definition dialog. + AttdefDefault, + /// The Default of the Edit Attribute Definition dialog. + AttdefEdit, +} + +/// The categories the dialog offers, with their fields (only the ones the +/// application evaluates). +pub const CATEGORIES: &[(&str, &[&str])] = &[ + ("All", &[]), + ("Date & Time", &["CreateDate", "Date", "PlotDate", "SaveDate"]), + ( + "Document", + &["Author", "Comments", "Filename", "Filesize", "HyperlinkBase", "Keywords", "LastSavedBy", "Subject", "Title"], + ), + ("Objects", &["NamedObject", "Object"]), + ("Other", &["DieselExpression", "SystemVariable"]), + ("Plot", &["Login", "PlotDate"]), +]; + +/// The fields of a category; "All" lists every field once, sorted. +pub fn fields_of(category: usize) -> Vec<&'static str> { + if category == 0 { + let mut all: Vec<&'static str> = CATEGORIES.iter().flat_map(|(_, f)| f.iter().copied()).collect(); + all.sort_unstable_by_key(|s| s.to_ascii_lowercase()); + all.dedup(); + return all; + } + CATEGORIES.get(category).map(|(_, f)| f.to_vec()).unwrap_or_default() +} + +/// The date formats the dialog lists, in the reference order. +pub const DATE_FORMATS: &[&str] = &[ + "M/d/yyyy", "dddd, MMMM dd, yyyy", "MMMM d, yyyy", "M/d/yy", "yyyy-MM-dd", "d-MMM-yy", + "M.d.yyyy", "MMM. d, yy", "d MMMM yyyy", "dd.MM.yyyy", "dd/MM/yyyy", "yyyy/MM/dd", + "yyyy-M-d", "MMMM yy", "MMM-yy", "M/d/yyyy h:mm tt", "M/d/yyyy h:mm:ss tt", "h:mm tt", + "h:mm:ss tt", "HH:mm", "HH:mm:ss", "%#x", "%#c", "%x", "%c", "%X", +]; + +pub const TEXT_CASES: &[&str] = &["(none)", "Uppercase", "Lowercase", "First capital", "Title case"]; +pub const SIZE_UNITS: &[&str] = &["Bytes", "Kilobytes", "Megabytes"]; +pub const NAMED_TYPES: &[&str] = &["Block", "Dimstyle", "Layer", "Linetype", "Tablestyle", "Textstyle", "View"]; + +/// How a field is formatted, by its name. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FieldKind { + Date, + Text, + Filename, + Filesize, + SystemVariable, + Diesel, + NamedObject, + Object, +} + +pub fn kind_of(name: &str) -> FieldKind { + match name { + "CreateDate" | "Date" | "PlotDate" | "SaveDate" => FieldKind::Date, + "Filename" => FieldKind::Filename, + "Filesize" => FieldKind::Filesize, + "SystemVariable" => FieldKind::SystemVariable, + "DieselExpression" => FieldKind::Diesel, + "NamedObject" => FieldKind::NamedObject, + "Object" => FieldKind::Object, + _ => FieldKind::Text, + } +} + +/// The Field dialog's working copy. +#[derive(Debug, Clone)] +pub struct FieldDialogState { + pub target: FieldTarget, + pub category: usize, + pub name: &'static str, + pub date_format: String, + pub text_case: usize, + /// 1 = path only, 2 = file name only, 3 = path and file name. + pub file_parts: u8, + pub file_extension: bool, + pub size_unit: usize, + pub sysvar: String, + pub diesel: String, + pub named_type: usize, + pub named_names: Vec<(String, codec::Handle)>, + pub named: Option, + /// The object picked for an Object field, its type and properties. + pub object: Option<(codec::Handle, String)>, + pub object_props: Vec<&'static str>, + pub object_prop: Option<&'static str>, + pub preview: String, + /// Today in each of DATE_FORMATS, for the Examples list. + pub examples: Vec, +} + +impl FieldDialogState { + pub fn new(target: FieldTarget) -> Self { + Self { + target, + category: 0, + name: "Date", + date_format: "%x".into(), + text_case: 0, + file_parts: 3, + file_extension: true, + size_unit: 0, + sysvar: crate::entities::field::SYSVARS[0].into(), + diesel: String::new(), + named_type: 2, + named_names: Vec::new(), + named: None, + object: None, + object_props: Vec::new(), + object_prop: None, + preview: String::new(), + examples: Vec::new(), + } + } + + /// The field code (without the `%<…>%` wrapper) and the objects it refers to. + pub fn code(&self) -> (String, Vec) { + let case = |base: String| match self.text_case { + 0 => base, + n => format!("{base} \\f \"%tc{n}\""), + }; + match kind_of(self.name) { + FieldKind::Date => (format!("\\AcVar {} \\f \"{}\"", self.name, self.date_format), vec![]), + FieldKind::Text => (case(format!("\\AcVar {}", self.name)), vec![]), + FieldKind::Filename => { + let bits = self.file_parts + if self.file_extension && self.file_parts & 2 != 0 { 4 } else { 0 }; + (case(format!("\\AcVar Filename \\f \"%fn{bits}\"")), vec![]) + } + FieldKind::Filesize => (format!("\\AcVar Filesize \\f \"%ld%by{}\"", self.size_unit + 1), vec![]), + FieldKind::SystemVariable => (format!("\\AcVar {}", self.sysvar), vec![]), + FieldKind::Diesel => (format!("\\AcDiesel {}", self.diesel), vec![]), + FieldKind::NamedObject => match self.named.and_then(|i| self.named_names.get(i)) { + Some((_, handle)) => (case("\\AcObjProp Object(%<\\_ObjIdx 0>%).Name".into()), vec![*handle]), + None => ("\\AcObjProp".into(), vec![]), + }, + FieldKind::Object => match (&self.object, self.object_prop) { + (Some((handle, _)), Some(prop)) => { + (format!("\\AcObjProp Object(%<\\_ObjIdx 0>%).{prop}"), vec![*handle]) + } + _ => ("\\AcObjProp".into(), vec![]), + }, + } + } + + /// The field can be inserted (an object field needs its object and + /// property, a named object its name, a DIESEL field an expression). + pub fn complete(&self) -> bool { + match kind_of(self.name) { + FieldKind::NamedObject => self.named.is_some(), + FieldKind::Object => self.object.is_some() && self.object_prop.is_some(), + FieldKind::Diesel => !self.diesel.trim().is_empty(), + _ => true, + } + } +} + +/// The geometric properties a field can show for an object of this type. +pub fn object_properties(entity: &codec::EntityType) -> Vec<&'static str> { + use codec::EntityType as E; + match entity { + E::Line(_) => vec!["Center", "EndPoint", "Length", "StartPoint"], + E::Circle(_) => vec!["Area", "Center", "Circumference", "Diameter", "Radius"], + E::Arc(_) => vec!["Center", "Length", "Radius"], + E::Ellipse(_) => vec!["Area", "Center"], + E::LwPolyline(_) | E::Polyline2D(_) | E::Spline(_) => vec!["Area", "Length"], + E::Hatch(_) => vec!["Area"], + _ => vec![], + } +} + +#[derive(Debug, Clone)] +pub enum FieldDialogMsg { + Category(usize), + Name(&'static str), + DateFormat(String), + DateExample(usize), + TextCase(usize), + FileParts(u8), + FileExtension(bool), + SizeUnit(usize), + SysVar(String), + Diesel(String), + NamedType(usize), + Named(usize), + SelectObject, + ObjectProp(&'static str), + Help, + Ok, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Pick(usize, String); + +impl std::fmt::Display for Pick { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.1) + } +} + +fn msg(m: FieldDialogMsg) -> Message { + Message::FieldDialog(m) +} + +fn card<'a>(title: String, content: impl Into>) -> Element<'a, Message> { + container(column![text(title.to_uppercase()).size(10).style(accent_text), content.into()].spacing(8)) + .padding([10, 12]) + .width(Fill) + .style(card_style) + .into() +} + +/// A selectable list: one row per item, the chosen one highlighted. +fn list<'a>(rows: Vec<(String, bool, FieldDialogMsg)>, height: f32) -> Element<'a, Message> { + let rows = rows.into_iter().map(|(label, on, m)| { + button(text(label).size(12)) + .on_press(msg(m)) + .style(button_style(on)) + .padding([3, 8]) + .width(Fill) + .into() + }); + container(scrollable(iced::widget::Column::with_children(rows.collect::>>()).spacing(1))) + .height(Length::Fixed(height)) + .padding(2) + .style(|theme: &iced::Theme| field_style(theme, text_input::Status::Active).into_container()) + .into() +} + +trait IntoContainer { + fn into_container(self) -> container::Style; +} + +impl IntoContainer for text_input::Style { + fn into_container(self) -> container::Style { + container::Style { + background: Some(self.background), + border: self.border, + ..Default::default() + } + } +} + +fn case_list<'a>(state: &FieldDialogState) -> Element<'a, Message> { + list( + TEXT_CASES + .iter() + .enumerate() + .map(|(i, c)| (t!(*c).into_owned(), state.text_case == i, FieldDialogMsg::TextCase(i))) + .collect(), + 130.0, + ) +} + +fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> { + let examples = &state.examples; + match kind_of(state.name) { + FieldKind::Date => column![ + row![ + text(t!("Date format")).size(12).width(Length::Fixed(96.0)), + text_input("", &state.date_format) + .size(12) + .padding([5, 8]) + .style(field_style) + .on_input(|v| msg(FieldDialogMsg::DateFormat(v))), + ] + .spacing(8) + .align_y(iced::Center), + text(t!("Examples")).size(12), + list( + examples + .iter() + .enumerate() + .map(|(i, e)| (e.clone(), DATE_FORMATS.get(i) == Some(&state.date_format.as_str()), FieldDialogMsg::DateExample(i))) + .collect(), + 220.0, + ), + ] + .spacing(8) + .into(), + FieldKind::Text => column![text(t!("Format")).size(12), case_list(state)].spacing(8).into(), + FieldKind::Filename => { + let part = |label: &str, bits: u8| { + checkbox(state.file_parts == bits) + .label(t!(label).into_owned()) + .text_size(12) + .size(14) + .on_toggle(move |_| msg(FieldDialogMsg::FileParts(bits))) + }; + let mut ext = checkbox(state.file_extension) + .label(t!("Display file extension").into_owned()) + .text_size(12) + .size(14); + if state.file_parts & 2 != 0 { + ext = ext.on_toggle(|v| msg(FieldDialogMsg::FileExtension(v))); + } + column![ + part("Filename only", 2), + part("Path only", 1), + part("Path and filename", 3), + ext, + text(t!("Format")).size(12), + case_list(state), + ] + .spacing(8) + .into() + } + FieldKind::Filesize => column![ + text(t!("Format")).size(12), + list( + SIZE_UNITS + .iter() + .enumerate() + .map(|(i, u)| (t!(*u).into_owned(), state.size_unit == i, FieldDialogMsg::SizeUnit(i))) + .collect(), + 90.0, + ), + ] + .spacing(8) + .into(), + FieldKind::SystemVariable => column![ + text(t!("System variable")).size(12), + list( + crate::entities::field::SYSVARS + .iter() + .map(|v| (v.to_string(), state.sysvar == *v, FieldDialogMsg::SysVar(v.to_string()))) + .collect(), + 250.0, + ), + ] + .spacing(8) + .into(), + FieldKind::Diesel => column![ + text(t!("Diesel expression")).size(12), + text_input("$(getvar,dwgname)", &state.diesel) + .size(12) + .padding([5, 8]) + .style(field_style) + .on_input(|v| msg(FieldDialogMsg::Diesel(v))), + ] + .spacing(8) + .into(), + FieldKind::NamedObject => column![ + row![ + text(t!("Named object type")).size(12).width(Length::Fixed(120.0)), + pick_list( + Some(Pick(state.named_type, t!(NAMED_TYPES[state.named_type]).into_owned())), + NAMED_TYPES.iter().enumerate().map(|(i, n)| Pick(i, t!(*n).into_owned())).collect::>(), + |p: &Pick| p.1.clone(), + ) + .on_select(|p| msg(FieldDialogMsg::NamedType(p.0))) + .text_size(12) + .padding([5, 8]) + .width(Fill), + ] + .spacing(8) + .align_y(iced::Center), + text(t!("Name")).size(12), + list( + state + .named_names + .iter() + .enumerate() + .map(|(i, (n, _))| (n.clone(), state.named == Some(i), FieldDialogMsg::Named(i))) + .collect(), + 150.0, + ), + text(t!("Format")).size(12), + case_list(state), + ] + .spacing(8) + .into(), + FieldKind::Object => column![ + row![ + text(t!("Object type")).size(12).width(Length::Fixed(96.0)), + container( + text(state.object.as_ref().map(|(_, t)| t.clone()).unwrap_or_default()).size(12).style(muted_style) + ) + .padding([5, 8]) + .width(Fill), + button(text(t!("Select object")).size(12)) + .on_press(msg(FieldDialogMsg::SelectObject)) + .style(button_style(false)) + .padding([5, 10]), + ] + .spacing(8) + .align_y(iced::Center), + text(t!("Property")).size(12), + list( + state + .object_props + .iter() + .map(|p| (t!(*p).into_owned(), state.object_prop == Some(*p), FieldDialogMsg::ObjectProp(p))) + .collect(), + 180.0, + ), + ] + .spacing(8) + .into(), + } +} + +pub fn view<'a>( + state: &'a FieldDialogState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let categories: Vec = CATEGORIES.iter().enumerate().map(|(i, (c, _))| Pick(i, t!(*c).into_owned())).collect(); + let left = card( + t!("Field").into_owned(), + column![ + row![ + text(t!("Category")).size(12).width(Length::Fixed(70.0)), + pick_list(Some(categories[state.category].clone()), categories.clone(), |p: &Pick| p.1.clone()) + .on_select(|p| msg(FieldDialogMsg::Category(p.0))) + .text_size(12) + .padding([5, 8]) + .width(Fill), + ] + .spacing(8) + .align_y(iced::Center), + list( + fields_of(state.category) + .into_iter() + .map(|n| (n.to_string(), state.name == n, FieldDialogMsg::Name(n))) + .collect(), + 330.0, + ), + ] + .spacing(8), + ); + let preview = card( + t!("Preview").into_owned(), + container(text(if state.preview.is_empty() { "----".to_string() } else { state.preview.clone() }).size(15)) + .padding([6, 8]) + .width(Fill), + ); + let right = column![card(t!("Format").into_owned(), format_panel(state)), preview].spacing(10); + let (code, _) = state.code(); + let expression = column![ + text(t!("Field expression")).size(12), + container(text(format!("%<{code}>%")).size(12).font(iced::Font::MONOSPACE).style(muted_style)) + .padding([5, 8]) + .width(Fill) + .style(|theme: &iced::Theme| field_style(theme, text_input::Status::Disabled).into_container()), + ] + .spacing(4); + let mut insert = button(text(t!("Insert")).size(12)) + .style(button_style(true)) + .padding([6, 16]); + if state.complete() { + insert = insert.on_press(msg(FieldDialogMsg::Ok)); + } + let footer = row![ + button(text("?").size(12)).on_press(msg(FieldDialogMsg::Help)).style(button_style(false)).padding([5, 11]), + Space::new().width(Fill), + dialog_button(t!("Cancel"), Message::CloseModal, false), + insert, + ] + .spacing(6) + .align_y(iced::Center); + column![ + row![container(left).width(Length::Fixed(240.0)), right].spacing(10), + expression, + footer, + ] + .spacing(10) + .padding([10, 12]) + .width(sizing.width) + .into() +} diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index af3a8ebd1..3b3282d0f 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -1,5 +1,6 @@ pub mod about; pub mod attdef_dialog; +pub mod field_dialog; pub mod block_definition; pub mod block_palette; pub mod browser;