forked from 1b-agent/opencadstudio-upstream
feat(xref): row-level context menu actions and unified split-button toolbar
Right-click actions bound to the row under the cursor, split-button toolbar groups with shared borders and hover states, and a notice when an operation needs a selection first.
This commit is contained in:
1 parent
494a872e8d
commit
22a9bd602c
28 files changed
+352
-168
No files matched your search
@@ -3317,6 +3317,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE حدد عناصر التدوير أو [الوضع] (Enter للإنهاء):
|
.revolve-select-objects-or-mode = REVOLVE حدد عناصر التدوير أو [الوضع] (Enter للإنهاء):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = إنشاء
|
.create = إنشاء
|
||||||
.boolean = عمليات منطقية
|
.boolean = عمليات منطقية
|
||||||
|
|||||||
@@ -3304,6 +3304,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Изберете обекти за завъртане или [Режим] (Enter за край):
|
.revolve-select-objects-or-mode = REVOLVE Изберете обекти за завъртане или [Режим] (Enter за край):
|
||||||
|
|
||||||
.path = Път:
|
.path = Път:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Създаване
|
.create = Създаване
|
||||||
.boolean = Булеви операции
|
.boolean = Булеви операции
|
||||||
|
|||||||
@@ -3302,6 +3302,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Vyberte objekty k rotaci nebo [Režim] (Enter ukončí):
|
.revolve-select-objects-or-mode = REVOLVE Vyberte objekty k rotaci nebo [Režim] (Enter ukončí):
|
||||||
|
|
||||||
.path = Cesta:
|
.path = Cesta:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Vytvořit
|
.create = Vytvořit
|
||||||
.boolean = Booleovské operace
|
.boolean = Booleovské operace
|
||||||
|
|||||||
@@ -3303,6 +3303,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Objekte zum Rotieren wählen oder [Modus] (Enter zum Beenden):
|
.revolve-select-objects-or-mode = REVOLVE Objekte zum Rotieren wählen oder [Modus] (Enter zum Beenden):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Erstellen
|
.create = Erstellen
|
||||||
.boolean = Boolesche Operationen
|
.boolean = Boolesche Operationen
|
||||||
|
|||||||
@@ -3176,6 +3176,7 @@ insert =
|
|||||||
.not-found = Not found
|
.not-found = Not found
|
||||||
.orphaned = Orphaned
|
.orphaned = Orphaned
|
||||||
.unreferenced = Unreferenced
|
.unreferenced = Unreferenced
|
||||||
|
.xref-file-references = File References
|
||||||
.xref-change-path = Change Path
|
.xref-change-path = Change Path
|
||||||
.xref-make-absolute = Make Absolute
|
.xref-make-absolute = Make Absolute
|
||||||
.xref-make-relative = Make Relative
|
.xref-make-relative = Make Relative
|
||||||
|
|||||||
@@ -3177,6 +3177,7 @@ insert =
|
|||||||
.not-found = Not found
|
.not-found = Not found
|
||||||
.orphaned = Orphaned
|
.orphaned = Orphaned
|
||||||
.unreferenced = Unreferenced
|
.unreferenced = Unreferenced
|
||||||
|
.xref-file-references = File References
|
||||||
.xref-change-path = Change Path
|
.xref-change-path = Change Path
|
||||||
.xref-make-absolute = Make Absolute
|
.xref-make-absolute = Make Absolute
|
||||||
.xref-make-relative = Make Relative
|
.xref-make-relative = Make Relative
|
||||||
|
|||||||
@@ -3305,6 +3305,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Seleccione objetos para revolucionar o [Modo] (Intro para terminar):
|
.revolve-select-objects-or-mode = REVOLVE Seleccione objetos para revolucionar o [Modo] (Intro para terminar):
|
||||||
|
|
||||||
.path = Ruta:
|
.path = Ruta:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Crear
|
.create = Crear
|
||||||
.boolean = Booleanas
|
.boolean = Booleanas
|
||||||
|
|||||||
@@ -3304,6 +3304,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Valitse pyöräytettävät objektit tai [Tila] (Enter lopettaa):
|
.revolve-select-objects-or-mode = REVOLVE Valitse pyöräytettävät objektit tai [Tila] (Enter lopettaa):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Luo
|
.create = Luo
|
||||||
.boolean = Boolen operaatiot
|
.boolean = Boolen operaatiot
|
||||||
|
|||||||
@@ -3305,6 +3305,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Sélectionnez les objets de révolution ou [Mode] (Entrée pour terminer) :
|
.revolve-select-objects-or-mode = REVOLVE Sélectionnez les objets de révolution ou [Mode] (Entrée pour terminer) :
|
||||||
|
|
||||||
.path = Chemin :
|
.path = Chemin :
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Créer
|
.create = Créer
|
||||||
.boolean = Opérations booléennes
|
.boolean = Opérations booléennes
|
||||||
|
|||||||
@@ -3293,6 +3293,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE घुमाने के लिए ऑब्जेक्ट चुनें या [मोड] (समाप्त करने के लिए Enter):
|
.revolve-select-objects-or-mode = REVOLVE घुमाने के लिए ऑब्जेक्ट चुनें या [मोड] (समाप्त करने के लिए Enter):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = बनाएँ
|
.create = बनाएँ
|
||||||
.boolean = बूलियन संक्रियाएँ
|
.boolean = बूलियन संक्रियाएँ
|
||||||
|
|||||||
@@ -3304,6 +3304,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Válasszon megforgatandó objektumokat vagy [Mód] (Enter a befejezéshez):
|
.revolve-select-objects-or-mode = REVOLVE Válasszon megforgatandó objektumokat vagy [Mód] (Enter a befejezéshez):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Létrehozás
|
.create = Létrehozás
|
||||||
.boolean = Logikai műveletek
|
.boolean = Logikai műveletek
|
||||||
|
|||||||
@@ -3304,6 +3304,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Seleziona oggetti da rivoluzionare o [Modalità] (Invio per terminare):
|
.revolve-select-objects-or-mode = REVOLVE Seleziona oggetti da rivoluzionare o [Modalità] (Invio per terminare):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Crea
|
.create = Crea
|
||||||
.boolean = Operazioni booleane
|
.boolean = Operazioni booleane
|
||||||
|
|||||||
@@ -3293,6 +3293,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE 回転させるオブジェクトを選択、または[モード](Enterで終了):
|
.revolve-select-objects-or-mode = REVOLVE 回転させるオブジェクトを選択、または[モード](Enterで終了):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = 作成
|
.create = 作成
|
||||||
.boolean = ブール演算
|
.boolean = ブール演算
|
||||||
|
|||||||
@@ -3304,6 +3304,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE 회전할 객체 선택 또는 [모드] (Enter로 완료):
|
.revolve-select-objects-or-mode = REVOLVE 회전할 객체 선택 또는 [모드] (Enter로 완료):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = 생성
|
.create = 생성
|
||||||
.boolean = 불리언 연산
|
.boolean = 불리언 연산
|
||||||
|
|||||||
@@ -3303,6 +3303,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Selecteer objecten om te wentelen of [Modus] (Enter om te voltooien):
|
.revolve-select-objects-or-mode = REVOLVE Selecteer objecten om te wentelen of [Modus] (Enter om te voltooien):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Maken
|
.create = Maken
|
||||||
.boolean = Booleaanse bewerkingen
|
.boolean = Booleaanse bewerkingen
|
||||||
|
|||||||
@@ -3304,6 +3304,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Wybierz obiekty do obrotu lub [Tryb] (Enter kończy):
|
.revolve-select-objects-or-mode = REVOLVE Wybierz obiekty do obrotu lub [Tryb] (Enter kończy):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Utwórz
|
.create = Utwórz
|
||||||
.boolean = Operacje boolowskie
|
.boolean = Operacje boolowskie
|
||||||
|
|||||||
@@ -3304,6 +3304,7 @@ insert =
|
|||||||
.revolve-specify-start-angle = REVOLVE { common.specify }: { common.start-angle }
|
.revolve-specify-start-angle = REVOLVE { common.specify }: { common.start-angle }
|
||||||
.revolve-select-line-ray-or-construction-line-for-axis = REVOLVE Selecione uma linha, semirreta ou linha de construção como eixo:
|
.revolve-select-line-ray-or-construction-line-for-axis = REVOLVE Selecione uma linha, semirreta ou linha de construção como eixo:
|
||||||
.revolve-select-objects-or-mode = REVOLVE Selecione objetos para revolução ou [Modo] (Enter para concluir):
|
.revolve-select-objects-or-mode = REVOLVE Selecione objetos para revolução ou [Modo] (Enter para concluir):
|
||||||
|
.xref-file-references = File References
|
||||||
|
|
||||||
model =
|
model =
|
||||||
.create = Criar
|
.create = Criar
|
||||||
|
|||||||
@@ -3306,6 +3306,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE Выберите объекты для вращения или [Режим] (Enter — завершить):
|
.revolve-select-objects-or-mode = REVOLVE Выберите объекты для вращения или [Режим] (Enter — завершить):
|
||||||
|
|
||||||
.path = Путь:
|
.path = Путь:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Создание
|
.create = Создание
|
||||||
.boolean = Булевы операции
|
.boolean = Булевы операции
|
||||||
|
|||||||
@@ -3265,6 +3265,7 @@ insert =
|
|||||||
.specify-the-requested-path-or-point-or-use-undo = { common.specify }: { common.path } / { common.point } [{ common.undo }].
|
.specify-the-requested-path-or-point-or-use-undo = { common.specify }: { common.path } / { common.point } [{ common.undo }].
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = Oluştur
|
.create = Oluştur
|
||||||
.boolean = Boole işlemleri
|
.boolean = Boole işlemleri
|
||||||
|
|||||||
@@ -3288,6 +3288,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE 选择要旋转的对象或 [模式](按 Enter 完成):
|
.revolve-select-objects-or-mode = REVOLVE 选择要旋转的对象或 [模式](按 Enter 完成):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = 创建
|
.create = 创建
|
||||||
.boolean = 布尔运算
|
.boolean = 布尔运算
|
||||||
|
|||||||
@@ -3300,6 +3300,7 @@ insert =
|
|||||||
.revolve-select-objects-or-mode = REVOLVE 選取要旋轉的物件或 [模式](按 Enter 完成):
|
.revolve-select-objects-or-mode = REVOLVE 選取要旋轉的物件或 [模式](按 Enter 完成):
|
||||||
|
|
||||||
.path = Path:
|
.path = Path:
|
||||||
|
.xref-file-references = File References
|
||||||
model =
|
model =
|
||||||
.create = 建立
|
.create = 建立
|
||||||
.boolean = 布林運算
|
.boolean = 布林運算
|
||||||
|
|||||||
+12
-4
@@ -712,6 +712,8 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// with the last pointer position. Mirrors the Layers Name-column drag.
|
/// with the last pointer position. Mirrors the Layers Name-column drag.
|
||||||
pub(crate) xref_col_drag: Option<usize>,
|
pub(crate) xref_col_drag: Option<usize>,
|
||||||
pub(crate) xref_col_last: Option<iced::Point>,
|
pub(crate) xref_col_last: Option<iced::Point>,
|
||||||
|
/// Table/lower-pane split divider drag in progress.
|
||||||
|
pub(crate) xref_split_drag: bool,
|
||||||
/// Docked Insert Block panel state (search, preview size, cached thumbnails).
|
/// Docked Insert Block panel state (search, preview size, cached thumbnails).
|
||||||
pub(crate) block_palette: crate::ui::window::block_palette::BlockPalette,
|
pub(crate) block_palette: crate::ui::window::block_palette::BlockPalette,
|
||||||
/// Reference Manager palette state (display-only in Task 7).
|
/// Reference Manager palette state (display-only in Task 7).
|
||||||
@@ -2369,6 +2371,12 @@ pub enum Message {
|
|||||||
XrefColMove(iced::Point),
|
XrefColMove(iced::Point),
|
||||||
/// Pointer released during a column drag.
|
/// Pointer released during a column drag.
|
||||||
XrefColRelease,
|
XrefColRelease,
|
||||||
|
/// Start a table/lower-pane split divider drag.
|
||||||
|
XrefSplitGrab,
|
||||||
|
/// Pointer moved (panel space) during a split drag.
|
||||||
|
XrefSplitMove(iced::Point),
|
||||||
|
/// Pointer released during a split drag.
|
||||||
|
XrefSplitRelease,
|
||||||
/// Flip the palette's details/preview lower pane.
|
/// Flip the palette's details/preview lower pane.
|
||||||
XrefManagerTogglePreview,
|
XrefManagerTogglePreview,
|
||||||
/// Toggle the Attach dropdown menu.
|
/// Toggle the Attach dropdown menu.
|
||||||
@@ -2389,10 +2397,9 @@ pub enum Message {
|
|||||||
XrefManagerToggleExpand(u64),
|
XrefManagerToggleExpand(u64),
|
||||||
/// Selection-scoped palette operation (detach/unload/reload/overlay/pathtype).
|
/// Selection-scoped palette operation (detach/unload/reload/overlay/pathtype).
|
||||||
XrefManagerOp(crate::ui::window::xref_manager::XrefPaletteOp),
|
XrefManagerOp(crate::ui::window::xref_manager::XrefPaletteOp),
|
||||||
/// Details-pane "new path" draft text.
|
/// Row-scoped palette operation: selects row `index`, then applies the
|
||||||
XrefManagerPathInput(String),
|
/// operation to it.
|
||||||
/// Apply the "new path" draft to the anchor entry.
|
XrefRowOp(usize, crate::ui::window::xref_manager::XrefPaletteOp),
|
||||||
XrefManagerPathApply,
|
|
||||||
/// Prefill the command line for Find & Replace across references
|
/// Prefill the command line for Find & Replace across references
|
||||||
/// (`XREF Path Find <old> <new>`); the CLI parses and runs it.
|
/// (`XREF Path Find <old> <new>`); the CLI parses and runs it.
|
||||||
XrefFindReplacePrompt,
|
XrefFindReplacePrompt,
|
||||||
@@ -3772,6 +3779,7 @@ impl OpenCADStudio {
|
|||||||
dock_drag_target: None,
|
dock_drag_target: None,
|
||||||
xref_col_drag: None,
|
xref_col_drag: None,
|
||||||
xref_col_last: None,
|
xref_col_last: None,
|
||||||
|
xref_split_drag: false,
|
||||||
show_file_tabs: true,
|
show_file_tabs: true,
|
||||||
show_layout_tabs: true,
|
show_layout_tabs: true,
|
||||||
last_point: None,
|
last_point: None,
|
||||||
|
|||||||
@@ -686,6 +686,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
|||||||
self.dock_drag_target = None;
|
self.dock_drag_target = None;
|
||||||
self.xref_col_drag = None;
|
self.xref_col_drag = None;
|
||||||
self.xref_col_last = None;
|
self.xref_col_last = None;
|
||||||
|
self.xref_split_drag = false;
|
||||||
iced::Task::none()
|
iced::Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -908,6 +909,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
if picked.is_empty() {
|
if picked.is_empty() {
|
||||||
|
self.command_line
|
||||||
|
.push_info(crate::t!("Select a reference first.").as_ref());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let needs_host = matches!(
|
let needs_host = matches!(
|
||||||
@@ -1241,43 +1244,6 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
|||||||
self.post_ref_op(i);
|
self.post_ref_op(i);
|
||||||
self.refresh_xref_manager();
|
self.refresh_xref_manager();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the details-pane path draft to the anchor entry (verbatim
|
|
||||||
/// store, same engine fn as `XREF Path`).
|
|
||||||
pub(crate) fn xref_manager_path_apply(&mut self) {
|
|
||||||
if cfg!(target_arch = "wasm32") {
|
|
||||||
self.command_line.push_error(crate::t!("Reference changes are not available on web — the reference list is read-only.").as_ref());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let i = self.active_tab;
|
|
||||||
let anchor = self.xref_manager.anchor.and_then(|a| {
|
|
||||||
self.xref_manager
|
|
||||||
.entries
|
|
||||||
.get(a)
|
|
||||||
.filter(|_| !self.xref_manager.nested.contains(&a))
|
|
||||||
.map(|e| (e.key, e.name.clone()))
|
|
||||||
});
|
|
||||||
let Some((key, _)) = anchor else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let new_raw = self.xref_manager.path_input.clone();
|
|
||||||
if new_raw.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.push_undo_snapshot(i, "XREF-PATH");
|
|
||||||
match crate::io::xref::set_ref_path(&mut self.tabs[i].scene.document, key, &new_raw)
|
|
||||||
{
|
|
||||||
Ok(name) => {
|
|
||||||
self.command_line.push_output(crate::tf!(
|
|
||||||
"XREF: Path set for \"{}\" — Reload to apply.",
|
|
||||||
name
|
|
||||||
).as_ref());
|
|
||||||
self.post_ref_op(i);
|
|
||||||
}
|
|
||||||
Err(msg) => self.command_line.push_error(msg.as_str()),
|
|
||||||
}
|
|
||||||
self.refresh_xref_manager();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
+23
-6
@@ -2192,6 +2192,25 @@ impl OpenCADStudio {
|
|||||||
self.xref_col_last = None;
|
self.xref_col_last = None;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::XrefSplitGrab => {
|
||||||
|
self.xref_split_drag = true;
|
||||||
|
self.xref_col_last = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::XrefSplitMove(p) => {
|
||||||
|
if self.xref_split_drag {
|
||||||
|
if let Some(last) = self.xref_col_last {
|
||||||
|
self.xref_manager.drag_table_by(p.y - last.y);
|
||||||
|
}
|
||||||
|
self.xref_col_last = Some(p);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::XrefSplitRelease => {
|
||||||
|
self.xref_split_drag = false;
|
||||||
|
self.xref_col_last = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::XrefManagerTogglePreview => {
|
Message::XrefManagerTogglePreview => {
|
||||||
self.xref_manager.show_preview ^= true;
|
self.xref_manager.show_preview ^= true;
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -2284,12 +2303,10 @@ impl OpenCADStudio {
|
|||||||
self.xref_manager_op(op);
|
self.xref_manager_op(op);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::XrefManagerPathInput(text) => {
|
Message::XrefRowOp(index, op) => {
|
||||||
self.xref_manager.path_input = text;
|
// Row-scoped op: select the row first, then run the op.
|
||||||
Task::none()
|
self.xref_manager.right_click_select(index);
|
||||||
}
|
self.xref_manager_op(op);
|
||||||
Message::XrefManagerPathApply => {
|
|
||||||
self.xref_manager_path_apply();
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::XrefFindReplacePrompt => {
|
Message::XrefFindReplacePrompt => {
|
||||||
|
|||||||
+2
-1
@@ -1875,7 +1875,8 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
let workspace: Element<'_, Message> = row(parts).width(Fill).height(Fill).into();
|
let workspace: Element<'_, Message> = row(parts).width(Fill).height(Fill).into();
|
||||||
|
|
||||||
let any_dragging = self.dock_dragging.is_some();
|
let any_dragging = self.dock_dragging.is_some();
|
||||||
let any_resizing = self.dock_resizing.is_some() || self.xref_col_drag.is_some();
|
let any_resizing =
|
||||||
|
self.dock_resizing.is_some() || self.xref_col_drag.is_some() || self.xref_split_drag;
|
||||||
let workspace = if any_dragging {
|
let workspace = if any_dragging {
|
||||||
let id = self.dock_dragging.expect("guarded by any_dragging");
|
let id = self.dock_dragging.expect("guarded by any_dragging");
|
||||||
let side = self.dock_drag_target.map(|(s, _)| s).unwrap_or(
|
let side = self.dock_drag_target.map(|(s, _)| s).unwrap_or(
|
||||||
|
|||||||
@@ -978,6 +978,18 @@ pub fn collect_entries_with_prev(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve `raw` into `found_at` without statting: unloaded rows keep no
|
||||||
|
/// size/date (spec) but still know where their file lives, so previews
|
||||||
|
/// and path operations work without loading.
|
||||||
|
fn resolve_only(entry: &mut ReferenceEntry, raw: &str, base_dir: &Path) {
|
||||||
|
if raw.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(found) = resolve_path(raw, base_dir) {
|
||||||
|
entry.found_at = Some(found.to_string_lossy().into_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn file_name_only(raw: &str) -> &str {
|
fn file_name_only(raw: &str) -> &str {
|
||||||
raw.rsplit(['/', '\\']).next().unwrap_or(raw)
|
raw.rsplit(['/', '\\']).next().unwrap_or(raw)
|
||||||
}
|
}
|
||||||
@@ -999,6 +1011,7 @@ pub fn collect_entries_with_prev(
|
|||||||
entry.saved_path = br.xref_path.clone();
|
entry.saved_path = br.xref_path.clone();
|
||||||
if unloaded.contains(&UnloadKey::Direct(key)) {
|
if unloaded.contains(&UnloadKey::Direct(key)) {
|
||||||
entry.status = RefStatus::Unloaded;
|
entry.status = RefStatus::Unloaded;
|
||||||
|
resolve_only(&mut entry, &br.xref_path, base_dir);
|
||||||
} else {
|
} else {
|
||||||
stat_into(&mut entry, &br.xref_path, base_dir);
|
stat_into(&mut entry, &br.xref_path, base_dir);
|
||||||
entry.status = decide_status(
|
entry.status = decide_status(
|
||||||
@@ -1033,6 +1046,7 @@ pub fn collect_entries_with_prev(
|
|||||||
entry.saved_path = def.file_name.clone();
|
entry.saved_path = def.file_name.clone();
|
||||||
if unloaded.contains(&UnloadKey::Direct(key)) {
|
if unloaded.contains(&UnloadKey::Direct(key)) {
|
||||||
entry.status = RefStatus::Unloaded;
|
entry.status = RefStatus::Unloaded;
|
||||||
|
resolve_only(&mut entry, &def.file_name, base_dir);
|
||||||
} else {
|
} else {
|
||||||
stat_into(&mut entry, &def.file_name, base_dir);
|
stat_into(&mut entry, &def.file_name, base_dir);
|
||||||
entry.status = decide_status(
|
entry.status = decide_status(
|
||||||
@@ -1077,6 +1091,7 @@ pub fn collect_entries_with_prev(
|
|||||||
entry.saved_path = def.file_path.clone();
|
entry.saved_path = def.file_path.clone();
|
||||||
if unloaded.contains(&UnloadKey::Direct(key)) {
|
if unloaded.contains(&UnloadKey::Direct(key)) {
|
||||||
entry.status = RefStatus::Unloaded;
|
entry.status = RefStatus::Unloaded;
|
||||||
|
resolve_only(&mut entry, &def.file_path, base_dir);
|
||||||
} else {
|
} else {
|
||||||
stat_into(&mut entry, &def.file_path, base_dir);
|
stat_into(&mut entry, &def.file_path, base_dir);
|
||||||
entry.status = decide_status(
|
entry.status = decide_status(
|
||||||
|
|||||||
@@ -3157,6 +3157,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"XREF: bound \"{}\" with {} unremapped style handles (see bind limitations)." => Some(("insert", "xref-bound-with-unremapped-handles")),
|
"XREF: bound \"{}\" with {} unremapped style handles (see bind limitations)." => Some(("insert", "xref-bound-with-unremapped-handles")),
|
||||||
"XREF: cannot bind nested reference '{}'. Bind it in its host drawing." => Some(("insert", "xref-cannot-bind-nested-reference")),
|
"XREF: cannot bind nested reference '{}'. Bind it in its host drawing." => Some(("insert", "xref-cannot-bind-nested-reference")),
|
||||||
"Unreferenced" => Some(("insert", "unreferenced")),
|
"Unreferenced" => Some(("insert", "unreferenced")),
|
||||||
|
"File References" => Some(("insert", "xref-file-references")),
|
||||||
"Change Path" => Some(("insert", "xref-change-path")),
|
"Change Path" => Some(("insert", "xref-change-path")),
|
||||||
"Make Absolute" => Some(("insert", "xref-make-absolute")),
|
"Make Absolute" => Some(("insert", "xref-make-absolute")),
|
||||||
"Make Relative" => Some(("insert", "xref-make-relative")),
|
"Make Relative" => Some(("insert", "xref-make-relative")),
|
||||||
|
|||||||
+275
-120
@@ -10,7 +10,7 @@ use crate::io::xref::collect_entries_with_prev;
|
|||||||
use crate::io::xref_model::{normalize_lexical, Pathtype, RefKind, RefStatus, RefType, ReferenceEntry};
|
use crate::io::xref_model::{normalize_lexical, Pathtype, RefKind, RefStatus, RefType, ReferenceEntry};
|
||||||
use crate::ui::ROW_H;
|
use crate::ui::ROW_H;
|
||||||
use acadrust::CadDocument;
|
use acadrust::CadDocument;
|
||||||
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, text_input, tooltip};
|
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip};
|
||||||
use iced::Padding;
|
use iced::Padding;
|
||||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@@ -25,13 +25,46 @@ const IS_WASM: bool = cfg!(target_arch = "wasm32");
|
|||||||
/// Font size for table cells (mirrors `layers.rs`).
|
/// Font size for table cells (mirrors `layers.rs`).
|
||||||
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px at ROW_H=26
|
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px at ROW_H=26
|
||||||
/// Fixed table height so the details pane below keeps stable space.
|
/// Fixed table height so the details pane below keeps stable space.
|
||||||
const TABLE_H: f32 = 240.0;
|
const TABLE_H: f32 = 480.0;
|
||||||
|
/// Minimum table area height under split dragging.
|
||||||
|
const TABLE_MIN_H: f32 = 120.0;
|
||||||
/// Minimum table content width so columns stay readable in narrow docks.
|
/// Minimum table content width so columns stay readable in narrow docks.
|
||||||
const TABLE_MIN_W: f32 = 480.0;
|
const TABLE_MIN_W: f32 = 480.0;
|
||||||
/// Inter-column gutter shared by header, rows, and grab zones so columns align.
|
/// Inter-column gutter shared by header, rows, and grab zones so columns align.
|
||||||
const COL_GUTTER: f32 = 6.0;
|
const COL_GUTTER: f32 = 6.0;
|
||||||
|
/// Tree-mode text size: a step above the table cells, like the
|
||||||
|
/// industry tree.
|
||||||
/// Indent per tree depth level.
|
/// Indent per tree depth level.
|
||||||
const INDENT_W: f32 = 16.0;
|
const INDENT_W: f32 = 16.0;
|
||||||
|
|
||||||
|
/// Dashed vertical tree guide (one per ancestor level), stretched to the row.
|
||||||
|
const TREE_GUIDE_SVG: &[u8] = b"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 100\" preserveAspectRatio=\"none\"><line x1=\"8\" y1=\"0\" x2=\"8\" y2=\"100\" stroke=\"#808080\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"/></svg>";
|
||||||
|
|
||||||
|
/// Shared parsed handle for [`TREE_GUIDE_SVG`] (parsed once, not per row).
|
||||||
|
fn tree_guide_handle() -> iced::widget::svg::Handle {
|
||||||
|
static HANDLE: std::sync::OnceLock<iced::widget::svg::Handle> = std::sync::OnceLock::new();
|
||||||
|
HANDLE
|
||||||
|
.get_or_init(|| iced::widget::svg::Handle::from_memory(TREE_GUIDE_SVG))
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indent cells for `depth` ancestor levels: dashed guides explaining the
|
||||||
|
/// hierarchy, same total width as the old blank indent.
|
||||||
|
fn tree_indent(depth: u32) -> iced::widget::Row<'static, crate::app::Message> {
|
||||||
|
let mut row = iced::widget::Row::new().spacing(0);
|
||||||
|
for _ in 0..depth {
|
||||||
|
row = row.push(
|
||||||
|
container(
|
||||||
|
iced::widget::svg(tree_guide_handle())
|
||||||
|
.width(Length::Fixed(INDENT_W))
|
||||||
|
.height(Fill),
|
||||||
|
)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
row
|
||||||
|
}
|
||||||
|
const TREE_FONT_SZ: f32 = 13.0;
|
||||||
/// Longest edge of a preview image, in pixels.
|
/// Longest edge of a preview image, in pixels.
|
||||||
const PREVIEW_MAX: u32 = 256;
|
const PREVIEW_MAX: u32 = 256;
|
||||||
|
|
||||||
@@ -82,11 +115,12 @@ pub struct XrefManagerPanel {
|
|||||||
/// when the tab's revision moves (reuses the undo-snapshot counter, so
|
/// when the tab's revision moves (reuses the undo-snapshot counter, so
|
||||||
/// CLI and palette mutations both trip it).
|
/// CLI and palette mutations both trip it).
|
||||||
pub source_edit_revision: u64,
|
pub source_edit_revision: u64,
|
||||||
/// Draft for the details-pane "new path" edit.
|
|
||||||
pub path_input: String,
|
|
||||||
/// Reference-table column widths: Reference, Status, Size, Type, Date,
|
/// Reference-table column widths: Reference, Status, Size, Type, Date,
|
||||||
/// Saved Path. Draggable via the header dividers (clamped).
|
/// Saved Path. Draggable via the header dividers (clamped).
|
||||||
pub col_widths: [f32; 6],
|
pub col_widths: [f32; 6],
|
||||||
|
/// Reference-table area height. Draggable via the split divider above
|
||||||
|
/// the Details/Preview pane (clamped).
|
||||||
|
pub table_h: f32,
|
||||||
/// Open dropdown menus (one at a time; dismissed together).
|
/// Open dropdown menus (one at a time; dismissed together).
|
||||||
pub attach_open: bool,
|
pub attach_open: bool,
|
||||||
pub refresh_open: bool,
|
pub refresh_open: bool,
|
||||||
@@ -121,8 +155,8 @@ impl Default for XrefManagerPanel {
|
|||||||
nested: HashSet::new(),
|
nested: HashSet::new(),
|
||||||
source_tab_id: None,
|
source_tab_id: None,
|
||||||
source_edit_revision: 0,
|
source_edit_revision: 0,
|
||||||
path_input: String::new(),
|
|
||||||
col_widths: DEFAULT_COL_WIDTHS,
|
col_widths: DEFAULT_COL_WIDTHS,
|
||||||
|
table_h: TABLE_H,
|
||||||
attach_open: false,
|
attach_open: false,
|
||||||
refresh_open: false,
|
refresh_open: false,
|
||||||
path_open: false,
|
path_open: false,
|
||||||
@@ -370,6 +404,12 @@ impl XrefManagerPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply a vertical split-divider drag to the table area height
|
||||||
|
/// (clamped). Called with the pointer delta.
|
||||||
|
pub fn drag_table_by(&mut self, dy: f32) {
|
||||||
|
self.table_h = (self.table_h + dy).clamp(TABLE_MIN_H, 1200.0);
|
||||||
|
}
|
||||||
|
|
||||||
/// Expand/collapse one tree parent (no-op for unknown keys).
|
/// Expand/collapse one tree parent (no-op for unknown keys).
|
||||||
pub fn toggle_expand(&mut self, parent_key: u64) {
|
pub fn toggle_expand(&mut self, parent_key: u64) {
|
||||||
if !self.expanded.remove(&parent_key) && self.children.contains_key(&parent_key) {
|
if !self.expanded.remove(&parent_key) && self.children.contains_key(&parent_key) {
|
||||||
@@ -554,7 +594,8 @@ impl XrefManagerPanel {
|
|||||||
toolbar_tip(crate::t!("Attach").into_owned(), web_tip.clone())
|
toolbar_tip(crate::t!("Attach").into_owned(), web_tip.clone())
|
||||||
} else {
|
} else {
|
||||||
split_button(
|
split_button(
|
||||||
toolbar_btn(crate::t!("Attach").into_owned(), Some(Message::XAttachPick)),
|
crate::t!("Attach").into_owned(),
|
||||||
|
Some(Message::XAttachPick),
|
||||||
self.attach_open,
|
self.attach_open,
|
||||||
Message::XrefManagerAttachMenu,
|
Message::XrefManagerAttachMenu,
|
||||||
vec![
|
vec![
|
||||||
@@ -575,10 +616,8 @@ impl XrefManagerPanel {
|
|||||||
toolbar_tip(crate::t!("Refresh").into_owned(), web_tip.clone())
|
toolbar_tip(crate::t!("Refresh").into_owned(), web_tip.clone())
|
||||||
} else {
|
} else {
|
||||||
split_button(
|
split_button(
|
||||||
toolbar_btn(
|
crate::t!("Refresh").into_owned(),
|
||||||
crate::t!("Refresh").into_owned(),
|
Some(Message::XrefManagerRefresh),
|
||||||
Some(Message::XrefManagerRefresh),
|
|
||||||
),
|
|
||||||
self.refresh_open,
|
self.refresh_open,
|
||||||
Message::XrefManagerRefreshMenu,
|
Message::XrefManagerRefreshMenu,
|
||||||
vec![menu_item(
|
vec![menu_item(
|
||||||
@@ -588,23 +627,55 @@ impl XrefManagerPanel {
|
|||||||
)],
|
)],
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
// List / Tree are two buttons on their own left-aligned row; the
|
// List / Tree view buttons with active-state coloring: the current
|
||||||
// active mode renders inert.
|
// mode renders in the primary accent (blue), the other is clickable.
|
||||||
let list_btn = if self.tree {
|
let mode_btn = |label: String, active: bool| -> Element<'static, Message> {
|
||||||
toolbar_btn(crate::t!("List").into_owned(), Some(Message::XrefManagerToggleTree))
|
let msg = if active {
|
||||||
} else {
|
None
|
||||||
toolbar_btn(crate::t!("List").into_owned(), None)
|
} else {
|
||||||
|
Some(Message::XrefManagerToggleTree)
|
||||||
|
};
|
||||||
|
let mut b = button(text(label).size(11))
|
||||||
|
.style(move |theme: &Theme, status| {
|
||||||
|
let palette = theme.palette();
|
||||||
|
if active {
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(palette.primary.base.color)),
|
||||||
|
border: Border {
|
||||||
|
radius: 3.0.into(),
|
||||||
|
color: palette.primary.base.color,
|
||||||
|
width: 1.0,
|
||||||
|
},
|
||||||
|
text_color: palette.primary.base.text,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let pair = match status {
|
||||||
|
button::Status::Hovered | button::Status::Pressed => {
|
||||||
|
palette.background.strong
|
||||||
|
}
|
||||||
|
_ => palette.background.weak,
|
||||||
|
};
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(pair.color)),
|
||||||
|
border: Border {
|
||||||
|
radius: 3.0.into(),
|
||||||
|
color: palette.background.neutral.color,
|
||||||
|
width: 1.0,
|
||||||
|
},
|
||||||
|
text_color: pair.text,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.padding([4, 10]);
|
||||||
|
if let Some(m) = msg {
|
||||||
|
b = b.on_press(m);
|
||||||
|
}
|
||||||
|
b.into()
|
||||||
};
|
};
|
||||||
let tree_btn = if self.tree {
|
let list_btn = mode_btn(crate::t!("List").into_owned(), !self.tree);
|
||||||
toolbar_btn(crate::t!("Tree").into_owned(), None)
|
let tree_btn = mode_btn(crate::t!("Tree").into_owned(), self.tree);
|
||||||
} else {
|
|
||||||
toolbar_btn(crate::t!("Tree").into_owned(), Some(Message::XrefManagerToggleTree))
|
|
||||||
};
|
|
||||||
let mode_row: Element<'static, Message> =
|
|
||||||
container(row![list_btn, tree_btn].spacing(2))
|
|
||||||
.width(Fill)
|
|
||||||
.padding([2, 8])
|
|
||||||
.into();
|
|
||||||
// Reference operations (Detach/Unload/Reload/Bind/Overlay/Attach)
|
// Reference operations (Detach/Unload/Reload/Bind/Overlay/Attach)
|
||||||
// live in the per-row right-click menu, not the toolbar.
|
// live in the per-row right-click menu, not the toolbar.
|
||||||
// Change Path group: greyed until a reference whose path can change
|
// Change Path group: greyed until a reference whose path can change
|
||||||
@@ -653,10 +724,8 @@ impl XrefManagerPanel {
|
|||||||
Some(crate::t!("Select a single reference first.").into_owned())
|
Some(crate::t!("Select a single reference first.").into_owned())
|
||||||
};
|
};
|
||||||
split_button(
|
split_button(
|
||||||
toolbar_btn(
|
crate::t!("Change Path").into_owned(),
|
||||||
crate::t!("Change Path").into_owned(),
|
Some(Message::XrefManagerPathMenu),
|
||||||
Some(Message::XrefManagerPathMenu),
|
|
||||||
),
|
|
||||||
self.path_open,
|
self.path_open,
|
||||||
Message::XrefManagerPathMenu,
|
Message::XrefManagerPathMenu,
|
||||||
vec![
|
vec![
|
||||||
@@ -710,63 +779,35 @@ impl XrefManagerPanel {
|
|||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
)
|
)
|
||||||
.style(|theme: &Theme| container::Style {
|
.style(|theme: &Theme| container::Style {
|
||||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
background: Some(Background::Color(
|
||||||
|
theme.palette().background.weakest.color,
|
||||||
|
)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.padding([4, 8]);
|
.padding([4, 8]);
|
||||||
|
|
||||||
// ── Path-edit row ─────────────────────────────────────────────────
|
// ── File References pane title ──────────────────────────────────
|
||||||
// (Find & Replace moved to the Change Path menu — it prefills the
|
// List / Tree live on this row, right-aligned.
|
||||||
// command line instead of living in the panel.)
|
let table_title: Element<'_, Message> = container(
|
||||||
// Path edit applies to the anchor entry; nested anchors stay disabled.
|
|
||||||
let anchor_nested = self
|
|
||||||
.anchor
|
|
||||||
.is_some_and(|a| self.nested.contains(&a));
|
|
||||||
let path_apply = match self.anchor.and_then(|i| self.entries.get(i)) {
|
|
||||||
Some(_) if IS_WASM => toolbar_tip(
|
|
||||||
crate::t!("Set Path").into_owned(),
|
|
||||||
crate::t!("Reference changes are not available on web — the reference list is read-only.").into_owned(),
|
|
||||||
),
|
|
||||||
Some(_) if anchor_nested => toolbar_tip(
|
|
||||||
crate::t!("Set Path").into_owned(),
|
|
||||||
crate::t!("Nested references are read-only — edit them in their host drawing.")
|
|
||||||
.into_owned(),
|
|
||||||
),
|
|
||||||
Some(_) if !self.path_input.is_empty() => toolbar_btn(
|
|
||||||
crate::t!("Set Path").into_owned(),
|
|
||||||
Some(Message::XrefManagerPathApply),
|
|
||||||
),
|
|
||||||
Some(_) => toolbar_tip(
|
|
||||||
crate::t!("Set Path").into_owned(),
|
|
||||||
crate::t!("Type a new path first.").into_owned(),
|
|
||||||
),
|
|
||||||
None => toolbar_tip(
|
|
||||||
crate::t!("Set Path").into_owned(),
|
|
||||||
crate::t!("Select a reference first.").into_owned(),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let edit_row = container(
|
|
||||||
row![
|
row![
|
||||||
text(crate::t!("Path:")).size(11),
|
text(format!("{} :", crate::t!("File References").as_ref()))
|
||||||
text_input(crate::t!("new path for selection").as_ref(), &self.path_input)
|
|
||||||
.on_input(Message::XrefManagerPathInput)
|
|
||||||
.size(11)
|
.size(11)
|
||||||
.padding(4)
|
.width(Fill),
|
||||||
.width(Length::Fill),
|
list_btn,
|
||||||
path_apply,
|
tree_btn,
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
)
|
)
|
||||||
.style(|theme: &Theme| container::Style {
|
|
||||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.padding([4, 8]);
|
.padding(Padding {
|
||||||
|
top: 4.0,
|
||||||
// ── Column header ─────────────────────────────────────────────────
|
right: 8.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
left: 8.0,
|
||||||
|
})
|
||||||
|
.into();
|
||||||
// Each divider between header cells grabs for a width drag
|
// Each divider between header cells grabs for a width drag
|
||||||
// (`XrefColGrab(i)` resizes column `i`); mirrors the Layers
|
// (`XrefColGrab(i)` resizes column `i`); mirrors the Layers
|
||||||
// Name-column divider.
|
// Name-column divider.
|
||||||
@@ -867,7 +908,7 @@ impl XrefManagerPanel {
|
|||||||
.center_x(Fill)
|
.center_x(Fill)
|
||||||
.center_y(Fill)
|
.center_y(Fill)
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Length::Fixed(TABLE_H))
|
.height(Length::Fixed(self.table_h))
|
||||||
.into()
|
.into()
|
||||||
} else if self.tree {
|
} else if self.tree {
|
||||||
// Tree view replaces the table: icon + name rows in nesting
|
// Tree view replaces the table: icon + name rows in nesting
|
||||||
@@ -893,19 +934,19 @@ impl XrefManagerPanel {
|
|||||||
is_expanded,
|
is_expanded,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
scrollable(tree_col).height(Length::Fixed(TABLE_H)).into()
|
scrollable(tree_col).height(Length::Fixed(self.table_h)).into()
|
||||||
} else {
|
} else {
|
||||||
scrollable(
|
scrollable(
|
||||||
column![
|
column![
|
||||||
col_header,
|
col_header,
|
||||||
scrollable(rows_col).height(Length::Fixed(TABLE_H)),
|
scrollable(rows_col).height(Length::Fixed(self.table_h)),
|
||||||
]
|
]
|
||||||
.width(Length::Fixed(table_w)),
|
.width(Length::Fixed(table_w)),
|
||||||
)
|
)
|
||||||
.direction(iced::widget::scrollable::Direction::Horizontal(
|
.direction(iced::widget::scrollable::Direction::Horizontal(
|
||||||
iced::widget::scrollable::Scrollbar::new(),
|
iced::widget::scrollable::Scrollbar::new(),
|
||||||
))
|
))
|
||||||
.height(Length::Fixed(TABLE_H))
|
.height(Length::Fixed(self.table_h))
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
// Thin external padding + darkest surface around the table, like the
|
// Thin external padding + darkest surface around the table, like the
|
||||||
@@ -1023,15 +1064,30 @@ impl XrefManagerPanel {
|
|||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut content = column![title_bar, toolbar, mode_row, edit_row, table_rows];
|
// Horizontal split divider between the table and the lower pane:
|
||||||
|
// grabs vertically to give the table more (or less) height.
|
||||||
|
let split = mouse_area(
|
||||||
|
container(iced::widget::Space::new().height(Length::Fixed(6.0)))
|
||||||
|
.width(Fill)
|
||||||
|
.height(Length::Fixed(6.0)),
|
||||||
|
)
|
||||||
|
.on_press(Message::XrefSplitGrab)
|
||||||
|
.interaction(iced::mouse::Interaction::ResizingVertically);
|
||||||
|
let mut content = column![title_bar, toolbar, table_title, table_rows, split];
|
||||||
if let Some(notice) = notice {
|
if let Some(notice) = notice {
|
||||||
content = content.push(notice);
|
content = content.push(notice);
|
||||||
}
|
}
|
||||||
content = content.push(lower);
|
content = content.push(lower);
|
||||||
|
// Panel-wide move/release tracking for the split drag (mirrors the
|
||||||
|
// header divider tracking for columns).
|
||||||
|
let content: Element<'_, Message> = mouse_area(content.spacing(0))
|
||||||
|
.on_move(|p| Message::XrefSplitMove(p))
|
||||||
|
.on_release(Message::XrefSplitRelease)
|
||||||
|
.into();
|
||||||
// Fixed to the dock width like the block palette: the panel never
|
// Fixed to the dock width like the block palette: the panel never
|
||||||
// sizes itself from its (wider) table content, so the dock width
|
// sizes itself from its (wider) table content, so the dock width
|
||||||
// setting — and resize — actually takes effect.
|
// setting — and resize — actually takes effect.
|
||||||
container(content.spacing(0))
|
container(content)
|
||||||
.style(|theme: &Theme| container::Style {
|
.style(|theme: &Theme| container::Style {
|
||||||
background: Some(Background::Color(
|
background: Some(Background::Color(
|
||||||
theme.palette().background.base.color,
|
theme.palette().background.base.color,
|
||||||
@@ -1251,23 +1307,78 @@ fn toolbar_tip(label: String, tip: String) -> Element<'static, Message> {
|
|||||||
/// Split button: a default action plus a triangle that opens the rest in an
|
/// Split button: a default action plus a triangle that opens the rest in an
|
||||||
/// overlay menu (spec toolbar: Attach ▾, Refresh ▾, Change Path ▾).
|
/// overlay menu (spec toolbar: Attach ▾, Refresh ▾, Change Path ▾).
|
||||||
///
|
///
|
||||||
/// `main` is the already-gated default-action element; `toggle` flips this
|
/// Both halves share one bordered group look: the main half keeps only the
|
||||||
/// menu (closing the others is handled at the message site); `items` are the
|
/// left corners rounded (no right border), the caret keeps only the right
|
||||||
/// menu rows, each full-width. Dismissal (Escape / outside click) funnels to
|
/// corners (no left border), so the pair reads as a single control. Hover
|
||||||
|
/// and press states highlight each half independently.
|
||||||
|
/// `main_msg` runs the default action; `toggle` flips this menu (closing the
|
||||||
|
/// others is handled at the message site); `items` are the menu rows, each
|
||||||
|
/// full-width. Dismissal (Escape / outside click) funnels to
|
||||||
/// [`Message::XrefManagerDismissMenus`].
|
/// [`Message::XrefManagerDismissMenus`].
|
||||||
fn split_button(
|
fn split_button(
|
||||||
main: Element<'static, Message>,
|
main_label: String,
|
||||||
|
main_msg: Option<Message>,
|
||||||
menu_open: bool,
|
menu_open: bool,
|
||||||
toggle: Message,
|
toggle: Message,
|
||||||
items: Vec<Element<'static, Message>>,
|
items: Vec<Element<'static, Message>>,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
|
use iced::border::Radius;
|
||||||
|
// Triangle matches the main button's height and bordered style so the
|
||||||
|
// group reads as one split control: same padding, icon box fixed to the
|
||||||
|
// 11px text line height.
|
||||||
|
let half_style = |left: bool| {
|
||||||
|
move |theme: &Theme, status| {
|
||||||
|
let palette = theme.palette();
|
||||||
|
let pair = match status {
|
||||||
|
button::Status::Hovered | button::Status::Pressed => {
|
||||||
|
palette.background.strong
|
||||||
|
}
|
||||||
|
_ => palette.background.weak,
|
||||||
|
};
|
||||||
|
let radius = if left {
|
||||||
|
Radius {
|
||||||
|
top_left: 3.0,
|
||||||
|
top_right: 0.0,
|
||||||
|
bottom_right: 0.0,
|
||||||
|
bottom_left: 3.0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Radius {
|
||||||
|
top_left: 0.0,
|
||||||
|
top_right: 3.0,
|
||||||
|
bottom_right: 3.0,
|
||||||
|
bottom_left: 0.0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(pair.color)),
|
||||||
|
border: Border {
|
||||||
|
radius: radius.into(),
|
||||||
|
color: palette.background.neutral.color,
|
||||||
|
width: 1.0,
|
||||||
|
},
|
||||||
|
text_color: pair.text,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut main = button(text(main_label).size(11))
|
||||||
|
.style(half_style(true))
|
||||||
|
.padding([4, 10]);
|
||||||
|
if let Some(m) = main_msg {
|
||||||
|
main = main.on_press(m);
|
||||||
|
}
|
||||||
let caret = button(
|
let caret = button(
|
||||||
container(crate::ui::icons::themed_arrow_down(9.0)).align_y(iced::Center),
|
container(crate::ui::icons::themed_arrow_down(9.0))
|
||||||
|
.align_y(iced::Center)
|
||||||
|
.height(Length::Fixed(15.0)),
|
||||||
)
|
)
|
||||||
.on_press(toggle)
|
.on_press(toggle)
|
||||||
.style(button::subtle)
|
.style(half_style(false))
|
||||||
.padding([4, 6]);
|
.padding([4, 10]);
|
||||||
let head = row![main, caret].spacing(0).align_y(iced::Center);
|
// Overlap the halves by the 1px shared border so no double line shows
|
||||||
|
// where they meet.
|
||||||
|
let head = row![main, caret].spacing(-1.0).align_y(iced::Center);
|
||||||
let popup: Element<'static, Message> = container(
|
let popup: Element<'static, Message> = container(
|
||||||
column(items.into_iter().map(|item| {
|
column(items.into_iter().map(|item| {
|
||||||
container(item).width(Fill).into()
|
container(item).width(Fill).into()
|
||||||
@@ -1304,13 +1415,23 @@ fn menu_item(label: String, msg: Option<Message>, gate: Option<String>) -> Eleme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-row right-click menu: the reference operations act on the current
|
/// One right-click menu row, styled like the model-space context menu:
|
||||||
/// selection (right-click selects the row first when outside it), so one
|
/// borderless subtle text rows, not toolbar buttons.
|
||||||
/// static menu serves every row. Nested rows report per-entry errors from
|
fn menu_row(label: String, msg: Message) -> Element<'static, Message> {
|
||||||
/// the engine, matching the CLI wording.
|
button(text(label).size(12))
|
||||||
fn row_menu() -> Element<'static, Message> {
|
.on_press(msg)
|
||||||
|
.style(button::subtle)
|
||||||
|
.padding([4, 12])
|
||||||
|
.width(Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-row right-click menu. Each item targets its own row, so the
|
||||||
|
/// operation applies to the row under the cursor. Nested rows report
|
||||||
|
/// per-entry errors from the engine, matching the CLI wording.
|
||||||
|
fn row_menu_for(index: usize) -> Element<'static, Message> {
|
||||||
let item = |label: &str, op: XrefPaletteOp| -> Element<'static, Message> {
|
let item = |label: &str, op: XrefPaletteOp| -> Element<'static, Message> {
|
||||||
toolbar_btn(label.to_string(), Some(Message::XrefManagerOp(op)))
|
menu_row(label.to_string(), Message::XrefRowOp(index, op))
|
||||||
};
|
};
|
||||||
container(
|
container(
|
||||||
column![
|
column![
|
||||||
@@ -1422,7 +1543,7 @@ fn xref_row<'a>(
|
|||||||
)
|
)
|
||||||
.on_press(Message::XrefManagerSelect(index))
|
.on_press(Message::XrefManagerSelect(index))
|
||||||
.on_right_press(Message::XrefRowRightClick(index));
|
.on_right_press(Message::XrefRowRightClick(index));
|
||||||
iced_aw::ContextMenu::new(row, row_menu).into()
|
iced_aw::ContextMenu::new(row, move || row_menu_for(index)).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One tree-mode row: indent + optional expand arrow + file icon + name.
|
/// One tree-mode row: indent + optional expand arrow + file icon + name.
|
||||||
@@ -1435,11 +1556,7 @@ fn tree_row(
|
|||||||
is_expanded: bool,
|
is_expanded: bool,
|
||||||
) -> Element<'_, Message> {
|
) -> Element<'_, Message> {
|
||||||
let mut cells = row![].spacing(2).align_y(iced::Center);
|
let mut cells = row![].spacing(2).align_y(iced::Center);
|
||||||
if display.depth > 0 {
|
cells = cells.push(tree_indent(display.depth));
|
||||||
cells = cells.push(
|
|
||||||
iced::widget::Space::new().width(Length::Fixed(INDENT_W * display.depth as f32)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if show_expand {
|
if show_expand {
|
||||||
let key = entry.key;
|
let key = entry.key;
|
||||||
let arrow = button(crate::ui::icons::themed_arrow_toggle(is_expanded, 10.0))
|
let arrow = button(crate::ui::icons::themed_arrow_toggle(is_expanded, 10.0))
|
||||||
@@ -1454,10 +1571,10 @@ fn tree_row(
|
|||||||
cells = cells.push(arrow);
|
cells = cells.push(arrow);
|
||||||
}
|
}
|
||||||
cells = cells.push(
|
cells = cells.push(
|
||||||
container(crate::ui::icons::themed_secondary(crate::ui::icons::DOC, 12.0))
|
container(crate::ui::icons::themed_secondary(crate::ui::icons::DOC, 14.0))
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
);
|
);
|
||||||
cells = cells.push(text(entry.name.clone()).size(FONT_SZ));
|
cells = cells.push(text(entry.name.clone()).size(TREE_FONT_SZ));
|
||||||
let index = display.index;
|
let index = display.index;
|
||||||
let row = mouse_area(
|
let row = mouse_area(
|
||||||
container(cells.spacing(4).width(Fill))
|
container(cells.spacing(4).width(Fill))
|
||||||
@@ -1477,17 +1594,16 @@ fn tree_row(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.padding(Padding {
|
.padding(Padding {
|
||||||
top: 0.0,
|
top: 5.0,
|
||||||
bottom: 0.0,
|
bottom: 5.0,
|
||||||
left: 8.0,
|
left: 8.0,
|
||||||
right: 8.0,
|
right: 8.0,
|
||||||
})
|
})
|
||||||
.height(Length::Fixed(ROW_H))
|
|
||||||
.width(Fill),
|
.width(Fill),
|
||||||
)
|
)
|
||||||
.on_press(Message::XrefManagerSelect(index))
|
.on_press(Message::XrefManagerSelect(index))
|
||||||
.on_right_press(Message::XrefRowRightClick(index));
|
.on_right_press(Message::XrefRowRightClick(index));
|
||||||
iced_aw::ContextMenu::new(row, row_menu).into()
|
iced_aw::ContextMenu::new(row, move || row_menu_for(index)).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tree-mode host root: home icon + name with the current-drawing marker.
|
/// Tree-mode host root: home icon + name with the current-drawing marker.
|
||||||
@@ -1500,15 +1616,11 @@ fn tree_host_row(display: DisplayRow, host_name: &str) -> Element<'_, Message> {
|
|||||||
};
|
};
|
||||||
name.push('*');
|
name.push('*');
|
||||||
let mut cells = row![].spacing(2).align_y(iced::Center);
|
let mut cells = row![].spacing(2).align_y(iced::Center);
|
||||||
if display.depth > 0 {
|
cells = cells.push(tree_indent(display.depth));
|
||||||
cells = cells.push(
|
|
||||||
iced::widget::Space::new().width(Length::Fixed(INDENT_W * display.depth as f32)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
cells = cells.push(
|
cells = cells.push(
|
||||||
container(crate::ui::icons::themed_home(12.0)).align_y(iced::Center),
|
container(crate::ui::icons::themed_home(14.0)).align_y(iced::Center),
|
||||||
);
|
);
|
||||||
cells = cells.push(text(name).size(FONT_SZ));
|
cells = cells.push(text(name).size(TREE_FONT_SZ));
|
||||||
container(cells.spacing(4).width(Fill))
|
container(cells.spacing(4).width(Fill))
|
||||||
.style(|theme: &Theme| {
|
.style(|theme: &Theme| {
|
||||||
let pair = theme.palette().background.base;
|
let pair = theme.palette().background.base;
|
||||||
@@ -1519,12 +1631,11 @@ fn tree_host_row(display: DisplayRow, host_name: &str) -> Element<'_, Message> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.padding(Padding {
|
.padding(Padding {
|
||||||
top: 0.0,
|
top: 5.0,
|
||||||
bottom: 0.0,
|
bottom: 5.0,
|
||||||
left: 8.0,
|
left: 8.0,
|
||||||
right: 8.0,
|
right: 8.0,
|
||||||
})
|
})
|
||||||
.height(Length::Fixed(ROW_H))
|
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@@ -1663,9 +1774,9 @@ fn reference_preview(entry: &ReferenceEntry) -> Option<image::RgbaImage> {
|
|||||||
if IS_WASM {
|
if IS_WASM {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if !matches!(entry.status, RefStatus::Loaded | RefStatus::Stale) {
|
// Any entry whose file resolves decodes — including Unloaded and
|
||||||
return None;
|
// Unreferenced rows — so previews don't depend on load state. Missing
|
||||||
}
|
// files fail the decode below and fall back to the placeholder.
|
||||||
let found = entry.found_at.as_deref().filter(|s| !s.is_empty())?;
|
let found = entry.found_at.as_deref().filter(|s| !s.is_empty())?;
|
||||||
match entry.kind {
|
match entry.kind {
|
||||||
RefKind::DwgXref => {
|
RefKind::DwgXref => {
|
||||||
@@ -1856,6 +1967,16 @@ mod tests {
|
|||||||
assert_eq!(panel.table_content_width(), panel.col_widths.iter().sum::<f32>() + super::COL_GUTTER * 5.0);
|
assert_eq!(panel.table_content_width(), panel.col_widths.iter().sum::<f32>() + super::COL_GUTTER * 5.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_split_drag_clamps() {
|
||||||
|
let mut panel = XrefManagerPanel::default();
|
||||||
|
assert_eq!(panel.table_h, super::TABLE_H);
|
||||||
|
panel.drag_table_by(60.0);
|
||||||
|
assert_eq!(panel.table_h, super::TABLE_H + 60.0);
|
||||||
|
panel.drag_table_by(-10000.0);
|
||||||
|
assert_eq!(panel.table_h, super::TABLE_MIN_H);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_format_reads_extension() {
|
fn image_format_reads_extension() {
|
||||||
assert_eq!(image_format("C:/exa/plan.BMP"), "BMP");
|
assert_eq!(image_format("C:/exa/plan.BMP"), "BMP");
|
||||||
@@ -2047,4 +2168,38 @@ mod tests {
|
|||||||
);
|
);
|
||||||
std::fs::remove_dir_all(&dir).ok();
|
std::fs::remove_dir_all(&dir).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preview_decodes_unloaded_entry_with_file() {
|
||||||
|
// Previews key off file resolvability, not load state: an Unloaded
|
||||||
|
// image whose file exists still decodes.
|
||||||
|
use crate::io::xref_model::UnloadKey;
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"ocs_xref_preview_unloaded_{}_{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let doc = preview_doc(&dir, "img.png");
|
||||||
|
let key = doc
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.find_map(|(h, o)| match o {
|
||||||
|
acadrust::objects::ObjectType::ImageDefinition(_) => Some(h.value()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let mut unloaded = std::collections::HashSet::new();
|
||||||
|
unloaded.insert(UnloadKey::Direct(key));
|
||||||
|
let no_prev = std::collections::HashMap::new();
|
||||||
|
let mut panel = XrefManagerPanel::default();
|
||||||
|
panel.refresh(&doc, &dir, &unloaded, &no_prev, "host", "");
|
||||||
|
panel.toggle_select(0);
|
||||||
|
panel.refresh(&doc, &dir, &unloaded, &no_prev, "host", "");
|
||||||
|
assert_eq!(panel.previews.len(), 1);
|
||||||
|
std::fs::remove_dir_all(&dir).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user