using System.Windows; using System.Windows.Controls; using System.Windows.Forms; using System.Windows.Media; using System.IO; using System.Windows.Input; using TaskbarLauncher.Models; using TaskbarLauncher.Services; using MessageBox = System.Windows.MessageBox; using TreeView = System.Windows.Controls.TreeView; using TreeViewItem = System.Windows.Controls.TreeViewItem; namespace TaskbarLauncher; public partial class MainWindow : Window { private readonly LauncherController? controller; private LauncherItem? selectedItem; private bool isLoadingSelection; private bool isLoadingStartupState; private List? selectedPath; private string? loadedAutoIconPath; private System.Windows.Point dragStartPoint; public MainWindow() { InitializeComponent(); } public MainWindow(LauncherController controller) { InitializeComponent(); this.controller = controller; DisplayModeBox.ItemsSource = Enum.GetValues(); ItemDisplayModeBox.ItemsSource = Enum.GetValues(); TypeBox.ItemsSource = Enum.GetValues(); VersionText.Text = $"Version {AppInfo.Version} Build {AppInfo.Build}"; RefreshView(); } private void RefreshView(LauncherItem? itemToSelect = null) { if (controller is null) { return; } ItemsTree.Items.Clear(); foreach (LauncherItem item in controller.Config.Items) { ItemsTree.Items.Add(CreateTreeItem(item)); } HotkeyBox.Text = controller.Config.Hotkey; DisplayModeBox.SelectedItem = controller.Config.DisplayMode; LoadStartupState(); StatusText.Text = "Config file: config.json beside TaskbarLauncher.exe"; if (itemToSelect is not null && SelectTreeItem(itemToSelect)) { selectedItem = itemToSelect; selectedPath = FindItemPath(itemToSelect); LoadItemFields(itemToSelect); } else { ClearItemFields(); } } private static TreeViewItem CreateTreeItem(LauncherItem item) { var treeItem = new TreeViewItem { Header = $"{item.Title} ({item.Type})", Tag = item }; foreach (LauncherItem child in item.Children) { treeItem.Items.Add(CreateTreeItem(child)); } return treeItem; } private bool SelectTreeItem(LauncherItem item) { foreach (object root in ItemsTree.Items) { if (root is TreeViewItem treeItem && SelectTreeItem(treeItem, item)) { return true; } } return false; } private static bool SelectTreeItem(TreeViewItem treeItem, LauncherItem item) { if (ReferenceEquals(treeItem.Tag, item)) { treeItem.IsSelected = true; treeItem.BringIntoView(); return true; } foreach (object child in treeItem.Items) { if (child is TreeViewItem childTreeItem && SelectTreeItem(childTreeItem, item)) { treeItem.IsExpanded = true; return true; } } return false; } private void ItemsTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) { if (e.NewValue is not TreeViewItem treeItem || treeItem.Tag is not LauncherItem item) { selectedItem = null; ClearItemFields(); return; } selectedItem = item; selectedPath = FindItemPath(item); LoadItemFields(item); } private void ItemsTree_PreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) { if (e.LeftButton != MouseButtonState.Pressed) { dragStartPoint = e.GetPosition(ItemsTree); return; } System.Windows.Point currentPosition = e.GetPosition(ItemsTree); if (Math.Abs(currentPosition.X - dragStartPoint.X) < SystemParameters.MinimumHorizontalDragDistance && Math.Abs(currentPosition.Y - dragStartPoint.Y) < SystemParameters.MinimumVerticalDragDistance) { return; } if (ItemsTree.SelectedItem is TreeViewItem { Tag: LauncherItem item }) { DragDrop.DoDragDrop(ItemsTree, item, System.Windows.DragDropEffects.Move); } } private void ItemsTree_DragOver(object sender, System.Windows.DragEventArgs e) { e.Effects = CanDrop(e) ? System.Windows.DragDropEffects.Move : System.Windows.DragDropEffects.None; e.Handled = true; } private void ItemsTree_Drop(object sender, System.Windows.DragEventArgs e) { if (controller is null || !e.Data.GetDataPresent(typeof(LauncherItem)) || e.Data.GetData(typeof(LauncherItem)) is not LauncherItem draggedItem) { return; } TreeViewItem? targetTreeItem = FindAncestor(e.OriginalSource as DependencyObject); LauncherItem? targetItem = targetTreeItem?.Tag as LauncherItem; if (targetItem is not null && (ReferenceEquals(draggedItem, targetItem) || ContainsItem(draggedItem, targetItem))) { return; } ApplyCurrentItem(); List? sourceSiblings = FindSiblings(controller.Config.Items, draggedItem); if (sourceSiblings is null) { return; } sourceSiblings.Remove(draggedItem); if (targetItem is null) { controller.Config.Items.Add(draggedItem); } else if (targetItem.Type == LauncherItemType.Menu) { targetItem.Children.Add(draggedItem); } else { List? targetSiblings = FindSiblings(controller.Config.Items, targetItem); if (targetSiblings is null) { controller.Config.Items.Add(draggedItem); } else { int targetIndex = targetSiblings.IndexOf(targetItem); targetSiblings.Insert(targetIndex + 1, draggedItem); } } selectedPath = FindItemPath(draggedItem); RefreshView(draggedItem); StatusText.Text = $"Moved {draggedItem.Title}. Click Save to keep the new order."; e.Handled = true; } private bool CanDrop(System.Windows.DragEventArgs e) { if (!e.Data.GetDataPresent(typeof(LauncherItem)) || e.Data.GetData(typeof(LauncherItem)) is not LauncherItem draggedItem) { return false; } TreeViewItem? targetTreeItem = FindAncestor(e.OriginalSource as DependencyObject); LauncherItem? targetItem = targetTreeItem?.Tag as LauncherItem; return targetItem is null || (!ReferenceEquals(draggedItem, targetItem) && !ContainsItem(draggedItem, targetItem)); } private static bool ContainsItem(LauncherItem parent, LauncherItem possibleChild) { foreach (LauncherItem child in parent.Children) { if (ReferenceEquals(child, possibleChild) || ContainsItem(child, possibleChild)) { return true; } } return false; } private static T? FindAncestor(DependencyObject? current) where T : DependencyObject { while (current is not null) { if (current is T match) { return match; } current = VisualTreeHelper.GetParent(current); } return null; } private void LoadItemFields(LauncherItem item) { isLoadingSelection = true; TitleBox.Text = item.Title; TypeBox.SelectedItem = item.Type; TargetBox.Text = item.Target; ArgumentsBox.Text = item.Arguments ?? ""; IconPathBox.Text = item.IconPath ?? ""; loadedAutoIconPath = GetAutomaticIconPath(item); ItemDisplayModeBox.SelectedItem = item.DisplayMode; UpdateTargetAvailability(); isLoadingSelection = false; UpdatePreview(); } private void ClearItemFields() { selectedItem = null; selectedPath = null; isLoadingSelection = true; TitleBox.Text = ""; TypeBox.SelectedIndex = -1; TargetBox.Text = ""; ArgumentsBox.Text = ""; IconPathBox.Text = ""; loadedAutoIconPath = null; ItemDisplayModeBox.SelectedItem = controller?.Config.DisplayMode; UpdateTargetAvailability(); isLoadingSelection = false; UpdatePreview(); } private void ApplyCurrentItem() { if (controller is null || selectedItem is null) { return; } selectedItem.Title = string.IsNullOrWhiteSpace(TitleBox.Text) ? "Untitled" : TitleBox.Text.Trim(); selectedItem.Type = TypeBox.SelectedItem is LauncherItemType type ? type : LauncherItemType.App; selectedItem.Target = selectedItem.Type == LauncherItemType.Menu ? "" : TargetBox.Text.Trim(); selectedItem.Arguments = selectedItem.Type == LauncherItemType.Menu || string.IsNullOrWhiteSpace(ArgumentsBox.Text) ? null : ArgumentsBox.Text.Trim(); EnsureAutomaticIconPath(); selectedItem.IconPath = string.IsNullOrWhiteSpace(IconPathBox.Text) ? null : IconPathBox.Text.Trim(); selectedItem.DisplayMode = ItemDisplayModeBox.SelectedItem is LauncherDisplayMode itemMode ? itemMode : controller?.Config.DisplayMode ?? LauncherDisplayMode.LargeIconWithText; if (selectedItem.Type != LauncherItemType.Menu) { selectedItem.Children.Clear(); } } private void ApplyConfigFields() { if (controller is null) { return; } controller.Config.Hotkey = string.IsNullOrWhiteSpace(HotkeyBox.Text) ? "Ctrl+Alt+Space" : HotkeyBox.Text.Trim(); controller.Config.DisplayMode = DisplayModeBox.SelectedItem is LauncherDisplayMode mode ? mode : LauncherDisplayMode.LargeIconWithText; ApplyCurrentItem(); } private void LoadStartupState() { isLoadingStartupState = true; StartWithWindowsBox.IsChecked = StartupService.IsEnabled(); isLoadingStartupState = false; } private void StartWithWindowsBox_Changed(object sender, RoutedEventArgs e) { if (isLoadingStartupState) { return; } try { bool enabled = StartWithWindowsBox.IsChecked == true; StartupService.SetEnabled(enabled); StatusText.Text = enabled ? $"Startup enabled: {StartupService.ShortcutPath}" : "Startup disabled."; } catch (Exception ex) { MessageBox.Show($"Could not update startup setting.\n\n{ex.Message}", "Taskbar Launcher"); LoadStartupState(); } } private void AddRoot_Click(object sender, RoutedEventArgs e) { if (controller is null) { return; } ApplyCurrentItem(); LauncherItem item = CreateNewItem(controller.Config.DisplayMode); controller.Config.Items.Add(item); selectedPath = [controller.Config.Items.Count - 1]; RefreshView(item); } private void AddTopMenu_Click(object sender, RoutedEventArgs e) { if (controller is null) { return; } ApplyCurrentItem(); LauncherItem menu = CreateNewMenu(controller.Config.DisplayMode); controller.Config.Items.Add(menu); selectedPath = [controller.Config.Items.Count - 1]; RefreshView(menu); StatusText.Text = "Added top-level menu. Rename it, then Save."; } private void AddChild_Click(object sender, RoutedEventArgs e) { if (controller is null || selectedItem is null) { MessageBox.Show("Select a menu item first, then add a child item.", "Taskbar Launcher"); return; } LauncherItem parent = selectedItem; ApplyCurrentItem(); if (parent.Type != LauncherItemType.Menu) { MessageBox.Show("Child items can only be added under a Menu item.", "Taskbar Launcher"); return; } LauncherItem child = CreateNewItem(controller.Config.DisplayMode); parent.Children.Add(child); selectedPath = FindItemPath(child); RefreshView(child); } private void AddSubmenu_Click(object sender, RoutedEventArgs e) { if (controller is null || selectedItem is null) { MessageBox.Show("Select a menu first, then add a submenu.", "Taskbar Launcher"); return; } LauncherItem parent = selectedItem; ApplyCurrentItem(); if (parent.Type != LauncherItemType.Menu) { MessageBox.Show("Submenus can only be added under a Menu item.", "Taskbar Launcher"); return; } LauncherItem submenu = CreateNewMenu(controller.Config.DisplayMode); parent.Children.Add(submenu); selectedPath = FindItemPath(submenu); RefreshView(submenu); StatusText.Text = "Added submenu. Rename it, then Save."; } private static LauncherItem CreateNewItem(LauncherDisplayMode displayMode) { return new LauncherItem { Title = "New Item", Type = LauncherItemType.App, Target = "", DisplayMode = displayMode }; } private static LauncherItem CreateNewMenu(LauncherDisplayMode displayMode) { return new LauncherItem { Title = "New Menu", Type = LauncherItemType.Menu, Target = "", DisplayMode = displayMode }; } private void Delete_Click(object sender, RoutedEventArgs e) { if (controller is null || selectedItem is null) { return; } LauncherItem deletedItem = selectedItem; RemoveItem(controller.Config.Items, selectedItem); RefreshView(); StatusText.Text = $"Deleted: {deletedItem.Title}"; } private void MoveUp_Click(object sender, RoutedEventArgs e) { MoveSelected(-1); } private void MoveDown_Click(object sender, RoutedEventArgs e) { MoveSelected(1); } private void PromoteToTop_Click(object sender, RoutedEventArgs e) { if (controller is null || selectedItem is null) { return; } ApplyCurrentItem(); if (controller.Config.Items.Contains(selectedItem)) { StatusText.Text = $"{selectedItem.Title} is already a top-level item."; return; } LauncherItem itemToPromote = selectedItem; if (!RemoveItem(controller.Config.Items, itemToPromote)) { return; } controller.Config.Items.Add(itemToPromote); selectedPath = [controller.Config.Items.Count - 1]; RefreshView(itemToPromote); StatusText.Text = $"Promoted {itemToPromote.Title} to top level. Click Save to keep it."; } private void MoveSelected(int direction) { if (controller is null || selectedItem is null) { return; } ApplyCurrentItem(); List? siblings = FindSiblings(controller.Config.Items, selectedItem); if (siblings is null) { return; } int index = siblings.IndexOf(selectedItem); int newIndex = index + direction; if (index < 0 || newIndex < 0 || newIndex >= siblings.Count) { return; } siblings.RemoveAt(index); siblings.Insert(newIndex, selectedItem); RefreshView(selectedItem); } private static bool RemoveItem(List items, LauncherItem item) { if (items.Remove(item)) { return true; } foreach (LauncherItem child in items) { if (RemoveItem(child.Children, item)) { return true; } } return false; } private static List? FindSiblings(List items, LauncherItem item) { if (items.Contains(item)) { return items; } foreach (LauncherItem child in items) { List? found = FindSiblings(child.Children, item); if (found is not null) { return found; } } return null; } private void ApplyItem_Click(object sender, RoutedEventArgs e) { ApplyCurrentItem(); RefreshView(selectedItem); } private void Save_Click(object sender, RoutedEventArgs e) { if (controller is null) { return; } ApplyConfigFields(); ConfigService.Save(controller.Config); controller.ReloadConfig(); LauncherItem? reloadedSelection = selectedPath is null ? null : FindItemByPath(controller.Config.Items, selectedPath); RefreshView(reloadedSelection); StatusText.Text = reloadedSelection is null ? $"Saved: {ConfigService.ConfigPath}" : $"Saved {reloadedSelection.Title} ({reloadedSelection.Type}) to {ConfigService.ConfigPath}"; } private void BackupConfig_Click(object sender, RoutedEventArgs e) { try { if (controller is not null) { ApplyConfigFields(); ConfigService.Save(controller.Config); } string backupPath = ConfigService.Backup(); StatusText.Text = $"Backed up config to {backupPath}"; } catch (Exception ex) { MessageBox.Show($"Could not back up config.\n\n{ex.Message}", "Taskbar Launcher"); } } private void RestoreConfig_Click(object sender, RoutedEventArgs e) { using OpenFileDialog dialog = new() { Title = "Choose a config backup to restore", Filter = "JSON config files|*.json|All files|*.*", InitialDirectory = Directory.Exists(ConfigService.BackupFolder) ? ConfigService.BackupFolder : ConfigService.AppFolder }; if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK) { return; } MessageBoxResult result = MessageBox.Show( "Restore this config file and replace the current launcher config?", "Taskbar Launcher", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) { return; } try { ConfigService.Restore(dialog.FileName); controller?.ReloadConfig(); RefreshView(); StatusText.Text = $"Restored config from {dialog.FileName}"; } catch (Exception ex) { MessageBox.Show($"Could not restore config.\n\n{ex.Message}", "Taskbar Launcher"); } } private List? FindItemPath(LauncherItem item) { if (controller is null) { return null; } List path = []; return FindItemPath(controller.Config.Items, item, path) ? path : null; } private static bool FindItemPath(List items, LauncherItem item, List path) { for (int index = 0; index < items.Count; index++) { path.Add(index); if (ReferenceEquals(items[index], item) || FindItemPath(items[index].Children, item, path)) { return true; } path.RemoveAt(path.Count - 1); } return false; } private static LauncherItem? FindItemByPath(List items, List path) { List currentItems = items; LauncherItem? currentItem = null; foreach (int index in path) { if (index < 0 || index >= currentItems.Count) { return null; } currentItem = currentItems[index]; currentItems = currentItem.Children; } return currentItem; } private void ReloadConfig_Click(object sender, RoutedEventArgs e) { controller?.ReloadConfig(); RefreshView(); } private void OpenConfigFolder_Click(object sender, RoutedEventArgs e) { ConfigService.OpenConfigFolder(); } private void About_Click(object sender, RoutedEventArgs e) { AboutWindow aboutWindow = new() { Owner = this }; aboutWindow.ShowDialog(); } private void Close_Click(object sender, RoutedEventArgs e) { Hide(); } private void BrowseFile_Click(object sender, RoutedEventArgs e) { using OpenFileDialog dialog = new() { Title = "Choose an application, document, or shortcut", Filter = "Programs and shortcuts|*.exe;*.lnk;*.bat;*.cmd;*.ps1|All files|*.*" }; if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { TargetBox.Text = dialog.FileName; SetIconPathFromTargetIfAutomatic(dialog.FileName); } } private void BrowseFolder_Click(object sender, RoutedEventArgs e) { using FolderBrowserDialog dialog = new() { Description = "Choose a folder to launch" }; if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { TargetBox.Text = dialog.SelectedPath; TypeBox.SelectedItem = LauncherItemType.Folder; SetIconPathFromTargetIfAutomatic(dialog.SelectedPath); } } private void BrowseIcon_Click(object sender, RoutedEventArgs e) { using OpenFileDialog dialog = new() { Title = "Choose an icon file", Filter = "Icon files|*.ico;*.exe;*.dll|All files|*.*" }; if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { IconPathBox.Text = dialog.FileName; loadedAutoIconPath = null; } } private void ResetIcon_Click(object sender, RoutedEventArgs e) { if (TypeBox.SelectedItem is LauncherItemType.Menu) { IconPathBox.Text = ""; loadedAutoIconPath = null; UpdatePreview(); return; } string target = TargetBox.Text.Trim(); if (string.IsNullOrWhiteSpace(target)) { IconPathBox.Text = ""; loadedAutoIconPath = null; } else { IconPathBox.Text = target; loadedAutoIconPath = target; } UpdatePreview(); } private void TypeBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (!isLoadingSelection) { UpdateTargetAvailability(); UpdatePreview(); } } private void PreviewField_Changed(object sender, EventArgs e) { if (!isLoadingSelection) { UpdatePreview(); } } private void EnsureAutomaticIconPath() { if (TypeBox.SelectedItem is LauncherItemType.Menu) { return; } string target = TargetBox.Text.Trim(); if (!string.IsNullOrWhiteSpace(target)) { SetIconPathFromTargetIfAutomatic(target); } } private void SetIconPathFromTargetIfAutomatic(string target) { string currentIconPath = IconPathBox.Text.Trim(); if (string.IsNullOrWhiteSpace(currentIconPath) || string.Equals(currentIconPath, loadedAutoIconPath, StringComparison.OrdinalIgnoreCase)) { IconPathBox.Text = target; loadedAutoIconPath = target; } } private static string? GetAutomaticIconPath(LauncherItem item) { if (item.Type == LauncherItemType.Menu || string.IsNullOrWhiteSpace(item.Target)) { return null; } return string.Equals(item.IconPath, item.Target, StringComparison.OrdinalIgnoreCase) ? item.Target : null; } private void UpdatePreview() { if (PreviewHost is null) { return; } LauncherItem? previewItem = CreatePreviewItem(); if (previewItem is null) { PreviewHost.Content = new TextBlock { Text = "Select an item to preview it.", Foreground = (System.Windows.Media.Brush)System.Windows.Application.Current.Resources["AppMutedTextBrush"] }; PreviewNote.Text = ""; return; } LauncherDisplayMode displayMode = previewItem.DisplayMode; PreviewHost.Content = CreatePreviewControl(previewItem, displayMode); PreviewNote.Text = previewItem.Type == LauncherItemType.Menu ? "Menu headers are shown as expandable section headers in the launcher. Display size applies to app, folder, file, and website items." : $"Icon {DisplayModeMetrics.GetIconSize(displayMode):0}px, row {DisplayModeMetrics.GetRowHeight(displayMode):0}px."; } private LauncherItem? CreatePreviewItem() { if (selectedItem is null) { return null; } return new LauncherItem { Title = string.IsNullOrWhiteSpace(TitleBox.Text) ? "Untitled" : TitleBox.Text.Trim(), Type = TypeBox.SelectedItem is LauncherItemType type ? type : LauncherItemType.App, Target = TargetBox.Text.Trim(), Arguments = string.IsNullOrWhiteSpace(ArgumentsBox.Text) ? null : ArgumentsBox.Text.Trim(), IconPath = string.IsNullOrWhiteSpace(IconPathBox.Text) ? null : IconPathBox.Text.Trim(), DisplayMode = ItemDisplayModeBox.SelectedItem is LauncherDisplayMode mode ? mode : LauncherDisplayMode.CompactList }; } private static FrameworkElement CreatePreviewControl(LauncherItem item, LauncherDisplayMode displayMode) { if (item.Type == LauncherItemType.Menu) { return new TextBlock { Text = item.Title, FontWeight = FontWeights.SemiBold, VerticalAlignment = VerticalAlignment.Center }; } if (displayMode == LauncherDisplayMode.LargeIconOnly) { ImageSource? iconOnly = IconService.GetIcon(item, large: true); return iconOnly is null ? new TextBlock { Text = item.Title, HorizontalAlignment = System.Windows.HorizontalAlignment.Center } : new System.Windows.Controls.Image { Source = iconOnly, Width = DisplayModeMetrics.GetIconSize(displayMode), Height = DisplayModeMetrics.GetIconSize(displayMode), HorizontalAlignment = System.Windows.HorizontalAlignment.Center }; } StackPanel panel = new() { Orientation = System.Windows.Controls.Orientation.Horizontal }; bool large = displayMode == 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 }); } panel.Children.Add(new TextBlock { Text = item.Title, FontSize = displayMode == LauncherDisplayMode.CompactList ? 12 : 14, VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis }); return new Border { MinHeight = DisplayModeMetrics.GetRowHeight(displayMode), Child = panel }; } private void UpdateTargetAvailability() { bool isMenu = TypeBox.SelectedItem is LauncherItemType.Menu; TargetBox.IsEnabled = !isMenu; ArgumentsBox.IsEnabled = !isMenu; if (isMenu && !isLoadingSelection) { TargetBox.Text = ""; ArgumentsBox.Text = ""; } } protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { e.Cancel = true; Hide(); } }