using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; namespace DeskCore; /// /// Schnellstart-Suche: Tastenkürzel → Suchleiste mitten auf dem Bildschirm, tippen, Enter, läuft. /// Durchsucht Kategorien, Desktop, Startmenü, Start-Gruppen, Kategorien und Desktop-Seiten. /// Was du oft startest, steht weiter oben. /// public sealed class QuickSearch : Window { private const int MaxResults = 8; private sealed record Entry(string Name, string Sub, string? Path, string Glyph, string Key, Action Run, Action? Locate = null) { /// Für die Ähnlichkeit: Name ohne Endungen wie „- Verknüpfung“. public string SearchName { get; } = Name.Replace(" - Verknüpfung", "", StringComparison.OrdinalIgnoreCase); } private static QuickSearch? _open; private static List? _startMenu; private static DateTime _startMenuRead; private readonly TextBox _box; private readonly ListBox _list; private readonly TextBlock _hint; private List _all = []; private List _shown = []; private bool _closing; private static AppSettings S => AppSettings.Current; /// Per Tastenkürzel: öffnen bzw. wieder schließen. public static void Toggle() { if (_open is not null) { _open.CloseNow(); return; } _open = new QuickSearch(); _open.Show(); } private QuickSearch() { var res = Application.Current.Resources; WindowStyle = WindowStyle.None; AllowsTransparency = true; Background = Brushes.Transparent; ResizeMode = ResizeMode.NoResize; ShowInTaskbar = false; Topmost = true; Width = 680; SizeToContent = SizeToContent.Height; Title = "DeskCore Suche"; FontFamily = (FontFamily)res["UiFont"]; Foreground = (Brush)res["TextBrush"]; // Suchzeile _box = new TextBox { FontSize = 20, BorderThickness = new Thickness(0), Background = Brushes.Transparent, Padding = new Thickness(0), VerticalContentAlignment = VerticalAlignment.Center, Foreground = (Brush)res["TextBrush"], CaretBrush = (Brush)res["TextBrush"], }; var placeholder = new TextBlock { Text = "Programm, Datei, Start-Gruppe oder Kategorie suchen …", FontSize = 20, IsHitTestVisible = false, Foreground = (Brush)res["TextTertiaryBrush"], VerticalAlignment = VerticalAlignment.Center, }; _box.TextChanged += (_, _) => { placeholder.Visibility = _box.Text.Length == 0 ? Visibility.Visible : Visibility.Collapsed; Filter(); }; var searchRow = new DockPanel { Margin = new Thickness(18, 14, 18, 14) }; var glass = new TextBlock { Text = "", FontFamily = (FontFamily)res["IconFont"], FontSize = 20, Margin = new Thickness(0, 0, 14, 0), VerticalAlignment = VerticalAlignment.Center, Foreground = (Brush)res["AccentBrush"], }; DockPanel.SetDock(glass, Dock.Left); searchRow.Children.Add(glass); searchRow.Children.Add(new Grid { Children = { placeholder, _box } }); _list = new ListBox { BorderThickness = new Thickness(0), Background = Brushes.Transparent, Margin = new Thickness(6, 0, 6, 6), Focusable = false, Visibility = Visibility.Collapsed, }; ScrollViewer.SetHorizontalScrollBarVisibility(_list, ScrollBarVisibility.Disabled); _list.MouseDoubleClick += (_, _) => RunSelected(locate: false); _list.PreviewMouseLeftButtonUp += (_, e) => { // Ein Klick genügt – wie im Startmenü if (MainWindow.FindParent(e.OriginalSource as DependencyObject) is not null) RunSelected(locate: false); }; _hint = new TextBlock { FontSize = 12, Margin = new Thickness(18, 8, 18, 10), Foreground = (Brush)res["TextTertiaryBrush"], Text = "Enter Starten · Strg + Enter Speicherort öffnen · Esc Schließen", }; var divider = new Border { Height = 1, Background = (Brush)res["ControlStrokeBrush"] }; var footer = new Border { Child = _hint, BorderBrush = (Brush)res["ControlStrokeBrush"], BorderThickness = new Thickness(0, 1, 0, 0) }; var stack = new StackPanel { Children = { searchRow, divider, _list, footer } }; Content = new Border { Margin = new Thickness(24), CornerRadius = new CornerRadius(10), Background = (Brush)res["FlyoutBrush"], BorderBrush = (Brush)res["FlyoutStrokeBrush"], BorderThickness = new Thickness(1), Effect = (System.Windows.Media.Effects.Effect)res["DialogShadow"], Child = stack, }; PreviewKeyDown += OnKey; Deactivated += (_, _) => CloseNow(); Closed += (_, _) => { if (_open == this) _open = null; }; Loaded += (_, _) => { Position(); BringToFront(); _box.Focus(); BuildIndex(); }; } // ===================================================================== // Position & Fokus // ===================================================================== private void Position() { var mouse = GetCursorPos(out var p) ? new Point(p.X / Screens.Scale, p.Y / Screens.Scale) : new Point(0, 0); var wa = Screens.WorkAreaAt(mouse); Left = wa.Left + (wa.Width - Width) / 2; Top = wa.Top + wa.Height * 0.22; } private void BringToFront() { // Das Tastenkürzel erlaubt DeskCore, nach vorne zu kommen var hwnd = new WindowInteropHelper(this).Handle; SetForegroundWindow(hwnd); Activate(); } private void CloseNow() { if (_closing) return; _closing = true; Close(); } // ===================================================================== // Was durchsucht wird // ===================================================================== private async void BuildIndex() { var list = new List(); // Start-Gruppen foreach (var g in S.LaunchGroups) { var group = g; list.Add(new Entry(g.Name, $"Start-Gruppe · {g.Items.Count} {(g.Items.Count == 1 ? "Programm" : "Programme")}", null, "", Usage.GroupPrefix + g.Name, () => App.Win?.LaunchGroupNow(group))); } // Kategorien selbst (öffnet DeskCore dort) und Desktop-Seiten foreach (var c in Storage.Categories()) { var cat = c; list.Add(new Entry(c, "Kategorie in DeskCore öffnen", null, S.Categories.GetValueOrDefault(c)?.Glyph ?? "", "kategorie:" + c, () => { App.Win?.ShowFromBackground(); App.Win?.ShowCategory(cat); })); } if (S.Pages.Count > 1) for (int i = 0; i < S.Pages.Count; i++) { int page = i; list.Add(new Entry(S.Pages[i], "Desktop-Seite anzeigen", null, "", "seite:" + S.Pages[i], () => FenceManager.SwitchPage(page))); } _all = list; Filter(); // Dateien aus Kategorien, Desktop und Startmenü im Hintergrund sammeln (Startmenü 10 Minuten zwischengespeichert) bool withStartMenu = S.QuickSearchStartMenu; var categories = Storage.Categories(); var files = await Task.Run(() => { var l = new List(); foreach (var c in categories) foreach (var p in Storage.Entries(Storage.CategoryPath(c))) l.Add(FileEntry(p, c)); foreach (var p in Storage.DesktopEntries()) l.Add(FileEntry(p, "Desktop")); if (withStartMenu) { if (_startMenu is null || DateTime.Now - _startMenuRead > TimeSpan.FromMinutes(10)) { _startMenu = ReadStartMenu(); _startMenuRead = DateTime.Now; } l.AddRange(_startMenu); } return Dedupe(l); }); if (_closing) return; _all = list.Concat(files).ToList(); Filter(); } private static Entry FileEntry(string path, string where) => new(Storage.DisplayName(path), where, path, "", path, () => Usage.Start(path), () => Process.Start("explorer.exe", $"/select,\"{path}\"")); private static List ReadStartMenu() { var result = new List(); foreach (var root in new[] { Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu), }) { if (!Directory.Exists(root)) continue; IEnumerable files; try { files = Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories).ToList(); } catch { continue; } foreach (var f in files) { var ext = Path.GetExtension(f).ToLowerInvariant(); if (ext is not (".lnk" or ".url" or ".appref-ms")) continue; var name = Path.GetFileNameWithoutExtension(f); // Deinstallations- und Hilfe-Verknüpfungen will niemand per Suche starten if (name.Contains("uninstall", StringComparison.OrdinalIgnoreCase) || name.Contains("deinstall", StringComparison.OrdinalIgnoreCase)) continue; result.Add(new Entry(name, "Startmenü", f, "", f, () => Usage.Start(f), () => Process.Start("explorer.exe", $"/select,\"{f}\""))); } } return result; } /// /// Gleiches Programm aus Kategorie, Desktop und Startmenü nur einmal zeigen – die Reihenfolge der Quellen /// entscheidet (Kategorie vor Desktop vor Startmenü). Gleicher Name gilt als gleiches Programm. /// private static List Dedupe(IEnumerable entries) { var seen = new HashSet(StringComparer.OrdinalIgnoreCase); return entries.Where(e => e.Path is null || seen.Add(e.SearchName.Trim())).ToList(); } // ===================================================================== // Suchen & Anzeigen // ===================================================================== /// Wie gut passt der Name zur Eingabe? -1 = gar nicht. private static int Match(string name, string q) { if (q.Length == 0) return 0; var n = name.ToLowerInvariant(); if (n == q) return 1000; if (n.StartsWith(q)) return 800; var words = n.Split([' ', '-', '_', '.', '(', ')', '&'], StringSplitOptions.RemoveEmptyEntries); if (words.Any(w => w.StartsWith(q))) return 650; if (q.Length >= 2 && string.Concat(words.Select(w => w[0])).StartsWith(q)) return 600; // "vsc" → Visual Studio Code if (n.Contains(q)) return 450; // Buchstaben in der richtigen Reihenfolge ("fzla" → FileZilla) – aber nur ab einem Wortanfang, // sonst passt fast alles irgendwie ("ets" → Malwarebytes) if (q.Length < 3) return -1; for (int start = 0; start < n.Length; start++) { if (n[start] != q[0] || (start > 0 && char.IsLetterOrDigit(n[start - 1]))) continue; int k = 0; for (int j = start; j < n.Length && k < q.Length; j++) if (n[j] == q[k]) k++; if (k == q.Length) return 200; } return -1; } private void Filter() { var q = _box.Text.Trim().ToLowerInvariant(); if (q.Length == 0) { // Ohne Eingabe: zuletzt/oft benutzt _shown = _all.Where(e => Usage.Score(e.Key) > 0) .OrderByDescending(e => Usage.Score(e.Key)).Take(MaxResults).ToList(); } else { _shown = _all.Select(e => (e, m: Match(e.SearchName, q))) .Where(x => x.m >= 0) .OrderByDescending(x => x.m + Math.Min(300, Usage.Score(x.e.Key) * 40)) .ThenBy(x => x.e.Name.Length) .Take(MaxResults).Select(x => x.e).ToList(); } ShowResults(q.Length > 0); } private void ShowResults(bool searching) { var res = Application.Current.Resources; _list.Items.Clear(); int px = (int)Math.Round(32 * VisualTreeHelper.GetDpi(this).DpiScaleX); foreach (var e in _shown) { FrameworkElement icon; if (e.Path is not null) { var img = new Image { Width = 32, Height = 32 }; var item = new IconItem { Path = e.Path }; item.PropertyChanged += (_, _) => img.Source = item.Icon; IconLoader.Load(item, px); img.Source = item.Icon; icon = img; } else { var meta = e.Key.StartsWith("kategorie:") ? S.Categories.GetValueOrDefault(e.Name) : null; icon = new TextBlock { Text = e.Glyph, FontFamily = (FontFamily)res["IconFont"], FontSize = 22, Width = 32, TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Foreground = CategoryColors.Brush(meta?.Color) ?? (Brush)res["AccentBrush"], }; } icon.Margin = new Thickness(0, 0, 14, 0); DockPanel.SetDock(icon, Dock.Left); var texts = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; texts.Children.Add(new TextBlock { Text = e.Name, FontSize = 15, TextTrimming = TextTrimming.CharacterEllipsis }); texts.Children.Add(new TextBlock { Text = e.Sub, FontSize = 12, Foreground = (Brush)res["TextSecondaryBrush"] }); _list.Items.Add(new DockPanel { Margin = new Thickness(8, 6, 8, 6), Children = { icon, texts } }); } if (_shown.Count == 0 && searching) _list.Items.Add(new TextBlock { Text = "Nichts gefunden", Margin = new Thickness(12, 10, 12, 10), Foreground = (Brush)res["TextSecondaryBrush"], IsHitTestVisible = false, }); _list.Visibility = _list.Items.Count > 0 ? Visibility.Visible : Visibility.Collapsed; if (_shown.Count > 0) _list.SelectedIndex = 0; } private void OnKey(object sender, KeyEventArgs e) { switch (e.Key) { case Key.Escape: CloseNow(); e.Handled = true; break; case Key.Down or Key.Up when _shown.Count > 0: int i = _list.SelectedIndex + (e.Key == Key.Down ? 1 : -1); _list.SelectedIndex = (i + _shown.Count) % _shown.Count; _list.ScrollIntoView(_list.SelectedItem); e.Handled = true; break; case Key.Enter: RunSelected(locate: Keyboard.Modifiers.HasFlag(ModifierKeys.Control)); e.Handled = true; break; } } private void RunSelected(bool locate) { if (_list.SelectedIndex < 0 || _list.SelectedIndex >= _shown.Count) return; var entry = _shown[_list.SelectedIndex]; CloseNow(); // zuerst schließen, damit das gestartete Programm nach vorne kommt try { if (locate && entry.Locate is not null) entry.Locate(); else { entry.Run(); // Kategorien/Seiten mitzählen (Dateien zählt Usage.Start, Gruppen LaunchGroupNow selbst) if (entry.Path is null && !entry.Key.StartsWith(Usage.GroupPrefix)) Usage.Record(entry.Key); } } catch (Exception ex) { App.TrayIcon?.Notify("Starten fehlgeschlagen", $"„{entry.Name}“ konnte nicht geöffnet werden.\n{ex.Message}"); } } [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; } [DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT p); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hwnd); }