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(); } }