Files
DeskCore/FenceManager.cs
2026-10-04 14:48:53 +00:00

404 lines
17 KiB
C#
Raw Permalink 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.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
namespace DeskCore;
/// <summary>Verwaltet alle Desktop-Boxen: Anzeigen, Peek, Ausblenden, Snapshots und Anordnung je Bildschirm-Setup.</summary>
public static class FenceManager
{
private static readonly List<FenceWindow> _open = [];
private static AppSettings S => AppSettings.Current;
private static DispatcherTimer? _peekWatch;
private static string? _screenSignature;
/// <summary>Boxen gerade per "Schnell verstecken" ausgeblendet?</summary>
public static bool Hidden { get; private set; }
/// <summary>Boxen gerade per Peek vor allen Fenstern?</summary>
public static bool Peeking { get; private set; }
public static void Sync()
{
_screenSignature ??= ScreenSignature();
// Boxen auf nicht (mehr) vorhandenen Seiten landen auf der letzten Seite
foreach (var f in S.Fences) f.Page = Math.Clamp(f.Page, 0, S.Pages.Count - 1);
var wanted = S.FencesEnabled && !Hidden
? S.Fences.Where(f => f.Tabs.Count > 0 && f.Page == S.CurrentPage).ToList()
: [];
foreach (var w in _open.Where(w => !wanted.Contains(w.Def)).ToList())
{
w.Close();
_open.Remove(w);
}
var wa = Screens.PrimaryWorkArea;
foreach (var def in wanted)
{
var w = _open.FirstOrDefault(x => x.Def == def);
if (w is not null) { w.Refresh(); continue; }
if (def.X is null)
{
// neue Box: oben rechts beginnen – links liegen meist die Desktop-Icons
int perRow = Math.Max(1, (int)((wa.Width - 48) / (def.W + 16)));
int i = _open.Count;
def.X = wa.Right - 24 - def.W - (i % perRow) * (def.W + 16);
def.Y = wa.Top + 24 + (i / perRow) * (def.H + 16);
}
w = new FenceWindow(def);
_open.Add(w);
w.Show();
}
}
public static void RefreshAll()
{
foreach (var w in _open) w.Refresh();
}
public static void CloseAll()
{
foreach (var w in _open) w.Close();
_open.Clear();
}
public static void Remove(FenceDef def)
{
S.Fences.Remove(def);
S.Save();
Sync();
App.Win?.RefreshFromFences();
}
/// <summary>
/// Kategorie aus den anderen Boxen derselben Seite entfernen – pro Seite steht eine Kategorie nur in einer Box.
/// Auf anderen Seiten darf sie zusätzlich liegen (z. B. „Internet“ auf „Arbeit“ und auf „Freizeit“).
/// </summary>
public static void RemoveCategoryTab(string category, FenceDef? except, int page)
{
foreach (var f in S.Fences.Where(f => f != except && f.Page == page))
f.Tabs.RemoveAll(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase));
S.Fences.RemoveAll(f => f != except && f.Tabs.Count == 0);
}
/// <summary>Nach Verschieben/Zusammenlegen: doppelte Kategorien auf einer Seite auflösen – „keep“ behält seine.</summary>
private static void ResolveDuplicates(int page, FenceDef? keep)
{
if (keep is not null)
foreach (var t in keep.Tabs.Where(t => t.IsCategory).ToList())
RemoveCategoryTab(t.Value, keep, page);
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var f in S.Fences.Where(f => f.Page == page))
f.Tabs.RemoveAll(t => t.IsCategory && !seen.Add(t.Value));
S.Fences.RemoveAll(f => f.Tabs.Count == 0);
foreach (var f in S.Fences) f.ActiveTab = Math.Clamp(f.ActiveTab, 0, Math.Max(0, f.Tabs.Count - 1));
}
/// <summary>Tab als eigene Box daneben lösen.</summary>
public static void DetachTab(FenceDef def, int index)
{
if (index < 0 || index >= def.Tabs.Count || def.Tabs.Count < 2) return;
var tab = def.Tabs[index];
def.Tabs.RemoveAt(index);
def.ActiveTab = Math.Clamp(def.ActiveTab, 0, def.Tabs.Count - 1);
S.Fences.Add(new FenceDef
{
Page = def.Page, Tabs = [tab], X = (def.X ?? 0) + 40, Y = (def.Y ?? 0) + 40, W = def.W, H = def.H,
Background = def.Background, Opacity = def.Opacity, Blur = def.Blur, IconSize = def.IconSize, Sort = def.Sort, Tint = def.Tint,
});
S.Save();
Sync();
}
public static string? PickFolder()
{
var dlg = new Microsoft.Win32.OpenFolderDialog { Title = "Ordner für das Portal wählen" };
return dlg.ShowDialog() == true ? dlg.FolderName : null;
}
/// <summary>Neue Box, die live einen beliebigen Ordner zeigt (z. B. Downloads).</summary>
public static void AddPortal()
{
if (PickFolder() is not { } folder) return;
S.Fences.Add(new FenceDef { Page = S.CurrentPage, Tabs = [new FenceTab { Kind = FenceTab.FolderKind, Value = folder }] });
S.FencesEnabled = true;
S.Save();
Sync();
}
/// <summary>Flächen der sichtbaren Boxen (WPF-Einheiten), z. B. damit die Icon-Tönung sie ausspart.</summary>
public static List<Rect> OpenRects() =>
_open.Where(w => w.IsVisible).Select(w => new Rect(w.Left, w.Top, w.ActualWidth, w.ActualHeight)).ToList();
/// <summary>Nach dem Aufziehen: Auswahl "Neue Box hier" an der Mausposition.</summary>
public static void ShowCreateMenu(Rect area)
{
BoxPicker.Show(area, tab =>
{
if (tab.IsCategory) RemoveCategoryTab(tab.Value, except: null, S.CurrentPage);
S.Fences.Add(new FenceDef
{
Page = S.CurrentPage, Tabs = [tab],
X = area.X, Y = area.Y, W = Math.Max(180, area.Width), H = Math.Max(120, area.Height),
});
S.FencesEnabled = true;
S.Save();
Sync();
App.Win?.RefreshFromFences();
});
}
// =====================================================================
// Desktop-Seiten
// =====================================================================
/// <summary>Zur nächsten/vorigen Seite wechseln (rundum).</summary>
public static void NextPage(int delta)
{
if (S.Pages.Count < 2) return;
SwitchPage((S.CurrentPage + delta + S.Pages.Count) % S.Pages.Count);
}
public static void SwitchPage(int index)
{
index = Math.Clamp(index, 0, S.Pages.Count - 1);
if (Peeking) EndPeek();
bool changed = index != S.CurrentPage;
S.CurrentPage = index;
S.Save();
if (changed) Sync();
PageScheduler.ApplyWallpaper(index);
PageIndicator.Show(S.Pages[index], index, S.Pages.Count);
App.Win?.RefreshFromFences();
}
public static void MoveToPage(FenceDef def, int page)
{
def.Page = Math.Clamp(page, 0, S.Pages.Count - 1);
ResolveDuplicates(def.Page, keep: def);
S.Save();
Sync();
App.Win?.RefreshFromFences();
}
/// <summary>Box mit gleichem Inhalt und Aussehen zusätzlich auf eine andere Seite legen.</summary>
public static void CopyToPage(FenceDef def, int page)
{
var copy = AppSettings.CloneFences([def])[0];
copy.Id = Guid.NewGuid().ToString("N");
copy.Page = Math.Clamp(page, 0, S.Pages.Count - 1);
S.Fences.Add(copy);
ResolveDuplicates(copy.Page, keep: copy);
S.Save();
Sync();
App.Win?.RefreshFromFences();
App.TrayIcon?.Notify("Box kopiert", $"Die Box liegt jetzt auch auf „{S.Pages[copy.Page]}“ – an derselben Stelle.");
}
/// <summary>Seite löschen: ihre Boxen wandern auf die Seite davor.</summary>
public static void DeletePage(int index)
{
if (S.Pages.Count < 2 || index < 0 || index >= S.Pages.Count) return;
S.Pages.RemoveAt(index);
foreach (var f in S.Fences)
if (f.Page == index) f.Page = Math.Max(0, index - 1);
else if (f.Page > index) f.Page--;
// Kategorien, die es auf beiden Seiten gab, nicht doppelt zeigen
ResolveDuplicates(Math.Max(0, index - 1), keep: null);
if (S.CurrentPage >= index && S.CurrentPage > 0) S.CurrentPage--;
S.Save();
CloseAll();
Sync();
}
// =====================================================================
// Aussehen
// =====================================================================
public static void EditLook(FenceWindow w)
{
var def = w.Def;
var res = Application.Current.Resources;
var before = AppSettings.CloneFences([def])[0];
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 };
// Hintergrund
panel.Children.Add(Label("Hintergrund", 0));
var bgPanel = new WrapPanel();
void AddSwatch(WrapPanel p, string group, string? hex, Brush brush, string tip, Func<string?> get, Action<string?> set)
{
var rb = new RadioButton { Style = (Style)res["SwatchRadio"], GroupName = group, Background = brush, ToolTip = tip, IsChecked = get() == hex };
rb.Checked += (_, _) => { set(hex); w.ApplyLook(); };
p.Children.Add(rb);
}
AddSwatch(bgPanel, "bg", null, (Brush)res["FenceBrush"], "Passend zum Design", () => def.Background, v => def.Background = v);
foreach (var (name, hex) in CategoryColors.All.Concat([("Schwarz", "#101010"), ("Weiß", "#F5F5F5")]))
AddSwatch(bgPanel, "bg", hex, CategoryColors.Brush(hex)!, name, () => def.Background, v => def.Background = v);
panel.Children.Add(bgPanel);
// Deckkraft
panel.Children.Add(Label("Deckkraft"));
var opacity = new Slider { Minimum = 20, Maximum = 100, Value = Math.Round(def.Opacity * 100), TickFrequency = 5, IsSnapToTickEnabled = true };
opacity.ValueChanged += (_, _) => { def.Opacity = opacity.Value / 100; w.ApplyLook(); };
panel.Children.Add(opacity);
var blur = new CheckBox { Content = "Milchglas (Hintergrund verschwommen)", IsChecked = def.Blur, Margin = new Thickness(0, 12, 0, 0) };
blur.Click += (_, _) => { def.Blur = blur.IsChecked == true; w.ApplyLook(); };
panel.Children.Add(blur);
// Icon-Größe und Sortierung als Segmente
StackPanel Segments(string group, (string Label, string Value)[] options, string current, Action<string> set)
{
var sp = new StackPanel { Orientation = Orientation.Horizontal };
foreach (var (label, value) in options)
{
var rb = new RadioButton { Style = (Style)res["SegmentRadio"], GroupName = group, Content = label, IsChecked = value == current };
rb.Checked += (_, _) => { set(value); w.Refresh(); };
sp.Children.Add(rb);
}
return sp;
}
panel.Children.Add(Label("Icon-Größe"));
panel.Children.Add(new Border
{
Style = (Style)res["SegmentGroup"], HorizontalAlignment = HorizontalAlignment.Left,
Child = Segments("size", [("Klein", "Small"), ("Mittel", "Medium"), ("Groß", "Large")], def.IconSize.ToString(),
v => def.IconSize = Enum.Parse<ViewMode>(v)),
});
panel.Children.Add(Label("Sortieren nach"));
panel.Children.Add(new TextBlock
{
Text = "„Eigene“: Icons in der Box einfach an die gewünschte Stelle ziehen.", FontSize = 12, Margin = new Thickness(0, -4, 0, 8),
TextWrapping = TextWrapping.Wrap, Foreground = (Brush)res["TextSecondaryBrush"],
});
panel.Children.Add(new Border
{
Style = (Style)res["SegmentGroup"], HorizontalAlignment = HorizontalAlignment.Left,
Child = Segments("sort", [("Name", "Name"), ("Typ", "Typ"), ("Datum", "Datum"), ("Meistgenutzt", "Häufig"), ("Eigene", "Eigene")],
def.Sort, v => def.Sort = v),
});
// Icon-Tönung
panel.Children.Add(Label("Icon-Tönung"));
var tintPanel = new WrapPanel();
AddSwatch(tintPanel, "tint", null, Brushes.Transparent, "Keine Tönung", () => def.Tint, v => def.Tint = v);
foreach (var (name, hex) in CategoryColors.All)
AddSwatch(tintPanel, "tint", hex, CategoryColors.Brush(hex)!, name, () => def.Tint, v => def.Tint = v);
panel.Children.Add(tintPanel);
var owner = App.Win!;
if (Dialog.Show(owner, "Aussehen der Box", panel, "Speichern", "Abbrechen", null, width: 470))
S.Save();
else
{
// Vorschau zurücknehmen
(def.Background, def.Opacity, def.Blur, def.IconSize, def.Sort, def.Tint) =
(before.Background, before.Opacity, before.Blur, before.IconSize, before.Sort, before.Tint);
w.Refresh();
}
}
// =====================================================================
// Peek & Schnell verstecken
// =====================================================================
public static void TogglePeek()
{
if (Peeking) EndPeek();
else StartPeek();
}
private static void StartPeek()
{
if (_open.Count == 0) return;
if (Hidden) SetHidden(false);
Peeking = true;
foreach (var w in _open) w.SetPeek(true);
_open[0].Activate();
// Peek endet, sobald ein anderes Fenster aktiv wird
_peekWatch ??= new DispatcherTimer(TimeSpan.FromMilliseconds(300), DispatcherPriority.Background, (_, _) =>
{
if (!_open.Any(w => w.IsActive) && !_open.Any(w => w.IsMouseOver)) EndPeek();
}, Application.Current.Dispatcher);
_peekWatch.Start();
}
public static void EndPeek()
{
_peekWatch?.Stop();
if (!Peeking) return;
Peeking = false;
foreach (var w in _open) w.SetPeek(false);
}
/// <summary>Alle Boxen aus- bzw. wieder einblenden (Schnell verstecken).</summary>
public static void SetHidden(bool hidden)
{
if (Hidden == hidden) return;
Hidden = hidden;
if (hidden) EndPeek();
Sync();
}
// =====================================================================
// Bildschirme & Snapshots
// =====================================================================
/// <summary>Kennung der aktuellen Bildschirm-Anordnung (Anzahl, Größen, Positionen).</summary>
private static string ScreenSignature() =>
string.Join("|", System.Windows.Forms.Screen.AllScreens
.OrderBy(s => s.Bounds.X).ThenBy(s => s.Bounds.Y)
.Select(s => $"{s.Bounds.X},{s.Bounds.Y},{s.Bounds.Width}x{s.Bounds.Height}"));
private static Dictionary<string, double[]> CurrentPositions() =>
S.Fences.Where(f => f.X is not null).ToDictionary(f => f.Id, f => new[] { f.X!.Value, f.Y!.Value, f.W, f.H });
/// <summary>
/// Bildschirm angesteckt/abgesteckt oder Auflösung geändert: Anordnung der alten Konstellation merken
/// und – falls bekannt – die zur neuen Konstellation passende wiederherstellen.
/// </summary>
public static void OnDisplayChanged()
{
var newSig = ScreenSignature();
if (_screenSignature is { } oldSig && oldSig != newSig)
{
S.ScreenLayouts[oldSig] = CurrentPositions();
if (S.ScreenLayouts.TryGetValue(newSig, out var layout))
{
foreach (var f in S.Fences)
if (layout.TryGetValue(f.Id, out var r) && r.Length == 4)
(f.X, f.Y, f.W, f.H) = (r[0], r[1], r[2], r[3]);
foreach (var w in _open) w.Reapply();
}
S.Save();
}
_screenSignature = newSig;
foreach (var w in _open) w.EnsureOnScreen();
}
public static void SaveSnapshot(string name)
{
S.Snapshots.RemoveAll(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase));
S.Snapshots.Add(new FenceSnapshot { Name = name, Fences = AppSettings.CloneFences(S.Fences) });
S.Save();
}
/// <summary>Anordnung wiederherstellen. Gibt die Anordnung davor zurück (für Rückgängig).</summary>
public static List<FenceDef> RestoreSnapshot(FenceSnapshot snap) => ApplyFences(AppSettings.CloneFences(snap.Fences));
public static List<FenceDef> ApplyFences(List<FenceDef> fences)
{
var before = AppSettings.CloneFences(S.Fences);
CloseAll();
S.Fences = fences;
S.FencesEnabled = true;
S.Save();
Sync();
return before;
}
}