Files
DeskCore/FenceWindow.xaml.cs
T
2026-10-04 14:48:53 +00:00

733 lines
33 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);
Usage.Changed += OnUsageChanged;
Closed += (_, _) => { _portalWatcher?.Dispose(); Usage.Changed -= OnUsageChanged; };
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 (tab?.IsRecent == true)
items = Usage.Recent(RecentCount).Select(p => new IconItem { Path = p }).ToList();
else 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, tab);
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 = tab?.IsRecent == true ? "Noch nichts gestartet – was du über DeskCore öffnest, erscheint hier"
: 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);
/// <summary>Wie viele Einträge eine „Zuletzt benutzt“-Box zeigt.</summary>
private const int RecentCount = 24;
private void OnUsageChanged()
{
// Zuletzt-Tab oder Sortierung nach Häufigkeit → neu ordnen (kurz verzögert, falls mehrere Starts kommen)
if (Tab?.IsRecent == true || Def.Sort == "Häufig") Dispatcher.BeginInvoke(Refresh, DispatcherPriority.Background);
}
private List<IconItem> Sort(List<IconItem> items, FenceTab? tab)
{
// Zuletzt benutzt: neueste zuerst – außer es ist ausdrücklich "Häufig" gewählt
if (tab?.IsRecent == true)
return Def.Sort == "Häufig" ? items.OrderByDescending(i => Usage.Score(i.Path)).ToList() : items;
// Eigene Reihenfolge: wie gezogen; Neues hinten dran (alphabetisch)
if (Def.Sort == "Eigene" && tab is not null)
{
var order = tab.Order;
return items.OrderBy(i => order.FindIndex(n => string.Equals(n, Path.GetFileName(i.Path), StringComparison.OrdinalIgnoreCase)) is var k and >= 0 ? k : int.MaxValue)
.ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase).ToList();
}
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; } }),
"Häufig" => x.OrderByDescending(i => Usage.Score(i.Path)).ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase),
_ => 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.IsCategory ? S.Categories.GetValueOrDefault(t.Value) : null;
var glyph = new TextBlock
{
Text = t.IsRecent ? "" : 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.IsCategory ? "TextBrush" : "AccentBrush"),
};
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.IsRecent ? "Was du zuletzt über DeskCore gestartet hast" : 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 { Usage.Start(i.Path); }
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 { IsCategory: true } 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)
{
if (_dragging)
{
// Innerhalb der eigenen Box: neu anordnen (nicht bei „Zuletzt benutzt“ – die ordnet sich selbst)
e.Effects = Tab is { IsRecent: false } ? DragDropEffects.Copy : DragDropEffects.None;
ShowDropMarker(e.Effects == DragDropEffects.None ? null : e.GetPosition(Items));
}
else
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) && CurrentFolder is not null
? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move)
: DragDropEffects.None;
e.Handled = true;
}
private void Items_DragLeave(object sender, DragEventArgs e) => ShowDropMarker(null);
private void Items_Drop(object sender, DragEventArgs e)
{
ShowDropMarker(null);
if (_dragging)
{
if (Tab is { IsRecent: false } t) Reorder(t, Selected, e.GetPosition(Items));
return;
}
if (CurrentFolder is { } folder && e.Data.GetData(DataFormats.FileDrop) is string[] files)
App.Win?.TransferAll(files, folder);
}
// ---------- Eigene Reihenfolge ----------
/// <summary>Einfügestelle an der Mausposition: vor welchem Eintrag landet das Gezogene? (Count = ans Ende)</summary>
private (int Index, Rect? Marker) InsertionAt(Point p)
{
var list = Items.ItemsSource as List<IconItem> ?? [];
for (int k = 0; k < list.Count; k++)
{
if (Items.ItemContainerGenerator.ContainerFromIndex(k) is not ListBoxItem c || !c.IsVisible) continue;
var r = new Rect(c.TranslatePoint(new Point(0, 0), Items), c.RenderSize);
if (p.Y < r.Top) return (k, new Rect(r.Left - 2, r.Top, 3, r.Height)); // Zeile darüber, ganz links
if (p.Y <= r.Bottom && p.X < r.Left + r.Width / 2) return (k, new Rect(r.Left - 2, r.Top, 3, r.Height));
}
if (list.Count > 0 && Items.ItemContainerGenerator.ContainerFromIndex(list.Count - 1) is ListBoxItem last)
{
var r = new Rect(last.TranslatePoint(new Point(0, 0), Items), last.RenderSize);
return (list.Count, new Rect(r.Right - 1, r.Top, 3, r.Height));
}
return (list.Count, null);
}
private void ShowDropMarker(Point? p)
{
if (p is { } pos && InsertionAt(pos).Marker is { } m)
{
DropMarker.Margin = new Thickness(m.Left, m.Top, 0, 0);
DropMarker.Height = m.Height;
DropMarker.Visibility = Visibility.Visible;
}
else DropMarker.Visibility = Visibility.Collapsed;
}
private void Reorder(FenceTab tab, List<IconItem> moved, Point drop)
{
var list = Items.ItemsSource as List<IconItem> ?? [];
if (moved.Count == 0) return;
int index = InsertionAt(drop).Index;
// Einfügestelle in der Liste ohne die gezogenen Einträge
int before = list.Take(index).Count(i => moved.Contains(i));
var rest = list.Where(i => !moved.Contains(i)).ToList();
rest.InsertRange(Math.Clamp(index - before, 0, rest.Count), moved);
tab.Order = rest.Select(i => Path.GetFileName(i.Path)).ToList();
bool switched = Def.Sort != "Eigene";
Def.Sort = "Eigene";
S.Save();
Refresh();
foreach (var i in (Items.ItemsSource as List<IconItem> ?? []).Where(i => moved.Any(m => m.Path == i.Path)))
Items.SelectedItems.Add(i);
if (switched) App.TrayIcon?.Notify("Eigene Reihenfolge", "Die Box ist jetzt so sortiert, wie du die Icons ziehst. Ändern unter „Aussehen …“.");
}
// =====================================================================
// 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 { IsCategory: true } 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.IsCategory && string.Equals(x.Value, c, StringComparison.OrdinalIgnoreCase))))
add.Items.Add(MainWindow.MenuItem(c, S.Categories.GetValueOrDefault(c)?.Glyph ?? "", () => AddTab(new FenceTab { Value = c })));
if (!Def.Tabs.Any(x => x.IsRecent))
add.Items.Add(MainWindow.MenuItem("Zuletzt benutzt", "", () => AddTab(new FenceTab { Kind = FenceTab.RecentKind })));
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);
var copy = MainWindow.MenuItem("Auf Seite kopieren", "\uE8C8", 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);
var ci = MainWindow.MenuItem(S.Pages[p], "\uE8A1", () => FenceManager.CopyToPage(Def, page));
ci.IsEnabled = page != Def.Page;
copy.Items.Add(ci);
}
FenceMenu.Items.Add(move);
FenceMenu.Items.Add(copy);
}
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.IsCategory) menu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value)));
else if (t.IsFolder) 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 auf dieser Seite schon in einer anderen Box? Dann hierher holen statt doppelt anzeigen
if (tab.IsCategory) FenceManager.RemoveCategoryTab(tab.Value, except: Def, Def.Page);
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);
}