DeskCore 1.0.0
Desktop-Icons in eigene Kategorien sortieren: Windows-11-Design (hell/dunkel), Desktop-Boxen, automatisches Einsortieren nach Regeln, Rückgängig, Suche, Infobereich mit Tastenkürzel, Sicherung als ZIP und Installer ohne Adminrechte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
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;
|
||||
var wa = SystemParameters.WorkArea;
|
||||
Width = Math.Max(MinWidth, m.FenceW);
|
||||
Height = m.FenceCollapsed ? CollapsedHeight : Math.Max(120, m.FenceH);
|
||||
Left = Math.Clamp(m.FenceX ?? wa.Left + 24, wa.Left - Width + 80, wa.Right - 80);
|
||||
Top = Math.Clamp(m.FenceY ?? wa.Top + 24, wa.Top, wa.Bottom - 40);
|
||||
Body.Visibility = m.FenceCollapsed ? Visibility.Collapsed : Visibility.Visible;
|
||||
_applying = false;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (e.Key == Key.Enter) Selected.ForEach(i => Open(i.Path));
|
||||
}
|
||||
|
||||
private void Items_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||
{
|
||||
var sel = Selected;
|
||||
if (sel.Count == 0) { e.Handled = true; return; }
|
||||
ItemMenu.Items.Clear();
|
||||
ItemMenu.Items.Add(MainWindow.MenuItem("Öffnen", "", () => sel.ForEach(i => Open(i.Path)), bold: true));
|
||||
if (sel.Count == 1)
|
||||
ItemMenu.Items.Add(MainWindow.MenuItem("Dateispeicherort öffnen", "", () =>
|
||||
Process.Start("explorer.exe", $"/select,\"{sel[0].Path}\"")));
|
||||
ItemMenu.Items.Add(new Separator());
|
||||
ItemMenu.Items.Add(MainWindow.MenuItem("In DeskCore zeigen", "", 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 void Items_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
|
||||
? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move)
|
||||
: DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Items_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (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 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user