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

635 lines
28 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.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shell;
using System.Windows.Threading;
namespace DeskCore;
/// <summary>
/// Eine Box auf dem Desktop: zeigt eine oder mehrere Kategorien bzw. Ordner (Tabs),
/// mit eigenem Aussehen, Einklappen, Sperren, Chamäleon und Peek.
/// </summary>
public partial class FenceWindow : Window
{
private const double CollapsedHeight = 38;
private readonly DispatcherTimer _saveTimer;
private readonly DispatcherTimer _collapseTimer;
private FileSystemWatcher? _portalWatcher;
private DispatcherTimer? _portalRefresh;
private bool _applying;
private bool _hoverExpanded;
private bool _mouseInside;
/// <summary>Unterordner, in den im Ordner-Portal navigiert wurde (null = Wurzel).</summary>
private string? _portalSub;
public FenceDef Def { get; }
private static AppSettings S => AppSettings.Current;
public FenceWindow(FenceDef def)
{
InitializeComponent();
Def = def;
_saveTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(700), DispatcherPriority.Background, (_, _) =>
{
_saveTimer!.Stop();
S.Save();
}, Dispatcher);
_saveTimer.Stop();
_collapseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
_collapseTimer.Tick += (_, _) =>
{
_collapseTimer.Stop();
if (!_mouseInside && _hoverExpanded && !ItemMenu.IsOpen && !FenceMenu.IsOpen) CollapseAfterHover();
};
ApplyBounds();
LocationChanged += (_, _) => RememberBounds();
SizeChanged += (_, _) => RememberBounds();
SourceInitialized += (_, _) => PinToDesktop();
MouseEnter += (_, _) => OnHover(true);
MouseLeave += (_, _) => OnHover(false);
Closed += (_, _) => _portalWatcher?.Dispose();
Refresh();
}
// =====================================================================
// Position, Größe, Einklappen
// =====================================================================
private void ApplyBounds()
{
_applying = true;
Width = Math.Max(MinWidth, Def.W);
Height = Def.Collapsed && !_hoverExpanded ? CollapsedHeight : Math.Max(120, Def.H);
var wa = Screens.PrimaryWorkArea;
double left = Def.X ?? wa.Right - Width - 24, top = Def.Y ?? wa.Top + 24;
// Bildschirm abgesteckt? → zurück auf den Hauptbildschirm (oben rechts)
if (!Screens.IsReachable(new Rect(left, top, Width, Height)))
{
left = wa.Right - Width - 24;
top = wa.Top + 24;
Def.X = left;
Def.Y = top;
}
Left = left;
Top = top;
Body.Visibility = Def.Collapsed && !_hoverExpanded ? Visibility.Collapsed : Visibility.Visible;
_applying = false;
}
/// <summary>Position/Größe aus den Einstellungen neu übernehmen (z. B. nach Snapshot).</summary>
public void Reapply()
{
ApplyBounds();
ApplyLook();
Refresh();
}
public void EnsureOnScreen()
{
if (Screens.IsReachable(new Rect(Left, Top, ActualWidth, ActualHeight))) return;
Def.X = null;
Def.Y = null;
ApplyBounds();
S.Save();
}
private void RememberBounds()
{
if (_applying || !IsLoaded || _hoverExpanded) return;
Def.X = Left;
Def.Y = Top;
Def.W = Width;
if (!Def.Collapsed) Def.H = Height;
_saveTimer.Stop();
_saveTimer.Start();
}
private void ToggleCollapsed()
{
_hoverExpanded = false;
Def.Collapsed = !Def.Collapsed;
ApplyBounds();
S.Save();
}
/// <summary>Eingeklappte Box klappt beim Überfahren kurz auf; Chamäleon blendet ein/aus.</summary>
private void OnHover(bool inside)
{
_mouseInside = inside;
if (inside)
{
_collapseTimer.Stop();
if (Def.Collapsed && !_hoverExpanded)
{
_hoverExpanded = true;
ApplyBounds();
}
}
else if (_hoverExpanded) _collapseTimer.Start();
ApplyChameleon();
}
private void CollapseAfterHover()
{
_hoverExpanded = false;
ApplyBounds();
}
// =====================================================================
// Aussehen
// =====================================================================
public void ApplyLook()
{
// Hintergrund: eigene Farbe oder Design-Farbe, mit eingestellter Deckkraft
var baseColor = Def.Background is { } hex && CategoryColors.Brush(hex) is SolidColorBrush custom
? custom.Color
: ((SolidColorBrush)FindResource("FenceBrush")).Color;
double opacity = Math.Clamp(Def.Opacity, 0.2, 1);
if (Def.Blur) opacity = Math.Min(opacity, 0.75); // sonst sieht man vom Milchglas nichts
var bg = new SolidColorBrush(Color.FromArgb((byte)(opacity * 255), baseColor.R, baseColor.G, baseColor.B));
bg.Freeze();
Root.Background = bg;
Root.CornerRadius = new CornerRadius(Def.Blur ? 0 : 8); // Milchglas wirkt nur eckig sauber
// Icon-Größe
var (w, h, icon) = Def.IconSize switch
{
ViewMode.Small => (64.0, 66.0, 24.0),
ViewMode.Large => (98.0, 104.0, 48.0),
_ => (78.0, 80.0, 32.0),
};
Resources["FTileW"] = w;
Resources["FTileH"] = h;
Resources["FIcon"] = icon;
Resources["FTint"] = CategoryColors.Brush(Def.Tint) ?? Brushes.Transparent;
// Sperren: nicht verschieb-/vergrößerbar
Chrome.CaptionHeight = Def.Locked ? 0 : 36;
Chrome.ResizeBorderThickness = Def.Locked ? new Thickness(0) : new Thickness(6);
LockGlyph.Visibility = Def.Locked ? Visibility.Visible : Visibility.Collapsed;
if (IsLoaded) SetBlur(Def.Blur);
ApplyChameleon();
}
/// <summary>Chamäleon: Box ist fast unsichtbar, bis die Maus darüberfährt (nicht während Peek).</summary>
private void ApplyChameleon()
{
double target = S.FenceChameleon && !_mouseInside && !FenceManager.Peeking ? 0.08 : 1;
Root.BeginAnimation(OpacityProperty, new DoubleAnimation(target, TimeSpan.FromMilliseconds(target < 1 ? 600 : 150)));
}
// =====================================================================
// Inhalt & Tabs
// =====================================================================
private FenceTab? Tab => Def.Active;
/// <summary>Ordner des aktiven Tabs (bei Ordner-Portalen ggf. ein Unterordner).</summary>
private string? CurrentFolder => Tab switch
{
null => null,
{ IsFolder: true } t => _portalSub ?? t.Value,
{ } t => Storage.CategoryPath(t.Value),
};
public void Refresh()
{
ApplyLook();
BuildTabs();
var tab = Tab;
var folder = CurrentFolder;
bool portal = tab?.IsFolder == true;
PortalBar.Visibility = portal && _portalSub is not null ? Visibility.Visible : Visibility.Collapsed;
if (portal && _portalSub is not null) PortalPath.Text = Path.GetRelativePath(tab!.Value, _portalSub);
WatchPortal(portal ? folder : null);
List<IconItem> items = [];
if (folder is not null && Directory.Exists(folder))
items = (portal ? PortalEntries(folder) : Storage.Entries(folder)).Select(p => new IconItem { Path = p }).ToList();
items = Sort(items);
int px = (int)Math.Round((double)Resources["FIcon"] * VisualTreeHelper.GetDpi(this).DpiScaleX);
foreach (var i in items) IconLoader.Load(i, px);
Items.ItemsSource = items;
CountText.Text = items.Count.ToString();
EmptyText.Text = folder is not null && !Directory.Exists(folder) ? "Ordner nicht gefunden" : "Leer – Icons hierher ziehen";
EmptyText.Visibility = items.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
/// <summary>Ordner-Portal zeigt alles außer versteckten/System-Dateien.</summary>
private static IEnumerable<string> PortalEntries(string dir) => Storage.Entries(dir);
private List<IconItem> Sort(List<IconItem> items)
{
var folders = items.Where(i => Directory.Exists(i.Path));
var files = items.Where(i => !Directory.Exists(i.Path));
IEnumerable<IconItem> Order(IEnumerable<IconItem> x) => Def.Sort switch
{
"Typ" => x.OrderBy(i => Path.GetExtension(i.Path), StringComparer.OrdinalIgnoreCase).ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase),
"Datum" => x.OrderByDescending(i => { try { return File.GetLastWriteTime(i.Path); } catch { return DateTime.MinValue; } }),
_ => x.OrderBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase),
};
return Order(folders).Concat(Order(files)).ToList();
}
private void BuildTabs()
{
TabStrip.Children.Clear();
for (int i = 0; i < Def.Tabs.Count; i++)
{
var t = Def.Tabs[i];
bool active = i == Math.Clamp(Def.ActiveTab, 0, Def.Tabs.Count - 1);
var meta = t.IsFolder ? null : S.Categories.GetValueOrDefault(t.Value);
var glyph = new TextBlock
{
Text = t.IsFolder ? "" : meta?.Glyph ?? "",
FontFamily = (FontFamily)FindResource("IconFont"), FontSize = 13, VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 7, 0),
Foreground = CategoryColors.Brush(meta?.Color) ?? (Brush)FindResource(t.IsFolder ? "AccentBrush" : "TextBrush"),
};
var label = new TextBlock
{
Text = t.Title, VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis,
FontSize = 13, MaxWidth = Def.Tabs.Count == 1 ? 220 : 120,
FontWeight = active ? FontWeights.SemiBold : FontWeights.Normal,
Foreground = (Brush)FindResource(active ? "TextBrush" : "TextSecondaryBrush"),
};
var content = new StackPanel { Orientation = Orientation.Horizontal, Children = { glyph, label } };
var underline = new Border
{
Height = 2, CornerRadius = new CornerRadius(1), Margin = new Thickness(4, 0, 4, 0), VerticalAlignment = VerticalAlignment.Bottom,
Background = active && Def.Tabs.Count > 1 ? (Brush)FindResource("AccentBrush") : Brushes.Transparent,
};
var btn = new Button
{
Style = (Style)FindResource("SubtleButton"), Padding = new Thickness(6, 4, 6, 4), MinHeight = 0, Tag = i,
Content = new Grid { Children = { content, underline } }, Margin = new Thickness(0, 0, 2, 0),
ToolTip = t.IsFolder ? t.Value : $"Kategorie „{t.Value}“",
};
WindowChrome.SetIsHitTestVisibleInChrome(btn, Def.Tabs.Count > 1); // bei nur einem Tab: Titel = Ziehfläche
btn.Click += (_, _) => SwitchTab((int)btn.Tag);
btn.MouseRightButtonUp += (_, ev) => { ShowTabMenu((int)btn.Tag); ev.Handled = true; };
TabStrip.Children.Add(btn);
}
}
private void SwitchTab(int index)
{
if (index == Def.ActiveTab) return;
Def.ActiveTab = index;
_portalSub = null;
S.Save();
Refresh();
}
// ---------- Ordner-Portal ----------
private void WatchPortal(string? folder)
{
if (string.Equals(_portalWatcher?.Path, folder, StringComparison.OrdinalIgnoreCase)) return;
_portalWatcher?.Dispose();
_portalWatcher = null;
if (folder is null || !Directory.Exists(folder)) return;
_portalRefresh ??= new DispatcherTimer(TimeSpan.FromMilliseconds(400), DispatcherPriority.Background, (_, _) =>
{
_portalRefresh!.Stop();
Refresh();
}, Dispatcher);
try
{
_portalWatcher = new FileSystemWatcher(folder)
{
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite,
EnableRaisingEvents = true,
};
FileSystemEventHandler poke = (_, _) => Dispatcher.BeginInvoke(() => { _portalRefresh.Stop(); _portalRefresh.Start(); });
_portalWatcher.Created += poke;
_portalWatcher.Deleted += poke;
_portalWatcher.Renamed += (s, e) => poke(s, e);
}
catch { }
}
private void PortalUp_Click(object sender, RoutedEventArgs e)
{
if (Tab is not { IsFolder: true } t || _portalSub is null) return;
var parent = Path.GetDirectoryName(_portalSub.TrimEnd('\\'));
_portalSub = parent is null || Storage.SamePath(parent, t.Value) || !Storage.IsUnder(parent, t.Value) ? null : parent;
Refresh();
}
// =====================================================================
// Icons
// =====================================================================
private List<IconItem> Selected => Items.SelectedItems.Cast<IconItem>().ToList();
private void Open(IconItem i)
{
// Im Ordner-Portal öffnen Ordner sich innerhalb der Box
if (Tab?.IsFolder == true && Directory.Exists(i.Path) && !i.Path.EndsWith(".lnk", StringComparison.OrdinalIgnoreCase))
{
_portalSub = i.Path;
Refresh();
return;
}
try { Process.Start(new ProcessStartInfo(i.Path) { UseShellExecute = true }); }
catch { }
}
private void Items_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (MainWindow.FindParent<ListBoxItem>(e.OriginalSource as DependencyObject)?.DataContext is IconItem i) Open(i);
}
private void Items_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape && FenceManager.Peeking) { FenceManager.EndPeek(); e.Handled = true; return; }
var sel = Selected;
if (sel.Count == 0 || App.Win is not { } win) return;
switch (e.Key)
{
case Key.Enter: sel.ForEach(Open); e.Handled = true; break;
case Key.Delete: win.DeleteIcons(sel); e.Handled = true; break;
case Key.F2 when sel.Count == 1: win.RenameIcon(sel[0]); e.Handled = true; break;
case Key.Back when _portalSub is not null: PortalUp_Click(this, e); e.Handled = true; break;
}
}
private void Items_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
var sel = Selected;
if (sel.Count == 0 || App.Win is not { } win) { e.Handled = true; return; }
ItemMenu.Items.Clear();
ItemMenu.Items.Add(MainWindow.MenuItem("Öffnen", "", () => sel.ForEach(Open), bold: true, gesture: "Enter"));
if (sel.Count == 1)
{
ItemMenu.Items.Add(MainWindow.MenuItem("Dateispeicherort öffnen", "", () =>
Process.Start("explorer.exe", $"/select,\"{sel[0].Path}\"")));
ItemMenu.Items.Add(MainWindow.MenuItem("Umbenennen", "", () => win.RenameIcon(sel[0]), gesture: "F2"));
}
ItemMenu.Items.Add(new Separator());
bool inStorage = sel.All(i => Storage.IsInStorage(i.Path));
var moveTo = MainWindow.MenuItem(inStorage ? "Verschieben nach" : "Sichern in", inStorage ? "" : "", null);
var ownCategory = Tab is { IsFolder: false } t ? t.Value : null;
foreach (var c in Storage.Categories().Where(c => !string.Equals(c, ownCategory, StringComparison.OrdinalIgnoreCase)))
moveTo.Items.Add(MainWindow.MenuItem(c, S.Categories.GetValueOrDefault(c)?.Glyph ?? "",
() => win.TransferAll(sel.Select(i => i.Path), Storage.CategoryPath(c))));
if (moveTo.Items.Count > 0) ItemMenu.Items.Add(moveTo);
ItemMenu.Items.Add(win.LaunchGroupSubmenu(sel.Select(i => i.Path).ToList()));
if (inStorage)
ItemMenu.Items.Add(MainWindow.MenuItem("Zurück auf den Desktop legen", "",
() => win.TransferAll(sel.Select(i => i.Path), Storage.UserDesktop)));
ItemMenu.Items.Add(MainWindow.MenuItem("Löschen", "", () => win.DeleteIcons(sel), gesture: "Entf"));
}
// ---------- Ziehen ----------
private Point _dragStart;
private bool _dragging;
private void Items_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) => _dragStart = e.GetPosition(null);
private void Items_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || _dragging) return;
var d = e.GetPosition(null) - _dragStart;
if (Math.Abs(d.X) < SystemParameters.MinimumHorizontalDragDistance &&
Math.Abs(d.Y) < SystemParameters.MinimumVerticalDragDistance) return;
if (MainWindow.FindParent<ListBoxItem>(e.OriginalSource as DependencyObject) is null) return;
var paths = Selected.Select(i => i.Path).ToArray();
if (paths.Length == 0) return;
_dragging = true;
try
{
// nur "Kopieren" erlauben: der Explorer soll die Datei nicht aus der Kategorie entfernen.
// In eine andere Box/Kategorie gezogen, verschiebt DeskCore selbst.
DragDrop.DoDragDrop(Items, new DataObject(DataFormats.FileDrop, paths), DragDropEffects.Copy | DragDropEffects.Link);
}
finally { _dragging = false; }
}
private void Items_DragOver(object sender, DragEventArgs e)
{
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) && !_dragging && CurrentFolder is not null
? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move)
: DragDropEffects.None;
e.Handled = true;
}
private void Items_Drop(object sender, DragEventArgs e)
{
if (!_dragging && CurrentFolder is { } folder && e.Data.GetData(DataFormats.FileDrop) is string[] files)
App.Win?.TransferAll(files, folder);
}
// =====================================================================
// Menüs
// =====================================================================
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// gesperrte Box: Titelleiste ist normale Fläche → Doppelklick selbst auswerten
if (e.ClickCount == 2) { ToggleCollapsed(); e.Handled = true; }
}
private void MenuButton_Click(object sender, RoutedEventArgs e)
{
FenceMenu.Items.Clear();
if (Tab is { IsFolder: false } t)
FenceMenu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value)));
else if (Tab is { IsFolder: true } pt)
FenceMenu.Items.Add(MainWindow.MenuItem("Im Explorer öffnen", "", () => Process.Start("explorer.exe", $"\"{CurrentFolder ?? pt.Value}\"")));
// Tabs
var add = MainWindow.MenuItem("Tab hinzufügen", "", null);
foreach (var c in Storage.Categories().Where(c => !Def.Tabs.Any(x => !x.IsFolder && string.Equals(x.Value, c, StringComparison.OrdinalIgnoreCase))))
add.Items.Add(MainWindow.MenuItem(c, S.Categories.GetValueOrDefault(c)?.Glyph ?? "", () => AddTab(new FenceTab { Value = c })));
if (add.Items.Count > 0) add.Items.Add(new Separator());
add.Items.Add(MainWindow.MenuItem("Ordner-Portal…", "", () =>
{
if (FenceManager.PickFolder() is { } folder) AddTab(new FenceTab { Kind = FenceTab.FolderKind, Value = folder });
}));
FenceMenu.Items.Add(add);
if (Def.Tabs.Count > 1)
FenceMenu.Items.Add(MainWindow.MenuItem($"Tab „{Tab?.Title}“ entfernen", "", () => RemoveTab(Def.ActiveTab)));
FenceMenu.Items.Add(new Separator());
FenceMenu.Items.Add(MainWindow.MenuItem("Aussehen…", "", () => FenceManager.EditLook(this)));
FenceMenu.Items.Add(MainWindow.MenuItem(Def.Collapsed ? "Ausklappen" : "Einklappen",
Def.Collapsed ? "" : "", ToggleCollapsed));
FenceMenu.Items.Add(MainWindow.MenuItem(Def.Locked ? "Entsperren" : "Sperren (nicht mehr verschiebbar)",
Def.Locked ? "" : "", () => { Def.Locked = !Def.Locked; S.Save(); ApplyLook(); }));
if (S.Pages.Count > 1)
{
var move = MainWindow.MenuItem("Auf Seite verschieben", "\uE8AB", null);
for (int p = 0; p < S.Pages.Count; p++)
{
int page = p;
var mi = MainWindow.MenuItem(S.Pages[p], "\uE8A1", () => FenceManager.MoveToPage(Def, page));
mi.IsEnabled = page != Def.Page;
move.Items.Add(mi);
}
FenceMenu.Items.Add(move);
}
FenceMenu.Items.Add(new Separator());
FenceMenu.Items.Add(MainWindow.MenuItem("Box schließen", "", () => FenceManager.Remove(Def)));
FenceMenu.PlacementTarget = MenuButton;
FenceMenu.IsOpen = true;
}
private void ShowTabMenu(int index)
{
if (index < 0 || index >= Def.Tabs.Count) return;
var t = Def.Tabs[index];
var menu = new ContextMenu();
if (!t.IsFolder) menu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value)));
else menu.Items.Add(MainWindow.MenuItem("Im Explorer öffnen", "", () => Process.Start("explorer.exe", $"\"{t.Value}\"")));
if (Def.Tabs.Count > 1)
{
menu.Items.Add(MainWindow.MenuItem("Als eigene Box lösen", "", () => FenceManager.DetachTab(Def, index)));
menu.Items.Add(MainWindow.MenuItem("Tab entfernen", "", () => RemoveTab(index)));
}
menu.IsOpen = true;
}
private void AddTab(FenceTab tab)
{
// Kategorie steckt schon in einer anderen Box? Dann dorthin verschieben statt doppelt anzeigen
if (!tab.IsFolder) FenceManager.RemoveCategoryTab(tab.Value, except: Def);
Def.Tabs.Add(tab);
Def.ActiveTab = Def.Tabs.Count - 1;
_portalSub = null;
S.Save();
FenceManager.Sync();
Refresh();
}
private void RemoveTab(int index)
{
if (index < 0 || index >= Def.Tabs.Count) return;
Def.Tabs.RemoveAt(index);
Def.ActiveTab = Math.Clamp(Def.ActiveTab, 0, Math.Max(0, Def.Tabs.Count - 1));
_portalSub = null;
S.Save();
if (Def.Tabs.Count == 0) FenceManager.Remove(Def);
else Refresh();
}
// =====================================================================
// Auf dem Desktop festhalten, Peek, Milchglas
// =====================================================================
private IntPtr Hwnd => new WindowInteropHelper(this).Handle;
private void PinToDesktop()
{
var hwnd = Hwnd;
// Besitzer = Desktop (Progman): bleibt bei Win+D sichtbar und liegt über den Desktop-Icons
var progman = FindWindow("Progman", null);
if (progman != IntPtr.Zero) SetWindowLongPtr(hwnd, GWLP_HWNDPARENT, progman);
// Nicht in Alt+Tab
var ex = GetWindowLongPtr(hwnd, GWL_EXSTYLE).ToInt64();
SetWindowLongPtr(hwnd, GWL_EXSTYLE, new IntPtr((ex | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW));
HwndSource.FromHwnd(hwnd).AddHook(Hook);
SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
SetBlur(Def.Blur);
}
/// <summary>Peek: Box vor alle Fenster holen bzw. wieder ganz nach hinten.</summary>
public void SetPeek(bool on)
{
var hwnd = Hwnd;
if (hwnd == IntPtr.Zero) return;
SetWindowPos(hwnd, on ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
if (!on) SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
ApplyChameleon();
}
private IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case WM_WINDOWPOSCHANGING when !FenceManager.Peeking:
// immer ganz unten bleiben, nie über anderen Fenstern (außer während Peek)
var wp = Marshal.PtrToStructure<WINDOWPOS>(lParam);
wp.hwndInsertAfter = HWND_BOTTOM;
wp.flags &= ~SWP_NOZORDER;
Marshal.StructureToPtr(wp, lParam, false);
break;
case WM_NCLBUTTONDBLCLK:
ToggleCollapsed();
handled = true;
break;
case WM_SYSCOMMAND when (wParam.ToInt32() & 0xFFF0) == SC_MAXIMIZE:
handled = true;
break;
}
return IntPtr.Zero;
}
/// <summary>Milchglas (verschwommener Hintergrund) über die Windows-Composition-Schnittstelle.</summary>
private void SetBlur(bool on)
{
var hwnd = Hwnd;
if (hwnd == IntPtr.Zero) return;
var accent = new AccentPolicy { AccentState = on ? 3 : 0 }; // 3 = ACCENT_ENABLE_BLURBEHIND
int size = Marshal.SizeOf(accent);
var ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(accent, ptr, false);
var data = new WindowCompositionAttributeData { Attribute = 19, Data = ptr, SizeOfData = size }; // 19 = WCA_ACCENT_POLICY
SetWindowCompositionAttribute(hwnd, ref data);
}
catch { }
finally { Marshal.FreeHGlobal(ptr); }
}
private const int GWL_EXSTYLE = -20, GWLP_HWNDPARENT = -8;
private const long WS_EX_TOOLWINDOW = 0x80, WS_EX_APPWINDOW = 0x40000;
private const int WM_WINDOWPOSCHANGING = 0x46, WM_NCLBUTTONDBLCLK = 0xA3, WM_SYSCOMMAND = 0x112, SC_MAXIMIZE = 0xF030;
private const uint SWP_NOSIZE = 0x1, SWP_NOMOVE = 0x2, SWP_NOZORDER = 0x4, SWP_NOACTIVATE = 0x10;
private static readonly IntPtr HWND_BOTTOM = new(1), HWND_TOPMOST = new(-1), HWND_NOTOPMOST = new(-2);
[StructLayout(LayoutKind.Sequential)]
private struct WINDOWPOS
{
public IntPtr hwnd, hwndInsertAfter;
public int x, y, cx, cy;
public uint flags;
}
[StructLayout(LayoutKind.Sequential)]
private struct AccentPolicy
{
public int AccentState, AccentFlags, GradientColor, AnimationId;
}
[StructLayout(LayoutKind.Sequential)]
private struct WindowCompositionAttributeData
{
public int Attribute;
public IntPtr Data;
public int SizeOfData;
}
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindow(string? cls, string? title);
[DllImport("user32.dll")] private static extern IntPtr GetWindowLongPtr(IntPtr h, int i);
[DllImport("user32.dll")] private static extern IntPtr SetWindowLongPtr(IntPtr h, int i, IntPtr v);
[DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
[DllImport("user32.dll")] private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
}