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()
|
||||
};
|
||||
|
||||
@@ -196,6 +196,25 @@ pub enum XrefPaletteOp {
|
||||
Pathtype(Pathtype),
|
||||
}
|
||||
|
||||
/// Stable row identity carried by row messages instead of the positional
|
||||
/// display index (#49): a rescan/reorder between view and dispatch must not
|
||||
/// retarget the row. Same `(key, saved_path)` pair `refresh` uses to preserve
|
||||
/// selection (key alone can collide across host/nested scopes).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct XrefRowId {
|
||||
pub key: u64,
|
||||
pub saved_path: String,
|
||||
}
|
||||
|
||||
impl XrefRowId {
|
||||
pub fn of(key: u64, saved_path: &str) -> Self {
|
||||
Self {
|
||||
key,
|
||||
saved_path: saved_path.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl XrefManagerPanel {
|
||||
/// Rebuild `entries` from `doc` via [`collect_entries_with_prev`] and
|
||||
/// recompute the parent → children linkage for tree mode. Selection/anchor
|
||||
@@ -310,13 +329,24 @@ impl XrefManagerPanel {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolve a row id to its current display index (entries reorder on
|
||||
/// every rescan). `None` = row gone meanwhile; callers no-op (#49).
|
||||
pub fn row_index(&self, id: &XrefRowId) -> Option<usize> {
|
||||
self.entries
|
||||
.iter()
|
||||
.position(|e| e.key == id.key && e.saved_path == id.saved_path)
|
||||
}
|
||||
|
||||
/// Click handling for row picks with normal GUI list semantics:
|
||||
/// - plain click selects exactly one row (previous selection clears);
|
||||
/// - Ctrl/Cmd-click toggles one row;
|
||||
/// - Shift-click extends a contiguous range from the anchor (or the
|
||||
/// clicked row when there is no anchor yet).
|
||||
/// Tree mode always single-selects per spec, whatever the modifiers.
|
||||
pub fn click_select(&mut self, index: usize, extend: SelectExtend) {
|
||||
pub fn click_select(&mut self, id: &XrefRowId, extend: SelectExtend) {
|
||||
let Some(index) = self.row_index(id) else {
|
||||
return;
|
||||
};
|
||||
if index == HOST_ROW || index >= self.entries.len() {
|
||||
return;
|
||||
}
|
||||
@@ -365,19 +395,29 @@ impl XrefManagerPanel {
|
||||
/// Legacy toggle entry point: Ctrl-style toggle in list mode, single
|
||||
/// select in tree mode. Kept for tests and non-pointer callers.
|
||||
pub fn toggle_select(&mut self, index: usize) {
|
||||
self.click_select(index, SelectExtend::Toggle);
|
||||
let Some(id) = self
|
||||
.entries
|
||||
.get(index)
|
||||
.map(|e| XrefRowId::of(e.key, &e.saved_path))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.click_select(&id, SelectExtend::Toggle);
|
||||
}
|
||||
|
||||
/// Right-click selection: select the row alone when it is not already
|
||||
/// selected (standard right-click behavior — the menu then acts on the
|
||||
/// selection, which now includes this row). Keeps multi-selections when
|
||||
/// right-clicking inside them.
|
||||
pub fn right_click_select(&mut self, index: usize) {
|
||||
pub fn right_click_select(&mut self, id: &XrefRowId) {
|
||||
let Some(index) = self.row_index(id) else {
|
||||
return;
|
||||
};
|
||||
if index == HOST_ROW || index >= self.entries.len() {
|
||||
return;
|
||||
}
|
||||
if !self.selected.contains(&index) {
|
||||
self.click_select(index, SelectExtend::Single);
|
||||
self.click_select(id, SelectExtend::Single);
|
||||
} else {
|
||||
self.ensure_preview_for_anchor();
|
||||
}
|
||||
@@ -1627,14 +1667,14 @@ fn menu_separator() -> Element<'static, Message> {
|
||||
/// path tools. No Bind, no Attach/Overlay — those are DWG-only and
|
||||
/// hidden entirely. Change Path Type shows a right triangle and a
|
||||
/// second list adjacent to "Change Path Type" on hover.
|
||||
fn row_menu_for(index: usize, change_path_open: bool) -> Element<'static, Message> {
|
||||
fn row_menu_for(id: XrefRowId, change_path_open: bool) -> Element<'static, Message> {
|
||||
let item = |label: &str, op: XrefPaletteOp| -> Element<'static, Message> {
|
||||
menu_row(label.to_string(), Message::XrefRowOp(index, op))
|
||||
menu_row(label.to_string(), Message::XrefRowOp(id.clone(), op))
|
||||
};
|
||||
let pathtype_item = |label: &str, pt: Pathtype| -> Element<'static, Message> {
|
||||
submenu_row(
|
||||
label.to_string(),
|
||||
Message::XrefRowOp(index, XrefPaletteOp::Pathtype(pt)),
|
||||
Message::XrefRowOp(id.clone(), XrefPaletteOp::Pathtype(pt)),
|
||||
)
|
||||
};
|
||||
let change_path_row: Element<'static, Message> = mouse_area(
|
||||
@@ -1681,11 +1721,11 @@ fn row_menu_for(index: usize, change_path_open: bool) -> Element<'static, Messag
|
||||
menu_separator(),
|
||||
menu_row(
|
||||
crate::t!("Select New Path").into_owned(),
|
||||
Message::XrefRowPathPick(index),
|
||||
Message::XrefRowPathPick(id.clone()),
|
||||
),
|
||||
menu_row(
|
||||
crate::t!("Find and Replace...").into_owned(),
|
||||
Message::XrefRowFindReplacePrompt(index),
|
||||
Message::XrefRowFindReplacePrompt(id.clone()),
|
||||
),
|
||||
]
|
||||
.spacing(MENU_SPACING)
|
||||
@@ -1821,6 +1861,8 @@ fn xref_row<'a>(
|
||||
.align_y(iced::Center);
|
||||
|
||||
let index = display.index;
|
||||
// #49: messages carry stable identity; the index stays for zebra striping only.
|
||||
let row_id = XrefRowId::of(entry.key, &entry.saved_path);
|
||||
let row = mouse_area(
|
||||
container(content)
|
||||
.style(move |theme: &Theme| {
|
||||
@@ -1847,9 +1889,12 @@ fn xref_row<'a>(
|
||||
.height(Length::Fixed(ROW_H))
|
||||
.width(Fill),
|
||||
)
|
||||
.on_press(Message::XrefManagerSelect(index));
|
||||
let cm = iced_aw::ContextMenu::new(row, move || row_menu_for(index, change_path_open));
|
||||
RightClickArea::new(cm, Message::XrefRowRightClick(index)).into()
|
||||
.on_press(Message::XrefManagerSelect(row_id.clone()));
|
||||
let cm = iced_aw::ContextMenu::new(row, {
|
||||
let row_id = row_id.clone();
|
||||
move || row_menu_for(row_id.clone(), change_path_open)
|
||||
});
|
||||
RightClickArea::new(cm, Message::XrefRowRightClick(row_id)).into()
|
||||
}
|
||||
|
||||
/// One tree-mode row: indent + optional expand arrow + file icon + name.
|
||||
@@ -1883,6 +1928,8 @@ fn tree_row(
|
||||
);
|
||||
cells = cells.push(text(entry.name.clone()).size(TREE_FONT_SZ));
|
||||
let index = display.index;
|
||||
// #49: messages carry stable identity; the index stays for zebra striping only.
|
||||
let row_id = XrefRowId::of(entry.key, &entry.saved_path);
|
||||
let row = mouse_area(
|
||||
container(cells.spacing(4).width(Fill))
|
||||
.style(move |theme: &Theme| {
|
||||
@@ -1908,9 +1955,12 @@ fn tree_row(
|
||||
})
|
||||
.width(Fill),
|
||||
)
|
||||
.on_press(Message::XrefManagerSelect(index));
|
||||
let cm = iced_aw::ContextMenu::new(row, move || row_menu_for(index, change_path_open));
|
||||
RightClickArea::new(cm, Message::XrefRowRightClick(index)).into()
|
||||
.on_press(Message::XrefManagerSelect(row_id.clone()));
|
||||
let cm = iced_aw::ContextMenu::new(row, {
|
||||
let row_id = row_id.clone();
|
||||
move || row_menu_for(row_id.clone(), change_path_open)
|
||||
});
|
||||
RightClickArea::new(cm, Message::XrefRowRightClick(row_id)).into()
|
||||
}
|
||||
|
||||
/// Tree-mode host root: home icon + name with the current-drawing marker.
|
||||
@@ -2344,6 +2394,8 @@ mod tests {
|
||||
#[test]
|
||||
fn click_select_single_then_range() {
|
||||
use super::SelectExtend;
|
||||
use super::XrefRowId;
|
||||
let id = |key: u64, saved: &str| XrefRowId::of(key, saved);
|
||||
let mut panel = XrefManagerPanel::default();
|
||||
panel.entries = vec![
|
||||
entry(1, "A", "a.dwg"),
|
||||
@@ -2351,27 +2403,27 @@ mod tests {
|
||||
entry(3, "C", "c.dwg"),
|
||||
];
|
||||
// Plain click selects exactly one row.
|
||||
panel.click_select(0, SelectExtend::Single);
|
||||
panel.click_select(2, SelectExtend::Single);
|
||||
panel.click_select(&id(1, "a.dwg"), SelectExtend::Single);
|
||||
panel.click_select(&id(3, "c.dwg"), SelectExtend::Single);
|
||||
assert_eq!(panel.selected, HashSet::from([2]));
|
||||
assert_eq!(panel.anchor, Some(2));
|
||||
// Shift-click extends a contiguous range from the anchor.
|
||||
panel.click_select(2, SelectExtend::Single);
|
||||
panel.click_select(0, SelectExtend::Range);
|
||||
panel.click_select(&id(3, "c.dwg"), SelectExtend::Single);
|
||||
panel.click_select(&id(1, "a.dwg"), SelectExtend::Range);
|
||||
assert_eq!(panel.selected, HashSet::from([0, 1, 2]));
|
||||
// Repeated extends keep the original anchor — rows are never lost.
|
||||
panel.entries.push(entry(4, "D", "d.dwg"));
|
||||
panel.entries.push(entry(5, "E", "e.dwg"));
|
||||
panel.click_select(4, SelectExtend::Range);
|
||||
panel.click_select(&id(5, "e.dwg"), SelectExtend::Range);
|
||||
assert_eq!(panel.selected, HashSet::from([2, 3, 4]));
|
||||
assert_eq!(panel.anchor, Some(2));
|
||||
// Ctrl-click toggles one row.
|
||||
panel.click_select(3, SelectExtend::Toggle);
|
||||
panel.click_select(&id(4, "d.dwg"), SelectExtend::Toggle);
|
||||
assert_eq!(panel.selected, HashSet::from([2, 4]));
|
||||
// Range with no anchor starts at the clicked row.
|
||||
let mut fresh = XrefManagerPanel::default();
|
||||
fresh.entries = vec![entry(1, "A", "a.dwg"), entry(2, "B", "b.dwg")];
|
||||
fresh.click_select(1, SelectExtend::Range);
|
||||
fresh.click_select(&id(2, "b.dwg"), SelectExtend::Range);
|
||||
assert_eq!(fresh.selected, HashSet::from([1]));
|
||||
}
|
||||
|
||||
@@ -2570,9 +2622,10 @@ mod tests {
|
||||
/// opens it with a synthetic right-click on the underlay, mirroring a
|
||||
/// user right-clicking a reference row.
|
||||
fn open_row_menu(change_path_open: bool) -> MenuSimulator {
|
||||
let row_id = XrefRowId::of(0, "");
|
||||
let mut ui = iced_test::simulator(iced_aw::ContextMenu::new(
|
||||
text("anchor"),
|
||||
move || row_menu_for(0, change_path_open),
|
||||
move || row_menu_for(row_id.clone(), change_path_open),
|
||||
));
|
||||
let anchor = ui.find("anchor").expect("anchor underlay present");
|
||||
let center = anchor
|
||||
@@ -2586,11 +2639,11 @@ mod tests {
|
||||
ui
|
||||
}
|
||||
|
||||
fn published_ops(ui: MenuSimulator) -> Vec<(usize, XrefPaletteOp)> {
|
||||
fn published_ops(ui: MenuSimulator) -> Vec<(XrefRowId, XrefPaletteOp)> {
|
||||
let mut ops = Vec::new();
|
||||
for message in ui.into_messages() {
|
||||
if let Message::XrefRowOp(index, op) = message {
|
||||
ops.push((index, op));
|
||||
if let Message::XrefRowOp(id, op) = message {
|
||||
ops.push((id, op));
|
||||
}
|
||||
}
|
||||
ops
|
||||
@@ -2603,8 +2656,8 @@ mod tests {
|
||||
ui.click(unload.as_str())
|
||||
.expect("Unload row is clickable inside the overlay");
|
||||
assert!(
|
||||
published_ops(ui).contains(&(0, XrefPaletteOp::Unload)),
|
||||
"clicking Unload must publish XrefRowOp(0, Unload)"
|
||||
published_ops(ui).contains(&(XrefRowId::of(0, ""), XrefPaletteOp::Unload)),
|
||||
"clicking Unload must publish XrefRowOp(row, Unload)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2643,7 +2696,7 @@ mod tests {
|
||||
ui.click(make_absolute.as_str())
|
||||
.expect("flyout row is clickable inside the overlay");
|
||||
assert!(
|
||||
published_ops(ui).contains(&(0, XrefPaletteOp::Pathtype(Pathtype::Full))),
|
||||
published_ops(ui).contains(&(XrefRowId::of(0, ""), XrefPaletteOp::Pathtype(Pathtype::Full))),
|
||||
"clicking Make Absolute must publish the Full-pathtype op"
|
||||
);
|
||||
}
|
||||
@@ -2671,7 +2724,7 @@ mod tests {
|
||||
// buttons again.
|
||||
use iced_core::Widget;
|
||||
|
||||
let mut menu = iced_aw::ContextMenu::new(text("anchor"), || row_menu_for(0, false));
|
||||
let mut menu = iced_aw::ContextMenu::new(text("anchor"), || row_menu_for(XrefRowId::of(0, ""), false));
|
||||
let mut tree = iced_core::widget::Tree::new(
|
||||
&menu as &dyn Widget<Message, iced::Theme, iced::Renderer>,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user