Files
DeskCore/FenceWindow.xaml.cs
T
M_ViperandClaude Opus 5.5 1ae6e1abc2 Fortschritt, .NET-Prüfung, Updates, Fenster merken, Box-Funktionen
- Große Kopien laufen im Hintergrund mit Fortschritt und Abbrechen;
  abgebrochene Kopien werden aufgeräumt, das Original bleibt unverändert
- Sichern/Wiederherstellen mit Fortschritt, keine halben ZIP-Dateien
- Installer auf .NET Framework 4.8: erkennt fehlende .NET 9 Desktop Runtime
  und installiert sie auf Wunsch (Microsoft-Signatur wird geprüft)
- Update-Hinweis über Gitea-Releases, Update mit automatischem Neustart
- Fenstergröße und -position werden gemerkt
- Desktop-Boxen: umbenennen, löschen, verschieben, herausziehen
- Fenster und Boxen kehren auf sichtbare Bildschirme zurück
- README ergänzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:46:48 +02:00

347 lines
13 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.Threading;
namespace DeskCore;
/// <summary>Eine "Box" auf dem Desktop, die den Inhalt einer Kategorie zeigt.</summary>
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;
}
/// <summary>Nach Bildschirm-Änderungen: Box wieder sichtbar machen.</summary>
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<IconItem> Selected => Items.SelectedItems.Cast<IconItem>().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<ListBoxItem>(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<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);
}
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<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;
}
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);
}
/// <summary>Hält die Desktop-Boxen passend zu den Einstellungen.</summary>
public static class FenceManager
{
private static readonly List<FenceWindow> _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();
}
}