forked from 1b-agent/opencadstudio-upstream
fix(xref): carry stable row identity in row messages
XrefManagerSelect/RightClick/RowOp/PathPick/FindReplace carried positional display indices that rescans could retarget. New XrefRowId{key,saved_path} (the identity refresh uses) in all 5 variants, resolved via row_index at dispatch with graceful no-op; panel selection stays index-based. display.index remains for view-time styling/lookup only. Tests: xref_rowop_unknown_id_is_noop (RED first); existing dialog/simulator/click tests migrated to ids. Full suite: 2044 passed / 0 failed / 25 ignored.
This commit is contained in:
1 parent
7bbc5b0319
commit
6fa11887c7
4 files changed
+185
-89
No files matched your search
+13
-10
@@ -2671,11 +2671,11 @@ pub enum Message {
|
||||
ToggleXrefManager,
|
||||
/// Rescan the active drawing's references into the palette.
|
||||
XrefManagerRefresh,
|
||||
/// Toggle one palette row (entry index) in the multi-selection set.
|
||||
XrefManagerSelect(usize),
|
||||
/// Toggle one palette row (stable row id) in the multi-selection set.
|
||||
XrefManagerSelect(crate::ui::window::xref_manager::XrefRowId),
|
||||
/// Right-click on a palette row: single-select it when outside the
|
||||
/// selection (the context menu then acts on the selection).
|
||||
XrefRowRightClick(usize),
|
||||
XrefRowRightClick(crate::ui::window::xref_manager::XrefRowId),
|
||||
/// Flip the palette's list/tree presentation.
|
||||
XrefManagerToggleTree,
|
||||
/// Start a reference-table column divider drag (column index).
|
||||
@@ -2712,16 +2712,19 @@ pub enum Message {
|
||||
XrefManagerToggleExpand(u64),
|
||||
/// Selection-scoped palette operation (detach/unload/reload/overlay/pathtype).
|
||||
XrefManagerOp(crate::ui::window::xref_manager::XrefPaletteOp),
|
||||
/// Row-scoped palette operation: selects row `index`, then applies the
|
||||
/// operation to it.
|
||||
XrefRowOp(usize, crate::ui::window::xref_manager::XrefPaletteOp),
|
||||
/// Row-scoped palette operation: selects the row by stable id, then
|
||||
/// applies the operation to it.
|
||||
XrefRowOp(
|
||||
crate::ui::window::xref_manager::XrefRowId,
|
||||
crate::ui::window::xref_manager::XrefPaletteOp,
|
||||
),
|
||||
/// Prefill the command line for Find & Replace across references
|
||||
/// (`XREF Path Find <old> <new>`); the CLI parses and runs it.
|
||||
XrefFindReplacePrompt,
|
||||
/// Pick a new path for a specific row index.
|
||||
XrefRowPathPick(usize),
|
||||
/// Prefill the command line for Find & Replace for a specific row index.
|
||||
XrefRowFindReplacePrompt(usize),
|
||||
/// Pick a new path for a specific row (stable id).
|
||||
XrefRowPathPick(crate::ui::window::xref_manager::XrefRowId),
|
||||
/// Prefill the command line for Find & Replace for a specific row id.
|
||||
XrefRowFindReplacePrompt(crate::ui::window::xref_manager::XrefRowId),
|
||||
XrefRowChangePathEnter,
|
||||
XrefRowChangePathLeave,
|
||||
LayerToggleVisible(String),
|
||||
|
||||
+46
-13
@@ -1818,6 +1818,35 @@ mod tests {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xref_rowop_unknown_id_is_noop() {
|
||||
// #49 RED: row messages carry (key, saved_path) identity and resolve
|
||||
// at dispatch; a deleted row's id must no-op instead of hitting whatever
|
||||
// row now sits at the old index. Fails to compile until the payload
|
||||
// carries XrefRowId instead of usize.
|
||||
use crate::app::Message;
|
||||
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
|
||||
let mut app = fresh();
|
||||
let i = app.active_tab;
|
||||
let start = app.command_line.history.len();
|
||||
let ghost = XrefRowId {
|
||||
key: u64::MAX,
|
||||
saved_path: "gone.dwg".to_string(),
|
||||
};
|
||||
let records_before = app.tabs[i].scene.document.block_records.iter().count();
|
||||
let _ = app.update(Message::XrefRowOp(ghost, XrefPaletteOp::Detach));
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.block_records.iter().count(),
|
||||
records_before,
|
||||
"no-op must not disturb the document"
|
||||
);
|
||||
let out = palette_output(&app, start);
|
||||
assert!(
|
||||
!out.contains("detached"),
|
||||
"unknown id must not detach anything, got: {out:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_rowop_nested_reports_per_entry() {
|
||||
// Behavioral matrix: every row op on a NESTED row via the GUI
|
||||
@@ -1825,7 +1854,7 @@ mod tests {
|
||||
// wording), never silently skip.
|
||||
use codec::tables::BlockRecord;
|
||||
use crate::app::Message;
|
||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
||||
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
|
||||
let dir = palette_tmpdir("nestedmatrix");
|
||||
let mut host_doc = codec::CadDocument::new();
|
||||
let mut inner = BlockRecord::new("INNER");
|
||||
@@ -1843,8 +1872,9 @@ mod tests {
|
||||
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
||||
app.tabs[i].current_path = Some(dir.join("app.dwg"));
|
||||
app.refresh_xref_manager();
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.name == "INNER").expect("nested INNER listed");
|
||||
assert!(app.xref_manager.entries[idx].parent_key.is_some());
|
||||
let row = app.xref_manager.entries.iter().find(|e| e.name == "INNER").expect("nested INNER listed");
|
||||
assert!(row.parent_key.is_some());
|
||||
let row_id = XrefRowId::of(row.key, &row.saved_path);
|
||||
for op in [
|
||||
XrefPaletteOp::Detach,
|
||||
XrefPaletteOp::Unload,
|
||||
@@ -1853,7 +1883,7 @@ mod tests {
|
||||
XrefPaletteOp::Overlay,
|
||||
] {
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, op));
|
||||
let _ = app.update(Message::XrefRowOp(row_id.clone(), op));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("INNER"), "op {op:?} on nested row gave: {out:?}");
|
||||
assert!(
|
||||
@@ -1870,7 +1900,7 @@ mod tests {
|
||||
// drive the exact GUI message path (row right-click menu item).
|
||||
use codec::tables::BlockRecord;
|
||||
use crate::app::Message;
|
||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
||||
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
|
||||
let mut app = fresh();
|
||||
let i = app.active_tab;
|
||||
for name in ["PLAN", "SITE"] {
|
||||
@@ -1882,15 +1912,17 @@ mod tests {
|
||||
}
|
||||
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_repro_host.dwg"));
|
||||
app.refresh_xref_manager();
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.name == "PLAN").expect("PLAN listed");
|
||||
let plan = app.xref_manager.entries.iter().find(|e| e.name == "PLAN").expect("PLAN listed").clone();
|
||||
let plan_id = XrefRowId::of(plan.key, &plan.saved_path);
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Detach));
|
||||
let _ = app.update(Message::XrefRowOp(plan_id, XrefPaletteOp::Detach));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("PLAN"), "detach output missing, got: {out:?}");
|
||||
assert!(app.tabs[i].scene.document.block_records.get("PLAN").is_none(), "PLAN definition must be gone");
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.name == "SITE").expect("SITE listed");
|
||||
let site = app.xref_manager.entries.iter().find(|e| e.name == "SITE").expect("SITE listed").clone();
|
||||
let site_id = XrefRowId::of(site.key, &site.saved_path);
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Unload));
|
||||
let _ = app.update(Message::XrefRowOp(site_id, XrefPaletteOp::Unload));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("SITE"), "unload output missing, got: {out:?}");
|
||||
assert_eq!(
|
||||
@@ -1906,7 +1938,7 @@ mod tests {
|
||||
use codec::tables::BlockRecord;
|
||||
use crate::app::Message;
|
||||
use crate::io::xref_model::{Pathtype, RefType};
|
||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
||||
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
|
||||
let mut app = fresh();
|
||||
let i = app.active_tab;
|
||||
let mut br = BlockRecord::new("PLAN");
|
||||
@@ -1916,16 +1948,17 @@ mod tests {
|
||||
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
||||
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_rowop_host.dwg"));
|
||||
app.refresh_xref_manager();
|
||||
let idx = app.xref_manager.entries.iter().position(|e| e.name == "PLAN").expect("PLAN listed");
|
||||
let plan = app.xref_manager.entries.iter().find(|e| e.name == "PLAN").expect("PLAN listed").clone();
|
||||
let plan_id = XrefRowId::of(plan.key, &plan.saved_path);
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Overlay));
|
||||
let _ = app.update(Message::XrefRowOp(plan_id.clone(), XrefPaletteOp::Overlay));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("PLAN"), "got: {out:?}");
|
||||
let br = app.tabs[i].scene.document.block_records.get("PLAN").unwrap();
|
||||
assert!(br.flags.is_xref_overlay && !br.flags.is_xref);
|
||||
assert_eq!(app.xref_manager.entries.iter().find(|e| e.name == "PLAN").unwrap().ref_type, RefType::Overlay);
|
||||
let start = app.command_line.history.len();
|
||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Pathtype(Pathtype::None)));
|
||||
let _ = app.update(Message::XrefRowOp(plan_id, XrefPaletteOp::Pathtype(Pathtype::None)));
|
||||
let out = palette_output(&app, start);
|
||||
assert!(out.contains("PLAN"), "got: {out:?}");
|
||||
let br = app.tabs[i].scene.document.block_records.get("PLAN").unwrap();
|
||||
|
||||
+43
-36
@@ -2613,7 +2613,7 @@ impl OpenCADStudio {
|
||||
self.refresh_xref_manager();
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefManagerSelect(index) => {
|
||||
Message::XrefManagerSelect(id) => {
|
||||
use crate::ui::window::xref_manager::SelectExtend;
|
||||
// Normal GUI list behavior off the globally tracked
|
||||
// modifiers (same source as the layer list): plain click
|
||||
@@ -2625,12 +2625,12 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
SelectExtend::Single
|
||||
};
|
||||
self.xref_manager.click_select(index, extend);
|
||||
self.xref_manager.click_select(&id, extend);
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefRowRightClick(index) => {
|
||||
Message::XrefRowRightClick(id) => {
|
||||
self.xref_manager.row_change_path_open = false;
|
||||
self.xref_manager.right_click_select(index);
|
||||
self.xref_manager.right_click_select(&id);
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefManagerToggleTree => {
|
||||
@@ -2775,62 +2775,69 @@ impl OpenCADStudio {
|
||||
self.xref_manager_op(op);
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefRowOp(index, op) => {
|
||||
Message::XrefRowOp(id, op) => {
|
||||
// Open is not a palette op — it navigates to the file.
|
||||
if op == crate::ui::window::xref_manager::XrefPaletteOp::Open {
|
||||
self.xref_manager.right_click_select(index);
|
||||
self.xref_manager.right_click_select(&id);
|
||||
self.xref_manager.row_change_path_open = false;
|
||||
if let Some(entry) = self.xref_manager.entries.get(index) {
|
||||
if let Some(found) = entry.found_at.clone() {
|
||||
let is_dwg = found.to_ascii_lowercase().ends_with(".dwg")
|
||||
|| found.to_ascii_lowercase().ends_with(".dxf");
|
||||
self.command_line.push_output(crate::tf!("XOPEN: opening \"{}\".", found).as_ref());
|
||||
if is_dwg {
|
||||
return self.update(Message::OpenRecent(std::path::PathBuf::from(found)));
|
||||
if let Some(index) = self.xref_manager.row_index(&id) {
|
||||
if let Some(entry) = self.xref_manager.entries.get(index) {
|
||||
if let Some(found) = entry.found_at.clone() {
|
||||
let is_dwg = found.to_ascii_lowercase().ends_with(".dwg")
|
||||
|| found.to_ascii_lowercase().ends_with(".dxf");
|
||||
self.command_line.push_output(crate::tf!("XOPEN: opening \"{}\".", found).as_ref());
|
||||
if is_dwg {
|
||||
return self.update(Message::OpenRecent(std::path::PathBuf::from(found)));
|
||||
} else {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = open::that_detached(&found);
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = open::that_detached(&found);
|
||||
self.command_line.push_error(
|
||||
crate::t!("File not found — check the saved path.").as_ref(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("File not found — check the saved path.").as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
// Attach prompts the file dialog to pick a drawing to attach.
|
||||
if matches!(op, crate::ui::window::xref_manager::XrefPaletteOp::Attach) {
|
||||
self.xref_manager.right_click_select(index);
|
||||
self.xref_manager.right_click_select(&id);
|
||||
self.xref_manager.row_change_path_open = false;
|
||||
self.command_line.push_output(crate::t!("XATTACH").as_ref());
|
||||
return self.update(Message::XAttachPick);
|
||||
}
|
||||
// Row-scoped op: select the row first, then run the op.
|
||||
self.xref_manager.right_click_select(index);
|
||||
self.xref_manager.right_click_select(&id);
|
||||
self.xref_manager.row_change_path_open = false;
|
||||
self.xref_manager_op(op);
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefRowPathPick(index) => {
|
||||
self.xref_manager.right_click_select(index);
|
||||
Message::XrefRowPathPick(id) => {
|
||||
self.xref_manager.right_click_select(&id);
|
||||
self.xref_manager.row_change_path_open = false;
|
||||
self.update(Message::XrefPathPick)
|
||||
}
|
||||
Message::XrefRowFindReplacePrompt(index) => {
|
||||
self.xref_manager.right_click_select(index);
|
||||
Message::XrefRowFindReplacePrompt(id) => {
|
||||
self.xref_manager.right_click_select(&id);
|
||||
self.xref_manager.row_change_path_open = false;
|
||||
let prefill = if let Some(entry) = self.xref_manager.entries.get(index) {
|
||||
let saved = &entry.saved_path;
|
||||
if let Some(parent) = std::path::Path::new(saved).parent().and_then(|p| p.to_str()) {
|
||||
if !parent.is_empty() {
|
||||
format!("XREF Path Find \"{}\" ", parent)
|
||||
} else {
|
||||
"XREF Path Find ".to_string()
|
||||
}
|
||||
} else {
|
||||
"XREF Path Find ".to_string()
|
||||
}
|
||||
let prefill = if let Some(index) = self.xref_manager.row_index(&id) {
|
||||
self.xref_manager.entries.get(index).map_or_else(
|
||||
|| "XREF Path Find ".to_string(),
|
||||
|entry| {
|
||||
let saved = &entry.saved_path;
|
||||
if let Some(parent) = std::path::Path::new(saved).parent().and_then(|p| p.to_str()) {
|
||||
if !parent.is_empty() {
|
||||
format!("XREF Path Find \"{}\" ", parent)
|
||||
} else {
|
||||
"XREF Path Find ".to_string()
|
||||
}
|
||||
} else {
|
||||
"XREF Path Find ".to_string()
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
"XREF Path Find ".to_string()
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user