9 Commits

Author SHA1 Message Date
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
10 changed files with 623 additions and 15 deletions

View File

@@ -60,6 +60,8 @@
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/>

View File

@@ -22,6 +22,7 @@ public partial class MainWindow : Window
private List<int>? selectedPath;
private string? loadedAutoIconPath;
private System.Windows.Point dragStartPoint;
private sealed record TargetValidationIssue(string ItemPath, string Problem);
public MainWindow()
{
@@ -40,6 +41,18 @@ public partial class MainWindow : Window
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)
@@ -70,9 +83,46 @@ 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 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.PreviewMouseRightButtonDown += (_, e) =>
{
treeItem.IsSelected = true;
e.Handled = false;
};
foreach (LauncherItem child in item.Children)
{
treeItem.Items.Add(CreateTreeItem(child));
@@ -81,6 +131,22 @@ public partial class MainWindow : Window
return treeItem;
}
private static string GetTreeStatusPrefix(LauncherItem item)
{
List<string> 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)
@@ -776,6 +842,85 @@ public partial class MainWindow : Window
StatusText.Text = "Added submenu. Rename it, then Save.";
}
private void ToggleFavorite_Click(object sender, RoutedEventArgs e)
{
if (controller is null || selectedItem is null)
{
return;
}
ApplyCurrentItem();
selectedItem.IsFavorite = !selectedItem.IsFavorite;
LauncherItem itemToSelect = selectedItem;
selectedPath = FindItemPath(itemToSelect);
RefreshView(itemToSelect);
StatusText.Text = itemToSelect.IsFavorite
? $"Pinned {itemToSelect.Title}. Click Save to keep it."
: $"Unpinned {itemToSelect.Title}. Click Save to keep it.";
}
private void ToggleDisabled_Click(object sender, RoutedEventArgs e)
{
if (controller is null || selectedItem is null)
{
return;
}
ApplyCurrentItem();
selectedItem.IsDisabled = !selectedItem.IsDisabled;
LauncherItem itemToSelect = selectedItem;
selectedPath = FindItemPath(itemToSelect);
RefreshView(itemToSelect);
StatusText.Text = itemToSelect.IsDisabled
? $"Disabled {itemToSelect.Title}. Click Save to keep it."
: $"Enabled {itemToSelect.Title}. Click Save to keep it.";
}
private void OpenContainingFolder_Click(object sender, RoutedEventArgs e)
{
if (selectedItem is null)
{
return;
}
ApplyCurrentItem();
if (!LauncherController.OpenContainingFolder(selectedItem, out string? error) && !string.IsNullOrWhiteSpace(error))
{
MessageBox.Show(error, "Taskbar Launcher");
}
}
private static bool CanOpenContainingFolder(LauncherItem item)
{
return item.Type is LauncherItemType.App or LauncherItemType.File && !string.IsNullOrWhiteSpace(item.Target);
}
private void DuplicateItem_Click(object sender, RoutedEventArgs e)
{
if (controller is null || selectedItem is null)
{
return;
}
ApplyCurrentItem();
string originalTitle = selectedItem.Title;
LauncherItem duplicate = DuplicateItem(selectedItem);
List<LauncherItem>? siblings = FindSiblings(controller.Config.Items, selectedItem);
if (siblings is null)
{
controller.Config.Items.Add(duplicate);
}
else
{
int selectedIndex = siblings.IndexOf(selectedItem);
siblings.Insert(selectedIndex + 1, duplicate);
}
selectedPath = FindItemPath(duplicate);
RefreshView(duplicate);
StatusText.Text = $"Duplicated {originalTitle}. Click Save to keep it.";
}
private static LauncherItem CreateNewItem(LauncherDisplayMode displayMode)
{
return new LauncherItem
@@ -798,6 +943,34 @@ public partial class MainWindow : Window
};
}
private static LauncherItem DuplicateItem(LauncherItem item)
{
return DuplicateItem(item, renameRoot: true);
}
private static LauncherItem DuplicateItem(LauncherItem item, bool renameRoot)
{
return new LauncherItem
{
Title = renameRoot ? GetDuplicateTitle(item.Title) : item.Title,
Type = item.Type,
Target = item.Target,
Arguments = item.Arguments,
IconPath = item.IconPath,
IsFavorite = item.IsFavorite,
IsDisabled = item.IsDisabled,
DisplayMode = item.DisplayMode,
Children = item.Children.Select(child => DuplicateItem(child, renameRoot: false)).ToList()
};
}
private static string GetDuplicateTitle(string title)
{
return string.IsNullOrWhiteSpace(title)
? "Copy"
: $"{title} Copy";
}
private void Delete_Click(object sender, RoutedEventArgs e)
{
if (controller is null || selectedItem is null)
@@ -1171,6 +1344,159 @@ public partial class MainWindow : Window
ConfigService.OpenConfigFolder();
}
private void ValidateTargets_Click(object sender, RoutedEventArgs e)
{
if (controller is null)
{
return;
}
ApplyConfigFields();
List<TargetValidationIssue> issues = [];
ValidateTargets(controller.Config.Items, [], issues);
if (issues.Count == 0)
{
StatusText.Text = "Validation complete. No broken targets found.";
MessageBox.Show("No broken targets found.", "Taskbar Launcher");
return;
}
string message = $"Found {issues.Count} possible broken target(s):\n\n" +
string.Join("\n", issues.Take(20).Select(issue => $"- {issue.ItemPath}: {issue.Problem}"));
if (issues.Count > 20)
{
message += $"\n\nShowing first 20 of {issues.Count}.";
}
StatusText.Text = $"Validation found {issues.Count} possible broken target(s).";
MessageBox.Show(message, "Taskbar Launcher");
}
private static void ValidateTargets(IEnumerable<LauncherItem> items, List<string> parentNames, List<TargetValidationIssue> issues)
{
foreach (LauncherItem item in items)
{
List<string> itemPath = [.. parentNames, string.IsNullOrWhiteSpace(item.Title) ? "Untitled" : item.Title];
if (!item.IsDisabled && TryGetTargetIssue(item, out string? problem) && problem is not null)
{
issues.Add(new TargetValidationIssue(string.Join(" > ", itemPath), problem));
}
if (item.Children.Count > 0)
{
ValidateTargets(item.Children, itemPath, issues);
}
}
}
private static bool TryGetTargetIssue(LauncherItem item, out string? problem)
{
problem = null;
if (item.Type == LauncherItemType.Menu || item.IsDisabled)
{
return false;
}
if (string.IsNullOrWhiteSpace(item.Target))
{
problem = "Target is blank.";
return true;
}
string target = Environment.ExpandEnvironmentVariables(item.Target.Trim());
switch (item.Type)
{
case LauncherItemType.Folder:
if (!Directory.Exists(target))
{
problem = "Folder does not exist.";
return true;
}
return false;
case LauncherItemType.Website:
if (!Uri.TryCreate(target, UriKind.Absolute, out Uri? uri) ||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
{
problem = "Website target is not a valid http or https URL.";
return true;
}
return false;
case LauncherItemType.App:
if (!File.Exists(target) && !CommandExistsOnPath(target))
{
problem = "Application or shortcut was not found.";
return true;
}
return false;
case LauncherItemType.File:
if (!File.Exists(target))
{
problem = "File does not exist.";
return true;
}
return false;
default:
problem = "Item type is not recognized.";
return true;
}
}
private static bool CommandExistsOnPath(string target)
{
if (target.IndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]) >= 0)
{
return false;
}
string? pathValue = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrWhiteSpace(pathValue))
{
return false;
}
string extension = Path.GetExtension(target);
IEnumerable<string> candidateNames = string.IsNullOrWhiteSpace(extension)
? GetExecutableCandidateNames(target)
: [target];
foreach (string directory in pathValue.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
foreach (string candidateName in candidateNames)
{
string candidatePath = Path.Combine(directory, candidateName);
if (File.Exists(candidatePath))
{
return true;
}
}
}
return false;
}
private static IEnumerable<string> GetExecutableCandidateNames(string target)
{
string? pathExtValue = Environment.GetEnvironmentVariable("PATHEXT");
string[] extensions = string.IsNullOrWhiteSpace(pathExtValue)
? [".EXE", ".CMD", ".BAT", ".COM"]
: pathExtValue.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (string extension in extensions)
{
yield return target + extension;
}
}
private void About_Click(object sender, RoutedEventArgs e)
{
AboutWindow aboutWindow = new()
@@ -1377,6 +1703,8 @@ public partial class MainWindow : Window
Target = TargetBox.Text.Trim(),
Arguments = string.IsNullOrWhiteSpace(ArgumentsBox.Text) ? null : ArgumentsBox.Text.Trim(),
IconPath = string.IsNullOrWhiteSpace(IconPathBox.Text) ? null : IconPathBox.Text.Trim(),
IsFavorite = selectedItem.IsFavorite,
IsDisabled = selectedItem.IsDisabled,
DisplayMode = ItemDisplayModeBox.SelectedItem is LauncherDisplayMode mode
? mode
: selectedItem.DisplayMode

View File

@@ -8,6 +8,8 @@ public sealed class LauncherConfig
public LauncherThemeScheme ThemeScheme { get; set; } = LauncherThemeScheme.MarkHaven;
public List<LauncherItem> RecentItems { get; set; } = [];
public List<LauncherItem> Items { get; set; } = [];
}
@@ -23,6 +25,10 @@ public sealed class LauncherItem
public string? IconPath { get; set; }
public bool IsFavorite { get; set; }
public bool IsDisabled { get; set; }
public LauncherDisplayMode DisplayMode { get; set; } = LauncherDisplayMode.CompactList;
public List<LauncherItem> Children { get; set; } = [];

