395 lines
16 KiB
C#
395 lines
16 KiB
C#
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;
|
||
}
|
||
}
|