Add HeaderContextMenu (#10547)

* Add HeaderContextMenu

* Up

* Up
This commit is contained in:
DHR60
2026-11-08 08:00:47 +00:00
committed by GitHub
parent 92a9da8b6b
commit 54ecc045b5
7 changed files with 210 additions and 83 deletions
@@ -102,6 +102,21 @@ public static class ConfigHandler
config.UiItem ??= new(); config.UiItem ??= new();
config.UiItem.MainColumnItem ??= []; config.UiItem.MainColumnItem ??= [];
config.UiItem.WindowSizeItem ??= []; config.UiItem.WindowSizeItem ??= [];
if (config.UiItem.FrozenColumnCount == null)
{
var lvColumnItem = config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
if (lvColumnItem.Count == 0
|| (lvColumnItem.Count >= 2
&& lvColumnItem[0].Name.Equals("ConfigType")
&& lvColumnItem[1].Name.Equals("Remarks")))
{
config.UiItem.FrozenColumnCount = 2;
}
else
{
config.UiItem.FrozenColumnCount = 0;
}
}
if (config.UiItem.CurrentLanguage.IsNullOrEmpty()) if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
{ {
@@ -97,6 +97,7 @@ public class UIItem
public List<ColumnItem> MainColumnItem { get; set; } public List<ColumnItem> MainColumnItem { get; set; }
public List<WindowSizeItem> WindowSizeItem { get; set; } public List<WindowSizeItem> WindowSizeItem { get; set; }
public bool HideColumnIpInfo { get; set; } public bool HideColumnIpInfo { get; set; }
public int? FrozenColumnCount { get; set; }
} }
public class ConstItem public class ConstItem
+45 -9
View File
@@ -996,15 +996,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Columns 的本地化字符串。
/// </summary>
public static string menuColumns {
get {
return ResourceManager.GetString("menuColumns", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Close Connection 的本地化字符串。 /// 查找类似 Close Connection 的本地化字符串。
/// </summary> /// </summary>
@@ -1041,6 +1032,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Currently frozen first {0} column(s) 的本地化字符串。
/// </summary>
public static string menuCurrentlyFrozenColumns {
get {
return ResourceManager.GetString("menuCurrentlyFrozenColumns", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Latency Test 的本地化字符串。 /// 查找类似 Latency Test 的本地化字符串。
/// </summary> /// </summary>
@@ -1185,6 +1185,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Freeze up to this column 的本地化字符串。
/// </summary>
public static string menuFreezeToThisColumn {
get {
return ResourceManager.GetString("menuFreezeToThisColumn", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Full Config Template Setting 的本地化字符串。 /// 查找类似 Full Config Template Setting 的本地化字符串。
/// </summary> /// </summary>
@@ -1230,6 +1239,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Hide this column 的本地化字符串。
/// </summary>
public static string menuHideThisColumn {
get {
return ResourceManager.GetString("menuHideThisColumn", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Import Rules From Clipboard 的本地化字符串。 /// 查找类似 Import Rules From Clipboard 的本地化字符串。
/// </summary> /// </summary>
@@ -1806,6 +1824,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Show columns 的本地化字符串。
/// </summary>
public static string menuShowColumns {
get {
return ResourceManager.GetString("menuShowColumns", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Show or hide the main window 的本地化字符串。 /// 查找类似 Show or hide the main window 的本地化字符串。
/// </summary> /// </summary>
@@ -1995,6 +2022,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Unfreeze all columns 的本地化字符串。
/// </summary>
public static string menuUnfreezeAllColumns {
get {
return ResourceManager.GetString("menuUnfreezeAllColumns", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 {0} Website 的本地化字符串。 /// 查找类似 {0} Website 的本地化字符串。
/// </summary> /// </summary>
+14 -2
View File
@@ -1917,8 +1917,8 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="FillCorrectFakeIPv6Range" xml:space="preserve"> <data name="FillCorrectFakeIPv6Range" xml:space="preserve">
<value>Enter a valid IPv6 CIDR range with at least two addresses, for example fc00::/18.</value> <value>Enter a valid IPv6 CIDR range with at least two addresses, for example fc00::/18.</value>
</data> </data>
<data name="menuColumns" xml:space="preserve"> <data name="menuShowColumns" xml:space="preserve">
<value>Columns</value> <value>Show columns</value>
</data> </data>
<data name="TbWarp" xml:space="preserve"> <data name="TbWarp" xml:space="preserve">
<value>Warp</value> <value>Warp</value>
@@ -1926,4 +1926,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="LvPreResolve" xml:space="preserve"> <data name="LvPreResolve" xml:space="preserve">
<value>Pre-resolve server domains</value> <value>Pre-resolve server domains</value>
</data> </data>
<data name="menuFreezeToThisColumn" xml:space="preserve">
<value>Freeze up to this column</value>
</data>
<data name="menuUnfreezeAllColumns" xml:space="preserve">
<value>Unfreeze all columns</value>
</data>
<data name="menuCurrentlyFrozenColumns" xml:space="preserve">
<value>Currently frozen first {0} column(s)</value>
</data>
<data name="menuHideThisColumn" xml:space="preserve">
<value>Hide this column</value>
</data>
</root> </root>
+14 -2
View File
@@ -1909,8 +1909,8 @@
<data name="FillCorrectFakeIPv6Range" xml:space="preserve"> <data name="FillCorrectFakeIPv6Range" xml:space="preserve">
<value>请输入至少包含两个地址的有效 IPv6 网段,例如 fc00::/18。</value> <value>请输入至少包含两个地址的有效 IPv6 网段,例如 fc00::/18。</value>
</data> </data>
<data name="menuColumns" xml:space="preserve"> <data name="menuShowColumns" xml:space="preserve">
<value>列</value> <value>显示列</value>
</data> </data>
<data name="TbWarp" xml:space="preserve"> <data name="TbWarp" xml:space="preserve">
<value>Warp</value> <value>Warp</value>
@@ -1918,4 +1918,16 @@
<data name="LvPreResolve" xml:space="preserve"> <data name="LvPreResolve" xml:space="preserve">
<value>预解析节点域名</value> <value>预解析节点域名</value>
</data> </data>
<data name="menuFreezeToThisColumn" xml:space="preserve">
<value>冻结至此列</value>
</data>
<data name="menuUnfreezeAllColumns" xml:space="preserve">
<value>取消所有冻结</value>
</data>
<data name="menuCurrentlyFrozenColumns" xml:space="preserve">
<value>当前已冻结前 {0} 列</value>
</data>
<data name="menuHideThisColumn" xml:space="preserve">
<value>隐藏此列</value>
</data>
</root> </root>
@@ -16,48 +16,6 @@
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<conv:DelayColorConverter x:Key="DelayColorConverter" /> <conv:DelayColorConverter x:Key="DelayColorConverter" />
<ControlTheme x:Key="{x:Type DataGridColumnChooser}" TargetType="DataGridColumnChooser">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate x:DataType="DataGridColumnChooserItem">
<CheckBox
Margin="0,2"
Content="{Binding Header}"
IsChecked="{Binding IsVisible, Mode=TwoWay}"
IsEnabled="{Binding CanUserHide}" />
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Spacing="4" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Button Classes="IconButton Success" ToolTip.Tip="{TemplateBinding ToolTip.Tip}">
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedLeft">
<Border
MaxWidth="260"
MaxHeight="360"
Padding="8">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsPresenter />
</ScrollViewer>
</Border>
</Flyout>
</Button.Flyout>
<ContentPresenter Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}" />
</Button>
</ControlTemplate>
</Setter.Value>
</Setter>
</ControlTheme>
</UserControl.Resources> </UserControl.Resources>
<Grid> <Grid>
<DockPanel> <DockPanel>
@@ -111,25 +69,6 @@
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}" PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}"
ToolTip.Tip="{x:Static resx:ResUI.MsgServerTitle}" /> ToolTip.Tip="{x:Static resx:ResUI.MsgServerTitle}" />
<Button
x:Name="btnAutofitColumnWidth"
Margin="{StaticResource MarginLr4}"
Classes="IconButton Success"
ToolTip.Tip="{x:Static resx:ResUI.menuProfileAutofitColumnWidth}">
<Button.Content>
<PathIcon Data="{StaticResource SemiIconExpand}" />
</Button.Content>
</Button>
<DataGridColumnChooser
Margin="{StaticResource MarginLr4}"
DataGrid="{Binding ElementName=lstProfiles}"
ToolTip.Tip="{x:Static resx:ResUI.menuColumns}">
<DataGridColumnChooser.Header>
<PathIcon Data="{StaticResource SemiIconSetting}" />
</DataGridColumnChooser.Header>
</DataGridColumnChooser>
<Button <Button
x:Name="btnFastRealPing" x:Name="btnFastRealPing"
Margin="{StaticResource MarginLr4}" Margin="{StaticResource MarginLr4}"
@@ -1,4 +1,5 @@
using Avalonia.Controls.DataGridDragDrop; using Avalonia.Controls.DataGridDragDrop;
using Avalonia.VisualTree;
using DialogHostAvalonia; using DialogHostAvalonia;
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
@@ -17,12 +18,12 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
menuSelectAll.Click += menuSelectAll_Click; menuSelectAll.Click += menuSelectAll_Click;
btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click;
txtServerFilter.KeyDown += TxtServerFilter_KeyDown; txtServerFilter.KeyDown += TxtServerFilter_KeyDown;
lstProfiles.KeyDown += LstProfiles_KeyDown; lstProfiles.KeyDown += LstProfiles_KeyDown;
lstProfiles.SelectionChanged += lstProfiles_SelectionChanged; lstProfiles.SelectionChanged += lstProfiles_SelectionChanged;
lstProfiles.DoubleTapped += LstProfiles_DoubleTapped; lstProfiles.DoubleTapped += LstProfiles_DoubleTapped;
lstProfiles.Sorting += LstProfiles_Sorting; lstProfiles.Sorting += LstProfiles_Sorting;
SetupColumnHeaderContextMenu();
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
@@ -315,11 +316,6 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
} }
} }
private void BtnAutofitColumnWidth_Click(object? sender, RoutedEventArgs e)
{
AutofitColumnWidth();
}
private void AutofitColumnWidth() private void AutofitColumnWidth()
{ {
try try
@@ -401,6 +397,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
} }
} }
} }
lstProfiles.FrozenColumnCount = _config.UiItem.FrozenColumnCount ?? 0;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -427,6 +424,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
{ {
try try
{ {
_config.UiItem.FrozenColumnCount = lstProfiles.FrozenColumnCount;
List<ColumnItem> lvColumnItem = []; List<ColumnItem> lvColumnItem = [];
foreach (var item2 in lstProfiles.Columns) foreach (var item2 in lstProfiles.Columns)
{ {
@@ -449,6 +447,120 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
} }
} }
private void SetupColumnHeaderContextMenu()
{
lstProfiles.AddHandler(PointerPressedEvent, (sender, e) =>
{
if (e.GetCurrentPoint(lstProfiles).Properties.IsRightButtonPressed)
{
var header = (e.Source as Visual)?.FindAncestorOfType<DataGridColumnHeader>();
if (header?.OwningColumn != null)
{
e.Handled = true;
ShowColumnHeaderContextMenu(header, header.OwningColumn);
}
}
}, RoutingStrategies.Tunnel);
}
private void ShowColumnHeaderContextMenu(DataGridColumnHeader header, DataGridColumn column)
{
var menu = new ContextMenu();
var colTitle = column.Header?.ToString() ?? string.Empty;
var visibleColumns = lstProfiles.Columns
.Where(c => c.IsVisible)
.OrderBy(c => c.DisplayIndex < 0 ? int.MaxValue : c.DisplayIndex)
.ToList();
var targetIndex = visibleColumns.IndexOf(column);
var targetCount = targetIndex >= 0 ? targetIndex + 1 : 0;
var autofitColumnWidthColumn = new MenuItem
{
Header = ResUI.menuProfileAutofitColumnWidth,
};
autofitColumnWidthColumn.Click += (s, ev) =>
{
AutofitColumnWidth();
};
var hideColumn = new MenuItem
{
Header = ResUI.menuHideThisColumn,
IsEnabled = column.IsVisible,
};
hideColumn.Click += (s, ev) =>
{
column.IsVisible = false;
};
var showColumns = new MenuItem
{
Header = ResUI.menuShowColumns,
};
var dgColumnLst = lstProfiles.Columns.OrderBy(c => c.DisplayIndex < 0 ? int.MaxValue : c.DisplayIndex).ToList();
foreach (var col in dgColumnLst)
{
var columnItem = new MenuItem
{
Header = col.Header,
IsChecked = col.IsVisible,
IsEnabled = col.CanUserHide,
Padding = new Thickness(6, 2),
StaysOpenOnClick = true,
ToggleType = MenuItemToggleType.CheckBox,
};
columnItem.Click += (_, _) => col.IsVisible = columnItem.IsChecked;
showColumns.Items.Add(columnItem);
}
var freezeToHereStr = ResUI.menuFreezeToThisColumn;
if (!string.IsNullOrEmpty(colTitle))
{
freezeToHereStr += $" ({colTitle})";
}
var freezeToHere = new MenuItem
{
Header = freezeToHereStr,
IsEnabled = targetCount > 0,
};
freezeToHere.Click += (s, ev) =>
{
lstProfiles.FrozenColumnCount = targetCount;
};
var unfreezeAll = new MenuItem
{
Header = ResUI.menuUnfreezeAllColumns,
IsEnabled = lstProfiles.FrozenColumnCount > 0,
};
unfreezeAll.Click += (s, ev) =>
{
lstProfiles.FrozenColumnCount = 0;
};
var statusItemStr = string.Format(ResUI.menuCurrentlyFrozenColumns, lstProfiles.FrozenColumnCount);
var statusItem = new MenuItem
{
Header = statusItemStr,
IsEnabled = false,
};
menu.Items.Add(autofitColumnWidthColumn);
menu.Items.Add(new Separator());
menu.Items.Add(hideColumn);
menu.Items.Add(showColumns);
menu.Items.Add(new Separator());
menu.Items.Add(freezeToHere);
menu.Items.Add(unfreezeAll);
menu.Items.Add(new Separator());
menu.Items.Add(statusItem);
header.ContextMenu = menu;
menu.Open(header);
}
#endregion UI #endregion UI
private sealed class ProfileRowDropHandler(ProfilesViewModel viewModel) : IDataGridRowDropHandler private sealed class ProfileRowDropHandler(ProfilesViewModel viewModel) : IDataGridRowDropHandler