View File

@@ -13,7 +13,15 @@ Download the compiled Windows release from:
- System tray launcher
- Global hotkey, default `Ctrl+Alt+Space`
- Search/filter in the launcher popup
- 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
- Validate missing app, file, and folder targets from Settings
- 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
- Automatic icon selection from app/folder targets
- Portable `config.json`

View File

@@ -190,10 +190,20 @@ public static class ConfigService
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)
@@ -226,6 +236,16 @@ public static class ConfigService
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)
{
NormalizeDisplayModes(item);

View File

@@ -9,6 +9,8 @@ namespace TaskbarLauncher.Services;
public sealed class LauncherController : IDisposable
{
private const int MaxRecentItems = 8;
private readonly HotkeyService hotkeyService = new();
private NotifyIcon? notifyIcon;
private LauncherPopup? popup;
@@ -41,6 +43,12 @@ public sealed class LauncherController : IDisposable
settingsWindow.Activate();
}
public void ShowSettings(LauncherItem itemToEdit)
{
settingsWindow ??= new MainWindow(this);
settingsWindow.SelectItemForEdit(itemToEdit);
}
public void ShowAbout()
{
if (aboutWindow?.IsVisible == true)
@@ -64,7 +72,7 @@ public sealed class LauncherController : IDisposable
return;
}
popup = new LauncherPopup(Config, LaunchItem, OpenAllItems, ShowSettings);
popup = new LauncherPopup(Config, LaunchItem, OpenAllItems, ShowSettings, ShowSettings, OpenContainingFolderFromPopup);
popup.Closed += (_, _) => popup = null;
popup.ShowNearTaskbar();
}
@@ -130,6 +138,7 @@ public sealed class LauncherController : IDisposable
{
if (TryLaunchItem(item, out string? error))
{
RecordRecentItem(item);
popup?.Close();
return;
}
@@ -140,6 +149,45 @@ public sealed class LauncherController : IDisposable
}
}
private void RecordRecentItem(LauncherItem item)
{
if (item.Type == LauncherItemType.Menu || item.IsDisabled || string.IsNullOrWhiteSpace(item.Target))
{
return;
}
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,
IconPath = item.IconPath,
IsFavorite = false,
IsDisabled = false,
DisplayMode = item.DisplayMode,
Children = []
};
}
private void OpenAllItems(LauncherItem menuItem)
{
List<string> failures = [];
@@ -147,7 +195,7 @@ public sealed class LauncherController : IDisposable
foreach (LauncherItem child in menuItem.Children)
{
if (child.Type == LauncherItemType.Menu || string.IsNullOrWhiteSpace(child.Target))
if (child.Type == LauncherItemType.Menu || child.IsDisabled || string.IsNullOrWhiteSpace(child.Target))
{
continue;
}
@@ -155,6 +203,7 @@ public sealed class LauncherController : IDisposable
if (TryLaunchItem(child, out string? error))
{
launchedCount++;
RecordRecentItem(child);
}
else if (!string.IsNullOrWhiteSpace(error))
{
@@ -172,7 +221,15 @@ public sealed class LauncherController : IDisposable
}
}
private static bool TryLaunchItem(LauncherItem item, out string? error)
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;
@@ -181,6 +238,42 @@ public sealed class LauncherController : IDisposable
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);
try

