Initial TaskbarLauncher release baseline
This commit is contained in:
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
*.suo
|
||||
*.pdb
|
||||
|
||||
# Portable runtime data
|
||||
config.json
|
||||
backups/
|
||||
|
||||
# Local/editor noise
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
# Generated release output kept outside the repo when possible
|
||||
*.zip
|
||||
182
App.xaml
Normal file
182
App.xaml
Normal file
@@ -0,0 +1,182 @@
|
||||
<Application x:Class="TaskbarLauncher.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
ShutdownMode="OnExplicitShutdown">
|
||||
<Application.Resources>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Margin" Value="0,2"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="Background" Value="{DynamicResource AppControlBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="ButtonBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="1"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
RecognizesAccessKey="True"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="ButtonBorder" Property="Background" Value="{DynamicResource AppControlBrush}"/>
|
||||
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="ButtonBorder" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.55"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource AppAccentBrush}"/>
|
||||
</Style>
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
|
||||
<Setter Property="Padding" Value="6,4"/>
|
||||
<Setter Property="MinHeight" Value="28"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<ToggleButton x:Name="ToggleButton"
|
||||
Focusable="False"
|
||||
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
ClickMode="Press"
|
||||
Background="{DynamicResource AppSurfaceBrush}"
|
||||
BorderBrush="{DynamicResource AppBorderBrush}">
|
||||
<ToggleButton.Template>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border x:Name="ComboToggleBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="1">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="ComboToggleBorder" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter TargetName="ComboToggleBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="ComboToggleBorder" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter TargetName="ComboToggleBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="ComboToggleBorder" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter TargetName="ComboToggleBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</ToggleButton.Template>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="28"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter Margin="8,3,4,3"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding SelectionBoxItem}"
|
||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||
ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}"/>
|
||||
<Path Grid.Column="1"
|
||||
Width="9"
|
||||
Height="5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{DynamicResource AppTextBrush}"
|
||||
Data="M 0 0 L 4.5 5 L 9 0 Z"/>
|
||||
</Grid>
|
||||
</ToggleButton>
|
||||
<Popup x:Name="Popup"
|
||||
Placement="Bottom"
|
||||
AllowsTransparency="True"
|
||||
Focusable="False"
|
||||
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||
PopupAnimation="Slide">
|
||||
<Border Background="{DynamicResource AppSurfaceBrush}"
|
||||
BorderBrush="{DynamicResource AppBorderBrush}"
|
||||
BorderThickness="1"
|
||||
MinWidth="{TemplateBinding ActualWidth}"
|
||||
MaxHeight="{TemplateBinding MaxDropDownHeight}">
|
||||
<ScrollViewer>
|
||||
<ItemsPresenter/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.55"/>
|
||||
</Trigger>
|
||||
<Trigger SourceName="ToggleButton" Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="ToggleButton" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter TargetName="ToggleButton" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger SourceName="ToggleButton" Property="IsPressed" Value="True">
|
||||
<Setter TargetName="ToggleButton" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter TargetName="ToggleButton" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
|
||||
<Setter Property="Padding" Value="6,4"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource AppAccentBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppSelectedTextBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource AppAccentBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppSelectedTextBrush}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="TreeView">
|
||||
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
|
||||
</Style>
|
||||
<Style TargetType="TreeViewItem">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource AppAccentBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppSelectedTextBrush}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="GroupBox">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
|
||||
</Style>
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
|
||||
</Style>
|
||||
<Style TargetType="Expander">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
|
||||
</Style>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
23
App.xaml.cs
Normal file
23
App.xaml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using TaskbarLauncher.Services;
|
||||
|
||||
namespace TaskbarLauncher;
|
||||
|
||||
public partial class App : System.Windows.Application
|
||||
{
|
||||
private LauncherController? controller;
|
||||
|
||||
protected override void OnStartup(System.Windows.StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
ThemeService.Apply();
|
||||
controller = new LauncherController();
|
||||
controller.Start();
|
||||
}
|
||||
|
||||
protected override void OnExit(System.Windows.ExitEventArgs e)
|
||||
{
|
||||
controller?.Dispose();
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
10
AssemblyInfo.cs
Normal file
10
AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly:ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
166
MainWindow.xaml
Normal file
166
MainWindow.xaml
Normal file
@@ -0,0 +1,166 @@
|
||||
<Window x:Class="TaskbarLauncher.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="Taskbar Launcher Settings" Height="700" Width="940"
|
||||
MinHeight="620" MinWidth="820"
|
||||
Background="{DynamicResource AppWindowBrush}"
|
||||
Foreground="{DynamicResource AppTextBrush}">
|
||||
<DockPanel Margin="18">
|
||||
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,14">
|
||||
<TextBlock Text="Taskbar Launcher" FontSize="22" FontWeight="SemiBold"/>
|
||||
<TextBlock x:Name="VersionText"
|
||||
Foreground="{DynamicResource AppMutedTextBrush}"/>
|
||||
<TextBlock Text="Portable launcher settings are stored beside the app in config.json."
|
||||
Foreground="{DynamicResource AppMutedTextBrush}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border DockPanel.Dock="Bottom" BorderThickness="1,0,0,0" Padding="0,12,0,0"
|
||||
BorderBrush="{DynamicResource AppBorderBrush}">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="Save" Click="Save_Click" Margin="0,0,8,0"/>
|
||||
<Button Content="Backup Config" Click="BackupConfig_Click" Margin="0,0,8,0"/>
|
||||
<Button Content="Restore Config" Click="RestoreConfig_Click" Margin="0,0,8,0"/>
|
||||
<Button Content="Reload Config" Click="ReloadConfig_Click" Margin="0,0,8,0"/>
|
||||
<Button Content="Open Config Folder" Click="OpenConfigFolder_Click" Margin="0,0,8,0"/>
|
||||
<Button Content="Close" Click="Close_Click"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<GroupBox Header="Launcher Items" Margin="0,0,12,0">
|
||||
<DockPanel Margin="8">
|
||||
<UniformGrid DockPanel.Dock="Bottom" Columns="2" Margin="0,8,0,0">
|
||||
<Button Content="Add Root" Click="AddRoot_Click"/>
|
||||
<Button Content="Add Top Menu" Click="AddTopMenu_Click"/>
|
||||
<Button Content="Add Child" Click="AddChild_Click"/>
|
||||
<Button Content="Add Submenu" Click="AddSubmenu_Click"/>
|
||||
<Button Content="Move Up" Click="MoveUp_Click"/>
|
||||
<Button Content="Move Down" Click="MoveDown_Click"/>
|
||||
<Button Content="Promote To Top" Click="PromoteToTop_Click"/>
|
||||
<Button Content="Delete" Click="Delete_Click"/>
|
||||
<Button Content="Apply Item" Click="ApplyItem_Click"/>
|
||||
</UniformGrid>
|
||||
<TreeView x:Name="ItemsTree"
|
||||
AllowDrop="True"
|
||||
SelectedItemChanged="ItemsTree_SelectedItemChanged"
|
||||
PreviewMouseMove="ItemsTree_PreviewMouseMove"
|
||||
DragOver="ItemsTree_DragOver"
|
||||
Drop="ItemsTree_Drop"/>
|
||||
</DockPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="Settings" Grid.Column="1">
|
||||
<Grid Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="16"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="110"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="Hotkey" VerticalAlignment="Center" Margin="0,0,8,8"/>
|
||||
<TextBox x:Name="HotkeyBox" Grid.Column="1" Margin="0,0,0,8"/>
|
||||
|
||||
<TextBlock Text="Default for New Items" Grid.Row="1" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<ComboBox x:Name="DisplayModeBox" Grid.Row="1" Grid.Column="1"/>
|
||||
|
||||
<TextBlock Text="Startup" Grid.Row="2" VerticalAlignment="Center" Margin="0,8,8,0"/>
|
||||
<CheckBox x:Name="StartWithWindowsBox" Grid.Row="2" Grid.Column="1"
|
||||
Content="Start with Windows"
|
||||
Margin="0,8,0,0"
|
||||
Checked="StartWithWindowsBox_Changed"
|
||||
Unchecked="StartWithWindowsBox_Changed"/>
|
||||
</Grid>
|
||||
|
||||
<Separator Grid.Row="1" VerticalAlignment="Center"/>
|
||||
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="110"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="Title" VerticalAlignment="Center" Margin="0,0,8,8"/>
|
||||
<TextBox x:Name="TitleBox" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,0,0,8"
|
||||
TextChanged="PreviewField_Changed"/>
|
||||
|
||||
<TextBlock Text="Type" Grid.Row="1" VerticalAlignment="Center" Margin="0,0,8,8"/>
|
||||
<ComboBox x:Name="TypeBox" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,0,0,8"
|
||||
SelectionChanged="TypeBox_SelectionChanged"/>
|
||||
|
||||
<TextBlock Text="Target" Grid.Row="2" VerticalAlignment="Center" Margin="0,0,8,8"/>
|
||||
<TextBox x:Name="TargetBox" Grid.Row="2" Grid.Column="1" Margin="0,0,8,8"
|
||||
TextChanged="PreviewField_Changed"/>
|
||||
<StackPanel Grid.Row="2" Grid.Column="2" Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<Button Content="File" Click="BrowseFile_Click" Margin="0,0,6,0" Padding="8,4"/>
|
||||
<Button Content="Folder" Click="BrowseFolder_Click" Padding="8,4"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Arguments" Grid.Row="3" VerticalAlignment="Center" Margin="0,0,8,8"/>
|
||||
<TextBox x:Name="ArgumentsBox" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,0,0,8"/>
|
||||
|
||||
<TextBlock Text="Icon Path" Grid.Row="4" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBox x:Name="IconPathBox" Grid.Row="4" Grid.Column="1" Margin="0,0,8,8"
|
||||
TextChanged="PreviewField_Changed"/>
|
||||
<StackPanel Grid.Row="4" Grid.Column="2" Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<Button Content="Icon" Click="BrowseIcon_Click" Padding="8,4" Margin="0,0,6,0"/>
|
||||
<Button Content="Reset" Click="ResetIcon_Click" Padding="8,4"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Selected Item Display" Grid.Row="5" VerticalAlignment="Center" Margin="0,0,8,8"/>
|
||||
<ComboBox x:Name="ItemDisplayModeBox" Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,0,0,8" SelectionChanged="PreviewField_Changed"/>
|
||||
|
||||
<TextBlock Text="Preview" Grid.Row="6" VerticalAlignment="Top" Margin="0,4,8,0"/>
|
||||
<Border Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource AppBorderBrush}"
|
||||
Background="{DynamicResource AppControlBrush}"
|
||||
Padding="8" MinHeight="72">
|
||||
<StackPanel>
|
||||
<ContentControl x:Name="PreviewHost"/>
|
||||
<TextBlock x:Name="PreviewNote" Margin="0,8,0,0"
|
||||
Foreground="{DynamicResource AppMutedTextBrush}"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="StatusText" Grid.Row="3" Margin="0,14,0,0"
|
||||
Foreground="{DynamicResource AppMutedTextBrush}"
|
||||
TextWrapping="Wrap"/>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
951
MainWindow.xaml.cs
Normal file
951
MainWindow.xaml.cs
Normal file
@@ -0,0 +1,951 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Media;
|
||||
using System.IO;
|
||||
using System.Windows.Input;
|
||||
using System.Reflection;
|
||||
using TaskbarLauncher.Models;
|
||||
using TaskbarLauncher.Services;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
using TreeView = System.Windows.Controls.TreeView;
|
||||
using TreeViewItem = System.Windows.Controls.TreeViewItem;
|
||||
|
||||
namespace TaskbarLauncher;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly LauncherController? controller;
|
||||
private LauncherItem? selectedItem;
|
||||
private bool isLoadingSelection;
|
||||
private bool isLoadingStartupState;
|
||||
private List<int>? selectedPath;
|
||||
private string? loadedAutoIconPath;
|
||||
private System.Windows.Point dragStartPoint;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public MainWindow(LauncherController controller)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.controller = controller;
|
||||
DisplayModeBox.ItemsSource = Enum.GetValues<LauncherDisplayMode>();
|
||||
ItemDisplayModeBox.ItemsSource = Enum.GetValues<LauncherDisplayMode>();
|
||||
TypeBox.ItemsSource = Enum.GetValues<LauncherItemType>();
|
||||
VersionText.Text = $"Version {GetAppVersion()}";
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
private void RefreshView(LauncherItem? itemToSelect = null)
|
||||
{
|
||||
if (controller is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ItemsTree.Items.Clear();
|
||||
foreach (LauncherItem item in controller.Config.Items)
|
||||
{
|
||||
ItemsTree.Items.Add(CreateTreeItem(item));
|
||||
}
|
||||
|
||||
HotkeyBox.Text = controller.Config.Hotkey;
|
||||
DisplayModeBox.SelectedItem = controller.Config.DisplayMode;
|
||||
LoadStartupState();
|
||||
StatusText.Text = $"Config file: {ConfigService.ConfigPath}";
|
||||
if (itemToSelect is not null && SelectTreeItem(itemToSelect))
|
||||
{
|
||||
selectedItem = itemToSelect;
|
||||
selectedPath = FindItemPath(itemToSelect);
|
||||
LoadItemFields(itemToSelect);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearItemFields();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAppVersion()
|
||||
{
|
||||
return Assembly.GetExecutingAssembly()
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
|
||||
.InformationalVersion ?? "development";
|
||||
}
|
||||
|
||||
private static TreeViewItem CreateTreeItem(LauncherItem item)
|
||||
{
|
||||
var treeItem = new TreeViewItem { Header = $"{item.Title} ({item.Type})", Tag = item };
|
||||
foreach (LauncherItem child in item.Children)
|
||||
{
|
||||
treeItem.Items.Add(CreateTreeItem(child));
|
||||
}
|
||||
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
private bool SelectTreeItem(LauncherItem item)
|
||||
{
|
||||
foreach (object root in ItemsTree.Items)
|
||||
{
|
||||
if (root is TreeViewItem treeItem && SelectTreeItem(treeItem, item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SelectTreeItem(TreeViewItem treeItem, LauncherItem item)
|
||||
{
|
||||
if (ReferenceEquals(treeItem.Tag, item))
|
||||
{
|
||||
treeItem.IsSelected = true;
|
||||
treeItem.BringIntoView();
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (object child in treeItem.Items)
|
||||
{
|
||||
if (child is TreeViewItem childTreeItem && SelectTreeItem(childTreeItem, item))
|
||||
{
|
||||
treeItem.IsExpanded = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ItemsTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
||||
{
|
||||
if (e.NewValue is not TreeViewItem treeItem || treeItem.Tag is not LauncherItem item)
|
||||
{
|
||||
selectedItem = null;
|
||||
ClearItemFields();
|
||||
return;
|
||||
}
|
||||
|
||||
selectedItem = item;
|
||||
selectedPath = FindItemPath(item);
|
||||
LoadItemFields(item);
|
||||
}
|
||||
|
||||
private void ItemsTree_PreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton != MouseButtonState.Pressed)
|
||||
{
|
||||
dragStartPoint = e.GetPosition(ItemsTree);
|
||||
return;
|
||||
}
|
||||
|
||||
System.Windows.Point currentPosition = e.GetPosition(ItemsTree);
|
||||
if (Math.Abs(currentPosition.X - dragStartPoint.X) < SystemParameters.MinimumHorizontalDragDistance &&
|
||||
Math.Abs(currentPosition.Y - dragStartPoint.Y) < SystemParameters.MinimumVerticalDragDistance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ItemsTree.SelectedItem is TreeViewItem { Tag: LauncherItem item })
|
||||
{
|
||||
DragDrop.DoDragDrop(ItemsTree, item, System.Windows.DragDropEffects.Move);
|
||||
}
|
||||
}
|
||||
|
||||
private void ItemsTree_DragOver(object sender, System.Windows.DragEventArgs e)
|
||||
{
|
||||
e.Effects = CanDrop(e) ? System.Windows.DragDropEffects.Move : System.Windows.DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void ItemsTree_Drop(object sender, System.Windows.DragEventArgs e)
|
||||
{
|
||||
if (controller is null ||
|
||||
!e.Data.GetDataPresent(typeof(LauncherItem)) ||
|
||||
e.Data.GetData(typeof(LauncherItem)) is not LauncherItem draggedItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TreeViewItem? targetTreeItem = FindAncestor<TreeViewItem>(e.OriginalSource as DependencyObject);
|
||||
LauncherItem? targetItem = targetTreeItem?.Tag as LauncherItem;
|
||||
if (targetItem is not null && (ReferenceEquals(draggedItem, targetItem) || ContainsItem(draggedItem, targetItem)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyCurrentItem();
|
||||
List<LauncherItem>? sourceSiblings = FindSiblings(controller.Config.Items, draggedItem);
|
||||
if (sourceSiblings is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sourceSiblings.Remove(draggedItem);
|
||||
|
||||
if (targetItem is null)
|
||||
{
|
||||
controller.Config.Items.Add(draggedItem);
|
||||
}
|
||||
else if (targetItem.Type == LauncherItemType.Menu)
|
||||
{
|
||||
targetItem.Children.Add(draggedItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
List<LauncherItem>? targetSiblings = FindSiblings(controller.Config.Items, targetItem);
|
||||
if (targetSiblings is null)
|
||||
{
|
||||
controller.Config.Items.Add(draggedItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
int targetIndex = targetSiblings.IndexOf(targetItem);
|
||||
targetSiblings.Insert(targetIndex + 1, draggedItem);
|
||||
}
|
||||
}
|
||||
|
||||
selectedPath = FindItemPath(draggedItem);
|
||||
RefreshView(draggedItem);
|
||||
StatusText.Text = $"Moved {draggedItem.Title}. Click Save to keep the new order.";
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private bool CanDrop(System.Windows.DragEventArgs e)
|
||||
{
|
||||
if (!e.Data.GetDataPresent(typeof(LauncherItem)) ||
|
||||
e.Data.GetData(typeof(LauncherItem)) is not LauncherItem draggedItem)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TreeViewItem? targetTreeItem = FindAncestor<TreeViewItem>(e.OriginalSource as DependencyObject);
|
||||
LauncherItem? targetItem = targetTreeItem?.Tag as LauncherItem;
|
||||
return targetItem is null || (!ReferenceEquals(draggedItem, targetItem) && !ContainsItem(draggedItem, targetItem));
|
||||
}
|
||||
|
||||
private static bool ContainsItem(LauncherItem parent, LauncherItem possibleChild)
|
||||
{
|
||||
foreach (LauncherItem child in parent.Children)
|
||||
{
|
||||
if (ReferenceEquals(child, possibleChild) || ContainsItem(child, possibleChild))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static T? FindAncestor<T>(DependencyObject? current)
|
||||
where T : DependencyObject
|
||||
{
|
||||
while (current is not null)
|
||||
{
|
||||
if (current is T match)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void LoadItemFields(LauncherItem item)
|
||||
{
|
||||
isLoadingSelection = true;
|
||||
TitleBox.Text = item.Title;
|
||||
TypeBox.SelectedItem = item.Type;
|
||||
TargetBox.Text = item.Target;
|
||||
ArgumentsBox.Text = item.Arguments ?? "";
|
||||
IconPathBox.Text = item.IconPath ?? "";
|
||||
loadedAutoIconPath = GetAutomaticIconPath(item);
|
||||
ItemDisplayModeBox.SelectedItem = item.DisplayMode;
|
||||
UpdateTargetAvailability();
|
||||
isLoadingSelection = false;
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
private void ClearItemFields()
|
||||
{
|
||||
selectedItem = null;
|
||||
selectedPath = null;
|
||||
isLoadingSelection = true;
|
||||
TitleBox.Text = "";
|
||||
TypeBox.SelectedIndex = -1;
|
||||
TargetBox.Text = "";
|
||||
ArgumentsBox.Text = "";
|
||||
IconPathBox.Text = "";
|
||||
loadedAutoIconPath = null;
|
||||
ItemDisplayModeBox.SelectedItem = controller?.Config.DisplayMode;
|
||||
UpdateTargetAvailability();
|
||||
isLoadingSelection = false;
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
private void ApplyCurrentItem()
|
||||
{
|
||||
if (controller is null || selectedItem is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
selectedItem.Title = string.IsNullOrWhiteSpace(TitleBox.Text) ? "Untitled" : TitleBox.Text.Trim();
|
||||
selectedItem.Type = TypeBox.SelectedItem is LauncherItemType type ? type : LauncherItemType.App;
|
||||
selectedItem.Target = selectedItem.Type == LauncherItemType.Menu ? "" : TargetBox.Text.Trim();
|
||||
selectedItem.Arguments = selectedItem.Type == LauncherItemType.Menu || string.IsNullOrWhiteSpace(ArgumentsBox.Text)
|
||||
? null
|
||||
: ArgumentsBox.Text.Trim();
|
||||
EnsureAutomaticIconPath();
|
||||
selectedItem.IconPath = string.IsNullOrWhiteSpace(IconPathBox.Text) ? null : IconPathBox.Text.Trim();
|
||||
selectedItem.DisplayMode = ItemDisplayModeBox.SelectedItem is LauncherDisplayMode itemMode
|
||||
? itemMode
|
||||
: controller?.Config.DisplayMode ?? LauncherDisplayMode.LargeIconWithText;
|
||||
|
||||
if (selectedItem.Type != LauncherItemType.Menu)
|
||||
{
|
||||
selectedItem.Children.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyConfigFields()
|
||||
{
|
||||
if (controller is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
controller.Config.Hotkey = string.IsNullOrWhiteSpace(HotkeyBox.Text) ? "Ctrl+Alt+Space" : HotkeyBox.Text.Trim();
|
||||
controller.Config.DisplayMode = DisplayModeBox.SelectedItem is LauncherDisplayMode mode
|
||||
? mode
|
||||
: LauncherDisplayMode.LargeIconWithText;
|
||||
ApplyCurrentItem();
|
||||
}
|
||||
|
||||
private void LoadStartupState()
|
||||
{
|
||||
isLoadingStartupState = true;
|
||||
StartWithWindowsBox.IsChecked = StartupService.IsEnabled();
|
||||
isLoadingStartupState = false;
|
||||
}
|
||||
|
||||
private void StartWithWindowsBox_Changed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (isLoadingStartupState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bool enabled = StartWithWindowsBox.IsChecked == true;
|
||||
StartupService.SetEnabled(enabled);
|
||||
StatusText.Text = enabled
|
||||
? $"Startup enabled: {StartupService.ShortcutPath}"
|
||||
: "Startup disabled.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Could not update startup setting.\n\n{ex.Message}", "Taskbar Launcher");
|
||||
LoadStartupState();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRoot_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (controller is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyCurrentItem();
|
||||
LauncherItem item = CreateNewItem(controller.Config.DisplayMode);
|
||||
controller.Config.Items.Add(item);
|
||||
selectedPath = [controller.Config.Items.Count - 1];
|
||||
RefreshView(item);
|
||||
}
|
||||
|
||||
private void AddTopMenu_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (controller is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyCurrentItem();
|
||||
LauncherItem menu = CreateNewMenu(controller.Config.DisplayMode);
|
||||
controller.Config.Items.Add(menu);
|
||||
selectedPath = [controller.Config.Items.Count - 1];
|
||||
RefreshView(menu);
|
||||
StatusText.Text = "Added top-level menu. Rename it, then Save.";
|
||||
}
|
||||
|
||||
private void AddChild_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (controller is null || selectedItem is null)
|
||||
{
|
||||
MessageBox.Show("Select a menu item first, then add a child item.", "Taskbar Launcher");
|
||||
return;
|
||||
}
|
||||
|
||||
LauncherItem parent = selectedItem;
|
||||
ApplyCurrentItem();
|
||||
if (parent.Type != LauncherItemType.Menu)
|
||||
{
|
||||
MessageBox.Show("Child items can only be added under a Menu item.", "Taskbar Launcher");
|
||||
return;
|
||||
}
|
||||
|
||||
LauncherItem child = CreateNewItem(controller.Config.DisplayMode);
|
||||
parent.Children.Add(child);
|
||||
selectedPath = FindItemPath(child);
|
||||
RefreshView(child);
|
||||
}
|
||||
|
||||
private void AddSubmenu_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (controller is null || selectedItem is null)
|
||||
{
|
||||
MessageBox.Show("Select a menu first, then add a submenu.", "Taskbar Launcher");
|
||||
return;
|
||||
}
|
||||
|
||||
LauncherItem parent = selectedItem;
|
||||
ApplyCurrentItem();
|
||||
if (parent.Type != LauncherItemType.Menu)
|
||||
{
|
||||
MessageBox.Show("Submenus can only be added under a Menu item.", "Taskbar Launcher");
|
||||
return;
|
||||
}
|
||||
|
||||
LauncherItem submenu = CreateNewMenu(controller.Config.DisplayMode);
|
||||
parent.Children.Add(submenu);
|
||||
selectedPath = FindItemPath(submenu);
|
||||
RefreshView(submenu);
|
||||
StatusText.Text = "Added submenu. Rename it, then Save.";
|
||||
}
|
||||
|
||||
private static LauncherItem CreateNewItem(LauncherDisplayMode displayMode)
|
||||
{
|
||||
return new LauncherItem
|
||||
{
|
||||
Title = "New Item",
|
||||
Type = LauncherItemType.App,
|
||||
Target = "",
|
||||
DisplayMode = displayMode
|
||||
};
|
||||
}
|
||||
|
||||
private static LauncherItem CreateNewMenu(LauncherDisplayMode displayMode)
|
||||
{
|
||||
return new LauncherItem
|
||||
{
|
||||
Title = "New Menu",
|
||||
Type = LauncherItemType.Menu,
|
||||
Target = "",
|
||||
DisplayMode = displayMode
|
||||
};
|
||||
}
|
||||
|
||||
private void Delete_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (controller is null || selectedItem is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LauncherItem deletedItem = selectedItem;
|
||||
RemoveItem(controller.Config.Items, selectedItem);
|
||||
RefreshView();
|
||||
StatusText.Text = $"Deleted: {deletedItem.Title}";
|
||||
}
|
||||
|
||||
private void MoveUp_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MoveSelected(-1);
|
||||
}
|
||||
|
||||
private void MoveDown_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MoveSelected(1);
|
||||
}
|
||||
|
||||
private void PromoteToTop_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (controller is null || selectedItem is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyCurrentItem();
|
||||
if (controller.Config.Items.Contains(selectedItem))
|
||||
{
|
||||
StatusText.Text = $"{selectedItem.Title} is already a top-level item.";
|
||||
return;
|
||||
}
|
||||
|
||||
LauncherItem itemToPromote = selectedItem;
|
||||
if (!RemoveItem(controller.Config.Items, itemToPromote))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
controller.Config.Items.Add(itemToPromote);
|
||||
selectedPath = [controller.Config.Items.Count - 1];
|
||||
RefreshView(itemToPromote);
|
||||
StatusText.Text = $"Promoted {itemToPromote.Title} to top level. Click Save to keep it.";
|
||||
}
|
||||
|
||||
private void MoveSelected(int direction)
|
||||
{
|
||||
if (controller is null || selectedItem is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyCurrentItem();
|
||||
List<LauncherItem>? siblings = FindSiblings(controller.Config.Items, selectedItem);
|
||||
if (siblings is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int index = siblings.IndexOf(selectedItem);
|
||||
int newIndex = index + direction;
|
||||
if (index < 0 || newIndex < 0 || newIndex >= siblings.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
siblings.RemoveAt(index);
|
||||
siblings.Insert(newIndex, selectedItem);
|
||||
RefreshView(selectedItem);
|
||||
}
|
||||
|
||||
private static bool RemoveItem(List<LauncherItem> items, LauncherItem item)
|
||||
{
|
||||
if (items.Remove(item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (LauncherItem child in items)
|
||||
{
|
||||
if (RemoveItem(child.Children, item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<LauncherItem>? FindSiblings(List<LauncherItem> items, LauncherItem item)
|
||||
{
|
||||
if (items.Contains(item))
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
foreach (LauncherItem child in items)
|
||||
{
|
||||
List<LauncherItem>? found = FindSiblings(child.Children, item);
|
||||
if (found is not null)
|
||||
{
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ApplyItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ApplyCurrentItem();
|
||||
RefreshView(selectedItem);
|
||||
}
|
||||
|
||||
private void Save_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (controller is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyConfigFields();
|
||||
ConfigService.Save(controller.Config);
|
||||
controller.ReloadConfig();
|
||||
LauncherItem? reloadedSelection = selectedPath is null ? null : FindItemByPath(controller.Config.Items, selectedPath);
|
||||
RefreshView(reloadedSelection);
|
||||
StatusText.Text = reloadedSelection is null
|
||||
? $"Saved: {ConfigService.ConfigPath}"
|
||||
: $"Saved {reloadedSelection.Title} ({reloadedSelection.Type}) to {ConfigService.ConfigPath}";
|
||||
}
|
||||
|
||||
private void BackupConfig_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (controller is not null)
|
||||
{
|
||||
ApplyConfigFields();
|
||||
ConfigService.Save(controller.Config);
|
||||
}
|
||||
|
||||
string backupPath = ConfigService.Backup();
|
||||
StatusText.Text = $"Backed up config to {backupPath}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Could not back up config.\n\n{ex.Message}", "Taskbar Launcher");
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreConfig_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using OpenFileDialog dialog = new()
|
||||
{
|
||||
Title = "Choose a config backup to restore",
|
||||
Filter = "JSON config files|*.json|All files|*.*",
|
||||
InitialDirectory = Directory.Exists(ConfigService.BackupFolder) ? ConfigService.BackupFolder : ConfigService.AppFolder
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MessageBoxResult result = MessageBox.Show(
|
||||
"Restore this config file and replace the current launcher config?",
|
||||
"Taskbar Launcher",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result != MessageBoxResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ConfigService.Restore(dialog.FileName);
|
||||
controller?.ReloadConfig();
|
||||
RefreshView();
|
||||
StatusText.Text = $"Restored config from {dialog.FileName}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Could not restore config.\n\n{ex.Message}", "Taskbar Launcher");
|
||||
}
|
||||
}
|
||||
|
||||
private List<int>? FindItemPath(LauncherItem item)
|
||||
{
|
||||
if (controller is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<int> path = [];
|
||||
return FindItemPath(controller.Config.Items, item, path) ? path : null;
|
||||
}
|
||||
|
||||
private static bool FindItemPath(List<LauncherItem> items, LauncherItem item, List<int> path)
|
||||
{
|
||||
for (int index = 0; index < items.Count; index++)
|
||||
{
|
||||
path.Add(index);
|
||||
if (ReferenceEquals(items[index], item) || FindItemPath(items[index].Children, item, path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
path.RemoveAt(path.Count - 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static LauncherItem? FindItemByPath(List<LauncherItem> items, List<int> path)
|
||||
{
|
||||
List<LauncherItem> currentItems = items;
|
||||
LauncherItem? currentItem = null;
|
||||
|
||||
foreach (int index in path)
|
||||
{
|
||||
if (index < 0 || index >= currentItems.Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
currentItem = currentItems[index];
|
||||
currentItems = currentItem.Children;
|
||||
}
|
||||
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
private void ReloadConfig_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
controller?.ReloadConfig();
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
private void OpenConfigFolder_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ConfigService.OpenConfigFolder();
|
||||
}
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void BrowseFile_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using OpenFileDialog dialog = new()
|
||||
{
|
||||
Title = "Choose an application, document, or shortcut",
|
||||
Filter = "Programs and shortcuts|*.exe;*.lnk;*.bat;*.cmd;*.ps1|All files|*.*"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
TargetBox.Text = dialog.FileName;
|
||||
SetIconPathFromTargetIfAutomatic(dialog.FileName);
|
||||
}
|
||||
}
|
||||
|
||||
private void BrowseFolder_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using FolderBrowserDialog dialog = new()
|
||||
{
|
||||
Description = "Choose a folder to launch"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
TargetBox.Text = dialog.SelectedPath;
|
||||
TypeBox.SelectedItem = LauncherItemType.Folder;
|
||||
SetIconPathFromTargetIfAutomatic(dialog.SelectedPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void BrowseIcon_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using OpenFileDialog dialog = new()
|
||||
{
|
||||
Title = "Choose an icon file",
|
||||
Filter = "Icon files|*.ico;*.exe;*.dll|All files|*.*"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
IconPathBox.Text = dialog.FileName;
|
||||
loadedAutoIconPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetIcon_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (TypeBox.SelectedItem is LauncherItemType.Menu)
|
||||
{
|
||||
IconPathBox.Text = "";
|
||||
loadedAutoIconPath = null;
|
||||
UpdatePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
string target = TargetBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
IconPathBox.Text = "";
|
||||
loadedAutoIconPath = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
IconPathBox.Text = target;
|
||||
loadedAutoIconPath = target;
|
||||
}
|
||||
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
private void TypeBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (!isLoadingSelection)
|
||||
{
|
||||
UpdateTargetAvailability();
|
||||
UpdatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewField_Changed(object sender, EventArgs e)
|
||||
{
|
||||
if (!isLoadingSelection)
|
||||
{
|
||||
UpdatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAutomaticIconPath()
|
||||
{
|
||||
if (TypeBox.SelectedItem is LauncherItemType.Menu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string target = TargetBox.Text.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
SetIconPathFromTargetIfAutomatic(target);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetIconPathFromTargetIfAutomatic(string target)
|
||||
{
|
||||
string currentIconPath = IconPathBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(currentIconPath) ||
|
||||
string.Equals(currentIconPath, loadedAutoIconPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
IconPathBox.Text = target;
|
||||
loadedAutoIconPath = target;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetAutomaticIconPath(LauncherItem item)
|
||||
{
|
||||
if (item.Type == LauncherItemType.Menu || string.IsNullOrWhiteSpace(item.Target))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return string.Equals(item.IconPath, item.Target, StringComparison.OrdinalIgnoreCase)
|
||||
? item.Target
|
||||
: null;
|
||||
}
|
||||
|
||||
private void UpdatePreview()
|
||||
{
|
||||
if (PreviewHost is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LauncherItem? previewItem = CreatePreviewItem();
|
||||
if (previewItem is null)
|
||||
{
|
||||
PreviewHost.Content = new TextBlock
|
||||
{
|
||||
Text = "Select an item to preview it.",
|
||||
Foreground = (System.Windows.Media.Brush)System.Windows.Application.Current.Resources["AppMutedTextBrush"]
|
||||
};
|
||||
PreviewNote.Text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
LauncherDisplayMode displayMode = previewItem.DisplayMode;
|
||||
PreviewHost.Content = CreatePreviewControl(previewItem, displayMode);
|
||||
PreviewNote.Text = previewItem.Type == LauncherItemType.Menu
|
||||
? "Menu headers are shown as expandable section headers in the launcher. Display size applies to app, folder, file, and website items."
|
||||
: $"Icon {DisplayModeMetrics.GetIconSize(displayMode):0}px, row {DisplayModeMetrics.GetRowHeight(displayMode):0}px.";
|
||||
}
|
||||
|
||||
private LauncherItem? CreatePreviewItem()
|
||||
{
|
||||
if (selectedItem is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new LauncherItem
|
||||
{
|
||||
Title = string.IsNullOrWhiteSpace(TitleBox.Text) ? "Untitled" : TitleBox.Text.Trim(),
|
||||
Type = TypeBox.SelectedItem is LauncherItemType type ? type : LauncherItemType.App,
|
||||
Target = TargetBox.Text.Trim(),
|
||||
Arguments = string.IsNullOrWhiteSpace(ArgumentsBox.Text) ? null : ArgumentsBox.Text.Trim(),
|
||||
IconPath = string.IsNullOrWhiteSpace(IconPathBox.Text) ? null : IconPathBox.Text.Trim(),
|
||||
DisplayMode = ItemDisplayModeBox.SelectedItem is LauncherDisplayMode mode ? mode : LauncherDisplayMode.CompactList
|
||||
};
|
||||
}
|
||||
|
||||
private static FrameworkElement CreatePreviewControl(LauncherItem item, LauncherDisplayMode displayMode)
|
||||
{
|
||||
if (item.Type == LauncherItemType.Menu)
|
||||
{
|
||||
return new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
}
|
||||
|
||||
if (displayMode == LauncherDisplayMode.LargeIconOnly)
|
||||
{
|
||||
ImageSource? iconOnly = IconService.GetIcon(item, large: true);
|
||||
return iconOnly is null
|
||||
? new TextBlock { Text = item.Title, HorizontalAlignment = System.Windows.HorizontalAlignment.Center }
|
||||
: new System.Windows.Controls.Image
|
||||
{
|
||||
Source = iconOnly,
|
||||
Width = DisplayModeMetrics.GetIconSize(displayMode),
|
||||
Height = DisplayModeMetrics.GetIconSize(displayMode),
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Center
|
||||
};
|
||||
}
|
||||
|
||||
StackPanel panel = new() { Orientation = System.Windows.Controls.Orientation.Horizontal };
|
||||
bool large = displayMode == LauncherDisplayMode.LargeIconWithText;
|
||||
ImageSource? icon = IconService.GetIcon(item, large);
|
||||
if (icon is not null)
|
||||
{
|
||||
double size = DisplayModeMetrics.GetIconSize(displayMode);
|
||||
panel.Children.Add(new System.Windows.Controls.Image
|
||||
{
|
||||
Source = icon,
|
||||
Width = size,
|
||||
Height = size,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
}
|
||||
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
FontSize = displayMode == LauncherDisplayMode.CompactList ? 12 : 14,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
|
||||
return new Border
|
||||
{
|
||||
MinHeight = DisplayModeMetrics.GetRowHeight(displayMode),
|
||||
Child = panel
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateTargetAvailability()
|
||||
{
|
||||
bool isMenu = TypeBox.SelectedItem is LauncherItemType.Menu;
|
||||
TargetBox.IsEnabled = !isMenu;
|
||||
ArgumentsBox.IsEnabled = !isMenu;
|
||||
if (isMenu && !isLoadingSelection)
|
||||
{
|
||||
TargetBox.Text = "";
|
||||
ArgumentsBox.Text = "";
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
e.Cancel = true;
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
44
Models/LauncherConfig.cs
Normal file
44
Models/LauncherConfig.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
namespace TaskbarLauncher.Models;
|
||||
|
||||
public sealed class LauncherConfig
|
||||
{
|
||||
public string Hotkey { get; set; } = "Ctrl+Alt+Space";
|
||||
|
||||
public LauncherDisplayMode DisplayMode { get; set; } = LauncherDisplayMode.CompactList;
|
||||
|
||||
public List<LauncherItem> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class LauncherItem
|
||||
{
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
public LauncherItemType Type { get; set; } = LauncherItemType.App;
|
||||
|
||||
public string Target { get; set; } = "";
|
||||
|
||||
public string? Arguments { get; set; }
|
||||
|
||||
public string? IconPath { get; set; }
|
||||
|
||||
public LauncherDisplayMode DisplayMode { get; set; } = LauncherDisplayMode.CompactList;
|
||||
|
||||
public List<LauncherItem> Children { get; set; } = [];
|
||||
}
|
||||
|
||||
public enum LauncherItemType
|
||||
{
|
||||
App,
|
||||
Folder,
|
||||
File,
|
||||
Website,
|
||||
Menu
|
||||
}
|
||||
|
||||
public enum LauncherDisplayMode
|
||||
{
|
||||
LargeIconOnly,
|
||||
SmallIconWithText,
|
||||
LargeIconWithText,
|
||||
CompactList
|
||||
}
|
||||
158
Services/ConfigService.cs
Normal file
158
Services/ConfigService.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using TaskbarLauncher.Models;
|
||||
|
||||
namespace TaskbarLauncher.Services;
|
||||
|
||||
public static class ConfigService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
static ConfigService()
|
||||
{
|
||||
JsonOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
}
|
||||
|
||||
public static string AppFolder => AppContext.BaseDirectory;
|
||||
|
||||
public static string ConfigPath => Path.Combine(AppFolder, "config.json");
|
||||
|
||||
public static string BackupFolder => Path.Combine(AppFolder, "backups");
|
||||
|
||||
public static LauncherConfig Load()
|
||||
{
|
||||
if (!File.Exists(ConfigPath))
|
||||
{
|
||||
LauncherConfig defaults = CreateDefaultConfig();
|
||||
Save(defaults);
|
||||
return defaults;
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(ConfigPath);
|
||||
LauncherConfig config = JsonSerializer.Deserialize<LauncherConfig>(json, JsonOptions) ?? CreateDefaultConfig();
|
||||
NormalizeDisplayModes(config);
|
||||
Save(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static void Save(LauncherConfig config)
|
||||
{
|
||||
string json = JsonSerializer.Serialize(config, JsonOptions);
|
||||
File.WriteAllText(ConfigPath, json);
|
||||
}
|
||||
|
||||
public static string Backup()
|
||||
{
|
||||
Directory.CreateDirectory(BackupFolder);
|
||||
if (!File.Exists(ConfigPath))
|
||||
{
|
||||
Save(CreateDefaultConfig());
|
||||
}
|
||||
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
|
||||
string backupPath = Path.Combine(BackupFolder, $"config-{timestamp}.json");
|
||||
File.Copy(ConfigPath, backupPath, overwrite: false);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
public static LauncherConfig Restore(string sourcePath)
|
||||
{
|
||||
if (!File.Exists(sourcePath))
|
||||
{
|
||||
throw new FileNotFoundException("The selected config file was not found.", sourcePath);
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(sourcePath);
|
||||
LauncherConfig config = JsonSerializer.Deserialize<LauncherConfig>(json, JsonOptions)
|
||||
?? throw new InvalidDataException("The selected config file could not be read.");
|
||||
|
||||
NormalizeDisplayModes(config);
|
||||
Save(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static void OpenConfigFolder()
|
||||
{
|
||||
Directory.CreateDirectory(AppFolder);
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = AppFolder,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
|
||||
private static LauncherConfig CreateDefaultConfig()
|
||||
{
|
||||
LauncherConfig config = new()
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new LauncherItem
|
||||
{
|
||||
Title = "Folders",
|
||||
Type = LauncherItemType.Menu,
|
||||
Children =
|
||||
[
|
||||
new LauncherItem
|
||||
{
|
||||
Title = "Documents",
|
||||
Type = LauncherItemType.Folder,
|
||||
Target = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
|
||||
},
|
||||
new LauncherItem
|
||||
{
|
||||
Title = "Downloads",
|
||||
Type = LauncherItemType.Folder,
|
||||
Target = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads")
|
||||
}
|
||||
]
|
||||
},
|
||||
new LauncherItem
|
||||
{
|
||||
Title = "Apps",
|
||||
Type = LauncherItemType.Menu,
|
||||
Children =
|
||||
[
|
||||
new LauncherItem
|
||||
{
|
||||
Title = "Notepad",
|
||||
Type = LauncherItemType.App,
|
||||
Target = "notepad.exe",
|
||||
IconPath = "notepad.exe"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
NormalizeDisplayModes(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
private static void NormalizeDisplayModes(LauncherConfig config)
|
||||
{
|
||||
foreach (LauncherItem item in config.Items)
|
||||
{
|
||||
NormalizeDisplayModes(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeDisplayModes(LauncherItem item)
|
||||
{
|
||||
if (!Enum.IsDefined(item.DisplayMode))
|
||||
{
|
||||
item.DisplayMode = LauncherDisplayMode.CompactList;
|
||||
}
|
||||
|
||||
foreach (LauncherItem child in item.Children)
|
||||
{
|
||||
NormalizeDisplayModes(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Services/DisplayModeMetrics.cs
Normal file
30
Services/DisplayModeMetrics.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using TaskbarLauncher.Models;
|
||||
|
||||
namespace TaskbarLauncher.Services;
|
||||
|
||||
public static class DisplayModeMetrics
|
||||
{
|
||||
public static double GetIconSize(LauncherDisplayMode displayMode)
|
||||
{
|
||||
return displayMode switch
|
||||
{
|
||||
LauncherDisplayMode.CompactList => 14,
|
||||
LauncherDisplayMode.SmallIconWithText => 20,
|
||||
LauncherDisplayMode.LargeIconWithText => 32,
|
||||
LauncherDisplayMode.LargeIconOnly => 40,
|
||||
_ => 14
|
||||
};
|
||||
}
|
||||
|
||||
public static double GetRowHeight(LauncherDisplayMode displayMode)
|
||||
{
|
||||
return displayMode switch
|
||||
{
|
||||
LauncherDisplayMode.CompactList => 28,
|
||||
LauncherDisplayMode.SmallIconWithText => 34,
|
||||
LauncherDisplayMode.LargeIconWithText => 52,
|
||||
LauncherDisplayMode.LargeIconOnly => 56,
|
||||
_ => 28
|
||||
};
|
||||
}
|
||||
}
|
||||
116
Services/HotkeyService.cs
Normal file
116
Services/HotkeyService.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace TaskbarLauncher.Services;
|
||||
|
||||
public sealed class HotkeyService : IDisposable
|
||||
{
|
||||
private const int WmHotkey = 0x0312;
|
||||
private const int HotkeyId = 9001;
|
||||
|
||||
private HwndSource? source;
|
||||
private Window? window;
|
||||
|
||||
public event EventHandler? Pressed;
|
||||
|
||||
public void Register(string hotkey)
|
||||
{
|
||||
Unregister();
|
||||
|
||||
window = new Window
|
||||
{
|
||||
Width = 0,
|
||||
Height = 0,
|
||||
ShowInTaskbar = false,
|
||||
WindowStyle = WindowStyle.None,
|
||||
AllowsTransparency = true,
|
||||
Opacity = 0,
|
||||
Left = -10000,
|
||||
Top = -10000
|
||||
};
|
||||
|
||||
window.SourceInitialized += (_, _) =>
|
||||
{
|
||||
var helper = new WindowInteropHelper(window);
|
||||
source = HwndSource.FromHwnd(helper.Handle);
|
||||
source?.AddHook(WndProc);
|
||||
|
||||
ParseHotkey(hotkey, out uint modifiers, out uint key);
|
||||
RegisterHotKey(helper.Handle, HotkeyId, modifiers, key);
|
||||
};
|
||||
|
||||
window.Show();
|
||||
window.Hide();
|
||||
}
|
||||
|
||||
public void Unregister()
|
||||
{
|
||||
if (window is not null)
|
||||
{
|
||||
var helper = new WindowInteropHelper(window);
|
||||
if (helper.Handle != IntPtr.Zero)
|
||||
{
|
||||
UnregisterHotKey(helper.Handle, HotkeyId);
|
||||
}
|
||||
}
|
||||
|
||||
source?.RemoveHook(WndProc);
|
||||
source = null;
|
||||
window?.Close();
|
||||
window = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Unregister();
|
||||
}
|
||||
|
||||
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WmHotkey && wParam.ToInt32() == HotkeyId)
|
||||
{
|
||||
Pressed?.Invoke(this, EventArgs.Empty);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
private static void ParseHotkey(string hotkey, out uint modifiers, out uint key)
|
||||
{
|
||||
modifiers = 0;
|
||||
key = (uint)KeyInterop.VirtualKeyFromKey(Key.Space);
|
||||
|
||||
foreach (string part in hotkey.Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (part.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || part.Equals("Control", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
modifiers |= 0x0002;
|
||||
}
|
||||
else if (part.Equals("Alt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
modifiers |= 0x0001;
|
||||
}
|
||||
else if (part.Equals("Shift", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
modifiers |= 0x0004;
|
||||
}
|
||||
else if (part.Equals("Win", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
modifiers |= 0x0008;
|
||||
}
|
||||
else if (Enum.TryParse(part, true, out Key parsedKey))
|
||||
{
|
||||
key = (uint)KeyInterop.VirtualKeyFromKey(parsedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
|
||||
}
|
||||
127
Services/IconService.cs
Normal file
127
Services/IconService.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using TaskbarLauncher.Models;
|
||||
|
||||
namespace TaskbarLauncher.Services;
|
||||
|
||||
public static class IconService
|
||||
{
|
||||
private const uint ShgfiIcon = 0x000000100;
|
||||
private const uint ShgfiLargeIcon = 0x000000000;
|
||||
private const uint ShgfiSmallIcon = 0x000000001;
|
||||
private const uint ShgfiUseFileAttributes = 0x000000010;
|
||||
private const uint FileAttributeDirectory = 0x00000010;
|
||||
private const uint FileAttributeNormal = 0x00000080;
|
||||
|
||||
public static ImageSource? GetIcon(LauncherItem item, bool large)
|
||||
{
|
||||
ImageSource? explicitIcon = TryLoadIcon(item.IconPath, large, useAttributes: false);
|
||||
if (explicitIcon is not null)
|
||||
{
|
||||
return explicitIcon;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.Target))
|
||||
{
|
||||
ImageSource? targetIcon = TryLoadIcon(item.Target, large, useAttributes: false);
|
||||
if (targetIcon is not null)
|
||||
{
|
||||
return targetIcon;
|
||||
}
|
||||
}
|
||||
|
||||
return item.Type switch
|
||||
{
|
||||
LauncherItemType.Folder => TryLoadIcon("folder", large, useAttributes: true, isDirectory: true),
|
||||
LauncherItemType.Website => TryLoadIcon(".url", large, useAttributes: true),
|
||||
LauncherItemType.File => TryLoadIcon(".txt", large, useAttributes: true),
|
||||
LauncherItemType.Menu => TryLoadIcon("folder", large, useAttributes: true, isDirectory: true),
|
||||
_ => TryLoadIcon(".exe", large, useAttributes: true)
|
||||
};
|
||||
}
|
||||
|
||||
private static ImageSource? TryLoadIcon(string? path, bool large, bool useAttributes, bool isDirectory = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
uint flags = ShgfiIcon | (large ? ShgfiLargeIcon : ShgfiSmallIcon);
|
||||
uint attributes = isDirectory ? FileAttributeDirectory : FileAttributeNormal;
|
||||
if (useAttributes)
|
||||
{
|
||||
flags |= ShgfiUseFileAttributes;
|
||||
}
|
||||
|
||||
IntPtr result = SHGetFileInfo(path, attributes, out ShFileInfo fileInfo, (uint)Marshal.SizeOf<ShFileInfo>(), flags);
|
||||
if (result == IntPtr.Zero || fileInfo.IconHandle == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ImageSource image = Imaging.CreateBitmapSourceFromHIcon(
|
||||
fileInfo.IconHandle,
|
||||
Int32Rect.Empty,
|
||||
BitmapSizeOptions.FromEmptyOptions());
|
||||
|
||||
image.Freeze();
|
||||
DestroyIcon(fileInfo.IconHandle);
|
||||
return image;
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
using Icon? icon = File.Exists(path) ? Icon.ExtractAssociatedIcon(path) : null;
|
||||
if (icon is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ImageSource image = Imaging.CreateBitmapSourceFromHIcon(
|
||||
icon.Handle,
|
||||
Int32Rect.Empty,
|
||||
BitmapSizeOptions.FromEmptyOptions());
|
||||
image.Freeze();
|
||||
return image;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr SHGetFileInfo(
|
||||
string path,
|
||||
uint fileAttributes,
|
||||
out ShFileInfo fileInfo,
|
||||
uint fileInfoSize,
|
||||
uint flags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool DestroyIcon(IntPtr icon);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct ShFileInfo
|
||||
{
|
||||
public IntPtr IconHandle;
|
||||
public int IconIndex;
|
||||
public uint Attributes;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
||||
public string DisplayName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
|
||||
public string TypeName;
|
||||
}
|
||||
}
|
||||
109
Services/LauncherController.cs
Normal file
109
Services/LauncherController.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using TaskbarLauncher.Models;
|
||||
using TaskbarLauncher.Views;
|
||||
|
||||
namespace TaskbarLauncher.Services;
|
||||
|
||||
public sealed class LauncherController : IDisposable
|
||||
{
|
||||
private readonly HotkeyService hotkeyService = new();
|
||||
private NotifyIcon? notifyIcon;
|
||||
private LauncherPopup? popup;
|
||||
private MainWindow? settingsWindow;
|
||||
|
||||
public LauncherConfig Config { get; private set; } = new();
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Config = ConfigService.Load();
|
||||
CreateTrayIcon();
|
||||
RegisterHotkey();
|
||||
}
|
||||
|
||||
public void ReloadConfig()
|
||||
{
|
||||
Config = ConfigService.Load();
|
||||
RegisterHotkey();
|
||||
popup?.Close();
|
||||
popup = null;
|
||||
}
|
||||
|
||||
public void ShowSettings()
|
||||
{
|
||||
settingsWindow ??= new MainWindow(this);
|
||||
settingsWindow.Show();
|
||||
settingsWindow.Activate();
|
||||
}
|
||||
|
||||
public void TogglePopup()
|
||||
{
|
||||
if (popup?.IsVisible == true)
|
||||
{
|
||||
popup.Close();
|
||||
popup = null;
|
||||
return;
|
||||
}
|
||||
|
||||
popup = new LauncherPopup(Config, LaunchItem, ShowSettings);
|
||||
popup.Closed += (_, _) => popup = null;
|
||||
popup.ShowNearTaskbar();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
hotkeyService.Dispose();
|
||||
notifyIcon?.Dispose();
|
||||
}
|
||||
|
||||
private void RegisterHotkey()
|
||||
{
|
||||
hotkeyService.Pressed -= HotkeyPressed;
|
||||
hotkeyService.Register(Config.Hotkey);
|
||||
hotkeyService.Pressed += HotkeyPressed;
|
||||
}
|
||||
|
||||
private void HotkeyPressed(object? sender, EventArgs e)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(TogglePopup);
|
||||
}
|
||||
|
||||
private void CreateTrayIcon()
|
||||
{
|
||||
notifyIcon = new NotifyIcon
|
||||
{
|
||||
Text = "Taskbar Launcher",
|
||||
Icon = SystemIcons.Application,
|
||||
Visible = true,
|
||||
ContextMenuStrip = new ContextMenuStrip()
|
||||
};
|
||||
|
||||
notifyIcon.DoubleClick += (_, _) => TogglePopup();
|
||||
notifyIcon.ContextMenuStrip.Items.Add("Open Launcher", null, (_, _) => TogglePopup());
|
||||
notifyIcon.ContextMenuStrip.Items.Add("Settings", null, (_, _) => ShowSettings());
|
||||
notifyIcon.ContextMenuStrip.Items.Add("Reload Config", null, (_, _) => ReloadConfig());
|
||||
notifyIcon.ContextMenuStrip.Items.Add("Open Config Folder", null, (_, _) => ConfigService.OpenConfigFolder());
|
||||
notifyIcon.ContextMenuStrip.Items.Add("Exit", null, (_, _) => System.Windows.Application.Current.Shutdown());
|
||||
}
|
||||
|
||||
private void LaunchItem(LauncherItem item)
|
||||
{
|
||||
if (item.Type == LauncherItemType.Menu || string.IsNullOrWhiteSpace(item.Target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
FileName = item.Target,
|
||||
Arguments = item.Arguments ?? "",
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = Directory.Exists(item.Target) ? item.Target : ConfigService.AppFolder
|
||||
};
|
||||
|
||||
Process.Start(startInfo);
|
||||
popup?.Close();
|
||||
}
|
||||
}
|
||||
68
Services/StartupService.cs
Normal file
68
Services/StartupService.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.IO;
|
||||
|
||||
namespace TaskbarLauncher.Services;
|
||||
|
||||
public static class StartupService
|
||||
{
|
||||
private const string ShortcutName = "TaskbarLauncher.lnk";
|
||||
|
||||
public static string StartupFolder => Environment.GetFolderPath(Environment.SpecialFolder.Startup);
|
||||
|
||||
public static string ShortcutPath => Path.Combine(StartupFolder, ShortcutName);
|
||||
|
||||
public static bool IsEnabled()
|
||||
{
|
||||
return File.Exists(ShortcutPath);
|
||||
}
|
||||
|
||||
public static void SetEnabled(bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
CreateShortcut();
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveShortcut();
|
||||
}
|
||||
}
|
||||
|
||||
public static string? GetShortcutTarget()
|
||||
{
|
||||
if (!File.Exists(ShortcutPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type shellType = Type.GetTypeFromProgID("WScript.Shell")
|
||||
?? throw new InvalidOperationException("Windows Script Host is not available.");
|
||||
dynamic shell = Activator.CreateInstance(shellType)
|
||||
?? throw new InvalidOperationException("Could not create Windows Script Host shell.");
|
||||
dynamic shortcut = shell.CreateShortcut(ShortcutPath);
|
||||
return shortcut.TargetPath;
|
||||
}
|
||||
|
||||
private static void CreateShortcut()
|
||||
{
|
||||
Directory.CreateDirectory(StartupFolder);
|
||||
|
||||
string executablePath = Environment.ProcessPath ?? Path.Combine(ConfigService.AppFolder, "TaskbarLauncher.exe");
|
||||
Type shellType = Type.GetTypeFromProgID("WScript.Shell")
|
||||
?? throw new InvalidOperationException("Windows Script Host is not available.");
|
||||
dynamic shell = Activator.CreateInstance(shellType)
|
||||
?? throw new InvalidOperationException("Could not create Windows Script Host shell.");
|
||||
dynamic shortcut = shell.CreateShortcut(ShortcutPath);
|
||||
shortcut.TargetPath = executablePath;
|
||||
shortcut.WorkingDirectory = ConfigService.AppFolder;
|
||||
shortcut.Description = "Start Taskbar Launcher";
|
||||
shortcut.Save();
|
||||
}
|
||||
|
||||
private static void RemoveShortcut()
|
||||
{
|
||||
if (File.Exists(ShortcutPath))
|
||||
{
|
||||
File.Delete(ShortcutPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
Services/ThemeService.cs
Normal file
84
Services/ThemeService.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Win32;
|
||||
using MediaColor = System.Windows.Media.Color;
|
||||
|
||||
namespace TaskbarLauncher.Services;
|
||||
|
||||
public static class ThemeService
|
||||
{
|
||||
private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
|
||||
private const string DwmKey = @"Software\Microsoft\Windows\DWM";
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
bool isLight = IsLightTheme();
|
||||
MediaColor accent = GetAccentColor();
|
||||
MediaColor window = isLight ? MediaColor.FromRgb(0xF7, 0xF7, 0xF7) : MediaColor.FromRgb(0x20, 0x20, 0x20);
|
||||
MediaColor surface = isLight ? System.Windows.Media.Colors.White : MediaColor.FromRgb(0x2B, 0x2B, 0x2B);
|
||||
MediaColor control = isLight ? MediaColor.FromRgb(0xF3, 0xF3, 0xF3) : MediaColor.FromRgb(0x33, 0x33, 0x33);
|
||||
MediaColor hover = isLight ? MediaColor.FromRgb(0xEA, 0xF3, 0xFF) : MediaColor.FromRgb(0x3B, 0x4A, 0x5C);
|
||||
MediaColor text = isLight ? MediaColor.FromRgb(0x1A, 0x1A, 0x1A) : MediaColor.FromRgb(0xF3, 0xF3, 0xF3);
|
||||
MediaColor muted = isLight ? MediaColor.FromRgb(0x66, 0x66, 0x66) : MediaColor.FromRgb(0xB8, 0xB8, 0xB8);
|
||||
MediaColor border = isLight ? MediaColor.FromRgb(0xC8, 0xC8, 0xC8) : MediaColor.FromRgb(0x55, 0x55, 0x55);
|
||||
MediaColor selectedText = IsDark(accent) ? System.Windows.Media.Colors.White : System.Windows.Media.Colors.Black;
|
||||
|
||||
SetBrush("AppWindowBrush", window);
|
||||
SetBrush("AppSurfaceBrush", surface);
|
||||
SetBrush("AppControlBrush", control);
|
||||
SetBrush("AppControlHoverBrush", hover);
|
||||
SetBrush("AppTextBrush", text);
|
||||
SetBrush("AppMutedTextBrush", muted);
|
||||
SetBrush("AppBorderBrush", border);
|
||||
SetBrush("AppAccentBrush", accent);
|
||||
SetBrush("AppSelectedTextBrush", selectedText);
|
||||
|
||||
SetBrush(System.Windows.SystemColors.WindowBrushKey, surface);
|
||||
SetBrush(System.Windows.SystemColors.WindowTextBrushKey, text);
|
||||
SetBrush(System.Windows.SystemColors.ControlBrushKey, control);
|
||||
SetBrush(System.Windows.SystemColors.ControlTextBrushKey, text);
|
||||
SetBrush(System.Windows.SystemColors.ControlDarkBrushKey, border);
|
||||
SetBrush(System.Windows.SystemColors.HighlightBrushKey, accent);
|
||||
SetBrush(System.Windows.SystemColors.HighlightTextBrushKey, selectedText);
|
||||
}
|
||||
|
||||
private static bool IsLightTheme()
|
||||
{
|
||||
object? value = Registry.CurrentUser.OpenSubKey(PersonalizeKey)?.GetValue("AppsUseLightTheme");
|
||||
return value is null || Convert.ToInt32(value) != 0;
|
||||
}
|
||||
|
||||
private static MediaColor GetAccentColor()
|
||||
{
|
||||
object? value = Registry.CurrentUser.OpenSubKey(DwmKey)?.GetValue("AccentColor");
|
||||
if (value is int colorValue)
|
||||
{
|
||||
byte r = (byte)(colorValue & 0xFF);
|
||||
byte g = (byte)((colorValue >> 8) & 0xFF);
|
||||
byte b = (byte)((colorValue >> 16) & 0xFF);
|
||||
return MediaColor.FromRgb(r, g, b);
|
||||
}
|
||||
|
||||
return MediaColor.FromRgb(0x00, 0x78, 0xD4);
|
||||
}
|
||||
|
||||
private static void SetBrush(string key, MediaColor color)
|
||||
{
|
||||
SolidColorBrush brush = new(color);
|
||||
brush.Freeze();
|
||||
System.Windows.Application.Current.Resources[key] = brush;
|
||||
}
|
||||
|
||||
private static void SetBrush(ResourceKey key, MediaColor color)
|
||||
{
|
||||
SolidColorBrush brush = new(color);
|
||||
brush.Freeze();
|
||||
System.Windows.Application.Current.Resources[key] = brush;
|
||||
}
|
||||
|
||||
private static bool IsDark(MediaColor color)
|
||||
{
|
||||
double luminance = (0.299 * color.R) + (0.587 * color.G) + (0.114 * color.B);
|
||||
return luminance < 140;
|
||||
}
|
||||
}
|
||||
17
TaskbarLauncher.csproj
Normal file
17
TaskbarLauncher.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);bin\**;obj\**;..\TaskbarLauncher-build\**</DefaultItemExcludes>
|
||||
<Version>1.0.0</Version>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<InformationalVersion>1.0.0</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
98
Tools/New-TaskbarLauncherRelease.ps1
Normal file
98
Tools/New-TaskbarLauncherRelease.ps1
Normal file
@@ -0,0 +1,98 @@
|
||||
param(
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Runtime = "win-x64",
|
||||
[switch]$IncludeCurrentConfig,
|
||||
[switch]$SelfContained
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$projectDir = Split-Path -Parent $PSScriptRoot
|
||||
$workspaceDir = Split-Path -Parent $projectDir
|
||||
$projectPath = Join-Path $projectDir "TaskbarLauncher.csproj"
|
||||
|
||||
[xml]$projectXml = Get-Content -LiteralPath $projectPath
|
||||
$version = $projectXml.Project.PropertyGroup.Version | Select-Object -First 1
|
||||
if ([string]::IsNullOrWhiteSpace($version)) {
|
||||
$version = "0.0.0"
|
||||
}
|
||||
|
||||
$packageKind = if ($SelfContained) { "self-contained" } else { "small" }
|
||||
$releaseRoot = Join-Path $workspaceDir "TaskbarLauncher-releases"
|
||||
$releaseDir = Join-Path $releaseRoot "TaskbarLauncher-v$version-$packageKind"
|
||||
$zipPath = Join-Path $releaseRoot "TaskbarLauncher-v$version-$packageKind.zip"
|
||||
$buildDir = Join-Path $workspaceDir "TaskbarLauncher-build-release-$packageKind"
|
||||
|
||||
Get-Process TaskbarLauncher -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
|
||||
if (Test-Path -LiteralPath $releaseDir) {
|
||||
Remove-Item -LiteralPath $releaseDir -Recurse -Force
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $releaseDir | Out-Null
|
||||
|
||||
dotnet publish $projectPath `
|
||||
-c $Configuration `
|
||||
-r $Runtime `
|
||||
--self-contained:$($SelfContained.IsPresent.ToString().ToLowerInvariant()) `
|
||||
-p:PublishSingleFile=false `
|
||||
-p:DebugType=None `
|
||||
-p:DebugSymbols=false `
|
||||
-p:BaseOutputPath="$buildDir\bin\" `
|
||||
-p:BaseIntermediateOutputPath="$buildDir\obj\" `
|
||||
-o $releaseDir
|
||||
|
||||
Get-ChildItem -LiteralPath $releaseDir -Filter "*.pdb" -ErrorAction SilentlyContinue |
|
||||
Remove-Item -Force
|
||||
|
||||
if ($IncludeCurrentConfig) {
|
||||
$configPath = Join-Path $workspaceDir "TaskbarLauncher-editor-publish\config.json"
|
||||
if (Test-Path -LiteralPath $configPath) {
|
||||
Copy-Item -LiteralPath $configPath -Destination (Join-Path $releaseDir "config.json") -Force
|
||||
}
|
||||
}
|
||||
|
||||
$runtimeNote = if ($SelfContained) {
|
||||
"This package includes the .NET runtime. No separate .NET install is required."
|
||||
} else {
|
||||
"This small package requires Microsoft .NET 10 Desktop Runtime x64."
|
||||
}
|
||||
|
||||
$readme = @"
|
||||
Taskbar Launcher v$version
|
||||
|
||||
Requirements:
|
||||
- Windows x64
|
||||
- $runtimeNote
|
||||
|
||||
.NET 10 Desktop Runtime download:
|
||||
https://dotnet.microsoft.com/en-us/download/dotnet/10.0
|
||||
|
||||
Run:
|
||||
Double-click TaskbarLauncher.exe.
|
||||
|
||||
Notes:
|
||||
- The app stores portable settings in config.json beside TaskbarLauncher.exe.
|
||||
- If config.json is missing, the app creates a clean starter config on first run.
|
||||
- Use Settings > Backup Config before making larger menu changes.
|
||||
"@
|
||||
|
||||
Set-Content -LiteralPath (Join-Path $releaseDir "README.txt") -Value $readme -Encoding UTF8
|
||||
|
||||
if (Test-Path -LiteralPath $zipPath) {
|
||||
Remove-Item -LiteralPath $zipPath -Force
|
||||
}
|
||||
|
||||
Compress-Archive -Path (Join-Path $releaseDir "*") -DestinationPath $zipPath -Force
|
||||
|
||||
$folderSize = (Get-ChildItem -LiteralPath $releaseDir -Recurse | Measure-Object -Property Length -Sum).Sum
|
||||
$zip = Get-Item -LiteralPath $zipPath
|
||||
|
||||
[pscustomobject]@{
|
||||
Version = $version
|
||||
PackageKind = $packageKind
|
||||
ReleaseFolder = $releaseDir
|
||||
ZipPath = $zip.FullName
|
||||
FolderBytes = $folderSize
|
||||
ZipBytes = $zip.Length
|
||||
}
|
||||
36
Views/LauncherPopup.xaml
Normal file
36
Views/LauncherPopup.xaml
Normal file
@@ -0,0 +1,36 @@
|
||||
<Window x:Class="TaskbarLauncher.Views.LauncherPopup"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Taskbar Launcher"
|
||||
Width="360" SizeToContent="Height"
|
||||
MaxHeight="720"
|
||||
WindowStyle="None"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
Background="{DynamicResource AppWindowBrush}"
|
||||
Foreground="{DynamicResource AppTextBrush}">
|
||||
<Border BorderThickness="1"
|
||||
BorderBrush="{DynamicResource AppBorderBrush}"
|
||||
Background="{DynamicResource AppSurfaceBrush}"
|
||||
Padding="10">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<TextBlock Text="Taskbar Launcher" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
<Button Content="Settings" HorizontalAlignment="Right" Margin="12,0,0,0" Padding="8,4"
|
||||
Click="Settings_Click"/>
|
||||
</StackPanel>
|
||||
<TextBox x:Name="SearchBox"
|
||||
DockPanel.Dock="Top"
|
||||
Margin="0,0,0,8"
|
||||
Padding="8,5"
|
||||
TextChanged="SearchBox_TextChanged"
|
||||
PreviewKeyDown="SearchBox_PreviewKeyDown"/>
|
||||
<ScrollViewer x:Name="ItemsScrollViewer"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="ItemsPanel"/>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
331
Views/LauncherPopup.xaml.cs
Normal file
331
Views/LauncherPopup.xaml.cs
Normal file
@@ -0,0 +1,331 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using TaskbarLauncher.Models;
|
||||
using TaskbarLauncher.Services;
|
||||
|
||||
namespace TaskbarLauncher.Views;
|
||||
|
||||
public partial class LauncherPopup : Window
|
||||
{
|
||||
private const double ScreenMargin = 12;
|
||||
private const double HeaderAndChromeHeight = 118;
|
||||
private bool isRepositionQueued;
|
||||
private readonly List<System.Windows.Controls.Button> visibleLaunchButtons = [];
|
||||
|
||||
private readonly LauncherConfig config;
|
||||
private readonly Action<LauncherItem> launchItem;
|
||||
private readonly Action showSettings;
|
||||
|
||||
public LauncherPopup(LauncherConfig config, Action<LauncherItem> launchItem, Action showSettings)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.config = config;
|
||||
this.launchItem = launchItem;
|
||||
this.showSettings = showSettings;
|
||||
BuildItems();
|
||||
SizeChanged += (_, _) => QueueReposition();
|
||||
}
|
||||
|
||||
public void ShowNearTaskbar()
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
ApplyScreenBounds();
|
||||
|
||||
Show();
|
||||
UpdateLayout();
|
||||
RepositionAboveTaskbar();
|
||||
Activate();
|
||||
SearchBox.Focus();
|
||||
}
|
||||
|
||||
private void QueueReposition()
|
||||
{
|
||||
if (isRepositionQueued)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isRepositionQueued = true;
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
isRepositionQueued = false;
|
||||
ApplyScreenBounds();
|
||||
RepositionAboveTaskbar();
|
||||
}, DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void ApplyScreenBounds()
|
||||
{
|
||||
Rect workArea = SystemParameters.WorkArea;
|
||||
double maxPopupHeight = Math.Max(240, workArea.Height - (ScreenMargin * 2));
|
||||
MaxHeight = maxPopupHeight;
|
||||
ItemsScrollViewer.MaxHeight = Math.Max(160, maxPopupHeight - HeaderAndChromeHeight);
|
||||
Left = Math.Max(workArea.Left + ScreenMargin, workArea.Right - Width - ScreenMargin);
|
||||
}
|
||||
|
||||
private void RepositionAboveTaskbar()
|
||||
{
|
||||
Rect workArea = SystemParameters.WorkArea;
|
||||
double height = ActualHeight > 0 ? ActualHeight : DesiredSize.Height;
|
||||
double clampedHeight = Math.Min(height, MaxHeight);
|
||||
|
||||
if (height > MaxHeight && !double.IsNaN(MaxHeight) && MaxHeight > 0)
|
||||
{
|
||||
Height = MaxHeight;
|
||||
clampedHeight = MaxHeight;
|
||||
}
|
||||
else if (Height > 0 && height < MaxHeight)
|
||||
{
|
||||
ClearValue(HeightProperty);
|
||||
}
|
||||
|
||||
Top = Math.Max(workArea.Top + ScreenMargin, workArea.Bottom - clampedHeight - ScreenMargin);
|
||||
}
|
||||
|
||||
private void BuildItems()
|
||||
{
|
||||
ItemsPanel.Children.Clear();
|
||||
visibleLaunchButtons.Clear();
|
||||
bool expandMenusByDefault = config.Items.Count <= 2;
|
||||
foreach (LauncherItem item in config.Items)
|
||||
{
|
||||
ItemsPanel.Children.Add(CreateItemControl(item, 0, expandMenusByDefault));
|
||||
}
|
||||
}
|
||||
|
||||
private FrameworkElement CreateItemControl(LauncherItem item, int depth, bool expandMenusByDefault)
|
||||
{
|
||||
if (item.Type == LauncherItemType.Menu)
|
||||
{
|
||||
Expander expander = new()
|
||||
{
|
||||
Header = item.Title,
|
||||
IsExpanded = expandMenusByDefault,
|
||||
Margin = new Thickness(depth * 12, 4, 0, 4)
|
||||
};
|
||||
|
||||
StackPanel children = new();
|
||||
foreach (LauncherItem child in item.Children)
|
||||
{
|
||||
children.Children.Add(CreateItemControl(child, depth + 1, expandMenusByDefault));
|
||||
}
|
||||
|
||||
expander.Content = children;
|
||||
return expander;
|
||||
}
|
||||
|
||||
System.Windows.Controls.Button button = new()
|
||||
{
|
||||
Content = CreateButtonContent(item),
|
||||
Margin = new Thickness(depth * 12, 2, 0, 2),
|
||||
MinHeight = DisplayModeMetrics.GetRowHeight(GetEffectiveDisplayMode(item)),
|
||||
Tag = item
|
||||
};
|
||||
|
||||
button.Click += (_, _) => launchItem(item);
|
||||
button.KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
launchItem(item);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Escape)
|
||||
{
|
||||
Close();
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
visibleLaunchButtons.Add(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
string query = SearchBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
BuildItems();
|
||||
}
|
||||
else
|
||||
{
|
||||
BuildSearchResults(query);
|
||||
}
|
||||
|
||||
QueueReposition();
|
||||
}
|
||||
|
||||
private void BuildSearchResults(string query)
|
||||
{
|
||||
ItemsPanel.Children.Clear();
|
||||
visibleLaunchButtons.Clear();
|
||||
|
||||
List<LauncherItem> matches = [];
|
||||
CollectMatches(config.Items, query, matches);
|
||||
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
ItemsPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "No matches",
|
||||
Foreground = (System.Windows.Media.Brush)System.Windows.Application.Current.Resources["AppMutedTextBrush"],
|
||||
Margin = new Thickness(4, 8, 4, 8)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (LauncherItem item in matches)
|
||||
{
|
||||
ItemsPanel.Children.Add(CreateItemControl(item, 0, expandMenusByDefault: false));
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectMatches(IEnumerable<LauncherItem> items, string query, List<LauncherItem> matches)
|
||||
{
|
||||
foreach (LauncherItem item in items)
|
||||
{
|
||||
bool isLaunchable = item.Type != LauncherItemType.Menu;
|
||||
if (isLaunchable && Matches(item, query))
|
||||
{
|
||||
matches.Add(item);
|
||||
}
|
||||
|
||||
if (item.Children.Count > 0)
|
||||
{
|
||||
CollectMatches(item.Children, query, matches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Matches(LauncherItem item, string query)
|
||||
{
|
||||
return item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
item.Target.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.Arguments?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
}
|
||||
|
||||
private void SearchBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
Close();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Enter && visibleLaunchButtons.FirstOrDefault()?.Tag is LauncherItem item)
|
||||
{
|
||||
launchItem(item);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Down && visibleLaunchButtons.FirstOrDefault() is System.Windows.Controls.Button firstButton)
|
||||
{
|
||||
Keyboard.ClearFocus();
|
||||
firstButton.Focus();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
Close();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
private object CreateButtonContent(LauncherItem item)
|
||||
{
|
||||
LauncherDisplayMode displayMode = GetEffectiveDisplayMode(item);
|
||||
|
||||
if (displayMode == LauncherDisplayMode.LargeIconOnly)
|
||||
{
|
||||
ImageSource? iconOnly = IconService.GetIcon(item, large: true);
|
||||
if (iconOnly is not null)
|
||||
{
|
||||
double iconOnlySize = DisplayModeMetrics.GetIconSize(displayMode);
|
||||
return new System.Windows.Controls.Image
|
||||
{
|
||||
Source = iconOnly,
|
||||
Width = iconOnlySize,
|
||||
Height = iconOnlySize,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Center
|
||||
};
|
||||
}
|
||||
|
||||
return new TextBlock
|
||||
{
|
||||
Text = GetLabel(item),
|
||||
FontSize = 20,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Center
|
||||
};
|
||||
}
|
||||
|
||||
StackPanel panel = new() { Orientation = System.Windows.Controls.Orientation.Horizontal };
|
||||
bool large = displayMode is LauncherDisplayMode.LargeIconOnly or LauncherDisplayMode.LargeIconWithText;
|
||||
ImageSource? icon = IconService.GetIcon(item, large);
|
||||
|
||||
if (icon is not null)
|
||||
{
|
||||
double size = DisplayModeMetrics.GetIconSize(displayMode);
|
||||
panel.Children.Add(new System.Windows.Controls.Image
|
||||
{
|
||||
Source = icon,
|
||||
Width = size,
|
||||
Height = size,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = GetLabel(item),
|
||||
FontSize = displayMode == LauncherDisplayMode.SmallIconWithText ? 12 : 14,
|
||||
Width = displayMode == LauncherDisplayMode.SmallIconWithText ? 64 : 74,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
}
|
||||
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
FontSize = displayMode == LauncherDisplayMode.CompactList ? 12 : 14,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private LauncherDisplayMode GetEffectiveDisplayMode(LauncherItem item)
|
||||
{
|
||||
return item.DisplayMode;
|
||||
}
|
||||
|
||||
private static string GetLabel(LauncherItem item)
|
||||
{
|
||||
return item.Type switch
|
||||
{
|
||||
LauncherItemType.Folder => "[Folder]",
|
||||
LauncherItemType.Website => "[Web]",
|
||||
LauncherItemType.File => "[File]",
|
||||
_ => "[App]"
|
||||
};
|
||||
}
|
||||
|
||||
private void Settings_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
showSettings();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user