diff --git a/Cargo.toml b/Cargo.toml index e54730af9..85cc56ce7 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", "Win32_Security_Authentication_Identity"] } [target.'cfg(target_os = "linux")'.dependencies] ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index f5d834f21..97fec3890 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -4087,6 +4087,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 2edce499f..0fb1d43e9 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -4074,6 +4074,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 e6e05362d..d17ecfb1d 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -4072,6 +4072,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 1b1eb8178..7fd9f0bd4 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -4073,6 +4073,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 cbcf9e61e..53601f4be 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -4075,6 +4075,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 28488dab6..dcbbb7b69 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -4076,6 +4076,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 1b5622845..df1b19661 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -4075,6 +4075,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 31f327b3b..99f18d514 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -4074,6 +4074,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 368c8a7b9..bc00d9334 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -4075,6 +4075,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 446adc7c6..59440e35e 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -4063,6 +4063,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 3671865c6..2066c5156 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -4074,6 +4074,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 b7025924b..bab01a81c 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -4074,6 +4074,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 b4c802a2d..f0df48710 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -4063,6 +4063,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 c029b91a2..6ad109cc7 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -4074,6 +4074,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 55bbd86e6..2f5e39e39 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -4073,6 +4073,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 b0c37418a..c31421220 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -4074,6 +4074,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 e807f3699..e96dea04d 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -4075,6 +4075,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 300a070ad..e3eb50fa6 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -4076,6 +4076,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 37244fcc9..3b01d7c2c 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -4035,6 +4035,44 @@ 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-linked = Bağlantılı + .field-current-precision = Geçerli hassasiyet + .field-evaluate = Hesapla + .field-text-to-display = Görüntülenecek metin + .field-address = Adres + .field-select-link-file = Bağlanacak dosyayı seçin + .field-current-units = Geçerli birimler + .field-use-scale-name = Ölçek adını kullan + .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 d378127b5..290f64be0 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -4058,6 +4058,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 983234fbf..c38cc4a04 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -4070,6 +4070,44 @@ 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-linked = Linked + .field-current-precision = Current precision + .field-evaluate = Evaluate + .field-text-to-display = Text to display + .field-address = Address + .field-select-link-file = Select a file to link + .field-current-units = Current units + .field-use-scale-name = Use scale name + .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 21d236090..b2802535d 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..3fa2deef5 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(); @@ -288,6 +309,8 @@ impl OpenCADStudio { session.annotative = spec.annotative; session.on_screen = state.on_screen; } + // The choice is kept with the user settings, as the reference keeps it. + self.persist_settings_if_changed(); self.reset_command_start_state(i); if state.align_below { let previous = attdef::session().last.clone(); @@ -336,7 +359,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 +371,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..5c084fc78 --- /dev/null +++ b/src/app/commands/field.rs @@ -0,0 +1,228 @@ +//! 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::Formula(v) => state.formula = v, + FieldDialogMsg::FormulaFormat(k) => state.formula_format = k, + FieldDialogMsg::FormulaPrecision(k) => state.formula_precision = k, + FieldDialogMsg::Evaluate => {} + FieldDialogMsg::HyperlinkText(v) => state.hyperlink_text = v, + FieldDialogMsg::HyperlinkUrl(v) => state.hyperlink_url = v, + FieldDialogMsg::BrowseHyperlink => { + return Task::perform( + async { + rfd::AsyncFileDialog::new() + .set_title(crate::t!("Select a file to link").as_ref()) + .pick_file() + .await + .map(|h| crate::sys::handle_path(&h)) + }, + |path| match path { + Some(path) => Message::FieldDialog(FieldDialogMsg::HyperlinkUrl(path.to_string_lossy().into_owned())), + None => Message::Noop, + }, + ); + } + FieldDialogMsg::PlotScale(k) => state.plot_scale = k, + 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 8b24fbf6c..7b9f22f2b 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; @@ -292,6 +293,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); } @@ -664,6 +668,7 @@ inventory::submit!(crate::command::CommandRegistration { "DGNFRAME", "PDFOSNAP", "UOSNAP", + "FIELDDISPLAY", "PDFIMPORTMODE", "PDFIMPORTFILTER", "PDFIMPORTLAYERS", diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index e9bb61320..5f9fee957 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1025,6 +1025,7 @@ impl OpenCADStudio { | "DWFOSNAP" | "DGNOSNAP" | "UOSNAP" + | "FIELDDISPLAY" | "PDFIMPORTMODE" | "PDFIMPORTFILTER" | "PDFIMPORTLAYERS" @@ -1232,6 +1233,40 @@ impl OpenCADStudio { } return Some(self.finish_dispatch(cmd)); } + if name == "FIELDDISPLAY" { + let current = i16::from(crate::entities::field::display()); + match value.as_deref().map(|v| v.trim().parse::().ok().filter(|v| (0..=1).contains(v))) { + Some(Some(mode)) => { + if current != mode { + crate::entities::field::set_display(mode == 1); + // A profile setting: redraw the fields of every open drawing. + for tab in &mut self.tabs { + let changes: Vec<_> = tab + .scene + .document + .entities() + .filter(|e| crate::entities::field::hosts_field(&tab.scene.document, e)) + .map(|e| (e.common().handle, crate::scene::ChangeKind::Modified)) + .collect(); + if !changes.is_empty() { + tab.scene.bump_entities(&changes); + } + } + self.save_config(); + } + } + Some(None) => { + self.command_line.push_error(crate::t!("Requires 0 or 1 only.").as_ref()); + self.command_line.push_output(&format!("Enter new value for FIELDDISPLAY <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + None => { + self.command_line.push_output(&format!("Enter new value for FIELDDISPLAY <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + } + return Some(self.finish_dispatch(cmd)); + } if matches!(name.as_str(), "PDFOSNAP" | "DWFOSNAP" | "DGNOSNAP" | "UOSNAP") { use crate::scene::model::pdf_vector as pv; use codec::entities::UnderlayType; diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index da6f4057a..dab7a6c47 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -81,6 +81,7 @@ pub(super) const NAMES: &[&str] = &[ "ribbon_dropdown", "dialog_ok", "attdef_dialog", + "field_dialog", "close_document", "toggle_properties", "toggle_layers", @@ -958,6 +959,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()), @@ -975,6 +978,42 @@ 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`, `formula=(1+2)*3`, `formula_format=`, + // `precision=`, `link_text=…`, `link_url=…`, `plot_scale=`. + "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, + "formula" => F::Formula(v.into()), + "formula_format" => F::FormulaFormat(index()?), + "precision" => F::FormulaPrecision(index()?), + "link_text" => F::HyperlinkText(v.into()), + "link_url" => F::HyperlinkUrl(v.into()), + "plot_scale" => F::PlotScale(index()?), + _ => 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; @@ -1001,6 +1040,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 be4af59e3..392e1bed1 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, @@ -1938,6 +1940,7 @@ pub enum ModalKind { GeometricTolerance, AttDef, AttDefEdit, + Field, DraftingSettings, AutoConstrainSettings, LayerStateEditor, @@ -3891,6 +3894,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>), @@ -4086,6 +4090,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/settings.rs b/src/app/settings.rs index a3b679815..690114e91 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -406,6 +406,10 @@ pub struct UserSettings { /// IMAGEFRAME of the profile: used by new drawings and by drawings that /// carry no image variables of their own. pub image_frame: i16, + /// The Attribute Definition dialog's "Specify on-screen" choice. + pub attdef_on_screen: bool, + /// FIELDDISPLAY: fields show on a gray background. + pub field_display: bool, /// Nested-copy symbol handling: false inserts, true binds. pub ncopy_bind: bool, /// Last Options page; unknown saved names fall back without rejecting the config. @@ -784,6 +788,8 @@ impl Default for UserSettings { right_click_hold_ms: 250, grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT, image_frame: 1, + attdef_on_screen: true, + field_display: true, ncopy_bind: false, cursor_type: CursorType::Crosshair, crosshair_color: None, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 328ce22dd..2edd18786 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -676,6 +676,8 @@ impl OpenCADStudio { right_click_hold_ms: self.right_click_hold_ms, grip_object_limit: self.grip_object_limit, image_frame: crate::scene::frame::profile_image_mode(), + attdef_on_screen: crate::modules::draw::draw::attdef::session().on_screen, + field_display: crate::entities::field::display(), ncopy_bind: self.ncopy_bind, cursor_type: self.cursor_type, crosshair_color: self.crosshair_color, @@ -774,6 +776,8 @@ impl OpenCADStudio { self.right_click_hold_ms = super::super::settings::clamp_right_click_hold_ms(s.right_click_hold_ms); self.grip_object_limit = s.grip_object_limit.clamp(0, 32767); crate::scene::frame::set_profile_image_mode(s.image_frame); + crate::modules::draw::draw::attdef::session().on_screen = s.attdef_on_screen; + crate::entities::field::set_display(s.field_display); self.ncopy_bind = s.ncopy_bind; self.cursor_type = s.cursor_type; self.crosshair_color = s.crosshair_color; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index e19ec4fa7..fdcac3bea 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1817,6 +1817,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); @@ -8520,6 +8521,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 2bfb97a4f..d5f81eae6 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"), @@ -579,6 +580,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 885a4ebf9..0ba7f8953 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2634,6 +2634,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..c05b5ccb3 100644 --- a/src/entities/field.rs +++ b/src/entities/field.rs @@ -11,16 +11,35 @@ 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 + // Fields show local time. Unix epoch is Julian Day 2440587.5. + now_utc_julian() + utc_offset_days() + } + + fn file_times(&self) -> Option<(f64, f64)> { + #[cfg(not(target_arch = "wasm32"))] + { + let meta = std::fs::metadata(self.0?.source_path.as_deref()?).ok()?; + let julian = |t: std::time::SystemTime| -> Option { + let secs = t.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs_f64(); + Some(secs / 86_400.0 + 2_440_587.5 + utc_offset_days()) + }; + return Some((julian(meta.created().ok()?)?, julian(meta.modified().ok()?)?)); + } + #[cfg(target_arch = "wasm32")] + None } fn login(&self) -> Option { + // The account's display name, as the reference shows it. + if let Some(name) = display_name() { + return Some(name); + } #[cfg(not(target_arch = "wasm32"))] { for var in ["USER", "LOGNAME", "USERNAME"] { @@ -34,6 +53,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 +87,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 +95,88 @@ 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", +]; + +/// Real system variables show six decimals, as the reference shows them +/// (TEXTSIZE 2.500000). +fn num(v: f64) -> String { + format!("{v:.6}") +} + +/// 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 +195,168 @@ 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() +} + +/// FIELDDISPLAY: whether fields show on a gray background (not plotted). +/// A profile setting, so one value for every drawing. +static FIELD_DISPLAY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); + +pub fn display() -> bool { + FIELD_DISPLAY.load(std::sync::atomic::Ordering::Relaxed) +} + +pub fn set_display(on: bool) { + FIELD_DISPLAY.store(on, std::sync::atomic::Ordering::Relaxed); +} + +/// The gray the reference draws behind a field. +pub const BACKGROUND: [f32; 4] = [203.0 / 255.0, 203.0 / 255.0, 203.0 / 255.0, 1.0]; + +/// Whether `entity` hosts a field: its extension dictionary has ACAD_FIELD. +pub fn hosts_field(document: &CadDocument, entity: &codec::entities::EntityType) -> bool { + let Some(xdict) = entity.common().xdictionary_handle else { + return false; + }; + matches!( + document.objects.get(&xdict), + Some(codec::objects::ObjectType::Dictionary(d)) if d.get("ACAD_FIELD").is_some() + ) +} + +fn now_utc_julian() -> f64 { + epoch_secs() as f64 / 86_400.0 + 2_440_587.5 +} + +/// Local time minus UTC, in days (the zone's current offset). +pub fn utc_offset_days() -> f64 { + #[cfg(target_os = "windows")] + { + use windows_sys::Win32::Foundation::FILETIME; + use windows_sys::Win32::Storage::FileSystem::FileTimeToLocalFileTime; + // 100 ns ticks since 1601-01-01. + let ticks = (epoch_secs() as u64 + 11_644_473_600) * 10_000_000; + let utc = FILETIME { dwLowDateTime: ticks as u32, dwHighDateTime: (ticks >> 32) as u32 }; + let mut local = FILETIME { dwLowDateTime: 0, dwHighDateTime: 0 }; + if unsafe { FileTimeToLocalFileTime(&utc, &mut local) } != 0 { + let local = ((local.dwHighDateTime as u64) << 32) | local.dwLowDateTime as u64; + return (local as i64 - ticks as i64) as f64 / 864_000_000_000.0; + } + 0.0 + } + #[cfg(not(target_os = "windows"))] + 0.0 +} + +/// A DWG stores its header dates in universal time only; the local TDCREATE +/// and TDUPDATE come from the zone, as the reference derives them on load. +pub fn local_header_dates(document: &mut CadDocument) { + let h = &mut document.header; + let offset = utc_offset_days(); + if h.universal_create_date_julian != 0.0 && h.create_date_julian == h.universal_create_date_julian { + h.create_date_julian = h.universal_create_date_julian + offset; + } + if h.universal_update_date_julian != 0.0 && h.update_date_julian == h.universal_update_date_julian { + h.update_date_julian = h.universal_update_date_julian + offset; + } +} + +/// Stamp the header's update date (and the creation date of a new drawing) +/// in local and universal time before a save. +pub fn stamp_save_dates(document: &mut CadDocument) { + let utc = now_utc_julian(); + let local = utc + utc_offset_days(); + let h = &mut document.header; + if h.create_date_julian == 0.0 && h.universal_create_date_julian == 0.0 { + h.create_date_julian = local; + h.universal_create_date_julian = utc; + } + h.update_date_julian = local; + h.universal_update_date_julian = utc; +} + +/// The signed-in account's display name, when it has one. +fn display_name() -> Option { + #[cfg(target_os = "windows")] + { + use windows_sys::Win32::Security::Authentication::Identity::{GetUserNameExW, NameDisplay}; + let mut buffer = [0u16; 256]; + let mut len = buffer.len() as u32; + if unsafe { GetUserNameExW(NameDisplay, buffer.as_mut_ptr(), &mut len) } && len > 0 { + return Some(String::from_utf16_lossy(&buffer[..len as usize])).filter(|n| !n.trim().is_empty()); + } + None + } + #[cfg(not(target_os = "windows"))] + None +} diff --git a/src/io/mod.rs b/src/io/mod.rs index 45fbc2f33..a412dc859 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -723,6 +723,7 @@ async fn load_web_bytes( } fix_viewport_status_flags(&mut doc); fix_current_style_names(&mut doc); + crate::entities::field::local_header_dates(&mut doc); progress.set(crate::app::OPEN_PHASE_CACHING, 7000, 0, 1); let dropped = purge_corrupt_entities(&mut doc); if !recovery_mode && dropped > 0 { @@ -764,6 +765,7 @@ pub fn load_bytes(name: &str, bytes: Vec) -> Result { .map_err(|e| e.to_string())?; fix_viewport_status_flags(&mut doc); fix_current_style_names(&mut doc); + crate::entities::field::local_header_dates(&mut doc); Ok(doc) } "dxf" => { @@ -775,6 +777,7 @@ pub fn load_bytes(name: &str, bytes: Vec) -> Result { fix_dxf_layout_plot_settings(&mut doc); fix_viewport_status_flags(&mut doc); fix_current_style_names(&mut doc); + crate::entities::field::local_header_dates(&mut doc); Ok(doc) } _ => Err(format!("Unsupported file format: .{ext}")), @@ -1040,6 +1043,7 @@ fn finalize_loaded_outcome( } fix_viewport_status_flags(doc); fix_current_style_names(doc); + crate::entities::field::local_header_dates(doc); resolve_raster_image_paths(doc, path.parent()); doc.source_path = Some(path.to_string_lossy().into_owned()); Ok(outcome) diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 07051b431..4ec7f0109 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -217,6 +217,44 @@ 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")), + "Linked" => Some(("insert", "field-cat-linked")), + "Current precision" => Some(("insert", "field-current-precision")), + "Evaluate" => Some(("insert", "field-evaluate")), + "Text to display" => Some(("insert", "field-text-to-display")), + "Address" => Some(("insert", "field-address")), + "Select a file to link" => Some(("insert", "field-select-link-file")), + "Current units" => Some(("insert", "field-current-units")), + "Use scale name" => Some(("insert", "field-use-scale-name")), + "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..53e62dfa4 --- /dev/null +++ b/src/modules/annotate/field_cmd.rs @@ -0,0 +1,192 @@ +// 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; + // No defined width: the field text never wraps. + mtext.rectangle_width = 0.0; + 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/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 433932665..79459ef6c 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1408,6 +1408,59 @@ pub fn tessellate( // also carries the glyph quads built above. if bins.is_empty() { let mut wires: Vec = Vec::new(); + // FIELDDISPLAY: a field shows on a gray box behind its + // glyphs, on screen only. + if text_aabb != WireModel::UNBOUNDED_AABB + && crate::entities::field::display() + && crate::entities::field::hosts_field(document, entity) + { + let corner_groups = match entity { + EntityType::MText(m) => { + let rotation = stroke_groups + .iter() + .find_map(|group| group.run.as_ref().map(|run| run.rotation as f64)) + .unwrap_or(m.rotation); + oriented_mtext_corner_groups(&sdf_verts, m, rotation, 0.0, anno) + } + EntityType::Text(t) => vec![oriented_text_corners( + &sdf_verts, + [t.insertion_point.x, t.insertion_point.y], + t.rotation, + 0.0, + )], + EntityType::AttributeEntity(a) => vec![oriented_text_corners( + &sdf_verts, + [a.insertion_point.x, a.insertion_point.y], + a.rotation, + 0.0, + )], + _ => Vec::new(), + }; + let mut ft = Vec::with_capacity(6 * corner_groups.len()); + let mut ftl = Vec::with_capacity(6 * corner_groups.len()); + for corners in &corner_groups { + for &k in &[0usize, 1, 2, 0, 2, 3] { + let (h, lo) = split_ds_xyz(corners[k][0], corners[k][1], elev_v); + ft.push(h); + ftl.push(lo); + } + } + if !ft.is_empty() { + wires.push(WireModel { + display_visible: true, + plot_visible: false, + name: name.clone(), + color: crate::entities::field::BACKGROUND, + selected, + line_weight_px, + aabb: WireModel::UNBOUNDED_AABB, + plinegen: true, + fill_tris: ft, + fill_tris_low: ftl, + ..Default::default() + }); + } + } // MTEXT background and frame follow the glyph bounds. if text_aabb != WireModel::UNBOUNDED_AABB { if let EntityType::MText(m) = entity { diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 119490162..1adb6bf23 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -2533,6 +2533,7 @@ impl super::Scene { } } crate::io::xref::strip_resolved_xref_content(&mut document); + crate::entities::field::stamp_save_dates(&mut document); document } diff --git a/src/ui/window/attdef_dialog.rs b/src/ui/window/attdef_dialog.rs index df548fcf8..bceab8f3e 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 { @@ -248,8 +257,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(text("fx").size(12).into(), t!("Insert field").into_owned(), None)) + value.push(pick(text("fx").size(12).into(), t!("Insert field").into_owned(), Some(AttdefDialogMsg::InsertField))) } .spacing(6) .into(), @@ -381,7 +389,7 @@ pub fn view_edit<'a>(state: &'a AttdefEditState, sizing: crate::ui::modal::Modal true, AttdefDialogMsg::EditDefault ), - pick(text("fx").size(12).into(), t!("Insert field").into_owned(), None), + pick(text("fx").size(12).into(), 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..8862378db --- /dev/null +++ b/src/ui/window/field_dialog.rs @@ -0,0 +1,666 @@ +//! 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"], + ), + ("Linked", &["Hyperlink"]), + ("Objects", &["Formula", "NamedObject", "Object"]), + ("Other", &["DieselExpression", "SystemVariable"]), + ( + "Plot", + &["DeviceName", "Login", "PageSetupName", "PaperSize", "PlotDate", "PlotOrientation", "PlotScale", "PlotStyleTable"], + ), +]; + +/// 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, + Formula, + Hyperlink, + PlotScale, +} + +/// Formula precisions: the current one, then 0 to 8 decimals. +pub const FORMULA_PRECISIONS: &[&str] = + &["Current precision", "0", "0.0", "0.00", "0.000", "0.0000", "0.00000", "0.000000", "0.0000000", "0.00000000"]; + +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, + "Formula" => FieldKind::Formula, + "Hyperlink" => FieldKind::Hyperlink, + "PlotScale" => FieldKind::PlotScale, + _ => 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 formula: String, + /// Indexes into codec FORMULA_FORMATS and FORMULA_PRECISIONS. + pub formula_format: usize, + pub formula_precision: usize, + pub hyperlink_text: String, + pub hyperlink_url: String, + /// Index into codec PLOT_SCALE_FORMATS. + pub plot_scale: usize, + 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, + formula: String::new(), + formula_format: 0, + formula_precision: 0, + hyperlink_text: String::new(), + hyperlink_url: String::new(), + plot_scale: 0, + 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 }; + // The text case goes first in the same format: "%tc1%fn6". + let case = match self.text_case { + 0 => String::new(), + n => format!("%tc{n}"), + }; + (format!("\\AcVar Filename \\f \"{case}%fn{bits}\""), vec![]) + } + // Bytes are whole; kilo- and megabytes keep two decimals. + FieldKind::Filesize => ( + match self.size_unit { + 0 => "\\AcVar Filesize \\f \"%ld%by1\"".to_string(), + n => format!("\\AcVar Filesize \\f \"%.2f%by{}\"", n + 1), + }, + vec![], + ), + FieldKind::Formula => { + let format = codec::fields::FORMULA_FORMATS.get(self.formula_format).map_or("", |f| f.1); + let precision = match self.formula_precision { + 0 => String::new(), + n => format!("%pr{}", n - 1), + }; + let body = format!("\\AcExpr ({})", self.formula.trim()); + match format!("{format}{precision}") { + f if f.is_empty() => (body, vec![]), + f => (format!("{body} \\f \"{f}\""), vec![]), + } + } + FieldKind::Hyperlink => ( + format!("\\AcVar \\href \"{}##{}#0\"", self.hyperlink_url.trim(), self.hyperlink_text), + vec![], + ), + FieldKind::PlotScale => ( + codec::fields::PLOT_SCALE_FORMATS + .get(self.plot_scale) + .map_or_else(|| "\\AcVar PlotScale".to_string(), |f| f.1.to_string()), + 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(), + FieldKind::Formula => !self.formula.trim().is_empty(), + FieldKind::Hyperlink => !self.hyperlink_url.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), + Formula(String), + FormulaFormat(usize), + FormulaPrecision(usize), + Evaluate, + HyperlinkText(String), + HyperlinkUrl(String), + BrowseHyperlink, + PlotScale(usize), + 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::Formula => { + let formats: Vec = codec::fields::FORMULA_FORMATS + .iter() + .enumerate() + .map(|(i, (n, _))| Pick(i, t!(*n).into_owned())) + .collect(); + let precisions: Vec = + FORMULA_PRECISIONS.iter().enumerate().map(|(i, n)| Pick(i, t!(*n).into_owned())).collect(); + column![ + row![ + text(t!("Format")).size(12).width(Length::Fixed(80.0)), + pick_list(Some(formats[state.formula_format].clone()), formats.clone(), |p: &Pick| p.1.clone()) + .on_select(|p| msg(FieldDialogMsg::FormulaFormat(p.0))) + .text_size(12) + .padding([5, 8]) + .width(Fill), + ] + .spacing(8) + .align_y(iced::Center), + row![ + text(t!("Precision")).size(12).width(Length::Fixed(80.0)), + pick_list( + Some(precisions[state.formula_precision].clone()), + precisions.clone(), + |p: &Pick| p.1.clone(), + ) + .on_select(|p| msg(FieldDialogMsg::FormulaPrecision(p.0))) + .text_size(12) + .padding([5, 8]) + .width(Fill), + ] + .spacing(8) + .align_y(iced::Center), + ] + .spacing(8) + .into() + } + FieldKind::Hyperlink => column![ + row![ + text(t!("Text to display")).size(12).width(Length::Fixed(120.0)), + text_input("", &state.hyperlink_text) + .size(12) + .padding([5, 8]) + .style(field_style) + .on_input(|v| msg(FieldDialogMsg::HyperlinkText(v))), + ] + .spacing(8) + .align_y(iced::Center), + row![ + text(t!("Address")).size(12).width(Length::Fixed(120.0)), + text_input("https://", &state.hyperlink_url) + .size(12) + .padding([5, 8]) + .font(iced::Font::MONOSPACE) + .style(field_style) + .on_input(|v| msg(FieldDialogMsg::HyperlinkUrl(v))), + button(text("…").size(12)) + .on_press(msg(FieldDialogMsg::BrowseHyperlink)) + .style(button_style(false)) + .padding([5, 10]), + ] + .spacing(8) + .align_y(iced::Center), + ] + .spacing(8) + .into(), + FieldKind::PlotScale => column![ + text(t!("Format")).size(12), + list( + codec::fields::PLOT_SCALE_FORMATS + .iter() + .enumerate() + .map(|(i, (n, _))| (t!(*n).into_owned(), state.plot_scale == i, FieldDialogMsg::PlotScale(i))) + .collect(), + 170.0, + ), + ] + .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 = match kind_of(state.name) { + FieldKind::Formula => column![ + card( + t!("Formula").into_owned(), + column![ + text_input("(12+3)*2", &state.formula) + .size(12) + .padding([8, 8]) + .font(iced::Font::MONOSPACE) + .style(field_style) + .on_input(|v| msg(FieldDialogMsg::Formula(v))), + button(text(t!("Evaluate")).size(12)) + .on_press(msg(FieldDialogMsg::Evaluate)) + .style(button_style(false)) + .padding([5, 14]), + ] + .spacing(8), + ), + card(t!("Format").into_owned(), format_panel(state)), + preview, + ] + .spacing(10), + FieldKind::Hyperlink => column![card(t!("Hyperlink").into_owned(), format_panel(state)), preview].spacing(10), + _ => 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;