1620 lines
61 KiB
C#
1620 lines
61 KiB
C#
using NavisworksTransport.Core.AliasTree;
|
||
using NavisworksTransport.Core.Config;
|
||
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 Autodesk.Navisworks.Api.DocumentParts;
|
||
using NavisApplication = Autodesk.Navisworks.Api.Application;
|
||
|
||
namespace NavisworksTransport.UI.WPF.Views
|
||
{
|
||
/// <summary>
|
||
/// 别名导航树面板 — 核心 UI 逻辑
|
||
/// </summary>
|
||
public partial class AliasTreeControl : UserControl
|
||
{
|
||
// ============================================================
|
||
// 状态
|
||
// ============================================================
|
||
|
||
private AliasDataStore _dataStore;
|
||
private string _copiedAliasTreeJson;
|
||
|
||
/// <summary>当控件检测到模型就绪但数据存储未连接时触发</summary>
|
||
internal event Action OnDatabaseNeeded;
|
||
/// <summary>保存别名前触发(确保数据库已创建)</summary>
|
||
internal event Action BeforeSaveAlias;
|
||
private Dictionary<string, AliasNodeViewModel> _nodeMap = new Dictionary<string, AliasNodeViewModel>();
|
||
/// <summary>记录 ExpandAndSelect 按需加载但无别名的深层节点,下次选择时清理</summary>
|
||
private HashSet<string> _onDemandNodeKeys = new HashSet<string>();
|
||
private bool _isInternalSelection = false;
|
||
private Autodesk.Navisworks.Api.ModelItem _currentSelectionItem;
|
||
private bool _built = false;
|
||
|
||
/// <summary>完整展示的最大层级深度(默认2层)</summary>
|
||
public int MaxFullDisplayDepth { get; set; } = 2;
|
||
|
||
// ============================================================
|
||
// 生命周期
|
||
// ============================================================
|
||
|
||
public AliasTreeControl()
|
||
{
|
||
InitializeComponent();
|
||
|
||
// 从配置文件读取初始值
|
||
MaxFullDisplayDepth = ConfigManager.Instance.Current.AliasTree?.MaxFullDisplayDepth ?? 2;
|
||
|
||
// 订阅配置变更,实时生效
|
||
ConfigManager.Instance.CategoryConfigurationChanged += OnAliasTreeConfigChanged;
|
||
|
||
// 将 AliasTreeView 的 items 源指向根节点集合
|
||
var rootNodes = new System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>();
|
||
AliasTreeView.ItemsSource = rootNodes;
|
||
|
||
// 文档切换事件
|
||
NavisApplication.ActiveDocumentChanged += (s, e) => { _built = false; RebuildTree(); };
|
||
NavisApplication.ActiveDocumentChanging += (s, e) => { _built = false; };
|
||
|
||
// 模型加载事件:面板启动时文档已存在但模型未加载时触发
|
||
var doc = NavisApplication.ActiveDocument;
|
||
if (doc != null)
|
||
doc.Models.CollectionChanged += (s, e) =>
|
||
{
|
||
_built = false;
|
||
RebuildTree();
|
||
OnDatabaseNeeded?.Invoke();
|
||
};
|
||
|
||
HookSelectionEvents();
|
||
|
||
// 立即构建:面板打开时如果文档已存在,不走事件也要构建
|
||
RebuildTree();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前数据存储(null 表示未连接)
|
||
/// </summary>
|
||
public AliasDataStore GetDataStore() => _dataStore;
|
||
|
||
/// <summary>
|
||
/// 设置数据存储(由外部在 PathDatabase 就绪后调用)
|
||
/// </summary>
|
||
public void SetDataStore(AliasDataStore store)
|
||
{
|
||
_dataStore = store;
|
||
LogManager.Info($"[别名树] SetDataStore: _built={_built}, aliasCount={store?.Count}");
|
||
if (!_built)
|
||
RebuildTree();
|
||
else
|
||
RefreshAliasesFromStore();
|
||
|
||
// 无论树是否已构建,确保已存别名的深层节点路径被加载
|
||
var aliasMap = _dataStore?.LoadAll();
|
||
if (aliasMap != null && aliasMap.Count > 0)
|
||
EnsureAliasedPathsVisible(aliasMap);
|
||
|
||
// EnsureAliasedPathsVisible 可能创建了新节点,需要再刷一次别名到这些节点上
|
||
if (_built)
|
||
RefreshAliasesFromStore();
|
||
}
|
||
|
||
private void RefreshAliasesFromStore()
|
||
{
|
||
if (_dataStore == null) return;
|
||
try
|
||
{
|
||
var aliasMap = _dataStore.LoadAll();
|
||
LogManager.Info($"[别名树] RefreshAliases: loaded={aliasMap.Count}, nodeMap={_nodeMap.Count}");
|
||
int matched = 0;
|
||
foreach (var kvp in aliasMap)
|
||
{
|
||
int sepIdx = kvp.Key.IndexOf("||", StringComparison.Ordinal);
|
||
string lookupKey = sepIdx >= 0 ? kvp.Key.Substring(sepIdx + 2) : kvp.Key;
|
||
if (_nodeMap.TryGetValue(lookupKey, out var node))
|
||
{
|
||
try
|
||
{
|
||
node.Alias = kvp.Value;
|
||
matched++;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] RefreshAliases set error: key={kvp.Key}, {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
LogManager.Info($"[别名树] RefreshAliases: matched={matched}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] RefreshAliases 异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理资源
|
||
/// </summary>
|
||
public void Cleanup()
|
||
{
|
||
ConfigManager.Instance.CategoryConfigurationChanged -= OnAliasTreeConfigChanged;
|
||
UnhookSelectionEvents();
|
||
_dataStore = null;
|
||
_nodeMap.Clear();
|
||
}
|
||
|
||
private void OnAliasTreeConfigChanged(object sender, Tuple<ConfigCategory, ConfigurationChangedEventArgs> args)
|
||
{
|
||
var config = ConfigManager.Instance.Current;
|
||
int newDepth = config.AliasTree?.MaxFullDisplayDepth ?? 2;
|
||
if (MaxFullDisplayDepth != newDepth)
|
||
{
|
||
MaxFullDisplayDepth = newDepth;
|
||
LogManager.Info($"[别名树] 配置变更: MaxFullDisplayDepth={newDepth}");
|
||
_built = false;
|
||
RebuildTree();
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 选择事件(双向联动)
|
||
// ============================================================
|
||
|
||
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 (Exception ex) { LogManager.Debug($"[别名树] HookSelection事件异常: {ex.Message}"); }
|
||
}
|
||
|
||
private void UnhookSelectionEvents()
|
||
{
|
||
try
|
||
{
|
||
var doc = NavisApplication.ActiveDocument;
|
||
if (doc != null)
|
||
{
|
||
doc.CurrentSelection.Changed -= OnNavisSelectionChanged;
|
||
}
|
||
}
|
||
catch (Exception ex) { LogManager.Debug($"[别名树] UnhookSelection异常: {ex.Message}"); }
|
||
_selectionHooked = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 别名树选中 → 同步到 Navisworks 选择
|
||
/// </summary>
|
||
private void OnTreeViewSelected(AliasNodeViewModel node)
|
||
{
|
||
// 由 NW 选择触发的 ExpandAndSelect 过程中的被动选中事件,不反向同步
|
||
if (_isInternalSelection) return;
|
||
|
||
LogManager.Debug($"[别名树] OnTreeViewSelected: {node.OriginalName}");
|
||
|
||
_isInternalSelection = true;
|
||
try
|
||
{
|
||
_currentSelectionItem = node.ModelItem;
|
||
TbCurrentNode.Text = $"当前: {node.OriginalName}";
|
||
var doc = NavisApplication.ActiveDocument;
|
||
if (doc != null && doc.CurrentSelection != null && node.ModelItem != null)
|
||
{
|
||
var coll = new Autodesk.Navisworks.Api.ModelItemCollection { node.ModelItem };
|
||
try
|
||
{
|
||
doc.CurrentSelection.Clear();
|
||
doc.CurrentSelection.CopyFrom(coll);
|
||
}
|
||
catch (NullReferenceException)
|
||
{
|
||
// NW API 内部 NRE(文档过渡状态),忽略
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] NW选择异常: {ex.GetType().Name}");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] 选中同步异常: {ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
_isInternalSelection = false;
|
||
}
|
||
}
|
||
|
||
private void StartInlineEdit(AliasNodeViewModel node, TreeViewItem tvi)
|
||
{
|
||
// 直接用,不需要 tvi
|
||
StartInlineEdit(node);
|
||
}
|
||
|
||
private void StartInlineEdit(AliasNodeViewModel node)
|
||
{
|
||
ClearAllEditing();
|
||
node.IsEditing = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Navisworks 选择变化 → 同步到别名树高亮
|
||
/// </summary>
|
||
private void OnNavisSelectionChanged(object sender, EventArgs e)
|
||
{
|
||
if (_isInternalSelection) return;
|
||
|
||
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}";
|
||
|
||
LogManager.Debug($"[别名树] NW选中: {item.DisplayName}");
|
||
|
||
ExpandAndSelect(item);
|
||
}
|
||
else
|
||
{
|
||
_currentSelectionItem = null;
|
||
TbCurrentNode.Text = selected.Count > 1 ? $"当前: ({selected.Count} 个选中)" : "当前: 无";
|
||
}
|
||
}
|
||
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<AliasNodeViewModel>;
|
||
if (rootNodes == null) return;
|
||
|
||
rootNodes.Clear();
|
||
_nodeMap.Clear();
|
||
|
||
var doc = NavisApplication.ActiveDocument;
|
||
if (doc == null || doc.Models == null) return;
|
||
|
||
// 全量加载别名到内存
|
||
Dictionary<string, string> aliasMap = null;
|
||
Dictionary<string, string> displayPathToAlias = null;
|
||
if (_dataStore != null)
|
||
{
|
||
aliasMap = _dataStore.LoadAll();
|
||
displayPathToAlias = BuildDisplayPathAliasMap(aliasMap);
|
||
}
|
||
|
||
// 遍历所有 root items
|
||
foreach (var model in doc.Models)
|
||
{
|
||
var rootItem = model.RootItem;
|
||
if (rootItem != null)
|
||
{
|
||
var node = BuildNode(rootItem, aliasMap, displayPathToAlias, 0, "", 0);
|
||
if (node != null)
|
||
rootNodes.Add(node);
|
||
}
|
||
}
|
||
|
||
// 重新挂载选择事件(可能文档刚切换)
|
||
HookSelectionEvents();
|
||
|
||
// 对已有别名的深层节点,强制加载其路径使其可见
|
||
if (aliasMap != null && aliasMap.Count > 0)
|
||
EnsureAliasedPathsVisible(aliasMap);
|
||
|
||
LogManager.Info($"[别名树] 树构建完成,共 {_nodeMap.Count} 个节点");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] 构建失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归构建节点树(主线程调用)。
|
||
/// siblingIndex 用于同名兄弟去重,0 表示第一个(不追加后缀)。
|
||
/// </summary>
|
||
private AliasNodeViewModel BuildNode(Autodesk.Navisworks.Api.ModelItem modelItem,
|
||
Dictionary<string, string> aliasMap,
|
||
Dictionary<string, string> displayPathToAlias,
|
||
int depth, string parentDisplayPath, int siblingIndex)
|
||
{
|
||
if (modelItem == null) return null;
|
||
|
||
string displayName = !string.IsNullOrEmpty(modelItem.DisplayName) ? modelItem.DisplayName : "几何对象";
|
||
// 父节点 key 可能是完整格式 "IndexPath||DisplayPath",也可能是 ".../name#N"
|
||
// 只取纯 DisplayPath 部分(去掉 || 前缀和结尾的 #N 后缀)
|
||
string parentPath = parentDisplayPath;
|
||
int sepIdx = parentPath.IndexOf("||", StringComparison.Ordinal);
|
||
if (sepIdx >= 0) parentPath = parentPath.Substring(sepIdx + 2);
|
||
string displayPath = string.IsNullOrEmpty(parentPath)
|
||
? displayName
|
||
: parentPath + "/" + displayName;
|
||
// 同名兄弟去重:第二个起追加 #索引
|
||
string displayPathKey = siblingIndex > 0 ? displayPath + "#" + siblingIndex : displayPath;
|
||
|
||
// 别名匹配:精确匹配 displayPathKey
|
||
string alias = null;
|
||
displayPathToAlias?.TryGetValue(displayPathKey, out alias);
|
||
|
||
var node = new AliasNodeViewModel
|
||
{
|
||
NodeKey = displayPathKey,
|
||
ModelItem = modelItem,
|
||
OriginalName = displayName,
|
||
Alias = alias,
|
||
ChildCount = modelItem.Children.Count(),
|
||
Depth = depth
|
||
};
|
||
|
||
_nodeMap[displayPathKey] = node;
|
||
|
||
// 深度限制:超过阈值且无别名 → 不展开子节点,不加占位
|
||
bool hasChildren = node.ChildCount > 0;
|
||
if (hasChildren && depth >= MaxFullDisplayDepth && string.IsNullOrEmpty(alias))
|
||
hasChildren = false;
|
||
|
||
if (hasChildren)
|
||
{
|
||
node.Children.Add(new AliasNodeViewModel
|
||
{
|
||
DisplayName = "加载中...",
|
||
OriginalName = "加载中..."
|
||
});
|
||
}
|
||
|
||
return node;
|
||
}
|
||
|
||
private void LazyLoadChildren(AliasNodeViewModel parentNode, Dictionary<string, string> aliasMap,
|
||
Dictionary<string, string> displayPathToAlias)
|
||
{
|
||
// 检查是否只有占位节点
|
||
if (parentNode.Children.Count != 1 || parentNode.Children[0].OriginalName != "加载中...")
|
||
return;
|
||
|
||
parentNode.Children.Clear();
|
||
|
||
try
|
||
{
|
||
var item = parentNode.ModelItem;
|
||
if (item == null) return;
|
||
|
||
// 同名去重:统计每个名字已出现的次数
|
||
var nameCount = new Dictionary<string, int>();
|
||
foreach (var childItem in item.Children)
|
||
{
|
||
string name = !string.IsNullOrEmpty(childItem.DisplayName) ? childItem.DisplayName : "几何对象";
|
||
nameCount.TryGetValue(name, out int count);
|
||
int siblingIdx = count;
|
||
nameCount[name] = count + 1;
|
||
|
||
var childNode = BuildNode(childItem, aliasMap, displayPathToAlias,
|
||
parentNode.Depth + 1, parentNode.NodeKey, siblingIdx);
|
||
if (childNode != null)
|
||
{
|
||
childNode.Parent = parentNode;
|
||
parentNode.Children.Add(childNode);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] 懒加载子节点失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
/// <summary>
|
||
/// 从 aliasMap 构建 DisplayPath → alias 索引(去掉 IndexPath|| 前缀)
|
||
/// </summary>
|
||
private static Dictionary<string, string> BuildDisplayPathAliasMap(Dictionary<string, string> aliasMap)
|
||
{
|
||
if (aliasMap == null || aliasMap.Count == 0) return null;
|
||
var map = new Dictionary<string, string>();
|
||
foreach (var kvp in aliasMap)
|
||
{
|
||
// key 格式: "IndexPath||DisplayPath" → 取 DisplayPath 部分
|
||
int sepIdx = kvp.Key.IndexOf("||", StringComparison.Ordinal);
|
||
string displayPath = sepIdx >= 0 ? kvp.Key.Substring(sepIdx + 2) : kvp.Key;
|
||
map[displayPath] = kvp.Value;
|
||
}
|
||
return map;
|
||
}
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 确保节点有完整 NodeKey(含 IndexPath)。首次保存别名时调用 CreatePathId 升级 key。
|
||
/// </summary>
|
||
private void EnsureFullNodeKey(AliasNodeViewModel node)
|
||
{
|
||
if (node == null || node.ModelItem == null) return;
|
||
// 已是完整格式
|
||
if (node.NodeKey != null && node.NodeKey.Contains("||")) return;
|
||
|
||
try
|
||
{
|
||
var identity = AliasNodeIdentity.FromModelItem(node.ModelItem);
|
||
node.NodeKey = identity.ToKey();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] EnsureFullNodeKey 失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private Autodesk.Navisworks.Api.ModelItem FindModelItemByNodeKey(string key)
|
||
{
|
||
if (string.IsNullOrEmpty(key)) return null;
|
||
|
||
// 判断 key 格式:含 "||" 为完整格式 → ResolvePathId;否则为 DisplayPath → _nodeMap 查找
|
||
int sepIdx = key.IndexOf("||", StringComparison.Ordinal);
|
||
if (sepIdx >= 0)
|
||
{
|
||
if (!AliasNodeIdentity.TryParseKey(key, out var identity) || string.IsNullOrEmpty(identity.IndexPath))
|
||
return null;
|
||
return ResolveByIndexPath(identity.IndexPath);
|
||
}
|
||
|
||
// 简化格式:DisplayPath → 查 _nodeMap
|
||
if (_nodeMap.TryGetValue(key, out var node) && node.ModelItem != null)
|
||
return node.ModelItem;
|
||
|
||
return null;
|
||
}
|
||
|
||
private Autodesk.Navisworks.Api.ModelItem ResolveByIndexPath(string indexPath)
|
||
{
|
||
try
|
||
{
|
||
var doc = NavisApplication.ActiveDocument;
|
||
if (doc == null || doc.Models == null) return null;
|
||
|
||
int colonIdx = indexPath.IndexOf(':');
|
||
if (colonIdx < 0) return null;
|
||
if (!int.TryParse(indexPath.Substring(0, colonIdx), out int modelIndex))
|
||
return null;
|
||
string pathId = indexPath.Substring(colonIdx + 1);
|
||
|
||
return doc.Models.ResolvePathId(new ModelItemPathId { ModelIndex = modelIndex, PathId = pathId });
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 展开并高亮指定 ModelItem
|
||
// 策略:利用 ModelItem.Parent 链逐层匹配,不依赖 IndexPath。
|
||
// ============================================================
|
||
|
||
private void ExpandAndSelect(Autodesk.Navisworks.Api.ModelItem item)
|
||
{
|
||
if (item == null) return;
|
||
|
||
LogManager.Info($"[别名树] ExpandAndSelect: target={item.DisplayName}");
|
||
|
||
// 整个 ExpandAndSelect 期间阻止树→NW 反向联动
|
||
_isInternalSelection = true;
|
||
try
|
||
{
|
||
ExpandAndSelectInternal(item);
|
||
}
|
||
finally
|
||
{
|
||
_isInternalSelection = false;
|
||
}
|
||
}
|
||
|
||
private void ExpandAndSelectInternal(Autodesk.Navisworks.Api.ModelItem item)
|
||
{
|
||
// 0. 清理上次按需加载的无别名深层节点
|
||
CleanupOnDemandNodes();
|
||
|
||
// 1. 沿 Parent 链构建祖先列表(根 → 目标)
|
||
var chain = new System.Collections.Generic.List<Autodesk.Navisworks.Api.ModelItem>();
|
||
var current = item;
|
||
while (current != null)
|
||
{
|
||
chain.Insert(0, current);
|
||
current = current.Parent;
|
||
}
|
||
|
||
// 2. 在 rootNodes 中找匹配的根节点
|
||
var rootNodes = AliasTreeView.ItemsSource
|
||
as System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>;
|
||
if (rootNodes == null || rootNodes.Count == 0) return;
|
||
|
||
AliasNodeViewModel treeNode = null;
|
||
foreach (var rn in rootNodes)
|
||
{
|
||
if (rn.ModelItem != null && rn.ModelItem.Equals(chain[0]))
|
||
{
|
||
treeNode = rn;
|
||
break;
|
||
}
|
||
}
|
||
if (treeNode == null)
|
||
{
|
||
LogManager.Warning($"[别名树] 根节点未找到: {chain[0].DisplayName}");
|
||
return;
|
||
}
|
||
|
||
// 3. 目标就是根节点
|
||
if (chain.Count == 1)
|
||
{
|
||
SelectTreeNode(treeNode);
|
||
return;
|
||
}
|
||
|
||
// 4. 沿链逐层向下,匹配/加载子节点
|
||
for (int i = 1; i < chain.Count; i++)
|
||
{
|
||
var targetModelItem = chain[i];
|
||
|
||
// 展开当前节点
|
||
ExpandTreeNode(treeNode);
|
||
|
||
// 加载子节点(有占位 → 全量懒加载;否则只找目标那一个)
|
||
var aliasMap = _dataStore?.LoadAll();
|
||
var displayAlias = BuildDisplayPathAliasMap(aliasMap);
|
||
if (treeNode.Children.Count == 1 && treeNode.Children[0].OriginalName == "加载中...")
|
||
{
|
||
LazyLoadChildren(treeNode, aliasMap, displayAlias);
|
||
}
|
||
|
||
// 在已加载的子节点中查找匹配
|
||
AliasNodeViewModel child = null;
|
||
foreach (var c in treeNode.Children)
|
||
{
|
||
if (c.OriginalName != "加载中..." && c.ModelItem != null && c.ModelItem.Equals(targetModelItem))
|
||
{
|
||
child = c;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 没找到(深度超限节点)→ 直接用 ModelItem 创建
|
||
if (child == null)
|
||
{
|
||
child = BuildNode(targetModelItem, aliasMap, displayAlias,
|
||
treeNode.Depth + 1, treeNode.NodeKey, 0);
|
||
if (child != null)
|
||
{
|
||
child.Parent = treeNode;
|
||
treeNode.Children.Add(child);
|
||
// 超限且无别名 → 下次选择时清理
|
||
if (child.Depth >= MaxFullDisplayDepth && !child.HasAlias)
|
||
_onDemandNodeKeys.Add(child.NodeKey);
|
||
}
|
||
}
|
||
|
||
if (child == null)
|
||
{
|
||
LogManager.Warning($"[别名树] 无法定位子节点: {targetModelItem.DisplayName}");
|
||
return;
|
||
}
|
||
|
||
treeNode = child;
|
||
}
|
||
|
||
// 5. 选中最终节点
|
||
SelectTreeNode(treeNode);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 展开树节点(程序化,不触发 Expanded 事件)
|
||
/// </summary>
|
||
private void ExpandTreeNode(AliasNodeViewModel node)
|
||
{
|
||
var tvi = GetTreeViewItemForNode(node);
|
||
if (tvi != null)
|
||
{
|
||
_isProgrammaticExpand = true;
|
||
tvi.IsExpanded = true;
|
||
_isProgrammaticExpand = false;
|
||
tvi.UpdateLayout();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 选中并滚动到指定树节点
|
||
/// </summary>
|
||
private void SelectTreeNode(AliasNodeViewModel node)
|
||
{
|
||
var parent = node.Parent;
|
||
if (parent != null)
|
||
{
|
||
var parentTvi = GetTreeViewItemForNode(parent);
|
||
if (parentTvi != null)
|
||
{
|
||
parentTvi.BringIntoView();
|
||
parentTvi.UpdateLayout();
|
||
|
||
Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
AliasTreeView.UpdateLayout();
|
||
var childTvi = parentTvi.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem;
|
||
if (childTvi != null)
|
||
{
|
||
_isProgrammaticSelect = true;
|
||
childTvi.IsSelected = true;
|
||
_isProgrammaticSelect = false;
|
||
childTvi.BringIntoView();
|
||
}
|
||
}), System.Windows.Threading.DispatcherPriority.Background);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 无父节点(根节点)直接选中
|
||
var tvi = GetTreeViewItemForNode(node);
|
||
if (tvi != null)
|
||
{
|
||
_isProgrammaticSelect = true;
|
||
tvi.IsSelected = true;
|
||
_isProgrammaticSelect = false;
|
||
tvi.BringIntoView();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理上次按需加载的、无别名的深层节点。
|
||
/// ExpandAndSelect 每次调用前执行,确保前一次超限无别名节点被移除。
|
||
/// </summary>
|
||
private void CleanupOnDemandNodes()
|
||
{
|
||
if (_onDemandNodeKeys.Count == 0) return;
|
||
|
||
foreach (var key in _onDemandNodeKeys)
|
||
{
|
||
if (_nodeMap.TryGetValue(key, out var node))
|
||
{
|
||
node.Parent?.Children.Remove(node);
|
||
_nodeMap.Remove(key);
|
||
}
|
||
}
|
||
_onDemandNodeKeys.Clear();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保所有已存别名的节点路径在树中可见。
|
||
/// 在 RebuildTree 后调用,对深层别名节点逐级加载其祖先。
|
||
/// </summary>
|
||
private void EnsureAliasedPathsVisible(Dictionary<string, string> aliasMap)
|
||
{
|
||
LogManager.Info($"[别名树] EnsureAliasedPathsVisible: 共 {aliasMap.Count} 条别名");
|
||
foreach (var kvp in aliasMap)
|
||
{
|
||
LogManager.Debug($"[别名树] alias: key={kvp.Key} -> \"{kvp.Value}\"");
|
||
}
|
||
|
||
int loaded = 0;
|
||
foreach (var kvp in aliasMap)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Debug($"[别名树] EnsureAliased: key={kvp.Key}, alias={kvp.Value}");
|
||
var item = FindModelItemByNodeKey(kvp.Key);
|
||
if (item == null)
|
||
{
|
||
LogManager.Debug($"[别名树] EnsureAliased: FindModelItemByNodeKey null, key={kvp.Key}");
|
||
continue;
|
||
}
|
||
if (EnsurePathLoaded(item))
|
||
loaded++;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] 加载别名路径失败: {kvp.Key}, {ex.Message}");
|
||
}
|
||
}
|
||
if (loaded > 0)
|
||
LogManager.Info($"[别名树] 已确保 {loaded} 个别名节点路径可见");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 沿 ModelItem 父链逐层加载节点到树中(不选中)。
|
||
/// 逻辑与 ExpandAndSelect 相同,只是最终不选中。
|
||
/// </summary>
|
||
private bool EnsurePathLoaded(Autodesk.Navisworks.Api.ModelItem item)
|
||
{
|
||
// 1. 构建祖先链
|
||
var chain = new System.Collections.Generic.List<Autodesk.Navisworks.Api.ModelItem>();
|
||
var current = item;
|
||
while (current != null)
|
||
{
|
||
chain.Insert(0, current);
|
||
current = current.Parent;
|
||
}
|
||
|
||
var chainStr = string.Join(" -> ", chain.ConvertAll(c => c.DisplayName ?? "?"));
|
||
LogManager.Debug($"[别名树] EnsurePathLoaded: chain=[{chainStr}], depth={chain.Count - 1}");
|
||
|
||
// 2. 匹配根节点
|
||
var rootNodes = AliasTreeView.ItemsSource
|
||
as System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>;
|
||
if (rootNodes == null) return false;
|
||
|
||
AliasNodeViewModel treeNode = null;
|
||
foreach (var rn in rootNodes)
|
||
{
|
||
if (rn.ModelItem != null && rn.ModelItem.Equals(chain[0]))
|
||
{
|
||
treeNode = rn;
|
||
break;
|
||
}
|
||
}
|
||
if (treeNode == null) return false;
|
||
if (chain.Count == 1) return true;
|
||
|
||
// 3. 逐层加载
|
||
var aliasMap = _dataStore?.LoadAll();
|
||
var displayAlias = BuildDisplayPathAliasMap(aliasMap);
|
||
for (int i = 1; i < chain.Count; i++)
|
||
{
|
||
var targetModelItem = chain[i];
|
||
ExpandTreeNode(treeNode);
|
||
|
||
if (treeNode.Children.Count == 1 && treeNode.Children[0].OriginalName == "加载中...")
|
||
LazyLoadChildren(treeNode, aliasMap, displayAlias);
|
||
|
||
AliasNodeViewModel child = null;
|
||
foreach (var c in treeNode.Children)
|
||
{
|
||
if (c.OriginalName != "加载中..." && c.ModelItem != null && c.ModelItem.Equals(targetModelItem))
|
||
{
|
||
child = c;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (child == null)
|
||
{
|
||
int siblingIdx = NavisworksTransport.Core.AliasTree.AliasNodeIdentity.ComputeSiblingIndex(targetModelItem);
|
||
child = BuildNode(targetModelItem, aliasMap, displayAlias,
|
||
treeNode.Depth + 1, treeNode.NodeKey, siblingIdx);
|
||
if (child != null)
|
||
{
|
||
child.Parent = treeNode;
|
||
treeNode.Children.Add(child);
|
||
}
|
||
}
|
||
|
||
if (child == null)
|
||
{
|
||
LogManager.Debug($"[别名树] EnsurePathLoaded: child null at L{i}");
|
||
return false;
|
||
}
|
||
treeNode = child;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 TreeView 根开始,沿 parent 链找 ViewModel 对应的 TreeViewItem。
|
||
/// 虚拟化下可能返回 null。
|
||
/// </summary>
|
||
private TreeViewItem GetTreeViewItemForNode(AliasNodeViewModel node)
|
||
{
|
||
if (node == null) return null;
|
||
|
||
// 收集从根到目标的路径
|
||
var path = new List<AliasNodeViewModel>();
|
||
var current = node;
|
||
while (current != null)
|
||
{
|
||
path.Insert(0, current);
|
||
current = current.Parent;
|
||
}
|
||
|
||
// 从 TreeView 根开始找
|
||
ItemsControl container = AliasTreeView;
|
||
foreach (var item in path)
|
||
{
|
||
if (item == null) break;
|
||
AliasTreeView.UpdateLayout();
|
||
container.UpdateLayout();
|
||
var tvi = container.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
|
||
if (tvi == null) return null;
|
||
container = tvi;
|
||
}
|
||
|
||
return container as TreeViewItem;
|
||
}
|
||
|
||
// ============================================================
|
||
// 事件处理:工具栏
|
||
// ============================================================
|
||
|
||
private void OnAliasInputKeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
if (e.Key == Key.Enter)
|
||
{
|
||
BeforeSaveAlias?.Invoke();
|
||
if (_currentSelectionItem == null || _dataStore == null) return;
|
||
string input = TxtAliasInput.Text?.Trim();
|
||
if (string.IsNullOrEmpty(input)) return;
|
||
if (input == _currentSelectionItem?.DisplayName)
|
||
input = null;
|
||
|
||
var identity = AliasNodeIdentity.FromModelItem(_currentSelectionItem);
|
||
LogManager.Info($"[别名树] 保存别名: {input}, NodeKey={identity.ToKey()}");
|
||
_dataStore.SetAlias(identity, input);
|
||
TxtAliasInput.Clear();
|
||
RefreshNodeDisplay(identity.ToKey());
|
||
}
|
||
}
|
||
|
||
private void OnAutoGenClick(object sender, RoutedEventArgs e)
|
||
{
|
||
// 阶段 3 实现
|
||
SetStatus("自动生成尚未实现");
|
||
}
|
||
|
||
private void OnExportClick(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_dataStore == null) { SetStatus("数据库未连接"); return; }
|
||
|
||
var dlg = new Microsoft.Win32.SaveFileDialog
|
||
{
|
||
Filter = "JSON 文件|*.alias.json",
|
||
Title = "导出别名映射"
|
||
};
|
||
if (dlg.ShowDialog() != true) return;
|
||
|
||
try
|
||
{
|
||
var allAliases = _dataStore.LoadAll();
|
||
var list = new List<object>();
|
||
foreach (var kvp in allAliases)
|
||
{
|
||
list.Add(new { key = kvp.Key, alias = kvp.Value });
|
||
}
|
||
|
||
var docName = NavisApplication.ActiveDocument?.FileName ?? "unknown";
|
||
var data = new
|
||
{
|
||
version = 1,
|
||
exportedAt = DateTime.Now.ToString("s"),
|
||
documentName = System.IO.Path.GetFileName(docName),
|
||
aliases = list
|
||
};
|
||
|
||
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
var json = serializer.Serialize(data);
|
||
System.IO.File.WriteAllText(dlg.FileName, json, System.Text.Encoding.UTF8);
|
||
SetStatus($"已导出 {list.Count} 条别名");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] 导出失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void OnImportClick(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_dataStore == null) { SetStatus("数据库未连接"); return; }
|
||
|
||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||
{
|
||
Filter = "JSON 文件|*.json",
|
||
Title = "导入别名映射"
|
||
};
|
||
if (dlg.ShowDialog() != true) return;
|
||
|
||
try
|
||
{
|
||
string json = System.IO.File.ReadAllText(dlg.FileName, System.Text.Encoding.UTF8);
|
||
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
var data = serializer.Deserialize<Dictionary<string, object>>(json);
|
||
|
||
var aliasesArr = data["aliases"] as System.Collections.ArrayList;
|
||
if (aliasesArr == null) { SetStatus("无效的别名文件"); return; }
|
||
|
||
var entries = new Dictionary<string, string>();
|
||
foreach (Dictionary<string, object> item in aliasesArr)
|
||
{
|
||
string key = item["key"] as string;
|
||
string alias = item["alias"] as string;
|
||
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(alias))
|
||
entries[key] = alias;
|
||
}
|
||
|
||
_dataStore.ImportBatch(entries);
|
||
RefreshAliasesFromStore();
|
||
SetStatus($"已导入 {entries.Count} 条别名");
|
||
LogManager.Info($"[别名树] 导入完成: {entries.Count} 条");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] 导入失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
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<TextBox>(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 OnEditTextBoxVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||
{
|
||
if (sender is TextBox tb && tb.IsVisible)
|
||
{
|
||
Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
tb.Focus();
|
||
tb.SelectAll();
|
||
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||
}
|
||
}
|
||
|
||
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 (string.IsNullOrEmpty(newAlias) || newAlias == node.OriginalName)
|
||
newAlias = null;
|
||
BeforeSaveAlias?.Invoke();
|
||
if (_dataStore == null) return;
|
||
|
||
LogManager.Info($"[别名树] 内联编辑: {newAlias}, NodeKey={node.NodeKey}");
|
||
try
|
||
{
|
||
EnsureFullNodeKey(node);
|
||
AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity);
|
||
_dataStore.SetAlias(identity, newAlias);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] SetAlias 异常: {ex.Message}");
|
||
return;
|
||
}
|
||
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();
|
||
LogManager.Info($"[别名树] OnCtxEditClick: node={node?.OriginalName ?? "null"}");
|
||
if (node == null) return;
|
||
|
||
// 从可视化树中查找 TreeViewItem,不依赖 ContainerFromItem
|
||
var tvi = FindTreeViewItemByDataContext(AliasTreeView, node);
|
||
LogManager.Info($"[别名树] OnCtxEditClick: tvi found={tvi != null}");
|
||
if (tvi != null)
|
||
StartInlineEdit(node, tvi);
|
||
}
|
||
|
||
private static TreeViewItem FindTreeViewItemByDataContext(ItemsControl parent, object dataContext)
|
||
{
|
||
// 先检查直接子项
|
||
for (int i = 0; i < parent.Items.Count; i++)
|
||
{
|
||
var item = parent.Items[i];
|
||
if (item == dataContext)
|
||
{
|
||
return parent.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
|
||
}
|
||
if (item is AliasNodeViewModel vm)
|
||
{
|
||
// 递归搜索子项(先获取容器,再搜它的子项)
|
||
var tvi = parent.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
|
||
if (tvi != null && tvi.Items.Count > 0)
|
||
{
|
||
var found = FindTreeViewItemByDataContext(tvi, dataContext);
|
||
if (found != null) return found;
|
||
}
|
||
}
|
||
}
|
||
return 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;
|
||
|
||
EnsureFullNodeKey(node);
|
||
AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity);
|
||
_dataStore.DeleteAlias(identity);
|
||
node.Alias = null;
|
||
ApplyAliasHighlight(node.NodeKey, false);
|
||
}
|
||
|
||
|
||
private void OnCtxCopyNameClick(object sender, RoutedEventArgs e)
|
||
{
|
||
var node = GetSelectedNode();
|
||
if (node == null) return;
|
||
try
|
||
{
|
||
Clipboard.SetText(node.OriginalName);
|
||
}
|
||
catch (Exception ex) { LogManager.Debug($"[别名树] Clipboard异常: {ex.Message}"); }
|
||
}
|
||
|
||
// ============================================================
|
||
// 子树别名导出/导入 / 复制/粘贴
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 解析带 #N 后缀的段名 ("Room101#1" → "Room101", 1)
|
||
/// </summary>
|
||
private static void ParseSuffixedSegment(string segment, out string baseName, out int index)
|
||
{
|
||
int hashIdx = segment.LastIndexOf('#');
|
||
if (hashIdx > 0 && int.TryParse(segment.Substring(hashIdx + 1), out index))
|
||
{
|
||
baseName = segment.Substring(0, hashIdx);
|
||
}
|
||
else
|
||
{
|
||
baseName = segment;
|
||
index = 0;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收集源节点子树下所有别名,返回相对路径→别名 字典(不含源节点自身)
|
||
/// </summary>
|
||
private Dictionary<string, string> CollectSubtreeAliases(AliasNodeIdentity sourceIdentity)
|
||
{
|
||
var result = new Dictionary<string, string>();
|
||
var allAliases = _dataStore.LoadAll();
|
||
if (allAliases == null || allAliases.Count == 0) return result;
|
||
|
||
string prefix = sourceIdentity.DisplayPath + "/";
|
||
|
||
foreach (var kvp in allAliases)
|
||
{
|
||
if (!AliasNodeIdentity.TryParseKey(kvp.Key, out var id)) continue;
|
||
|
||
if (id.DisplayPath.StartsWith(prefix, StringComparison.Ordinal))
|
||
{
|
||
string relativePath = id.DisplayPath.Substring(prefix.Length);
|
||
result[relativePath] = kvp.Value;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将相对路径→别名 字典应用到目标节点子树。
|
||
/// 逐段 Children 寻径,任一缺失则返回错误信息取消全部。
|
||
/// </summary>
|
||
private string TryApplySubtreeAliases(AliasNodeViewModel targetNode, Dictionary<string, string> relativeAliases)
|
||
{
|
||
if (relativeAliases == null || relativeAliases.Count == 0) return "没有可导入的别名数据";
|
||
|
||
EnsureFullNodeKey(targetNode);
|
||
if (!AliasNodeIdentity.TryParseKey(targetNode.NodeKey, out var targetIdentity))
|
||
return "无法解析目标节点标识";
|
||
|
||
var targetItem = FindModelItemByNodeKey(targetNode.NodeKey);
|
||
if (targetItem == null) return "无法定位目标模型节点";
|
||
|
||
var batch = new Dictionary<string, string>();
|
||
|
||
foreach (var kvp in relativeAliases)
|
||
{
|
||
Autodesk.Navisworks.Api.ModelItem current = targetItem;
|
||
string[] segments = kvp.Key.Split('/');
|
||
bool found = true;
|
||
|
||
foreach (var segment in segments)
|
||
{
|
||
ParseSuffixedSegment(segment, out var baseName, out var index);
|
||
int matchCount = 0;
|
||
Autodesk.Navisworks.Api.ModelItem matched = null;
|
||
|
||
foreach (var child in current.Children)
|
||
{
|
||
if (string.Equals(child.DisplayName, baseName, StringComparison.Ordinal))
|
||
{
|
||
if (matchCount == index) { matched = child; break; }
|
||
matchCount++;
|
||
}
|
||
}
|
||
|
||
if (matched == null) { found = false; break; }
|
||
current = matched;
|
||
}
|
||
|
||
if (!found) return $"目标子树缺少路径:{kvp.Key},已取消";
|
||
|
||
var descIdentity = AliasNodeIdentity.FromModelItem(current);
|
||
batch[descIdentity.ToKey()] = kvp.Value;
|
||
}
|
||
|
||
if (batch.Count > 0)
|
||
{
|
||
_dataStore.ImportBatch(batch);
|
||
RefreshAliasesFromStore();
|
||
SetStatus($"已导入 {batch.Count} 条别名到目标子树");
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private void OnCtxExportSubtreeClick(object sender, RoutedEventArgs e)
|
||
{
|
||
var node = GetSelectedNode();
|
||
if (node == null || _dataStore == null) return;
|
||
|
||
EnsureFullNodeKey(node);
|
||
if (!AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity))
|
||
{ SetStatus("无法解析节点标识"); return; }
|
||
|
||
var entries = CollectSubtreeAliases(identity);
|
||
if (entries.Count == 0) { SetStatus("该节点下没有别名数据"); return; }
|
||
|
||
var defaultName = string.Join("_", identity.DisplayPath.Split(System.IO.Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries)) + ".alias-subtree.json";
|
||
var dlg = new Microsoft.Win32.SaveFileDialog
|
||
{
|
||
Filter = "JSON 文件|*.alias-subtree.json",
|
||
Title = "导出子树别名",
|
||
FileName = defaultName
|
||
};
|
||
if (dlg.ShowDialog() != true) return;
|
||
|
||
try
|
||
{
|
||
var data = new
|
||
{
|
||
version = 1,
|
||
type = "alias_subtree",
|
||
exportedAt = DateTime.Now.ToString("s"),
|
||
sourceDisplayPath = identity.DisplayPath,
|
||
aliases = entries
|
||
};
|
||
|
||
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
File.WriteAllText(dlg.FileName, serializer.Serialize(data), System.Text.Encoding.UTF8);
|
||
SetStatus($"已导出 {entries.Count} 条子树别名");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] 子树导出失败: {ex.Message}");
|
||
SetStatus("导出失败,见日志");
|
||
}
|
||
}
|
||
|
||
private void OnCtxImportSubtreeClick(object sender, RoutedEventArgs e)
|
||
{
|
||
var node = GetSelectedNode();
|
||
if (node == null || _dataStore == null) return;
|
||
|
||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||
{
|
||
Filter = "JSON 文件|*.alias-subtree.json;*.json",
|
||
Title = "导入子树别名"
|
||
};
|
||
if (dlg.ShowDialog() != true) return;
|
||
|
||
try
|
||
{
|
||
var json = File.ReadAllText(dlg.FileName, System.Text.Encoding.UTF8);
|
||
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
var data = serializer.Deserialize<Dictionary<string, object>>(json);
|
||
|
||
if (!(data.TryGetValue("type", out var typeObj) && (typeObj as string) == "alias_subtree"))
|
||
{ SetStatus("无效的子树别名文件"); return; }
|
||
|
||
if (!(data.TryGetValue("aliases", out var aliasesObj) && aliasesObj is Dictionary<string, object> rawAliases))
|
||
{ SetStatus("别名数据格式无效"); return; }
|
||
|
||
var entries = new Dictionary<string, string>();
|
||
foreach (var kvp in rawAliases)
|
||
{
|
||
var alias = kvp.Value as string;
|
||
if (!string.IsNullOrEmpty(kvp.Key) && !string.IsNullOrEmpty(alias))
|
||
entries[kvp.Key] = alias;
|
||
}
|
||
|
||
if (entries.Count == 0) { SetStatus("文件不包含有效别名"); return; }
|
||
|
||
var error = TryApplySubtreeAliases(node, entries);
|
||
if (error != null) SetStatus(error);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] 子树导入失败: {ex.Message}");
|
||
SetStatus("导入失败,见日志");
|
||
}
|
||
}
|
||
|
||
private void OnCtxCopyAliasTreeClick(object sender, RoutedEventArgs e)
|
||
{
|
||
var node = GetSelectedNode();
|
||
if (node == null || _dataStore == null) return;
|
||
|
||
EnsureFullNodeKey(node);
|
||
if (!AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity))
|
||
{ SetStatus("无法解析节点标识"); return; }
|
||
|
||
var entries = CollectSubtreeAliases(identity);
|
||
if (entries.Count == 0) { SetStatus("该节点下没有别名数据"); return; }
|
||
|
||
var data = new
|
||
{
|
||
version = 1,
|
||
type = "alias_subtree",
|
||
exportedAt = DateTime.Now.ToString("s"),
|
||
sourceDisplayPath = identity.DisplayPath,
|
||
aliases = entries
|
||
};
|
||
|
||
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
_copiedAliasTreeJson = serializer.Serialize(data);
|
||
SetStatus($"已复制 {entries.Count} 条别名树");
|
||
}
|
||
|
||
private void OnCtxPasteAliasTreeClick(object sender, RoutedEventArgs e)
|
||
{
|
||
var node = GetSelectedNode();
|
||
if (node == null || _dataStore == null) return;
|
||
|
||
if (string.IsNullOrEmpty(_copiedAliasTreeJson))
|
||
{ SetStatus("没有已复制的别名树数据"); return; }
|
||
|
||
try
|
||
{
|
||
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
var data = serializer.Deserialize<Dictionary<string, object>>(_copiedAliasTreeJson);
|
||
|
||
if (!(data.TryGetValue("type", out var typeObj) && (typeObj as string) == "alias_subtree"))
|
||
{ SetStatus("内存数据无效"); return; }
|
||
|
||
if (!(data.TryGetValue("aliases", out var aliasesObj) && aliasesObj is Dictionary<string, object> rawAliases))
|
||
{ SetStatus("别名数据格式无效"); return; }
|
||
|
||
var entries = new Dictionary<string, string>();
|
||
foreach (var kvp in rawAliases)
|
||
{
|
||
var alias = kvp.Value as string;
|
||
if (!string.IsNullOrEmpty(kvp.Key) && !string.IsNullOrEmpty(alias))
|
||
entries[kvp.Key] = alias;
|
||
}
|
||
|
||
if (entries.Count == 0) { SetStatus("别名数据为空"); return; }
|
||
|
||
var error = TryApplySubtreeAliases(node, entries);
|
||
if (error != null) SetStatus(error);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Debug($"[别名树] 粘贴别名树异常: {ex.Message}");
|
||
SetStatus("别名树数据格式无效");
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 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)
|
||
{
|
||
int sepIdx = key.IndexOf("||", StringComparison.Ordinal);
|
||
string lookupKey = sepIdx >= 0 ? key.Substring(sepIdx + 2) : key;
|
||
if (_nodeMap.TryGetValue(lookupKey, 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<T>(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<T>(child);
|
||
if (result != null) return result;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ============================================================
|
||
// TreeView 子节点懒加载
|
||
// ============================================================
|
||
|
||
private void OnTreeViewItemExpanded(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_isProgrammaticExpand) return;
|
||
if (!(e.OriginalSource is TreeViewItem tvi) || !(tvi.DataContext is AliasNodeViewModel node))
|
||
return;
|
||
var aliasMap = _dataStore?.LoadAll();
|
||
var displayAlias = BuildDisplayPathAliasMap(aliasMap);
|
||
LazyLoadChildren(node, aliasMap, displayAlias);
|
||
}
|
||
|
||
private bool _isProgrammaticSelect;
|
||
private bool _isProgrammaticExpand;
|
||
|
||
/// <summary>
|
||
/// 树节点被选中时:如果 500ms 内再次选中同一节点 → 进入内联编辑
|
||
/// </summary>
|
||
/// <summary>
|
||
/// 右键前先选中节点,确保右键菜单操作的是正确节点
|
||
/// </summary>
|
||
private void OnTreeViewItemRightClick(object sender, MouseButtonEventArgs e)
|
||
{
|
||
try
|
||
{
|
||
var tvi = FindAncestor(e.OriginalSource as DependencyObject);
|
||
if (tvi != null)
|
||
{
|
||
tvi.IsSelected = true;
|
||
tvi.Focus();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] RCK-异常: {ex.GetType().Name}");
|
||
}
|
||
}
|
||
|
||
private static TreeViewItem FindAncestor(DependencyObject current)
|
||
{
|
||
try
|
||
{
|
||
while (current != null)
|
||
{
|
||
if (current is TreeViewItem tvi) return tvi;
|
||
current = VisualTreeHelper.GetParent(current);
|
||
}
|
||
}
|
||
catch (Exception ex) { LogManager.Warning($"[别名树] FindAncestor异常: {ex.GetType().Name}"); }
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 左键点击已选中节点 → 进入内联编辑
|
||
/// </summary>
|
||
private void OnTreeViewItemLeftClick(object sender, MouseButtonEventArgs e)
|
||
{
|
||
try
|
||
{
|
||
// 从点击元素的 DataContext 获取节点,不遍历可视化树
|
||
if (e.OriginalSource is FrameworkElement fe && fe.DataContext is AliasNodeViewModel node && !node.IsOrphan)
|
||
{
|
||
var selNode = GetSelectedNode();
|
||
if (node == selNode)
|
||
StartInlineEdit(node);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[别名树] LCK-异常: {ex.GetType().Name}");
|
||
}
|
||
}
|
||
|
||
private void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
|
||
{
|
||
if (e.OriginalSource is TreeViewItem tvi && tvi.DataContext is AliasNodeViewModel node && !node.IsOrphan)
|
||
{
|
||
if (!_isProgrammaticSelect)
|
||
OnTreeViewSelected(node);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 选中状态下点击 → 进入内联编辑(模仿 DataGrid 行为)
|
||
/// </summary>
|
||
/// <summary>
|
||
/// F2 键 → 进入内联编辑
|
||
/// </summary>
|
||
private void UserControl_PreviewKeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
if (e.Key == Key.F2)
|
||
{
|
||
EnterEditSelectedNode();
|
||
e.Handled = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 双击 → 进入内联编辑
|
||
/// </summary>
|
||
private void OnTreeViewDoubleClick(object sender, MouseButtonEventArgs e)
|
||
{
|
||
EnterEditSelectedNode();
|
||
}
|
||
|
||
private void EnterEditSelectedNode()
|
||
{
|
||
var node = GetSelectedNode();
|
||
if (node != null)
|
||
{
|
||
var tvi = AliasTreeView.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem;
|
||
if (tvi != null) StartInlineEdit(node, tvi);
|
||
}
|
||
}
|
||
}
|
||
}
|