47 Commits
v1.0.0 ... main

Author SHA1 Message Date
Ray Berezowski
5d18022cde Remove dev-only notes from public release 2026-09-04 14:53:19 -04:00
Ray Berezowski
2525499c0f Merge dev for v1.0.2 2026-09-04 14:53:08 -04:00
Ray Berezowski
e956760a0d Prepare v1.0.2 release 2026-09-04 14:52:59 -04:00
Ray Berezowski
b96347843c Polish settings item editor layout 2026-08-26 22:48:29 -04:00
Ray Berezowski
0baf87d979 Add launcher item notes 2026-08-26 22:43:58 -04:00
Ray Berezowski
57b2c4b0cc Add working folder and minimized launch options 2026-08-26 22:37:09 -04:00
Ray Berezowski
c5fd18e6d3 Fix admin launch and settings visibility 2026-08-26 22:30:00 -04:00
Ray Berezowski
1601da8155 Add run as administrator option 2026-08-26 22:10:55 -04:00
Ray Berezowski
6d9bbbab02 Add launcher item tooltips 2026-08-26 22:04:26 -04:00
Ray Berezowski
5421e3dde3 Restore menu keyboard navigation 2026-08-26 21:56:55 -04:00
Ray Berezowski
004a7e7c89 Polish launcher keyboard navigation 2026-08-26 21:49:23 -04:00
Ray Berezowski
f2ccca2ed1 Improve launcher search results 2026-08-26 21:45:57 -04:00
Ray Berezowski
c606573900 Add menu sort action 2026-08-26 21:41:45 -04:00
Ray Berezowski
86f667f595 Add selected item import export 2026-08-26 21:38:57 -04:00
Ray Berezowski
8ec53ff9d6 Move target validation to top of config menu 2026-08-26 21:34:16 -04:00
Ray Berezowski
691cb405fd Add target validation action 2026-08-26 21:30:13 -04:00
Ray Berezowski
992ac9a7b5 Add open containing folder action 2026-08-26 21:24:25 -04:00
Ray Berezowski
31c62bb89a Add disabled launcher items 2026-08-26 21:06:25 -04:00
Ray Berezowski
fe86573e10 Add popup pin toggle 2026-08-26 21:01:05 -04:00
Ray Berezowski
9f1b012d15 Add pinned launcher items 2026-08-26 20:41:20 -04:00
Ray Berezowski
749a856faa Add recent launcher items 2026-08-26 19:58:03 -04:00
Ray Berezowski
32f08bc0fd Add launcher edit action 2026-08-26 19:49:49 -04:00
Ray Berezowski
362cd94138 Add duplicate item action 2026-08-26 19:24:08 -04:00
Ray Berezowski
5aacdbb22c Add drag and drop item creation 2026-08-26 18:50:46 -04:00
Ray Berezowski
e8525e8f55 Bump build number to 1.0.1.1 2026-08-26 10:19:42 -04:00
Ray Berezowski
f8d0ca9dd7 Use sharp PNG header logo 2026-08-25 21:20:57 -04:00
Ray Berezowski
780f3fa038 Enlarge header app icon 2026-08-25 18:23:23 -04:00
Ray Berezowski
6b758d4c95 Use app icon in launcher headers 2026-08-25 17:52:51 -04:00
Ray Berezowski
ad68fade24 Tune color scheme palettes 2026-08-25 17:44:48 -04:00
Ray Berezowski
c5f4fe2d0e Add selectable color schemes 2026-08-25 17:31:54 -04:00
Ray Berezowski
8760d96310 Polish launcher visual design 2026-08-25 17:22:26 -04:00
Ray Berezowski
f3955b0ffc Add open all menu action 2026-06-03 17:17:29 -04:00
Ray Berezowski
15a7f81a28 Track future launcher ideas 2026-06-03 17:12:27 -04:00
Ray Berezowski
832f7f3012 Add launcher popup header icon 2026-06-02 22:15:37 -04:00
Ray Berezowski
3bd58af9d9 Theme config context menu 2026-06-02 22:12:43 -04:00
Ray Berezowski
28689bbd16 Avoid overwriting item display mode fallback 2026-06-02 22:06:30 -04:00
Ray Berezowski
1511aae219 Consolidate config actions into menu 2026-06-02 22:00:35 -04:00
Ray Berezowski
84e89ec11c Persist selected item display mode changes 2026-06-02 21:52:42 -04:00
Ray Berezowski
fb9258d339 Improve hotkey capture and validation 2026-06-02 21:47:00 -04:00
Ray Berezowski
8211200fcc Open import config from backups folder 2026-06-02 21:38:10 -04:00
Ray Berezowski
b0fc1b4679 Add config import and export 2026-06-02 21:28:59 -04:00
Ray Berezowski
835ce2159b Merge branch 'main' into dev 2026-06-02 21:19:56 -04:00
Ray Berezowski
44e9517453 Bump version to 1.0.1 2026-06-02 21:18:39 -04:00
Ray Berezowski
c35d687751 Add dev-only testing notes 2026-06-02 21:12:43 -04:00
Ray Berezowski
95f79ae6d0 Use cache-busting exact screenshot names 2026-06-02 21:07:00 -04:00
Ray Berezowski
6d5b9bc2a8 Tighten README screenshot crops 2026-06-02 21:02:27 -04:00
Ray Berezowski
d40f1f2935 Add README screenshots 2026-06-02 20:59:34 -04:00
22 changed files with 2806 additions and 223 deletions

View File

@@ -2,8 +2,8 @@
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"
Title="About Taskbar Launcher" Title="About Taskbar Launcher"
Height="410" Height="430"
Width="560" Width="580"
ResizeMode="NoResize" ResizeMode="NoResize"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Background="{DynamicResource AppWindowBrush}" Background="{DynamicResource AppWindowBrush}"
@@ -15,6 +15,11 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Background="{DynamicResource AppPanelBrush}"
BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"
CornerRadius="16"
Padding="16">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="96"/> <ColumnDefinition Width="96"/>
@@ -25,7 +30,8 @@
Height="76" Height="76"
BorderThickness="1" BorderThickness="1"
BorderBrush="{DynamicResource AppAccentBrush}" BorderBrush="{DynamicResource AppAccentBrush}"
Background="{DynamicResource AppSurfaceBrush}" Background="{DynamicResource AppOrangeSoftBrush}"
CornerRadius="14"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Top"> VerticalAlignment="Top">
<Image Source="pack://application:,,,/Assets/AboutLogo.png" <Image Source="pack://application:,,,/Assets/AboutLogo.png"
@@ -42,13 +48,15 @@
TextWrapping="Wrap"/> TextWrapping="Wrap"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border>
<Border Grid.Row="1" <Border Grid.Row="1"
Margin="0,22,0,18" Margin="0,22,0,18"
Padding="14" Padding="14"
BorderThickness="1" BorderThickness="1"
BorderBrush="{DynamicResource AppBorderBrush}" BorderBrush="{DynamicResource AppBorderBrush}"
Background="{DynamicResource AppSurfaceBrush}"> Background="{DynamicResource AppSurfaceBrush}"
CornerRadius="14">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/> <ColumnDefinition Width="120"/>
@@ -89,6 +97,7 @@
<Button Grid.Row="2" <Button Grid.Row="2"
Content="Close" Content="Close"
Click="Close_Click" Click="Close_Click"
Padding="18,8"
HorizontalAlignment="Right"/> HorizontalAlignment="Right"/>
</Grid> </Grid>
</Window> </Window>

View File

