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
This commit is contained in:
parent
3f985aa235
commit
f3392ddbf6
@ -218,6 +218,9 @@
|
||||
<Compile Include="src\UI\WPF\Views\LogisticsControlPanel.xaml.cs">
|
||||
<DependentUpon>LogisticsControlPanel.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="src\UI\WPF\Views\AliasTreeControl.xaml.cs">
|
||||
<DependentUpon>AliasTreeControl.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="src\UI\WPF\Views\ModelSettingsView.xaml.cs">
|
||||
<DependentUpon>ModelSettingsView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@ -288,6 +291,7 @@
|
||||
<Compile Include="src\UI\WPF\ViewModels\ModelSettingsViewModel.cs" />
|
||||
<Compile Include="src\UI\WPF\ViewModels\AnimationControlViewModel.cs" />
|
||||
<Compile Include="src\UI\WPF\ViewModels\PathEditingViewModel.cs" />
|
||||
<Compile Include="src\UI\WPF\ViewModels\AliasNodeViewModel.cs" />
|
||||
<Compile Include="src\UI\WPF\ViewModels\RailAssemblyWorkflowContext.cs" />
|
||||
<Compile Include="src\UI\WPF\ViewModels\SystemManagementViewModel.cs" />
|
||||
<Compile Include="src\UI\WPF\ViewModels\CollisionReportViewModel.cs" />
|
||||
@ -391,6 +395,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="src\UI\WPF\Views\AliasTreeControl.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="src\UI\WPF\Views\EditCoordinatesWindow.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@ -508,6 +516,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="src\UI\WPF\Resources\AliasTreeIcons.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WinFX\3.0\Microsoft.WinFX.targets" Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\WinFX\3.0\Microsoft.WinFX.targets')" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
|
||||
136
src/UI/WPF/Resources/AliasTreeIcons.xaml
Normal file
136
src/UI/WPF/Resources/AliasTreeIcons.xaml
Normal file
@ -0,0 +1,136 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- ── 别名树工具栏图标 ── -->
|
||||
|
||||
<!-- 保存图标(磁盘) -->
|
||||
<Geometry x:Key="SaveIconGeometry">
|
||||
M15,9H5V5H15M15,19H5V15H15M17,3H3V21H17V16L21,12L17,8V3Z
|
||||
</Geometry>
|
||||
|
||||
<!-- 导出图标(向上箭头出盒子) -->
|
||||
<Geometry x:Key="ExportIconGeometry">
|
||||
M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z
|
||||
</Geometry>
|
||||
|
||||
<!-- 导入图标(向下箭头入盒子) -->
|
||||
<Geometry x:Key="ImportIconGeometry">
|
||||
M5,10H19V12H5M12,5L5,12H9V21H15V12H19L12,5Z
|
||||
</Geometry>
|
||||
|
||||
<!-- 自动生成图标(齿轮) -->
|
||||
<Geometry x:Key="AutoGenIconGeometry">
|
||||
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
|
||||
</Geometry>
|
||||
|
||||
<!-- 清除图标(垃圾桶) -->
|
||||
<Geometry x:Key="ClearIconGeometry">
|
||||
M6,19V7H18V19H6M9,4H15L14.5,3H9.5L9,4M19,5V7H5V5H8L8.5,4H15.5L16,5H19Z
|
||||
</Geometry>
|
||||
|
||||
<!-- 批量编辑图标(列表+铅笔) -->
|
||||
<Geometry x:Key="BatchEditIconGeometry">
|
||||
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
|
||||
</Geometry>
|
||||
|
||||
<!-- 聚焦图标(瞄准镜) -->
|
||||
<Geometry x:Key="FocusIconGeometry">
|
||||
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
|
||||
</Geometry>
|
||||
|
||||
<!-- 铅笔编辑图标 -->
|
||||
<Geometry x:Key="EditIconGeometry">
|
||||
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
|
||||
</Geometry>
|
||||
|
||||
<!-- 刷新图标 -->
|
||||
<Geometry x:Key="RefreshIconGeometry">
|
||||
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
|
||||
</Geometry>
|
||||
|
||||
<!-- ── 别名树工具栏按钮样式 ── -->
|
||||
|
||||
<!-- 图标按钮:紧凑 24×24,透明背景,悬停蓝色高亮 -->
|
||||
<Style x:Key="AliasToolbarButtonStyle" TargetType="Button">
|
||||
<Setter Property="Width" Value="24"/>
|
||||
<Setter Property="Height" Value="24"/>
|
||||
<Setter Property="Margin" Value="1,0"/>
|
||||
<Setter Property="Padding" Value="2"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="#FF666666"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="ToolTipService.ShowOnDisabled" Value="True"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="2">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="#FFD4E7FF"/>
|
||||
<Setter Property="Foreground" Value="#FF2B579A"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="#FF4472C4"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="#FFBBBBBB"/>
|
||||
<Setter Property="Opacity" Value="0.5"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 紧凑分隔线 -->
|
||||
<Style x:Key="AliasToolbarSeparatorStyle" TargetType="Separator">
|
||||
<Setter Property="Width" Value="1"/>
|
||||
<Setter Property="Height" Value="16"/>
|
||||
<Setter Property="Margin" Value="3,0"/>
|
||||
<Setter Property="Background" Value="#FFCCCCCC"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<!-- ── 别名树 TreeViewItem 样式 ── -->
|
||||
|
||||
<Style x:Key="AliasTreeViewItemStyle" TargetType="TreeViewItem">
|
||||
<Setter Property="IsExpanded" Value="False"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Padding" Value="2,1"/>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
108
src/UI/WPF/ViewModels/AliasNodeViewModel.cs
Normal file
108
src/UI/WPF/ViewModels/AliasNodeViewModel.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 别名树单节点 ViewModel。
|
||||
/// 支持 INotifyPropertyChanged 以驱动 TreeView 实时更新。
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>节点的序列化标识 key(对应 AliasNodeIdentity.ToKey())</summary>
|
||||
public string NodeKey { get; set; }
|
||||
|
||||
/// <summary>当前显示文本(别名优先,无别名回退到原名)</summary>
|
||||
public string DisplayName
|
||||
{
|
||||
get => _displayName;
|
||||
set { _displayName = value; OnPropertyChanged(nameof(DisplayName)); }
|
||||
}
|
||||
|
||||
/// <summary>Navisworks 原始名称(只读)</summary>
|
||||
public string OriginalName
|
||||
{
|
||||
get => _originalName;
|
||||
set { _originalName = value; OnPropertyChanged(nameof(OriginalName)); }
|
||||
}
|
||||
|
||||
/// <summary>用户别名</summary>
|
||||
public string Alias
|
||||
{
|
||||
get => _alias;
|
||||
set
|
||||
{
|
||||
_alias = value;
|
||||
HasAlias = !string.IsNullOrEmpty(value);
|
||||
// 更新显示文本
|
||||
DisplayName = HasAlias ? value : (OriginalName ?? "?");
|
||||
OnPropertyChanged(nameof(Alias));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>是否处于就地编辑状态</summary>
|
||||
public bool IsEditing
|
||||
{
|
||||
get => _isEditing;
|
||||
set { _isEditing = value; OnPropertyChanged(nameof(IsEditing)); }
|
||||
}
|
||||
|
||||
/// <summary>是否孤儿节点(模型结构变化后无法定位)</summary>
|
||||
public bool IsOrphan
|
||||
{
|
||||
get => _isOrphan;
|
||||
set { _isOrphan = value; OnPropertyChanged(nameof(IsOrphan)); }
|
||||
}
|
||||
|
||||
/// <summary>是否有别名</summary>
|
||||
public bool HasAlias
|
||||
{
|
||||
get => _hasAlias;
|
||||
set { _hasAlias = value; OnPropertyChanged(nameof(HasAlias)); }
|
||||
}
|
||||
|
||||
/// <summary>子节点数量(用于展开箭头)</summary>
|
||||
public int ChildCount
|
||||
{
|
||||
get => _childCount;
|
||||
set { _childCount = value; OnPropertyChanged(nameof(ChildCount)); OnPropertyChanged(nameof(HasChildren)); }
|
||||
}
|
||||
|
||||
/// <summary>是否有子节点</summary>
|
||||
public bool HasChildren => ChildCount > 0;
|
||||
|
||||
/// <summary>子节点集合</summary>
|
||||
public System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel> Children { get; }
|
||||
= new System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>();
|
||||
|
||||
/// <summary>父节点引用(用于回溯路径)</summary>
|
||||
public AliasNodeViewModel Parent { get; set; }
|
||||
|
||||
public AliasNodeViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新显示文本(别名变更新调用)
|
||||
/// </summary>
|
||||
public void RefreshDisplay()
|
||||
{
|
||||
DisplayName = HasAlias ? Alias : (OriginalName ?? "?");
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
213
src/UI/WPF/Views/AliasTreeControl.xaml
Normal file
213
src/UI/WPF/Views/AliasTreeControl.xaml
Normal file
@ -0,0 +1,213 @@
|
||||
<UserControl x:Class="NavisworksTransport.UI.WPF.Views.AliasTreeControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:NavisworksTransport.UI.WPF.ViewModels"
|
||||
xmlns:converters="clr-namespace:NavisworksTransport.UI.WPF.Converters">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="../Resources/NavisworksStyles.xaml"/>
|
||||
<ResourceDictionary Source="../Resources/AliasTreeIcons.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Converters -->
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
|
||||
<!-- 别名树 TreeView 的模板 -->
|
||||
<HierarchicalDataTemplate DataType="{x:Type vm:AliasNodeViewModel}"
|
||||
ItemsSource="{Binding Children}">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,1"
|
||||
VerticalAlignment="Center">
|
||||
<!-- 别名前缀标记 ◆ -->
|
||||
<TextBlock Text="◆ " FontSize="11"
|
||||
Visibility="{Binding HasAlias, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
Foreground="#FF1565C0" VerticalAlignment="Center"/>
|
||||
<!-- 无别名时显示灰色斜体原名 -->
|
||||
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center"
|
||||
FontSize="12" TextTrimming="CharacterEllipsis" MaxWidth="200">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="#FF888888"/>
|
||||
<Setter Property="FontStyle" Value="Italic"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasAlias}" Value="True">
|
||||
<Setter Property="Foreground" Value="#FF1565C0"/>
|
||||
<Setter Property="FontStyle" Value="Normal"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsOrphan}" Value="True">
|
||||
<Setter Property="TextDecorations" Value="Strikethrough"/>
|
||||
<Setter Property="Foreground" Value="#FFAAAAAA"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<!-- 编辑文本框(IsEditing=true 时显示) -->
|
||||
<TextBox Text="{Binding Alias, UpdateSourceTrigger=PropertyChanged}"
|
||||
Width="150" VerticalAlignment="Center"
|
||||
Visibility="Collapsed" FontSize="12"
|
||||
KeyDown="OnAliasTextBoxKeyDown"
|
||||
LostFocus="OnAliasTextBoxLostFocus"
|
||||
Background="#FFFFF8DC"
|
||||
BorderBrush="#FF1565C0" BorderThickness="1">
|
||||
<TextBox.Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsEditing}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
|
||||
<!-- 铅笔编辑按钮(鼠标悬停显示) -->
|
||||
<Button Style="{StaticResource AliasToolbarButtonStyle}"
|
||||
ToolTip="编辑别名"
|
||||
Tag="{Binding}"
|
||||
Click="OnEditClick"
|
||||
Width="18" Height="18"
|
||||
Margin="2,0,0,0">
|
||||
<Path Data="{StaticResource EditIconGeometry}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Width="14" Height="14" Stretch="Uniform"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</HierarchicalDataTemplate>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<DockPanel>
|
||||
<!-- ── 顶部工具栏 ── -->
|
||||
<Border DockPanel.Dock="Top"
|
||||
Background="#FFF5F5F5"
|
||||
BorderBrush="#FFDDDDDD"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="4,2">
|
||||
<DockPanel>
|
||||
<!-- 当前选中节点名 -->
|
||||
<TextBlock x:Name="TbCurrentNode"
|
||||
DockPanel.Dock="Left"
|
||||
Text="当前: 无"
|
||||
VerticalAlignment="Center"
|
||||
Margin="2,0,6,0"
|
||||
FontSize="11"
|
||||
Foreground="#FF666666"
|
||||
MaxWidth="120"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
|
||||
<!-- 图标工具栏 -->
|
||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
|
||||
<Button x:Name="BtnSave" Style="{StaticResource AliasToolbarButtonStyle}"
|
||||
ToolTip="保存别名 (Ctrl+S)"
|
||||
IsEnabled="False"
|
||||
Click="OnSaveCurrentClick">
|
||||
<Path Data="{StaticResource SaveIconGeometry}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Width="16" Height="16" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Separator Style="{StaticResource AliasToolbarSeparatorStyle}"/>
|
||||
<Button x:Name="BtnAutoGen" Style="{StaticResource AliasToolbarButtonStyle}"
|
||||
ToolTip="自动生成别名"
|
||||
Click="OnAutoGenClick">
|
||||
<Path Data="{StaticResource AutoGenIconGeometry}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Width="16" Height="16" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Separator Style="{StaticResource AliasToolbarSeparatorStyle}"/>
|
||||
<Button x:Name="BtnExport" Style="{StaticResource AliasToolbarButtonStyle}"
|
||||
ToolTip="导出别名 (JSON)"
|
||||
Click="OnExportClick">
|
||||
<Path Data="{StaticResource ExportIconGeometry}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Width="16" Height="16" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Button x:Name="BtnImport" Style="{StaticResource AliasToolbarButtonStyle}"
|
||||
ToolTip="导入别名 (JSON)"
|
||||
Click="OnImportClick">
|
||||
<Path Data="{StaticResource ImportIconGeometry}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Width="16" Height="16" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Separator Style="{StaticResource AliasToolbarSeparatorStyle}"/>
|
||||
<Button x:Name="BtnClearAll" Style="{StaticResource AliasToolbarButtonStyle}"
|
||||
ToolTip="清除全部别名"
|
||||
Click="OnClearAllClick">
|
||||
<Path Data="{StaticResource ClearIconGeometry}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Width="16" Height="16" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Button x:Name="BtnRefresh" Style="{StaticResource AliasToolbarButtonStyle}"
|
||||
ToolTip="刷新树结构"
|
||||
Click="OnRefreshClick">
|
||||
<Path Data="{StaticResource RefreshIconGeometry}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Width="16" Height="16" Stretch="Uniform"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 别名输入框 -->
|
||||
<TextBox x:Name="TxtAliasInput"
|
||||
DockPanel.Dock="Right"
|
||||
Width="100"
|
||||
Height="20"
|
||||
FontSize="11"
|
||||
Margin="0,0,4,0"
|
||||
ToolTip="输入别名后回车保存"
|
||||
KeyDown="OnAliasInputKeyDown"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── 别名树 ── -->
|
||||
<TreeView x:Name="AliasTreeView"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||
Background="White"
|
||||
BorderThickness="0"
|
||||
ItemContainerStyle="{StaticResource AliasTreeViewItemStyle}">
|
||||
<TreeView.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="编辑别名" Click="OnCtxEditClick">
|
||||
<MenuItem.Icon>
|
||||
<Path Data="{StaticResource EditIconGeometry}"
|
||||
Fill="#FF666666" Width="14" Height="14" Stretch="Uniform"
|
||||
Margin="0,0,4,0"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="批量编辑子节点..." Click="OnCtxBatchEditClick">
|
||||
<MenuItem.Icon>
|
||||
<Path Data="{StaticResource BatchEditIconGeometry}"
|
||||
Fill="#FF666666" Width="14" Height="14" Stretch="Uniform"
|
||||
Margin="0,0,4,0"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="清除别名" Click="OnCtxClearClick"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="在视图中聚焦" Click="OnCtxFocusClick">
|
||||
<MenuItem.Icon>
|
||||
<Path Data="{StaticResource FocusIconGeometry}"
|
||||
Fill="#FF666666" Width="14" Height="14" Stretch="Uniform"
|
||||
Margin="0,0,4,0"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<MenuItem Header="复制原名" Click="OnCtxCopyNameClick"/>
|
||||
</ContextMenu>
|
||||
</TreeView.ContextMenu>
|
||||
</TreeView>
|
||||
|
||||
<!-- ── 底部状态栏 ── -->
|
||||
<StatusBar DockPanel.Dock="Bottom"
|
||||
Background="#FFF8F8F8"
|
||||
BorderBrush="#FFDDDDDD"
|
||||
BorderThickness="0,1,0,0"
|
||||
Height="20"
|
||||
Visibility="Collapsed">
|
||||
<StatusBarItem x:Name="StatusBarItem">
|
||||
<TextBlock x:Name="TbStatus" FontSize="10" Foreground="#FF666666"/>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
716
src/UI/WPF/Views/AliasTreeControl.xaml.cs
Normal file
716
src/UI/WPF/Views/AliasTreeControl.xaml.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 别名导航树面板 — 核心 UI 逻辑
|
||||
/// </summary>
|
||||
public partial class AliasTreeControl : UserControl
|
||||
{
|
||||
// ============================================================
|
||||
// 状态
|
||||
// ============================================================
|
||||
|
||||
private AliasDataStore _dataStore;
|
||||
private Dictionary<string, AliasNodeViewModel> _nodeMap = new Dictionary<string, AliasNodeViewModel>();
|
||||
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<AliasNodeViewModel>();
|
||||
AliasTreeView.ItemsSource = rootNodes;
|
||||
|
||||
// 文档切换事件
|
||||
NavisApplication.ActiveDocumentChanged += (s, e) => RebuildTree();
|
||||
NavisApplication.ActiveDocumentChanging += (s, e) => { _built = false; };
|
||||
|
||||
HookSelectionEvents();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置数据存储(由外部在 PathDatabase 就绪后调用)
|
||||
/// </summary>
|
||||
public void SetDataStore(AliasDataStore store)
|
||||
{
|
||||
_dataStore = store;
|
||||
RebuildTree();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理资源
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 别名树选中 → 同步到 Navisworks 选择
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navisworks 选择变化 → 同步到别名树高亮
|
||||
/// </summary>
|
||||
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<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;
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归构建节点树(主线程调用)
|
||||
/// </summary>
|
||||
private AliasNodeViewModel BuildNode(Autodesk.Navisworks.Api.ModelItem modelItem,
|
||||
Dictionary<string, string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 懒加载子节点(在 Expanded 事件中触发)
|
||||
/// </summary>
|
||||
private void LazyLoadChildren(AliasNodeViewModel parentNode, Dictionary<string, string> 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<AliasNodeViewModel>();
|
||||
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<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 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<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 (!(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user