From f3392ddbf6a5c59f83eeee7694f084b0d0554228 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sun, 31 May 2026 12:49:14 +0800 Subject: [PATCH] phase 2: VM + TreeView skeleton for alias tree - AliasNodeViewModel: INotifyPropertyChanged, alias/display/editing state - AliasTreeIcons.xaml: vector icon geometry + toolbar/button styles - AliasTreeControl.xaml: TreeView with icon toolbar, status bar, context menu - AliasTreeControl.xaml.cs: tree building, lazy loading, bidir selection sync, inline editing, alias highlight (via ModelHighlightHelper) - Both projects compile successfully --- TransportPlugin.csproj | 12 + src/UI/WPF/Resources/AliasTreeIcons.xaml | 136 ++++ src/UI/WPF/ViewModels/AliasNodeViewModel.cs | 108 +++ src/UI/WPF/Views/AliasTreeControl.xaml | 213 ++++++ src/UI/WPF/Views/AliasTreeControl.xaml.cs | 716 ++++++++++++++++++++ 5 files changed, 1185 insertions(+) create mode 100644 src/UI/WPF/Resources/AliasTreeIcons.xaml create mode 100644 src/UI/WPF/ViewModels/AliasNodeViewModel.cs create mode 100644 src/UI/WPF/Views/AliasTreeControl.xaml create mode 100644 src/UI/WPF/Views/AliasTreeControl.xaml.cs diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index 63376ee..3a7251b 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -218,6 +218,9 @@ LogisticsControlPanel.xaml + + AliasTreeControl.xaml + ModelSettingsView.xaml @@ -288,6 +291,7 @@ + @@ -391,6 +395,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -508,6 +516,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + diff --git a/src/UI/WPF/Resources/AliasTreeIcons.xaml b/src/UI/WPF/Resources/AliasTreeIcons.xaml new file mode 100644 index 0000000..aee4b81 --- /dev/null +++ b/src/UI/WPF/Resources/AliasTreeIcons.xaml @@ -0,0 +1,136 @@ + + + + + + + M15,9H5V5H15M15,19H5V15H15M17,3H3V21H17V16L21,12L17,8V3Z + + + + + M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z + + + + + M5,10H19V12H5M12,5L5,12H9V21H15V12H19L12,5Z + + + + + M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12 + A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12 + C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 + 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.97 19.05,5.05L16.56,6.05 + C16.04,5.65 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2 + H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.65 7.44,6.05 + L4.95,5.05C4.73,4.97 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 + 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 + 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73 + C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.95C7.96,18.35 8.5,18.68 + 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 + 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.35 16.56,17.95L19.05,18.95 + C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 + 21.54,14.63L19.43,12.97Z + + + + + M6,19V7H18V19H6M9,4H15L14.5,3H9.5L9,4M19,5V7H5V5H8L8.5,4H15.5L16,5H19Z + + + + + M3,17.25V21H6.75L17.81,9.94L14.06,6.19L3,17.25ZM20.71,7.04C21.1,6.65 + 21.1,6.02 20.71,5.63L18.37,3.29C17.98,2.9 17.35,2.9 16.96,3.29 + L15.13,5.12L18.88,8.87L20.71,7.04Z + + + + + M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 + 22,12C22,6.48 17.52,2 12,2ZM12,20C7.59,20 4,16.41 4,12C4,7.59 + 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20ZM12,9 + C10.34,9 9,10.34 9,12C9,13.66 10.34,15 12,15C13.66,15 15,13.66 + 15,12C15,10.34 13.66,9 12,9Z + + + + + M3,17.25V21H6.75L17.81,9.94L14.06,6.19L3,17.25ZM20.71,7.04C21.1,6.65 + 21.1,6.02 20.71,5.63L18.37,3.29C17.98,2.9 17.35,2.9 16.96,3.29 + L15.13,5.12L18.88,8.87L20.71,7.04Z + + + + + M17.65,6.35C16.2,4.9 14.21,4 12,4C7.58,4 4.01,7.58 4.01,12 + C4.01,16.42 7.58,20 12,20C15.73,20 18.84,17.45 19.73,14H17.65 + C16.83,16.33 14.61,18 12,18C8.69,18 6,15.31 6,12C6,8.69 8.69,6 + 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z + + + + + + + + + + + + + + + diff --git a/src/UI/WPF/ViewModels/AliasNodeViewModel.cs b/src/UI/WPF/ViewModels/AliasNodeViewModel.cs new file mode 100644 index 0000000..fd02a01 --- /dev/null +++ b/src/UI/WPF/ViewModels/AliasNodeViewModel.cs @@ -0,0 +1,108 @@ +using System; +using System.ComponentModel; + +namespace NavisworksTransport.UI.WPF.ViewModels +{ + /// + /// 别名树单节点 ViewModel。 + /// 支持 INotifyPropertyChanged 以驱动 TreeView 实时更新。 + /// + public class AliasNodeViewModel : INotifyPropertyChanged + { + private string _displayName; + private string _originalName; + private string _alias; + private bool _isEditing; + private bool _isOrphan; + private bool _hasAlias; + private int _childCount; + + /// 节点的序列化标识 key(对应 AliasNodeIdentity.ToKey()) + public string NodeKey { get; set; } + + /// 当前显示文本(别名优先,无别名回退到原名) + public string DisplayName + { + get => _displayName; + set { _displayName = value; OnPropertyChanged(nameof(DisplayName)); } + } + + /// Navisworks 原始名称(只读) + public string OriginalName + { + get => _originalName; + set { _originalName = value; OnPropertyChanged(nameof(OriginalName)); } + } + + /// 用户别名 + public string Alias + { + get => _alias; + set + { + _alias = value; + HasAlias = !string.IsNullOrEmpty(value); + // 更新显示文本 + DisplayName = HasAlias ? value : (OriginalName ?? "?"); + OnPropertyChanged(nameof(Alias)); + } + } + + /// 是否处于就地编辑状态 + public bool IsEditing + { + get => _isEditing; + set { _isEditing = value; OnPropertyChanged(nameof(IsEditing)); } + } + + /// 是否孤儿节点(模型结构变化后无法定位) + public bool IsOrphan + { + get => _isOrphan; + set { _isOrphan = value; OnPropertyChanged(nameof(IsOrphan)); } + } + + /// 是否有别名 + public bool HasAlias + { + get => _hasAlias; + set { _hasAlias = value; OnPropertyChanged(nameof(HasAlias)); } + } + + /// 子节点数量(用于展开箭头) + public int ChildCount + { + get => _childCount; + set { _childCount = value; OnPropertyChanged(nameof(ChildCount)); OnPropertyChanged(nameof(HasChildren)); } + } + + /// 是否有子节点 + public bool HasChildren => ChildCount > 0; + + /// 子节点集合 + public System.Collections.ObjectModel.ObservableCollection Children { get; } + = new System.Collections.ObjectModel.ObservableCollection(); + + /// 父节点引用(用于回溯路径) + public AliasNodeViewModel Parent { get; set; } + + public AliasNodeViewModel() + { + } + + /// + /// 刷新显示文本(别名变更新调用) + /// + public void RefreshDisplay() + { + DisplayName = HasAlias ? Alias : (OriginalName ?? "?"); + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/UI/WPF/Views/AliasTreeControl.xaml b/src/UI/WPF/Views/AliasTreeControl.xaml new file mode 100644 index 0000000..be35ca8 --- /dev/null +++ b/src/UI/WPF/Views/AliasTreeControl.xaml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UI/WPF/Views/AliasTreeControl.xaml.cs b/src/UI/WPF/Views/AliasTreeControl.xaml.cs new file mode 100644 index 0000000..a4c7bd7 --- /dev/null +++ b/src/UI/WPF/Views/AliasTreeControl.xaml.cs @@ -0,0 +1,716 @@ +using NavisworksTransport.Core.AliasTree; +using NavisworksTransport.UI.WPF.ViewModels; +using NavisworksTransport.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using NavisApplication = Autodesk.Navisworks.Api.Application; + +namespace NavisworksTransport.UI.WPF.Views +{ + /// + /// 别名导航树面板 — 核心 UI 逻辑 + /// + public partial class AliasTreeControl : UserControl + { + // ============================================================ + // 状态 + // ============================================================ + + private AliasDataStore _dataStore; + private Dictionary _nodeMap = new Dictionary(); + private bool _isInternalSelection = false; + private Autodesk.Navisworks.Api.ModelItem _currentSelectionItem; + private bool _built = false; + + // ============================================================ + // 生命周期 + // ============================================================ + + public AliasTreeControl() + { + InitializeComponent(); + + // 将 AliasTreeView 的 items 源指向根节点集合 + var rootNodes = new System.Collections.ObjectModel.ObservableCollection(); + AliasTreeView.ItemsSource = rootNodes; + + // 文档切换事件 + NavisApplication.ActiveDocumentChanged += (s, e) => RebuildTree(); + NavisApplication.ActiveDocumentChanging += (s, e) => { _built = false; }; + + HookSelectionEvents(); + } + + /// + /// 设置数据存储(由外部在 PathDatabase 就绪后调用) + /// + public void SetDataStore(AliasDataStore store) + { + _dataStore = store; + RebuildTree(); + } + + /// + /// 清理资源 + /// + public void Cleanup() + { + UnhookSelectionEvents(); + _dataStore = null; + _nodeMap.Clear(); + } + + // ============================================================ + // 选择事件(双向联动) + // ============================================================ + + private bool _selectionHooked = false; + + private void HookSelectionEvents() + { + if (_selectionHooked) return; + try + { + var doc = NavisApplication.ActiveDocument; + if (doc != null) + { + doc.CurrentSelection.Changed += OnNavisSelectionChanged; + _selectionHooked = true; + } + } + catch { /* 文档未就绪,跳过 */ } + } + + private void UnhookSelectionEvents() + { + try + { + var doc = NavisApplication.ActiveDocument; + if (doc != null) + { + doc.CurrentSelection.Changed -= OnNavisSelectionChanged; + } + } + catch { } + _selectionHooked = false; + } + + /// + /// 别名树选中 → 同步到 Navisworks 选择 + /// + private void OnTreeViewSelected(object sender, RoutedEventArgs e) + { + if (!(e.OriginalSource is TreeViewItem tvi) || !(tvi.DataContext is AliasNodeViewModel node)) + return; + if (node.IsOrphan) return; // 孤儿节点不允许选中 + + _isInternalSelection = true; + try + { + var doc = NavisApplication.ActiveDocument; + if (doc != null) + { + var coll = new Autodesk.Navisworks.Api.ModelItemCollection(); + var item = FindModelItemByNodeKey(node.NodeKey); + if (item != null) + { + coll.Add(item); + doc.CurrentSelection.Clear(); + doc.CurrentSelection.CopyFrom(coll); + } + } + } + catch (Exception ex) + { + LogManager.Error($"[别名树] 选中同步失败: {ex.Message}"); + } + finally + { + _isInternalSelection = false; + } + } + + /// + /// Navisworks 选择变化 → 同步到别名树高亮 + /// + private void OnNavisSelectionChanged(object sender, EventArgs e) + { + if (_isInternalSelection) return; + + Dispatcher.BeginInvoke(new Action(() => + { + try + { + var doc = NavisApplication.ActiveDocument; + if (doc == null) return; + + var selected = doc.CurrentSelection.SelectedItems; + if (selected.Count == 1) + { + var item = selected.First(); + _currentSelectionItem = item; + TbCurrentNode.Text = $"当前: {item.DisplayName}"; + BtnSave.IsEnabled = true; + + // 在树中展开并高亮 + ExpandAndSelect(item); + } + else + { + _currentSelectionItem = null; + TbCurrentNode.Text = selected.Count > 1 ? $"当前: ({selected.Count} 个选中)" : "当前: 无"; + BtnSave.IsEnabled = false; + } + } + catch (Exception ex) + { + LogManager.Error($"[别名树] 选择变化处理失败: {ex.Message}"); + } + })); + } + + // ============================================================ + // 树构建 + // ============================================================ + + private void RebuildTree() + { + if (_built) return; + _built = true; + + try + { + var rootNodes = AliasTreeView.ItemsSource as System.Collections.ObjectModel.ObservableCollection; + if (rootNodes == null) return; + + rootNodes.Clear(); + _nodeMap.Clear(); + + var doc = NavisApplication.ActiveDocument; + if (doc == null || doc.Models == null) return; + + // 全量加载别名到内存 + Dictionary aliasMap = null; + if (_dataStore != null) + aliasMap = _dataStore.LoadAll(); + + // 遍历所有 root items + foreach (var model in doc.Models) + { + var rootItem = model.RootItem; + if (rootItem != null) + { + var node = BuildNode(rootItem, aliasMap); + if (node != null) + rootNodes.Add(node); + } + } + + // 重新挂载选择事件(可能文档刚切换) + HookSelectionEvents(); + + LogManager.Info($"[别名树] 树构建完成,共 {_nodeMap.Count} 个节点"); + } + catch (Exception ex) + { + LogManager.Error($"[别名树] 构建失败: {ex.Message}"); + } + } + + /// + /// 递归构建节点树(主线程调用) + /// + private AliasNodeViewModel BuildNode(Autodesk.Navisworks.Api.ModelItem modelItem, + Dictionary aliasMap) + { + if (modelItem == null) return null; + + var identity = AliasNodeIdentity.FromModelItem(modelItem); + string key = identity.ToKey(); + + string alias = null; + aliasMap?.TryGetValue(key, out alias); + + var node = new AliasNodeViewModel + { + NodeKey = key, + OriginalName = modelItem.DisplayName ?? "?", + Alias = alias, + ChildCount = modelItem.Children.Count() + }; + + _nodeMap[key] = node; + + // 子节点(第一层不懒加载,展开时在 Expanded 事件中再填充) + // 但先加一个占位节点让 TreeView 显示展开箭头 + if (node.ChildCount > 0) + { + node.Children.Add(new AliasNodeViewModel + { + DisplayName = "加载中...", + OriginalName = "加载中..." + }); + } + + return node; + } + + /// + /// 懒加载子节点(在 Expanded 事件中触发) + /// + private void LazyLoadChildren(AliasNodeViewModel parentNode, Dictionary aliasMap) + { + // 检查是否只有占位节点 + if (parentNode.Children.Count != 1 || parentNode.Children[0].OriginalName != "加载中...") + return; + + parentNode.Children.Clear(); + + try + { + var doc = NavisApplication.ActiveDocument; + if (doc == null) return; + + // 从 nodeKey 反推出 ModelItem + var item = FindModelItemByNodeKey(parentNode.NodeKey); + if (item == null) return; + + foreach (var childItem in item.Children) + { + var childNode = BuildNode(childItem, aliasMap); + if (childNode != null) + { + childNode.Parent = parentNode; + parentNode.Children.Add(childNode); + } + } + } + catch (Exception ex) + { + LogManager.Error($"[别名树] 懒加载子节点失败: {ex.Message}"); + } + } + + // ============================================================ + // 从 NodeKey 回溯定位 ModelItem + // ============================================================ + + private Autodesk.Navisworks.Api.ModelItem FindModelItemByNodeKey(string key) + { + if (!AliasNodeIdentity.TryParseKey(key, out var identity)) + return null; + + try + { + var doc = NavisApplication.ActiveDocument; + if (doc == null) return null; + + // 沿 HierarchicalPath 匹配 DisplayName + string[] parts = identity.HierarchicalPath.Split('/'); + // parts[0] 是模型名/根节点名 + // 遍历 Models 找匹配的根 + foreach (var model in doc.Models) + { + var current = model.RootItem; + if (current == null) continue; + + // 检查根节点 + if (parts.Length > 0 && current.DisplayName != parts[0]) + continue; + + for (int i = 1; i < parts.Length; i++) + { + string targetName = parts[i]; + Autodesk.Navisworks.Api.ModelItem matchedChild = null; + foreach (var child in current.Children) + { + if (child.DisplayName == targetName) + { + matchedChild = child; + break; + } + } + if (matchedChild == null) return null; + current = matchedChild; + } + + // 到了叶子层级,用 SiblingIndex 在同名兄弟中定位 + if (identity.SiblingIndex > 0) + { + int idx = 0; + foreach (var child in current.Children) + { + if (child.DisplayName == parts.Last()) + { + if (idx == identity.SiblingIndex) + return child; + idx++; + } + } + return null; + } + + return current; + } + } + catch { } + + return null; + } + + // ============================================================ + // 展开并高亮指定 ModelItem + // ============================================================ + + private void ExpandAndSelect(Autodesk.Navisworks.Api.ModelItem item) + { + var identity = AliasNodeIdentity.FromModelItem(item); + string key = identity.ToKey(); + + if (!_nodeMap.TryGetValue(key, out var targetNode)) + return; + + // 收集从根到目标的路径 + var path = new List(); + var current = targetNode; + while (current != null) + { + path.Insert(0, current); + current = current.Parent; + } + + // 逐层展开 + ItemsControl currentContainer = AliasTreeView; + foreach (var node in path) + { + // 触发懒加载 + var aliasMap = _dataStore?.LoadAll(); + LazyLoadChildren(node, aliasMap); + + if (currentContainer is TreeViewItem tvi) + { + tvi.IsExpanded = true; + tvi.UpdateLayout(); + } + + var container = currentContainer.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem; + if (container == null) break; + currentContainer = container; + } + + // 高亮目标 + if (currentContainer is TreeViewItem targetTvi && targetTvi.DataContext is AliasNodeViewModel target + && target.NodeKey == key) + { + targetTvi.IsSelected = true; + targetTvi.BringIntoView(); + } + } + + // ============================================================ + // 事件处理:工具栏 + // ============================================================ + + private void OnSaveCurrentClick(object sender, RoutedEventArgs e) + { + if (_currentSelectionItem == null || _dataStore == null) return; + string input = TxtAliasInput.Text?.Trim(); + if (string.IsNullOrEmpty(input)) return; + + var identity = AliasNodeIdentity.FromModelItem(_currentSelectionItem); + _dataStore.SetAlias(identity, input); + TxtAliasInput.Clear(); + + // 刷新树中对应节点 + RefreshNodeDisplay(identity.ToKey()); + } + + private void OnAliasInputKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter && BtnSave.IsEnabled) + OnSaveCurrentClick(null, null); + } + + private void OnAutoGenClick(object sender, RoutedEventArgs e) + { + // 阶段 3 实现 + SetStatus("自动生成尚未实现"); + } + + private void OnExportClick(object sender, RoutedEventArgs e) + { + // 阶段 5 实现 + SetStatus("导出尚未实现"); + } + + private void OnImportClick(object sender, RoutedEventArgs e) + { + // 阶段 5 实现 + SetStatus("导入尚未实现"); + } + + private void OnClearAllClick(object sender, RoutedEventArgs e) + { + if (_dataStore == null) return; + + var result = System.Windows.MessageBox.Show( + "确定要清除全部别名吗?此操作不可撤销。", + "清除全部别名", + MessageBoxButton.YesNo, + MessageBoxImage.Warning); + + if (result == MessageBoxResult.Yes) + { + _dataStore.ClearAll(); + RebuildTree(); + SetStatus("已清除全部别名"); + } + } + + private void OnRefreshClick(object sender, RoutedEventArgs e) + { + _built = false; + RebuildTree(); + SetStatus("树已刷新"); + } + + // ============================================================ + // 事件处理:内联编辑 + // ============================================================ + + private void OnEditClick(object sender, RoutedEventArgs e) + { + if (!(sender is Button btn) || !(btn.Tag is AliasNodeViewModel node)) + return; + + // 清除所有编辑状态 + ClearAllEditing(); + node.IsEditing = true; + + Dispatcher.BeginInvoke(new Action(() => + { + var container = AliasTreeView.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem; + if (container == null) return; + var txt = FindVisualChild(container); + if (txt != null) + { + txt.Focus(); + txt.SelectAll(); + } + }), System.Windows.Threading.DispatcherPriority.Render); + } + + private void OnAliasTextBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + CommitEdit(sender as TextBox); + } + else if (e.Key == Key.Escape) + { + CancelEdit(sender as TextBox); + } + } + + private void OnAliasTextBoxLostFocus(object sender, RoutedEventArgs e) + { + CommitEdit(sender as TextBox); + } + + private void CommitEdit(TextBox textBox) + { + if (textBox?.DataContext is AliasNodeViewModel node) + { + string newAlias = textBox.Text?.Trim(); + if (_dataStore != null) + { + AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity); + _dataStore.SetAlias(identity, newAlias); + } + node.Alias = newAlias; + node.IsEditing = false; + + // 清除别名高亮 / 应用别名高亮 + ApplyAliasHighlight(node.NodeKey, node.HasAlias); + } + } + + private void CancelEdit(TextBox textBox) + { + if (textBox?.DataContext is AliasNodeViewModel node) + { + node.RefreshDisplay(); + node.IsEditing = false; + } + } + + private void ClearAllEditing() + { + foreach (var kvp in _nodeMap) + { + kvp.Value.IsEditing = false; + } + } + + // ============================================================ + // 事件处理:右键菜单 + // ============================================================ + + private AliasNodeViewModel GetSelectedNode() + { + return AliasTreeView.SelectedItem as AliasNodeViewModel; + } + + private void OnCtxEditClick(object sender, RoutedEventArgs e) + { + var node = GetSelectedNode(); + if (node == null) return; + OnEditClick(new Button { Tag = node }, null); + } + + private void OnCtxBatchEditClick(object sender, RoutedEventArgs e) + { + var node = GetSelectedNode(); + if (node == null) return; + + // 阶段 3 实现批量对话框 + SetStatus($"批量编辑 {node.NodeKey} 的子节点 — 尚未实现"); + } + + private void OnCtxClearClick(object sender, RoutedEventArgs e) + { + var node = GetSelectedNode(); + if (node == null || _dataStore == null) return; + + AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity); + _dataStore.DeleteAlias(identity); + node.Alias = null; + ApplyAliasHighlight(node.NodeKey, false); + } + + private void OnCtxFocusClick(object sender, RoutedEventArgs e) + { + var node = GetSelectedNode(); + if (node == null || node.IsOrphan) return; + + var item = FindModelItemByNodeKey(node.NodeKey); + if (item == null) return; + + try + { + var doc = NavisApplication.ActiveDocument; + if (doc == null) return; + + var coll = new Autodesk.Navisworks.Api.ModelItemCollection { item }; + doc.CurrentSelection.Clear(); + doc.CurrentSelection.CopyFrom(coll); + } + catch (Exception ex) + { + LogManager.Error($"[别名树] 聚焦失败: {ex.Message}"); + } + } + + private void OnCtxCopyNameClick(object sender, RoutedEventArgs e) + { + var node = GetSelectedNode(); + if (node == null) return; + try + { + Clipboard.SetText(node.OriginalName); + } + catch { } + } + + // ============================================================ + // 3D 高亮 + // ============================================================ + + private void ApplyAliasHighlight(string nodeKey, bool highlight) + { + var item = FindModelItemByNodeKey(nodeKey); + if (item == null) return; + + var coll = new Autodesk.Navisworks.Api.ModelItemCollection { item }; + + if (highlight) + { + ModelHighlightHelper.HighlightItems("AliasHighlight", coll); + } + else + { + ModelHighlightHelper.ClearCategory("AliasHighlight"); + } + } + + // ============================================================ + // 辅助 + // ============================================================ + + private void RefreshNodeDisplay(string key) + { + if (_nodeMap.TryGetValue(key, out var node)) + { + if (_dataStore != null) + { + AliasNodeIdentity.TryParseKey(key, out var identity); + node.Alias = _dataStore.GetAlias(identity); + } + node.RefreshDisplay(); + } + } + + private void SetStatus(string message) + { + TbStatus.Text = message; + StatusBarItem.Visibility = Visibility.Visible; + // 3 秒后自动隐藏 + var timer = new System.Windows.Threading.DispatcherTimer + { + Interval = TimeSpan.FromSeconds(3) + }; + timer.Tick += (s, e2) => + { + timer.Stop(); + StatusBarItem.Visibility = Visibility.Collapsed; + }; + timer.Start(); + } + + private static T FindVisualChild(DependencyObject parent) where T : DependencyObject + { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is T t) return t; + var result = FindVisualChild(child); + if (result != null) return result; + } + return null; + } + + // ============================================================ + // TreeView 子节点懒加载 + // ============================================================ + + private void OnTreeViewItemExpanded(object sender, RoutedEventArgs e) + { + if (!(e.OriginalSource is TreeViewItem tvi) || !(tvi.DataContext is AliasNodeViewModel node)) + return; + + var aliasMap = _dataStore?.LoadAll(); + LazyLoadChildren(node, aliasMap); + } + + private void OnTreeViewItemSelected(object sender, RoutedEventArgs e) + { + OnTreeViewSelected(sender, e); + } + } +}