@@ -14,7 +14,7 @@ public partial class AboutWindow : Window
AuthorText.Text = AppInfo.Author; AuthorText.Text = AppInfo.Author;
VersionText.Text = AppInfo.Version; VersionText.Text = AppInfo.Version;
BuildText.Text = AppInfo.Build; BuildText.Text = AppInfo.Build;
ConfigPathText.Text = ConfigService.ConfigPath; ConfigPathText.Text = "config.json beside TaskbarLauncher.exe";
if (string.IsNullOrWhiteSpace(AppInfo.GiveawaySiteUrl)) if (string.IsNullOrWhiteSpace(AppInfo.GiveawaySiteUrl))
{ {

164
App.xaml
View File

@@ -5,8 +5,10 @@
<Application.Resources> <Application.Resources>
<Style TargetType="Button"> <Style TargetType="Button">
<Setter Property="Margin" Value="0,2"/> <Setter Property="Margin" Value="0,2"/>
<Setter Property="Padding" Value="10,8"/> <Setter Property="Padding" Value="12,8"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/> <Setter Property="MinHeight" Value="34"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Background" Value="{DynamicResource AppControlBrush}"/> <Setter Property="Background" Value="{DynamicResource AppControlBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
@@ -17,6 +19,7 @@
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1" BorderThickness="1"
CornerRadius="10"
Padding="{TemplateBinding Padding}"> Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
@@ -24,11 +27,11 @@
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ButtonBorder" Property="Background" Value="{DynamicResource AppControlBrush}"/> <Setter TargetName="ButtonBorder" Property="Background" Value="{DynamicResource AppControlHoverBrush}"/>
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/> <Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
</Trigger> </Trigger>
<Trigger Property="IsPressed" Value="True"> <Trigger Property="IsPressed" Value="True">
<Setter TargetName="ButtonBorder" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/> <Setter TargetName="ButtonBorder" Property="Background" Value="{DynamicResource AppAccentSoftBrush}"/>
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/> <Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
</Trigger> </Trigger>
<Trigger Property="IsEnabled" Value="False"> <Trigger Property="IsEnabled" Value="False">
@@ -44,13 +47,42 @@
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
<Setter Property="CaretBrush" Value="{DynamicResource AppAccentBrush}"/> <Setter Property="CaretBrush" Value="{DynamicResource AppAccentBrush}"/>
<Setter Property="Padding" Value="9,6"/>
<Setter Property="MinHeight" Value="34"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="TextBoxBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
CornerRadius="10">
<ScrollViewer x:Name="PART_ContentHost"
Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="TextBoxBorder" Property="Background" Value="{DynamicResource AppPanelBrush}"/>
<Setter TargetName="TextBoxBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="TextBoxBorder" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
<Setter TargetName="TextBoxBorder" 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>
<Style TargetType="ComboBox"> <Style TargetType="ComboBox">
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/> <Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
<Setter Property="Padding" Value="6,4"/> <Setter Property="Padding" Value="9,6"/>
<Setter Property="MinHeight" Value="28"/> <Setter Property="MinHeight" Value="34"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="ComboBox"> <ControlTemplate TargetType="ComboBox">
@@ -66,20 +98,21 @@
<Border x:Name="ComboToggleBorder" <Border x:Name="ComboToggleBorder"
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"> BorderThickness="1"
CornerRadius="10">
<ContentPresenter/> <ContentPresenter/>
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ComboToggleBorder" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/> <Setter TargetName="ComboToggleBorder" Property="Background" Value="{DynamicResource AppPanelBrush}"/>
<Setter TargetName="ComboToggleBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/> <Setter TargetName="ComboToggleBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
</Trigger> </Trigger>
<Trigger Property="IsPressed" Value="True"> <Trigger Property="IsPressed" Value="True">
<Setter TargetName="ComboToggleBorder" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/> <Setter TargetName="ComboToggleBorder" Property="Background" Value="{DynamicResource AppAccentSoftBrush}"/>
<Setter TargetName="ComboToggleBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/> <Setter TargetName="ComboToggleBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
</Trigger> </Trigger>
<Trigger Property="IsChecked" Value="True"> <Trigger Property="IsChecked" Value="True">
<Setter TargetName="ComboToggleBorder" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/> <Setter TargetName="ComboToggleBorder" Property="Background" Value="{DynamicResource AppAccentSoftBrush}"/>
<Setter TargetName="ComboToggleBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/> <Setter TargetName="ComboToggleBorder" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
</Trigger> </Trigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
@@ -114,6 +147,8 @@
<Border Background="{DynamicResource AppSurfaceBrush}" <Border Background="{DynamicResource AppSurfaceBrush}"
BorderBrush="{DynamicResource AppBorderBrush}" BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1" BorderThickness="1"
CornerRadius="10"
Padding="3"
MinWidth="{TemplateBinding ActualWidth}" MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"> MaxHeight="{TemplateBinding MaxDropDownHeight}">
<ScrollViewer> <ScrollViewer>
@@ -127,11 +162,11 @@
<Setter Property="Opacity" Value="0.55"/> <Setter Property="Opacity" Value="0.55"/>
</Trigger> </Trigger>
<Trigger SourceName="ToggleButton" Property="IsMouseOver" Value="True"> <Trigger SourceName="ToggleButton" Property="IsMouseOver" Value="True">
<Setter TargetName="ToggleButton" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/> <Setter TargetName="ToggleButton" Property="Background" Value="{DynamicResource AppPanelBrush}"/>
<Setter TargetName="ToggleButton" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/> <Setter TargetName="ToggleButton" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
</Trigger> </Trigger>
<Trigger SourceName="ToggleButton" Property="IsPressed" Value="True"> <Trigger SourceName="ToggleButton" Property="IsPressed" Value="True">
<Setter TargetName="ToggleButton" Property="Background" Value="{DynamicResource AppSurfaceBrush}"/> <Setter TargetName="ToggleButton" Property="Background" Value="{DynamicResource AppAccentSoftBrush}"/>
<Setter TargetName="ToggleButton" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/> <Setter TargetName="ToggleButton" Property="BorderBrush" Value="{DynamicResource AppAccentBrush}"/>
</Trigger> </Trigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
@@ -142,21 +177,24 @@
<Style TargetType="ComboBoxItem"> <Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/> <Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
<Setter Property="Padding" Value="6,4"/> <Setter Property="Padding" Value="9,7"/>
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsHighlighted" Value="True"> <Trigger Property="IsHighlighted" Value="True">
<Setter Property="Background" Value="{DynamicResource AppAccentBrush}"/> <Setter Property="Background" Value="{DynamicResource AppAccentSoftBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AppSelectedTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
</Trigger> </Trigger>
<Trigger Property="IsSelected" Value="True"> <Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource AppAccentBrush}"/> <Setter Property="Background" Value="{DynamicResource AppAccentSoftBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AppSelectedTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
</Trigger> </Trigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<Style TargetType="TreeView"> <Style TargetType="TreeView">
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/> <Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8"/>
</Style> </Style>
<Style TargetType="TreeViewItem"> <Style TargetType="TreeViewItem">
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
@@ -168,12 +206,104 @@
</Trigger> </Trigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<Style TargetType="ContextMenu">
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContextMenu">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10"
Padding="{TemplateBinding Padding}">
<ItemsPresenter/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="MenuItem">
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
<Setter Property="Padding" Value="12,8"/>
<Setter Property="MinWidth" Value="190"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border x:Name="MenuItemBorder"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
CornerRadius="8">
<ContentPresenter ContentSource="Header"
RecognizesAccessKey="True"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="MenuItemBorder" Property="Background" Value="{DynamicResource AppAccentSoftBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.55"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="Separator">
<Setter Property="Margin" Value="6,4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Separator">
<Border Height="1" Background="{DynamicResource AppBorderBrush}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="GroupBox"> <Style TargetType="GroupBox">
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
<Setter Property="Padding" Value="12"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupBox">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="1"
Margin="0,8,0,0"
Background="{DynamicResource AppSurfaceBrush}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
CornerRadius="14"
Padding="{TemplateBinding Padding}">
<ContentPresenter/>
</Border>
<Border Grid.Row="0"
HorizontalAlignment="Left"
Background="{DynamicResource AppOrangeSoftBrush}"
BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"
CornerRadius="999"
Padding="10,4">
<ContentPresenter ContentSource="Header"
RecognizesAccessKey="True"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style> </Style>
<Style TargetType="CheckBox"> <Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style> </Style>
<Style TargetType="Expander"> <Style TargetType="Expander">
<Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource AppTextBrush}"/>

View File

@@ -10,9 +10,21 @@ public partial class App : System.Windows.Application
{ {
base.OnStartup(e); base.OnStartup(e);
ThemeService.Apply();
controller = new LauncherController(); controller = new LauncherController();
controller.Start(); controller.Start();
if (e.Args.Contains("--show-settings", StringComparer.OrdinalIgnoreCase))
{
controller.ShowSettings();
}
else if (e.Args.Contains("--show-about", StringComparer.OrdinalIgnoreCase))
{
controller.ShowAbout();
}
else if (e.Args.Contains("--show-launcher", StringComparer.OrdinalIgnoreCase))
{
controller.TogglePopup();
}
} }
protected override void OnExit(System.Windows.ExitEventArgs e) protected override void OnExit(System.Windows.ExitEventArgs e)

BIN
Assets/HeaderLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -4,30 +4,101 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" mc:Ignorable="d"
Title="Taskbar Launcher Settings" Height="700" Width="940" Title="Taskbar Launcher Settings" Height="740" Width="980"
MinHeight="620" MinWidth="820" MinHeight="660" MinWidth="860"
Background="{DynamicResource AppWindowBrush}" Background="{DynamicResource AppWindowBrush}"
Foreground="{DynamicResource AppTextBrush}"> Foreground="{DynamicResource AppTextBrush}">
<Window.Resources>
<Style x:Key="SettingsSectionBorder" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource AppPanelBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="12"/>
<Setter Property="Margin" Value="0,0,0,12"/>
</Style>
<Style x:Key="SettingsSectionTitle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
<Style x:Key="SettingsFieldLabel" TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{DynamicResource AppMutedTextBrush}"/>
<Setter Property="Margin" Value="0,0,8,8"/>
</Style>
</Window.Resources>
<DockPanel Margin="18"> <DockPanel Margin="18">
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,14"> <Border DockPanel.Dock="Top"
<TextBlock Text="Taskbar Launcher" FontSize="22" FontWeight="SemiBold"/> Margin="0,0,0,16"
Padding="16,14"
Background="{DynamicResource AppPanelBrush}"
BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"
CornerRadius="16">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Width="56"
Height="56"
Margin="0,0,12,0"
Background="{DynamicResource AppOrangeSoftBrush}"
BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"
CornerRadius="14">
<Image Source="pack://application:,,,/Assets/HeaderLogo.png"
Width="48"
Height="48"
Stretch="Uniform"/>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="Taskbar Launcher" FontSize="23" FontWeight="SemiBold"/>
<TextBlock x:Name="VersionText" <TextBlock x:Name="VersionText"
Margin="0,2,0,0"
Foreground="{DynamicResource AppMutedTextBrush}"/> Foreground="{DynamicResource AppMutedTextBrush}"/>
<TextBlock Text="Portable launcher settings are stored beside the app in config.json." <TextBlock Text="Portable launcher settings are stored beside the app in config.json."
Margin="0,2,0,0"
Foreground="{DynamicResource AppMutedTextBrush}"/> Foreground="{DynamicResource AppMutedTextBrush}"/>
</StackPanel> </StackPanel>
</Grid>
</Border>
<Border DockPanel.Dock="Bottom" BorderThickness="1,0,0,0" Padding="0,12,0,0" <Border DockPanel.Dock="Bottom"
BorderBrush="{DynamicResource AppBorderBrush}"> Margin="0,16,0,0"
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> Padding="12"
Background="{DynamicResource AppPanelBrush}"
BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"
CornerRadius="14">
<WrapPanel HorizontalAlignment="Right">
<Button Content="Save" Click="Save_Click" Margin="0,0,8,0"/> <Button Content="Save" Click="Save_Click" Margin="0,0,8,0"/>
<Button Content="Backup Config" Click="BackupConfig_Click" Margin="0,0,8,0"/> <Button x:Name="ConfigMenuButton"
<Button Content="Restore Config" Click="RestoreConfig_Click" Margin="0,0,8,0"/> Content="Config..."
<Button Content="Reload Config" Click="ReloadConfig_Click" Margin="0,0,8,0"/> Click="ConfigMenuButton_Click"
<Button Content="Open Config Folder" Click="OpenConfigFolder_Click" Margin="0,0,8,0"/> Margin="0,0,8,0">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Validate Targets" Click="ValidateTargets_Click"/>
<Separator/>
<MenuItem Header="Backup Config" Click="BackupConfig_Click"/>
<MenuItem Header="Restore from Backup" Click="RestoreConfig_Click"/>
<Separator/>
<MenuItem Header="Export Config" Click="ExportConfig_Click"/>
<MenuItem Header="Import Config" Click="ImportConfig_Click"/>
<MenuItem Header="Export Selected Item" Click="ExportSelectedItem_Click"/>
<MenuItem Header="Import Item" Click="ImportItem_Click"/>
<Separator/>
<MenuItem Header="Reset to Defaults" Click="ResetConfig_Click"/>
<MenuItem Header="Reload Config" Click="ReloadConfig_Click"/>
<MenuItem Header="Open Config Folder" Click="OpenConfigFolder_Click"/>
</ContextMenu>
</Button.ContextMenu>
</Button>
<Button Content="About" Click="About_Click" Margin="0,0,8,0"/> <Button Content="About" Click="About_Click" Margin="0,0,8,0"/>
<Button Content="Close" Click="Close_Click"/> <Button Content="Close" Click="Close_Click"/>
</StackPanel> </WrapPanel>
</Border> </Border>
<Grid> <Grid>
@@ -36,9 +107,9 @@
<ColumnDefinition Width="3*"/> <ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<GroupBox Header="Launcher Items" Margin="0,0,12,0"> <GroupBox Header="Launcher Items" Margin="0,0,14,0">
<DockPanel Margin="8"> <DockPanel>
<UniformGrid DockPanel.Dock="Bottom" Columns="2" Margin="0,8,0,0"> <UniformGrid DockPanel.Dock="Bottom" Columns="2" Margin="0,10,0,0">
<Button Content="Add Root" Click="AddRoot_Click"/> <Button Content="Add Root" Click="AddRoot_Click"/>
<Button Content="Add Top Menu" Click="AddTopMenu_Click"/> <Button Content="Add Top Menu" Click="AddTopMenu_Click"/>
<Button Content="Add Child" Click="AddChild_Click"/> <Button Content="Add Child" Click="AddChild_Click"/>
@@ -51,6 +122,7 @@
</UniformGrid> </UniformGrid>
<TreeView x:Name="ItemsTree" <TreeView x:Name="ItemsTree"
AllowDrop="True" AllowDrop="True"
Margin="0"
SelectedItemChanged="ItemsTree_SelectedItemChanged" SelectedItemChanged="ItemsTree_SelectedItemChanged"
PreviewMouseMove="ItemsTree_PreviewMouseMove" PreviewMouseMove="ItemsTree_PreviewMouseMove"
DragOver="ItemsTree_DragOver" DragOver="ItemsTree_DragOver"
@@ -59,14 +131,19 @@
</GroupBox> </GroupBox>
<GroupBox Header="Settings" Grid.Column="1"> <GroupBox Header="Settings" Grid.Column="1">
<Grid Margin="12"> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="16"/> <RowDefinition Height="16"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Background="{DynamicResource AppPanelBrush}"
BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/> <ColumnDefinition Width="110"/>
@@ -76,25 +153,38 @@
<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> </Grid.RowDefinitions>
<TextBlock Text="Hotkey" VerticalAlignment="Center" Margin="0,0,8,8"/> <TextBlock Text="Hotkey" VerticalAlignment="Center" Margin="0,0,8,8"/>
<TextBox x:Name="HotkeyBox" Grid.Column="1" Margin="0,0,0,8"/> <TextBox x:Name="HotkeyBox" Grid.Column="1" Margin="0,0,0,8"
PreviewKeyDown="HotkeyBox_PreviewKeyDown"/>
<TextBlock Text="Default for New Items" Grid.Row="1" VerticalAlignment="Center" Margin="0,0,8,0"/> <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"/> <ComboBox x:Name="DisplayModeBox" Grid.Row="1" Grid.Column="1" Margin="0,0,0,8"/>
<TextBlock Text="Startup" Grid.Row="2" VerticalAlignment="Center" Margin="0,8,8,0"/> <TextBlock Text="Color Scheme" Grid.Row="2" VerticalAlignment="Center" Margin="0,0,8,8"/>
<CheckBox x:Name="StartWithWindowsBox" Grid.Row="2" Grid.Column="1" <ComboBox x:Name="ThemeSchemeBox" Grid.Row="2" Grid.Column="1" Margin="0,0,0,8"
SelectionChanged="ThemeSchemeBox_SelectionChanged"/>
<TextBlock Text="Startup" Grid.Row="3" VerticalAlignment="Center" Margin="0,8,8,0"/>
<CheckBox x:Name="StartWithWindowsBox" Grid.Row="3" Grid.Column="1"
Content="Start with Windows" Content="Start with Windows"
Margin="0,8,0,0" Margin="0,8,0,0"
Checked="StartWithWindowsBox_Changed" Checked="StartWithWindowsBox_Changed"
Unchecked="StartWithWindowsBox_Changed"/> Unchecked="StartWithWindowsBox_Changed"/>
</Grid> </Grid>
</Border>
<Separator Grid.Row="1" VerticalAlignment="Center"/> <Separator Grid.Row="1" VerticalAlignment="Center"/>
<Grid Grid.Row="2"> <ScrollViewer Grid.Row="2"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<Border Style="{StaticResource SettingsSectionBorder}">
<Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/> <ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@@ -106,48 +196,120 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Item" Grid.ColumnSpan="3" Style="{StaticResource SettingsSectionTitle}"/>
<TextBlock Text="Title" Grid.Row="1" Style="{StaticResource SettingsFieldLabel}"/>
<TextBox x:Name="TitleBox" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,0,0,8"
TextChanged="PreviewField_Changed"/>
<TextBlock Text="Type" Grid.Row="2" Style="{StaticResource SettingsFieldLabel}"/>
<ComboBox x:Name="TypeBox" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,0,0,8"
SelectionChanged="TypeBox_SelectionChanged"/>
<TextBlock Text="Target" Grid.Row="3" Style="{StaticResource SettingsFieldLabel}"/>
<TextBox x:Name="TargetBox" Grid.Row="3" Grid.Column="1" Margin="0,0,8,8"
TextChanged="PreviewField_Changed"/>
<StackPanel Grid.Row="3" 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="Notes" Grid.Row="4" VerticalAlignment="Top" Foreground="{DynamicResource AppMutedTextBrush}" Margin="0,4,8,0"/>
<TextBox x:Name="NotesBox"
Grid.Row="4"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0"
AcceptsReturn="True"
MinHeight="58"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
TextChanged="PreviewField_Changed"/>
</Grid>
</Border>
<Border Style="{StaticResource SettingsSectionBorder}">
<Grid>
<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"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Text="Title" VerticalAlignment="Center" Margin="0,0,8,8"/> <TextBlock Text="Launch" Grid.ColumnSpan="3" Style="{StaticResource SettingsSectionTitle}"/>
<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"/> <TextBlock Text="Arguments" Grid.Row="1" Style="{StaticResource SettingsFieldLabel}"/>
<ComboBox x:Name="TypeBox" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,0,0,8" <TextBox x:Name="ArgumentsBox" 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"/> <TextBlock Text="Working Folder" Grid.Row="2" Style="{StaticResource SettingsFieldLabel}"/>
<TextBox x:Name="TargetBox" Grid.Row="2" Grid.Column="1" Margin="0,0,8,8" <TextBox x:Name="WorkingFolderBox" Grid.Row="2" Grid.Column="1" Margin="0,0,8,8"/>
TextChanged="PreviewField_Changed"/> <Button x:Name="WorkingFolderBrowseButton"
<StackPanel Grid.Row="2" Grid.Column="2" Orientation="Horizontal" Margin="0,0,0,8"> Grid.Row="2" Grid.Column="2"
<Button Content="File" Click="BrowseFile_Click" Margin="0,0,6,0" Padding="8,4"/> Content="Folder"
<Button Content="Folder" Click="BrowseFolder_Click" Padding="8,4"/> Click="BrowseWorkingFolder_Click"
Padding="8,4"
Margin="0,0,0,8"/>
<TextBlock Text="Options" Grid.Row="3" VerticalAlignment="Top" Foreground="{DynamicResource AppMutedTextBrush}" Margin="0,2,8,0"/>
<StackPanel Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2">
<CheckBox x:Name="RunAsAdminBox"
Content="Run as administrator"
Margin="0,0,0,6"
Checked="LaunchOptionBox_Changed"
Unchecked="LaunchOptionBox_Changed"/>
<CheckBox x:Name="StartMinimizedBox"
Content="Start minimized"
Checked="LaunchOptionBox_Changed"
Unchecked="LaunchOptionBox_Changed"/>
</StackPanel> </StackPanel>
</Grid>
</Border>
<TextBlock Text="Arguments" Grid.Row="3" VerticalAlignment="Center" Margin="0,0,8,8"/> <Border Style="{StaticResource SettingsSectionBorder}">
<TextBox x:Name="ArgumentsBox" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,0,0,8"/> <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Icon Path" Grid.Row="4" VerticalAlignment="Center" Margin="0,0,8,0"/> <TextBlock Text="Appearance" Grid.ColumnSpan="3" Style="{StaticResource SettingsSectionTitle}"/>
<TextBox x:Name="IconPathBox" Grid.Row="4" Grid.Column="1" Margin="0,0,8,8"
<TextBlock Text="Icon Path" Grid.Row="1" Style="{StaticResource SettingsFieldLabel}"/>
<TextBox x:Name="IconPathBox" Grid.Row="1" Grid.Column="1" Margin="0,0,8,8"
TextChanged="PreviewField_Changed"/> TextChanged="PreviewField_Changed"/>
<StackPanel Grid.Row="4" Grid.Column="2" Orientation="Horizontal" Margin="0,0,0,8"> <StackPanel Grid.Row="1" 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="Icon" Click="BrowseIcon_Click" Padding="8,4" Margin="0,0,6,0"/>
<Button Content="Reset" Click="ResetIcon_Click" Padding="8,4"/> <Button Content="Reset" Click="ResetIcon_Click" Padding="8,4"/>
</StackPanel> </StackPanel>
<TextBlock Text="Selected Item Display" Grid.Row="5" VerticalAlignment="Center" Margin="0,0,8,8"/> <TextBlock Text="Display" Grid.Row="2" Style="{StaticResource SettingsFieldLabel}"/>
<ComboBox x:Name="ItemDisplayModeBox" Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" <ComboBox x:Name="ItemDisplayModeBox" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
Margin="0,0,0,8" SelectionChanged="PreviewField_Changed"/> Margin="0" SelectionChanged="ItemDisplayModeBox_SelectionChanged"/>
</Grid>
</Border>
<TextBlock Text="Preview" Grid.Row="6" VerticalAlignment="Top" Margin="0,4,8,0"/> <Border Style="{StaticResource SettingsSectionBorder}" Margin="0">
<Border Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="2" <StackPanel>
BorderThickness="1" <TextBlock Text="Preview" Style="{StaticResource SettingsSectionTitle}"/>
<Border BorderThickness="1"
BorderBrush="{DynamicResource AppBorderBrush}" BorderBrush="{DynamicResource AppBorderBrush}"
Background="{DynamicResource AppControlBrush}" Background="{DynamicResource AppSurfaceBrush}"
Padding="8" MinHeight="72"> CornerRadius="12"
Padding="10" MinHeight="76">
<StackPanel> <StackPanel>
<ContentControl x:Name="PreviewHost"/> <ContentControl x:Name="PreviewHost"/>
<TextBlock x:Name="PreviewNote" Margin="0,8,0,0" <TextBlock x:Name="PreviewNote" Margin="0,8,0,0"
@@ -155,9 +317,12 @@
TextWrapping="Wrap"/> TextWrapping="Wrap"/>
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<TextBlock x:Name="StatusText" Grid.Row="3" Margin="0,14,0,0" <TextBlock x:Name="StatusText" Grid.Row="3" Margin="4,14,4,0"
Foreground="{DynamicResource AppMutedTextBrush}" Foreground="{DynamicResource AppMutedTextBrush}"
TextWrapping="Wrap"/> TextWrapping="Wrap"/>
</Grid> </Grid>

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,10 @@ public sealed class LauncherConfig
public LauncherDisplayMode DisplayMode { get; set; } = LauncherDisplayMode.CompactList; public LauncherDisplayMode DisplayMode { get; set; } = LauncherDisplayMode.CompactList;
public LauncherThemeScheme ThemeScheme { get; set; } = LauncherThemeScheme.MarkHaven;
public List<LauncherItem> RecentItems { get; set; } = [];
public List<LauncherItem> Items { get; set; } = []; public List<LauncherItem> Items { get; set; } = [];
} }
@@ -19,8 +23,20 @@ public sealed class LauncherItem
public string? Arguments { get; set; } public string? Arguments { get; set; }
public string? WorkingFolder { get; set; }
public string? Notes { get; set; }
public string? IconPath { get; set; } public string? IconPath { get; set; }
public bool IsFavorite { get; set; }
public bool IsDisabled { get; set; }
public bool RunAsAdmin { get; set; }
public bool StartMinimized { get; set; }
public LauncherDisplayMode DisplayMode { get; set; } = LauncherDisplayMode.CompactList; public LauncherDisplayMode DisplayMode { get; set; } = LauncherDisplayMode.CompactList;
public List<LauncherItem> Children { get; set; } = []; public List<LauncherItem> Children { get; set; } = [];
@@ -42,3 +58,12 @@ public enum LauncherDisplayMode
LargeIconWithText, LargeIconWithText,
CompactList CompactList
} }
public enum LauncherThemeScheme
{
MarkHaven,
MGETBlueOrange,
MGETOrange,
System,
WindowsClassic
}

