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

395 lines
16 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.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 allen Boxen (außer except) entfernen – eine Kategorie soll nur in einer Box stehen.</summary>
public static void RemoveCategoryTab(string category, FenceDef? except)
{
foreach (var f in S.Fences.Where(f => f != except))
f.Tabs.RemoveAll(t => !t.IsFolder && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase));
S.Fences.RemoveAll(f => f != except && f.Tabs.Count == 0);
}
/// <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: Menü "Neue Box hier" an der Mausposition.</summary>
public static void ShowCreateMenu(Rect area)
{
var menu = new ContextMenu { Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint };
var header = MainWindow.MenuItem("Neue Box hier für …", "\uE8A1", null, bold: true);
header.IsEnabled = false;
menu.Items.Add(header);
menu.Items.Add(new Separator());
void Create(FenceTab tab)
{
if (!tab.IsFolder) RemoveCategoryTab(tab.Value, except: null);
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();
}
foreach (var c in Storage.Categories())
{
var label = S.HasFence(c) ? $"{c} (aus anderer Box hierher holen)" : c;
menu.Items.Add(MainWindow.MenuItem(label, S.Categories.GetValueOrDefault(c)?.Glyph ?? "\uF12B",
() => Create(new FenceTab { Value = c })));
}
if (Storage.Categories().Count > 0) menu.Items.Add(new Separator());
menu.Items.Add(MainWindow.MenuItem("Neue Kategorie …", "\uE710", () =>
{
if (App.Win?.CreateCategoryInteractive() is { } name) Create(new FenceTab { Value = name });
}));
menu.Items.Add(MainWindow.MenuItem("Ordner-Portal …", "\uF12B", () =>
{
if (PickFolder() is { } folder) Create(new FenceTab { Kind = FenceTab.FolderKind, Value = folder });
}));
menu.Items.Add(new Separator());
menu.Items.Add(MainWindow.MenuItem("Abbrechen", "\uE711", () => { }));
// Damit das Menü bei Klick daneben zugeht, muss es im Vordergrund sein
menu.Opened += (_, _) =>
{
if (PresentationSource.FromVisual(menu) is System.Windows.Interop.HwndSource src) SetForegroundWindow(src.Handle);
};
menu.IsOpen = true;
}
[System.Runtime.InteropServices.DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hwnd);
// =====================================================================
// 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();
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);
S.Save();
Sync();
}
/// <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--;
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 Border
{
Style = (Style)res["SegmentGroup"], HorizontalAlignment = HorizontalAlignment.Left,
Child = Segments("sort", [("Name", "Name"), ("Typ", "Typ"), ("Datum", "Datum")], 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;
}
}