From 92a3a8ee4df24cfeae63348801dbc0b0c01aa927 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:39:28 +0300 Subject: [PATCH] REFEDIT: Edit Reference dialog and unique names for xref objects REFEDIT opens a dialog after the pick: the nesting chain as chips with a preview of the chosen definition, all objects or picking them in the drawing, attribute definitions (offered only when the definition has them), locking objects outside the working set and unique names for an external reference's named objects. The settings are remembered. With unique names an xref's dependent layers, linetypes, text styles and blocks are checked out as $N$NAME records of their own; without, under the plain name, sharing a host record of that name. Save writes them back under the reference's names, and the records made for the session are removed when it closes. -REFEDIT uses the remembered settings. Co-Authored-By: Claude Opus 5.5 --- locales/ar-SA/opencadstudio.ftl | 13 ++ locales/bg-BG/opencadstudio.ftl | 13 ++ locales/cs-CZ/opencadstudio.ftl | 13 ++ locales/de-DE/opencadstudio.ftl | 13 ++ locales/el-GR/opencadstudio.ftl | 13 ++ locales/en-US/opencadstudio.ftl | 13 ++ locales/es-ES/opencadstudio.ftl | 13 ++ locales/fi-FI/opencadstudio.ftl | 13 ++ locales/fr-FR/opencadstudio.ftl | 13 ++ locales/hi-IN/opencadstudio.ftl | 13 ++ locales/hu-HU/opencadstudio.ftl | 13 ++ locales/it-IT/opencadstudio.ftl | 13 ++ locales/ja-JP/opencadstudio.ftl | 13 ++ locales/ko-KR/opencadstudio.ftl | 13 ++ locales/lo-LA/opencadstudio.ftl | 13 ++ locales/nl-NL/opencadstudio.ftl | 13 ++ locales/pl-PL/opencadstudio.ftl | 13 ++ locales/pt-BR/opencadstudio.ftl | 13 ++ locales/ru-RU/opencadstudio.ftl | 13 ++ locales/tr-TR/opencadstudio.ftl | 13 ++ locales/zh-CN/opencadstudio.ftl | 13 ++ locales/zh-TW/opencadstudio.ftl | 13 ++ src/app/commands/refedit.rs | 348 +++++++++++++++++++++++++++-- src/app/control/actions.rs | 31 +++ src/app/mod.rs | 9 + src/app/settings.rs | 28 +++ src/app/update/file.rs | 2 + src/app/update/mod.rs | 4 + src/app/view/modal.rs | 7 + src/modules/draw/modify/refedit.rs | 118 +++++++++- src/ui/window/mod.rs | 1 + src/ui/window/pdf_dialogs.rs | 2 +- src/ui/window/refedit_dialog.rs | 247 ++++++++++++++++++++ 33 files changed, 1061 insertions(+), 22 deletions(-) create mode 100644 src/ui/window/refedit_dialog.rs diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index f210ce5eb..83da99f84 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -6471,6 +6471,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = حدد المرجع: .reference-not-found = لم يتم العثور على المرجع. .not-valid-reference = الكائن ليس مرجعًا صالحًا diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 694d37d17..81bc600b0 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -6459,6 +6459,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Изберете препратка: .reference-not-found = Препратката не е намерена. .not-valid-reference = Обектът не е валидна препратка diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 55dc8ab97..202348e69 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -6456,6 +6456,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Vyberte referenci: .reference-not-found = Reference nebyla nalezena. .not-valid-reference = Objekt není platná reference diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index ef91c0ea0..f832e5060 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -6457,6 +6457,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Referenz wählen: .reference-not-found = Referenz nicht gefunden. .not-valid-reference = Objekt ist keine gültige Referenz diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index afa07b2fb..059f657f3 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -6459,6 +6459,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Επιλέξτε αναφορά: .reference-not-found = Η αναφορά δεν βρέθηκε. .not-valid-reference = Το αντικείμενο δεν είναι έγκυρη αναφορά diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index c0a15929d..caf552d8d 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -6458,6 +6458,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Select reference: .reference-not-found = Reference not found. .not-valid-reference = Object is not a valid reference diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 786b46eaf..076337343 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -6459,6 +6459,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Seleccione la referencia: .reference-not-found = No se encuentra la referencia. .not-valid-reference = El objeto no es una referencia válida diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 5381c7037..3c00e054e 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -6456,6 +6456,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Valitse viittaus: .reference-not-found = Viittausta ei löydy. .not-valid-reference = Objekti ei ole kelvollinen viittaus diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index b9f67f2da..be94b811b 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -6459,6 +6459,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Sélectionnez la référence : .reference-not-found = Référence introuvable. .not-valid-reference = L'objet n'est pas une référence valide diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 1626cd702..58165a730 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -6447,6 +6447,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = संदर्भ चुनें: .reference-not-found = संदर्भ नहीं मिला। .not-valid-reference = ऑब्जेक्ट मान्य संदर्भ नहीं है diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index fb17ef462..573b4fc56 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -6443,6 +6443,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Válassza ki a hivatkozást: .reference-not-found = A hivatkozás nem található. .not-valid-reference = Az objektum nem érvényes hivatkozás diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 85e49acad..4efd1d7c9 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -6458,6 +6458,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Selezionare il riferimento: .reference-not-found = Riferimento non trovato. .not-valid-reference = L'oggetto non è un riferimento valido diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 72df97414..3646f0a6c 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -6447,6 +6447,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = 参照を選択: .reference-not-found = 参照が見つかりません。 .not-valid-reference = オブジェクトは有効な参照ではありません diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 9899dbd75..37b33269e 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -6458,6 +6458,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = 참조 선택: .reference-not-found = 참조를 찾을 수 없습니다. .not-valid-reference = 객체가 유효한 참조가 아닙니다 diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index 3b9358f63..12ff7ddd6 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -6459,6 +6459,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = ເລືອກການອ້າງອີງ: .reference-not-found = ບໍ່ພົບການອ້າງອີງ. .not-valid-reference = ວັດຖຸບໍ່ແມ່ນການອ້າງອີງທີ່ຖືກຕ້ອງ diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 25ce0ed57..f37315210 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -6457,6 +6457,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Selecteer referentie: .reference-not-found = Referentie niet gevonden. .not-valid-reference = Object is geen geldige referentie diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index eb2c2c96c..ea40f9964 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -6458,6 +6458,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Wybierz odnośnik: .reference-not-found = Nie znaleziono odnośnika. .not-valid-reference = Obiekt nie jest prawidłowym odnośnikiem diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 120d5b580..d7f4d86bc 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -6459,6 +6459,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Selecione a referência: .reference-not-found = Referência não encontrada. .not-valid-reference = O objeto não é uma referência válida diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index e0838f93c..798650c15 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -6460,6 +6460,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = Выберите ссылку: .reference-not-found = Ссылка не найдена. .not-valid-reference = Объект не является допустимой ссылкой diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index c16d192e1..bd4cec166 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -6380,6 +6380,19 @@ view-background = .select-image = Arka Plan Görüntüsü Seç ref-edit = + .dialog-title = Referansı Düzenle + .dialog-reference = Düzenlenecek referans + .dialog-chain-hint = Seçilen noktadaki zincir; bir çipe tıklayarak düzeyi değiştirin. + .dialog-path = Yol: + .dialog-working-set = Çalışma kümesi + .dialog-all-objects = Tüm nesneler + .dialog-pick-objects = Çizimde seçeceğim + .dialog-attributes = Öznitelik tanımlarını da düzenle + .dialog-session = Oturum + .dialog-lock = Kümede olmayan nesneleri kilitle + .dialog-unique = Çakışan katman, stil ve blok adlarını benzersiz yap + .dialog-start = Düzenlemeye Başla + .dialog-help = Bir blok referansını veya dış referansı yerinde düzenlemek için açar. .select-reference = Referans seçin: .reference-not-found = Referans bulunamadı. .not-valid-reference = Nesne geçerli bir referans değil diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 8a81b1bc1..eeb18bd8f 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -6442,6 +6442,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = 选择参照: .reference-not-found = 找不到参照。 .not-valid-reference = 对象不是有效的参照 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 28ac1a835..0d1e246f2 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -6454,6 +6454,19 @@ view-background = .select-image = Select Background Image ref-edit = + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. .select-reference = 選取參考: .reference-not-found = 找不到參考。 .not-valid-reference = 物件不是有效的參考 diff --git a/src/app/commands/refedit.rs b/src/app/commands/refedit.rs index bc8ec3732..1b5db8388 100644 --- a/src/app/commands/refedit.rs +++ b/src/app/commands/refedit.rs @@ -4,10 +4,11 @@ use crate::app::{Message, OpenCADStudio}; use crate::modules::draw::modify::refedit::{ - apply_insert_inverse_transform, apply_insert_transform, level_hits, - nesting_chain, RefCloseCommand, RefEditOptionsCommand, RefEditPickCommand, RefEditSession, - RefSetCommand, NOT_CHECKED_OUT, + apply_insert_inverse_transform, apply_insert_transform, level_hits, nesting_chain, + rename_symbols, ChainLevel, RefCloseCommand, RefEditNames, RefEditOptionsCommand, + RefEditPickCommand, RefEditSession, RefSetCommand, SymbolKind, NOT_CHECKED_OUT, }; +use crate::ui::window::refedit_dialog::{RefEditDialogMsg, RefEditDialogState, RefEditLevel}; use codec::{EntityType, Handle}; use iced::Task; use std::collections::HashSet; @@ -142,25 +143,127 @@ impl OpenCADStudio { .iter() .map(|level| level_hits(&self.tabs[i].scene.document, level)) .collect(); - let command = RefEditOptionsCommand::new(top, chain, hits); + let command = RefEditOptionsCommand::new( + top, + chain, + hits, + self.refedit_options.lock, + self.refedit_options.unique_names, + ); use crate::command::CadCommand; self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } else { - // The dialog's defaults: the innermost reference, all objects, - // attribute definitions not displayed. - let path: Vec = chain[1..] - .iter() - .map(|l| l.insert.value().to_string()) - .collect(); - let _ = self.dispatch_command(&format!( - "REFEDIT_BEGIN:{}:{}:0:", - top.value(), - path.join(",") - )); + self.open_refedit_dialog(i, top, chain); } } + /// The Edit Reference dialog over the picked chain: the innermost level + /// chosen, the remembered settings. + fn open_refedit_dialog(&mut self, i: usize, top: Handle, chain: Vec) { + let scene = &self.tabs[i].scene; + let document = &scene.document; + let levels = chain + .iter() + .map(|level| { + let record = document.block_records.get(&level.block_name); + RefEditLevel { + insert: level.insert, + name: level.block_name.clone(), + wires: scene.block_preview_wires(&level.block_name), + has_attdefs: record.is_some_and(|br| { + br.entity_handles.iter().any(|h| { + matches!(document.get_entity(*h), Some(EntityType::AttributeDefinition(_))) + }) + }), + xref_path: record + .filter(|br| br.flags.is_xref || br.flags.is_xref_overlay) + .map(|br| br.xref_path.clone()) + .filter(|p| !p.is_empty()), + } + }) + .collect::>(); + let options = &self.refedit_options; + self.refedit_dialog = Some(RefEditDialogState { + top, + level: levels.len().saturating_sub(1), + levels, + chain, + nested: options.nested, + attributes: options.attributes, + lock: options.lock, + unique: options.unique_names, + }); + self.active_modal = Some(crate::app::ModalKind::RefEdit); + } + + pub(in crate::app) fn update_refedit_dialog(&mut self, message: RefEditDialogMsg) -> Task { + let Some(state) = self.refedit_dialog.as_mut() else { + return Task::none(); + }; + match message { + RefEditDialogMsg::Level(level) => { + state.level = level.min(state.levels.len().saturating_sub(1)) + } + RefEditDialogMsg::Nested(on) => state.nested = on, + RefEditDialogMsg::Attributes(on) => state.attributes = on, + RefEditDialogMsg::Lock(on) => state.lock = on, + RefEditDialogMsg::Unique(on) => state.unique = on, + RefEditDialogMsg::Help => { + self.command_line + .push_info(&crate::tr!("ref-edit", "dialog-help")); + } + RefEditDialogMsg::Ok => { + let Some(state) = self.refedit_dialog.take() else { + return Task::none(); + }; + self.close_active_modal(); + let attributes = state.attributes && state.attributes_enabled(); + self.refedit_options = crate::app::settings::RefEditOptions { + unique_names: state.unique, + attributes: state.attributes, + lock: state.lock, + nested: state.nested, + }; + let i = self.active_tab; + if state.nested { + let hits = state + .chain + .iter() + .map(|level| level_hits(&self.tabs[i].scene.document, level)) + .collect(); + let command = RefEditOptionsCommand::nested_pick( + state.top, + state.chain, + hits, + state.level, + attributes, + state.lock, + state.unique, + ); + use crate::command::CadCommand; + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } else { + let last = state.level.min(state.chain.len().saturating_sub(1)); + let path: Vec = state.chain[1..=last] + .iter() + .map(|l| l.insert.value().to_string()) + .collect(); + return self.dispatch_command(&format!( + "REFEDIT_BEGIN:{}:{}:{}::{}:{}", + state.top.value(), + path.join(","), + attributes as u8, + state.lock as u8, + state.unique as u8 + )); + } + } + } + Task::none() + } + /// `top[:path[:attributes[:nested]]]` — open the session. fn begin_refedit(&mut self, i: usize, args: &str) { let mut parts = args.split(':'); @@ -181,6 +284,10 @@ impl OpenCADStudio { let path = handles(parts.next()); let attributes = parts.next() == Some("1"); let nested = handles(parts.next()); + let lock = parts.next().map_or(self.refedit_options.lock, |v| v == "1"); + let unique = parts + .next() + .map_or(self.refedit_options.unique_names, |v| v == "1"); if self.tabs[i].refedit_session.is_some() || self.reject_locked_edit(i, top) { return; } @@ -206,6 +313,15 @@ impl OpenCADStudio { }); } let level = chain.last().expect("non-empty chain").clone(); + // An external reference's dependent names (`XREF|NAME`) of the + // outermost xref in the chain. + let xref_prefix = chain.iter().find_map(|l| { + document + .block_records + .get(&l.block_name) + .filter(|br| br.flags.is_xref || br.flags.is_xref_overlay) + .map(|br| format!("{}|", br.name)) + }); let Some(br) = document.block_records.get(&level.block_name) else { return; }; @@ -240,6 +356,8 @@ impl OpenCADStudio { // Everything allocated from here on is part of the session. handle_watermark: document.next_handle().max(document.header.handle_seed), xref_path, + lock, + names: RefEditNames::default(), }; let copies: Vec = extracted .iter() @@ -247,6 +365,15 @@ impl OpenCADStudio { .collect(); self.push_undo_snapshot(i, "REFEDIT"); + let mut session = session; + let copies = match xref_prefix { + Some(prefix) => { + let (copies, names) = self.refedit_host_names(i, copies, &prefix, unique); + session.names = names; + copies + } + None => copies, + }; let mut temp_handles = Vec::new(); for mut entity in copies { apply_insert_transform(&mut entity, &session); @@ -254,7 +381,6 @@ impl OpenCADStudio { entity.common_mut().owner_handle = Handle::NULL; temp_handles.push(self.tabs[i].scene.add_entity(entity)); } - let mut session = session; session.temp_handles = temp_handles.clone(); self.tabs[i].refedit_session = Some(session); self.tabs[i].refedit_closed.clear(); @@ -262,7 +388,7 @@ impl OpenCADStudio { self.tabs[i] .scene .set_refedit_keep(Some(temp_handles.iter().copied().collect::>())); - self.tabs[i].scene.refedit_lock = true; + self.tabs[i].scene.refedit_lock = lock; self.tabs[i].scene.deselect_all(); self.tabs[i].dirty = true; // No active command — the definition's objects are edited freely @@ -370,6 +496,10 @@ impl OpenCADStudio { .iter() .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) .map(|mut entity| { + // Back to the reference's own dependent names. + rename_symbols(&mut entity, &|kind, name| { + session.names.original(kind, name).map(str::to_string) + }); apply_insert_inverse_transform(&mut entity, &session); let mut entity = normalize_entity_for_block(entity); entity.common_mut().handle = Handle::NULL; @@ -452,6 +582,7 @@ impl OpenCADStudio { } fn finish_refedit_close(&mut self, i: usize, session: RefEditSession) { + self.drop_refedit_names(i, &session.names); self.tabs[i].refedit_closed.push(session); self.tabs[i].scene.refedit_lock = false; self.tabs[i].scene.set_refedit_keep(None); @@ -581,8 +712,8 @@ impl OpenCADStudio { .map(|e| e.common().handle) .filter(|h| session.in_working_set(*h)) .collect(); + self.tabs[i].scene.refedit_lock = session.lock; self.tabs[i].refedit_session = Some(session); - self.tabs[i].scene.refedit_lock = true; self.tabs[i].scene.set_refedit_keep(Some(keep)); } } @@ -600,4 +731,185 @@ impl OpenCADStudio { _ => {} } } + + /// The working set's objects of an external reference under host names: + /// `XREF|NAME` becomes `$N$NAME` (the first free N) with records of its + /// own when `unique`, else `NAME`, sharing a host record of that name when + /// one exists. Missing records are copied from the reference's. + fn refedit_host_names( + &mut self, + i: usize, + mut copies: Vec, + prefix: &str, + unique: bool, + ) -> (Vec, RefEditNames) { + let document = &mut self.tabs[i].scene.document; + let deps = crate::app::ClipboardDeps::capture(document, &copies); + let strip = |name: &str| -> Option { + (name.len() > prefix.len() + && name.is_char_boundary(prefix.len()) + && name[..prefix.len()].eq_ignore_ascii_case(prefix)) + .then(|| name[prefix.len()..].to_string()) + }; + let exists = |document: &codec::CadDocument, kind: SymbolKind, name: &str| match kind { + SymbolKind::Layer => document.layers.contains(name), + SymbolKind::Linetype => document.line_types.contains(name), + SymbolKind::TextStyle => document.text_styles.contains(name), + SymbolKind::Block => document.block_records.get(name).is_some(), + }; + let wanted: Vec<(SymbolKind, String)> = deps + .layers + .iter() + .map(|r| (SymbolKind::Layer, r.name.clone())) + .chain(deps.linetypes.iter().map(|r| (SymbolKind::Linetype, r.name.clone()))) + .chain(deps.text_styles.iter().map(|r| (SymbolKind::TextStyle, r.name.clone()))) + .chain(deps.blocks.iter().map(|b| (SymbolKind::Block, b.name.clone()))) + .collect(); + let mut names = RefEditNames::default(); + for (kind, original) in wanted { + let Some(base) = strip(&original) else { + continue; + }; + let taken = |names: &RefEditNames, candidate: &str| { + exists(document, kind, candidate) + || names + .renamed + .iter() + .any(|(k, used, _)| *k == kind && used.eq_ignore_ascii_case(candidate)) + }; + // Layer 0 is the host's own in every drawing. + let used = if kind == SymbolKind::Layer && base == "0" { + base + } else if unique { + (0u32..) + .map(|n| format!("${n}${base}")) + .find(|candidate| !taken(&names, candidate)) + .expect("a free name") + } else { + base + }; + if !exists(document, kind, &used) + && !names + .created + .iter() + .any(|(k, n)| *k == kind && n.eq_ignore_ascii_case(&used)) + { + names.created.push((kind, used.clone())); + } + names.renamed.push((kind, used, original)); + } + let used_for = |kind: SymbolKind, name: &str| { + names + .renamed + .iter() + .find(|(k, _, original)| *k == kind && original.eq_ignore_ascii_case(name)) + .map(|(_, used, _)| used.clone()) + }; + let created = |kind: SymbolKind, name: &str| -> Option { + used_for(kind, name).filter(|used| { + names + .created + .iter() + .any(|(k, n)| *k == kind && n.eq_ignore_ascii_case(used)) + }) + }; + for record in &deps.layers { + if let Some(used) = created(SymbolKind::Layer, &record.name) { + let mut copy = record.clone(); + copy.name = used; + copy.flags.xref_dependent = false; + copy.xref_block_record_handle = Handle::NULL; + if let Some(linetype) = used_for(SymbolKind::Linetype, ©.line_type) { + copy.line_type = linetype; + } + copy.handle = document.allocate_handle(); + let _ = document.layers.add(copy); + } + } + for record in &deps.linetypes { + if let Some(used) = created(SymbolKind::Linetype, &record.name) { + let mut copy = record.clone(); + copy.name = used; + copy.xref_dependent = false; + copy.xref_block_record_handle = Handle::NULL; + copy.handle = document.allocate_handle(); + let _ = document.line_types.add(copy); + } + } + for record in &deps.text_styles { + if let Some(used) = created(SymbolKind::TextStyle, &record.name) { + let mut copy = record.clone(); + copy.name = used; + copy.xref_dependent = false; + copy.xref_block_record_handle = Handle::NULL; + copy.handle = document.allocate_handle(); + let _ = document.text_styles.add(copy); + } + } + let rename = |kind: SymbolKind, name: &str| used_for(kind, name); + let blocks: Vec = deps + .blocks + .iter() + .filter_map(|def| { + let used = created(SymbolKind::Block, &def.name)?; + let mut entities = def.entities.clone(); + for entity in &mut entities { + rename_symbols(entity, &rename); + } + Some(crate::app::BlockDef { + name: used, + base_point: def.base_point, + entities, + }) + }) + .collect(); + crate::app::command_driver::clipboard::merge_block_defs(document, &blocks); + for entity in &mut copies { + rename_symbols(entity, &rename); + } + (copies, names) + } + + /// Removes the host records made for a session that nothing uses any more. + fn drop_refedit_names(&mut self, i: usize, names: &RefEditNames) { + for (kind, name) in names.created.iter().rev() { + let scene = &mut self.tabs[i].scene; + let in_use = scene.document.entities().any(|entity| { + let common = entity.common(); + match kind { + SymbolKind::Layer => common.layer.eq_ignore_ascii_case(name), + SymbolKind::Linetype => common.linetype.eq_ignore_ascii_case(name), + SymbolKind::TextStyle => match entity { + EntityType::Text(t) => t.style.eq_ignore_ascii_case(name), + EntityType::MText(t) => t.style.eq_ignore_ascii_case(name), + EntityType::AttributeDefinition(a) => { + a.text_style.eq_ignore_ascii_case(name) + } + _ => false, + }, + SymbolKind::Block => matches!( + entity, + EntityType::Insert(insert) if insert.block_name.eq_ignore_ascii_case(name) + ), + } + }); + if in_use { + continue; + } + match kind { + SymbolKind::Layer => { + scene.document.layers.remove(name); + } + SymbolKind::Linetype => { + scene.document.line_types.remove(name); + } + SymbolKind::TextStyle => { + scene.document.text_styles.remove(name); + } + SymbolKind::Block => { + let _ = crate::scene::block_service::erase(scene, name); + } + } + } + } } diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 6fa262f35..9315e04bf 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -104,6 +104,7 @@ pub(super) const NAMES: &[&str] = &[ "external_change_cancel", "pdf_dialog_ok", "view_background", + "refedit_dialog", "pdf_layer_toggle", "pc_manager_toggle", "pc_manager", @@ -1884,6 +1885,36 @@ impl OpenCADStudio { }) } "ribbon_dropdown" => Message::ToggleRibbonDropdown(string(req, "value")?.into()), + // The Edit Reference dialog, one field per call: "level=" (0 = + // the picked reference), "nested=0|1" (pick the working set in the + // drawing), "attributes=0|1", "lock=0|1", "unique=0|1", "ok" or + // "cancel". + "refedit_dialog" => { + use crate::ui::window::refedit_dialog::RefEditDialogMsg as R; + if self.active_modal != Some(crate::app::ModalKind::RefEdit) { + return Err(failure("no_dialog", "The Reference Edit dialog is not open")); + } + let spec = string(req, "value")?; + let (key, value) = spec.split_once('=').unwrap_or((spec, "")); + let flag = || match value { + "1" => Ok(true), + "0" => Ok(false), + _ => Err(failure("invalid_value", spec)), + }; + if key == "cancel" { + Message::CloseModal + } else { + Message::RefEditDialog(match key { + "ok" => R::Ok, + "level" => R::Level(value.parse().map_err(|_| failure("invalid_value", spec))?), + "nested" => R::Nested(flag()?), + "attributes" => R::Attributes(flag()?), + "lock" => R::Lock(flag()?), + "unique" => R::Unique(flag()?), + _ => return Err(failure("invalid_value", "Unknown Reference Edit field")), + }) + } + } // The -VIEW Background dialog, one field per call: "type=gradient", // "color|top|middle|bottom=r,g,b", "three=1", "rotation=30", // "image=", "position=center|stretch|tile", "aspect=1", "offset=x,y" (pixels), "scale=x,y", diff --git a/src/app/mod.rs b/src/app/mod.rs index 5fd2a51ca..c6e1ab758 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -582,6 +582,8 @@ pub(super) struct OpenCADStudio { pc_section: Option, /// -VIEW sEttings Background dialog. view_background: Option, + /// The Edit Reference dialog (REFEDIT). + refedit_dialog: Option, underlay_layers: Option, pdf_import_settings: Option, pdf_import_file: Option, @@ -790,6 +792,8 @@ pub(super) struct OpenCADStudio { block_freq: std::collections::HashMap, /// BLOCKMRULIST: how many recent blocks the palette keeps (0–100). pub(crate) block_mru_list: u8, + /// The Edit Reference dialog's remembered settings. + pub(crate) refedit_options: crate::app::settings::RefEditOptions, /// BLOCKREDEFINEMODE (0–2). pub(crate) block_redefine_mode: u8, /// BLOCKNAVIGATE: the Libraries tab's start folder ("." for none). @@ -2153,6 +2157,7 @@ pub enum ModalKind { PointCloudColorMap, PcSection, ViewBackground, + RefEdit, UnderlayLayers, PdfImportSettings, PdfImportFile, @@ -4348,6 +4353,8 @@ pub enum Message { PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg), AttdefDialog(crate::ui::window::attdef_dialog::AttdefDialogMsg), FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg), + /// An edit in the Edit Reference dialog. + RefEditDialog(crate::ui::window::refedit_dialog::RefEditDialogMsg), /// A click or search in the Point Cloud Manager. PcManager(crate::ui::window::pc_manager::PcManagerMsg), Count(crate::ui::window::count_palette::CountMsg), @@ -4559,6 +4566,7 @@ impl OpenCADStudio { point_cloud_color_map: None, pc_section: None, view_background: None, + refedit_dialog: None, underlay_layers: None, pdf_import_settings: None, pdf_import_file: None, @@ -4658,6 +4666,7 @@ impl OpenCADStudio { block_mru: Vec::new(), block_freq: std::collections::HashMap::new(), block_mru_list: 50, + refedit_options: Default::default(), block_redefine_mode: 1, block_navigate: ".".to_string(), insname: String::new(), diff --git a/src/app/settings.rs b/src/app/settings.rs index 834c1874a..a63453fb1 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -652,6 +652,9 @@ pub struct UserSettings { /// BLOCKNAVIGATE (default "."). #[serde(default = "default_block_navigate")] pub block_navigate: String, + /// The Edit Reference dialog's settings. + #[serde(default)] + pub refedit: RefEditOptions, /// XFADECTL: how far objects outside the REFEDIT working set fade (0–90, default 70). #[serde(default = "default_xfadectl")] pub xfadectl: u8, @@ -687,6 +690,30 @@ pub struct PaletteBlockRef { pub drawing: bool, } +/// The Edit Reference dialog's settings, remembered between sessions: give +/// an external reference's clashing named objects unique names, check out +/// attribute definitions, lock what is outside the working set, pick the +/// working set's objects in the drawing. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct RefEditOptions { + pub unique_names: bool, + pub attributes: bool, + pub lock: bool, + pub nested: bool, +} + +impl Default for RefEditOptions { + fn default() -> Self { + Self { + unique_names: true, + attributes: false, + lock: true, + nested: false, + } + } +} + /// The Blocks palette's insertion options. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct BlockInsertOptions { @@ -938,6 +965,7 @@ impl Default for UserSettings { block_mru_list: 50, block_redefine_mode: 1, block_navigate: ".".to_string(), + refedit: RefEditOptions::default(), xfadectl: 70, count_color: 3, count_error_color: 1, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 4612a082c..c4cd4b69c 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -873,6 +873,7 @@ impl OpenCADStudio { block_mru_list: self.block_mru_list, block_redefine_mode: self.block_redefine_mode, block_navigate: self.block_navigate.clone(), + refedit: self.refedit_options.clone(), xfadectl: crate::scene::cache::block_cache::refedit_fade_ctl() as u8, count_color: self.count_palette.color, count_error_color: self.count_palette.error_color, @@ -1036,6 +1037,7 @@ impl OpenCADStudio { self.block_redefine_mode = s.block_redefine_mode.min(2); self.block_navigate = s.block_navigate.clone(); crate::scene::cache::block_cache::set_refedit_fade_ctl(i32::from(s.xfadectl.min(90))); + self.refedit_options = s.refedit.clone(); self.count_palette.color = s.count_color; self.count_palette.error_color = s.count_error_color; self.count_palette.service = s.count_service; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 184afbe13..8747c9244 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2274,6 +2274,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::RefEditDialog(message) => self.update_refedit_dialog(message), Message::PcManager(message) => self.update_pc_manager(message), Message::Count(message) => self.on_count(message), Message::SheetSet(message) => self.on_sheet_set(message), @@ -9487,6 +9488,9 @@ impl OpenCADStudio { Task::none() }; } + if self.active_modal == Some(super::ModalKind::RefEdit) { + self.refedit_dialog = None; + } // Cancelling the section settings drops the waiting extraction. if self.active_modal == Some(super::ModalKind::PcSection) { crate::modules::insert::pc_extract::drop_job(); diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index bb3cb3ec3..22bd59749 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -40,6 +40,7 @@ impl OpenCADStudio { crate::tr!("view-background", "title"), self.view_background.as_ref().map_or("", |s| s.view.as_str()) ), + Some(K::RefEdit) => crate::tr!("ref-edit", "dialog-title"), Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(), Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(), Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(), @@ -528,6 +529,12 @@ impl OpenCADStudio { crate::ui::window::pdf_dialogs::view_background(state, flow) }) } + super::super::ModalKind::RefEdit => { + let state = self.refedit_dialog.as_ref()?; + sized_flow(ex, 560, 470, |flow| { + crate::ui::window::refedit_dialog::view(state, flow) + }) + } super::super::ModalKind::UnderlayLayers => { let state = self.underlay_layers.as_ref()?; sized_flow(ex, 460, 520, |flow| { diff --git a/src/modules/draw/modify/refedit.rs b/src/modules/draw/modify/refedit.rs index 792628795..efe85307f 100644 --- a/src/modules/draw/modify/refedit.rs +++ b/src/modules/draw/modify/refedit.rs @@ -75,6 +75,79 @@ pub struct RefEditSession { pub handle_watermark: u64, /// The referenced file when the definition is an xref. pub xref_path: Option, + /// Objects outside the working set can't be selected while it is open. + pub lock: bool, + /// The external reference's named objects the working set uses under + /// host names, and the host records made for the session. + pub names: RefEditNames, +} + +/// A kind of named object the working set's objects refer to. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SymbolKind { + Layer, + Linetype, + TextStyle, + Block, +} + +/// How an external reference's dependent names (`XREF|NAME`) are used in +/// the host while its objects are checked out: as `$0$NAME` records of +/// their own (unique names), or under the plain name, sharing a host record +/// of that name when there is one. +#[derive(Debug, Clone, Default)] +pub struct RefEditNames { + /// (kind, name used in the host, the reference's dependent name). + pub renamed: Vec<(SymbolKind, String, String)>, + /// Records added to the host for the session, removed when it closes. + pub created: Vec<(SymbolKind, String)>, +} + +impl RefEditNames { + /// The reference's own name for a host name used in the session. + pub fn original(&self, kind: SymbolKind, name: &str) -> Option<&str> { + self.renamed + .iter() + .find(|(k, used, _)| *k == kind && used.eq_ignore_ascii_case(name)) + .map(|(_, _, original)| original.as_str()) + } +} + +/// Rewrites every named-object reference of an entity through `map` +/// (layer, linetype, text style, block). +pub fn rename_symbols(entity: &mut EntityType, map: &dyn Fn(SymbolKind, &str) -> Option) { + let common = entity.common_mut(); + if let Some(name) = map(SymbolKind::Layer, &common.layer) { + common.layer = name; + common.layer_handle = None; + } + if let Some(name) = map(SymbolKind::Linetype, &common.linetype) { + common.linetype = name; + common.linetype_handle = None; + } + match entity { + EntityType::Insert(insert) => { + if let Some(name) = map(SymbolKind::Block, &insert.block_name) { + insert.block_name = name; + } + } + EntityType::Text(text) => { + if let Some(name) = map(SymbolKind::TextStyle, &text.style) { + text.style = name; + } + } + EntityType::MText(text) => { + if let Some(name) = map(SymbolKind::TextStyle, &text.style) { + text.style = name; + } + } + EntityType::AttributeDefinition(attdef) => { + if let Some(name) = map(SymbolKind::TextStyle, &attdef.text_style) { + attdef.text_style = name; + } + } + _ => {} + } } impl RefEditSession { @@ -315,10 +388,23 @@ pub struct RefEditOptionsCommand { level: usize, step: OptionStep, nested: Vec, + /// The session settings (lock objects outside the working set, unique + /// names for an external reference's named objects). + lock: bool, + unique: bool, + /// Set when the Edit Reference dialog already chose the attribute + /// option: the nested pick then opens the session directly. + attributes: Option, } impl RefEditOptionsCommand { - pub fn new(top: Handle, chain: Vec, hits: Vec) -> Self { + pub fn new( + top: Handle, + chain: Vec, + hits: Vec, + lock: bool, + unique: bool, + ) -> Self { let level = chain.len().saturating_sub(1); Self { top, @@ -327,9 +413,30 @@ impl RefEditOptionsCommand { level, step: OptionStep::Nesting, nested: Vec::new(), + lock, + unique, + attributes: None, } } + /// `Select nested objects:` for the dialog's "pick in the drawing" + /// working set at `level`. + pub fn nested_pick( + top: Handle, + chain: Vec, + hits: Vec, + level: usize, + attributes: bool, + lock: bool, + unique: bool, + ) -> Self { + let mut command = Self::new(top, chain, hits, lock, unique); + command.level = level.min(command.chain.len().saturating_sub(1)); + command.step = OptionStep::Nested; + command.attributes = Some(attributes); + command + } + fn begin(&self, attributes: bool) -> CmdResult { let path: Vec = self.chain[1..=self.level] .iter() @@ -338,11 +445,13 @@ impl RefEditOptionsCommand { let nested: Vec = self.nested.iter().map(|h| h.value().to_string()).collect(); CmdResult::Relaunch( format!( - "REFEDIT_BEGIN:{}:{}:{}:{}", + "REFEDIT_BEGIN:{}:{}:{}:{}:{}:{}", self.top.value(), path.join(","), attributes as u8, - nested.join(",") + nested.join(","), + self.lock as u8, + self.unique as u8 ), vec![], ) @@ -450,6 +559,9 @@ impl CadCommand for RefEditOptionsCommand { if n == 0 { return CmdResult::CancelWithMessage(t!("0 items selected").into_owned()); } + if let Some(attributes) = self.attributes { + return self.begin(attributes); + } self.step = OptionStep::Attributes; CmdResult::ReportMeasurement(if n == 1 { t!("1 item selected").into_owned() diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index c6c870366..91c96cf01 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -29,6 +29,7 @@ pub mod plot; pub mod plugin_manager; pub mod print_all; pub mod recovery; +pub mod refedit_dialog; pub mod sheet_set; pub mod shortcuts; pub mod update_notice; diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index cc11a8f44..786dc31ab 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -302,7 +302,7 @@ pub(crate) fn well_style(theme: &Theme) -> container::Style { } /// A titled card. -fn card<'a>(title: String, content: impl Into>) -> Element<'a, Message> { +pub(crate) fn card<'a>(title: String, content: impl Into>) -> Element<'a, Message> { container( column![ text(title.to_uppercase()).size(10).style(accent_text), diff --git a/src/ui/window/refedit_dialog.rs b/src/ui/window/refedit_dialog.rs new file mode 100644 index 000000000..e4fdb2fd3 --- /dev/null +++ b/src/ui/window/refedit_dialog.rs @@ -0,0 +1,247 @@ +//! Edit Reference dialog (REFEDIT): the nesting level to check out, shown as +//! a chain of chips beside a preview of the chosen definition, how the +//! working set is filled, whether attribute definitions come along, and the +//! session settings (lock what is outside the working set, give clashing +//! named objects of an external reference unique names). + +use iced::widget::{button, column, container, row, text, toggler, Space}; +use iced::{Border, Element, Fill, Length, Theme}; + +use crate::app::Message; +use crate::scene::model::wire_model::WireModel; +use crate::ui::style::common::muted_style; +use crate::ui::style::form::{button_style, dialog_button}; +use crate::ui::window::pdf_dialogs::{card, well_style}; + +/// One edit in the Edit Reference dialog. +#[derive(Debug, Clone)] +pub enum RefEditDialogMsg { + /// The nesting level picked in the chain (0 = the selected reference). + Level(usize), + /// Pick the working set's objects in the drawing (else all of them). + Nested(bool), + Attributes(bool), + Lock(bool), + Unique(bool), + Help, + Ok, +} + +fn msg(m: RefEditDialogMsg) -> Message { + Message::RefEditDialog(m) +} + +/// One level of the nesting chain as the dialog shows it. +pub struct RefEditLevel { + /// The reference's handle: the picked object for level 0, the + /// definition-owned reference below it. + pub insert: codec::Handle, + pub name: String, + /// The definition's preview. + pub wires: Vec, + /// The definition holds attribute definitions (the attribute switch is + /// only offered then). + pub has_attdefs: bool, + /// The referenced file of an external reference. + pub xref_path: Option, +} + +pub struct RefEditDialogState { + pub top: codec::Handle, + /// The picked chain, outermost first. + pub chain: Vec, + pub levels: Vec, + pub level: usize, + pub nested: bool, + pub attributes: bool, + pub lock: bool, + pub unique: bool, +} + +impl RefEditDialogState { + /// The attribute switch counts only where the chosen definition has + /// attribute definitions. + pub fn attributes_enabled(&self) -> bool { + self.levels.get(self.level).is_some_and(|l| l.has_attdefs) + } +} + +fn label<'a>(value: String, enabled: bool) -> Element<'a, Message> { + text(value) + .size(12) + .style(move |theme: &Theme| { + if enabled { + text::Style::default() + } else { + text::Style { + color: Some(theme.palette().background.strong.color), + } + } + }) + .width(Fill) + .into() +} + +/// A labelled switch on one row; inactive while `on_toggle` is None. +fn switch_row<'a>( + name: String, + value: bool, + on_toggle: Option RefEditDialogMsg>, +) -> Element<'a, Message> { + let enabled = on_toggle.is_some(); + let mut switch = toggler(value).size(18.0); + if let Some(on) = on_toggle { + switch = switch.on_toggle(move |v| msg(on(v))); + } + row![label(name, enabled), switch] + .spacing(8) + .align_y(iced::Center) + .into() +} + +/// A level of the chain as a pill; the chosen one framed in the accent colour. +fn level_chip<'a>(name: &str, index: usize, chosen: bool) -> Element<'a, Message> { + button(text(name.to_string()).size(12)) + .on_press(msg(RefEditDialogMsg::Level(index))) + .padding([5, 12]) + .style(move |theme: &Theme, status| { + let mut style = button_style(false)(theme, status); + style.border = Border { + width: if chosen { 1.5 } else { 1.0 }, + radius: 14.0.into(), + color: if chosen { + theme.palette().primary.base.color + } else { + theme.palette().background.strong.color + }, + }; + if chosen { + style.text_color = theme.palette().primary.base.color; + } + style + }) + .into() +} + +pub fn view<'a>( + state: &'a RefEditDialogState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let current = state.levels.get(state.level); + let preview: Element<'a, Message> = container(match current { + Some(level) => crate::ui::window::block_palette::preview_canvas(&level.wires, 104.0), + None => Space::new().into(), + }) + .width(Length::Fixed(150.0)) + .height(Length::Fixed(104.0)) + .padding(4) + .style(well_style) + .into(); + let mut chips = row![].spacing(6).align_y(iced::Center); + for (index, level) in state.levels.iter().enumerate() { + if index > 0 { + chips = chips.push(text("›").size(13).style(muted_style)); + } + chips = chips.push(level_chip(&level.name, index, index == state.level)); + } + let mut about = column![text(crate::tr!("ref-edit", "dialog-chain-hint")) + .size(11) + .style(muted_style)] + .spacing(4); + if let Some(path) = current.and_then(|l| l.xref_path.as_deref()) { + // A long path keeps its end, where the file name is. + const MAX: usize = 64; + let count = path.chars().count(); + let shown = if count > MAX { + format!("…{}", path.chars().skip(count - (MAX - 1)).collect::()) + } else { + path.to_string() + }; + about = about.push( + text(format!("{} {}", crate::tr!("ref-edit", "dialog-path"), shown)) + .size(11) + .style(muted_style), + ); + } + let reference = card( + crate::tr!("ref-edit", "dialog-reference"), + row![ + preview, + column![iced::widget::scrollable(chips).direction( + iced::widget::scrollable::Direction::Horizontal( + iced::widget::scrollable::Scrollbar::new().width(4).scroller_width(4) + ) + ), about] + .spacing(8) + .width(Fill) + ] + .spacing(12), + ); + + let segment = |nested: bool, name: String| { + button(text(name).size(12).width(Fill).align_x(iced::Center)) + .on_press(msg(RefEditDialogMsg::Nested(nested))) + .style(button_style(state.nested == nested)) + .padding([6, 8]) + .width(Fill) + }; + let method = container( + row![ + segment(false, crate::tr!("ref-edit", "dialog-all-objects")), + segment(true, crate::tr!("ref-edit", "dialog-pick-objects")), + ] + .spacing(2), + ) + .padding(2) + .width(Fill) + .style(well_style); + let attributes_enabled = state.attributes_enabled(); + let working_set = card( + crate::tr!("ref-edit", "dialog-working-set"), + column![ + method, + switch_row( + crate::tr!("ref-edit", "dialog-attributes"), + state.attributes && attributes_enabled, + attributes_enabled.then_some(RefEditDialogMsg::Attributes as fn(bool) -> _), + ), + ] + .spacing(10), + ); + let session = card( + crate::tr!("ref-edit", "dialog-session"), + column![ + switch_row( + crate::tr!("ref-edit", "dialog-lock"), + state.lock, + Some(RefEditDialogMsg::Lock), + ), + switch_row( + crate::tr!("ref-edit", "dialog-unique"), + state.unique, + Some(RefEditDialogMsg::Unique), + ), + ] + .spacing(10), + ); + let footer = row![ + button(text("?").size(12)) + .on_press(msg(RefEditDialogMsg::Help)) + .style(button_style(false)) + .padding([5, 11]), + Space::new().width(Fill), + dialog_button(crate::t!("Cancel"), Message::CloseModal, false), + dialog_button( + std::borrow::Cow::Owned(crate::tr!("ref-edit", "dialog-start")), + msg(RefEditDialogMsg::Ok), + true + ), + ] + .spacing(6) + .align_y(iced::Center); + column![reference, working_set, session, footer] + .spacing(10) + .padding([10, 12]) + .width(sizing.width) + .into() +}