diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 83175dbf1..56f74d30a 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -6609,3 +6609,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index cc20285c3..cfa9ca912 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -6597,3 +6597,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 66afe10f6..506042abb 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -6594,3 +6594,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 61eef3101..2a0d73593 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -6595,3 +6595,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 07d706269..17e21790a 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -6597,3 +6597,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index e241beee5..2e90f5aa9 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -6596,3 +6596,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 9a4f1c86b..d2f14d030 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -6597,3 +6597,17 @@ ucs-follow = .prompt = UCSFOLLOW: introduce 0 (conservar vista) o 1 (seguir SCP) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activa una ventana de modelo; no se aplica al espacio papel. .invalid = UCSFOLLOW: solo se permite 0 o 1. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 0ec188498..048c40d76 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -6594,3 +6594,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index ae9d437ba..d401e3114 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -6597,3 +6597,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index db5a95696..3f26684e8 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -6585,3 +6585,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index a31cd56a4..b76bbcf58 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -6581,3 +6581,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 1d1563b99..279ed1f1d 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -6596,3 +6596,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 26de504d7..7bf028d9c 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -6585,3 +6585,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index c43410f04..67b1695d3 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -6596,3 +6596,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index 8f51fc302..371423d9a 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -6597,3 +6597,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 761118ba4..e5109a004 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -6595,3 +6595,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 16d72a254..0757a6921 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -6596,3 +6596,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 3ffec2a7b..c50da078b 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -6597,3 +6597,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 3840440f1..40cd7df64 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -6598,3 +6598,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 2733014ca..ae4416c3c 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -6518,3 +6518,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Öznitelik Yöneticisi + .modes = Kipler + .references = __ocs_fmt_0__ referans + .counts = Çizimde __ocs_fmt_0__ · model alanında __ocs_fmt_1__ + .columns = Sütunlar: + .constant-note = Sabit kipi tanımda değiştirilemez. + .live = Değişiklikleri çizimde canlı göster + .apply-existing = Mevcut referanslara uygula + .emphasize = Yinelenen etiketleri vurgula + .sync = Eşitle + .help = BATTMAN bir bloğun öznitelik tanımlarını yönetir. + .no-attributed-block = Blok tablosunda __ocs_fmt_0__ adlı öznitelikli blok yok. diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 81edca569..59547a326 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -6580,3 +6580,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index b7941d3ed..bb5da1925 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -6592,3 +6592,17 @@ ucs-follow = .prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>: .paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS. .invalid = UCSFOLLOW: only 0 or 1 is allowed. + +attman = + .title = Block Attribute Manager + .modes = Modes + .references = __ocs_fmt_0__ references + .counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__ + .columns = Columns: + .constant-note = The Constant mode cannot be changed in the definition. + .live = Show changes in the drawing as they are made + .apply-existing = Apply changes to existing references + .emphasize = Emphasize duplicate tags + .sync = Sync + .help = BATTMAN manages the attribute definitions of a block. + .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. diff --git a/src/app/commands/attman.rs b/src/app/commands/attman.rs new file mode 100644 index 000000000..f3bac6acf --- /dev/null +++ b/src/app/commands/attman.rs @@ -0,0 +1,548 @@ +//! Block Attribute Manager (BATTMAN / ATTMAN): loading an attributed block's +//! definitions into the dialog and writing the edited list back to the +//! block definition and, when asked, to its existing references. + +use codec::entities::AttributeDefinition; +use codec::{EntityType, Handle}; +use iced::Task; + +use crate::app::{Message, OpenCADStudio}; +use crate::modules::insert::block_cmdline::BlockTable; +use crate::scene::view::dispatch::{apply_common_prop, apply_geom_prop}; +use crate::ui::window::attman_dialog::{ + self as dialog, AttManBlock, AttManMsg, AttManRow, AttManState, EditTab, +}; + +impl OpenCADStudio { + /// Open the dialog on the block of the first preselected attributed + /// reference, else on the first attributed block. + pub(in crate::app) fn open_attman(&mut self, i: usize, preselected: &[Handle]) -> Task { + let document = &self.tabs[i].scene.document; + let table = BlockTable::of(document); + let model = document + .block_records + .get("*Model_Space") + .map_or(Handle::NULL, |br| br.handle); + let blocks: Vec = table + .attributed + .iter() + .map(|name| { + let mut found = 0; + let mut in_space = 0; + for entity in document.entities() { + if let EntityType::Insert(insert) = entity { + if insert.block_name.eq_ignore_ascii_case(name) { + found += 1; + if insert.common.owner_handle == model { + in_space += 1; + } + } + } + } + AttManBlock { + name: name.clone(), + found, + in_space, + } + }) + .collect(); + let start = preselected + .iter() + .find_map(|h| match document.get_entity(*h) { + Some(EntityType::Insert(insert)) => blocks + .iter() + .position(|b| b.name.eq_ignore_ascii_case(&insert.block_name)), + _ => None, + }) + .unwrap_or(0); + let mut styles: Vec = document + .text_styles + .iter() + .filter(|s| !s.name.is_empty()) + .map(|s| s.name.clone()) + .collect(); + styles.sort_by_key(|s| s.to_lowercase()); + let layers: Vec = document.layers.iter().map(|l| l.name.clone()).collect(); + let linetypes: Vec = document + .line_types + .iter() + .map(|lt| lt.name.clone()) + .filter(|n| !n.eq_ignore_ascii_case("ByLayer") && !n.eq_ignore_ascii_case("ByBlock")) + .collect(); + self.attman = Some(AttManState { + blocks, + block: start, + rows: Vec::new(), + removed: Vec::new(), + selected: None, + tab: EditTab::Attribute, + edit_open: true, + drafts: Default::default(), + dirty: false, + live_steps: 0, + more_columns: false, + styles, + layers, + linetypes, + error: None, + }); + self.attman_load(i); + self.tabs[i].active_cmd = None; + self.active_modal = Some(crate::app::ModalKind::AttMan); + Task::none() + } + + /// Read the shown block's attribute definitions, in block order. + fn attman_load(&mut self, i: usize) { + let document = &self.tabs[i].scene.document; + let Some(state) = self.attman.as_mut() else { + return; + }; + let Some(name) = state.current_block().map(|b| b.name.clone()) else { + return; + }; + state.rows = document + .block_records + .get(&name) + .map(|br| { + br.entity_handles + .iter() + .filter_map(|h| match document.get_entity(*h) { + Some(EntityType::AttributeDefinition(def)) => Some(AttManRow { + handle: *h, + original_tag: def.tag.clone(), + def: def.clone(), + }), + _ => None, + }) + .collect() + }) + .unwrap_or_default(); + state.removed.clear(); + state.dirty = false; + state.selected = (!state.rows.is_empty()).then_some(0); + state.load_drafts(); + } + + pub(in crate::app) fn on_attman(&mut self, m: AttManMsg) -> Task { + let i = self.active_tab; + let Some(state) = self.attman.as_mut() else { + return Task::none(); + }; + // Edits of the selected row. + let mut edited = true; + let at = state.selected; + match m { + AttManMsg::Block(index) => { + if index == state.block || index >= state.blocks.len() { + return Task::none(); + } + // Pending changes of the shown block are kept. + if state.dirty && !self.attman_commit(i, true, false) { + return Task::none(); + } + if let Some(state) = self.attman.as_mut() { + state.block = index; + } + self.attman_load(i); + return Task::none(); + } + AttManMsg::PickBlock => { + self.active_modal = None; + let command = crate::modules::insert::attman::AttmanPickCommand; + use crate::command::CadCommand as _; + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + return Task::none(); + } + AttManMsg::Select(index) => { + if index < state.rows.len() { + state.selected = Some(index); + state.load_drafts(); + } + return Task::none(); + } + AttManMsg::MoveUp | AttManMsg::MoveDown => { + let Some(at) = at else { + return Task::none(); + }; + let to = if matches!(m, AttManMsg::MoveUp) { + at.checked_sub(1) + } else { + Some(at + 1).filter(|t| *t < state.rows.len()) + }; + let Some(to) = to else { + return Task::none(); + }; + state.rows.swap(at, to); + state.selected = Some(to); + } + AttManMsg::Remove => { + let Some(at) = at else { + return Task::none(); + }; + if state.rows.len() < 2 { + return Task::none(); + } + let row = state.rows.remove(at); + state.removed.push(row.handle); + state.selected = Some(at.min(state.rows.len() - 1)); + state.load_drafts(); + } + AttManMsg::ToggleEdit => { + state.edit_open = !state.edit_open; + return Task::none(); + } + AttManMsg::Tab(tab) => { + state.tab = tab; + return Task::none(); + } + AttManMsg::MoreColumns => { + state.more_columns = !state.more_columns; + return Task::none(); + } + AttManMsg::Column(column, on) => { + let bit = 1u32 << (column as u32); + let mut prefs = dialog::prefs(); + prefs.columns = if on { + prefs.columns | bit + } else { + prefs.columns & !bit + }; + return Task::none(); + } + AttManMsg::ApplyExisting(on) => { + dialog::prefs().apply_existing = on; + return Task::none(); + } + AttManMsg::Emphasize(on) => { + dialog::prefs().emphasize_duplicates = on; + return Task::none(); + } + AttManMsg::Live(on) => { + dialog::prefs().live = on; + if on && state.dirty { + self.attman_commit(i, false, false); + } + return Task::none(); + } + AttManMsg::DismissError => { + state.error = None; + return Task::none(); + } + AttManMsg::Help => { + self.command_line.push_info( + crate::t!("BATTMAN manages the attribute definitions of a block.").as_ref(), + ); + return Task::none(); + } + AttManMsg::Sync => { + self.attman_commit(i, true, true); + return Task::none(); + } + AttManMsg::Apply => { + self.attman_commit(i, true, false); + return Task::none(); + } + AttManMsg::Ok => { + if self.attman_commit(i, true, false) { + self.attman = None; + self.close_active_modal(); + } + return Task::none(); + } + AttManMsg::Cancel => { + let steps = state.live_steps; + self.attman = None; + self.close_active_modal(); + if steps > 0 { + self.undo_steps(steps); + } + return Task::none(); + } + _ => { + edited = false; + } + } + if edited { + state.dirty = true; + return self.attman_after_edit(i); + } + // Field edits of the selected row. + let Some(row) = at.and_then(|at| state.rows.get_mut(at)) else { + return Task::none(); + }; + let def = &mut row.def; + let yes = |on: bool| if on { "Yes" } else { "No" }; + match m { + AttManMsg::Tag(value) => { + state.drafts.tag = value.clone(); + if crate::ui::window::attdef_dialog::tag_error(&value).is_none() { + geom(def, "att_tag", &value); + } + } + AttManMsg::Prompt(value) => geom(def, "att_prompt", &value), + AttManMsg::Default(value) => geom(def, "att_default", &value), + AttManMsg::Mode(key, on) => { + let field = match key { + "invisible" => "att_invisible", + "verify" => "att_verify", + "preset" => "att_preset", + "multiline" => "att_multiline", + _ => return Task::none(), + }; + geom(def, field, yes(on)); + } + AttManMsg::Style(value) => geom(def, "att_style", &value), + AttManMsg::Justify(justify) => geom(def, "att_justify", justify.label()), + AttManMsg::Height(value) => { + state.drafts.height = value.clone(); + geom(def, "att_h", &value); + } + AttManMsg::Rotation(value) => { + state.drafts.rotation = value.clone(); + geom(def, "att_rot", &value); + } + AttManMsg::Width(value) => { + state.drafts.width = value.clone(); + geom(def, "att_wf", &value); + } + AttManMsg::Oblique(value) => { + state.drafts.oblique = value.clone(); + geom(def, "att_ob", &value); + } + AttManMsg::Boundary(value) => { + state.drafts.boundary = value.clone(); + geom(def, "att_bwidth", &value); + } + AttManMsg::Annotative(on) => geom(def, "att_annotative", yes(on)), + AttManMsg::Backwards(on) => geom(def, "att_backward", yes(on)), + AttManMsg::UpsideDown(on) => geom(def, "att_upside_down", yes(on)), + AttManMsg::Layer(value) => common(def, "layer", &value), + AttManMsg::Linetype(value) => common(def, "linetype", &value), + AttManMsg::Color(value) => { + if let Some(color) = dialog::parse_color(&value) { + def.common.color = color; + } + } + AttManMsg::Lineweight(value) => { + if let Some(lw) = dialog::parse_lineweight(&value) { + def.common.line_weight = lw; + } + } + _ => return Task::none(), + } + state.dirty = true; + self.attman_after_edit(i) + } + + fn attman_after_edit(&mut self, i: usize) -> Task { + if dialog::prefs().live { + self.attman_commit(i, false, false); + } + Task::none() + } + + /// The pick of `_ATTMAN_PICK `: show the picked reference's + /// block. A reference of a block without attributes is reported. + pub(in crate::app) fn attman_picked(&mut self, i: usize, handle: &str) -> Task { + self.active_modal = Some(crate::app::ModalKind::AttMan); + let Ok(value) = u64::from_str_radix(handle, 16) else { + return Task::none(); + }; + let name = match self.tabs[i].scene.document.get_entity(Handle::new(value)) { + Some(EntityType::Insert(insert)) => insert.block_name.clone(), + _ => return Task::none(), + }; + let index = self.attman.as_ref().and_then(|state| { + state + .blocks + .iter() + .position(|b| b.name.eq_ignore_ascii_case(&name)) + }); + match index { + Some(index) => self.on_attman(AttManMsg::Block(index)), + None => { + if let Some(state) = self.attman.as_mut() { + state.error = Some( + crate::tf!("Block table contains no attributed block named {}.", name) + .into_owned(), + ); + } + Task::none() + } + } + } + + /// Write the working copy to the drawing. `finish` closes the undo step + /// (Apply / OK / Sync); otherwise the step stays open for the next live + /// edit. `sync` rebuilds the references' attributes even when "apply to + /// existing references" is off. Returns false when a field is invalid. + fn attman_commit(&mut self, i: usize, finish: bool, sync: bool) -> bool { + let Some(state) = self.attman.as_mut() else { + return false; + }; + if finish { + if let Some(error) = state + .rows + .iter() + .find_map(|row| crate::ui::window::attdef_dialog::tag_error(&row.def.tag)) + .or_else(|| { + crate::ui::window::attdef_dialog::tag_error(&state.drafts.tag) + .filter(|_| state.selected.is_some()) + }) + { + state.error = Some(crate::t!(error).into_owned()); + return false; + } + state.error = None; + } + let Some(name) = state.current_block().map(|b| b.name.clone()) else { + return false; + }; + let rows: Vec<(Handle, String, AttributeDefinition)> = state + .rows + .iter() + .map(|r| (r.handle, r.original_tag.clone(), r.def.clone())) + .collect(); + let removed = state.removed.clone(); + let write = state.dirty || sync; + let open = state.live_steps > 0 && self.tabs[i].history.pending.is_some(); + if write { + // One undo step; a live step something else closed starts anew. + if !open { + self.push_undo_snapshot(i, "BATTMAN"); + } + self.attman_write(i, &name, &rows, &removed, sync || dialog::prefs().apply_existing); + } + let Some(state) = self.attman.as_mut() else { + return false; + }; + if finish { + if state.live_steps > 0 || write { + self.finish_pending_history(i); + } + if let Some(state) = self.attman.as_mut() { + state.live_steps = 0; + state.dirty = false; + state.removed.clear(); + for row in &mut state.rows { + row.original_tag = row.def.tag.clone(); + } + } + } else if write { + if !open { + state.live_steps += 1; + } + // The references now hold the written tags. + if sync || dialog::prefs().apply_existing { + for row in &mut state.rows { + row.original_tag = row.def.tag.clone(); + } + } + } + true + } + + /// The definition list of `block` becomes `rows` (in that order) less + /// `removed`; with `sync`, every reference is rebuilt from it, keeping + /// each attribute's value through a tag rename. + fn attman_write( + &mut self, + i: usize, + block: &str, + rows: &[(Handle, String, AttributeDefinition)], + removed: &[Handle], + sync: bool, + ) { + let document = &mut self.tabs[i].scene.document; + for handle in removed { + if document.get_entity(*handle).is_some() { + document.remove_entity(*handle); + } + } + // Each definition keeps its object; the definitions take the slots + // the block's definitions held, in the new order. + for (handle, _, def) in rows { + if let Some(EntityType::AttributeDefinition(slot)) = document.get_entity_mut(*handle) { + let mut def = def.clone(); + def.common.handle = *handle; + def.common.owner_handle = slot.common.owner_handle; + *slot = def; + } + } + if let Some(br) = document.block_records.get_mut(block) { + br.entity_handles.retain(|h| !removed.contains(h)); + let mut order = rows.iter().map(|(handle, _, _)| *handle); + for slot in br.entity_handles.iter_mut() { + if rows.iter().any(|(handle, _, _)| handle == slot) { + if let Some(next) = order.next() { + *slot = next; + } + } + } + } + // Annotative definitions carry the scale context data. + for (handle, _, def) in rows { + crate::scene::annotative::set_entity_annotative( + &mut self.tabs[i].scene.document, + *handle, + def.flags.annotative, + ); + } + if sync { + // A renamed definition keeps the value each reference held. + let renames: Vec<(String, String)> = rows + .iter() + .filter(|(_, old, def)| !old.eq_ignore_ascii_case(&def.tag)) + .map(|(_, old, def)| (old.clone(), def.tag.clone())) + .collect(); + if !renames.is_empty() { + let inserts: Vec = self.tabs[i] + .scene + .document + .entities() + .filter_map(|e| match e { + EntityType::Insert(ins) if ins.block_name.eq_ignore_ascii_case(block) => { + Some(ins.common.handle) + } + _ => None, + }) + .collect(); + for handle in inserts { + if let Some(EntityType::Insert(ins)) = + self.tabs[i].scene.document.get_entity_mut(handle) + { + for attribute in &mut ins.attributes { + if let Some((_, new)) = renames + .iter() + .find(|(old, _)| old.eq_ignore_ascii_case(&attribute.tag)) + { + attribute.tag = new.clone(); + } + } + } + } + } + self.attsync_block_with(i, block, false); + } + self.tabs[i].scene.bump_geometry(); + self.tabs[i].dirty = true; + } +} + +fn geom(def: &mut AttributeDefinition, field: &str, value: &str) { + let mut entity = EntityType::AttributeDefinition(def.clone()); + apply_geom_prop(&mut entity, field, value); + if let EntityType::AttributeDefinition(updated) = entity { + *def = updated; + } +} + +fn common(def: &mut AttributeDefinition, field: &str, value: &str) { + let mut entity = EntityType::AttributeDefinition(def.clone()); + apply_common_prop(&mut entity, field, value); + if let EntityType::AttributeDefinition(updated) = entity { + *def = updated; + } +} diff --git a/src/app/commands/block_cmdline.rs b/src/app/commands/block_cmdline.rs index 61b26bcd9..76b62aa2e 100644 --- a/src/app/commands/block_cmdline.rs +++ b/src/app/commands/block_cmdline.rs @@ -135,7 +135,12 @@ impl OpenCADStudio { .push_output("This drawing contains no attributed blocks."); return Some(Task::none()); } - self.open_attedit_dialog(); + let preselected = self.tabs[i].scene.selected_handles_in_order(); + return Some(self.open_attman(i, &preselected)); + } + cmd if cmd == "_ATTMAN_PICK" || cmd.starts_with("_ATTMAN_PICK ") => { + self.tabs[i].active_cmd = None; + return Some(self.attman_picked(i, cmd["_ATTMAN_PICK".len()..].trim())); } _ => return None, } @@ -166,6 +171,11 @@ impl OpenCADStudio { /// attributes whose definition is gone are dropped. Constant definitions /// carry no attribute. pub(in crate::app) fn attsync_block(&mut self, i: usize, block: &str) { + self.attsync_block_with(i, block, true); + } + + /// [`Self::attsync_block`], opening its own undo step only when `undo`. + pub(in crate::app) fn attsync_block_with(&mut self, i: usize, block: &str, undo: bool) { use codec::Entity as _; let doc = &self.tabs[i].scene.document; let attdefs: Vec = doc @@ -200,7 +210,9 @@ impl OpenCADStudio { if inserts.is_empty() { return; } - self.push_undo_snapshot(i, "ATTSYNC"); + if undo { + self.push_undo_snapshot(i, "ATTSYNC"); + } let mut changes = Vec::new(); for handle in inserts { let document = &self.tabs[i].scene.document; diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index e59fdbb38..9c92cbabe 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -1731,9 +1731,9 @@ mod tests { .join("\n") } - // ATTMAN and BATTMAN are the Block Attribute Manager; this build routes both - // to the attribute editor (ATTEDIT). A drawing without attributed blocks - // has nothing to manage, which both report instead of opening it. + // ATTMAN and BATTMAN open the Block Attribute Manager. A drawing without + // attributed blocks has nothing to manage, which both report instead of + // opening it. #[test] fn attman_and_battman_report_a_drawing_without_attributed_blocks() { for command in ["ATTMAN", "BATTMAN"] { diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 1b98f6377..933c8b6a2 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 attman; mod block_cmdline; mod blocks; mod blockvars; diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index aa397d1f4..b5ab30ddd 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -120,6 +120,7 @@ pub(super) const NAMES: &[&str] = &[ "dialog_ok", "attdef_dialog", "field_dialog", + "attman_dialog", "sheet_set", "close_document", "toggle_properties", @@ -1547,6 +1548,96 @@ impl OpenCADStudio { _ => return Err(failure("invalid_value", "Unknown attdef dialog field")), }) } + // The open Block Attribute Manager: `block=`, `select=`, + // `up`, `down`, `remove`, `edit`, `tab=attribute|text|properties`, + // `tag=…`, `prompt=…`, `default=…`, `mode=:0|1`, + // `style=…`, `justify=MC`, `height=…`, `rotation=…`, `width=…`, `oblique=…`, + // `boundary=…`, `annotative=0|1`, `backwards=0|1`, `upside_down=0|1`, + // `layer=…`, `linetype=…`, `color=…`, `lineweight=…`, `column=:0|1`, + // `more_columns`, `apply_existing=0|1`, `emphasize=0|1`, `live=0|1`, + // `pick`, `sync`, `apply`, `ok`, `cancel`. + "attman_dialog" => { + use crate::ui::window::attman_dialog::{AttManMsg as A, Column, EditTab}; + let Some(state) = self.attman.as_ref() else { + return Err(failure("no_dialog", "The attribute manager is not open")); + }; + let value = string(req, "value")?; + let (key, v) = value.split_once('=').unwrap_or((value, "")); + let on = v == "1"; + Message::AttMan(match key { + "block" => A::Block( + state + .blocks + .iter() + .position(|b| b.name.eq_ignore_ascii_case(v)) + .ok_or_else(|| failure("invalid_value", "No such attributed block"))?, + ), + "select" => A::Select( + v.parse() + .ok() + .filter(|r: &usize| *r < state.rows.len()) + .ok_or_else(|| failure("invalid_value", "No such row"))?, + ), + "up" => A::MoveUp, + "down" => A::MoveDown, + "remove" => A::Remove, + "edit" => A::ToggleEdit, + "tab" => A::Tab(match v { + "text" => EditTab::Text, + "properties" => EditTab::Properties, + _ => EditTab::Attribute, + }), + "tag" => A::Tag(v.into()), + "prompt" => A::Prompt(v.into()), + "default" => A::Default(v.into()), + "mode" => { + let (mode, state) = v.split_once(':').unwrap_or((v, "1")); + let mode = match mode { + "invisible" => "invisible", + "verify" => "verify", + "preset" => "preset", + "multiline" => "multiline", + _ => return Err(failure("invalid_value", "Unknown mode")), + }; + A::Mode(mode, state == "1") + } + "style" => A::Style(v.into()), + "justify" => A::Justify( + crate::modules::draw::draw::attdef::Justify::from_keyword(v) + .ok_or_else(|| failure("invalid_value", "Unknown justification"))?, + ), + "height" => A::Height(v.into()), + "rotation" => A::Rotation(v.into()), + "width" => A::Width(v.into()), + "oblique" => A::Oblique(v.into()), + "boundary" => A::Boundary(v.into()), + "annotative" => A::Annotative(on), + "backwards" => A::Backwards(on), + "upside_down" => A::UpsideDown(on), + "layer" => A::Layer(v.into()), + "linetype" => A::Linetype(v.into()), + "color" => A::Color(v.into()), + "lineweight" => A::Lineweight(v.into()), + "column" => { + let (name, state) = v.split_once(':').unwrap_or((v, "1")); + A::Column( + Column::from_name(name) + .ok_or_else(|| failure("invalid_value", "Unknown column"))?, + state == "1", + ) + } + "more_columns" => A::MoreColumns, + "apply_existing" => A::ApplyExisting(on), + "emphasize" => A::Emphasize(on), + "live" => A::Live(on), + "pick" => A::PickBlock, + "sync" => A::Sync, + "apply" => A::Apply, + "ok" => A::Ok, + "cancel" => A::Cancel, + _ => return Err(failure("invalid_value", "Unknown attribute manager 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=`, @@ -2119,6 +2210,9 @@ impl OpenCADStudio { Some(crate::app::ModalKind::Field) => { Message::FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg::Ok) } + Some(crate::app::ModalKind::AttMan) => { + Message::AttMan(crate::ui::window::attman_dialog::AttManMsg::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 8e3bebe25..fb979d557 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -585,6 +585,8 @@ pub(super) struct OpenCADStudio { /// ATTDEF dialog and Edit Attribute Definition dialog; `None` while closed. attdef_dialog: Option, attdef_edit: Option, + /// Block Attribute Manager working copy; `None` while it is closed. + attman: Option, /// Field dialog working copy; `None` while closed. field_dialog: Option, point_cloud_color_map: Option, @@ -2184,6 +2186,8 @@ pub enum ModalKind { GeometricTolerance, AttDef, AttDefEdit, + /// Block Attribute Manager (BATTMAN / ATTMAN). + AttMan, CountInvalidArea, Field, DraftingSettings, @@ -4395,6 +4399,8 @@ 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), + /// Block Attribute Manager edits. + AttMan(crate::ui::window::attman_dialog::AttManMsg), FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg), /// An edit in the Edit Reference dialog. RefEditDialog(crate::ui::window::refedit_dialog::RefEditDialogMsg), @@ -4607,6 +4613,7 @@ impl OpenCADStudio { pdf_attach: None, point_cloud_attach: None, attdef_dialog: None, + attman: None, attdef_edit: None, field_dialog: None, point_cloud_color_map: None, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index bcfb76142..547544318 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -232,6 +232,7 @@ impl OpenCADStudio { } Some(GeometricTolerance) => self.geometric_tolerance = None, Some(BlockDefinition) => self.block_definition = None, + Some(AttMan) => self.attman = None, Some(BlockEditPicker) => self.block_edit_picker = None, Some(BlockSaveAs) => self.block_save_as = None, Some(PdfAttach) => self.pdf_attach = None, @@ -2407,6 +2408,7 @@ impl OpenCADStudio { Message::PdfDialog(message) => self.update_pdf_dialog(message), Message::AttdefDialog(message) => self.on_attdef_dialog(message), + Message::AttMan(message) => self.on_attman(message), Message::FieldDialog(message) => self.on_field_dialog(message), Message::RefEditDialog(message) => self.update_refedit_dialog(message), Message::PcManager(message) => self.update_pc_manager(message), @@ -9571,6 +9573,13 @@ impl OpenCADStudio { self.close_field_dialog(); return Task::none(); } + // Closing the attribute manager is its Cancel: live changes + // are taken back. + if self.active_modal == Some(super::ModalKind::AttMan) { + return self.on_attman( + crate::ui::window::attman_dialog::AttManMsg::Cancel, + ); + } 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 dfb36033d..8a3d668fa 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -52,6 +52,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::AttMan) => crate::t!("Block Attribute Manager").into_owned(), Some(K::CountInvalidArea) => crate::t!("Count - Invalid Area").into_owned(), Some(K::Field) => crate::t!("Field").into_owned(), Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(), @@ -629,6 +630,12 @@ impl OpenCADStudio { super::super::ModalKind::CountInvalidArea => sized_flow(ex, 520, 260, |flow| { crate::ui::window::count_palette::invalid_area_view(&self.count_palette, flow) }), + super::super::ModalKind::AttMan => { + let state = self.attman.as_ref()?; + sized_flow(ex, 880, 660, |flow| { + crate::ui::window::attman_dialog::view(state, flow) + }) + } super::super::ModalKind::AttDefEdit => { let state = self.attdef_edit.as_ref()?; sized_flow(ex, 460, 220, |flow| { diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 46a790cc3..d1d1c0cb5 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -3,6 +3,18 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> { match source { + "Block Attribute Manager" => Some(("attman", "title")), + "Modes" => Some(("attman", "modes")), + "{} references" => Some(("attman", "references")), + "In drawing: {} · in model space: {}" => Some(("attman", "counts")), + "Columns:" => Some(("attman", "columns")), + "The Constant mode cannot be changed in the definition." => Some(("attman", "constant-note")), + "Show changes in the drawing as they are made" => Some(("attman", "live")), + "Apply changes to existing references" => Some(("attman", "apply-existing")), + "Emphasize duplicate tags" => Some(("attman", "emphasize")), + "Sync" => Some(("attman", "sync")), + "BATTMAN manages the attribute definitions of a block." => Some(("attman", "help")), + "Block table contains no attributed block named {}." => Some(("attman", "no-attributed-block")), "{} entities added" => Some(("ref-edit", "entities-added")), "Use REFCLOSE or the Refedit toolbar to end reference editing session." => Some(("ref-edit", "use-refclose")), "Specify opposite corner:" => Some(("ref-edit", "opposite-corner")), diff --git a/src/modules/insert/attman.rs b/src/modules/insert/attman.rs index d126d05da..1bfa5c452 100644 --- a/src/modules/insert/attman.rs +++ b/src/modules/insert/attman.rs @@ -8,3 +8,44 @@ pub fn tool() -> ToolDef { event: ModuleEvent::Command("ATTMAN".to_string()), } } + +/// The Block Attribute Manager's "Select block" button: one block reference +/// picked in the drawing, handed back as `_ATTMAN_PICK `. +pub struct AttmanPickCommand; + +impl crate::command::CadCommand for AttmanPickCommand { + fn name(&self) -> &'static str { + "BATTMAN" + } + + fn prompt(&self) -> String { + "Select a block:".into() + } + + fn needs_entity_pick(&self) -> bool { + true + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn on_entity_pick(&mut self, handle: codec::Handle, _pt: glam::DVec3) -> crate::command::CmdResult { + if handle.is_null() { + return crate::command::CmdResult::NeedPoint; + } + crate::command::CmdResult::Dispatch(format!("_ATTMAN_PICK {:X}", handle.value())) + } + + fn on_point(&mut self, _pt: glam::DVec3) -> crate::command::CmdResult { + crate::command::CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> crate::command::CmdResult { + crate::command::CmdResult::Dispatch("_ATTMAN_PICK".into()) + } + + fn on_escape(&mut self) -> crate::command::CmdResult { + crate::command::CmdResult::Dispatch("_ATTMAN_PICK".into()) + } +} diff --git a/src/modules/insert/mod.rs b/src/modules/insert/mod.rs index a8677fe3c..1f4c63d3a 100644 --- a/src/modules/insert/mod.rs +++ b/src/modules/insert/mod.rs @@ -2,7 +2,7 @@ mod attdef; pub(crate) mod attedit; -mod attman; +pub(crate) mod attman; pub mod base_point; pub(crate) mod block_cmdline; mod content_browser; diff --git a/src/ui/window/attman_dialog.rs b/src/ui/window/attman_dialog.rs new file mode 100644 index 000000000..958356557 --- /dev/null +++ b/src/ui/window/attman_dialog.rs @@ -0,0 +1,1037 @@ +//! Block Attribute Manager (BATTMAN / ATTMAN): the attribute definitions of +//! one attributed block as a table, with their order, editing, removal and +//! the update of the existing references. +//! +//! Blocks are chips across the top; the columns the table shows are chips +//! under it; the row actions sit in a column beside it; the selected row is +//! edited in a card below (Attribute / Text / Properties) and the settings +//! are switches in the footer. + +use codec::entities::AttributeDefinition; +use codec::Handle; +use iced::widget::{ + button, column, container, pick_list, row, scrollable, text, text_input, toggler, Space, +}; +use iced::{Background, Border, Color, Element, Fill, Length, Theme}; + +use crate::app::Message; +use crate::modules::draw::draw::attdef::Justify; +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, well_style}; + +static ICON_UP: &[u8] = include_bytes!("../../../assets/icons/ui/up.svg"); +static ICON_PICK: &[u8] = include_bytes!("../../../assets/icons/blocks/select_objects.svg"); + +/// The columns the table can show besides Tag, in the order of their chips. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Column { + Prompt, + Default, + Modes, + Annotative, + Style, + Height, + Layer, + Justify, + Rotation, + WidthFactor, + Oblique, + BoundaryWidth, + Linetype, + Color, + Lineweight, +} + +impl Column { + pub const ALL: [Column; 15] = [ + Column::Prompt, + Column::Default, + Column::Modes, + Column::Annotative, + Column::Style, + Column::Height, + Column::Layer, + Column::Justify, + Column::Rotation, + Column::WidthFactor, + Column::Oblique, + Column::BoundaryWidth, + Column::Linetype, + Column::Color, + Column::Lineweight, + ]; + /// The chips shown before "+N". + const FIRST: usize = 7; + + pub fn label(self) -> &'static str { + match self { + Column::Prompt => "Prompt", + Column::Default => "Default", + Column::Modes => "Modes", + Column::Annotative => "Annotative", + Column::Style => "Style", + Column::Height => "Height", + Column::Layer => "Layer", + Column::Justify => "Justification", + Column::Rotation => "Rotation", + Column::WidthFactor => "Width factor", + Column::Oblique => "Oblique angle", + Column::BoundaryWidth => "Boundary width", + Column::Linetype => "Linetype", + Column::Color => "Color", + Column::Lineweight => "Lineweight", + } + } + + pub fn from_name(name: &str) -> Option { + let key: String = name + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .collect::() + .to_ascii_lowercase(); + Column::ALL.into_iter().find(|c| { + c.label() + .chars() + .filter(|ch| ch.is_ascii_alphanumeric()) + .collect::() + .eq_ignore_ascii_case(&key) + }) + } + + fn bit(self) -> u32 { + 1 << (self as u32) + } + + fn cell(self, def: &AttributeDefinition) -> String { + let num = |v: f64| crate::ui::window::block_palette::short(v); + match self { + Column::Prompt => def.prompt.clone(), + Column::Default => def.default_value.clone(), + Column::Modes => modes(def), + Column::Annotative => yes_no(def.flags.annotative), + Column::Style => style_name(def), + Column::Height => num(def.height), + Column::Layer => def.common.layer.clone(), + Column::Justify => t!(justify(def).label()).into_owned(), + Column::Rotation => num(def.rotation.to_degrees()), + Column::WidthFactor => num(def.width_factor), + Column::Oblique => num(def.oblique_angle.to_degrees()), + Column::BoundaryWidth => def + .embedded_mtext + .as_ref() + .map_or(String::new(), |m| num(m.rectangle_width)), + Column::Linetype => crate::ui::properties::linetype_display_name(&def.common.linetype), + Column::Color => color_label(def.common.color), + Column::Lineweight => lineweight_label(def.common.line_weight), + } + } +} + +/// What the dialog remembers between openings. +#[derive(Debug, Clone, Copy)] +pub struct AttManPrefs { + /// Bit set of [`Column`]s shown. + pub columns: u32, + /// Apply and Sync rebuild the attributes of the existing references. + pub apply_existing: bool, + /// Rows sharing a tag are drawn in the danger colour. + pub emphasize_duplicates: bool, + /// Every edit is written to the drawing at once. + pub live: bool, +} + +static PREFS: std::sync::Mutex = std::sync::Mutex::new(AttManPrefs { + columns: (1 << Column::Prompt as u32) + | (1 << Column::Default as u32) + | (1 << Column::Modes as u32) + | (1 << Column::Annotative as u32), + apply_existing: true, + emphasize_duplicates: true, + live: true, +}); + +pub fn prefs() -> std::sync::MutexGuard<'static, AttManPrefs> { + PREFS.lock().unwrap_or_else(|e| e.into_inner()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum EditTab { + #[default] + Attribute, + Text, + Properties, +} + +/// One attributed block the dialog can show. +#[derive(Debug, Clone)] +pub struct AttManBlock { + pub name: String, + /// References anywhere in the drawing. + pub found: usize, + /// References in the current space. + pub in_space: usize, +} + +/// One attribute definition of the shown block. +#[derive(Debug, Clone)] +pub struct AttManRow { + /// The definition object in the block. + pub handle: Handle, + /// The tag the definition had when the block was loaded, for keeping the + /// references' values when it is renamed. + pub original_tag: String, + /// The working copy. + pub def: AttributeDefinition, +} + +/// The edit card's typed fields, kept as text while they are typed. +#[derive(Debug, Clone, Default)] +pub struct Drafts { + pub tag: String, + pub height: String, + pub rotation: String, + pub width: String, + pub oblique: String, + pub boundary: String, +} + +pub struct AttManState { + pub blocks: Vec, + pub block: usize, + pub rows: Vec, + /// Definitions removed from the shown block since it was loaded. + pub removed: Vec, + pub selected: Option, + pub tab: EditTab, + /// The edit card is shown. + pub edit_open: bool, + pub drafts: Drafts, + /// The working copy differs from the drawing. + pub dirty: bool, + /// Undo steps the live changes since the last Apply took (one unless + /// something closed the open step). + pub live_steps: usize, + /// The column chips past the first ones are shown. + pub more_columns: bool, + pub styles: Vec, + pub layers: Vec, + pub linetypes: Vec, + pub error: Option, +} + +impl AttManState { + pub fn current_block(&self) -> Option<&AttManBlock> { + self.blocks.get(self.block) + } + + pub fn selected_row(&self) -> Option<&AttManRow> { + self.selected.and_then(|i| self.rows.get(i)) + } + + /// Load the drafts from the selected row. + pub fn load_drafts(&mut self) { + let num = |v: f64| crate::ui::window::block_palette::short(v); + self.drafts = match self.selected_row() { + Some(row) => { + let d = &row.def; + Drafts { + tag: d.tag.clone(), + height: num(d.height), + rotation: num(d.rotation.to_degrees()), + width: num(d.width_factor), + oblique: num(d.oblique_angle.to_degrees()), + boundary: d + .embedded_mtext + .as_ref() + .map_or("0".into(), |m| num(m.rectangle_width)), + } + } + None => Drafts::default(), + }; + } + + /// Tags held by more than one row, upper-cased. + fn duplicate_tags(&self) -> Vec { + let mut seen = std::collections::HashMap::::new(); + for row in &self.rows { + *seen.entry(row.def.tag.to_ascii_uppercase()).or_default() += 1; + } + seen.into_iter() + .filter(|(_, n)| *n > 1) + .map(|(tag, _)| tag) + .collect() + } +} + +/// One edit in the dialog. +#[derive(Debug, Clone)] +pub enum AttManMsg { + Block(usize), + PickBlock, + Select(usize), + MoveUp, + MoveDown, + ToggleEdit, + Remove, + Tab(EditTab), + Column(Column, bool), + MoreColumns, + Tag(String), + Prompt(String), + Default(String), + /// "invisible", "verify", "preset" or "multiline". + Mode(&'static str, bool), + Style(String), + Justify(Justify), + Height(String), + Rotation(String), + Width(String), + Oblique(String), + Boundary(String), + Annotative(bool), + Backwards(bool), + UpsideDown(bool), + Layer(String), + Linetype(String), + Color(String), + Lineweight(String), + ApplyExisting(bool), + Emphasize(bool), + Live(bool), + Sync, + Apply, + Ok, + Cancel, + Help, + DismissError, +} + +fn msg(m: AttManMsg) -> Message { + Message::AttMan(m) +} + +// ── Cell text ───────────────────────────────────────────────────────────── + +fn yes_no(on: bool) -> String { + if on { t!("Yes") } else { t!("No") }.into_owned() +} + +/// Mode letters: I invisible, C constant, V verify, P preset, L locked +/// position, M multiple lines. +pub fn modes(def: &AttributeDefinition) -> String { + let f = &def.flags; + [ + (f.invisible, 'I'), + (f.constant, 'C'), + (f.verify, 'V'), + (f.preset, 'P'), + (f.locked_position || def.lock_position, 'L'), + (def.is_multiline, 'M'), + ] + .into_iter() + .filter_map(|(on, letter)| on.then_some(letter)) + .collect() +} + +pub fn justify(def: &AttributeDefinition) -> Justify { + Justify::from_alignment(def.horizontal_alignment, def.vertical_alignment) +} + +fn style_name(def: &AttributeDefinition) -> String { + if def.text_style.trim().is_empty() { + "Standard".into() + } else { + def.text_style.clone() + } +} + +/// The colours the Properties tab offers. +pub const COLORS: [&str; 9] = [ + "ByLayer", "ByBlock", "Red", "Yellow", "Green", "Cyan", "Blue", "Magenta", "White", +]; + +pub fn color_label(color: codec::types::Color) -> String { + use codec::types::Color as C; + match color { + C::ByLayer => "ByLayer".into(), + C::ByBlock => "ByBlock".into(), + C::Index(n @ 1..=7) => COLORS[n as usize + 1].into(), + C::Index(n) => format!("{n}"), + C::Rgb { r, g, b } => format!("{r},{g},{b}"), + C::None => "None".into(), + } +} + +pub fn parse_color(label: &str) -> Option { + use codec::types::Color as C; + match COLORS.iter().position(|c| c.eq_ignore_ascii_case(label.trim())) { + Some(0) => Some(C::ByLayer), + Some(1) => Some(C::ByBlock), + Some(i) => Some(C::Index(i as u8 - 1)), + None => label + .trim() + .parse::() + .ok() + .filter(|n| (1..=255).contains(n)) + .map(C::Index), + } +} + +/// The lineweights the Properties tab offers, in 1/100 mm. +const LINEWEIGHTS: [i16; 24] = [ + 0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, + 200, 211, +]; + +pub fn lineweight_label(lw: codec::types::LineWeight) -> String { + use codec::types::LineWeight as L; + match lw { + L::ByLayer => "ByLayer".into(), + L::ByBlock => "ByBlock".into(), + L::Default => "Default".into(), + L::Value(v) => format!("{:.2} mm", f64::from(v) / 100.0), + } +} + +pub fn parse_lineweight(label: &str) -> Option { + use codec::types::LineWeight as L; + let s = label.trim(); + match s.to_ascii_lowercase().as_str() { + "bylayer" => Some(L::ByLayer), + "byblock" => Some(L::ByBlock), + "default" => Some(L::Default), + _ => { + let mm = s.trim_end_matches("mm").trim().parse::().ok()?; + let v = (mm * 100.0).round() as i16; + LINEWEIGHTS.contains(&v).then_some(L::Value(v)) + } + } +} + +fn lineweight_options() -> Vec { + ["ByLayer", "ByBlock", "Default"] + .into_iter() + .map(str::to_string) + .chain( + LINEWEIGHTS + .iter() + .map(|v| lineweight_label(codec::types::LineWeight::Value(*v))), + ) + .collect() +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct JustifyItem(Justify); + +impl std::fmt::Display for JustifyItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&t!(self.0.label())) + } +} + +// ── View ────────────────────────────────────────────────────────────────── + +fn panel<'a>(content: impl Into>) -> Element<'a, Message> { + container(content) + .padding([10, 12]) + .width(Fill) + .style(card_style) + .into() +} + +/// A pill: framed in the accent colour while on. +fn pill<'a>(label: String, on: bool, message: Option) -> Element<'a, Message> { + let b = button(text(label).size(11)) + .padding([4, 11]) + .style(move |theme: &Theme, status| { + let palette = theme.palette(); + let mut style = button_style(false)(theme, status); + style.border = Border { + width: 1.0, + radius: 12.0.into(), + color: if on { + palette.primary.base.color + } else { + palette.background.strong.color + }, + }; + if on { + style.background = Some(Background::Color( + palette.primary.base.color.scale_alpha(0.16), + )); + } + style + }); + match message { + Some(m) => b.on_press(m).into(), + None => b.into(), + } +} + +fn icon_button<'a>( + icon: &'static [u8], + rotation: f32, + message: Option, + danger: bool, +) -> Element<'a, Message> { + let enabled = message.is_some(); + let svg = iced::widget::svg(crate::ui::icons::themed_handle(icon)) + .width(14) + .height(14) + .rotation(iced::Radians(rotation)) + .style(move |theme: &Theme, _| iced::widget::svg::Style { + color: Some(if danger && enabled { + theme.palette().danger.base.color + } else if enabled { + theme.palette().background.base.text + } else { + theme.palette().background.base.text.scale_alpha(0.35) + }), + }); + let b = button(container(svg).center_x(Fill).center_y(Fill)) + .width(30) + .height(28) + .padding(0) + .style(button_style(false)); + match message { + Some(m) => b.on_press(m).into(), + None => b.into(), + } +} + +fn labeled_switch<'a>(label: String, on: bool, f: fn(bool) -> AttManMsg) -> Element<'a, Message> { + row![ + toggler(on).on_toggle(move |v| msg(f(v))).size(16.0), + text(label).size(11), + ] + .spacing(6) + .align_y(iced::Center) + .into() +} + +fn field_row<'a>( + label: String, + value: &'a str, + on_input: Option AttManMsg>, +) -> Element<'a, Message> { + let input: Element<'a, Message> = match on_input { + Some(f) => text_input("", value) + .on_input(move |v| msg(f(v))) + .style(field_style) + .size(11) + .padding([4, 8]) + .width(Fill) + .into(), + None => crate::ui::read_only::field(value, 11.0, Fill), + }; + row![ + text(label) + .size(11) + .style(muted_style) + .width(Length::Fixed(96.0)), + input + ] + .spacing(8) + .align_y(iced::Center) + .into() +} + +fn choice_row<'a>( + label: String, + selected: String, + options: Vec, + on: Option AttManMsg>, +) -> Element<'a, Message> { + let list: Element<'a, Message> = match on { + Some(f) => pick_list(Some(selected), options, |s: &String| s.clone()) + .on_select(move |v| msg(f(v))) + .text_size(11) + .padding([3, 8]) + .width(Fill) + .into(), + None => crate::ui::read_only::field(&selected, 11.0, Fill), + }; + row![ + text(label) + .size(11) + .style(muted_style) + .width(Length::Fixed(96.0)), + list + ] + .spacing(8) + .align_y(iced::Center) + .into() +} + +fn segmented<'a>(current: EditTab) -> Element<'a, Message> { + let tabs = [ + (EditTab::Attribute, t!("Attribute").into_owned()), + (EditTab::Text, t!("Text").into_owned()), + (EditTab::Properties, t!("Properties").into_owned()), + ]; + let buttons = tabs.into_iter().map(|(tab, label)| { + button(text(label).size(11).width(Fill).align_x(iced::Center)) + .on_press(msg(AttManMsg::Tab(tab))) + .style(button_style(tab == current)) + .padding([5, 6]) + .width(Fill) + .into() + }); + container(iced::widget::Row::with_children(buttons.collect::>()).spacing(2)) + .padding(2) + .width(Length::Fixed(330.0)) + .style(well_style) + .into() +} + +fn table<'a>(state: &'a AttManState, columns: &[Column], emphasize: bool) -> Element<'a, Message> { + let duplicates = if emphasize { + state.duplicate_tags() + } else { + Vec::new() + }; + let header = { + let mut r = row![text(t!("Tag")).size(11).style(muted_style).width(Fill)]; + for c in columns { + r = r.push(text(t!(c.label())).size(11).style(muted_style).width(Fill)); + } + container(r.spacing(8)).padding([4, 8]) + }; + let rows = state.rows.iter().enumerate().map(|(index, entry)| { + let duplicate = duplicates.contains(&entry.def.tag.to_ascii_uppercase()); + let chosen = state.selected == Some(index); + let tint = move |theme: &Theme| text::Style { + color: duplicate.then(|| theme.palette().danger.base.color), + }; + let mut r = row![text(entry.def.tag.clone()).size(11).style(tint).width(Fill)]; + for c in columns { + r = r.push(text(c.cell(&entry.def)).size(11).style(tint).width(Fill)); + } + button(r.spacing(8)) + .on_press(msg(AttManMsg::Select(index))) + .padding([5, 8]) + .width(Fill) + .style(move |theme: &Theme, status| { + let palette = theme.palette(); + let mut style = button::text(theme, status); + style.text_color = palette.background.base.text; + style.background = if chosen { + Some(Background::Color(palette.primary.base.color.scale_alpha(0.18))) + } else if matches!(status, button::Status::Hovered) { + Some(Background::Color(palette.background.strong.color)) + } else { + None + }; + style + }) + .into() + }); + column![ + header, + container(Space::new().height(1)) + .width(Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.strong.color)), + ..Default::default() + }), + scrollable(column(rows.collect::>>()).spacing(1)) + .height(Length::Fixed(150.0)), + ] + .spacing(2) + .width(Fill) + .into() +} + +fn edit_card<'a>(state: &'a AttManState, row_: &'a AttManRow) -> Element<'a, Message> { + let def = &row_.def; + let title = text(format!("{} — {}", def.tag, t!("Edit")).to_uppercase()) + .size(10) + .style(accent_text); + let body: Element<'a, Message> = match state.tab { + EditTab::Attribute => { + let constant = def.flags.constant; + let mode = |key: &'static str, label: &str, on: bool| { + pill( + t!(label).into_owned(), + on, + Some(msg(AttManMsg::Mode(key, !on))), + ) + }; + let fields = column![ + field_row(t!("Tag").into_owned(), &state.drafts.tag, Some(AttManMsg::Tag)), + field_row( + t!("Prompt").into_owned(), + &def.prompt, + (!constant).then_some(AttManMsg::Prompt as fn(String) -> AttManMsg), + ), + field_row( + t!("Default").into_owned(), + &def.default_value, + Some(AttManMsg::Default), + ), + ] + .spacing(6) + .width(Fill); + let modes = column![ + row![ + mode("invisible", "Invisible", def.flags.invisible), + mode("verify", "Verify", def.flags.verify), + ] + .spacing(6), + row![ + mode("preset", "Preset", def.flags.preset), + mode("multiline", "Multiple lines", def.is_multiline), + ] + .spacing(6), + text(t!("The Constant mode cannot be changed in the definition.")) + .size(10) + .style(muted_style), + ] + .spacing(6) + .width(Length::Fixed(230.0)); + row![fields, modes].spacing(18).into() + } + EditTab::Text => { + let justify_list: Element<'a, Message> = if def.is_multiline { + crate::ui::read_only::field(&t!(justify(def).label()), 11.0, Fill) + } else { + pick_list( + Some(JustifyItem(justify(def))), + Justify::ALL.map(JustifyItem).to_vec(), + |j: &JustifyItem| j.to_string(), + ) + .on_select(|j: JustifyItem| msg(AttManMsg::Justify(j.0))) + .text_size(11) + .padding([3, 8]) + .width(Fill) + .into() + }; + let left = column![ + choice_row( + t!("Text Style").into_owned(), + style_name(def), + state.styles.clone(), + Some(AttManMsg::Style), + ), + row![ + text(t!("Justification")) + .size(11) + .style(muted_style) + .width(Length::Fixed(96.0)), + justify_list + ] + .spacing(8) + .align_y(iced::Center), + field_row( + t!("Height").into_owned(), + &state.drafts.height, + Some(AttManMsg::Height) + ), + field_row( + t!("Rotation").into_owned(), + &state.drafts.rotation, + Some(AttManMsg::Rotation) + ), + ] + .spacing(6) + .width(Fill); + let right = column![ + field_row( + t!("Width factor").into_owned(), + &state.drafts.width, + Some(AttManMsg::Width) + ), + field_row( + t!("Oblique angle").into_owned(), + &state.drafts.oblique, + Some(AttManMsg::Oblique) + ), + field_row( + t!("Boundary width").into_owned(), + &state.drafts.boundary, + def.is_multiline + .then_some(AttManMsg::Boundary as fn(String) -> AttManMsg), + ), + row![ + pill( + t!("Annotative").into_owned(), + def.flags.annotative, + Some(msg(AttManMsg::Annotative(!def.flags.annotative))), + ), + pill( + t!("Backwards").into_owned(), + def.text_generation_flags & 0x2 != 0, + Some(msg(AttManMsg::Backwards( + def.text_generation_flags & 0x2 == 0 + ))), + ), + pill( + t!("Upside down").into_owned(), + def.text_generation_flags & 0x4 != 0, + Some(msg(AttManMsg::UpsideDown( + def.text_generation_flags & 0x4 == 0 + ))), + ), + ] + .spacing(6), + ] + .spacing(6) + .width(Fill); + row![left, right].spacing(18).into() + } + EditTab::Properties => { + let mut linetypes = vec!["ByLayer".to_string(), "ByBlock".to_string()]; + linetypes.extend(state.linetypes.iter().cloned()); + let linetype = if def.common.linetype.is_empty() { + "ByLayer".to_string() + } else { + def.common.linetype.clone() + }; + row![ + column![ + choice_row( + t!("Layer").into_owned(), + def.common.layer.clone(), + state.layers.clone(), + Some(AttManMsg::Layer), + ), + choice_row( + t!("Linetype").into_owned(), + linetype, + linetypes, + Some(AttManMsg::Linetype), + ), + ] + .spacing(6) + .width(Fill), + column![ + choice_row( + t!("Color").into_owned(), + color_label(def.common.color), + COLORS.iter().map(|c| c.to_string()).collect(), + Some(AttManMsg::Color), + ), + choice_row( + t!("Lineweight").into_owned(), + lineweight_label(def.common.line_weight), + lineweight_options(), + Some(AttManMsg::Lineweight), + ), + ] + .spacing(6) + .width(Fill), + ] + .spacing(18) + .into() + } + }; + let live = prefs().live; + panel( + column![ + title, + segmented(state.tab), + body, + labeled_switch( + t!("Show changes in the drawing as they are made").into_owned(), + live, + AttManMsg::Live + ), + ] + .spacing(10), + ) +} + +pub fn view<'a>( + state: &'a AttManState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let prefs = *prefs(); + // ── Blocks ── + let mut chips = row![icon_button( + ICON_PICK, + 0.0, + Some(msg(AttManMsg::PickBlock)), + false + )] + .spacing(6) + .align_y(iced::Center); + for (index, block) in state.blocks.iter().enumerate() { + let on = index == state.block; + let label = if on { + format!( + "{} · {}", + block.name, + crate::tf!("{} references", block.found) + ) + } else { + block.name.clone() + }; + chips = chips.push(pill(label, on, Some(msg(AttManMsg::Block(index))))); + } + let counts = state.current_block().map_or(String::new(), |b| { + crate::tf!( + "In drawing: {} · in model space: {}", + b.found, + b.in_space + ) + .into_owned() + }); + let blocks_card = panel( + row![ + scrollable(chips).direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::new().width(3).scroller_width(3) + )), + Space::new().width(Fill), + text(counts).size(10).style(muted_style), + ] + .spacing(10) + .align_y(iced::Center), + ); + + // ── Table ── + let columns: Vec = Column::ALL + .into_iter() + .filter(|c| prefs.columns & c.bit() != 0) + .collect(); + let mut column_chips = row![text(t!("Columns:")).size(11).style(muted_style)] + .spacing(6) + .align_y(iced::Center); + let shown = if state.more_columns { + Column::ALL.len() + } else { + Column::FIRST + }; + for c in &Column::ALL[..shown] { + let on = prefs.columns & c.bit() != 0; + column_chips = column_chips.push(pill( + t!(c.label()).into_owned(), + on, + Some(msg(AttManMsg::Column(*c, !on))), + )); + } + column_chips = column_chips.push(pill( + if state.more_columns { + "−".to_string() + } else { + format!("+{}", Column::ALL.len() - Column::FIRST) + }, + false, + Some(msg(AttManMsg::MoreColumns)), + )); + let count = state.rows.len(); + let at = state.selected; + let actions = column![ + icon_button( + ICON_UP, + 0.0, + at.filter(|i| *i > 0).map(|_| msg(AttManMsg::MoveUp)), + false + ), + icon_button( + ICON_UP, + std::f32::consts::PI, + at.filter(|i| i + 1 < count).map(|_| msg(AttManMsg::MoveDown)), + false + ), + icon_button( + crate::ui::icons::MORE, + 0.0, + at.map(|_| msg(AttManMsg::ToggleEdit)), + false + ), + // The last attribute of a block cannot be removed. + icon_button( + crate::ui::icons::CLOSE, + 0.0, + at.filter(|_| count > 1).map(|_| msg(AttManMsg::Remove)), + true + ), + ] + .spacing(6); + let table_card = panel( + row![ + column![ + table(state, &columns, prefs.emphasize_duplicates), + scrollable(column_chips).direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::new().width(3).scroller_width(3) + )), + ] + .spacing(10) + .width(Fill), + actions, + ] + .spacing(10), + ); + + let mut content = column![blocks_card, table_card].spacing(10); + if state.edit_open { + if let Some(selected) = state.selected_row() { + content = content.push(edit_card(state, selected)); + } + } + if let Some(error) = &state.error { + content = content.push( + container( + row![ + crate::ui::icons::semantic( + include_bytes!("../../../assets/icons/ui/warning_triangle.svg"), + 15.0 + ), + text(error.clone()).size(11).width(Fill), + button(text("×").size(12)) + .on_press(msg(AttManMsg::DismissError)) + .style(button_style(false)) + .padding([1, 6]), + ] + .spacing(8) + .align_y(iced::Center), + ) + .padding([6, 10]) + .width(Fill) + .style(|theme: &Theme| { + let danger = theme.palette().danger.base.color; + container::Style { + background: Some(Background::Color(danger.scale_alpha(0.16))), + border: Border { + width: 1.0, + radius: 6.0.into(), + color: danger, + }, + ..Default::default() + } + }), + ); + } + let footer = row![ + button(text("?").size(12)) + .on_press(msg(AttManMsg::Help)) + .style(button_style(false)) + .padding([3, 10]), + labeled_switch( + t!("Apply changes to existing references").into_owned(), + prefs.apply_existing, + AttManMsg::ApplyExisting + ), + labeled_switch( + t!("Emphasize duplicate tags").into_owned(), + prefs.emphasize_duplicates, + AttManMsg::Emphasize + ), + Space::new().width(Fill), + dialog_button(t!("Sync"), msg(AttManMsg::Sync), false), + dialog_button(t!("Apply"), msg(AttManMsg::Apply), false), + dialog_button(t!("OK"), msg(AttManMsg::Ok), true), + ] + .spacing(12) + .align_y(iced::Center); + container(content.push(footer)) + .padding([10, 14]) + .width(sizing.width) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(Color::TRANSPARENT)), + ..Default::default() + }) + .into() +} diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index b12716252..9dbc3430c 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -2,6 +2,7 @@ pub mod about; pub mod alias_editor; pub mod annotation_data; pub mod attdef_dialog; +pub mod attman_dialog; pub mod attribute_editor; pub mod auto_constrain_settings; pub mod block_definition;