2 Commits

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

View File

@@ -14,6 +14,7 @@ Download the compiled Windows release from:
- Global hotkey, default `Ctrl+Alt+Space`
- Search/filter in the launcher popup with parent menu context
- 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

View File

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

View File

@@ -14,6 +14,7 @@ public partial class LauncherPopup : Window
private const double HeaderAndChromeHeight = 118;
private bool isRepositionQueued;
private readonly List<System.Windows.Controls.Button> visibleLaunchButtons = [];
private readonly List<System.Windows.Controls.Control> visibleNavigationControls = [];
private readonly LauncherConfig config;
private readonly Action<LauncherItem> launchItem;
@@ -96,6 +97,7 @@ public partial class LauncherPopup : Window
{
ItemsPanel.Children.Clear();
visibleLaunchButtons.Clear();
visibleNavigationControls.Clear();
if (config.RecentItems.Any(item => !item.IsDisabled))
{
@@ -107,6 +109,8 @@ public partial class LauncherPopup : Window
{
ItemsPanel.Children.Add(CreateItemControl(item, 0, expandMenusByDefault));
}
RebuildNavigationLists();
}
private Expander CreateRecentItemsControl()
@@ -115,8 +119,12 @@ public partial class LauncherPopup : Window
{
Header = new TextBlock { Text = "Recent" },
IsExpanded = true,
Margin = new Thickness(0, 4, 0, 8)
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))
@@ -136,8 +144,14 @@ public partial class LauncherPopup : Window
{
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 GetDisplayItems(item.Children))
@@ -155,7 +169,8 @@ public partial class LauncherPopup : Window
Margin = new Thickness(depth * 12, 2, 0, 2),
MinHeight = DisplayModeMetrics.GetRowHeight(GetEffectiveDisplayMode(item)),
HorizontalContentAlignment = System.Windows.HorizontalAlignment.Left,
Tag = item
Tag = item,
ToolTip = CreateItemToolTip(item)
};
button.Click += (_, _) => launchItem(item);
@@ -165,7 +180,6 @@ public partial class LauncherPopup : Window
}
button.KeyDown += LaunchButton_KeyDown;
visibleLaunchButtons.Add(button);
return button;
}
@@ -192,7 +206,8 @@ public partial class LauncherPopup : Window
return new TextBlock
{
Text = item.Title,
ContextMenu = contextMenu
ContextMenu = contextMenu,
ToolTip = CreateItemToolTip(item)
};
}
@@ -329,6 +344,8 @@ public partial class LauncherPopup : Window
{
ItemsPanel.Children.Add(CreateSearchResultControl(match));
}
RebuildNavigationLists();
}
private FrameworkElement CreateSearchResultControl(SearchMatch match)
@@ -340,16 +357,56 @@ public partial class LauncherPopup : Window
MinHeight = Math.Max(DisplayModeMetrics.GetRowHeight(GetEffectiveDisplayMode(match.Item)), 48),
HorizontalContentAlignment = System.Windows.HorizontalAlignment.Left,
Tag = match.Item,
ContextMenu = CreateItemContextMenu(match.Item)
ContextMenu = CreateItemContextMenu(match.Item),
ToolTip = CreateItemToolTip(match.Item, match.ParentPath)
};
button.Click += (_, _) => launchItem(match.Item);
button.KeyDown += LaunchButton_KeyDown;
visibleLaunchButtons.Add(button);
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 object CreateSearchResultContent(SearchMatch match)
{
StackPanel panel = new();
@@ -370,6 +427,46 @@ public partial class LauncherPopup : Window
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 (item.IsFavorite)
{
AddToolTipLine(panel, "Pinned to top");
}
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))
@@ -415,14 +512,14 @@ public partial class LauncherPopup : Window
if (e.Key == Key.Down)
{
FocusLaunchButton(0);
FocusNavigationControl(0);
e.Handled = true;
return;
}
if (e.Key == Key.Up)
{
FocusLaunchButton(visibleLaunchButtons.Count - 1);
FocusNavigationControl(visibleNavigationControls.Count - 1);
e.Handled = true;
return;
}
@@ -462,7 +559,37 @@ public partial class LauncherPopup : Window
return;
}
int index = visibleLaunchButtons.IndexOf(button);
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;
@@ -470,7 +597,7 @@ public partial class LauncherPopup : Window
if (e.Key == Key.Down)
{
FocusLaunchButton(index + 1);
FocusNavigationControl(index + 1);
e.Handled = true;
return;
}
@@ -484,7 +611,7 @@ public partial class LauncherPopup : Window
}
else
{
FocusLaunchButton(index - 1);
FocusNavigationControl(index - 1);
}
e.Handled = true;
@@ -493,30 +620,30 @@ public partial class LauncherPopup : Window
if (e.Key == Key.Home)
{
FocusLaunchButton(0);
FocusNavigationControl(0);
e.Handled = true;
return;
}
if (e.Key == Key.End)
{
FocusLaunchButton(visibleLaunchButtons.Count - 1);
FocusNavigationControl(visibleNavigationControls.Count - 1);
e.Handled = true;
}
}
private void FocusLaunchButton(int index)
private void FocusNavigationControl(int index)
{
if (visibleLaunchButtons.Count == 0)
if (visibleNavigationControls.Count == 0)
{
return;
}
int clampedIndex = Math.Clamp(index, 0, visibleLaunchButtons.Count - 1);
System.Windows.Controls.Button button = visibleLaunchButtons[clampedIndex];
int clampedIndex = Math.Clamp(index, 0, visibleNavigationControls.Count - 1);
System.Windows.Controls.Control control = visibleNavigationControls[clampedIndex];
Keyboard.ClearFocus();
button.Focus();
button.BringIntoView();
control.Focus();
control.BringIntoView();
}
protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)

View File

@@ -30,6 +30,8 @@ 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
Open the tray menu and choose **Settings**.