using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; namespace DeskCore; /// Verwaltet alle Desktop-Boxen: Anzeigen, Peek, Ausblenden, Snapshots und Anordnung je Bildschirm-Setup. public static class FenceManager { private static readonly List _open = []; private static AppSettings S => AppSettings.Current; private static DispatcherTimer? _peekWatch; private static string? _screenSignature; /// Boxen gerade per "Schnell verstecken" ausgeblendet? public static bool Hidden { get; private set; } /// Boxen gerade per Peek vor allen Fenstern? 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(); } /// Kategorie aus allen Boxen (außer except) entfernen – eine Kategorie soll nur in einer Box stehen. 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); } /// Tab als eigene Box daneben lösen. 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; } /// Neue Box, die live einen beliebigen Ordner zeigt (z. B. Downloads). 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(); } /// Flächen der sichtbaren Boxen (WPF-Einheiten), z. B. damit die Icon-Tönung sie ausspart. public static List OpenRects() => _open.Where(w => w.IsVisible).Select(w => new Rect(w.Left, w.Top, w.ActualWidth, w.ActualHeight)).ToList(); /// Nach dem Aufziehen: Menü "Neue Box hier" an der Mausposition. 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 // ===================================================================== /// Zur nächsten/vorigen Seite wechseln (rundum). 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(); } /// Seite löschen: ihre Boxen wandern auf die Seite davor. 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 get, Action 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 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(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); } /// Alle Boxen aus- bzw. wieder einblenden (Schnell verstecken). public static void SetHidden(bool hidden) { if (Hidden == hidden) return; Hidden = hidden; if (hidden) EndPeek(); Sync(); } // ===================================================================== // Bildschirme & Snapshots // ===================================================================== /// Kennung der aktuellen Bildschirm-Anordnung (Anzahl, Größen, Positionen). 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 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 }); /// /// Bildschirm angesteckt/abgesteckt oder Auflösung geändert: Anordnung der alten Konstellation merken /// und – falls bekannt – die zur neuen Konstellation passende wiederherstellen. /// 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(); } /// Anordnung wiederherstellen. Gibt die Anordnung davor zurück (für Rückgängig). public static List RestoreSnapshot(FenceSnapshot snap) => ApplyFences(AppSettings.CloneFences(snap.Fences)); public static List ApplyFences(List fences) { var before = AppSettings.CloneFences(S.Fences); CloseAll(); S.Fences = fences; S.FencesEnabled = true; S.Save(); Sync(); return before; } }