2 Commits

Author SHA1 Message Date
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
7 changed files with 260 additions and 4 deletions

View File

@@ -69,6 +69,8 @@
<MenuItem Header="Reset to Defaults" Click="ResetConfig_Click"/> <MenuItem Header="Reset to Defaults" Click="ResetConfig_Click"/>
<MenuItem Header="Reload Config" Click="ReloadConfig_Click"/> <MenuItem Header="Reload Config" Click="ReloadConfig_Click"/>
<MenuItem Header="Open Config Folder" Click="OpenConfigFolder_Click"/> <MenuItem Header="Open Config Folder" Click="OpenConfigFolder_Click"/>
<Separator/>
<MenuItem Header="Validate Targets" Click="ValidateTargets_Click"/>
</ContextMenu> </ContextMenu>
</Button.ContextMenu> </Button.ContextMenu>
</Button> </Button>

View File

@@ -22,6 +22,7 @@ public partial class MainWindow : Window
private List<int>? selectedPath; private List<int>? selectedPath;
private string? loadedAutoIconPath; private string? loadedAutoIconPath;
private System.Windows.Point dragStartPoint; private System.Windows.Point dragStartPoint;
private sealed record TargetValidationIssue(string ItemPath, string Problem);
public MainWindow() public MainWindow()
{ {
@@ -96,6 +97,13 @@ public partial class MainWindow : Window
}; };
disableMenuItem.Click += ToggleDisabled_Click; 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" }; var duplicateMenuItem = new System.Windows.Controls.MenuItem { Header = "Duplicate Item" };
duplicateMenuItem.Click += DuplicateItem_Click; duplicateMenuItem.Click += DuplicateItem_Click;
@@ -107,6 +115,7 @@ public partial class MainWindow : Window
}; };
treeItem.ContextMenu.Items.Add(pinMenuItem); treeItem.ContextMenu.Items.Add(pinMenuItem);
treeItem.ContextMenu.Items.Add(disableMenuItem); treeItem.ContextMenu.Items.Add(disableMenuItem);
treeItem.ContextMenu.Items.Add(openContainingFolderMenuItem);
treeItem.ContextMenu.Items.Add(duplicateMenuItem); treeItem.ContextMenu.Items.Add(duplicateMenuItem);
treeItem.PreviewMouseRightButtonDown += (_, e) => treeItem.PreviewMouseRightButtonDown += (_, e) =>
{ {
@@ -867,6 +876,25 @@ public partial class MainWindow : Window
: $"Enabled {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) private void DuplicateItem_Click(object sender, RoutedEventArgs e)
{ {
if (controller is null || selectedItem is null) if (controller is null || selectedItem is null)
@@ -1316,6 +1344,159 @@ public partial class MainWindow : Window
ConfigService.OpenConfigFolder(); 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) private void About_Click(object sender, RoutedEventArgs e)
{ {
AboutWindow aboutWindow = new() AboutWindow aboutWindow = new()

View File

@@ -18,6 +18,8 @@ Download the compiled Windows release from:
- Right-click launcher entries to edit them 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 to pin or unpin them from the top
- Right-click launcher entries or Settings items to temporarily disable them - 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 item duplication in Settings
- Right-click items in Settings to pin them to the top of their menu - 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

View File

@@ -72,7 +72,7 @@ public sealed class LauncherController : IDisposable
return; return;
} }
popup = new LauncherPopup(Config, LaunchItem, OpenAllItems, ShowSettings, ShowSettings); popup = new LauncherPopup(Config, LaunchItem, OpenAllItems, ShowSettings, ShowSettings, OpenContainingFolderFromPopup);
popup.Closed += (_, _) => popup = null; popup.Closed += (_, _) => popup = null;
popup.ShowNearTaskbar(); popup.ShowNearTaskbar();
} }
@@ -221,6 +221,50 @@ public sealed class LauncherController : IDisposable
} }
} }
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) private static bool TryLaunchItem(LauncherItem item, out string? error)
{ {
error = null; error = null;

View File

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

View File

@@ -20,8 +20,9 @@ public partial class LauncherPopup : Window
private readonly Action<LauncherItem> openAllItems; private readonly Action<LauncherItem> openAllItems;
private readonly Action showSettings; private readonly Action showSettings;
private readonly Action<LauncherItem> editItem; private readonly Action<LauncherItem> editItem;
private readonly Action<LauncherItem> openContainingFolder;
public LauncherPopup(LauncherConfig config, Action<LauncherItem> launchItem, Action<LauncherItem> openAllItems, Action showSettings, Action<LauncherItem> editItem) 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;
@@ -29,6 +30,7 @@ public partial class LauncherPopup : Window
this.openAllItems = openAllItems; this.openAllItems = openAllItems;
this.showSettings = showSettings; this.showSettings = showSettings;
this.editItem = editItem; this.editItem = editItem;
this.openContainingFolder = openContainingFolder;
BuildItems(); BuildItems();
SizeChanged += (_, _) => QueueReposition(); SizeChanged += (_, _) => QueueReposition();
} }
@@ -210,6 +212,7 @@ public partial class LauncherPopup : Window
System.Windows.Controls.ContextMenu contextMenu = new(); System.Windows.Controls.ContextMenu contextMenu = new();
contextMenu.Items.Add(CreatePinMenuItem(item)); contextMenu.Items.Add(CreatePinMenuItem(item));
contextMenu.Items.Add(CreateDisableMenuItem(item)); contextMenu.Items.Add(CreateDisableMenuItem(item));
contextMenu.Items.Add(CreateOpenContainingFolderMenuItem(item));
contextMenu.Items.Add(CreateEditMenuItem(item)); contextMenu.Items.Add(CreateEditMenuItem(item));
return contextMenu; return contextMenu;
} }
@@ -250,6 +253,22 @@ public partial class LauncherPopup : Window
return disableMenuItem; 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) private System.Windows.Controls.MenuItem CreateEditMenuItem(LauncherItem item)
{ {
System.Windows.Controls.MenuItem editMenuItem = new() System.Windows.Controls.MenuItem editMenuItem = new()
@@ -271,6 +290,11 @@ public partial class LauncherPopup : Window
return item.Type != LauncherItemType.Menu && !item.IsDisabled && !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) private static IEnumerable<LauncherItem> GetDisplayItems(IEnumerable<LauncherItem> items)
{ {
return items return items

View File

@@ -22,6 +22,7 @@ Install the **.NET Desktop Runtime** for **Windows x64**.
5. Right-click a launcher entry and choose **Edit in Settings** to jump to that item. 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**. 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. 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**. Recently launched entries appear in the launcher popup under **Recent**.
@@ -39,6 +40,8 @@ From Settings you can:
- Right-click an item and choose **Duplicate Item** to copy it - 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 **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 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 - 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