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.Threading;
namespace DeskCore;
/// Eine "Box" auf dem Desktop, die den Inhalt einer Kategorie zeigt.
public partial class FenceWindow : Window
{
private const double CollapsedHeight = 38;
private readonly DispatcherTimer _saveTimer;
private bool _applying;
public string Category { get; private set; }
public FenceWindow(string category)
{
InitializeComponent();
Category = category;
_saveTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(700), DispatcherPriority.Background, (_, _) =>
{
_saveTimer!.Stop();
AppSettings.Current.Save();
}, Dispatcher);
_saveTimer.Stop();
ApplyBounds();
LocationChanged += (_, _) => RememberBounds();
SizeChanged += (_, _) => RememberBounds();
SourceInitialized += (_, _) => PinToDesktop();
Refresh();
}
private CategoryMeta Meta => AppSettings.Current.Meta(Category);
public void Rename(string newName)
{
Category = newName;
Refresh();
}
// ---------- Position/Größe ----------
private void ApplyBounds()
{
_applying = true;
var m = Meta;
Width = Math.Max(MinWidth, m.FenceW);
Height = m.FenceCollapsed ? CollapsedHeight : Math.Max(120, m.FenceH);
var wa = Screens.PrimaryWorkArea;
double left = m.FenceX ?? wa.Right - Width - 24, top = m.FenceY ?? 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;
m.FenceX = left;
m.FenceY = top;
}
Left = left;
Top = top;
Body.Visibility = m.FenceCollapsed ? Visibility.Collapsed : Visibility.Visible;
_applying = false;
}
/// Nach Bildschirm-Änderungen: Box wieder sichtbar machen.
public void EnsureOnScreen()
{
if (Screens.IsReachable(new Rect(Left, Top, ActualWidth, ActualHeight))) return;
Meta.FenceX = null;
Meta.FenceY = null;
ApplyBounds();
AppSettings.Current.Save();
}
private void RememberBounds()
{
if (_applying || !IsLoaded) return;
var m = Meta;
m.FenceX = Left;
m.FenceY = Top;
m.FenceW = Width;
if (!m.FenceCollapsed) m.FenceH = Height;
_saveTimer.Stop();
_saveTimer.Start();
}
private void ToggleCollapsed()
{
Meta.FenceCollapsed = !Meta.FenceCollapsed;
ApplyBounds();
AppSettings.Current.Save();
}
// ---------- Inhalt ----------
public void Refresh()
{
var meta = Meta;
TitleText.Text = Category;
GlyphText.Text = meta.Glyph ?? "";
GlyphText.Foreground = CategoryColors.Brush(meta.Color) ?? (Brush)FindResource("TextBrush");
var items = Storage.Entries(Storage.CategoryPath(Category))
.Select(p => new IconItem { Path = p })
.OrderBy(i => !Directory.Exists(i.Path)).ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase)
.ToList();
int px = (int)Math.Round(32 * VisualTreeHelper.GetDpi(this).DpiScaleX);
foreach (var i in items) IconLoader.Load(i, px);
Items.ItemsSource = items;
CountText.Text = items.Count.ToString();
EmptyText.Visibility = items.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
private List Selected => Items.SelectedItems.Cast().ToList();
private static void Open(string path)
{
try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); }
catch { }
}
private void Items_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (MainWindow.FindParent(e.OriginalSource as DependencyObject)?.DataContext is IconItem i) Open(i.Path);
}
private void Items_KeyDown(object sender, KeyEventArgs e)
{
var sel = Selected;
if (sel.Count == 0 || App.Win is not { } win) return;
switch (e.Key)
{
case Key.Enter: sel.ForEach(i => Open(i.Path)); 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;
}
}
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", "\uE8A7", () => sel.ForEach(i => Open(i.Path)), bold: true, gesture: "Enter"));
if (sel.Count == 1)
{
ItemMenu.Items.Add(MainWindow.MenuItem("Dateispeicherort öffnen", "\uED25", () =>
Process.Start("explorer.exe", $"/select,\"{sel[0].Path}\"")));
ItemMenu.Items.Add(MainWindow.MenuItem("Umbenennen", "\uE70F", () => win.RenameIcon(sel[0]), gesture: "F2"));
}
ItemMenu.Items.Add(new Separator());
var moveTo = MainWindow.MenuItem("Verschieben nach", "\uE8DE", null);
foreach (var c in Storage.Categories().Where(c => !string.Equals(c, Category, StringComparison.OrdinalIgnoreCase)))
moveTo.Items.Add(MainWindow.MenuItem(c, AppSettings.Current.Categories.GetValueOrDefault(c)?.Glyph ?? "\uF12B",
() => win.TransferAll(sel.Select(i => i.Path), Storage.CategoryPath(c))));
if (moveTo.Items.Count > 0) ItemMenu.Items.Add(moveTo);
ItemMenu.Items.Add(MainWindow.MenuItem("Zurück auf den Desktop legen", "\uE7F4",
() => win.TransferAll(sel.Select(i => i.Path), Storage.UserDesktop)));
ItemMenu.Items.Add(MainWindow.MenuItem("Löschen", "\uE74D", () => win.DeleteIcons(sel), gesture: "Entf"));
ItemMenu.Items.Add(new Separator());
ItemMenu.Items.Add(MainWindow.MenuItem("In DeskCore zeigen", "\uE8A7", ShowInApp));
}
private void MenuButton_Click(object sender, RoutedEventArgs e)
{
FenceMenu.Items.Clear();
FenceMenu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", ShowInApp));
FenceMenu.Items.Add(MainWindow.MenuItem(Meta.FenceCollapsed ? "Ausklappen" : "Einklappen",
Meta.FenceCollapsed ? "" : "", ToggleCollapsed));
FenceMenu.Items.Add(new Separator());
FenceMenu.Items.Add(MainWindow.MenuItem("Box ausblenden", "", () =>
{
Meta.Fence = false;
AppSettings.Current.Save();
FenceManager.Sync();
}));
FenceMenu.PlacementTarget = MenuButton;
FenceMenu.IsOpen = true;
}
private void ShowInApp() => App.Win?.ShowCategory(Category);
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(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);
}
finally { _dragging = false; }
}
private void Items_DragOver(object sender, DragEventArgs e)
{
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) && !_dragging
? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move)
: DragDropEffects.None;
e.Handled = true;
}
private void Items_Drop(object sender, DragEventArgs e)
{
if (!_dragging && e.Data.GetData(DataFormats.FileDrop) is string[] files)
App.Win?.TransferAll(files, Storage.CategoryPath(Category));
}
// ---------- Auf dem Desktop festhalten ----------
private void PinToDesktop()
{
var hwnd = new WindowInteropHelper(this).Handle;
// 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);
}
private IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case WM_WINDOWPOSCHANGING:
// immer ganz unten bleiben, nie über anderen Fenstern
var wp = Marshal.PtrToStructure(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;
}
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);
[StructLayout(LayoutKind.Sequential)]
private struct WINDOWPOS
{
public IntPtr hwnd, hwndInsertAfter;
public int x, y, cx, cy;
public uint flags;
}
[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);
}
/// Hält die Desktop-Boxen passend zu den Einstellungen.
public static class FenceManager
{
private static readonly List _open = [];
public static void Sync()
{
var s = AppSettings.Current;
var wanted = s.FencesEnabled
? Storage.Categories().Where(c => s.Categories.GetValueOrDefault(c)?.Fence == true).ToList()
: [];
foreach (var f in _open.Where(f => !wanted.Contains(f.Category, StringComparer.OrdinalIgnoreCase)).ToList())
{
f.Close();
_open.Remove(f);
}
var wa = SystemParameters.WorkArea;
foreach (var c in wanted)
{
var f = _open.FirstOrDefault(x => string.Equals(x.Category, c, StringComparison.OrdinalIgnoreCase));
if (f is not null) { f.Refresh(); continue; }
// Neue Box: neben die vorhandenen setzen
var meta = s.Meta(c);
if (meta.FenceX is null)
{
// oben rechts beginnen – links liegen meist die Desktop-Icons
int perRow = Math.Max(1, (int)((wa.Width - 48) / (meta.FenceW + 16)));
int i = _open.Count;
meta.FenceX = wa.Right - 24 - meta.FenceW - (i % perRow) * (meta.FenceW + 16);
meta.FenceY = wa.Top + 24 + (i / perRow) * (meta.FenceH + 16);
}
f = new FenceWindow(c);
_open.Add(f);
f.Show();
}
}
public static void RefreshAll()
{
foreach (var f in _open) f.Refresh();
}
public static void EnsureOnScreen()
{
foreach (var f in _open) f.EnsureOnScreen();
}
public static void Renamed(string oldName, string newName)
{
_open.FirstOrDefault(x => string.Equals(x.Category, oldName, StringComparison.OrdinalIgnoreCase))?.Rename(newName);
}
public static void CloseAll()
{
foreach (var f in _open) f.Close();
_open.Clear();
}
}