feat: inline editing with click-to-edit, TextBox overlay, Focus+SelectAll

- Grid overlay: TextBox covers TextBlock when editing (no more dual display)
- EditValue property: auto-initialized to DisplayName on edit start
- Click on already-selected node enters inline edit (PreviewMouseLeftButtonDown)
- Right-click auto-selects node before context menu
- TextBox IsVisibleChanged: Focus + SelectAll on Loaded priority
- XAML Visibility moved to Style setter (fix DataTrigger precedence)
- Light yellow background (#FFFFFDE6)
This commit is contained in:
tian 2026-05-31 15:52:03 +08:00
parent ad398652a7
commit d358c31a7d
3 changed files with 191 additions and 59 deletions

View File

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Windows.Input;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UI.WPF.ViewModels
@ -56,9 +57,25 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public bool IsEditing
{
get => _isEditing;
set { _isEditing = value; OnPropertyChanged(nameof(IsEditing)); }
set
{
if (_isEditing == value) return;
_isEditing = value;
OnPropertyChanged(nameof(IsEditing));
// 进入编辑时初始化 EditValue 为当前显示名
if (value)
EditValue = DisplayName;
}
}
/// <summary>编辑框的临时文本(进入编辑时初始化为 DisplayName</summary>
public string EditValue
{
get => _editValue;
set { _editValue = value; OnPropertyChanged(nameof(EditValue)); }
}
private string _editValue;
/// <summary>是否孤儿节点(模型结构变化后无法定位)</summary>
public bool IsOrphan
{
@ -92,8 +109,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public AliasNodeViewModel()
{
StartEditCommand = new RelayCommand(() => IsEditing = true);
}
/// <summary>启动内联编辑的命令</summary>
public ICommand StartEditCommand { get; }
/// <summary>
/// 刷新显示文本(别名变更新调用)
/// </summary>

View File

@ -2,7 +2,8 @@
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">
xmlns:converters="clr-namespace:NavisworksTransport.UI.WPF.Converters"
PreviewKeyDown="UserControl_PreviewKeyDown">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@ -16,16 +17,16 @@
<!-- 别名树 TreeView 的模板 -->
<HierarchicalDataTemplate DataType="{x:Type vm:AliasNodeViewModel}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Margin="0,1"
VerticalAlignment="Center">
<!-- 节点名:别名用暗红色,原名用黑色 -->
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center"
<Grid VerticalAlignment="Center">
<!-- 显示模式 -->
<TextBlock Text="{Binding DisplayName}"
FontSize="12" TextTrimming="CharacterEllipsis" MaxWidth="200"
FontWeight="SemiBold">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#FF1A1A1A"/>
<Setter Property="FontStyle" Value="Normal"/>
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasAlias}" Value="True">
<Setter Property="Foreground" Value="#FFC62828"/>
@ -37,21 +38,25 @@
<Setter Property="TextDecorations" Value="Strikethrough"/>
<Setter Property="Foreground" Value="#FFAAAAAA"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsEditing}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<!-- 编辑文本框IsEditing=true 时显示) -->
<TextBox Text="{Binding Alias, UpdateSourceTrigger=PropertyChanged}"
Width="150" VerticalAlignment="Center"
Visibility="Collapsed" FontSize="12"
<!-- 编辑模式 -->
<TextBox Text="{Binding EditValue, UpdateSourceTrigger=PropertyChanged}"
FontSize="12"
KeyDown="OnAliasTextBoxKeyDown"
LostFocus="OnAliasTextBoxLostFocus"
Background="#FFFFF8DC"
IsVisibleChanged="OnEditTextBoxVisibleChanged"
Background="#FFFFFDE6"
BorderBrush="#FF1565C0" BorderThickness="1">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsEditing}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
@ -60,19 +65,7 @@
</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>
</Grid>
</HierarchicalDataTemplate>
</ResourceDictionary>
</UserControl.Resources>
@ -98,14 +91,6 @@
<!-- 图标工具栏 -->
<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="自动生成别名"
@ -165,7 +150,9 @@
BorderThickness="0"
ItemContainerStyle="{StaticResource AliasTreeViewItemStyle}"
TreeViewItem.Expanded="OnTreeViewItemExpanded"
TreeViewItem.Selected="OnTreeViewItemSelected">
TreeViewItem.Selected="OnTreeViewItemSelected"
TreeViewItem.PreviewMouseRightButtonDown="OnTreeViewItemRightClick"
TreeViewItem.PreviewMouseLeftButtonDown="OnTreeViewItemLeftClick">
<TreeView.Resources>
<!-- 无焦点时也显示蓝色选中态(浅灰蓝底色 + 白字,匹配选择树) -->
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}"

View File

