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);
}