Initial TaskbarLauncher release baseline
This commit is contained in:
36
Views/LauncherPopup.xaml
Normal file
36
Views/LauncherPopup.xaml
Normal file
@@ -0,0 +1,36 @@
|
||||
<Window x:Class="TaskbarLauncher.Views.LauncherPopup"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Taskbar Launcher"
|
||||
Width="360" SizeToContent="Height"
|
||||
MaxHeight="720"
|
||||
WindowStyle="None"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
Background="{DynamicResource AppWindowBrush}"
|
||||
Foreground="{DynamicResource AppTextBrush}">
|
||||
<Border BorderThickness="1"
|
||||
BorderBrush="{DynamicResource AppBorderBrush}"
|
||||
Background="{DynamicResource AppSurfaceBrush}"
|
||||
Padding="10">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<TextBlock Text="Taskbar Launcher" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
<Button Content="Settings" HorizontalAlignment="Right" Margin="12,0,0,0" Padding="8,4"
|
||||
Click="Settings_Click"/>
|
||||
</StackPanel>
|
||||
<TextBox x:Name="SearchBox"
|
||||
DockPanel.Dock="Top"
|
||||
Margin="0,0,0,8"
|
||||
Padding="8,5"
|
||||
TextChanged="SearchBox_TextChanged"
|
||||
PreviewKeyDown="SearchBox_PreviewKeyDown"/>
|
||||
<ScrollViewer x:Name="ItemsScrollViewer"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="ItemsPanel"/>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
331
Views/LauncherPopup.xaml.cs
Normal file
331
Views/LauncherPopup.xaml.cs
Normal file
@@ -0,0 +1,331 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using TaskbarLauncher.Models;
|
||||
using TaskbarLauncher.Services;
|
||||
|
||||
namespace TaskbarLauncher.Views;
|
||||
|
||||
public partial class LauncherPopup : Window
|
||||
{
|
||||
private const double ScreenMargin = 12;
|
||||
private const double HeaderAndChromeHeight = 118;
|
||||
private bool isRepositionQueued;
|
||||
private readonly List<System.Windows.Controls.Button> visibleLaunchButtons = [];
|
||||
|
||||
private readonly LauncherConfig config;
|
||||
private readonly Action<LauncherItem> launchItem;
|
||||
private readonly Action showSettings;
|
||||
|
||||
public LauncherPopup(LauncherConfig config, Action<LauncherItem> launchItem, Action showSettings)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.config = config;
|
||||
this.launchItem = launchItem;
|
||||
this.showSettings = showSettings;
|
||||
BuildItems();
|
||||
SizeChanged += (_, _) => QueueReposition();
|
||||
}
|
||||
|
||||
public void ShowNearTaskbar()
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
ApplyScreenBounds();
|
||||
|
||||
Show();
|
||||
UpdateLayout();
|
||||
RepositionAboveTaskbar();
|
||||
Activate();
|
||||
SearchBox.Focus();
|
||||
}
|
||||
|
||||
private void QueueReposition()
|
||||
{
|
||||
if (isRepositionQueued)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isRepositionQueued = true;
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
isRepositionQueued = false;
|
||||
ApplyScreenBounds();
|
||||
RepositionAboveTaskbar();
|
||||
}, DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void ApplyScreenBounds()
|
||||
{
|
||||
Rect workArea = SystemParameters.WorkArea;
|
||||
double maxPopupHeight = Math.Max(240, workArea.Height - (ScreenMargin * 2));
|
||||
MaxHeight = maxPopupHeight;
|
||||
ItemsScrollViewer.MaxHeight = Math.Max(160, maxPopupHeight - HeaderAndChromeHeight);
|
||||
Left = Math.Max(workArea.Left + ScreenMargin, workArea.Right - Width - ScreenMargin);
|
||||
}
|
||||
|
||||
private void RepositionAboveTaskbar()
|
||||
{
|
||||
Rect workArea = SystemParameters.WorkArea;
|
||||
double height = ActualHeight > 0 ? ActualHeight : DesiredSize.Height;
|
||||
double clampedHeight = Math.Min(height, MaxHeight);
|
||||
|
||||
if (height > MaxHeight && !double.IsNaN(MaxHeight) && MaxHeight > 0)
|
||||
{
|
||||
Height = MaxHeight;
|
||||
clampedHeight = MaxHeight;
|
||||
}
|
||||
else if (Height > 0 && height < MaxHeight)
|
||||
{
|
||||
ClearValue(HeightProperty);
|
||||
}
|
||||
|
||||
Top = Math.Max(workArea.Top + ScreenMargin, workArea.Bottom - clampedHeight - ScreenMargin);
|
||||
}
|
||||
|
||||
private void BuildItems()
|
||||
{
|
||||
ItemsPanel.Children.Clear();
|
||||
visibleLaunchButtons.Clear();
|
||||
bool expandMenusByDefault = config.Items.Count <= 2;
|
||||
foreach (LauncherItem item in config.Items)
|
||||
{
|
||||
ItemsPanel.Children.Add(CreateItemControl(item, 0, expandMenusByDefault));
|
||||
}
|
||||
}
|
||||
|
||||
private FrameworkElement CreateItemControl(LauncherItem item, int depth, bool expandMenusByDefault)
|
||||
{
|
||||
if (item.Type == LauncherItemType.Menu)
|
||||
{
|
||||
Expander expander = new()
|
||||
{
|
||||
Header = item.Title,
|
||||
IsExpanded = expandMenusByDefault,
|
||||
Margin = new Thickness(depth * 12, 4, 0, 4)
|
||||
};
|
||||
|
||||
StackPanel children = new();
|
||||
foreach (LauncherItem child in item.Children)
|
||||
{
|
||||
children.Children.Add(CreateItemControl(child, depth + 1, expandMenusByDefault));
|
||||
}
|
||||
|
||||
expander.Content = children;
|
||||
return expander;
|
||||
}
|
||||
|
||||
System.Windows.Controls.Button button = new()
|
||||
{
|
||||
Content = CreateButtonContent(item),
|
||||
Margin = new Thickness(depth * 12, 2, 0, 2),
|
||||
MinHeight = DisplayModeMetrics.GetRowHeight(GetEffectiveDisplayMode(item)),
|
||||
Tag = item
|
||||
};
|
||||
|
||||
button.Click += (_, _) => launchItem(item);
|
||||
button.KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
launchItem(item);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Escape)
|
||||
{
|
||||
Close();
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
visibleLaunchButtons.Add(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
string query = SearchBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
BuildItems();
|
||||
}
|
||||
else
|
||||
{
|
||||
BuildSearchResults(query);
|
||||
}
|
||||
|
||||
QueueReposition();
|
||||
}
|
||||
|
||||
private void BuildSearchResults(string query)
|
||||
{
|
||||
ItemsPanel.Children.Clear();
|
||||
visibleLaunchButtons.Clear();
|
||||
|
||||
List<LauncherItem> matches = [];
|
||||
CollectMatches(config.Items, query, matches);
|
||||
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
ItemsPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "No matches",
|
||||
Foreground = (System.Windows.Media.Brush)System.Windows.Application.Current.Resources["AppMutedTextBrush"],
|
||||
Margin = new Thickness(4, 8, 4, 8)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (LauncherItem item in matches)
|
||||
{
|
||||
ItemsPanel.Children.Add(CreateItemControl(item, 0, expandMenusByDefault: false));
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectMatches(IEnumerable<LauncherItem> items, string query, List<LauncherItem> matches)
|
||||
{
|
||||
foreach (LauncherItem item in items)
|
||||
{
|
||||
bool isLaunchable = item.Type != LauncherItemType.Menu;
|
||||
if (isLaunchable && Matches(item, query))
|
||||
{
|
||||
matches.Add(item);
|
||||
}
|
||||
|
||||
if (item.Children.Count > 0)
|
||||
{
|
||||
CollectMatches(item.Children, query, matches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Matches(LauncherItem item, string query)
|
||||
{
|
||||
return item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
item.Target.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.Arguments?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
}
|
||||
|
||||
private void SearchBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
Close();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Enter && visibleLaunchButtons.FirstOrDefault()?.Tag is LauncherItem item)
|
||||
{
|
||||
launchItem(item);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Down && visibleLaunchButtons.FirstOrDefault() is System.Windows.Controls.Button firstButton)
|
||||
{
|
||||
Keyboard.ClearFocus();
|
||||
firstButton.Focus();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
Close();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
private object CreateButtonContent(LauncherItem item)
|
||||
{
|
||||
LauncherDisplayMode displayMode = GetEffectiveDisplayMode(item);
|
||||
|
||||
if (displayMode == LauncherDisplayMode.LargeIconOnly)
|
||||
{
|
||||
ImageSource? iconOnly = IconService.GetIcon(item, large: true);
|
||||
if (iconOnly is not null)
|
||||
{
|
||||
double iconOnlySize = DisplayModeMetrics.GetIconSize(displayMode);
|
||||
return new System.Windows.Controls.Image
|
||||
{
|
||||
Source = iconOnly,
|
||||
Width = iconOnlySize,
|
||||
Height = iconOnlySize,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Center
|
||||
};
|
||||
}
|
||||
|
||||
return new TextBlock
|
||||
{
|
||||
Text = GetLabel(item),
|
||||
FontSize = 20,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Center
|
||||
};
|
||||
}
|
||||
|
||||
StackPanel panel = new() { Orientation = System.Windows.Controls.Orientation.Horizontal };
|
||||
bool large = displayMode is LauncherDisplayMode.LargeIconOnly or LauncherDisplayMode.LargeIconWithText;
|
||||
ImageSource? icon = IconService.GetIcon(item, large);
|
||||
|
||||
if (icon is not null)
|
||||
{
|
||||
double size = DisplayModeMetrics.GetIconSize(displayMode);
|
||||
panel.Children.Add(new System.Windows.Controls.Image
|
||||
{
|
||||
Source = icon,
|
||||
Width = size,
|
||||
Height = size,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = GetLabel(item),
|
||||
FontSize = displayMode == LauncherDisplayMode.SmallIconWithText ? 12 : 14,
|
||||
Width = displayMode == LauncherDisplayMode.SmallIconWithText ? 64 : 74,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
}
|
||||
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
FontSize = displayMode == LauncherDisplayMode.CompactList ? 12 : 14,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private LauncherDisplayMode GetEffectiveDisplayMode(LauncherItem item)
|
||||
{
|
||||
return item.DisplayMode;
|
||||
}
|
||||
|
||||
private static string GetLabel(LauncherItem item)
|
||||
{
|
||||
return item.Type switch
|
||||
{
|
||||
LauncherItemType.Folder => "[Folder]",
|
||||
LauncherItemType.Website => "[Web]",
|
||||
LauncherItemType.File => "[File]",
|
||||
_ => "[App]"
|
||||
};
|
||||
}
|
||||
|
||||
private void Settings_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
showSettings();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user