View File

@@ -12,8 +12,8 @@
<Authors>Ray Berezowski</Authors>
<ApplicationIcon>Assets\AppIcon.ico</ApplicationIcon>
<Version>1.0.1</Version>
<AssemblyVersion>1.0.1.2</AssemblyVersion>
<FileVersion>1.0.1.2</FileVersion>
<AssemblyVersion>1.0.1.11</AssemblyVersion>
<FileVersion>1.0.1.11</FileVersion>
<InformationalVersion>1.0.1</InformationalVersion>
</PropertyGroup>

View File

@@ -19,14 +19,18 @@ public partial class LauncherPopup : Window
private readonly Action<LauncherItem> launchItem;
private readonly Action<LauncherItem> openAllItems;
private readonly Action showSettings;
private readonly Action<LauncherItem> editItem;
private readonly Action<LauncherItem> openContainingFolder;
public LauncherPopup(LauncherConfig config, Action<LauncherItem> launchItem, Action<LauncherItem> openAllItems, Action showSettings)
public LauncherPopup(LauncherConfig config, Action<LauncherItem> launchItem, Action<LauncherItem> openAllItems, Action showSettings, Action<LauncherItem> editItem, Action<LauncherItem> openContainingFolder)
{
InitializeComponent();
this.config = config;
this.launchItem = launchItem;
this.openAllItems = openAllItems;
this.showSettings = showSettings;
this.editItem = editItem;
this.openContainingFolder = openContainingFolder;
BuildItems();
SizeChanged += (_, _) => QueueReposition();
}
@@ -91,14 +95,39 @@ public partial class LauncherPopup : Window
{
ItemsPanel.Children.Clear();
visibleLaunchButtons.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));
}
}
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)
};
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)
{
@@ -110,9 +139,9 @@ public partial class LauncherPopup : Window
};
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;
@@ -129,6 +158,11 @@ public partial class LauncherPopup : Window
};
button.Click += (_, _) => launchItem(item);
if (allowEdit)
{
button.ContextMenu = CreateItemContextMenu(item);
}
button.KeyDown += (_, e) =>
{
if (e.Key == Key.Enter)
@@ -161,6 +195,10 @@ public partial class LauncherPopup : Window
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
{
@@ -169,9 +207,99 @@ public partial class LauncherPopup : Window
};
}
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(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 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 && !string.IsNullOrWhiteSpace(item.Target);
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)
@@ -216,9 +344,9 @@ public partial class LauncherPopup : Window
private static void CollectMatches(IEnumerable<LauncherItem> items, string query, List<LauncherItem> matches)
{
foreach (LauncherItem item in items)
foreach (LauncherItem item in GetDisplayItems(items))
{
bool isLaunchable = item.Type != LauncherItemType.Menu;
bool isLaunchable = item.Type != LauncherItemType.Menu && !item.IsDisabled;
if (isLaunchable && Matches(item, query))
{
matches.Add(item);

View File

@@ -1,6 +1,8 @@
{
"Hotkey": "Ctrl+Alt+Space",
"DisplayMode": "CompactList",
"ThemeScheme": "MarkHaven",
"RecentItems": [],
"Items": [
{
"Title": "Folders",
@@ -8,6 +10,8 @@
"Target": "",
"Arguments": null,
"IconPath": null,
"IsFavorite": false,
"IsDisabled": false,
"DisplayMode": "CompactList",
"Children": [
{
@@ -16,6 +20,8 @@
"Target": "%USERPROFILE%\\Documents",
"Arguments": null,
"IconPath": "%USERPROFILE%\\Documents",
"IsFavorite": false,
"IsDisabled": false,
"DisplayMode": "CompactList",
"Children": []
},
@@ -25,6 +31,8 @@
"Target": "%USERPROFILE%\\Downloads",
"Arguments": null,
"IconPath": "%USERPROFILE%\\Downloads",
"IsFavorite": false,
"IsDisabled": false,
"DisplayMode": "CompactList",
"Children": []
}
@@ -36,6 +44,8 @@
"Target": "",
"Arguments": null,
"IconPath": null,
"IsFavorite": false,
"IsDisabled": false,
"DisplayMode": "CompactList",
"Children": [
{
@@ -44,6 +54,8 @@
"Target": "notepad.exe",
"Arguments": null,
"IconPath": "notepad.exe",
"IsFavorite": false,
"IsDisabled": false,
"DisplayMode": "CompactList",
"Children": []
}

View File

@@ -19,6 +19,12 @@ 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**.
## Configure
@@ -31,6 +37,11 @@ From Settings you can:
- 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**
- Use **Config... > Validate Targets** to check for missing app, file, and folder targets
- Set per-item icon sizes
- Back up and restore `config.json`
- Enable Start with Windows