@ -125,12 +125,8 @@ namespace NavisworksTransport.UI.WPF.Views
/// <summary>
/// 别名树选中 → 同步到 Navisworks 选择
/// </summary>
private void OnTreeViewSelected(object sender, RoutedEventArgs e)
private void OnTreeViewSelected(AliasNodeViewModel node)
{
if (!(e.OriginalSource is TreeViewItem tvi) || !(tvi.DataContext is AliasNodeViewModel node))
return;
if (node.IsOrphan) return;
LogManager.Info($"[别名树] 树点击: NodeKey={node.NodeKey}, OriginalName={node.OriginalName}, ModelItem={node.ModelItem?.DisplayName ?? "null"}");
_isInternalSelection = true;
@ -154,6 +150,18 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
private void StartInlineEdit(AliasNodeViewModel node, TreeViewItem tvi)
{
// 直接用,不需要 tvi
StartInlineEdit(node);
}
private void StartInlineEdit(AliasNodeViewModel node)
{
ClearAllEditing();
node.IsEditing = true;
}
/// <summary>
/// Navisworks 选择变化 → 同步到别名树高亮
/// </summary>
@ -172,7 +180,6 @@ namespace NavisworksTransport.UI.WPF.Views
var item = selected.First();
_currentSelectionItem = item;
TbCurrentNode.Text = $"当前: {item.DisplayName}";
BtnSave.IsEnabled = true;
LogManager.Info($"[别名树] NW选中: DisplayName={item.DisplayName}, ClassName={item.ClassName}");
@ -184,7 +191,6 @@ namespace NavisworksTransport.UI.WPF.Views
{
_currentSelectionItem = null;
TbCurrentNode.Text = selected.Count > 1 ? $"当前: ({selected.Count} 个选中)" : "当前: 无";
BtnSave.IsEnabled = false;
}
}
catch (Exception ex)
@ -510,7 +516,9 @@ namespace NavisworksTransport.UI.WPF.Views
{
if (currentContainer is TreeViewItem t)
{
_isProgrammaticSelect = true;
t.IsSelected = true;
_isProgrammaticSelect = false;
t.BringIntoView();
}
return;
@ -562,7 +570,9 @@ namespace NavisworksTransport.UI.WPF.Views
var childTvi = parentTvi.ItemContainerGenerator.ContainerFromItem(current) as TreeViewItem;
if (childTvi != null)
{
_isProgrammaticSelect = true;
childTvi.IsSelected = true;
_isProgrammaticSelect = false;
childTvi.BringIntoView();
}
else
@ -613,25 +623,20 @@ namespace NavisworksTransport.UI.WPF.Views
// 事件处理:工具栏
// ============================================================
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);
LogManager.Info($"[别名树] 保存别名: {input}, NodeKey={identity.ToKey()}");
_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);
if (e.Key == Key.Enter)
{
if (_currentSelectionItem == null || _dataStore == null) return;
string input = TxtAliasInput.Text?.Trim();
if (string.IsNullOrEmpty(input)) return;
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)
@ -715,6 +720,18 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
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);
@ -768,8 +785,38 @@ namespace NavisworksTransport.UI.WPF.Views
private void OnCtxEditClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
LogManager.Info($"[别名树] OnCtxEditClick: node={node?.OriginalName ?? "null"}");
if (node == null) return;
OnEditClick(new Button { Tag = node }, null);
// 从可视化树中查找 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)
@ -906,9 +953,86 @@ namespace NavisworksTransport.UI.WPF.Views
LazyLoadChildren(node, aliasMap);
}
private bool _isProgrammaticSelect;
/// <summary>
/// 树节点被选中时:如果 500ms 内再次选中同一节点 → 进入内联编辑
/// </summary>
/// <summary>
/// 右键前先选中节点,确保右键菜单操作的是正确节点
/// </summary>
private void OnTreeViewItemRightClick(object sender, MouseButtonEventArgs e)
{
var tvi = FindAncestor(e.OriginalSource as DependencyObject);
if (tvi != null)
{
tvi.IsSelected = true;
tvi.Focus();
}
}
private static TreeViewItem FindAncestor(DependencyObject current)
{
while (current != null)
{
if (current is TreeViewItem tvi) return tvi;
current = VisualTreeHelper.GetParent(current);
}
return null;
}
/// <summary>
/// 左键点击已选中节点 → 进入内联编辑
/// </summary>
private void OnTreeViewItemLeftClick(object sender, MouseButtonEventArgs e)
{
var tvi = FindAncestor(e.OriginalSource as DependencyObject);
if (tvi != null && tvi.IsSelected && tvi.DataContext is AliasNodeViewModel node && !node.IsOrphan)
{
StartInlineEdit(node);
}
}
private void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
{
OnTreeViewSelected(sender, 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);
}
}
}
}