416 lines
19 KiB
C#
416 lines
19 KiB
C#
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;
|
||
}
|
||
Usage.Record(Usage.GroupPrefix + g.Name); // damit die Schnellstart-Suche häufige Gruppen weiter oben zeigt
|
||
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
|
||
{
|
||
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);
|
||
}
|
||
|
||
/// <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;
|
||
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 ----------
|
||
|
||
/// <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 "<Name> starten.lnk" auf den Desktop, die DeskCore.exe --launch "<Name>" 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); }
|
||
}
|
||
}
|