feat(xref): docked External References panel with previews and translations

Docked External References panel with host row, thumbnail previews, and translated interface strings.
This commit is contained in:
Karim Jerbi committed 2026-09-15 23:56:31 +01:00
1 parent d0d946a568
commit 019422d220
32 files changed
+2691 -184

No files matched your search

+72 -1
View File
@@ -599,6 +599,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
self.show_block_palette = false;
self.block_palette.placing = None;
}
PanelId::ExternalReferences => {
self.show_external_references = false;
}
PanelId::Properties => {
self.show_properties = false;
self.ribbon.set_properties(false);
@@ -692,6 +695,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
match id {
PanelId::Properties => self.show_properties,
PanelId::BlockPalette => self.show_block_palette,
PanelId::ExternalReferences => self.show_external_references,
}
}
@@ -772,6 +776,15 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
.unwrap_or_else(|| std::path::PathBuf::from("."));
let tab_id = self.tabs[i].id;
let revision = self.tabs[i].edit_revision;
let (host_name, host_path) = match &self.tabs[i].current_path {
Some(p) => (
p.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default(),
p.to_string_lossy().into_owned(),
),
None => (String::new(), String::new()),
};
let baselines = {
let tab = &self.tabs[i];
self.xref_manager.refresh(
@@ -779,6 +792,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
&base_dir,
tab.xref_unloaded.as_set(),
&tab.xref_stat_cache.0,
&host_name,
&host_path,
)
};
let tab = &mut self.tabs[i];
@@ -803,7 +818,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
/// XOPEN-return edit, undo/redo — all bump `edit_revision`). Cheap:
/// `collect_entries` stats files without re-parsing the host.
pub(crate) fn refresh_xref_manager_if_stale(&mut self) {
if self.active_modal != Some(crate::app::ModalKind::XrefManager) {
if !self.dock_panel_visible(crate::ui::dock::PanelId::ExternalReferences) {
return;
}
let i = self.active_tab;
@@ -1164,6 +1179,62 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
self.refresh_xref_manager();
}
/// Reload every direct drawing reference (toolbar Reload All). Same
/// engine path as `XRELOAD`: undo snapshot, session flags cleared,
/// full resolve, per-ref report, stat baselines refreshed.
pub(crate) fn xref_manager_reload_all(&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 Some(path) = self.tabs[i].current_path.clone() else {
self.command_line
.push_error(crate::t!("XREF Save the drawing first to resolve relative XREF paths.").as_ref());
return;
};
let Some(base_dir) = path.parent().map(|p| p.to_path_buf()) else {
self.command_line
.push_error(crate::t!("XREF Save the drawing first to resolve relative XREF paths.").as_ref());
return;
};
let reload_keys: Vec<u64> = crate::io::xref::collect_entries_with_prev(
&self.tabs[i].scene.document,
&base_dir,
self.tabs[i].xref_unloaded.as_set(),
&self.tabs[i].xref_stat_cache.0,
)
.iter()
.filter(|e| e.kind == crate::io::xref_model::RefKind::DwgXref && e.parent_key.is_none())
.map(|e| e.key)
.collect();
self.push_undo_snapshot(i, "XREF-RELOAD");
for key in &reload_keys {
self.tabs[i].xref_unloaded.remove(key);
self.tabs[i].xref_stat_cache.remove(key);
}
let (infos, _dropped) =
crate::io::xref::resolve_xrefs(&mut self.tabs[i].scene.document, &base_dir);
let fresh = crate::io::xref::collect_entries_with_prev(
&self.tabs[i].scene.document,
&base_dir,
self.tabs[i].xref_unloaded.as_set(),
&self.tabs[i].xref_stat_cache.0,
);
for e in &fresh {
if e.status == crate::io::xref_model::RefStatus::Loaded {
if let Some(m) = e.modified {
self.tabs[i].xref_stat_cache.insert(e.key, m);
}
}
}
for info in &infos {
self.report_xref_status(info);
}
self.post_ref_op(i);
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) {
+92 -5
View File
@@ -2131,12 +2131,15 @@ impl OpenCADStudio {
}
Message::ToggleXrefManager => {
if self.active_modal == Some(super::ModalKind::XrefManager) {
self.active_modal = None;
self.reset_modal_geometry();
} else {
self.show_external_references ^= true;
if self.show_external_references {
use crate::app::config::DockSide;
use crate::ui::dock::PanelId;
if self.dock.location(PanelId::ExternalReferences).is_none() {
self.dock.dock(PanelId::ExternalReferences, DockSide::Right, usize::MAX);
}
self.dock_expanded = Some(PanelId::ExternalReferences);
self.refresh_xref_manager();
self.active_modal = Some(super::ModalKind::XrefManager);
}
Task::none()
}
@@ -2152,6 +2155,90 @@ impl OpenCADStudio {
self.xref_manager.toggle_tree();
Task::none()
}
Message::XrefManagerTogglePreview => {
self.xref_manager.show_preview ^= true;
Task::none()
}
Message::XrefManagerAttachMenu => {
self.xref_manager.attach_open ^= true;
self.xref_manager.refresh_open = false;
self.xref_manager.path_open = false;
Task::none()
}
Message::XrefManagerRefreshMenu => {
self.xref_manager.refresh_open ^= true;
self.xref_manager.attach_open = false;
self.xref_manager.path_open = false;
Task::none()
}
Message::XrefManagerPathMenu => {
self.xref_manager.path_open ^= true;
self.xref_manager.attach_open = false;
self.xref_manager.refresh_open = false;
Task::none()
}
Message::XrefManagerDismissMenus => {
self.xref_manager.attach_open = false;
self.xref_manager.refresh_open = false;
self.xref_manager.path_open = false;
Task::none()
}
Message::XrefPathPick => Task::perform(
async {
let handle = crate::sys::file_dialog()
.set_title(crate::t!("Select New Path").as_ref())
.pick_file()
.await;
match handle {
Some(h) => Ok(crate::sys::handle_path(&h)),
None => Err("Cancelled".to_string()),
}
},
Message::XrefPathPickResult,
),
Message::XrefPathPickResult(Ok(path)) => {
// Select New Path applies the picked file to the single
// direct anchor entry (menu gates the rest).
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 Task::none();
};
let new_raw = path.to_string_lossy().into_owned();
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();
Task::none()
}
Message::XrefPathPickResult(Err(e)) => {
if e != "Cancelled" {
self.command_line.push_error(crate::tf!("XREF: {e}").as_ref());
}
Task::none()
}
Message::XrefManagerReloadAll => {
self.xref_manager_reload_all();
Task::none()
}
Message::XrefManagerToggleExpand(key) => {
self.xref_manager.toggle_expand(key);
Task::none()