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;
}
Usage.Record(Usage.GroupPrefix + g.Name); // damit die Schnellstart-Suche häufige Gruppen weiter oben zeigt
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
{
Usage.Start(p);
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;
Usage.Rename(Usage.GroupPrefix + before.Name, Usage.GroupPrefix + g.Name);
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); }
}
}