View File

@@ -8,12 +8,29 @@ Download the compiled Windows release from:
[Taskbar Launcher Releases](https://git.mget.ca/MGET/Taskbar-Launcher/releases) [Taskbar Launcher Releases](https://git.mget.ca/MGET/Taskbar-Launcher/releases)
Current stable release: `v1.0.2`
## Features ## Features
- System tray launcher - System tray launcher
- Global hotkey, default `Ctrl+Alt+Space` - Global hotkey, default `Ctrl+Alt+Space`
- Search/filter in the launcher popup - Search/filter in the launcher popup with parent menu context
- Nested menus and drag/drop ordering in Settings - Keyboard navigation in the launcher popup
- Tooltips for launcher items, menu paths, targets, and arguments
- Recent items section for quickly relaunching entries
- Nested menus, drag/drop ordering, and drag/drop item creation in Settings
- Right-click launcher entries to edit them in Settings
- Right-click launcher entries to pin or unpin them from the top
- Right-click launcher entries or Settings items to temporarily disable them
- Right-click app/file entries to open their containing folder
- Optional run-as-administrator mode for app and file entries
- Optional working folder and start-minimized launch options for app and file entries
- Per-item notes, visible in launcher tooltips and searchable from the popup
- Validate missing app, file, and folder targets from Settings
- Export and import selected items or menu sections
- Sort a selected menu or item level alphabetically
- Right-click item duplication in Settings
- Right-click items in Settings to pin them to the top of their menu
- Per-item icon and display mode - Per-item icon and display mode
- Automatic icon selection from app/folder targets - Automatic icon selection from app/folder targets
- Portable `config.json` - Portable `config.json`
@@ -21,6 +38,24 @@ Download the compiled Windows release from:
- Optional Start with Windows shortcut - Optional Start with Windows shortcut
- Windows light/dark theme support - Windows light/dark theme support
## Latest Stable
`v1.0.2` adds the newer launcher management and polish work from the development builds, including recent items, pinned items, drag/drop item creation, item duplication, item notes, target validation, selected item import/export, run-as-administrator support, working folders, start-minimized launch options, launcher tooltips, and the refreshed Settings layout.
## Screenshots
### Launcher Popup
![Taskbar Launcher popup](docs/screenshots/launcher-popup-exact.png)
### Settings
![Taskbar Launcher settings](docs/screenshots/settings-window-exact.png)
### About
![Taskbar Launcher about window](docs/screenshots/about-window-exact.png)
## Project Layout ## Project Layout
```text ```text
@@ -59,5 +94,5 @@ Recommended release flow for maintainers:
1. Update `<Version>` in `TaskbarLauncher.csproj`. 1. Update `<Version>` in `TaskbarLauncher.csproj`.
2. Commit the change. 2. Commit the change.
3. Build a release package locally. 3. Build a release package locally.
4. Tag the release, for example `v1.0.1`. 4. Tag the release, for example `v1.0.2`.
5. Upload the release zip to Gitea Releases. 5. Upload the release zip to Gitea Releases.

View File

@@ -36,7 +36,7 @@ public static class ConfigService
string json = File.ReadAllText(ConfigPath); string json = File.ReadAllText(ConfigPath);
LauncherConfig config = JsonSerializer.Deserialize<LauncherConfig>(json, JsonOptions) ?? CreateDefaultConfig(); LauncherConfig config = JsonSerializer.Deserialize<LauncherConfig>(json, JsonOptions) ?? CreateDefaultConfig();
NormalizeDisplayModes(config); NormalizeConfig(config);
Save(config); Save(config);
return config; return config;
} }
@@ -61,18 +61,85 @@ public static class ConfigService
return backupPath; return backupPath;
} }
public static LauncherConfig Restore(string sourcePath) public static string ExportTo(string destinationPath, LauncherConfig config)
{
if (string.IsNullOrWhiteSpace(destinationPath))
{
throw new ArgumentException("Choose a destination path for the exported config.", nameof(destinationPath));
}
string? destinationFolder = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrWhiteSpace(destinationFolder))
{
Directory.CreateDirectory(destinationFolder);
}
NormalizeConfig(config);
string json = JsonSerializer.Serialize(config, JsonOptions);
File.WriteAllText(destinationPath, json);
return destinationPath;
}
public static string ExportItemTo(string destinationPath, LauncherItem item)
{
if (string.IsNullOrWhiteSpace(destinationPath))
{
throw new ArgumentException("Choose a destination path for the exported item.", nameof(destinationPath));
}
string? destinationFolder = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrWhiteSpace(destinationFolder))
{
Directory.CreateDirectory(destinationFolder);
}
NormalizeDisplayModes(item);
string json = JsonSerializer.Serialize(item, JsonOptions);
File.WriteAllText(destinationPath, json);
return destinationPath;
}
public static LauncherItem ImportItemFrom(string sourcePath)
{ {
if (!File.Exists(sourcePath)) if (!File.Exists(sourcePath))
{ {
throw new FileNotFoundException("The selected config file was not found.", sourcePath); throw new FileNotFoundException("The selected item file was not found.", sourcePath);
} }
string json = File.ReadAllText(sourcePath); string json = File.ReadAllText(sourcePath);
LauncherConfig config = JsonSerializer.Deserialize<LauncherConfig>(json, JsonOptions) LauncherItem item;
?? throw new InvalidDataException("The selected config file could not be read."); try
{
item = JsonSerializer.Deserialize<LauncherItem>(json, JsonOptions)
?? throw new InvalidDataException("The selected item file could not be read.");
}
catch (JsonException ex)
{
throw new InvalidDataException("The selected file is not a valid Taskbar Launcher item export.", ex);
}
NormalizeDisplayModes(config); ValidateItem(item);
NormalizeDisplayModes(item);
return item;
}
public static LauncherConfig ImportFrom(string sourcePath)
{
LauncherConfig config = ReadConfigFile(sourcePath);
Save(config);
return config;
}
public static LauncherConfig ResetToDefaults()
{
LauncherConfig config = CreateDefaultConfig();
Save(config);
return config;
}
public static LauncherConfig Restore(string sourcePath)
{
LauncherConfig config = ReadConfigFile(sourcePath);
Save(config); Save(config);
return config; return config;
} }
@@ -131,12 +198,97 @@ public static class ConfigService
] ]
}; };
NormalizeDisplayModes(config); NormalizeConfig(config);
return config; return config;
} }
private static void NormalizeDisplayModes(LauncherConfig config) private static LauncherConfig ReadConfigFile(string sourcePath)
{ {
if (!File.Exists(sourcePath))
{
throw new FileNotFoundException("The selected config file was not found.", sourcePath);
}
string json = File.ReadAllText(sourcePath);
LauncherConfig config;
try
{
config = JsonSerializer.Deserialize<LauncherConfig>(json, JsonOptions)
?? throw new InvalidDataException("The selected config file could not be read.");
}
catch (JsonException ex)
{
throw new InvalidDataException("The selected file is not a valid Taskbar Launcher config.", ex);
}
ValidateConfig(config);
NormalizeConfig(config);
return config;
}
private static void ValidateConfig(LauncherConfig config)
{
if (config.Items is null)
{
throw new InvalidDataException("The selected config is missing launcher items.");
}
if (config.RecentItems is null)
{
config.RecentItems = [];
}
foreach (LauncherItem item in config.Items)
{
ValidateItem(item);
}
foreach (LauncherItem item in config.RecentItems)
{
ValidateItem(item);
}
}
private static void ValidateItem(LauncherItem item)
{
if (item.Children is null)
{
throw new InvalidDataException($"The item '{item.Title}' has an invalid children list.");
}
if (!Enum.IsDefined(item.Type))
{
throw new InvalidDataException($"The item '{item.Title}' has an invalid type.");
}
foreach (LauncherItem child in item.Children)
{
ValidateItem(child);
}
}
private static void NormalizeConfig(LauncherConfig config)
{
if (!Enum.IsDefined(config.DisplayMode))
{
config.DisplayMode = LauncherDisplayMode.CompactList;
}
if (!Enum.IsDefined(config.ThemeScheme))
{
config.ThemeScheme = LauncherThemeScheme.MarkHaven;
}
config.RecentItems ??= [];
config.RecentItems.RemoveAll(item => item.Type == LauncherItemType.Menu || item.IsDisabled || string.IsNullOrWhiteSpace(item.Target));
foreach (LauncherItem item in config.RecentItems)
{
item.IsFavorite = false;
item.IsDisabled = false;
item.Children = [];
NormalizeDisplayModes(item);
}
foreach (LauncherItem item in config.Items) foreach (LauncherItem item in config.Items)
{ {
NormalizeDisplayModes(item); NormalizeDisplayModes(item);

View File

@@ -1,4 +1,5 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Interop; using System.Windows.Interop;
@@ -17,6 +18,7 @@ public sealed class HotkeyService : IDisposable
public void Register(string hotkey) public void Register(string hotkey)
{ {
ParseHotkey(hotkey, out uint modifiers, out uint key);
Unregister(); Unregister();
window = new Window window = new Window
@@ -37,8 +39,13 @@ public sealed class HotkeyService : IDisposable
source = HwndSource.FromHwnd(helper.Handle); source = HwndSource.FromHwnd(helper.Handle);
source?.AddHook(WndProc); source?.AddHook(WndProc);
ParseHotkey(hotkey, out uint modifiers, out uint key); if (!RegisterHotKey(helper.Handle, HotkeyId, modifiers, key))
RegisterHotKey(helper.Handle, HotkeyId, modifiers, key); {
int error = Marshal.GetLastWin32Error();
throw new InvalidOperationException(
$"Windows could not register hotkey '{Normalize(hotkey)}'. It may already be used by another app.",
new Win32Exception(error));
}
}; };
window.Show(); window.Show();
@@ -78,12 +85,47 @@ public sealed class HotkeyService : IDisposable
return IntPtr.Zero; return IntPtr.Zero;
} }
public static string Normalize(string hotkey)
{
ParseHotkey(hotkey, out uint modifiers, out uint key);
Key parsedKey = KeyInterop.KeyFromVirtualKey((int)key);
List<string> parts = [];
if ((modifiers & 0x0002) != 0)
{
parts.Add("Ctrl");
}
if ((modifiers & 0x0001) != 0)
{
parts.Add("Alt");
}
if ((modifiers & 0x0004) != 0)
{
parts.Add("Shift");
}
if ((modifiers & 0x0008) != 0)
{
parts.Add("Win");
}
parts.Add(parsedKey.ToString());
return string.Join("+", parts);
}
private static void ParseHotkey(string hotkey, out uint modifiers, out uint key) private static void ParseHotkey(string hotkey, out uint modifiers, out uint key)
{ {
modifiers = 0; modifiers = 0;
key = (uint)KeyInterop.VirtualKeyFromKey(Key.Space); key = 0;
foreach (string part in hotkey.Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) string[] parts = hotkey
.Replace(",", "+", StringComparison.Ordinal)
.Replace(" ", "+", StringComparison.Ordinal)
.Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
foreach (string part in parts)
{ {
if (part.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || part.Equals("Control", StringComparison.OrdinalIgnoreCase)) if (part.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || part.Equals("Control", StringComparison.OrdinalIgnoreCase))
{ {
@@ -101,13 +143,47 @@ public sealed class HotkeyService : IDisposable
{ {
modifiers |= 0x0008; modifiers |= 0x0008;
} }
else if (Enum.TryParse(part, true, out Key parsedKey)) else if (TryParseKey(part, out Key parsedKey))
{ {
key = (uint)KeyInterop.VirtualKeyFromKey(parsedKey); key = (uint)KeyInterop.VirtualKeyFromKey(parsedKey);
} }
else
{
throw new FormatException($"'{part}' is not a valid hotkey key.");
} }
} }
if (modifiers == 0)
{
throw new FormatException("Hotkeys must include at least one modifier: Ctrl, Alt, Shift, or Win.");
}
if (key == 0)
{
throw new FormatException("Hotkeys must include a final key, such as Space, S, F12, or D1.");
}
}
private static bool TryParseKey(string value, out Key key)
{
if (Enum.TryParse(value, true, out key))
{
return true;
}
if (value.Length == 1 && char.IsLetter(value[0]))
{
return Enum.TryParse(value.ToUpperInvariant(), out key);
}
if (value.Length == 1 && char.IsDigit(value[0]))
{
return Enum.TryParse($"D{value}", out key);
}
return false;
}
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);

View File

@@ -9,6 +9,8 @@ namespace TaskbarLauncher.Services;
public sealed class LauncherController : IDisposable public sealed class LauncherController : IDisposable
{ {
private const int MaxRecentItems = 8;
private readonly HotkeyService hotkeyService = new(); private readonly HotkeyService hotkeyService = new();
private NotifyIcon? notifyIcon; private NotifyIcon? notifyIcon;
private LauncherPopup? popup; private LauncherPopup? popup;
@@ -20,6 +22,7 @@ public sealed class LauncherController : IDisposable
public void Start() public void Start()
{ {
Config = ConfigService.Load(); Config = ConfigService.Load();
ThemeService.Apply(Config.ThemeScheme);
CreateTrayIcon(); CreateTrayIcon();
RegisterHotkey(); RegisterHotkey();
} }
@@ -27,6 +30,7 @@ public sealed class LauncherController : IDisposable
public void ReloadConfig() public void ReloadConfig()
{ {
Config = ConfigService.Load(); Config = ConfigService.Load();
ThemeService.Apply(Config.ThemeScheme);
RegisterHotkey(); RegisterHotkey();
popup?.Close(); popup?.Close();
popup = null; popup = null;
@@ -39,6 +43,12 @@ public sealed class LauncherController : IDisposable
settingsWindow.Activate(); settingsWindow.Activate();
} }
public void ShowSettings(LauncherItem itemToEdit)
{
settingsWindow ??= new MainWindow(this);
settingsWindow.SelectItemForEdit(itemToEdit);
}
public void ShowAbout() public void ShowAbout()
{ {
if (aboutWindow?.IsVisible == true) if (aboutWindow?.IsVisible == true)
@@ -62,7 +72,7 @@ public sealed class LauncherController : IDisposable
return; return;
} }
popup = new LauncherPopup(Config, LaunchItem, ShowSettings); popup = new LauncherPopup(Config, LaunchItem, OpenAllItems, ShowSettings, ShowSettings, OpenContainingFolderFromPopup);
popup.Closed += (_, _) => popup = null; popup.Closed += (_, _) => popup = null;
popup.ShowNearTaskbar(); popup.ShowNearTaskbar();
} }
@@ -126,22 +136,232 @@ public sealed class LauncherController : IDisposable
private void LaunchItem(LauncherItem item) private void LaunchItem(LauncherItem item)
{ {
if (item.Type == LauncherItemType.Menu || string.IsNullOrWhiteSpace(item.Target)) if (TryLaunchItem(item, out string? error))
{
RecordRecentItem(item);
popup?.Close();
return;
}
if (!string.IsNullOrWhiteSpace(error))
{
System.Windows.MessageBox.Show(error, "Taskbar Launcher");
}
}
private void RecordRecentItem(LauncherItem item)
{
if (item.Type == LauncherItemType.Menu || item.IsDisabled || string.IsNullOrWhiteSpace(item.Target))
{ {
return; return;
} }
string target = Environment.ExpandEnvironmentVariables(item.Target); Config.RecentItems.RemoveAll(recent => IsSameRecentItem(recent, item));
Config.RecentItems.Insert(0, CreateRecentItem(item));
if (Config.RecentItems.Count > MaxRecentItems)
{
Config.RecentItems.RemoveRange(MaxRecentItems, Config.RecentItems.Count - MaxRecentItems);
}
ConfigService.Save(Config);
}
private static bool IsSameRecentItem(LauncherItem recent, LauncherItem item)
{
return string.Equals(recent.Target, item.Target, StringComparison.OrdinalIgnoreCase) &&
string.Equals(recent.Arguments ?? "", item.Arguments ?? "", StringComparison.OrdinalIgnoreCase);
}
private static LauncherItem CreateRecentItem(LauncherItem item)
{
return new LauncherItem
{
Title = item.Title,
Type = item.Type,
Target = item.Target,
Arguments = item.Arguments,
WorkingFolder = item.WorkingFolder,
Notes = item.Notes,
IconPath = item.IconPath,
IsFavorite = false,
IsDisabled = false,
RunAsAdmin = item.RunAsAdmin,
StartMinimized = item.StartMinimized,
DisplayMode = item.DisplayMode,
Children = []
};
}
private void OpenAllItems(LauncherItem menuItem)
{
List<string> failures = [];
int launchedCount = 0;
foreach (LauncherItem child in menuItem.Children)
{
if (child.Type == LauncherItemType.Menu || child.IsDisabled || string.IsNullOrWhiteSpace(child.Target))
{
continue;
}
if (TryLaunchItem(child, out string? error))
{
launchedCount++;
RecordRecentItem(child);
}
else if (!string.IsNullOrWhiteSpace(error))
{
failures.Add(error);
}
}
popup?.Close();
if (failures.Count > 0)
{
string message = $"Opened {launchedCount} item(s), but {failures.Count} item(s) failed:\n\n" +
string.Join("\n\n", failures.Take(5));
System.Windows.MessageBox.Show(message, "Taskbar Launcher");
}
}
private void OpenContainingFolderFromPopup(LauncherItem item)
{
if (!OpenContainingFolder(item, out string? error) && !string.IsNullOrWhiteSpace(error))
{
System.Windows.MessageBox.Show(error, "Taskbar Launcher");
}
}
public static bool OpenContainingFolder(LauncherItem item, out string? error)
{
error = null;
if (item.Type == LauncherItemType.Menu || string.IsNullOrWhiteSpace(item.Target))
{
return false;
}
string target = Environment.ExpandEnvironmentVariables(item.Target);
string? folderPath = Directory.Exists(target)
? target
: Path.GetDirectoryName(target);
if (string.IsNullOrWhiteSpace(folderPath) || !Directory.Exists(folderPath))
{
error = $"{item.Title}: Containing folder was not found.";
return false;
}
try
{
Process.Start(new ProcessStartInfo
{
FileName = folderPath,
UseShellExecute = true
});
return true;
}
catch (Exception ex)
{
error = $"{item.Title}: {ex.Message}";
return false;
}
}
private static bool TryLaunchItem(LauncherItem item, out string? error)
{
error = null;
if (item.Type == LauncherItemType.Menu || item.IsDisabled || string.IsNullOrWhiteSpace(item.Target))
{
return false;
}
string target = Environment.ExpandEnvironmentVariables(item.Target);
bool runAsAdmin = item.RunAsAdmin && CanRunAsAdmin(item.Type);
string launchTarget = runAsAdmin ? ResolveExecutableTarget(target) : target;
string workingDirectory = GetWorkingDirectory(item, target);
try
{
ProcessStartInfo startInfo = new() ProcessStartInfo startInfo = new()
{ {
FileName = target, FileName = launchTarget,
Arguments = item.Arguments ?? "", Arguments = item.Arguments ?? "",
UseShellExecute = true, UseShellExecute = true,
WorkingDirectory = Directory.Exists(target) ? target : ConfigService.AppFolder WorkingDirectory = workingDirectory
}; };
if (runAsAdmin)
{
startInfo.Verb = "runas";
}
if (item.StartMinimized && CanUseLaunchOptions(item.Type))
{
startInfo.WindowStyle = ProcessWindowStyle.Minimized;
}
Process.Start(startInfo); Process.Start(startInfo);
popup?.Close(); return true;
}
catch (Exception ex)
{
error = $"{item.Title}: {ex.Message}";
return false;
}
}
public static bool CanRunAsAdmin(LauncherItemType itemType)
{
return itemType is LauncherItemType.App or LauncherItemType.File;
}
public static bool CanUseLaunchOptions(LauncherItemType itemType)
{
return itemType is LauncherItemType.App or LauncherItemType.File;
}
private static string GetWorkingDirectory(LauncherItem item, string target)
{
if (CanUseLaunchOptions(item.Type) && !string.IsNullOrWhiteSpace(item.WorkingFolder))
{
string configuredFolder = Environment.ExpandEnvironmentVariables(item.WorkingFolder.Trim());
if (Directory.Exists(configuredFolder))
{
return configuredFolder;
}
}
return Directory.Exists(target) ? target : ConfigService.AppFolder;
}
private static string ResolveExecutableTarget(string target)
{
if (Path.IsPathRooted(target) || Path.GetDirectoryName(target) is { Length: > 0 })
{
return target;
}
string[] searchFolders =
[
Environment.CurrentDirectory,
Environment.GetFolderPath(Environment.SpecialFolder.Windows),
Environment.GetFolderPath(Environment.SpecialFolder.System),
.. (Environment.GetEnvironmentVariable("PATH") ?? "")
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
];
foreach (string folder in searchFolders.Where(Directory.Exists).Distinct(StringComparer.OrdinalIgnoreCase))
{
string candidate = Path.Combine(folder, target);
if (File.Exists(candidate))
{
return candidate;
}
}
return target;
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using Microsoft.Win32; using Microsoft.Win32;
using TaskbarLauncher.Models;
using MediaColor = System.Windows.Media.Color; using MediaColor = System.Windows.Media.Color;
namespace TaskbarLauncher.Services; namespace TaskbarLauncher.Services;
@@ -10,34 +11,32 @@ public static class ThemeService
private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
private const string DwmKey = @"Software\Microsoft\Windows\DWM"; private const string DwmKey = @"Software\Microsoft\Windows\DWM";
public static void Apply() public static void Apply(LauncherThemeScheme scheme = LauncherThemeScheme.MarkHaven)
{ {
bool isLight = IsLightTheme(); bool isLight = IsLightTheme();
MediaColor accent = GetAccentColor(); ThemePalette palette = GetPalette(scheme, isLight);
MediaColor window = isLight ? MediaColor.FromRgb(0xF7, 0xF7, 0xF7) : MediaColor.FromRgb(0x20, 0x20, 0x20); MediaColor accent = palette.Accent;
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; MediaColor selectedText = IsDark(accent) ? System.Windows.Media.Colors.White : System.Windows.Media.Colors.Black;
SetBrush("AppWindowBrush", window); SetBrush("AppWindowBrush", palette.Window);
SetBrush("AppSurfaceBrush", surface); SetBrush("AppSurfaceBrush", palette.Surface);
SetBrush("AppControlBrush", control); SetBrush("AppPanelBrush", palette.Panel);
SetBrush("AppControlHoverBrush", hover); SetBrush("AppControlBrush", palette.Control);
SetBrush("AppTextBrush", text); SetBrush("AppControlHoverBrush", palette.Hover);
SetBrush("AppMutedTextBrush", muted); SetBrush("AppTextBrush", palette.Text);
SetBrush("AppBorderBrush", border); SetBrush("AppMutedTextBrush", palette.Muted);
SetBrush("AppBorderBrush", palette.Border);
SetBrush("AppAccentBrush", accent); SetBrush("AppAccentBrush", accent);
SetBrush("AppAccentSoftBrush", palette.AccentSoft);
SetBrush("AppOrangeBrush", palette.Orange);
SetBrush("AppOrangeSoftBrush", palette.OrangeSoft);
SetBrush("AppSelectedTextBrush", selectedText); SetBrush("AppSelectedTextBrush", selectedText);
SetBrush(System.Windows.SystemColors.WindowBrushKey, surface); SetBrush(System.Windows.SystemColors.WindowBrushKey, palette.Surface);
SetBrush(System.Windows.SystemColors.WindowTextBrushKey, text); SetBrush(System.Windows.SystemColors.WindowTextBrushKey, palette.Text);
SetBrush(System.Windows.SystemColors.ControlBrushKey, control); SetBrush(System.Windows.SystemColors.ControlBrushKey, palette.Control);
SetBrush(System.Windows.SystemColors.ControlTextBrushKey, text); SetBrush(System.Windows.SystemColors.ControlTextBrushKey, palette.Text);
SetBrush(System.Windows.SystemColors.ControlDarkBrushKey, border); SetBrush(System.Windows.SystemColors.ControlDarkBrushKey, palette.Border);
SetBrush(System.Windows.SystemColors.HighlightBrushKey, accent); SetBrush(System.Windows.SystemColors.HighlightBrushKey, accent);
SetBrush(System.Windows.SystemColors.HighlightTextBrushKey, selectedText); SetBrush(System.Windows.SystemColors.HighlightTextBrushKey, selectedText);
} }
@@ -62,6 +61,123 @@ public static class ThemeService
return MediaColor.FromRgb(0x00, 0x78, 0xD4); return MediaColor.FromRgb(0x00, 0x78, 0xD4);
} }
private static ThemePalette GetPalette(LauncherThemeScheme scheme, bool isLight)
{
MediaColor windowsAccent = GetAccentColor();
MediaColor orange = MediaColor.FromRgb(0xD8, 0x78, 0x1D);
return scheme switch
{
LauncherThemeScheme.System => CreatePalette(
isLight,
windowsAccent,
orange,
window: LightDark(isLight, 0xF7F7F7, 0x202020),
surface: LightDark(isLight, 0xFFFFFF, 0x2B2B2B),
panel: LightDark(isLight, 0xF3F3F3, 0x333333),
control: LightDark(isLight, 0xF3F3F3, 0x333333),
hover: LightDark(isLight, 0xEAF3FF, 0x3B4A5C),
text: LightDark(isLight, 0x1A1A1A, 0xF3F3F3),
muted: LightDark(isLight, 0x666666, 0xB8B8B8),
border: LightDark(isLight, 0xC8C8C8, 0x555555)),
LauncherThemeScheme.WindowsClassic => CreatePalette(
isLight,
windowsAccent,
orange,
window: LightDark(isLight, 0xF0F0F0, 0x1F1F1F),
surface: LightDark(isLight, 0xFAFAFA, 0x2D2D30),
panel: LightDark(isLight, 0xFFFFFF, 0x252526),
control: LightDark(isLight, 0xF5F5F5, 0x333337),
hover: LightDark(isLight, 0xEDEDED, 0x3E3E42),
text: LightDark(isLight, 0x111111, 0xF1F1F1),
muted: LightDark(isLight, 0x5F5F5F, 0xBDBDBD),
border: LightDark(isLight, 0xD0D0D0, 0x565656)),
LauncherThemeScheme.MGETOrange => CreatePalette(
isLight,
orange,
orange,
window: LightDark(isLight, 0xF6F1EA, 0x181614),
surface: LightDark(isLight, 0xFFFCF8, 0x221F1B),
panel: LightDark(isLight, 0xFFF7EE, 0x2A241E),
control: LightDark(isLight, 0xFAF1E7, 0x322B24),
hover: LightDark(isLight, 0xFFEDDA, 0x423428),
text: LightDark(isLight, 0x241D17, 0xF8F2EA),
muted: LightDark(isLight, 0x75685C, 0xC3B5A6),
border: LightDark(isLight, 0xE4D1BC, 0x5B4B3C)),
LauncherThemeScheme.MGETBlueOrange => CreatePalette(
isLight,
LightDark(isLight, 0x1450A0, 0x78B4F0),
LightDark(isLight, 0xF4A000, 0xFFB400),
window: LightDark(isLight, 0xEEF3F9, 0x0D1424),
surface: LightDark(isLight, 0xFFFFFF, 0x141C2D),
panel: LightDark(isLight, 0xF5F8FC, 0x19243A),
control: LightDark(isLight, 0xFFF3DA, 0x2F2A20),
hover: LightDark(isLight, 0xE8F1FF, 0x203457),
text: LightDark(isLight, 0x162235, 0xEEF5FF),
muted: LightDark(isLight, 0x617086, 0xAAB8CC),
border: LightDark(isLight, 0xCBD7E7, 0x354762)),
_ => CreatePalette(
isLight,
LightDark(isLight, 0x146B52, 0x58D0A3),
LightDark(isLight, 0xB56B08, 0xF4BD54),
window: LightDark(isLight, 0xF9FBFA, 0x0D1A16),
surface: LightDark(isLight, 0xFFFFFF, 0x14231E),
panel: LightDark(isLight, 0xF9FBFA, 0x14231E),
control: LightDark(isLight, 0xEEF3F0, 0x1A2A24),
hover: LightDark(isLight, 0xE2F3EC, 0x163B2F),
text: LightDark(isLight, 0x192721, 0xEDF5F2),
muted: LightDark(isLight, 0x68766F, 0x9FB0A9),
border: LightDark(isLight, 0xD9E2DE, 0x30463D))
};
}
private static ThemePalette CreatePalette(
bool isLight,
MediaColor accent,
MediaColor orange,
MediaColor window,
MediaColor surface,
MediaColor panel,
MediaColor control,
MediaColor hover,
MediaColor text,
MediaColor muted,
MediaColor border)
{
return new ThemePalette(
window,
surface,
panel,
control,
hover,
text,
muted,
border,
accent,
Blend(accent, surface, isLight ? 0.14 : 0.24),
orange,
Blend(orange, surface, isLight ? 0.16 : 0.30));
}
private static MediaColor LightDark(bool isLight, int lightRgb, int darkRgb)
{
int rgb = isLight ? lightRgb : darkRgb;
return MediaColor.FromRgb((byte)(rgb >> 16), (byte)(rgb >> 8), (byte)rgb);
}
private static MediaColor Blend(MediaColor foreground, MediaColor background, double foregroundAmount)
{
double amount = Math.Clamp(foregroundAmount, 0, 1);
byte r = (byte)Math.Round((foreground.R * amount) + (background.R * (1 - amount)));
byte g = (byte)Math.Round((foreground.G * amount) + (background.G * (1 - amount)));
byte b = (byte)Math.Round((foreground.B * amount) + (background.B * (1 - amount)));
return MediaColor.FromRgb(r, g, b);
}
private static void SetBrush(string key, MediaColor color) private static void SetBrush(string key, MediaColor color)
{ {
SolidColorBrush brush = new(color); SolidColorBrush brush = new(color);
@@ -81,4 +197,18 @@ public static class ThemeService
double luminance = (0.299 * color.R) + (0.587 * color.G) + (0.114 * color.B); double luminance = (0.299 * color.R) + (0.587 * color.G) + (0.114 * color.B);
return luminance < 140; return luminance < 140;
} }
private sealed record ThemePalette(
MediaColor Window,
MediaColor Surface,
MediaColor Panel,
MediaColor Control,
MediaColor Hover,
MediaColor Text,
MediaColor Muted,
MediaColor Border,
MediaColor Accent,
MediaColor AccentSoft,
MediaColor Orange,
MediaColor OrangeSoft);
} }

View File

@@ -11,14 +11,16 @@
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Authors>Ray Berezowski</Authors> <Authors>Ray Berezowski</Authors>
<ApplicationIcon>Assets\AppIcon.ico</ApplicationIcon> <ApplicationIcon>Assets\AppIcon.ico</ApplicationIcon>
<Version>1.0.0</Version> <Version>1.0.2</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion> <AssemblyVersion>1.0.2.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion> <FileVersion>1.0.2.0</FileVersion>
<InformationalVersion>1.0.0</InformationalVersion> <InformationalVersion>1.0.2</InformationalVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Resource Include="Assets\AppIcon.ico" />
<Resource Include="Assets\AboutLogo.png" /> <Resource Include="Assets\AboutLogo.png" />
<Resource Include="Assets\HeaderLogo.png" />
<Resource Include="Assets\TrayIcon.ico" /> <Resource Include="Assets\TrayIcon.ico" />
</ItemGroup> </ItemGroup>

View File

@@ -2,7 +2,7 @@
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"
Title="Taskbar Launcher" Title="Taskbar Launcher"
Width="360" SizeToContent="Height" Width="380" SizeToContent="Height"
MaxHeight="720" MaxHeight="720"
WindowStyle="None" WindowStyle="None"
ResizeMode="NoResize" ResizeMode="NoResize"
@@ -13,17 +13,35 @@
<Border BorderThickness="1" <Border BorderThickness="1"
BorderBrush="{DynamicResource AppBorderBrush}" BorderBrush="{DynamicResource AppBorderBrush}"
Background="{DynamicResource AppSurfaceBrush}" Background="{DynamicResource AppSurfaceBrush}"
Padding="10"> CornerRadius="16"
Padding="12">
<DockPanel> <DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8"> <Grid DockPanel.Dock="Top" Margin="0,0,0,10">
<TextBlock Text="Taskbar Launcher" FontWeight="SemiBold" VerticalAlignment="Center"/> <Grid.ColumnDefinitions>
<Button Content="Settings" HorizontalAlignment="Right" Margin="12,0,0,0" Padding="8,4" <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image Source="pack://application:,,,/Assets/HeaderLogo.png"
Width="28"
Height="28"
Margin="0,0,10,0"
Stretch="Uniform"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1"
Text="Taskbar Launcher"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
<Button Grid.Column="2"
Content="Settings"
HorizontalAlignment="Right"
Padding="10,5"
Click="Settings_Click"/> Click="Settings_Click"/>
</StackPanel> </Grid>
<TextBox x:Name="SearchBox" <TextBox x:Name="SearchBox"
DockPanel.Dock="Top" DockPanel.Dock="Top"
Margin="0,0,0,8" Margin="0,0,0,10"
Padding="8,5" Padding="10,6"
TextChanged="SearchBox_TextChanged" TextChanged="SearchBox_TextChanged"
PreviewKeyDown="SearchBox_PreviewKeyDown"/> PreviewKeyDown="SearchBox_PreviewKeyDown"/>
<ScrollViewer x:Name="ItemsScrollViewer" <ScrollViewer x:Name="ItemsScrollViewer"

View File

@@ -14,17 +14,25 @@ public partial class LauncherPopup : Window
private const double HeaderAndChromeHeight = 118; private const double HeaderAndChromeHeight = 118;
private bool isRepositionQueued; private bool isRepositionQueued;
private readonly List<System.Windows.Controls.Button> visibleLaunchButtons = []; private readonly List<System.Windows.Controls.Button> visibleLaunchButtons = [];
private readonly List<System.Windows.Controls.Control> visibleNavigationControls = [];
private readonly LauncherConfig config; private readonly LauncherConfig config;
private readonly Action<LauncherItem> launchItem; private readonly Action<LauncherItem> launchItem;
private readonly Action<LauncherItem> openAllItems;
private readonly Action showSettings; private readonly Action showSettings;
private readonly Action<LauncherItem> editItem;
private readonly Action<LauncherItem> openContainingFolder;
private sealed record SearchMatch(LauncherItem Item, string ParentPath);
public LauncherPopup(LauncherConfig config, Action<LauncherItem> launchItem, Action showSettings) public LauncherPopup(LauncherConfig config, Action<LauncherItem> launchItem, Action<LauncherItem> openAllItems, Action showSettings, Action<LauncherItem> editItem, Action<LauncherItem> openContainingFolder)
{ {
InitializeComponent(); InitializeComponent();
this.config = config; this.config = config;
this.launchItem = launchItem; this.launchItem = launchItem;
this.openAllItems = openAllItems;
this.showSettings = showSettings; this.showSettings = showSettings;
this.editItem = editItem;
this.openContainingFolder = openContainingFolder;
BuildItems(); BuildItems();
SizeChanged += (_, _) => QueueReposition(); SizeChanged += (_, _) => QueueReposition();
} }
@@ -89,28 +97,66 @@ public partial class LauncherPopup : Window
{ {
ItemsPanel.Children.Clear(); ItemsPanel.Children.Clear();
visibleLaunchButtons.Clear(); visibleLaunchButtons.Clear();
visibleNavigationControls.Clear();
if (config.RecentItems.Any(item => !item.IsDisabled))
{
ItemsPanel.Children.Add(CreateRecentItemsControl());
}
bool expandMenusByDefault = config.Items.Count <= 2; bool expandMenusByDefault = config.Items.Count <= 2;
foreach (LauncherItem item in config.Items) foreach (LauncherItem item in GetDisplayItems(config.Items))
{ {
ItemsPanel.Children.Add(CreateItemControl(item, 0, expandMenusByDefault)); ItemsPanel.Children.Add(CreateItemControl(item, 0, expandMenusByDefault));
} }
RebuildNavigationLists();
} }
private FrameworkElement CreateItemControl(LauncherItem item, int depth, bool expandMenusByDefault) private Expander CreateRecentItemsControl()
{
Expander expander = new()
{
Header = new TextBlock { Text = "Recent" },
IsExpanded = true,
Margin = new Thickness(0, 4, 0, 8),
Focusable = true
};
expander.KeyDown += NavigationControl_KeyDown;
expander.Expanded += (_, _) => RebuildNavigationAndReposition();
expander.Collapsed += (_, _) => RebuildNavigationAndReposition();
StackPanel recentItems = new();
foreach (LauncherItem item in config.RecentItems.Where(item => !item.IsDisabled))
{
recentItems.Children.Add(CreateItemControl(item, 1, expandMenusByDefault: false, allowEdit: false));
}
expander.Content = recentItems;
return expander;
}
private FrameworkElement CreateItemControl(LauncherItem item, int depth, bool expandMenusByDefault, bool allowEdit = true)
{ {
if (item.Type == LauncherItemType.Menu) if (item.Type == LauncherItemType.Menu)
{ {
Expander expander = new() Expander expander = new()
{ {
Header = item.Title, Header = CreateMenuHeader(item),
IsExpanded = expandMenusByDefault, IsExpanded = expandMenusByDefault,
Margin = new Thickness(depth * 12, 4, 0, 4) Margin = new Thickness(depth * 12, 4, 0, 4),
Focusable = true,
Tag = item,
ToolTip = CreateItemToolTip(item)
}; };
expander.KeyDown += NavigationControl_KeyDown;
expander.Expanded += (_, _) => RebuildNavigationAndReposition();
expander.Collapsed += (_, _) => RebuildNavigationAndReposition();
StackPanel children = new(); StackPanel children = new();
foreach (LauncherItem child in item.Children) foreach (LauncherItem child in GetDisplayItems(item.Children))
{ {
children.Children.Add(CreateItemControl(child, depth + 1, expandMenusByDefault)); children.Children.Add(CreateItemControl(child, depth + 1, expandMenusByDefault, allowEdit));
} }
expander.Content = children; expander.Content = children;
@@ -122,25 +168,162 @@ public partial class LauncherPopup : Window
Content = CreateButtonContent(item), Content = CreateButtonContent(item),
Margin = new Thickness(depth * 12, 2, 0, 2), Margin = new Thickness(depth * 12, 2, 0, 2),
MinHeight = DisplayModeMetrics.GetRowHeight(GetEffectiveDisplayMode(item)), MinHeight = DisplayModeMetrics.GetRowHeight(GetEffectiveDisplayMode(item)),
Tag = item HorizontalContentAlignment = System.Windows.HorizontalAlignment.Left,
Tag = item,
ToolTip = CreateItemToolTip(item)
}; };
button.Click += (_, _) => launchItem(item); button.Click += (_, _) => launchItem(item);
button.KeyDown += (_, e) => if (allowEdit)
{ {
if (e.Key == Key.Enter) button.ContextMenu = CreateItemContextMenu(item);
{
launchItem(item);
e.Handled = true;
} }
else if (e.Key == Key.Escape)
button.KeyDown += LaunchButton_KeyDown;
return button;
}
private TextBlock CreateMenuHeader(LauncherItem item)
{ {
System.Windows.Controls.MenuItem openAllMenuItem = new()
{
Header = "Open All",
IsEnabled = item.Children.Any(IsLaunchable)
};
openAllMenuItem.Click += (_, e) =>
{
openAllItems(item);
e.Handled = true;
};
System.Windows.Controls.ContextMenu contextMenu = new();
contextMenu.Items.Add(openAllMenuItem);
contextMenu.Items.Add(new Separator());
contextMenu.Items.Add(CreatePinMenuItem(item));
contextMenu.Items.Add(CreateDisableMenuItem(item));
contextMenu.Items.Add(CreateEditMenuItem(item));
return new TextBlock
{
Text = item.Title,
ContextMenu = contextMenu,
ToolTip = CreateItemToolTip(item)
};
}
private System.Windows.Controls.ContextMenu CreateItemContextMenu(LauncherItem item)
{
System.Windows.Controls.ContextMenu contextMenu = new();
contextMenu.Items.Add(CreatePinMenuItem(item));
contextMenu.Items.Add(CreateDisableMenuItem(item));
contextMenu.Items.Add(CreateRunAsAdminMenuItem(item));
contextMenu.Items.Add(CreateOpenContainingFolderMenuItem(item));
contextMenu.Items.Add(CreateEditMenuItem(item));
return contextMenu;
}
private System.Windows.Controls.MenuItem CreatePinMenuItem(LauncherItem item)
{
System.Windows.Controls.MenuItem pinMenuItem = new()
{
Header = item.IsFavorite ? "Unpin from Top" : "Pin to Top"
};
pinMenuItem.Click += (_, e) =>
{
item.IsFavorite = !item.IsFavorite;
ConfigService.Save(config);
BuildItems();
QueueReposition();
e.Handled = true;
};
return pinMenuItem;
}
private System.Windows.Controls.MenuItem CreateDisableMenuItem(LauncherItem item)
{
System.Windows.Controls.MenuItem disableMenuItem = new()
{
Header = item.IsDisabled ? "Enable Item" : "Disable Item"
};
disableMenuItem.Click += (_, e) =>
{
item.IsDisabled = !item.IsDisabled;
ConfigService.Save(config);
BuildItems();
QueueReposition();
e.Handled = true;
};
return disableMenuItem;
}
private System.Windows.Controls.MenuItem CreateRunAsAdminMenuItem(LauncherItem item)
{
System.Windows.Controls.MenuItem runAsAdminMenuItem = new()
{
Header = item.RunAsAdmin ? "Run as Admin: On" : "Run as Admin: Off",
IsEnabled = LauncherController.CanRunAsAdmin(item.Type)
};
runAsAdminMenuItem.Click += (_, e) =>
{
item.RunAsAdmin = !item.RunAsAdmin;
ConfigService.Save(config);
BuildItems();
QueueReposition();
e.Handled = true;
};
return runAsAdminMenuItem;
}
private System.Windows.Controls.MenuItem CreateOpenContainingFolderMenuItem(LauncherItem item)
{
System.Windows.Controls.MenuItem openFolderMenuItem = new()
{
Header = "Open Containing Folder",
IsEnabled = CanOpenContainingFolder(item)
};
openFolderMenuItem.Click += (_, e) =>
{
openContainingFolder(item);
e.Handled = true;
};
return openFolderMenuItem;
}
private System.Windows.Controls.MenuItem CreateEditMenuItem(LauncherItem item)
{
System.Windows.Controls.MenuItem editMenuItem = new()
{
Header = "Edit in Settings"
};
editMenuItem.Click += (_, e) =>
{
editItem(item);
Close(); Close();
e.Handled = true; e.Handled = true;
}
}; };
visibleLaunchButtons.Add(button);
return button; return editMenuItem;
}
private static bool IsLaunchable(LauncherItem item)
{
return item.Type != LauncherItemType.Menu && !item.IsDisabled && !string.IsNullOrWhiteSpace(item.Target);
}
private static bool CanOpenContainingFolder(LauncherItem item)
{
return item.Type is LauncherItemType.App or LauncherItemType.File && !string.IsNullOrWhiteSpace(item.Target);
}
private static IEnumerable<LauncherItem> GetDisplayItems(IEnumerable<LauncherItem> items)
{
return items
.Where(item => !item.IsDisabled)
.OrderByDescending(item => item.IsFavorite);
} }
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
@@ -163,8 +346,8 @@ public partial class LauncherPopup : Window
ItemsPanel.Children.Clear(); ItemsPanel.Children.Clear();
visibleLaunchButtons.Clear(); visibleLaunchButtons.Clear();
List<LauncherItem> matches = []; List<SearchMatch> matches = [];
CollectMatches(config.Items, query, matches); CollectMatches(config.Items, query, [], matches);
if (matches.Count == 0) if (matches.Count == 0)
{ {
@@ -177,34 +360,179 @@ public partial class LauncherPopup : Window
return; return;
} }
foreach (LauncherItem item in matches) foreach (SearchMatch match in matches)
{ {
ItemsPanel.Children.Add(CreateItemControl(item, 0, expandMenusByDefault: false)); ItemsPanel.Children.Add(CreateSearchResultControl(match));
}
RebuildNavigationLists();
}
private FrameworkElement CreateSearchResultControl(SearchMatch match)
{
System.Windows.Controls.Button button = new()
{
Content = CreateSearchResultContent(match),
Margin = new Thickness(0, 2, 0, 2),
MinHeight = Math.Max(DisplayModeMetrics.GetRowHeight(GetEffectiveDisplayMode(match.Item)), 48),
HorizontalContentAlignment = System.Windows.HorizontalAlignment.Left,
Tag = match.Item,
ContextMenu = CreateItemContextMenu(match.Item),
ToolTip = CreateItemToolTip(match.Item, match.ParentPath)
};
button.Click += (_, _) => launchItem(match.Item);
button.KeyDown += LaunchButton_KeyDown;
return button;
}
private void RebuildNavigationAndReposition()
{
RebuildNavigationLists();
QueueReposition();
}
private void RebuildNavigationLists()
{
visibleLaunchButtons.Clear();
visibleNavigationControls.Clear();
AddVisibleNavigationControls(ItemsPanel.Children);
}
private void AddVisibleNavigationControls(UIElementCollection elements)
{
foreach (UIElement element in elements)
{
switch (element)
{
case Expander expander:
visibleNavigationControls.Add(expander);
if (expander.IsExpanded && expander.Content is System.Windows.Controls.Panel panel)
{
AddVisibleNavigationControls(panel.Children);
}
break;
case System.Windows.Controls.Button button:
visibleNavigationControls.Add(button);
if (button.Tag is LauncherItem)
{
visibleLaunchButtons.Add(button);
}
break;
}
} }
} }
private static void CollectMatches(IEnumerable<LauncherItem> items, string query, List<LauncherItem> matches) private object CreateSearchResultContent(SearchMatch match)
{ {
foreach (LauncherItem item in items) StackPanel panel = new();
panel.Children.Add((UIElement)CreateButtonContent(match.Item));
if (!string.IsNullOrWhiteSpace(match.ParentPath))
{ {
bool isLaunchable = item.Type != LauncherItemType.Menu; panel.Children.Add(new TextBlock
if (isLaunchable && Matches(item, query))
{ {
matches.Add(item); Text = match.ParentPath,
FontSize = 11,
Foreground = (System.Windows.Media.Brush)System.Windows.Application.Current.Resources["AppMutedTextBrush"],
Margin = new Thickness(0, 3, 0, 0),
TextTrimming = TextTrimming.CharacterEllipsis
});
}
return panel;
}
private static System.Windows.Controls.ToolTip CreateItemToolTip(LauncherItem item, string? parentPath = null)
{
StackPanel panel = new() { MaxWidth = 520 };
AddToolTipLine(panel, item.Title, bold: true);
AddToolTipLine(panel, $"Type: {item.Type}");
if (!string.IsNullOrWhiteSpace(parentPath))
{
AddToolTipLine(panel, $"Menu: {parentPath}");
}
if (!string.IsNullOrWhiteSpace(item.Target))
{
AddToolTipLine(panel, $"Target: {item.Target}");
}
if (!string.IsNullOrWhiteSpace(item.Arguments))
{
AddToolTipLine(panel, $"Arguments: {item.Arguments}");
}
if (!string.IsNullOrWhiteSpace(item.WorkingFolder))
{
AddToolTipLine(panel, $"Working folder: {item.WorkingFolder}");
}
if (!string.IsNullOrWhiteSpace(item.Notes))
{
AddToolTipLine(panel, $"Notes: {item.Notes}");
}
if (item.IsFavorite)
{
AddToolTipLine(panel, "Pinned to top");
}
if (item.RunAsAdmin)
{
AddToolTipLine(panel, "Runs as administrator");
}
if (item.StartMinimized)
{
AddToolTipLine(panel, "Starts minimized");
}
return new System.Windows.Controls.ToolTip { Content = panel };
}
private static void AddToolTipLine(StackPanel panel, string text, bool bold = false)
{
panel.Children.Add(new TextBlock
{
Text = text,
FontWeight = bold ? FontWeights.SemiBold : FontWeights.Normal,
TextWrapping = TextWrapping.Wrap,
Margin = panel.Children.Count == 0 ? new Thickness(0) : new Thickness(0, 3, 0, 0)
});
}
private static void CollectMatches(IEnumerable<LauncherItem> items, string query, List<string> parentNames, List<SearchMatch> matches)
{
foreach (LauncherItem item in GetDisplayItems(items))
{
bool isLaunchable = item.Type != LauncherItemType.Menu && !item.IsDisabled;
string parentPath = string.Join(" > ", parentNames);
if (isLaunchable && Matches(item, query, parentPath))
{
matches.Add(new SearchMatch(item, parentPath));
} }
if (item.Children.Count > 0) if (item.Children.Count > 0)
{ {
CollectMatches(item.Children, query, matches); string menuName = string.IsNullOrWhiteSpace(item.Title) ? "Untitled" : item.Title;
CollectMatches(item.Children, query, [.. parentNames, menuName], matches);
} }
} }
} }
private static bool Matches(LauncherItem item, string query) private static bool Matches(LauncherItem item, string query, string parentPath)
{ {
return item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) || return item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
item.Target.Contains(query, StringComparison.OrdinalIgnoreCase) || item.Target.Contains(query, StringComparison.OrdinalIgnoreCase) ||
(item.Arguments?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false); (item.Arguments?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false) ||
(item.Notes?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false) ||
parentPath.Contains(query, StringComparison.OrdinalIgnoreCase);
} }
private void SearchBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) private void SearchBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
@@ -223,14 +551,142 @@ public partial class LauncherPopup : Window
return; return;
} }
if (e.Key == Key.Down && visibleLaunchButtons.FirstOrDefault() is System.Windows.Controls.Button firstButton) if (e.Key == Key.Down)
{ {
Keyboard.ClearFocus(); FocusNavigationControl(0);
firstButton.Focus(); e.Handled = true;
return;
}
if (e.Key == Key.Up)
{
FocusNavigationControl(visibleNavigationControls.Count - 1);
e.Handled = true;
return;
}
if (e.Key == Key.Home)
{
SearchBox.CaretIndex = 0;
e.Handled = true;
return;
}
if (e.Key == Key.End)
{
SearchBox.CaretIndex = SearchBox.Text.Length;
e.Handled = true; e.Handled = true;
} }
} }
private void LaunchButton_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (sender is not System.Windows.Controls.Button button)
{
return;
}
if (e.Key == Key.Enter && button.Tag is LauncherItem item)
{
launchItem(item);
e.Handled = true;
return;
}
if (e.Key == Key.Escape)
{
Close();
e.Handled = true;
return;
}
HandleNavigationControlKey(button, e);
}
private void NavigationControl_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (sender is not System.Windows.Controls.Control control)
{
return;
}
if (e.Key == Key.Enter && control is Expander expander)
{
expander.IsExpanded = !expander.IsExpanded;
QueueReposition();
e.Handled = true;
return;
}
if (e.Key == Key.Escape)
{
Close();
e.Handled = true;
return;
}
HandleNavigationControlKey(control, e);
}
private void HandleNavigationControlKey(System.Windows.Controls.Control control, System.Windows.Input.KeyEventArgs e)
{
int index = visibleNavigationControls.IndexOf(control);
if (index < 0)
{
return;
}
if (e.Key == Key.Down)
{
FocusNavigationControl(index + 1);
e.Handled = true;
return;
}
if (e.Key == Key.Up)
{
if (index == 0)
{
SearchBox.Focus();
SearchBox.CaretIndex = SearchBox.Text.Length;
}
else
{
FocusNavigationControl(index - 1);
}
e.Handled = true;
return;
}
if (e.Key == Key.Home)
{
FocusNavigationControl(0);
e.Handled = true;
return;
}
if (e.Key == Key.End)
{
FocusNavigationControl(visibleNavigationControls.Count - 1);
e.Handled = true;
}
}
private void FocusNavigationControl(int index)
{
if (visibleNavigationControls.Count == 0)
{
return;
}
int clampedIndex = Math.Clamp(index, 0, visibleNavigationControls.Count - 1);
System.Windows.Controls.Control control = visibleNavigationControls[clampedIndex];
Keyboard.ClearFocus();
control.Focus();
control.BringIntoView();
}
protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e) protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
{ {
if (e.Key == Key.Escape) if (e.Key == Key.Escape)

View File

@@ -1,13 +1,21 @@
{ {
"Hotkey": "Ctrl+Alt+Space", "Hotkey": "Ctrl+Alt+Space",
"DisplayMode": "CompactList", "DisplayMode": "CompactList",
"ThemeScheme": "MarkHaven",
"RecentItems": [],
"Items": [ "Items": [
{ {
"Title": "Folders", "Title": "Folders",
"Type": "Menu", "Type": "Menu",
"Target": "", "Target": "",
"Arguments": null, "Arguments": null,
"WorkingFolder": null,
"Notes": null,
"IconPath": null, "IconPath": null,
"IsFavorite": false,
"IsDisabled": false,
"RunAsAdmin": false,
"StartMinimized": false,
"DisplayMode": "CompactList", "DisplayMode": "CompactList",
"Children": [ "Children": [
{ {
@@ -15,7 +23,13 @@
"Type": "Folder", "Type": "Folder",
"Target": "%USERPROFILE%\\Documents", "Target": "%USERPROFILE%\\Documents",
"Arguments": null, "Arguments": null,
"WorkingFolder": null,
"Notes": null,
"IconPath": "%USERPROFILE%\\Documents", "IconPath": "%USERPROFILE%\\Documents",
"IsFavorite": false,
"IsDisabled": false,
"RunAsAdmin": false,
"StartMinimized": false,
"DisplayMode": "CompactList", "DisplayMode": "CompactList",
"Children": [] "Children": []
}, },
@@ -24,7 +38,13 @@
"Type": "Folder", "Type": "Folder",
"Target": "%USERPROFILE%\\Downloads", "Target": "%USERPROFILE%\\Downloads",
"Arguments": null, "Arguments": null,
"WorkingFolder": null,
"Notes": null,
"IconPath": "%USERPROFILE%\\Downloads", "IconPath": "%USERPROFILE%\\Downloads",
"IsFavorite": false,
"IsDisabled": false,
"RunAsAdmin": false,
"StartMinimized": false,
"DisplayMode": "CompactList", "DisplayMode": "CompactList",
"Children": [] "Children": []
} }
@@ -35,7 +55,13 @@
"Type": "Menu", "Type": "Menu",
"Target": "", "Target": "",
"Arguments": null, "Arguments": null,
"WorkingFolder": null,
"Notes": null,
"IconPath": null, "IconPath": null,
"IsFavorite": false,
"IsDisabled": false,
"RunAsAdmin": false,
"StartMinimized": false,
"DisplayMode": "CompactList", "DisplayMode": "CompactList",
"Children": [ "Children": [
{ {
@@ -43,7 +69,13 @@
"Type": "App", "Type": "App",
"Target": "notepad.exe", "Target": "notepad.exe",
"Arguments": null, "Arguments": null,
"WorkingFolder": null,
"Notes": "Simple starter app entry.",
"IconPath": "notepad.exe", "IconPath": "notepad.exe",
"IsFavorite": false,
"IsDisabled": false,
"RunAsAdmin": false,
"StartMinimized": false,
"DisplayMode": "CompactList", "DisplayMode": "CompactList",
"Children": [] "Children": []
} }

View File

@@ -1,5 +1,7 @@
# Install and Use Taskbar Launcher # Install and Use Taskbar Launcher
Current stable release: `v1.0.2`
## Small Package ## Small Package
The small package requires: The small package requires:
@@ -19,6 +21,18 @@ Install the **.NET Desktop Runtime** for **Windows x64**.
2. Run `TaskbarLauncher.exe`. 2. Run `TaskbarLauncher.exe`.
3. The app appears in the system tray. 3. The app appears in the system tray.
4. Press `Ctrl+Alt+Space` to open the launcher. 4. Press `Ctrl+Alt+Space` to open the launcher.
5. Right-click a launcher entry and choose **Edit in Settings** to jump to that item.
6. Right-click a launcher entry and choose **Pin to Top** or **Unpin from Top**.
7. Right-click a launcher entry and choose **Disable Item** to hide it until it is enabled again from Settings.
8. Right-click an app or file entry and choose **Open Containing Folder**.
Recently launched entries appear in the launcher popup under **Recent**.
Search results include the parent menu path when an item is inside a menu.
Use arrow keys, Home, End, Enter, and Escape to navigate the launcher popup from the keyboard.
Hover over launcher items to see details such as type, menu path, target, and arguments.
## Configure ## Configure
@@ -30,6 +44,17 @@ From Settings you can:
- Add submenus - Add submenus
- Add folders, apps, files, and websites - Add folders, apps, files, and websites
- Drag/drop items to reorder - Drag/drop items to reorder
- Drag/drop files, folders, shortcuts, or web links into the launcher item list
- Right-click an item and choose **Duplicate Item** to copy it
- Right-click an item and choose **Pin to Top** to show it first in its menu
- Right-click an item and choose **Disable Item** or **Enable Item** to hide or restore it
- Right-click an app or file item and choose **Open Containing Folder**
- Enable **Run as administrator** for app and file items that need elevation
- Set a working folder or start apps minimized for app and file items
- Add notes to items for reminders, context, or search keywords
- Use **Config... > Validate Targets** to check for missing app, file, and folder targets
- Use **Config... > Export Selected Item** or **Import Item** to share one item or menu section
- Right-click an item and choose **Sort This Menu** or **Sort This Level**
- Set per-item icon sizes - Set per-item icon sizes
- Back up and restore `config.json` - Back up and restore `config.json`
- Enable Start with Windows - Enable Start with Windows

View File

@@ -21,6 +21,7 @@ Examples:
- `1.0.0`: first stable release - `1.0.0`: first stable release
- `1.0.1`: bug fix - `1.0.1`: bug fix
- `1.0.2`: tested feature and polish release
- `1.1.0`: new feature - `1.1.0`: new feature
Update version fields in: Update version fields in:
@@ -37,9 +38,9 @@ TaskbarLauncher.csproj
4. Tag the release: 4. Tag the release:
```powershell ```powershell
git tag v1.0.0 git tag v1.0.2
git push origin main git push origin main
git push origin v1.0.0 git push origin v1.0.2
``` ```
5. In Gitea, create a release from the tag. 5. In Gitea, create a release from the tag.

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB