Files
DeskCore/MainWindow.LaunchGroups.cs
T
2026-10-03 19:51:56 +00:00

414 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace DeskCore;
/// <summary>Start-Gruppen: ein Klick startet mehrere Programme nacheinander.</summary>
public partial class MainWindow
{
private LaunchGroupItem? CurrentGroup => LaunchList.SelectedItem as LaunchGroupItem;
/// <summary>Kategorie-Dialog von außen (z. B. "Neue Box hier" auf dem Desktop).</summary>
public string? CreateCategoryInteractive() => NewCategory();
private static string DelayText(LaunchGroup g) =>
g.DelaySeconds <= 0 ? "gleichzeitig" : $"{g.DelaySeconds:0.#} s Abstand";
/// <summary>Wo liegt ein Element? (für Tooltip/Listenansicht)</summary>
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<LaunchGroupItem>().FirstOrDefault(x => x.Group == g);
if (item is not null) LaunchList.SelectedItem = item;
}
// ---------- Starten ----------
/// <summary>Startet alle Programme der Gruppe nacheinander (mit eingestelltem Abstand).</summary>
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<string>();
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);
}
/// <summary>Für die Befehlszeile und Desktop-Verknüpfungen: DeskCore.exe --launch "Name".</summary>
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 ----------
/// <summary>Dialog für Name, Startabstand und Farbe. Gibt null bei Abbruch zurück.</summary>
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<string>? 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 ----------
/// <summary>
/// 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.
/// </summary>
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<string> 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<IconItem> 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();
}
/// <summary>Kontextmenü der Icons, wenn eine Start-Gruppe angezeigt wird.</summary>
private void FillGroupItemMenu(LaunchGroup g, List<IconItem> 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"));
}
/// <summary>Untermenü "Zur Start-Gruppe hinzufügen" (Haupt-Kontextmenü und Desktop-Boxen).</summary>
public MenuItem LaunchGroupSubmenu(List<string> 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<ListBoxItem>(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<ListBoxItem>(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<ListBoxItem>(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);
}
/// <summary>Legt "&lt;Name&gt; starten.lnk" auf den Desktop, die DeskCore.exe --launch "&lt;Name&gt;" aufruft.</summary>
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); }
}
}