feat: 补齐工具箱剩余 MVP 能力
实现轻量图标系统、图标选择器、本地图标导入和关联图标缓存。 补齐外观设置、捕获式快捷键录入、卡片右键菜单、分类图标编辑,以及分类和卡片拖拽排序。 同时将配置数据版本升级到 2,并在导入和加载时拒绝更高版本配置,避免误读未来格式。
This commit is contained in:
@@ -1,10 +1,16 @@
|
|||||||
<Application x:Class="PersonalToolbox.App"
|
<Application x:Class="PersonalToolbox.App"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:views="clr-namespace:PersonalToolbox.Views"
|
||||||
StartupUri="MainWindow.xaml">
|
StartupUri="MainWindow.xaml">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||||
|
<views:IconKeyToTextConverter x:Key="IconKeyToTextConverter" />
|
||||||
<SolidColorBrush x:Key="AppBackgroundBrush" Color="#F7F8FA" />
|
<SolidColorBrush x:Key="AppBackgroundBrush" Color="#F7F8FA" />
|
||||||
<SolidColorBrush x:Key="PanelBackgroundBrush" Color="#FFFFFF" />
|
<SolidColorBrush x:Key="PanelBackgroundBrush" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="CardBackgroundBrush" Color="#FBFCFE" />
|
||||||
|
<SolidColorBrush x:Key="BadgeBackgroundBrush" Color="#EDF2F7" />
|
||||||
|
<SolidColorBrush x:Key="IconBackgroundBrush" Color="#E8F0FF" />
|
||||||
<SolidColorBrush x:Key="BorderBrushSoft" Color="#D9DEE7" />
|
<SolidColorBrush x:Key="BorderBrushSoft" Color="#D9DEE7" />
|
||||||
<SolidColorBrush x:Key="PrimaryBrush" Color="#2563EB" />
|
<SolidColorBrush x:Key="PrimaryBrush" Color="#2563EB" />
|
||||||
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#172033" />
|
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#172033" />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
Background="{StaticResource AppBackgroundBrush}"
|
Background="{StaticResource AppBackgroundBrush}"
|
||||||
Loaded="Window_OnLoaded"
|
Loaded="Window_OnLoaded"
|
||||||
Closing="Window_OnClosing">
|
Closing="Window_OnClosing">
|
||||||
<Grid Margin="16">
|
<Grid x:Name="RootGrid" Margin="16">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
@@ -100,14 +100,32 @@
|
|||||||
Margin="0,10,0,10"
|
Margin="0,10,0,10"
|
||||||
ItemsSource="{Binding Categories}"
|
ItemsSource="{Binding Categories}"
|
||||||
SelectedItem="{Binding SelectedCategory}"
|
SelectedItem="{Binding SelectedCategory}"
|
||||||
|
AllowDrop="True"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
ToolTip="单击切换分类;搜索时会跨分类显示结果。">
|
ToolTip="单击切换分类;搜索时会跨分类显示结果。可拖拽分类排序,也可把工具拖到分类中。"
|
||||||
|
PreviewMouseLeftButtonDown="CategoryListBox_OnPreviewMouseLeftButtonDown"
|
||||||
|
MouseMove="CategoryListBox_OnMouseMove"
|
||||||
|
Drop="CategoryListBox_OnDrop">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding Name}"
|
<DockPanel Margin="0,2" LastChildFill="True">
|
||||||
Padding="8,7"
|
<Border Width="32"
|
||||||
TextTrimming="CharacterEllipsis"
|
Height="26"
|
||||||
ToolTip="{Binding Name}" />
|
CornerRadius="6"
|
||||||
|
Background="{StaticResource IconBackgroundBrush}"
|
||||||
|
DockPanel.Dock="Left">
|
||||||
|
<TextBlock Text="{Binding IconKey, Converter={StaticResource IconKeyToTextConverter}}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="11"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryBrush}" />
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="{Binding Name}"
|
||||||
|
Padding="8,7"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
ToolTip="{Binding Name}" />
|
||||||
|
</DockPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
@@ -120,6 +138,10 @@
|
|||||||
Margin="0,8,0,0"
|
Margin="0,8,0,0"
|
||||||
Command="{Binding RenameCategoryCommand}"
|
Command="{Binding RenameCategoryCommand}"
|
||||||
ToolTip="重命名当前选中的分类。" />
|
ToolTip="重命名当前选中的分类。" />
|
||||||
|
<Button Content="分类图标"
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
Command="{Binding EditCategoryIconCommand}"
|
||||||
|
ToolTip="为当前分类选择内置图标或本地图标。" />
|
||||||
<Button Content="删除分类"
|
<Button Content="删除分类"
|
||||||
Margin="0,8,0,0"
|
Margin="0,8,0,0"
|
||||||
Command="{Binding DeleteCategoryCommand}"
|
Command="{Binding DeleteCategoryCommand}"
|
||||||
@@ -137,12 +159,16 @@
|
|||||||
<ListBox x:Name="ToolsListBox"
|
<ListBox x:Name="ToolsListBox"
|
||||||
ItemsSource="{Binding Tools}"
|
ItemsSource="{Binding Tools}"
|
||||||
SelectedItem="{Binding SelectedTool}"
|
SelectedItem="{Binding SelectedTool}"
|
||||||
|
AllowDrop="True"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
MouseDoubleClick="ToolsListBox_OnMouseDoubleClick"
|
MouseDoubleClick="ToolsListBox_OnMouseDoubleClick"
|
||||||
PreviewMouseRightButtonDown="ToolsListBox_OnPreviewMouseRightButtonDown"
|
PreviewMouseRightButtonDown="ToolsListBox_OnPreviewMouseRightButtonDown"
|
||||||
|
PreviewMouseLeftButtonDown="ToolsListBox_OnPreviewMouseLeftButtonDown"
|
||||||
|
MouseMove="ToolsListBox_OnMouseMove"
|
||||||
|
Drop="ToolsListBox_OnDrop"
|
||||||
KeyDown="ToolsListBox_OnKeyDown"
|
KeyDown="ToolsListBox_OnKeyDown"
|
||||||
ToolTip="单击选中卡片,双击启动,右键打开管理菜单。">
|
ToolTip="单击选中卡片,双击启动,右键打开管理菜单。可拖拽调整卡片顺序。">
|
||||||
<ListBox.ItemsPanel>
|
<ListBox.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<WrapPanel IsItemsHost="True" />
|
<WrapPanel IsItemsHost="True" />
|
||||||
@@ -150,23 +176,39 @@
|
|||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate DataType="{x:Type vm:ToolCardViewModel}">
|
<DataTemplate DataType="{x:Type vm:ToolCardViewModel}">
|
||||||
<Border Width="210"
|
<Border Width="{Binding CardWidth}"
|
||||||
Height="146"
|
Height="{Binding CardHeight}"
|
||||||
Margin="6"
|
Margin="6"
|
||||||
Padding="12"
|
Padding="12"
|
||||||
CornerRadius="8"
|
CornerRadius="8"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
BorderBrush="{StaticResource BorderBrushSoft}"
|
BorderBrush="{StaticResource BorderBrushSoft}"
|
||||||
Background="#FBFCFE"
|
Background="{StaticResource CardBackgroundBrush}"
|
||||||
ToolTip="{Binding DetailText}">
|
ToolTip="{Binding DetailText}">
|
||||||
<Border.ContextMenu>
|
<Border.ContextMenu>
|
||||||
<ContextMenu>
|
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||||
<MenuItem Header="启动"
|
<MenuItem Header="启动"
|
||||||
ToolTip="启动该工具或组合。"
|
ToolTip="启动该工具或组合。"
|
||||||
Click="ContextLaunch_OnClick" />
|
Click="ContextLaunch_OnClick" />
|
||||||
<MenuItem Header="编辑"
|
<MenuItem Header="编辑"
|
||||||
ToolTip="编辑该工具或组合。"
|
ToolTip="编辑该工具或组合。"
|
||||||
Click="ContextEdit_OnClick" />
|
Click="ContextEdit_OnClick" />
|
||||||
|
<MenuItem Header="重命名"
|
||||||
|
ToolTip="只修改卡片显示名称。"
|
||||||
|
Click="ContextRename_OnClick" />
|
||||||
|
<MenuItem Header="复制"
|
||||||
|
ToolTip="复制该工具,并清除复制项的快捷键。"
|
||||||
|
Click="ContextDuplicate_OnClick" />
|
||||||
|
<MenuItem Header="移动到分类..."
|
||||||
|
ToolTip="选择一个分类并移动该工具。"
|
||||||
|
Click="ContextMoveToCategory_OnClick" />
|
||||||
|
<MenuItem Header="{Binding AutoRunMenuHeader}"
|
||||||
|
ToolTip="加入或取消工具箱启动时自动运行。"
|
||||||
|
Click="ContextToggleAutoRun_OnClick" />
|
||||||
|
<MenuItem Header="修改路径"
|
||||||
|
IsEnabled="{Binding CanFixPath}"
|
||||||
|
ToolTip="路径失效时快速打开编辑窗口修改目标路径。"
|
||||||
|
Click="ContextFixPath_OnClick" />
|
||||||
<MenuItem Header="删除"
|
<MenuItem Header="删除"
|
||||||
ToolTip="删除该工具。"
|
ToolTip="删除该工具。"
|
||||||
Click="ContextDelete_OnClick" />
|
Click="ContextDelete_OnClick" />
|
||||||
@@ -184,14 +226,41 @@
|
|||||||
<Border Width="42"
|
<Border Width="42"
|
||||||
Height="32"
|
Height="32"
|
||||||
CornerRadius="6"
|
CornerRadius="6"
|
||||||
Background="#E8F0FF"
|
Background="{StaticResource IconBackgroundBrush}"
|
||||||
DockPanel.Dock="Left"
|
DockPanel.Dock="Left"
|
||||||
ToolTip="工具图标占位;后续会扩展自动图标和图标库。">
|
ToolTip="工具图标,可来自自动缓存、内置图标库或本地图片。">
|
||||||
<TextBlock Text="{Binding IconText}"
|
<Grid>
|
||||||
HorizontalAlignment="Center"
|
<Image Source="{Binding IconImagePath}"
|
||||||
VerticalAlignment="Center"
|
Margin="4"
|
||||||
FontWeight="SemiBold"
|
Stretch="Uniform">
|
||||||
Foreground="{StaticResource PrimaryBrush}" />
|
<Image.Style>
|
||||||
|
<Style TargetType="Image">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding HasIconImage}" Value="False">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Image.Style>
|
||||||
|
</Image>
|
||||||
|
<TextBlock Text="{Binding IconText}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryBrush}">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding HasIconImage}" Value="True">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<TextBlock Text="{Binding TypeLabel}"
|
<TextBlock Text="{Binding TypeLabel}"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
@@ -215,7 +284,18 @@
|
|||||||
Foreground="{StaticResource SecondaryTextBrush}"
|
Foreground="{StaticResource SecondaryTextBrush}"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
ToolTip="{Binding Description}" />
|
ToolTip="{Binding Description}">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ShowDescription}" Value="False">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
<ItemsControl Grid.Row="3"
|
<ItemsControl Grid.Row="3"
|
||||||
ItemsSource="{Binding StatusBadges}"
|
ItemsSource="{Binding StatusBadges}"
|
||||||
@@ -228,7 +308,7 @@
|
|||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Border Background="#EDF2F7"
|
<Border Background="{StaticResource BadgeBackgroundBrush}"
|
||||||
CornerRadius="4"
|
CornerRadius="4"
|
||||||
Padding="5,2"
|
Padding="5,2"
|
||||||
Margin="0,0,5,4">
|
Margin="0,0,5,4">
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ public partial class MainWindow : Window
|
|||||||
private TrayService? _trayService;
|
private TrayService? _trayService;
|
||||||
private bool _initialized;
|
private bool _initialized;
|
||||||
private bool _exitRequested;
|
private bool _exitRequested;
|
||||||
|
private System.Windows.Point _dragStartPoint;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_viewModel = new MainWindowViewModel();
|
_viewModel = new MainWindowViewModel();
|
||||||
_viewModel.HotkeysRefreshRequested += (_, _) => RegisterHotkeys();
|
_viewModel.HotkeysRefreshRequested += (_, _) => RegisterHotkeys();
|
||||||
|
_viewModel.SettingsChanged += (_, _) => ApplyAppearance();
|
||||||
DataContext = _viewModel;
|
DataContext = _viewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +37,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
await _viewModel.InitializeAsync(this);
|
await _viewModel.InitializeAsync(this);
|
||||||
|
ApplyAppearance();
|
||||||
|
|
||||||
Width = _viewModel.Data.Settings.MainWindowWidth;
|
Width = _viewModel.Data.Settings.MainWindowWidth;
|
||||||
Height = _viewModel.Data.Settings.MainWindowHeight;
|
Height = _viewModel.Data.Settings.MainWindowHeight;
|
||||||
@@ -137,6 +140,46 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ContextRename_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_viewModel.RenameSelectedCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
_viewModel.RenameSelectedCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ContextDuplicate_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_viewModel.DuplicateSelectedCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
_viewModel.DuplicateSelectedCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ContextMoveToCategory_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_viewModel.MoveSelectedToCategoryCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
_viewModel.MoveSelectedToCategoryCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ContextToggleAutoRun_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_viewModel.ToggleSelectedAutoRunCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
_viewModel.ToggleSelectedAutoRunCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ContextFixPath_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_viewModel.FixSelectedPathCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
_viewModel.FixSelectedPathCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ContextDelete_OnClick(object sender, RoutedEventArgs e)
|
private void ContextDelete_OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_viewModel.DeleteSelectedCommand.CanExecute(null))
|
if (_viewModel.DeleteSelectedCommand.CanExecute(null))
|
||||||
@@ -161,6 +204,100 @@ public partial class MainWindow : Window
|
|||||||
_trayService?.RefreshMenuText();
|
_trayService?.RefreshMenuText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CategoryListBox_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
_dragStartPoint = e.GetPosition(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CategoryListBox_OnMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.LeftButton != MouseButtonState.Pressed || !IsDragDistanceReached(e.GetPosition(null)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = FindParent<ListBoxItem>((DependencyObject)e.OriginalSource);
|
||||||
|
if (item?.DataContext is CategoryItem category)
|
||||||
|
{
|
||||||
|
DragDrop.DoDragDrop(item, new System.Windows.DataObject("PersonalToolboxCategoryId", category.Id), System.Windows.DragDropEffects.Move);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CategoryListBox_OnDrop(object sender, System.Windows.DragEventArgs e)
|
||||||
|
{
|
||||||
|
var item = FindParent<ListBoxItem>((DependencyObject)e.OriginalSource);
|
||||||
|
if (item?.DataContext is not CategoryItem targetCategory)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Data.GetDataPresent("PersonalToolboxCategoryId"))
|
||||||
|
{
|
||||||
|
var sourceCategoryId = e.Data.GetData("PersonalToolboxCategoryId") as string;
|
||||||
|
if (!string.IsNullOrWhiteSpace(sourceCategoryId))
|
||||||
|
{
|
||||||
|
_viewModel.MoveCategory(sourceCategoryId, targetCategory.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.Data.GetDataPresent("PersonalToolboxToolId"))
|
||||||
|
{
|
||||||
|
var toolId = e.Data.GetData("PersonalToolboxToolId") as string;
|
||||||
|
if (!string.IsNullOrWhiteSpace(toolId))
|
||||||
|
{
|
||||||
|
_viewModel.MoveToolToCategory(toolId, targetCategory.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToolsListBox_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
_dragStartPoint = e.GetPosition(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToolsListBox_OnMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.LeftButton != MouseButtonState.Pressed || !IsDragDistanceReached(e.GetPosition(null)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = FindParent<ListBoxItem>((DependencyObject)e.OriginalSource);
|
||||||
|
if (item?.DataContext is ToolCardViewModel tool)
|
||||||
|
{
|
||||||
|
DragDrop.DoDragDrop(item, new System.Windows.DataObject("PersonalToolboxToolId", tool.Id), System.Windows.DragDropEffects.Move);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToolsListBox_OnDrop(object sender, System.Windows.DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (!e.Data.GetDataPresent("PersonalToolboxToolId"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceToolId = e.Data.GetData("PersonalToolboxToolId") as string;
|
||||||
|
var item = FindParent<ListBoxItem>((DependencyObject)e.OriginalSource);
|
||||||
|
if (string.IsNullOrWhiteSpace(sourceToolId) || item?.DataContext is not ToolCardViewModel targetTool)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewModel.MoveTool(sourceToolId, targetTool.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDragDistanceReached(System.Windows.Point point)
|
||||||
|
{
|
||||||
|
return Math.Abs(point.X - _dragStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance
|
||||||
|
|| Math.Abs(point.Y - _dragStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyAppearance()
|
||||||
|
{
|
||||||
|
AppearanceService.Apply(_viewModel.Data.Settings);
|
||||||
|
var scale = AppearanceService.NormalizeScale(_viewModel.Data.Settings.UiScale);
|
||||||
|
RootGrid.LayoutTransform = new ScaleTransform(scale, scale);
|
||||||
|
}
|
||||||
|
|
||||||
private void RequestExit()
|
private void RequestExit()
|
||||||
{
|
{
|
||||||
if (_viewModel.Data.Settings.ConfirmExit)
|
if (_viewModel.Data.Settings.ConfirmExit)
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ public sealed class AutoRunEntry
|
|||||||
|
|
||||||
public sealed class AppSettings
|
public sealed class AppSettings
|
||||||
{
|
{
|
||||||
public int DataVersion { get; set; } = 1;
|
public int DataVersion { get; set; } = 2;
|
||||||
public bool StartHiddenToTray { get; set; }
|
public bool StartHiddenToTray { get; set; }
|
||||||
public bool HideOnClose { get; set; } = true;
|
public bool HideOnClose { get; set; } = true;
|
||||||
public bool ConfirmExit { get; set; } = true;
|
public bool ConfirmExit { get; set; } = true;
|
||||||
@@ -164,6 +164,8 @@ public sealed class AppSettings
|
|||||||
public bool LogPanelExpanded { get; set; } = true;
|
public bool LogPanelExpanded { get; set; } = true;
|
||||||
public string Theme { get; set; } = "FollowSystem";
|
public string Theme { get; set; } = "FollowSystem";
|
||||||
public string CardSize { get; set; } = "Medium";
|
public string CardSize { get; set; } = "Medium";
|
||||||
|
public double UiScale { get; set; } = 1.0;
|
||||||
|
public bool ShowToolDescriptions { get; set; } = true;
|
||||||
public double MainWindowWidth { get; set; } = 1120;
|
public double MainWindowWidth { get; set; } = 1120;
|
||||||
public double MainWindowHeight { get; set; } = 720;
|
public double MainWindowHeight { get; set; } = 720;
|
||||||
}
|
}
|
||||||
@@ -178,7 +180,7 @@ public sealed class ToolboxData
|
|||||||
|
|
||||||
public sealed class DataEnvelope<T>
|
public sealed class DataEnvelope<T>
|
||||||
{
|
{
|
||||||
public int DataVersion { get; set; } = 1;
|
public int DataVersion { get; set; } = 2;
|
||||||
public List<T> Items { get; set; } = [];
|
public List<T> Items { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
src/PersonalToolbox/Services/AppearanceService.cs
Normal file
63
src/PersonalToolbox/Services/AppearanceService.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using PersonalToolbox.Models;
|
||||||
|
|
||||||
|
namespace PersonalToolbox.Services;
|
||||||
|
|
||||||
|
public static class AppearanceService
|
||||||
|
{
|
||||||
|
public static void Apply(AppSettings settings)
|
||||||
|
{
|
||||||
|
var resources = System.Windows.Application.Current.Resources;
|
||||||
|
var theme = NormalizeTheme(settings.Theme);
|
||||||
|
|
||||||
|
SetBrush(resources, "AppBackgroundBrush", theme == "Dark" ? "#151922" : "#F7F8FA");
|
||||||
|
SetBrush(resources, "PanelBackgroundBrush", theme == "Dark" ? "#1F2530" : "#FFFFFF");
|
||||||
|
SetBrush(resources, "CardBackgroundBrush", theme == "Dark" ? "#252C38" : "#FBFCFE");
|
||||||
|
SetBrush(resources, "BadgeBackgroundBrush", theme == "Dark" ? "#303847" : "#EDF2F7");
|
||||||
|
SetBrush(resources, "IconBackgroundBrush", theme == "Dark" ? "#263D63" : "#E8F0FF");
|
||||||
|
SetBrush(resources, "BorderBrushSoft", theme == "Dark" ? "#3B4658" : "#D9DEE7");
|
||||||
|
SetBrush(resources, "PrimaryBrush", theme == "Dark" ? "#7AA2FF" : "#2563EB");
|
||||||
|
SetBrush(resources, "PrimaryTextBrush", theme == "Dark" ? "#F4F7FB" : "#172033");
|
||||||
|
SetBrush(resources, "SecondaryTextBrush", theme == "Dark" ? "#B6C0D1" : "#5C667A");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double NormalizeScale(double value)
|
||||||
|
{
|
||||||
|
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||||
|
{
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Clamp(value, 0.85, 1.30);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeTheme(string? theme)
|
||||||
|
{
|
||||||
|
return theme switch
|
||||||
|
{
|
||||||
|
"Light" => "Light",
|
||||||
|
"Dark" => "Dark",
|
||||||
|
_ => IsSystemDarkTheme() ? "Dark" : "Light"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSystemDarkTheme()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||||
|
var value = key?.GetValue("AppsUseLightTheme");
|
||||||
|
return value is int intValue && intValue == 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetBrush(ResourceDictionary resources, string key, string color)
|
||||||
|
{
|
||||||
|
resources[key] = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(color));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ namespace PersonalToolbox.Services;
|
|||||||
|
|
||||||
public sealed class ConfigurationService
|
public sealed class ConfigurationService
|
||||||
{
|
{
|
||||||
private const int CurrentDataVersion = 1;
|
public const int CurrentDataVersion = 2;
|
||||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
@@ -25,10 +25,12 @@ public sealed class ConfigurationService
|
|||||||
private string ToolsPath => Path.Combine(ConfigDirectory, "tools.json");
|
private string ToolsPath => Path.Combine(ConfigDirectory, "tools.json");
|
||||||
private string AutoRunPath => Path.Combine(ConfigDirectory, "autorun.json");
|
private string AutoRunPath => Path.Combine(ConfigDirectory, "autorun.json");
|
||||||
|
|
||||||
public ConfigurationService()
|
public ConfigurationService(string? configDirectory = null)
|
||||||
{
|
{
|
||||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
ConfigDirectory = Path.Combine(appData, "PersonalToolbox");
|
ConfigDirectory = string.IsNullOrWhiteSpace(configDirectory)
|
||||||
|
? Path.Combine(appData, "PersonalToolbox")
|
||||||
|
: configDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ToolboxData> LoadAsync(CancellationToken cancellationToken = default)
|
public async Task<ToolboxData> LoadAsync(CancellationToken cancellationToken = default)
|
||||||
@@ -36,14 +38,25 @@ public sealed class ConfigurationService
|
|||||||
EnsureDirectories();
|
EnsureDirectories();
|
||||||
var toolsFileExists = File.Exists(ToolsPath);
|
var toolsFileExists = File.Exists(ToolsPath);
|
||||||
|
|
||||||
|
var settings = await ReadFileAsync<AppSettings>(SettingsPath, cancellationToken) ?? new AppSettings();
|
||||||
|
var categoriesEnvelope = await ReadEnvelopeAsync<CategoryItem>(CategoriesPath, cancellationToken);
|
||||||
|
var toolsEnvelope = await ReadEnvelopeAsync<ToolItem>(ToolsPath, cancellationToken);
|
||||||
|
var autoRunEnvelope = await ReadEnvelopeAsync<AutoRunEntry>(AutoRunPath, cancellationToken);
|
||||||
|
|
||||||
|
EnsureSupportedDataVersion(settings.DataVersion, "appsettings.json");
|
||||||
|
EnsureSupportedDataVersion(categoriesEnvelope.DataVersion, "categories.json");
|
||||||
|
EnsureSupportedDataVersion(toolsEnvelope.DataVersion, "tools.json");
|
||||||
|
EnsureSupportedDataVersion(autoRunEnvelope.DataVersion, "autorun.json");
|
||||||
|
|
||||||
var data = new ToolboxData
|
var data = new ToolboxData
|
||||||
{
|
{
|
||||||
Settings = await ReadFileAsync<AppSettings>(SettingsPath, cancellationToken) ?? new AppSettings(),
|
Settings = settings,
|
||||||
Categories = (await ReadEnvelopeAsync<CategoryItem>(CategoriesPath, cancellationToken)).Items,
|
Categories = categoriesEnvelope.Items,
|
||||||
Tools = (await ReadEnvelopeAsync<ToolItem>(ToolsPath, cancellationToken)).Items,
|
Tools = toolsEnvelope.Items,
|
||||||
AutoRunEntries = (await ReadEnvelopeAsync<AutoRunEntry>(AutoRunPath, cancellationToken)).Items
|
AutoRunEntries = autoRunEnvelope.Items
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MigrateData(data);
|
||||||
data.Settings.DataVersion = CurrentDataVersion;
|
data.Settings.DataVersion = CurrentDataVersion;
|
||||||
SystemToolService.EnsureDefaultCategories(data.Categories);
|
SystemToolService.EnsureDefaultCategories(data.Categories);
|
||||||
if (!toolsFileExists || data.Tools.Count == 0)
|
if (!toolsFileExists || data.Tools.Count == 0)
|
||||||
@@ -101,13 +114,16 @@ public sealed class ConfigurationService
|
|||||||
File.Delete(zipPath);
|
File.Delete(zipPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
ZipFile.CreateFromDirectory(ConfigDirectory, zipPath, CompressionLevel.Optimal, false);
|
using var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create);
|
||||||
|
AddFileIfExists(archive, SettingsPath, "appsettings.json");
|
||||||
|
AddFileIfExists(archive, CategoriesPath, "categories.json");
|
||||||
|
AddFileIfExists(archive, ToolsPath, "tools.json");
|
||||||
|
AddFileIfExists(archive, AutoRunPath, "autorun.json");
|
||||||
|
AddDirectoryIfExists(archive, IconsDirectory, "icons");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ToolboxData> ImportAsync(string sourcePath, CancellationToken cancellationToken = default)
|
public async Task<ToolboxData> ImportAsync(string sourcePath, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
BackupCurrentConfig();
|
|
||||||
|
|
||||||
var importDirectory = sourcePath;
|
var importDirectory = sourcePath;
|
||||||
var tempDirectory = "";
|
var tempDirectory = "";
|
||||||
if (File.Exists(sourcePath) && Path.GetExtension(sourcePath).Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
if (File.Exists(sourcePath) && Path.GetExtension(sourcePath).Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -119,6 +135,9 @@ public sealed class ConfigurationService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
ValidateImportDirectory(importDirectory);
|
||||||
|
BackupCurrentConfig();
|
||||||
|
|
||||||
CopyIfExists(Path.Combine(importDirectory, "appsettings.json"), SettingsPath);
|
CopyIfExists(Path.Combine(importDirectory, "appsettings.json"), SettingsPath);
|
||||||
CopyIfExists(Path.Combine(importDirectory, "categories.json"), CategoriesPath);
|
CopyIfExists(Path.Combine(importDirectory, "categories.json"), CategoriesPath);
|
||||||
CopyIfExists(Path.Combine(importDirectory, "tools.json"), ToolsPath);
|
CopyIfExists(Path.Combine(importDirectory, "tools.json"), ToolsPath);
|
||||||
@@ -266,4 +285,87 @@ public sealed class ConfigurationService
|
|||||||
File.Copy(file, targetPath, true);
|
File.Copy(file, targetPath, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void MigrateData(ToolboxData data)
|
||||||
|
{
|
||||||
|
data.Settings.Theme = data.Settings.Theme switch
|
||||||
|
{
|
||||||
|
"Light" or "Dark" or "FollowSystem" => data.Settings.Theme,
|
||||||
|
_ => "FollowSystem"
|
||||||
|
};
|
||||||
|
data.Settings.CardSize = data.Settings.CardSize switch
|
||||||
|
{
|
||||||
|
"Small" or "Medium" or "Large" => data.Settings.CardSize,
|
||||||
|
_ => "Medium"
|
||||||
|
};
|
||||||
|
data.Settings.UiScale = AppearanceService.NormalizeScale(data.Settings.UiScale);
|
||||||
|
data.Settings.DataVersion = CurrentDataVersion;
|
||||||
|
|
||||||
|
foreach (var category in data.Categories.Where(category => string.IsNullOrWhiteSpace(category.IconKey)))
|
||||||
|
{
|
||||||
|
category.IconKey = "category";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var tool in data.Tools.Where(tool => string.IsNullOrWhiteSpace(tool.IconKey)))
|
||||||
|
{
|
||||||
|
tool.IconKey = tool.Type switch
|
||||||
|
{
|
||||||
|
ToolType.System => "system",
|
||||||
|
ToolType.Url => "link",
|
||||||
|
ToolType.Combination => "combination",
|
||||||
|
_ => "local"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateImportDirectory(string importDirectory)
|
||||||
|
{
|
||||||
|
EnsureSupportedDataVersion(ReadDataVersion(Path.Combine(importDirectory, "appsettings.json")), "appsettings.json");
|
||||||
|
EnsureSupportedDataVersion(ReadDataVersion(Path.Combine(importDirectory, "categories.json")), "categories.json");
|
||||||
|
EnsureSupportedDataVersion(ReadDataVersion(Path.Combine(importDirectory, "tools.json")), "tools.json");
|
||||||
|
EnsureSupportedDataVersion(ReadDataVersion(Path.Combine(importDirectory, "autorun.json")), "autorun.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadDataVersion(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return CurrentDataVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
return document.RootElement.TryGetProperty("dataVersion", out var version) && version.TryGetInt32(out var value)
|
||||||
|
? value
|
||||||
|
: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureSupportedDataVersion(int dataVersion, string fileName)
|
||||||
|
{
|
||||||
|
if (dataVersion > CurrentDataVersion)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"{fileName} 的数据版本 {dataVersion} 高于当前支持的版本 {CurrentDataVersion}。请升级应用后再导入。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddFileIfExists(ZipArchive archive, string sourcePath, string entryName)
|
||||||
|
{
|
||||||
|
if (File.Exists(sourcePath))
|
||||||
|
{
|
||||||
|
archive.CreateEntryFromFile(sourcePath, entryName, CompressionLevel.Optimal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddDirectoryIfExists(ZipArchive archive, string sourceDirectory, string entryPrefix)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(sourceDirectory))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
var relativePath = Path.GetRelativePath(sourceDirectory, file).Replace('\\', '/');
|
||||||
|
archive.CreateEntryFromFile(file, $"{entryPrefix}/{relativePath}", CompressionLevel.Optimal);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using PersonalToolbox.Models;
|
using PersonalToolbox.Models;
|
||||||
|
|
||||||
@@ -214,6 +215,46 @@ public static class HotkeyParser
|
|||||||
return key != 0;
|
return key != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryFormatFromInput(ModifierKeys modifiers, Key key, out string hotkey)
|
||||||
|
{
|
||||||
|
hotkey = "";
|
||||||
|
if ((modifiers & (ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Shift | ModifierKeys.Windows)) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyText = KeyToText(key);
|
||||||
|
if (string.IsNullOrWhiteSpace(keyText))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = new List<string>();
|
||||||
|
if (modifiers.HasFlag(ModifierKeys.Control))
|
||||||
|
{
|
||||||
|
parts.Add("Ctrl");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiers.HasFlag(ModifierKeys.Alt))
|
||||||
|
{
|
||||||
|
parts.Add("Alt");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiers.HasFlag(ModifierKeys.Shift))
|
||||||
|
{
|
||||||
|
parts.Add("Shift");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiers.HasFlag(ModifierKeys.Windows))
|
||||||
|
{
|
||||||
|
parts.Add("Win");
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.Add(keyText);
|
||||||
|
hotkey = Normalize(string.Join("+", parts));
|
||||||
|
return TryParse(hotkey, out _, out _);
|
||||||
|
}
|
||||||
|
|
||||||
private static string NormalizePart(string value)
|
private static string NormalizePart(string value)
|
||||||
{
|
{
|
||||||
var part = value.Trim();
|
var part = value.Trim();
|
||||||
@@ -282,4 +323,47 @@ public static class HotkeyParser
|
|||||||
_ => 0
|
_ => 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string KeyToText(Key key)
|
||||||
|
{
|
||||||
|
if (key is >= Key.A and <= Key.Z)
|
||||||
|
{
|
||||||
|
return key.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key is >= Key.D0 and <= Key.D9)
|
||||||
|
{
|
||||||
|
return ((int)(key - Key.D0)).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key is >= Key.NumPad0 and <= Key.NumPad9)
|
||||||
|
{
|
||||||
|
return ((int)(key - Key.NumPad0)).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key is >= Key.F1 and <= Key.F24)
|
||||||
|
{
|
||||||
|
return key.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return key switch
|
||||||
|
{
|
||||||
|
Key.Escape => "Esc",
|
||||||
|
Key.Tab => "Tab",
|
||||||
|
Key.Space => "Space",
|
||||||
|
Key.Enter or Key.Return => "Enter",
|
||||||
|
Key.Back => "Backspace",
|
||||||
|
Key.Delete => "Delete",
|
||||||
|
Key.Insert => "Insert",
|
||||||
|
Key.Home => "Home",
|
||||||
|
Key.End => "End",
|
||||||
|
Key.PageUp => "PageUp",
|
||||||
|
Key.PageDown => "PageDown",
|
||||||
|
Key.Left => "Left",
|
||||||
|
Key.Up => "Up",
|
||||||
|
Key.Right => "Right",
|
||||||
|
Key.Down => "Down",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
242
src/PersonalToolbox/Services/IconService.cs
Normal file
242
src/PersonalToolbox/Services/IconService.cs
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using PersonalToolbox.Models;
|
||||||
|
|
||||||
|
namespace PersonalToolbox.Services;
|
||||||
|
|
||||||
|
public sealed class IconService
|
||||||
|
{
|
||||||
|
private readonly string _iconsDirectory;
|
||||||
|
private readonly string _cacheDirectory;
|
||||||
|
private readonly string _customDirectory;
|
||||||
|
|
||||||
|
public IconService(string iconsDirectory)
|
||||||
|
{
|
||||||
|
_iconsDirectory = iconsDirectory;
|
||||||
|
_cacheDirectory = Path.Combine(_iconsDirectory, "cache");
|
||||||
|
_customDirectory = Path.Combine(_iconsDirectory, "custom");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnsureDirectories()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_iconsDirectory);
|
||||||
|
Directory.CreateDirectory(_cacheDirectory);
|
||||||
|
Directory.CreateDirectory(_customDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnsureToolIcon(ToolItem tool)
|
||||||
|
{
|
||||||
|
if (!ShouldRefreshDefaultIcon(tool))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tool.IconKey = ResolveDefaultIconKey(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ResolveDefaultIconKey(ToolItem tool)
|
||||||
|
{
|
||||||
|
return tool.Type switch
|
||||||
|
{
|
||||||
|
ToolType.System => string.IsNullOrWhiteSpace(tool.IconKey) ? "system" : tool.IconKey,
|
||||||
|
ToolType.Url => "link",
|
||||||
|
ToolType.Combination => "combination",
|
||||||
|
ToolType.Local => ResolveLocalToolIconKey(tool.LaunchTarget),
|
||||||
|
_ => "toolbox"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? ResolveImagePath(string iconKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(iconKey))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = iconKey.StartsWith("cache:", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? Path.Combine(_cacheDirectory, iconKey["cache:".Length..])
|
||||||
|
: iconKey.StartsWith("custom:", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? Path.Combine(_customDirectory, iconKey["custom:".Length..])
|
||||||
|
: iconKey;
|
||||||
|
|
||||||
|
return File.Exists(path) ? path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ImportCustomIcon(string sourcePath)
|
||||||
|
{
|
||||||
|
EnsureDirectories();
|
||||||
|
var extension = Path.GetExtension(sourcePath);
|
||||||
|
var fileName = $"{Path.GetFileNameWithoutExtension(sourcePath)}_{HashText(sourcePath)}{extension}";
|
||||||
|
var targetPath = Path.Combine(_customDirectory, fileName);
|
||||||
|
File.Copy(sourcePath, targetPath, true);
|
||||||
|
return "custom:" + fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetIconText(string iconKey, ToolType type = ToolType.Local)
|
||||||
|
{
|
||||||
|
var builtIn = IconCatalog.Find(iconKey);
|
||||||
|
if (builtIn is not null)
|
||||||
|
{
|
||||||
|
return builtIn.Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
ToolType.System => "SYS",
|
||||||
|
ToolType.Local => "APP",
|
||||||
|
ToolType.Url => "URL",
|
||||||
|
ToolType.Combination => "COM",
|
||||||
|
_ => "BOX"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetIconName(string iconKey)
|
||||||
|
{
|
||||||
|
return IconCatalog.Find(iconKey)?.Name ?? "自定义图标";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveLocalToolIconKey(string? target)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(target))
|
||||||
|
{
|
||||||
|
return "local";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(target))
|
||||||
|
{
|
||||||
|
return "folder";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(target))
|
||||||
|
{
|
||||||
|
return "local";
|
||||||
|
}
|
||||||
|
|
||||||
|
var cached = TryCacheAssociatedIcon(target);
|
||||||
|
if (!string.IsNullOrWhiteSpace(cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetExtension(target).ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
".bat" or ".cmd" or ".ps1" => "script",
|
||||||
|
".txt" or ".md" or ".rtf" => "document",
|
||||||
|
".doc" or ".docx" or ".pdf" or ".xls" or ".xlsx" or ".ppt" or ".pptx" => "document",
|
||||||
|
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".webp" => "image",
|
||||||
|
".mp3" or ".wav" or ".flac" or ".aac" => "audio",
|
||||||
|
".mp4" or ".mov" or ".avi" or ".mkv" => "video",
|
||||||
|
".zip" or ".7z" or ".rar" => "archive",
|
||||||
|
".cs" or ".js" or ".ts" or ".json" or ".xml" or ".html" or ".css" => "code",
|
||||||
|
_ => "file"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? TryCacheAssociatedIcon(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureDirectories();
|
||||||
|
using var icon = Icon.ExtractAssociatedIcon(path);
|
||||||
|
if (icon is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = $"{HashText(path)}.png";
|
||||||
|
var cachePath = Path.Combine(_cacheDirectory, fileName);
|
||||||
|
if (!File.Exists(cachePath))
|
||||||
|
{
|
||||||
|
using var bitmap = icon.ToBitmap();
|
||||||
|
bitmap.Save(cachePath, ImageFormat.Png);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "cache:" + fileName;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldRefreshDefaultIcon(ToolItem tool)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(tool.IconKey))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tool.Type switch
|
||||||
|
{
|
||||||
|
ToolType.Local => tool.IconKey is "toolbox" or "local" or "folder",
|
||||||
|
ToolType.Url => tool.IconKey is "toolbox" or "link",
|
||||||
|
ToolType.Combination => tool.IconKey is "toolbox",
|
||||||
|
ToolType.System => false,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HashText(string value)
|
||||||
|
{
|
||||||
|
var hash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(value.ToLowerInvariant()));
|
||||||
|
return Convert.ToHexString(hash)[..16].ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record IconDefinition(string Key, string Name, string Group, string Text);
|
||||||
|
|
||||||
|
public static class IconCatalog
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyList<IconDefinition> Definitions =
|
||||||
|
[
|
||||||
|
new("toolbox", "工具箱", "通用", "BOX"),
|
||||||
|
new("category", "分类", "通用", "TAG"),
|
||||||
|
new("system", "系统", "系统", "SYS"),
|
||||||
|
new("settings", "设置", "系统", "SET"),
|
||||||
|
new("notepad", "记事本", "系统", "TXT"),
|
||||||
|
new("calculator", "计算器", "系统", "123"),
|
||||||
|
new("taskmgr", "任务管理器", "系统", "CPU"),
|
||||||
|
new("control", "控制面板", "系统", "CTL"),
|
||||||
|
new("device", "设备管理器", "系统", "DEV"),
|
||||||
|
new("disk", "磁盘", "系统", "DSK"),
|
||||||
|
new("service", "服务", "系统", "SVC"),
|
||||||
|
new("registry", "注册表", "系统", "REG"),
|
||||||
|
new("network", "网络", "系统", "NET"),
|
||||||
|
new("apps", "应用列表", "系统", "APP"),
|
||||||
|
new("local", "本地工具", "文件", "APP"),
|
||||||
|
new("file", "文件", "文件", "FIL"),
|
||||||
|
new("folder", "文件夹", "文件", "DIR"),
|
||||||
|
new("document", "文档", "文件", "DOC"),
|
||||||
|
new("image", "图片", "文件", "IMG"),
|
||||||
|
new("video", "视频", "文件", "VID"),
|
||||||
|
new("audio", "音频", "文件", "AUD"),
|
||||||
|
new("archive", "压缩包", "文件", "ZIP"),
|
||||||
|
new("code", "代码", "文件", "COD"),
|
||||||
|
new("script", "脚本", "操作", "CMD"),
|
||||||
|
new("link", "网址", "操作", "URL"),
|
||||||
|
new("combination", "组合", "工作区", "COM"),
|
||||||
|
new("work", "工作", "工作区", "WRK"),
|
||||||
|
new("study", "学习", "工作区", "STD"),
|
||||||
|
new("edit", "剪辑", "工作区", "CUT"),
|
||||||
|
new("design", "设计", "工作区", "DSN"),
|
||||||
|
new("dev", "开发", "工作区", "DEV"),
|
||||||
|
new("ai", "AI", "工作区", "AI"),
|
||||||
|
new("star", "星标", "通用", "STR"),
|
||||||
|
new("flash", "闪电", "通用", "PWR"),
|
||||||
|
new("grid", "网格", "通用", "GRD")
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IReadOnlyList<IconDefinition> All => Definitions;
|
||||||
|
|
||||||
|
public static IconDefinition? Find(string? key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key) || key.StartsWith("cache:", StringComparison.OrdinalIgnoreCase) || key.StartsWith("custom:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Definitions.FirstOrDefault(icon => icon.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
private readonly AutoRunService _autoRunService;
|
private readonly AutoRunService _autoRunService;
|
||||||
private readonly PathValidationService _pathValidationService;
|
private readonly PathValidationService _pathValidationService;
|
||||||
private readonly StartupService _startupService;
|
private readonly StartupService _startupService;
|
||||||
|
private readonly IconService _iconService;
|
||||||
private ToolboxData _data = new();
|
private ToolboxData _data = new();
|
||||||
private CategoryItem? _selectedCategory;
|
private CategoryItem? _selectedCategory;
|
||||||
private ToolCardViewModel? _selectedTool;
|
private ToolCardViewModel? _selectedTool;
|
||||||
@@ -29,16 +30,23 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
_autoRunService = new AutoRunService(_toolLaunchService);
|
_autoRunService = new AutoRunService(_toolLaunchService);
|
||||||
_pathValidationService = new PathValidationService();
|
_pathValidationService = new PathValidationService();
|
||||||
_startupService = new StartupService();
|
_startupService = new StartupService();
|
||||||
|
_iconService = new IconService(_configurationService.IconsDirectory);
|
||||||
|
|
||||||
AddLocalToolCommand = new RelayCommand(AddLocalTool);
|
AddLocalToolCommand = new RelayCommand(AddLocalTool);
|
||||||
AddUrlToolCommand = new RelayCommand(AddUrlTool);
|
AddUrlToolCommand = new RelayCommand(AddUrlTool);
|
||||||
AddCombinationCommand = new RelayCommand(AddCombination);
|
AddCombinationCommand = new RelayCommand(AddCombination);
|
||||||
LaunchSelectedCommand = new AsyncRelayCommand(LaunchSelectedAsync, () => SelectedTool is not null);
|
LaunchSelectedCommand = new AsyncRelayCommand(LaunchSelectedAsync, () => SelectedTool is not null);
|
||||||
EditSelectedCommand = new RelayCommand(EditSelectedTool, () => SelectedTool is not null);
|
EditSelectedCommand = new RelayCommand(EditSelectedTool, () => SelectedTool is not null);
|
||||||
|
RenameSelectedCommand = new RelayCommand(RenameSelectedTool, () => SelectedTool is not null);
|
||||||
|
DuplicateSelectedCommand = new RelayCommand(DuplicateSelectedTool, () => SelectedTool is not null);
|
||||||
|
ToggleSelectedAutoRunCommand = new RelayCommand(ToggleSelectedAutoRun, () => SelectedTool is not null);
|
||||||
|
MoveSelectedToCategoryCommand = new RelayCommand(MoveSelectedToCategory, () => SelectedTool is not null);
|
||||||
|
FixSelectedPathCommand = new RelayCommand(EditSelectedTool, () => SelectedTool?.CanFixPath == true);
|
||||||
DeleteSelectedCommand = new RelayCommand(DeleteSelectedTool, () => SelectedTool is not null);
|
DeleteSelectedCommand = new RelayCommand(DeleteSelectedTool, () => SelectedTool is not null);
|
||||||
OpenSettingsCommand = new RelayCommand(OpenSettings);
|
OpenSettingsCommand = new RelayCommand(OpenSettings);
|
||||||
AddCategoryCommand = new RelayCommand(AddCategory);
|
AddCategoryCommand = new RelayCommand(AddCategory);
|
||||||
RenameCategoryCommand = new RelayCommand(RenameCategory, () => SelectedCategory is not null);
|
RenameCategoryCommand = new RelayCommand(RenameCategory, () => SelectedCategory is not null);
|
||||||
|
EditCategoryIconCommand = new RelayCommand(EditCategoryIcon, () => SelectedCategory is not null);
|
||||||
DeleteCategoryCommand = new RelayCommand(DeleteCategory, () => SelectedCategory is not null && SelectedCategory.Id != SystemToolService.UncategorizedCategoryId);
|
DeleteCategoryCommand = new RelayCommand(DeleteCategory, () => SelectedCategory is not null && SelectedCategory.Id != SystemToolService.UncategorizedCategoryId);
|
||||||
ClearLogsCommand = new RelayCommand(() => Logs.Clear());
|
ClearLogsCommand = new RelayCommand(() => Logs.Clear());
|
||||||
CopyLogsCommand = new RelayCommand(CopyLogs, () => Logs.Count > 0);
|
CopyLogsCommand = new RelayCommand(CopyLogs, () => Logs.Count > 0);
|
||||||
@@ -46,6 +54,7 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
public event EventHandler? HotkeysRefreshRequested;
|
public event EventHandler? HotkeysRefreshRequested;
|
||||||
|
public event EventHandler? SettingsChanged;
|
||||||
|
|
||||||
public Window? Owner { get; set; }
|
public Window? Owner { get; set; }
|
||||||
|
|
||||||
@@ -58,10 +67,16 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
public RelayCommand AddCombinationCommand { get; }
|
public RelayCommand AddCombinationCommand { get; }
|
||||||
public AsyncRelayCommand LaunchSelectedCommand { get; }
|
public AsyncRelayCommand LaunchSelectedCommand { get; }
|
||||||
public RelayCommand EditSelectedCommand { get; }
|
public RelayCommand EditSelectedCommand { get; }
|
||||||
|
public RelayCommand RenameSelectedCommand { get; }
|
||||||
|
public RelayCommand DuplicateSelectedCommand { get; }
|
||||||
|
public RelayCommand ToggleSelectedAutoRunCommand { get; }
|
||||||
|
public RelayCommand MoveSelectedToCategoryCommand { get; }
|
||||||
|
public RelayCommand FixSelectedPathCommand { get; }
|
||||||
public RelayCommand DeleteSelectedCommand { get; }
|
public RelayCommand DeleteSelectedCommand { get; }
|
||||||
public RelayCommand OpenSettingsCommand { get; }
|
public RelayCommand OpenSettingsCommand { get; }
|
||||||
public RelayCommand AddCategoryCommand { get; }
|
public RelayCommand AddCategoryCommand { get; }
|
||||||
public RelayCommand RenameCategoryCommand { get; }
|
public RelayCommand RenameCategoryCommand { get; }
|
||||||
|
public RelayCommand EditCategoryIconCommand { get; }
|
||||||
public RelayCommand DeleteCategoryCommand { get; }
|
public RelayCommand DeleteCategoryCommand { get; }
|
||||||
public RelayCommand ClearLogsCommand { get; }
|
public RelayCommand ClearLogsCommand { get; }
|
||||||
public RelayCommand CopyLogsCommand { get; }
|
public RelayCommand CopyLogsCommand { get; }
|
||||||
@@ -126,6 +141,12 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
Owner = owner;
|
Owner = owner;
|
||||||
_data = await _configurationService.LoadAsync();
|
_data = await _configurationService.LoadAsync();
|
||||||
_isLogPanelExpanded = _data.Settings.LogPanelExpanded;
|
_isLogPanelExpanded = _data.Settings.LogPanelExpanded;
|
||||||
|
_iconService.EnsureDirectories();
|
||||||
|
foreach (var tool in _data.Tools.Where(tool => !tool.IsDeleted))
|
||||||
|
{
|
||||||
|
_iconService.EnsureToolIcon(tool);
|
||||||
|
}
|
||||||
|
|
||||||
OnPropertyChanged(nameof(IsLogPanelExpanded));
|
OnPropertyChanged(nameof(IsLogPanelExpanded));
|
||||||
|
|
||||||
var pathReport = _pathValidationService.ValidateTools(_data.Tools);
|
var pathReport = _pathValidationService.ValidateTools(_data.Tools);
|
||||||
@@ -222,12 +243,13 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
tool.Name = "新的本地工具";
|
tool.Name = "新的本地工具";
|
||||||
tool.IconKey = "folder";
|
tool.IconKey = "folder";
|
||||||
|
|
||||||
var edited = ToolEditorWindow.Edit(tool, Categories, Owner);
|
var edited = ToolEditorWindow.Edit(tool, Categories, _iconService, Owner);
|
||||||
if (edited is null)
|
if (edited is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_iconService.EnsureToolIcon(edited);
|
||||||
edited.SortOrder = NextToolSortOrder();
|
edited.SortOrder = NextToolSortOrder();
|
||||||
_data.Tools.Add(edited);
|
_data.Tools.Add(edited);
|
||||||
_ = _pathValidationService.ValidateTools(_data.Tools);
|
_ = _pathValidationService.ValidateTools(_data.Tools);
|
||||||
@@ -243,12 +265,13 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
tool.Name = "新的网址";
|
tool.Name = "新的网址";
|
||||||
tool.IconKey = "link";
|
tool.IconKey = "link";
|
||||||
|
|
||||||
var edited = ToolEditorWindow.Edit(tool, Categories, Owner);
|
var edited = ToolEditorWindow.Edit(tool, Categories, _iconService, Owner);
|
||||||
if (edited is null)
|
if (edited is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_iconService.EnsureToolIcon(edited);
|
||||||
edited.SortOrder = NextToolSortOrder();
|
edited.SortOrder = NextToolSortOrder();
|
||||||
_data.Tools.Add(edited);
|
_data.Tools.Add(edited);
|
||||||
RefreshTools();
|
RefreshTools();
|
||||||
@@ -264,12 +287,13 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
tool.IconKey = "combination";
|
tool.IconKey = "combination";
|
||||||
tool.Combination = new CombinationConfig();
|
tool.Combination = new CombinationConfig();
|
||||||
|
|
||||||
var edited = CombinationEditorWindow.Edit(tool, Categories, _data.Tools, _toolLaunchService, Owner);
|
var edited = CombinationEditorWindow.Edit(tool, Categories, _data.Tools, _toolLaunchService, _iconService, Owner);
|
||||||
if (edited is null)
|
if (edited is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_iconService.EnsureToolIcon(edited);
|
||||||
edited.SortOrder = NextToolSortOrder();
|
edited.SortOrder = NextToolSortOrder();
|
||||||
_data.Tools.Add(edited);
|
_data.Tools.Add(edited);
|
||||||
RefreshTools();
|
RefreshTools();
|
||||||
@@ -288,14 +312,15 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
var original = SelectedTool.Tool;
|
var original = SelectedTool.Tool;
|
||||||
var candidate = original.Clone();
|
var candidate = original.Clone();
|
||||||
ToolItem? edited = original.Type == ToolType.Combination
|
ToolItem? edited = original.Type == ToolType.Combination
|
||||||
? CombinationEditorWindow.Edit(candidate, Categories, _data.Tools, _toolLaunchService, Owner)
|
? CombinationEditorWindow.Edit(candidate, Categories, _data.Tools, _toolLaunchService, _iconService, Owner)
|
||||||
: ToolEditorWindow.Edit(candidate, Categories, Owner);
|
: ToolEditorWindow.Edit(candidate, Categories, _iconService, Owner);
|
||||||
|
|
||||||
if (edited is null)
|
if (edited is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_iconService.EnsureToolIcon(edited);
|
||||||
CopyToolValues(edited, original);
|
CopyToolValues(edited, original);
|
||||||
_ = _pathValidationService.ValidateTools(_data.Tools);
|
_ = _pathValidationService.ValidateTools(_data.Tools);
|
||||||
RefreshTools();
|
RefreshTools();
|
||||||
@@ -304,6 +329,83 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
RefreshHotkeys();
|
RefreshHotkeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RenameSelectedTool()
|
||||||
|
{
|
||||||
|
if (SelectedTool is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tool = SelectedTool.Tool;
|
||||||
|
var name = PromptWindow.Prompt("重命名工具", "工具名称", tool.Name, Owner);
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tool.Name = name.Trim();
|
||||||
|
RefreshTools();
|
||||||
|
AddLog(LogLevel.Success, $"已重命名工具:{tool.Name}");
|
||||||
|
_ = SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DuplicateSelectedTool()
|
||||||
|
{
|
||||||
|
if (SelectedTool is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var copy = SelectedTool.Tool.Clone();
|
||||||
|
copy.Id = Guid.NewGuid().ToString("N");
|
||||||
|
copy.Name = $"{copy.Name} 副本";
|
||||||
|
copy.SortOrder = NextToolSortOrder();
|
||||||
|
copy.Hotkey = null;
|
||||||
|
copy.HotkeyStatus = "未设置";
|
||||||
|
copy.IsDeleted = false;
|
||||||
|
if (copy.Type == ToolType.System)
|
||||||
|
{
|
||||||
|
copy.SystemToolKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_data.Tools.Add(copy);
|
||||||
|
RefreshTools();
|
||||||
|
SelectedTool = Tools.FirstOrDefault(tool => tool.Id == copy.Id);
|
||||||
|
AddLog(LogLevel.Success, $"已复制工具:{copy.Name}");
|
||||||
|
_ = SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleSelectedAutoRun()
|
||||||
|
{
|
||||||
|
if (SelectedTool is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tool = SelectedTool.Tool;
|
||||||
|
tool.AutoRunEnabled = !tool.AutoRunEnabled;
|
||||||
|
_configurationService.MergeAutoRunEntries(_data);
|
||||||
|
RefreshTools();
|
||||||
|
AddLog(LogLevel.Success, tool.AutoRunEnabled ? $"已加入启动时自动运行:{tool.Name}" : $"已取消启动时自动运行:{tool.Name}");
|
||||||
|
_ = SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveSelectedToCategory()
|
||||||
|
{
|
||||||
|
if (SelectedTool is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var category = CategoryPickerWindow.Select(Categories, SelectedTool.Tool.CategoryId, Owner);
|
||||||
|
if (category is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveToolToCategory(SelectedTool.Id, category.Id);
|
||||||
|
}
|
||||||
|
|
||||||
private void DeleteSelectedTool()
|
private void DeleteSelectedTool()
|
||||||
{
|
{
|
||||||
if (SelectedTool is null)
|
if (SelectedTool is null)
|
||||||
@@ -351,6 +453,7 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
RefreshTools();
|
RefreshTools();
|
||||||
AddLog(LogLevel.Success, "设置已保存。");
|
AddLog(LogLevel.Success, "设置已保存。");
|
||||||
_ = SaveAsync();
|
_ = SaveAsync();
|
||||||
|
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
RefreshHotkeys();
|
RefreshHotkeys();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,6 +498,25 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
_ = SaveAsync();
|
_ = SaveAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EditCategoryIcon()
|
||||||
|
{
|
||||||
|
if (SelectedCategory is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var iconKey = IconPickerWindow.SelectIcon(SelectedCategory.IconKey, _iconService, Owner);
|
||||||
|
if (string.IsNullOrWhiteSpace(iconKey))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedCategory.IconKey = iconKey;
|
||||||
|
RefreshCategories();
|
||||||
|
AddLog(LogLevel.Success, $"已更新分类图标:{SelectedCategory.Name}");
|
||||||
|
_ = SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private void DeleteCategory()
|
private void DeleteCategory()
|
||||||
{
|
{
|
||||||
if (SelectedCategory is null)
|
if (SelectedCategory is null)
|
||||||
@@ -492,6 +614,7 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
|
|
||||||
DeleteCategoryCommand.RaiseCanExecuteChanged();
|
DeleteCategoryCommand.RaiseCanExecuteChanged();
|
||||||
RenameCategoryCommand.RaiseCanExecuteChanged();
|
RenameCategoryCommand.RaiseCanExecuteChanged();
|
||||||
|
EditCategoryIconCommand.RaiseCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshTools()
|
private void RefreshTools()
|
||||||
@@ -512,7 +635,7 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
|
|
||||||
foreach (var tool in query.OrderBy(tool => tool.SortOrder).ThenBy(tool => tool.Name))
|
foreach (var tool in query.OrderBy(tool => tool.SortOrder).ThenBy(tool => tool.Name))
|
||||||
{
|
{
|
||||||
Tools.Add(new ToolCardViewModel(tool, ResolveCategoryName));
|
Tools.Add(new ToolCardViewModel(tool, ResolveCategoryName, _iconService.ResolveImagePath, _data.Settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectedTool = Tools.FirstOrDefault(tool => tool.Id == selectedId);
|
SelectedTool = Tools.FirstOrDefault(tool => tool.Id == selectedId);
|
||||||
@@ -539,15 +662,87 @@ public sealed class MainWindowViewModel : ObservableObject
|
|||||||
return _data.Categories.FirstOrDefault(category => category.Id == categoryId)?.Name ?? "未分类";
|
return _data.Categories.FirstOrDefault(category => category.Id == categoryId)?.Name ?? "未分类";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void MoveCategory(string sourceCategoryId, string targetCategoryId)
|
||||||
|
{
|
||||||
|
MoveById(_data.Categories, sourceCategoryId, targetCategoryId, category => category.Id, (category, order) => category.SortOrder = order);
|
||||||
|
RefreshCategories();
|
||||||
|
AddLog(LogLevel.Success, "已调整分类顺序。");
|
||||||
|
_ = SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveTool(string sourceToolId, string targetToolId)
|
||||||
|
{
|
||||||
|
var source = _data.Tools.FirstOrDefault(tool => tool.Id == sourceToolId && !tool.IsDeleted);
|
||||||
|
var target = _data.Tools.FirstOrDefault(tool => tool.Id == targetToolId && !tool.IsDeleted);
|
||||||
|
if (source is null || target is null || source.Id == target.Id)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.CategoryId = target.CategoryId;
|
||||||
|
var visibleTools = _data.Tools
|
||||||
|
.Where(tool => !tool.IsDeleted && tool.CategoryId == target.CategoryId)
|
||||||
|
.OrderBy(tool => tool.SortOrder)
|
||||||
|
.ThenBy(tool => tool.Name)
|
||||||
|
.ToList();
|
||||||
|
MoveById(visibleTools, sourceToolId, targetToolId, tool => tool.Id, (tool, order) => tool.SortOrder = order);
|
||||||
|
RefreshTools();
|
||||||
|
AddLog(LogLevel.Success, $"已调整工具顺序:{source.Name}");
|
||||||
|
_ = SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveToolToCategory(string toolId, string categoryId)
|
||||||
|
{
|
||||||
|
var tool = _data.Tools.FirstOrDefault(item => item.Id == toolId && !item.IsDeleted);
|
||||||
|
var category = _data.Categories.FirstOrDefault(item => item.Id == categoryId);
|
||||||
|
if (tool is null || category is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tool.CategoryId = category.Id;
|
||||||
|
tool.SortOrder = NextToolSortOrder();
|
||||||
|
SelectedCategory = category;
|
||||||
|
RefreshTools();
|
||||||
|
SelectedTool = Tools.FirstOrDefault(item => item.Id == tool.Id);
|
||||||
|
AddLog(LogLevel.Success, $"已移动“{tool.Name}”到分类:{category.Name}");
|
||||||
|
_ = SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private void RaiseSelectionCommandState()
|
private void RaiseSelectionCommandState()
|
||||||
{
|
{
|
||||||
LaunchSelectedCommand.RaiseCanExecuteChanged();
|
LaunchSelectedCommand.RaiseCanExecuteChanged();
|
||||||
EditSelectedCommand.RaiseCanExecuteChanged();
|
EditSelectedCommand.RaiseCanExecuteChanged();
|
||||||
|
RenameSelectedCommand.RaiseCanExecuteChanged();
|
||||||
|
DuplicateSelectedCommand.RaiseCanExecuteChanged();
|
||||||
|
ToggleSelectedAutoRunCommand.RaiseCanExecuteChanged();
|
||||||
|
MoveSelectedToCategoryCommand.RaiseCanExecuteChanged();
|
||||||
|
FixSelectedPathCommand.RaiseCanExecuteChanged();
|
||||||
DeleteSelectedCommand.RaiseCanExecuteChanged();
|
DeleteSelectedCommand.RaiseCanExecuteChanged();
|
||||||
RenameCategoryCommand.RaiseCanExecuteChanged();
|
RenameCategoryCommand.RaiseCanExecuteChanged();
|
||||||
|
EditCategoryIconCommand.RaiseCanExecuteChanged();
|
||||||
DeleteCategoryCommand.RaiseCanExecuteChanged();
|
DeleteCategoryCommand.RaiseCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void MoveById<T>(IList<T> items, string sourceId, string targetId, Func<T, string> idSelector, Action<T, int> setOrder)
|
||||||
|
{
|
||||||
|
var source = items.FirstOrDefault(item => idSelector(item) == sourceId);
|
||||||
|
var target = items.FirstOrDefault(item => idSelector(item) == targetId);
|
||||||
|
if (source is null || target is null || EqualityComparer<T>.Default.Equals(source, target))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Remove(source);
|
||||||
|
var targetIndex = items.IndexOf(target);
|
||||||
|
items.Insert(Math.Max(0, targetIndex), source);
|
||||||
|
|
||||||
|
for (var index = 0; index < items.Count; index++)
|
||||||
|
{
|
||||||
|
setOrder(items[index], index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void CopyToolValues(ToolItem source, ToolItem target)
|
private static void CopyToolValues(ToolItem source, ToolItem target)
|
||||||
{
|
{
|
||||||
target.Name = source.Name;
|
target.Name = source.Name;
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
using PersonalToolbox.Models;
|
using PersonalToolbox.Models;
|
||||||
|
using PersonalToolbox.Services;
|
||||||
|
|
||||||
namespace PersonalToolbox.ViewModels;
|
namespace PersonalToolbox.ViewModels;
|
||||||
|
|
||||||
public sealed class ToolCardViewModel : ObservableObject
|
public sealed class ToolCardViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly Func<string, string> _categoryNameResolver;
|
private readonly Func<string, string> _categoryNameResolver;
|
||||||
|
private readonly Func<string, string?> _iconPathResolver;
|
||||||
|
private readonly AppSettings _settings;
|
||||||
|
|
||||||
public ToolCardViewModel(ToolItem tool, Func<string, string> categoryNameResolver)
|
public ToolCardViewModel(
|
||||||
|
ToolItem tool,
|
||||||
|
Func<string, string> categoryNameResolver,
|
||||||
|
Func<string, string?> iconPathResolver,
|
||||||
|
AppSettings settings)
|
||||||
{
|
{
|
||||||
Tool = tool;
|
Tool = tool;
|
||||||
_categoryNameResolver = categoryNameResolver;
|
_categoryNameResolver = categoryNameResolver;
|
||||||
|
_iconPathResolver = iconPathResolver;
|
||||||
|
_settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ToolItem Tool { get; }
|
public ToolItem Tool { get; }
|
||||||
@@ -18,7 +27,24 @@ public sealed class ToolCardViewModel : ObservableObject
|
|||||||
public string Name => Tool.Name;
|
public string Name => Tool.Name;
|
||||||
public string Description => string.IsNullOrWhiteSpace(Tool.Description) ? "暂无说明" : Tool.Description;
|
public string Description => string.IsNullOrWhiteSpace(Tool.Description) ? "暂无说明" : Tool.Description;
|
||||||
public string CategoryName => _categoryNameResolver(Tool.CategoryId);
|
public string CategoryName => _categoryNameResolver(Tool.CategoryId);
|
||||||
public string IconText => IconKeyToText(Tool.IconKey, Tool.Type);
|
public string IconText => IconService.GetIconText(Tool.IconKey, Tool.Type);
|
||||||
|
public string? IconImagePath => _iconPathResolver(Tool.IconKey);
|
||||||
|
public bool HasIconImage => !string.IsNullOrWhiteSpace(IconImagePath);
|
||||||
|
public bool ShowDescription => _settings.ShowToolDescriptions;
|
||||||
|
public double CardWidth => _settings.CardSize switch
|
||||||
|
{
|
||||||
|
"Small" => 178,
|
||||||
|
"Large" => 248,
|
||||||
|
_ => 210
|
||||||
|
};
|
||||||
|
|
||||||
|
public double CardHeight => _settings.CardSize switch
|
||||||
|
{
|
||||||
|
"Small" => 126,
|
||||||
|
"Large" => 172,
|
||||||
|
_ => 146
|
||||||
|
};
|
||||||
|
|
||||||
public string TypeLabel => Tool.Type switch
|
public string TypeLabel => Tool.Type switch
|
||||||
{
|
{
|
||||||
ToolType.System => "系统",
|
ToolType.System => "系统",
|
||||||
@@ -72,53 +98,24 @@ public sealed class ToolCardViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string AutoRunMenuHeader => Tool.AutoRunEnabled ? "取消启动时自动运行" : "加入启动时自动运行";
|
||||||
|
public bool CanFixPath => Tool.Type == ToolType.Local && Tool.PathInvalid;
|
||||||
|
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(Name));
|
OnPropertyChanged(nameof(Name));
|
||||||
OnPropertyChanged(nameof(Description));
|
OnPropertyChanged(nameof(Description));
|
||||||
OnPropertyChanged(nameof(CategoryName));
|
OnPropertyChanged(nameof(CategoryName));
|
||||||
OnPropertyChanged(nameof(IconText));
|
OnPropertyChanged(nameof(IconText));
|
||||||
|
OnPropertyChanged(nameof(IconImagePath));
|
||||||
|
OnPropertyChanged(nameof(HasIconImage));
|
||||||
|
OnPropertyChanged(nameof(ShowDescription));
|
||||||
|
OnPropertyChanged(nameof(CardWidth));
|
||||||
|
OnPropertyChanged(nameof(CardHeight));
|
||||||
OnPropertyChanged(nameof(TypeLabel));
|
OnPropertyChanged(nameof(TypeLabel));
|
||||||
OnPropertyChanged(nameof(StatusBadges));
|
OnPropertyChanged(nameof(StatusBadges));
|
||||||
OnPropertyChanged(nameof(DetailText));
|
OnPropertyChanged(nameof(DetailText));
|
||||||
}
|
OnPropertyChanged(nameof(AutoRunMenuHeader));
|
||||||
|
OnPropertyChanged(nameof(CanFixPath));
|
||||||
private static string IconKeyToText(string iconKey, ToolType type)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(iconKey))
|
|
||||||
{
|
|
||||||
var mapped = iconKey.ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
"notepad" => "TXT",
|
|
||||||
"calculator" => "123",
|
|
||||||
"taskmgr" => "CPU",
|
|
||||||
"control" => "CTL",
|
|
||||||
"settings" => "SET",
|
|
||||||
"device" => "DEV",
|
|
||||||
"disk" => "DSK",
|
|
||||||
"service" => "SVC",
|
|
||||||
"registry" => "REG",
|
|
||||||
"network" => "NET",
|
|
||||||
"apps" => "APP",
|
|
||||||
"link" => "URL",
|
|
||||||
"folder" => "DIR",
|
|
||||||
"combination" => "COM",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(mapped))
|
|
||||||
{
|
|
||||||
return mapped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
ToolType.System => "SYS",
|
|
||||||
ToolType.Local => "LOC",
|
|
||||||
ToolType.Url => "URL",
|
|
||||||
ToolType.Combination => "COM",
|
|
||||||
_ => "BOX"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
src/PersonalToolbox/Views/CategoryPickerWindow.xaml
Normal file
38
src/PersonalToolbox/Views/CategoryPickerWindow.xaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<Window x:Class="PersonalToolbox.Views.CategoryPickerWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="移动到分类"
|
||||||
|
Width="360"
|
||||||
|
Height="420"
|
||||||
|
MinWidth="320"
|
||||||
|
MinHeight="360"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<ListBox x:Name="CategoriesListBox"
|
||||||
|
DisplayMemberPath="Name"
|
||||||
|
ToolTip="选择工具要移动到的一级分类。"
|
||||||
|
MouseDoubleClick="CategoriesListBox_OnMouseDoubleClick" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,12,0,0">
|
||||||
|
<Button Content="确定"
|
||||||
|
Width="88"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
IsDefault="True"
|
||||||
|
ToolTip="移动到选中的分类。"
|
||||||
|
Click="OkButton_OnClick" />
|
||||||
|
<Button Content="取消"
|
||||||
|
Width="88"
|
||||||
|
IsCancel="True"
|
||||||
|
ToolTip="不移动工具。"
|
||||||
|
Click="CancelButton_OnClick" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
45
src/PersonalToolbox/Views/CategoryPickerWindow.xaml.cs
Normal file
45
src/PersonalToolbox/Views/CategoryPickerWindow.xaml.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using PersonalToolbox.Models;
|
||||||
|
|
||||||
|
namespace PersonalToolbox.Views;
|
||||||
|
|
||||||
|
public partial class CategoryPickerWindow : Window
|
||||||
|
{
|
||||||
|
private CategoryPickerWindow(IEnumerable<CategoryItem> categories, string selectedCategoryId)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
CategoriesListBox.ItemsSource = categories.OrderBy(category => category.SortOrder).ThenBy(category => category.Name).ToList();
|
||||||
|
CategoriesListBox.SelectedItem = CategoriesListBox.Items
|
||||||
|
.OfType<CategoryItem>()
|
||||||
|
.FirstOrDefault(category => category.Id == selectedCategoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CategoryItem? SelectedCategory { get; private set; }
|
||||||
|
|
||||||
|
public static CategoryItem? Select(IEnumerable<CategoryItem> categories, string selectedCategoryId, Window? owner)
|
||||||
|
{
|
||||||
|
var window = new CategoryPickerWindow(categories, selectedCategoryId)
|
||||||
|
{
|
||||||
|
Owner = owner
|
||||||
|
};
|
||||||
|
|
||||||
|
return window.ShowDialog() == true ? window.SelectedCategory : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OkButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
SelectedCategory = CategoriesListBox.SelectedItem as CategoryItem;
|
||||||
|
DialogResult = SelectedCategory is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DialogResult = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CategoriesListBox_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
OkButton_OnClick(sender, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="名称" VerticalAlignment="Center" />
|
<TextBlock Text="名称" VerticalAlignment="Center" />
|
||||||
@@ -54,12 +55,30 @@
|
|||||||
ToolTip="描述这个组合会打开哪些环境或工具。" />
|
ToolTip="描述这个组合会打开哪些环境或工具。" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Text="快捷键" VerticalAlignment="Center" />
|
<TextBlock Grid.Row="2" Text="快捷键" VerticalAlignment="Center" />
|
||||||
<TextBox x:Name="HotkeyTextBox"
|
<Grid Grid.Row="2"
|
||||||
Grid.Row="2"
|
Grid.Column="1"
|
||||||
Grid.Column="1"
|
Margin="0,8,16,0">
|
||||||
MinHeight="32"
|
<Grid.ColumnDefinitions>
|
||||||
Margin="0,8,16,0"
|
<ColumnDefinition Width="*" />
|
||||||
ToolTip="格式示例:Ctrl + Alt + D。组合可被全局快捷键直接启动。" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox x:Name="HotkeyTextBox"
|
||||||
|
MinHeight="32"
|
||||||
|
ToolTip="格式示例:Ctrl + Alt + D。可点击录入来捕获快捷键。" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Content="录入"
|
||||||
|
Width="64"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
ToolTip="打开快捷键捕获窗口。"
|
||||||
|
Click="CaptureHotkeyButton_OnClick" />
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Content="清除"
|
||||||
|
Width="64"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
ToolTip="清除当前快捷键。"
|
||||||
|
Click="ClearHotkeyButton_OnClick" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Grid.Row="2"
|
<StackPanel Grid.Row="2"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
@@ -78,6 +97,33 @@
|
|||||||
<ComboBoxItem Content="失败后停止" Tag="Stop" />
|
<ComboBoxItem Content="失败后停止" Tag="Stop" />
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Text="图标" VerticalAlignment="Center" Margin="0,12,0,0" />
|
||||||
|
<StackPanel Grid.Row="3"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.ColumnSpan="3"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Margin="0,12,0,0">
|
||||||
|
<Border Width="42"
|
||||||
|
Height="30"
|
||||||
|
CornerRadius="6"
|
||||||
|
Background="{StaticResource IconBackgroundBrush}">
|
||||||
|
<TextBlock x:Name="IconPreviewTextBlock"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryBrush}" />
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="IconNameTextBlock"
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource SecondaryTextBrush}" />
|
||||||
|
<Button Content="选择图标"
|
||||||
|
Width="88"
|
||||||
|
Margin="12,0,0,0"
|
||||||
|
ToolTip="从内置图标库选择,或导入本地图片/ico。"
|
||||||
|
Click="ChooseIconButton_OnClick" />
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Row="1" Margin="0,16,0,0">
|
<Grid Grid.Row="1" Margin="0,16,0,0">
|
||||||
|
|||||||
@@ -13,18 +13,21 @@ public partial class CombinationEditorWindow : Window
|
|||||||
private readonly ToolItem _tool;
|
private readonly ToolItem _tool;
|
||||||
private readonly IReadOnlyList<ToolItem> _allTools;
|
private readonly IReadOnlyList<ToolItem> _allTools;
|
||||||
private readonly ToolLaunchService _launchService;
|
private readonly ToolLaunchService _launchService;
|
||||||
|
private readonly IconService _iconService;
|
||||||
private readonly ObservableCollection<CombinationMemberViewModel> _members = [];
|
private readonly ObservableCollection<CombinationMemberViewModel> _members = [];
|
||||||
|
|
||||||
private CombinationEditorWindow(
|
private CombinationEditorWindow(
|
||||||
ToolItem tool,
|
ToolItem tool,
|
||||||
IEnumerable<CategoryItem> categories,
|
IEnumerable<CategoryItem> categories,
|
||||||
IEnumerable<ToolItem> allTools,
|
IEnumerable<ToolItem> allTools,
|
||||||
ToolLaunchService launchService)
|
ToolLaunchService launchService,
|
||||||
|
IconService iconService)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_tool = tool;
|
_tool = tool;
|
||||||
_allTools = allTools.Where(item => !item.IsDeleted).ToList();
|
_allTools = allTools.Where(item => !item.IsDeleted).ToList();
|
||||||
_launchService = launchService;
|
_launchService = launchService;
|
||||||
|
_iconService = iconService;
|
||||||
|
|
||||||
_tool.Combination ??= new CombinationConfig();
|
_tool.Combination ??= new CombinationConfig();
|
||||||
var combination = _tool.Combination;
|
var combination = _tool.Combination;
|
||||||
@@ -39,6 +42,7 @@ public partial class CombinationEditorWindow : Window
|
|||||||
HotkeyTextBox.Text = tool.Hotkey;
|
HotkeyTextBox.Text = tool.Hotkey;
|
||||||
AutoRunCheckBox.IsChecked = tool.AutoRunEnabled;
|
AutoRunCheckBox.IsChecked = tool.AutoRunEnabled;
|
||||||
FailurePolicyComboBox.SelectedIndex = combination.FailurePolicy == FailurePolicy.Stop ? 1 : 0;
|
FailurePolicyComboBox.SelectedIndex = combination.FailurePolicy == FailurePolicy.Stop ? 1 : 0;
|
||||||
|
RefreshIconPreview();
|
||||||
|
|
||||||
foreach (var member in combination.Members.OrderBy(member => member.SortOrder))
|
foreach (var member in combination.Members.OrderBy(member => member.SortOrder))
|
||||||
{
|
{
|
||||||
@@ -53,9 +57,10 @@ public partial class CombinationEditorWindow : Window
|
|||||||
IEnumerable<CategoryItem> categories,
|
IEnumerable<CategoryItem> categories,
|
||||||
IEnumerable<ToolItem> allTools,
|
IEnumerable<ToolItem> allTools,
|
||||||
ToolLaunchService launchService,
|
ToolLaunchService launchService,
|
||||||
|
IconService iconService,
|
||||||
Window? owner)
|
Window? owner)
|
||||||
{
|
{
|
||||||
var window = new CombinationEditorWindow(tool, categories, allTools, launchService)
|
var window = new CombinationEditorWindow(tool, categories, allTools, launchService, iconService)
|
||||||
{
|
{
|
||||||
Owner = owner
|
Owner = owner
|
||||||
};
|
};
|
||||||
@@ -132,7 +137,7 @@ public partial class CombinationEditorWindow : Window
|
|||||||
_tool.CategoryId = categoryId;
|
_tool.CategoryId = categoryId;
|
||||||
_tool.Hotkey = string.IsNullOrWhiteSpace(hotkey) ? null : HotkeyParser.Normalize(hotkey);
|
_tool.Hotkey = string.IsNullOrWhiteSpace(hotkey) ? null : HotkeyParser.Normalize(hotkey);
|
||||||
_tool.AutoRunEnabled = AutoRunCheckBox.IsChecked == true;
|
_tool.AutoRunEnabled = AutoRunCheckBox.IsChecked == true;
|
||||||
_tool.IconKey = "combination";
|
_tool.IconKey = string.IsNullOrWhiteSpace(_tool.IconKey) ? "combination" : _tool.IconKey;
|
||||||
_tool.Combination = new CombinationConfig
|
_tool.Combination = new CombinationConfig
|
||||||
{
|
{
|
||||||
FailurePolicy = FailurePolicyComboBox.SelectedIndex == 1 ? FailurePolicy.Stop : FailurePolicy.Continue,
|
FailurePolicy = FailurePolicyComboBox.SelectedIndex == 1 ? FailurePolicy.Stop : FailurePolicy.Continue,
|
||||||
@@ -150,6 +155,35 @@ public partial class CombinationEditorWindow : Window
|
|||||||
DialogResult = true;
|
DialogResult = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CaptureHotkeyButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var hotkey = HotkeyCaptureWindow.Capture(HotkeyTextBox.Text, this, out var clearRequested);
|
||||||
|
if (clearRequested)
|
||||||
|
{
|
||||||
|
HotkeyTextBox.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HotkeyTextBox.Text = hotkey ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearHotkeyButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
HotkeyTextBox.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChooseIconButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var iconKey = IconPickerWindow.SelectIcon(_tool.IconKey, _iconService, this, "combination");
|
||||||
|
if (string.IsNullOrWhiteSpace(iconKey))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tool.IconKey = iconKey;
|
||||||
|
RefreshIconPreview();
|
||||||
|
}
|
||||||
|
|
||||||
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
|
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
DialogResult = false;
|
DialogResult = false;
|
||||||
@@ -186,4 +220,10 @@ public partial class CombinationEditorWindow : Window
|
|||||||
_members[index].Member.SortOrder = index;
|
_members[index].Member.SortOrder = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshIconPreview()
|
||||||
|
{
|
||||||
|
IconPreviewTextBlock.Text = IconService.GetIconText(_tool.IconKey, ToolType.Combination);
|
||||||
|
IconNameTextBlock.Text = IconService.GetIconName(_tool.IconKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/PersonalToolbox/Views/HotkeyCaptureWindow.xaml
Normal file
57
src/PersonalToolbox/Views/HotkeyCaptureWindow.xaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<Window x:Class="PersonalToolbox.Views.HotkeyCaptureWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="录入快捷键"
|
||||||
|
Width="420"
|
||||||
|
Height="220"
|
||||||
|
MinWidth="380"
|
||||||
|
MinHeight="200"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
PreviewKeyDown="Window_OnPreviewKeyDown">
|
||||||
|
<Grid Margin="18">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border BorderBrush="{StaticResource BorderBrushSoft}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="18"
|
||||||
|
Background="{StaticResource PanelBackgroundBrush}">
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="请按下要使用的快捷键"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryTextBrush}" />
|
||||||
|
<TextBlock Text="需要至少包含 Ctrl、Alt、Shift 或 Win 中的一个修饰键。"
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="{StaticResource SecondaryTextBrush}" />
|
||||||
|
<TextBlock x:Name="CapturedTextBlock"
|
||||||
|
Text="等待输入..."
|
||||||
|
Margin="0,18,0,0"
|
||||||
|
FontSize="22"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryBrush}"
|
||||||
|
ToolTip="捕获到有效组合后会自动关闭窗口。" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,12,0,0">
|
||||||
|
<Button Content="清除"
|
||||||
|
Width="88"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
ToolTip="清除当前快捷键。"
|
||||||
|
Click="ClearButton_OnClick" />
|
||||||
|
<Button Content="取消"
|
||||||
|
Width="88"
|
||||||
|
IsCancel="True"
|
||||||
|
ToolTip="不修改快捷键。"
|
||||||
|
Click="CancelButton_OnClick" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
63
src/PersonalToolbox/Views/HotkeyCaptureWindow.xaml.cs
Normal file
63
src/PersonalToolbox/Views/HotkeyCaptureWindow.xaml.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using PersonalToolbox.Services;
|
||||||
|
|
||||||
|
namespace PersonalToolbox.Views;
|
||||||
|
|
||||||
|
public partial class HotkeyCaptureWindow : Window
|
||||||
|
{
|
||||||
|
private HotkeyCaptureWindow(string? currentHotkey)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
CapturedTextBlock.Text = string.IsNullOrWhiteSpace(currentHotkey) ? "等待输入..." : currentHotkey;
|
||||||
|
Loaded += (_, _) => Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? CapturedHotkey { get; private set; }
|
||||||
|
public bool ClearRequested { get; private set; }
|
||||||
|
|
||||||
|
public static string? Capture(string? currentHotkey, Window? owner, out bool clearRequested)
|
||||||
|
{
|
||||||
|
var window = new HotkeyCaptureWindow(currentHotkey)
|
||||||
|
{
|
||||||
|
Owner = owner
|
||||||
|
};
|
||||||
|
|
||||||
|
var accepted = window.ShowDialog() == true;
|
||||||
|
clearRequested = accepted && window.ClearRequested;
|
||||||
|
return accepted ? window.CapturedHotkey : currentHotkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Window_OnPreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||||
|
{
|
||||||
|
var key = e.Key == Key.System ? e.SystemKey : e.Key == Key.ImeProcessed ? e.ImeProcessedKey : e.Key;
|
||||||
|
if (key is Key.LeftCtrl or Key.RightCtrl or Key.LeftAlt or Key.RightAlt or Key.LeftShift or Key.RightShift or Key.LWin or Key.RWin)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HotkeyParser.TryFormatFromInput(Keyboard.Modifiers, key, out var hotkey))
|
||||||
|
{
|
||||||
|
CapturedTextBlock.Text = "请包含修饰键";
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CapturedHotkey = hotkey;
|
||||||
|
DialogResult = true;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
CapturedHotkey = null;
|
||||||
|
ClearRequested = true;
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DialogResult = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/PersonalToolbox/Views/IconKeyToTextConverter.cs
Normal file
18
src/PersonalToolbox/Views/IconKeyToTextConverter.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using PersonalToolbox.Services;
|
||||||
|
|
||||||
|
namespace PersonalToolbox.Views;
|
||||||
|
|
||||||
|
public sealed class IconKeyToTextConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return IconService.GetIconText(value as string ?? "", Models.ToolType.Local);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return System.Windows.Data.Binding.DoNothing;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/PersonalToolbox/Views/IconPickerWindow.xaml
Normal file
82
src/PersonalToolbox/Views/IconPickerWindow.xaml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<Window x:Class="PersonalToolbox.Views.IconPickerWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="选择图标"
|
||||||
|
Width="520"
|
||||||
|
Height="520"
|
||||||
|
MinWidth="460"
|
||||||
|
MinHeight="440"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="自动/默认"
|
||||||
|
Width="96"
|
||||||
|
ToolTip="恢复为该工具或分类的默认图标。"
|
||||||
|
Click="DefaultButton_OnClick" />
|
||||||
|
<Button Content="选择本地图片"
|
||||||
|
Width="120"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
ToolTip="复制 png、jpg、bmp 或 ico 到配置目录的 icons/custom。"
|
||||||
|
Click="LocalIconButton_OnClick" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ListBox x:Name="IconsListBox"
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
ToolTip="从轻量内置图标库中选择一个图标。"
|
||||||
|
MouseDoubleClick="IconsListBox_OnMouseDoubleClick">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid Margin="4" MinHeight="38">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="52" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="96" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Width="42"
|
||||||
|
Height="30"
|
||||||
|
CornerRadius="6"
|
||||||
|
Background="{StaticResource IconBackgroundBrush}">
|
||||||
|
<TextBlock Text="{Binding Text}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryBrush}" />
|
||||||
|
</Border>
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
Text="{Binding Name}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource PrimaryTextBrush}" />
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
Text="{Binding Group}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource SecondaryTextBrush}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,12,0,0">
|
||||||
|
<Button Content="确定"
|
||||||
|
Width="88"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
IsDefault="True"
|
||||||
|
ToolTip="使用选中的图标。"
|
||||||
|
Click="OkButton_OnClick" />
|
||||||
|
<Button Content="取消"
|
||||||
|
Width="88"
|
||||||
|
IsCancel="True"
|
||||||
|
ToolTip="不修改图标。"
|
||||||
|
Click="CancelButton_OnClick" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
90
src/PersonalToolbox/Views/IconPickerWindow.xaml.cs
Normal file
90
src/PersonalToolbox/Views/IconPickerWindow.xaml.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using PersonalToolbox.Services;
|
||||||
|
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
|
||||||
|
|
||||||
|
namespace PersonalToolbox.Views;
|
||||||
|
|
||||||
|
public partial class IconPickerWindow : Window
|
||||||
|
{
|
||||||
|
private readonly IconService _iconService;
|
||||||
|
private readonly string _defaultIconKey;
|
||||||
|
|
||||||
|
private IconPickerWindow(string currentIconKey, string defaultIconKey, IconService iconService)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_defaultIconKey = defaultIconKey;
|
||||||
|
_iconService = iconService;
|
||||||
|
IconsListBox.ItemsSource = IconCatalog.All
|
||||||
|
.OrderBy(icon => icon.Group)
|
||||||
|
.ThenBy(icon => icon.Name)
|
||||||
|
.ToList();
|
||||||
|
IconsListBox.SelectedItem = IconsListBox.Items
|
||||||
|
.OfType<IconDefinition>()
|
||||||
|
.FirstOrDefault(icon => icon.Key.Equals(currentIconKey, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? SelectedIconKey { get; private set; }
|
||||||
|
|
||||||
|
public static string? SelectIcon(string currentIconKey, IconService iconService, Window? owner, string defaultIconKey = "toolbox")
|
||||||
|
{
|
||||||
|
var window = new IconPickerWindow(currentIconKey, defaultIconKey, iconService)
|
||||||
|
{
|
||||||
|
Owner = owner
|
||||||
|
};
|
||||||
|
|
||||||
|
return window.ShowDialog() == true ? window.SelectedIconKey : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DefaultButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
SelectedIconKey = _defaultIconKey;
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LocalIconButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dialog = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "选择本地图标",
|
||||||
|
Filter = "图片或图标 (*.png;*.jpg;*.jpeg;*.bmp;*.ico)|*.png;*.jpg;*.jpeg;*.bmp;*.ico|所有文件 (*.*)|*.*"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dialog.ShowDialog(this) != true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SelectedIconKey = _iconService.ImportCustomIcon(dialog.FileName);
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Windows.MessageBox.Show(this, $"导入图标失败:{ex.Message}", "选择图标", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OkButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (IconsListBox.SelectedItem is not IconDefinition icon)
|
||||||
|
{
|
||||||
|
System.Windows.MessageBox.Show(this, "请选择一个图标。", "选择图标", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedIconKey = icon.Key;
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DialogResult = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IconsListBox_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
OkButton_OnClick(sender, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,77 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="外观" ToolTip="管理主题、卡片大小、界面缩放和说明显示。">
|
||||||
|
<Grid Margin="18">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="130" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="主题" VerticalAlignment="Center" />
|
||||||
|
<ComboBox x:Name="ThemeComboBox"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="180"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ToolTip="可跟随系统,也可固定为浅色或深色。">
|
||||||
|
<ComboBoxItem Content="跟随系统" Tag="FollowSystem" />
|
||||||
|
<ComboBoxItem Content="浅色" Tag="Light" />
|
||||||
|
<ComboBoxItem Content="深色" Tag="Dark" />
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Text="卡片大小"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,14,0,0" />
|
||||||
|
<ComboBox x:Name="CardSizeComboBox"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="180"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Margin="0,14,0,0"
|
||||||
|
ToolTip="调整主界面卡片宽高。">
|
||||||
|
<ComboBoxItem Content="小" Tag="Small" />
|
||||||
|
<ComboBoxItem Content="中" Tag="Medium" />
|
||||||
|
<ComboBoxItem Content="大" Tag="Large" />
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
Text="界面缩放"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,18,0,0" />
|
||||||
|
<StackPanel Grid.Row="2"
|
||||||
|
Grid.Column="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Margin="0,18,0,0">
|
||||||
|
<Slider x:Name="ScaleSlider"
|
||||||
|
Width="220"
|
||||||
|
Minimum="0.85"
|
||||||
|
Maximum="1.30"
|
||||||
|
TickFrequency="0.05"
|
||||||
|
IsSnapToTickEnabled="True"
|
||||||
|
ToolTip="缩放主界面内容,适合不同显示器密度。" />
|
||||||
|
<TextBlock Text="{Binding ElementName=ScaleSlider, Path=Value, StringFormat={}{0:P0}}"
|
||||||
|
Width="54"
|
||||||
|
Margin="12,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource SecondaryTextBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ShowDescriptionCheckBox"
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="1"
|
||||||
|
Content="在卡片上显示工具说明"
|
||||||
|
Margin="0,18,0,0"
|
||||||
|
ToolTip="关闭后卡片更紧凑,但仍可通过悬浮提示查看详情。" />
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="自动运行" ToolTip="动态汇总所有开启启动时自动运行的工具。">
|
<TabItem Header="自动运行" ToolTip="动态汇总所有开启启动时自动运行的工具。">
|
||||||
<Grid Margin="18">
|
<Grid Margin="18">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -98,10 +169,14 @@
|
|||||||
|
|
||||||
<TabItem Header="快捷键" ToolTip="查看和编辑工具快捷键状态。">
|
<TabItem Header="快捷键" ToolTip="查看和编辑工具快捷键状态。">
|
||||||
<Grid Margin="18">
|
<Grid Margin="18">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
<DataGrid x:Name="HotkeysDataGrid"
|
<DataGrid x:Name="HotkeysDataGrid"
|
||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
CanUserAddRows="False"
|
CanUserAddRows="False"
|
||||||
ToolTip="快捷键状态会在主窗口注册后更新;可直接编辑快捷键文本。">
|
ToolTip="快捷键状态会在主窗口注册后更新;可录入、编辑或清除快捷键。">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="快捷键"
|
<DataGridTextColumn Header="快捷键"
|
||||||
Width="160"
|
Width="160"
|
||||||
@@ -120,6 +195,20 @@
|
|||||||
Binding="{Binding HotkeyStatus}" />
|
Binding="{Binding HotkeyStatus}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
<StackPanel Grid.Row="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,10,0,0">
|
||||||
|
<Button Content="录入快捷键"
|
||||||
|
Width="96"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
ToolTip="为选中的工具捕获一个快捷键。"
|
||||||
|
Click="CaptureHotkeyButton_OnClick" />
|
||||||
|
<Button Content="清除"
|
||||||
|
Width="76"
|
||||||
|
ToolTip="清除选中工具的快捷键。"
|
||||||
|
Click="ClearHotkeyButton_OnClick" />
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
using PersonalToolbox.Models;
|
using PersonalToolbox.Models;
|
||||||
using PersonalToolbox.Services;
|
using PersonalToolbox.Services;
|
||||||
using MessageBox = System.Windows.MessageBox;
|
using MessageBox = System.Windows.MessageBox;
|
||||||
@@ -44,10 +45,14 @@ public partial class SettingsWindow : Window
|
|||||||
LogExpandedCheckBox.IsChecked = _data.Settings.LogPanelExpanded;
|
LogExpandedCheckBox.IsChecked = _data.Settings.LogPanelExpanded;
|
||||||
GlobalHotkeysCheckBox.IsChecked = _data.Settings.GlobalHotkeysEnabled;
|
GlobalHotkeysCheckBox.IsChecked = _data.Settings.GlobalHotkeysEnabled;
|
||||||
StartupCheckBox.IsChecked = _startupService.IsEnabled();
|
StartupCheckBox.IsChecked = _startupService.IsEnabled();
|
||||||
|
SelectComboBoxItemByTag(ThemeComboBox, _data.Settings.Theme);
|
||||||
|
SelectComboBoxItemByTag(CardSizeComboBox, _data.Settings.CardSize);
|
||||||
|
ScaleSlider.Value = AppearanceService.NormalizeScale(_data.Settings.UiScale);
|
||||||
|
ShowDescriptionCheckBox.IsChecked = _data.Settings.ShowToolDescriptions;
|
||||||
|
|
||||||
RebuildAutoRunRows();
|
RebuildAutoRunRows();
|
||||||
AutoRunDataGrid.ItemsSource = _autoRunRows;
|
AutoRunDataGrid.ItemsSource = _autoRunRows;
|
||||||
HotkeysDataGrid.ItemsSource = _data.Tools.Where(tool => !tool.IsDeleted && !string.IsNullOrWhiteSpace(tool.Hotkey)).OrderBy(tool => tool.Name).ToList();
|
HotkeysDataGrid.ItemsSource = _data.Tools.Where(tool => !tool.IsDeleted).OrderBy(tool => tool.Name).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RebuildAutoRunRows()
|
private void RebuildAutoRunRows()
|
||||||
@@ -73,12 +78,21 @@ public partial class SettingsWindow : Window
|
|||||||
_data.Settings.ConfirmExit = ConfirmExitCheckBox.IsChecked == true;
|
_data.Settings.ConfirmExit = ConfirmExitCheckBox.IsChecked == true;
|
||||||
_data.Settings.LogPanelExpanded = LogExpandedCheckBox.IsChecked == true;
|
_data.Settings.LogPanelExpanded = LogExpandedCheckBox.IsChecked == true;
|
||||||
_data.Settings.GlobalHotkeysEnabled = GlobalHotkeysCheckBox.IsChecked == true;
|
_data.Settings.GlobalHotkeysEnabled = GlobalHotkeysCheckBox.IsChecked == true;
|
||||||
|
_data.Settings.Theme = GetSelectedTag(ThemeComboBox, "FollowSystem");
|
||||||
|
_data.Settings.CardSize = GetSelectedTag(CardSizeComboBox, "Medium");
|
||||||
|
_data.Settings.UiScale = AppearanceService.NormalizeScale(ScaleSlider.Value);
|
||||||
|
_data.Settings.ShowToolDescriptions = ShowDescriptionCheckBox.IsChecked == true;
|
||||||
|
|
||||||
foreach (var row in _autoRunRows)
|
foreach (var row in _autoRunRows)
|
||||||
{
|
{
|
||||||
row.Apply();
|
row.Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ValidateAndNormalizeHotkeys())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_data.AutoRunEntries = _autoRunRows.Select(row => row.Entry).OrderBy(row => row.SortOrder).ToList();
|
_data.AutoRunEntries = _autoRunRows.Select(row => row.Entry).OrderBy(row => row.SortOrder).ToList();
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -211,6 +225,30 @@ public partial class SettingsWindow : Window
|
|||||||
RebuildAutoRunRows();
|
RebuildAutoRunRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CaptureHotkeyButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (HotkeysDataGrid.SelectedItem is not ToolItem tool)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hotkey = HotkeyCaptureWindow.Capture(tool.Hotkey, this, out var clearRequested);
|
||||||
|
tool.Hotkey = clearRequested ? null : hotkey;
|
||||||
|
HotkeysDataGrid.Items.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearHotkeyButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (HotkeysDataGrid.SelectedItem is not ToolItem tool)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tool.Hotkey = null;
|
||||||
|
tool.HotkeyStatus = "未设置";
|
||||||
|
HotkeysDataGrid.Items.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
private void MoveAutoRunRow(int offset)
|
private void MoveAutoRunRow(int offset)
|
||||||
{
|
{
|
||||||
if (AutoRunDataGrid.SelectedItem is not AutoRunRow selected)
|
if (AutoRunDataGrid.SelectedItem is not AutoRunRow selected)
|
||||||
@@ -238,6 +276,23 @@ public partial class SettingsWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ValidateAndNormalizeHotkeys()
|
||||||
|
{
|
||||||
|
foreach (var tool in _data.Tools.Where(tool => !tool.IsDeleted && !string.IsNullOrWhiteSpace(tool.Hotkey)))
|
||||||
|
{
|
||||||
|
var hotkey = tool.Hotkey!.Trim();
|
||||||
|
if (!HotkeyParser.TryParse(hotkey, out _, out _))
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"“{tool.Name}”的快捷键格式无效,请重新录入。", "保存设置", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tool.Hotkey = HotkeyParser.Normalize(hotkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static ToolboxData CloneData(ToolboxData source)
|
private static ToolboxData CloneData(ToolboxData source)
|
||||||
{
|
{
|
||||||
return new ToolboxData
|
return new ToolboxData
|
||||||
@@ -252,6 +307,8 @@ public partial class SettingsWindow : Window
|
|||||||
LogPanelExpanded = source.Settings.LogPanelExpanded,
|
LogPanelExpanded = source.Settings.LogPanelExpanded,
|
||||||
Theme = source.Settings.Theme,
|
Theme = source.Settings.Theme,
|
||||||
CardSize = source.Settings.CardSize,
|
CardSize = source.Settings.CardSize,
|
||||||
|
UiScale = source.Settings.UiScale,
|
||||||
|
ShowToolDescriptions = source.Settings.ShowToolDescriptions,
|
||||||
MainWindowWidth = source.Settings.MainWindowWidth,
|
MainWindowWidth = source.Settings.MainWindowWidth,
|
||||||
MainWindowHeight = source.Settings.MainWindowHeight
|
MainWindowHeight = source.Settings.MainWindowHeight
|
||||||
},
|
},
|
||||||
@@ -301,4 +358,19 @@ public partial class SettingsWindow : Window
|
|||||||
Entry.IntervalAfterMs = Math.Max(0, IntervalAfterMs);
|
Entry.IntervalAfterMs = Math.Max(0, IntervalAfterMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SelectComboBoxItemByTag(System.Windows.Controls.ComboBox comboBox, string value)
|
||||||
|
{
|
||||||
|
comboBox.SelectedItem = comboBox.Items
|
||||||
|
.OfType<ComboBoxItem>()
|
||||||
|
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? comboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetSelectedTag(System.Windows.Controls.ComboBox comboBox, string fallback)
|
||||||
|
{
|
||||||
|
return comboBox.SelectedItem is ComboBoxItem item && item.Tag is not null
|
||||||
|
? item.Tag.ToString() ?? fallback
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="编辑工具"
|
Title="编辑工具"
|
||||||
Width="560"
|
Width="560"
|
||||||
Height="560"
|
Height="620"
|
||||||
MinWidth="520"
|
MinWidth="520"
|
||||||
WindowStartupLocation="CenterOwner">
|
WindowStartupLocation="CenterOwner">
|
||||||
<Grid Margin="18">
|
<Grid Margin="18">
|
||||||
@@ -113,12 +113,30 @@
|
|||||||
ToolTip="可执行文件或脚本启动时使用的工作目录,可留空。" />
|
ToolTip="可执行文件或脚本启动时使用的工作目录,可留空。" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="7" Text="快捷键" VerticalAlignment="Center" />
|
<TextBlock Grid.Row="7" Text="快捷键" VerticalAlignment="Center" />
|
||||||
<TextBox x:Name="HotkeyTextBox"
|
<Grid Grid.Row="7"
|
||||||
Grid.Row="7"
|
Grid.Column="1"
|
||||||
Grid.Column="1"
|
Margin="0,8,0,0">
|
||||||
Margin="0,8,0,0"
|
<Grid.ColumnDefinitions>
|
||||||
MinHeight="32"
|
<ColumnDefinition Width="*" />
|
||||||
ToolTip="格式示例:Ctrl + Alt + T。第一版要求至少包含一个修饰键。" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox x:Name="HotkeyTextBox"
|
||||||
|
MinHeight="32"
|
||||||
|
ToolTip="格式示例:Ctrl + Alt + T。可点击录入来捕获快捷键。" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Content="录入"
|
||||||
|
Width="64"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
ToolTip="打开快捷键捕获窗口。"
|
||||||
|
Click="CaptureHotkeyButton_OnClick" />
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Content="清除"
|
||||||
|
Width="64"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
ToolTip="清除当前快捷键。"
|
||||||
|
Click="ClearHotkeyButton_OnClick" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Grid.Row="8"
|
<StackPanel Grid.Row="8"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
@@ -131,6 +149,32 @@
|
|||||||
Margin="0,8,0,0"
|
Margin="0,8,0,0"
|
||||||
ToolTip="仅启动该工具时触发 UAC,工具箱自身不提权。" />
|
ToolTip="仅启动该工具时触发 UAC,工具箱自身不提权。" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="9" Text="图标" VerticalAlignment="Center" />
|
||||||
|
<StackPanel Grid.Row="9"
|
||||||
|
Grid.Column="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Margin="0,12,0,0">
|
||||||
|
<Border Width="42"
|
||||||
|
Height="30"
|
||||||
|
CornerRadius="6"
|
||||||
|
Background="{StaticResource IconBackgroundBrush}">
|
||||||
|
<TextBlock x:Name="IconPreviewTextBlock"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryBrush}" />
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="IconNameTextBlock"
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource SecondaryTextBrush}" />
|
||||||
|
<Button Content="选择图标"
|
||||||
|
Width="88"
|
||||||
|
Margin="12,0,0,0"
|
||||||
|
ToolTip="从内置图标库选择,或导入本地图片/ico。"
|
||||||
|
Click="ChooseIconButton_OnClick" />
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ namespace PersonalToolbox.Views;
|
|||||||
public partial class ToolEditorWindow : Window
|
public partial class ToolEditorWindow : Window
|
||||||
{
|
{
|
||||||
private readonly ToolItem _tool;
|
private readonly ToolItem _tool;
|
||||||
|
private readonly IconService _iconService;
|
||||||
|
|
||||||
private ToolEditorWindow(ToolItem tool, IEnumerable<CategoryItem> categories)
|
private ToolEditorWindow(ToolItem tool, IEnumerable<CategoryItem> categories, IconService iconService)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_tool = tool;
|
_tool = tool;
|
||||||
|
_iconService = iconService;
|
||||||
|
|
||||||
CategoryComboBox.ItemsSource = categories.ToList();
|
CategoryComboBox.ItemsSource = categories.ToList();
|
||||||
CategoryComboBox.SelectedValue = tool.CategoryId;
|
CategoryComboBox.SelectedValue = tool.CategoryId;
|
||||||
@@ -43,6 +45,7 @@ public partial class ToolEditorWindow : Window
|
|||||||
HotkeyTextBox.Text = tool.Hotkey;
|
HotkeyTextBox.Text = tool.Hotkey;
|
||||||
AutoRunCheckBox.IsChecked = tool.AutoRunEnabled;
|
AutoRunCheckBox.IsChecked = tool.AutoRunEnabled;
|
||||||
RunAsAdminCheckBox.IsChecked = tool.RunAsAdmin;
|
RunAsAdminCheckBox.IsChecked = tool.RunAsAdmin;
|
||||||
|
RefreshIconPreview();
|
||||||
|
|
||||||
if (tool.Type == ToolType.Url)
|
if (tool.Type == ToolType.Url)
|
||||||
{
|
{
|
||||||
@@ -60,9 +63,9 @@ public partial class ToolEditorWindow : Window
|
|||||||
|
|
||||||
public ToolItem EditedTool => _tool;
|
public ToolItem EditedTool => _tool;
|
||||||
|
|
||||||
public static ToolItem? Edit(ToolItem tool, IEnumerable<CategoryItem> categories, Window? owner)
|
public static ToolItem? Edit(ToolItem tool, IEnumerable<CategoryItem> categories, IconService iconService, Window? owner)
|
||||||
{
|
{
|
||||||
var window = new ToolEditorWindow(tool, categories)
|
var window = new ToolEditorWindow(tool, categories, iconService)
|
||||||
{
|
{
|
||||||
Owner = owner
|
Owner = owner
|
||||||
};
|
};
|
||||||
@@ -99,6 +102,39 @@ public partial class ToolEditorWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CaptureHotkeyButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var hotkey = HotkeyCaptureWindow.Capture(HotkeyTextBox.Text, this, out var clearRequested);
|
||||||
|
if (clearRequested)
|
||||||
|
{
|
||||||
|
HotkeyTextBox.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HotkeyTextBox.Text = hotkey ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearHotkeyButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
HotkeyTextBox.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChooseIconButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var previewTool = _tool.Clone();
|
||||||
|
previewTool.LaunchTarget = _tool.Type == ToolType.Url ? null : TargetTextBox.Text.Trim();
|
||||||
|
previewTool.Url = _tool.Type == ToolType.Url ? TargetTextBox.Text.Trim() : null;
|
||||||
|
var defaultKey = _iconService.ResolveDefaultIconKey(previewTool);
|
||||||
|
var iconKey = IconPickerWindow.SelectIcon(_tool.IconKey, _iconService, this, defaultKey);
|
||||||
|
if (string.IsNullOrWhiteSpace(iconKey))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tool.IconKey = iconKey;
|
||||||
|
RefreshIconPreview();
|
||||||
|
}
|
||||||
|
|
||||||
private void SaveButton_OnClick(object sender, RoutedEventArgs e)
|
private void SaveButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var name = NameTextBox.Text.Trim();
|
var name = NameTextBox.Text.Trim();
|
||||||
@@ -141,6 +177,7 @@ public partial class ToolEditorWindow : Window
|
|||||||
_tool.Name = name;
|
_tool.Name = name;
|
||||||
_tool.Description = DescriptionTextBox.Text.Trim();
|
_tool.Description = DescriptionTextBox.Text.Trim();
|
||||||
_tool.CategoryId = categoryId;
|
_tool.CategoryId = categoryId;
|
||||||
|
_tool.IconKey = string.IsNullOrWhiteSpace(_tool.IconKey) ? _iconService.ResolveDefaultIconKey(_tool) : _tool.IconKey;
|
||||||
_tool.Arguments = ArgumentsTextBox.Text.Trim();
|
_tool.Arguments = ArgumentsTextBox.Text.Trim();
|
||||||
_tool.WorkingDirectory = WorkingDirectoryTextBox.Text.Trim();
|
_tool.WorkingDirectory = WorkingDirectoryTextBox.Text.Trim();
|
||||||
_tool.Hotkey = string.IsNullOrWhiteSpace(hotkey) ? null : HotkeyParser.Normalize(hotkey);
|
_tool.Hotkey = string.IsNullOrWhiteSpace(hotkey) ? null : HotkeyParser.Normalize(hotkey);
|
||||||
@@ -150,6 +187,12 @@ public partial class ToolEditorWindow : Window
|
|||||||
DialogResult = true;
|
DialogResult = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshIconPreview()
|
||||||
|
{
|
||||||
|
IconPreviewTextBlock.Text = IconService.GetIconText(_tool.IconKey, _tool.Type);
|
||||||
|
IconNameTextBlock.Text = IconService.GetIconName(_tool.IconKey);
|
||||||
|
}
|
||||||
|
|
||||||
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
|
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
DialogResult = false;
|
DialogResult = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user