diff --git a/AboutWindow.xaml b/AboutWindow.xaml
index ed04a18..9156c5d 100644
--- a/AboutWindow.xaml
+++ b/AboutWindow.xaml
@@ -2,8 +2,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="About Taskbar Launcher"
- Height="410"
- Width="560"
+ Height="430"
+ Width="580"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource AppWindowBrush}"
@@ -15,6 +15,11 @@
+
@@ -25,7 +30,8 @@
Height="76"
BorderThickness="1"
BorderBrush="{DynamicResource AppAccentBrush}"
- Background="{DynamicResource AppSurfaceBrush}"
+ Background="{DynamicResource AppOrangeSoftBrush}"
+ CornerRadius="14"
HorizontalAlignment="Left"
VerticalAlignment="Top">
+
+ Background="{DynamicResource AppSurfaceBrush}"
+ CornerRadius="14">
@@ -89,6 +97,7 @@
diff --git a/App.xaml b/App.xaml
index fef3362..6e3df1f 100644
--- a/App.xaml
+++ b/App.xaml
@@ -5,8 +5,10 @@
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
+
-
+
@@ -36,9 +107,9 @@
-
-
-
+
+
+
@@ -51,6 +122,7 @@
-
+
-
+
+
@@ -76,88 +153,176 @@
+
+
-
+
-
+
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 4101d12..f4b60a1 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -3,6 +3,7 @@ using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Media;
using System.IO;
+using System.Text;
using System.Windows.Input;
using TaskbarLauncher.Models;
using TaskbarLauncher.Services;
@@ -21,6 +22,7 @@ public partial class MainWindow : Window
private List? selectedPath;
private string? loadedAutoIconPath;
private System.Windows.Point dragStartPoint;
+ private sealed record TargetValidationIssue(string ItemPath, string Problem);
public MainWindow()
{
@@ -33,11 +35,24 @@ public partial class MainWindow : Window
this.controller = controller;
DisplayModeBox.ItemsSource = Enum.GetValues();
ItemDisplayModeBox.ItemsSource = Enum.GetValues();
+ ThemeSchemeBox.ItemsSource = Enum.GetValues();
TypeBox.ItemsSource = Enum.GetValues();
VersionText.Text = $"Version {AppInfo.Version} Build {AppInfo.Build}";
RefreshView();
}
+ public void SelectItemForEdit(LauncherItem item)
+ {
+ if (controller is null)
+ {
+ return;
+ }
+
+ RefreshView(item);
+ Show();
+ Activate();
+ }
+
private void RefreshView(LauncherItem? itemToSelect = null)
{
if (controller is null)
@@ -53,6 +68,7 @@ public partial class MainWindow : Window
HotkeyBox.Text = controller.Config.Hotkey;
DisplayModeBox.SelectedItem = controller.Config.DisplayMode;
+ ThemeSchemeBox.SelectedItem = controller.Config.ThemeScheme;
LoadStartupState();
StatusText.Text = "Config file: config.json beside TaskbarLauncher.exe";
if (itemToSelect is not null && SelectTreeItem(itemToSelect))
@@ -67,9 +83,53 @@ public partial class MainWindow : Window
}
}
- private static TreeViewItem CreateTreeItem(LauncherItem item)
+ private TreeViewItem CreateTreeItem(LauncherItem item)
{
- var treeItem = new TreeViewItem { Header = $"{item.Title} ({item.Type})", Tag = item };
+ var pinMenuItem = new System.Windows.Controls.MenuItem
+ {
+ Header = item.IsFavorite ? "Unpin from Top" : "Pin to Top"
+ };
+ pinMenuItem.Click += ToggleFavorite_Click;
+
+ var disableMenuItem = new System.Windows.Controls.MenuItem
+ {
+ Header = item.IsDisabled ? "Enable Item" : "Disable Item"
+ };
+ disableMenuItem.Click += ToggleDisabled_Click;
+
+ var openContainingFolderMenuItem = new System.Windows.Controls.MenuItem
+ {
+ Header = "Open Containing Folder",
+ IsEnabled = CanOpenContainingFolder(item)
+ };
+ openContainingFolderMenuItem.Click += OpenContainingFolder_Click;
+
+ var duplicateMenuItem = new System.Windows.Controls.MenuItem { Header = "Duplicate Item" };
+ duplicateMenuItem.Click += DuplicateItem_Click;
+
+ var sortMenuItem = new System.Windows.Controls.MenuItem
+ {
+ Header = item.Type == LauncherItemType.Menu ? "Sort This Menu" : "Sort This Level"
+ };
+ sortMenuItem.Click += SortSelected_Click;
+
+ var treeItem = new TreeViewItem
+ {
+ Header = $"{GetTreeStatusPrefix(item)}{item.Title} ({item.Type})",
+ Tag = item,
+ ContextMenu = new System.Windows.Controls.ContextMenu()
+ };
+ treeItem.ContextMenu.Items.Add(pinMenuItem);
+ treeItem.ContextMenu.Items.Add(disableMenuItem);
+ treeItem.ContextMenu.Items.Add(openContainingFolderMenuItem);
+ treeItem.ContextMenu.Items.Add(duplicateMenuItem);
+ treeItem.ContextMenu.Items.Add(sortMenuItem);
+ treeItem.PreviewMouseRightButtonDown += (_, e) =>
+ {
+ treeItem.IsSelected = true;
+ e.Handled = false;
+ };
+
foreach (LauncherItem child in item.Children)
{
treeItem.Items.Add(CreateTreeItem(child));
@@ -78,6 +138,22 @@ public partial class MainWindow : Window
return treeItem;
}
+ private static string GetTreeStatusPrefix(LauncherItem item)
+ {
+ List parts = [];
+ if (item.IsFavorite)
+ {
+ parts.Add("Pinned");
+ }
+
+ if (item.IsDisabled)
+ {
+ parts.Add("Disabled");
+ }
+
+ return parts.Count == 0 ? "" : $"[{string.Join(", ", parts)}] ";
+ }
+
private bool SelectTreeItem(LauncherItem item)
{
foreach (object root in ItemsTree.Items)
@@ -114,6 +190,11 @@ public partial class MainWindow : Window
private void ItemsTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs
visibleLaunchButtons = [];
+ private readonly List visibleNavigationControls = [];
private readonly LauncherConfig config;
private readonly Action launchItem;
+ private readonly Action openAllItems;
private readonly Action showSettings;
+ private readonly Action editItem;
+ private readonly Action openContainingFolder;
+ private sealed record SearchMatch(LauncherItem Item, string ParentPath);
- public LauncherPopup(LauncherConfig config, Action launchItem, Action showSettings)
+ public LauncherPopup(LauncherConfig config, Action launchItem, Action openAllItems, Action showSettings, Action editItem, Action openContainingFolder)
{
InitializeComponent();
this.config = config;
this.launchItem = launchItem;
+ this.openAllItems = openAllItems;
this.showSettings = showSettings;
+ this.editItem = editItem;
+ this.openContainingFolder = openContainingFolder;
BuildItems();
SizeChanged += (_, _) => QueueReposition();
}
@@ -89,28 +97,66 @@ public partial class LauncherPopup : Window
{
ItemsPanel.Children.Clear();
visibleLaunchButtons.Clear();
+ visibleNavigationControls.Clear();
+
+ if (config.RecentItems.Any(item => !item.IsDisabled))
+ {
+ ItemsPanel.Children.Add(CreateRecentItemsControl());
+ }
+
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));
}
+
+ 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)
{
Expander expander = new()
{
- Header = item.Title,
+ Header = CreateMenuHeader(item),
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();
- 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;
@@ -122,27 +168,164 @@ public partial class LauncherPopup : Window
Content = CreateButtonContent(item),
Margin = new Thickness(depth * 12, 2, 0, 2),
MinHeight = DisplayModeMetrics.GetRowHeight(GetEffectiveDisplayMode(item)),
- Tag = item
+ HorizontalContentAlignment = System.Windows.HorizontalAlignment.Left,
+ Tag = item,
+ ToolTip = CreateItemToolTip(item)
};
button.Click += (_, _) => launchItem(item);
- button.KeyDown += (_, e) =>
+ if (allowEdit)
{
- if (e.Key == Key.Enter)
- {
- launchItem(item);
- e.Handled = true;
- }
- else if (e.Key == Key.Escape)
- {
- Close();
- e.Handled = true;
- }
- };
- visibleLaunchButtons.Add(button);
+ button.ContextMenu = CreateItemContextMenu(item);
+ }
+
+ 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();
+ e.Handled = true;
+ };
+
+ 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 GetDisplayItems(IEnumerable items)
+ {
+ return items
+ .Where(item => !item.IsDisabled)
+ .OrderByDescending(item => item.IsFavorite);
+ }
+
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
{
string query = SearchBox.Text.Trim();
@@ -163,8 +346,8 @@ public partial class LauncherPopup : Window
ItemsPanel.Children.Clear();
visibleLaunchButtons.Clear();
- List matches = [];
- CollectMatches(config.Items, query, matches);
+ List matches = [];
+ CollectMatches(config.Items, query, [], matches);
if (matches.Count == 0)
{
@@ -177,34 +360,179 @@ public partial class LauncherPopup : Window
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 items, string query, List 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;
- if (isLaunchable && Matches(item, query))
+ panel.Children.Add(new TextBlock
{
- 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 items, string query, List parentNames, List 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)
{
- 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) ||
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)
@@ -223,14 +551,142 @@ public partial class LauncherPopup : Window
return;
}
- if (e.Key == Key.Down && visibleLaunchButtons.FirstOrDefault() is System.Windows.Controls.Button firstButton)
+ if (e.Key == Key.Down)
{
- Keyboard.ClearFocus();
- firstButton.Focus();
+ FocusNavigationControl(0);
+ 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;
}
}
+ 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)
{
if (e.Key == Key.Escape)
diff --git a/config.example.json b/config.example.json
index 93055a7..1d5d1a9 100644
--- a/config.example.json
+++ b/config.example.json
@@ -1,13 +1,21 @@
{
"Hotkey": "Ctrl+Alt+Space",
"DisplayMode": "CompactList",
+ "ThemeScheme": "MarkHaven",
+ "RecentItems": [],
"Items": [
{
"Title": "Folders",
"Type": "Menu",
"Target": "",
"Arguments": null,
+ "WorkingFolder": null,
+ "Notes": null,
"IconPath": null,
+ "IsFavorite": false,
+ "IsDisabled": false,
+ "RunAsAdmin": false,
+ "StartMinimized": false,
"DisplayMode": "CompactList",
"Children": [
{
@@ -15,7 +23,13 @@
"Type": "Folder",
"Target": "%USERPROFILE%\\Documents",
"Arguments": null,
+ "WorkingFolder": null,
+ "Notes": null,
"IconPath": "%USERPROFILE%\\Documents",
+ "IsFavorite": false,
+ "IsDisabled": false,
+ "RunAsAdmin": false,
+ "StartMinimized": false,
"DisplayMode": "CompactList",
"Children": []
},
@@ -24,7 +38,13 @@
"Type": "Folder",
"Target": "%USERPROFILE%\\Downloads",
"Arguments": null,
+ "WorkingFolder": null,
+ "Notes": null,
"IconPath": "%USERPROFILE%\\Downloads",
+ "IsFavorite": false,
+ "IsDisabled": false,
+ "RunAsAdmin": false,
+ "StartMinimized": false,
"DisplayMode": "CompactList",
"Children": []
}
@@ -35,7 +55,13 @@
"Type": "Menu",
"Target": "",
"Arguments": null,
+ "WorkingFolder": null,
+ "Notes": null,
"IconPath": null,
+ "IsFavorite": false,
+ "IsDisabled": false,
+ "RunAsAdmin": false,
+ "StartMinimized": false,
"DisplayMode": "CompactList",
"Children": [
{
@@ -43,7 +69,13 @@
"Type": "App",
"Target": "notepad.exe",
"Arguments": null,
+ "WorkingFolder": null,
+ "Notes": "Simple starter app entry.",
"IconPath": "notepad.exe",
+ "IsFavorite": false,
+ "IsDisabled": false,
+ "RunAsAdmin": false,
+ "StartMinimized": false,
"DisplayMode": "CompactList",
"Children": []
}
diff --git a/docs/DEV-NOTES.md b/docs/DEV-NOTES.md
new file mode 100644
index 0000000..2d15ba8
--- /dev/null
+++ b/docs/DEV-NOTES.md
@@ -0,0 +1,47 @@
+# Developer Notes
+
+This document is for the `dev` branch only. Do not publish it on `main`.
+
+## Screenshot and Testing Startup Switches
+
+Taskbar Launcher supports a few command-line switches to open specific UI surfaces for documentation screenshots and manual testing:
+
+```powershell
+TaskbarLauncher.exe --show-settings
+TaskbarLauncher.exe --show-about
+TaskbarLauncher.exe --show-launcher
+```
+
+- `--show-settings` opens the Settings window after startup.
+- `--show-about` opens the About window after startup.
+- `--show-launcher` opens the launcher popup after startup.
+
+These switches are not part of the normal user workflow. They exist so development and documentation captures can be repeated cleanly.
+
+## Future Ideas
+
+These ideas are intentionally tracked on `dev` only. Do not publish them on `main` until a feature is built, tested, and ready for users.
+
+### Open All for Menu Sections
+
+Add a simple right-click action on menu and submenu headers:
+
+- `Open All`
+
+Expected behavior:
+
+- Right-click a top-level menu or submenu title.
+- Click `Open All`.
+- Launch each direct child item in that section.
+- For nested submenus, decide later whether `Open All` should include only direct items or recursively include nested children.
+- Skip unsupported items safely and show/log a clear result if something cannot be opened.
+
+This keeps Taskbar Launcher focused as a launcher. It gives project-style behavior without adding a full project dashboard.
+
+### Project Launcher Concept
+
+Parked for later as a possible personal/custom feature:
+
+- Project item type with actions for folder, VS Code, terminal, docs, Git/Gitea page, local server, Docker Compose, and helper scripts.
+- Could become useful, but it is larger than the current public app direction.
+- Do not start this until the simpler `Open All` behavior is tested and proves insufficient.
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
index 311e61b..d0ee5ac 100644
--- a/docs/INSTALL.md
+++ b/docs/INSTALL.md
@@ -1,5 +1,7 @@
# Install and Use Taskbar Launcher
+Current stable release: `v1.0.2`
+
## Small Package
The small package requires:
@@ -19,6 +21,18 @@ Install the **.NET Desktop Runtime** for **Windows x64**.
2. Run `TaskbarLauncher.exe`.
3. The app appears in the system tray.
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
@@ -30,6 +44,17 @@ From Settings you can:
- Add submenus
- Add folders, apps, files, and websites
- 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
- Back up and restore `config.json`
- Enable Start with Windows
diff --git a/docs/RELEASE.md b/docs/RELEASE.md
index 7ace77b..3eb8018 100644
--- a/docs/RELEASE.md
+++ b/docs/RELEASE.md
@@ -21,6 +21,7 @@ Examples:
- `1.0.0`: first stable release
- `1.0.1`: bug fix
+- `1.0.2`: tested feature and polish release
- `1.1.0`: new feature
Update version fields in:
@@ -37,9 +38,9 @@ TaskbarLauncher.csproj
4. Tag the release:
```powershell
-git tag v1.0.0
+git tag v1.0.2
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.
diff --git a/docs/screenshots/about-window-exact.png b/docs/screenshots/about-window-exact.png
index e6afb99..6157368 100644
Binary files a/docs/screenshots/about-window-exact.png and b/docs/screenshots/about-window-exact.png differ
diff --git a/docs/screenshots/launcher-popup-exact.png b/docs/screenshots/launcher-popup-exact.png
index 1e94d5c..3b86822 100644
Binary files a/docs/screenshots/launcher-popup-exact.png and b/docs/screenshots/launcher-popup-exact.png differ
diff --git a/docs/screenshots/settings-window-exact.png b/docs/screenshots/settings-window-exact.png
index 794b2be..7f04cd5 100644
Binary files a/docs/screenshots/settings-window-exact.png and b/docs/screenshots/settings-window-exact.png differ