using System.Diagnostics; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace DeskCore; /// Start-Gruppen: ein Klick startet mehrere Programme nacheinander. public partial class MainWindow { private LaunchGroupItem? CurrentGroup => LaunchList.SelectedItem as LaunchGroupItem; /// Kategorie-Dialog von außen (z. B. "Neue Box hier" auf dem Desktop). public string? CreateCategoryInteractive() => NewCategory(); private static string DelayText(LaunchGroup g) => g.DelaySeconds <= 0 ? "gleichzeitig" : $"{g.DelaySeconds:0.#} s Abstand"; /// Wo liegt ein Element? (für Tooltip/Listenansicht) private static string LocationOf(string path) { var dir = Path.GetDirectoryName(path) ?? ""; if (Storage.IsDesktopDir(dir)) return DesktopName; return Storage.IsInStorage(path) ? Path.GetFileName(dir) : dir; } private void LoadGroupIcons(LaunchGroup g) { var items = g.Items.Select(p => new IconItem { Path = p, Category = LocationOf(p) }).ToList(); HeaderText.Text = g.Name; int missing = items.Count(i => !File.Exists(i.Path) && !Directory.Exists(i.Path)); SubText.Text = $"{Plural(items.Count, "Programm", "Programme")} · {DelayText(g)}" + (missing > 0 ? $" · ⚠ {missing} nicht mehr vorhanden" : ""); SetEmpty("", "Noch leer", "Ziehe Programme hierher oder auf die Gruppe links – oder Rechtsklick auf ein Icon → „Zur Start-Gruppe hinzufügen“."); // Reihenfolge = Startreihenfolge, deshalb hier nicht sortieren var px = IconPixels; foreach (var item in items) IconLoader.Load(item, px); IconList.ItemsSource = items; EmptyState.Visibility = items.Count == 0 ? Visibility.Visible : Visibility.Collapsed; CategoryCommands.Visibility = Visibility.Collapsed; InfoBar.Visibility = Visibility.Collapsed; GroupCommands.Visibility = Visibility.Visible; } private void SelectGroup(LaunchGroup g) { var item = LaunchList.Items.OfType().FirstOrDefault(x => x.Group == g); if (item is not null) LaunchList.SelectedItem = item; } // ---------- Starten ---------- /// Startet alle Programme der Gruppe nacheinander (mit eingestelltem Abstand). public async void LaunchGroupNow(LaunchGroup g) { if (g.Items.Count == 0) { ShowToast($"„{g.Name}“ ist noch leer", "", info: true); return; } int started = 0; var problems = new List(); var items = g.Items.ToList(); for (int i = 0; i < items.Count; i++) { if (i > 0 && g.DelaySeconds > 0) await Task.Delay(TimeSpan.FromSeconds(g.DelaySeconds)); var p = items[i]; if (!File.Exists(p) && !Directory.Exists(p)) { problems.Add($"„{Storage.DisplayName(p)}“ gibt es nicht mehr"); continue; } try { Process.Start(new ProcessStartInfo(p) { UseShellExecute = true }); started++; } catch (Exception ex) { problems.Add($"„{Storage.DisplayName(p)}“: {ex.Message}"); } } var text = $"„{g.Name}“: {Plural(started, "Programm", "Programme")} gestartet" + (problems.Count > 0 ? $" · {problems.Count} nicht" : ""); if (IsVisible && IsActive) ShowToast(text, problems.Count > 0 ? "" : "", info: problems.Count > 0); else App.TrayIcon?.Notify(g.Name, problems.Count > 0 ? text + "\n" + string.Join("\n", problems.Take(3)) : text); } /// Für die Befehlszeile und Desktop-Verknüpfungen: DeskCore.exe --launch "Name". public bool LaunchGroupByName(string name) { var g = S.LaunchGroups.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); if (g is null) { App.TrayIcon?.Notify("Start-Gruppe nicht gefunden", $"Es gibt keine Start-Gruppe „{name}“."); return false; } LaunchGroupNow(g); return true; } private void LaunchCurrentGroup_Click(object sender, RoutedEventArgs e) { if (CurrentGroup is { } g) LaunchGroupNow(g.Group); } private void LaunchGroupPlay_Click(object sender, RoutedEventArgs e) { if ((sender as FrameworkElement)?.Tag is LaunchGroupItem g) LaunchGroupNow(g.Group); e.Handled = true; } // ---------- Anlegen / Bearbeiten ---------- /// Dialog für Name, Startabstand und Farbe. Gibt null bei Abbruch zurück. private (string Name, double Delay, string? Color)? LaunchGroupDialog(string title, string primary, LaunchGroup? existing) { var res = Application.Current.Resources; var panel = new StackPanel(); TextBlock Label(string t, double top = 16) => new() { Text = t, Margin = new Thickness(0, top, 0, 8), FontWeight = FontWeights.SemiBold, FontSize = 13, }; panel.Children.Add(Label("Name", 0)); var nameBox = new TextBox { Text = existing?.Name ?? "" }; panel.Children.Add(nameBox); panel.Children.Add(Label("Abstand zwischen den Programmen")); var delayText = new TextBlock { VerticalAlignment = VerticalAlignment.Center, Width = 110, Foreground = (Brush)res["TextSecondaryBrush"] }; var slider = new Slider { Minimum = 0, Maximum = 10, TickFrequency = 0.5, IsSnapToTickEnabled = true, Value = existing?.DelaySeconds ?? 1 }; void UpdateDelay() => delayText.Text = slider.Value <= 0 ? "gleichzeitig" : $"{slider.Value:0.#} Sekunden"; slider.ValueChanged += (_, _) => UpdateDelay(); UpdateDelay(); var row = new DockPanel(); DockPanel.SetDock(delayText, Dock.Right); row.Children.Add(delayText); row.Children.Add(slider); panel.Children.Add(row); panel.Children.Add(new TextBlock { Text = "Etwas Abstand hilft, wenn Programme beim Start viel Leistung brauchen.", FontSize = 12, Margin = new Thickness(0, 4, 0, 0), TextWrapping = TextWrapping.Wrap, Foreground = (Brush)res["TextTertiaryBrush"], }); panel.Children.Add(Label("Farbe")); string? color = existing?.Color; var swatches = new WrapPanel(); var none = new RadioButton { Style = (Style)res["SwatchRadio"], GroupName = "lgcolor", ToolTip = "Akzentfarbe", Background = (Brush)res["AccentBrush"], IsChecked = color is null, }; none.Checked += (_, _) => color = null; swatches.Children.Add(none); foreach (var (cname, hex) in CategoryColors.All) { var rb = new RadioButton { Style = (Style)res["SwatchRadio"], GroupName = "lgcolor", ToolTip = cname, Background = CategoryColors.Brush(hex), IsChecked = string.Equals(color, hex, StringComparison.OrdinalIgnoreCase), }; rb.Checked += (_, _) => color = hex; swatches.Children.Add(rb); } panel.Children.Add(swatches); string? Validate() { var n = nameBox.Text.Trim(); if (n.Length == 0) return "Bitte einen Namen eingeben."; if (n.Contains('"')) return "Der Name darf keine Anführungszeichen enthalten."; bool taken = S.LaunchGroups.Any(x => x != existing && string.Equals(x.Name, n, StringComparison.OrdinalIgnoreCase)); return taken ? "Eine Start-Gruppe mit diesem Namen gibt es schon." : null; } if (!Dialog.Show(this, title, panel, primary, "Abbrechen", Validate, nameBox, width: 470)) return null; return (nameBox.Text.Trim(), slider.Value, color); } private LaunchGroup? NewLaunchGroup(IEnumerable? initialItems = null) { var form = LaunchGroupDialog("Neue Start-Gruppe", "Erstellen", null); if (form is null) return null; var g = new LaunchGroup { Name = form.Value.Name, DelaySeconds = form.Value.Delay, Color = form.Value.Color }; S.LaunchGroups.Add(g); if (initialItems is not null) AddPathsToGroup(g, initialItems, toast: false); S.Save(); var entry = new UndoEntry($"Start-Gruppe „{g.Name}“ erstellen"); entry.Add(() => { S.LaunchGroups.Remove(g); S.Save(); return null; }); Undo.Push(entry); Refresh(); return g; } private void NewLaunchGroup_Click(object sender, RoutedEventArgs e) { if (NewLaunchGroup() is not { } g) return; ShowBrowse(); SelectGroup(g); ShowToast($"Start-Gruppe „{g.Name}“ erstellt – jetzt Programme hineinziehen", "", undoable: true); } private void EditLaunchGroup(LaunchGroup g) { var form = LaunchGroupDialog("Start-Gruppe bearbeiten", "Speichern", g); if (form is null) return; var before = (g.Name, g.DelaySeconds, g.Color); (g.Name, g.DelaySeconds, g.Color) = form.Value; S.Save(); var entry = new UndoEntry($"Start-Gruppe „{before.Name}“ bearbeiten"); entry.Add(() => { (g.Name, g.DelaySeconds, g.Color) = before; S.Save(); return null; }); Undo.Push(entry); Refresh(); if (before.Name != g.Name) ShowToast("Gespeichert – eine vorhandene Desktop-Verknüpfung bitte neu anlegen (sie nutzt noch den alten Namen)", "", info: true); else ShowToast("Start-Gruppe gespeichert", undoable: true); } private void EditLaunchGroup_Click(object sender, RoutedEventArgs e) { if (CurrentGroup is { } g) EditLaunchGroup(g.Group); } private void DeleteLaunchGroup(LaunchGroup g) { if (!Dialog.Confirm(this, "Start-Gruppe löschen?", $"„{g.Name}“ wird entfernt. Die Programme selbst bleiben unverändert.", "Löschen")) return; int index = S.LaunchGroups.IndexOf(g); S.LaunchGroups.Remove(g); S.Save(); var entry = new UndoEntry($"Start-Gruppe „{g.Name}“ löschen"); entry.Add(() => { S.LaunchGroups.Insert(Math.Min(index, S.LaunchGroups.Count), g); S.Save(); return null; }); Undo.Push(entry); Refresh(); ShowToast($"Start-Gruppe „{g.Name}“ gelöscht", "", undoable: true); } private void DeleteLaunchGroup_Click(object sender, RoutedEventArgs e) { if (CurrentGroup is { } g) DeleteLaunchGroup(g.Group); } private void MoveLaunchGroup(LaunchGroup g, int delta) { int i = S.LaunchGroups.IndexOf(g), j = i + delta; if (i < 0 || j < 0 || j >= S.LaunchGroups.Count) return; (S.LaunchGroups[i], S.LaunchGroups[j]) = (S.LaunchGroups[j], S.LaunchGroups[i]); S.Save(); Refresh(); } // ---------- Inhalt ---------- /// /// Desktop-Icons, die schon in einer Kategorie gesichert sind, werden durch die gesicherte Kopie ersetzt – /// so funktioniert die Gruppe auch noch, wenn das Icon später vom Desktop gelöscht wird. /// private static string ResolveForGroup(string path) { if (Storage.IsInStorage(path)) return path; foreach (var c in Storage.Categories()) if (Storage.Entries(Storage.CategoryPath(c)).FirstOrDefault(e => Storage.SameItem(e, path)) is { } saved) return saved; return path; } private void AddPathsToGroup(LaunchGroup g, IEnumerable paths, bool toast = true) { int added = 0, dup = 0; foreach (var raw in paths) { var p = ResolveForGroup(raw); if (g.Items.Contains(p, StringComparer.OrdinalIgnoreCase)) { dup++; continue; } g.Items.Add(p); added++; } S.Save(); if (!toast) return; Refresh(); if (added == 0) ShowToast(dup == 1 ? "Das ist schon in der Gruppe" : "Alles schon in der Gruppe", "", info: true); else ShowToast($"{Plural(added, "Programm", "Programme")} zu „{g.Name}“ hinzugefügt", ""); } private void RemoveFromGroup(LaunchGroup g, List sel) { var before = g.Items.ToList(); foreach (var i in sel) g.Items.RemoveAll(p => string.Equals(p, i.Path, StringComparison.OrdinalIgnoreCase)); S.Save(); var entry = new UndoEntry($"Aus „{g.Name}“ entfernen"); entry.Add(() => { g.Items = before; S.Save(); return null; }); Undo.Push(entry); Refresh(); ShowToast($"{Plural(sel.Count, "Programm", "Programme")} aus „{g.Name}“ entfernt – die Dateien bleiben erhalten", "", undoable: true); } private void MoveInGroup(LaunchGroup g, IconItem item, int delta) { int i = g.Items.FindIndex(p => string.Equals(p, item.Path, StringComparison.OrdinalIgnoreCase)), j = i + delta; if (i < 0 || j < 0 || j >= g.Items.Count) return; (g.Items[i], g.Items[j]) = (g.Items[j], g.Items[i]); S.Save(); Refresh(); } /// Kontextmenü der Icons, wenn eine Start-Gruppe angezeigt wird. private void FillGroupItemMenu(LaunchGroup g, List sel) { IconMenu.Items.Add(MenuItem("Öffnen", "", () => sel.ForEach(i => Open(i.Path)), bold: true, gesture: "Enter")); if (sel.Count == 1) { IconMenu.Items.Add(MenuItem("Dateispeicherort öffnen", "", () => Process.Start("explorer.exe", $"/select,\"{sel[0].Path}\""))); IconMenu.Items.Add(new Separator()); int idx = g.Items.FindIndex(p => string.Equals(p, sel[0].Path, StringComparison.OrdinalIgnoreCase)); var up = MenuItem("Früher starten", "", () => MoveInGroup(g, sel[0], -1)); up.IsEnabled = idx > 0; var down = MenuItem("Später starten", "", () => MoveInGroup(g, sel[0], +1)); down.IsEnabled = idx >= 0 && idx < g.Items.Count - 1; IconMenu.Items.Add(up); IconMenu.Items.Add(down); } IconMenu.Items.Add(new Separator()); IconMenu.Items.Add(MenuItem("Aus Start-Gruppe entfernen", "", () => RemoveFromGroup(g, sel), gesture: "Entf")); } /// Untermenü "Zur Start-Gruppe hinzufügen" (Haupt-Kontextmenü und Desktop-Boxen). public MenuItem LaunchGroupSubmenu(List paths) { var menu = MenuItem("Zur Start-Gruppe hinzufügen", "", null); foreach (var g in S.LaunchGroups) menu.Items.Add(MenuItem(g.Name, "", () => AddPathsToGroup(g, paths))); if (menu.Items.Count > 0) menu.Items.Add(new Separator()); menu.Items.Add(MenuItem("Neue Start-Gruppe…", "", () => { if (NewLaunchGroup(paths) is { } g) ShowToast($"Start-Gruppe „{g.Name}“ mit {Plural(g.Items.Count, "Programm", "Programmen")} erstellt", "", undoable: true); })); return menu; } // ---------- Seitenleiste ---------- private void LaunchList_ContextMenuOpening(object sender, ContextMenuEventArgs e) { if (FindParent(e.OriginalSource as DependencyObject)?.DataContext is not LaunchGroupItem item) { e.Handled = true; return; } var g = item.Group; int idx = S.LaunchGroups.IndexOf(g); LaunchMenu.Items.Clear(); LaunchMenu.Items.Add(MenuItem("Alle starten", "", () => LaunchGroupNow(g), bold: true)); LaunchMenu.Items.Add(MenuItem("Bearbeiten…", "", () => EditLaunchGroup(g))); LaunchMenu.Items.Add(MenuItem("Verknüpfung auf den Desktop", "", () => CreateGroupShortcut(g))); LaunchMenu.Items.Add(new Separator()); var up = MenuItem("Nach oben", "", () => MoveLaunchGroup(g, -1)); up.IsEnabled = idx > 0; var down = MenuItem("Nach unten", "", () => MoveLaunchGroup(g, +1)); down.IsEnabled = idx < S.LaunchGroups.Count - 1; LaunchMenu.Items.Add(up); LaunchMenu.Items.Add(down); LaunchMenu.Items.Add(new Separator()); LaunchMenu.Items.Add(MenuItem("Löschen", "", () => DeleteLaunchGroup(g))); } private void LaunchList_DragOver(object sender, DragEventArgs e) { var lbi = FindParent(e.OriginalSource as DependencyObject); bool ok = lbi is not null && e.Data.GetDataPresent(DataFormats.FileDrop); SetDropHighlight(ok ? lbi : null); e.Effects = ok ? DragDropEffects.Link | (e.AllowedEffects & DragDropEffects.Copy) : DragDropEffects.None; e.Handled = true; } private void LaunchList_Drop(object sender, DragEventArgs e) { SetDropHighlight(null); if (FindParent(e.OriginalSource as DependencyObject)?.DataContext is LaunchGroupItem item && e.Data.GetData(DataFormats.FileDrop) is string[] files) AddPathsToGroup(item.Group, files); } // ---------- Desktop-Verknüpfung ---------- private void LaunchGroupShortcut_Click(object sender, RoutedEventArgs e) { if (CurrentGroup is { } g) CreateGroupShortcut(g.Group); } /// Legt "<Name> starten.lnk" auf den Desktop, die DeskCore.exe --launch "<Name>" aufruft. private void CreateGroupShortcut(LaunchGroup g) { try { var exe = Environment.ProcessPath!; var safe = string.Concat(g.Name.Split(Path.GetInvalidFileNameChars())); var link = Storage.UniquePath(Storage.UserDesktop, $"{safe} starten.lnk"); Storage.MarkCreated(link); // nicht von der Auto-Sortierung einsammeln lassen dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")!)!; dynamic l = shell.CreateShortcut(link); l.TargetPath = exe; l.Arguments = $"--launch \"{g.Name}\""; l.WorkingDirectory = Path.GetDirectoryName(exe); l.IconLocation = $"{exe},0"; l.Description = $"Startet die DeskCore-Start-Gruppe „{g.Name}“"; l.Save(); ShowToast($"„{Path.GetFileNameWithoutExtension(link)}“ liegt jetzt auf dem Desktop", ""); } catch (Exception ex) { Error("Verknüpfung fehlgeschlagen", ex.Message); } } }