Upload via GUI (186 Dateien)

This commit is contained in:
2026-10-04 14:48:53 +00:00
parent c0b552ef39
commit 8efd5bc497
25 changed files with 2344 additions and 1015 deletions
+19 -1
View File
@@ -14,7 +14,7 @@ public partial class App : Application
private static Hotkey? _hotkey; private static Hotkey? _hotkey;
private static Hotkey? _peekHotkey; private static Hotkey? _peekHotkey;
private static Hotkey? _pagePrev, _pageNext; private static Hotkey? _pagePrev, _pageNext, _quickSearch;
private static System.Windows.Threading.DispatcherTimer? _desktopTimer; private static System.Windows.Threading.DispatcherTimer? _desktopTimer;
private static Watcher? _watcher; private static Watcher? _watcher;
private Mutex? _mutex; private Mutex? _mutex;
@@ -65,6 +65,9 @@ public partial class App : Application
_pageNext = new Hotkey(hwnd, 0xB004); _pageNext = new Hotkey(hwnd, 0xB004);
_pageNext.Pressed += () => FenceManager.NextPage(1); _pageNext.Pressed += () => FenceManager.NextPage(1);
_pageNext.Register(Hotkey.ModWin | Hotkey.ModAlt, 0x27); _pageNext.Register(Hotkey.ModWin | Hotkey.ModAlt, 0x27);
_quickSearch = new Hotkey(hwnd, 0xB005);
_quickSearch.Pressed += QuickSearch.Toggle;
ApplyQuickSearchHotkey();
TrayIcon = new Tray(); TrayIcon = new Tray();
TrayIcon.Clicked += () => Win.ShowFromBackground(); TrayIcon.Clicked += () => Win.ShowFromBackground();
@@ -110,6 +113,7 @@ public partial class App : Application
{ {
var s = AppSettings.Current; var s = AppSettings.Current;
if (Quitting) return; if (Quitting) return;
PageScheduler.Tick();
// Nur lesen, wenn eine Funktion die Positionen braucht // Nur lesen, wenn eine Funktion die Positionen braucht
if (s.DesktopTint is null && !s.DrawFences && !s.QuickHide) return; if (s.DesktopTint is null && !s.DrawFences && !s.QuickHide) return;
try { DesktopIcons.RefreshCache(); } catch { /* Explorer gerade neu gestartet o. Ä. */ } try { DesktopIcons.RefreshCache(); } catch { /* Explorer gerade neu gestartet o. Ä. */ }
@@ -124,6 +128,7 @@ public partial class App : Application
else if (Commands.Value(args, "--snapshot") is { } snap) Win.RestoreSnapshotByName(snap); else if (Commands.Value(args, "--snapshot") is { } snap) Win.RestoreSnapshotByName(snap);
else if (Commands.Has(args, "--toggle-desktop")) DesktopToggle.Toggle(); else if (Commands.Has(args, "--toggle-desktop")) DesktopToggle.Toggle();
else if (Commands.Has(args, "--peek")) FenceManager.TogglePeek(); else if (Commands.Has(args, "--peek")) FenceManager.TogglePeek();
else if (Commands.Has(args, "--search")) QuickSearch.Toggle();
else if (Commands.Value(args, "--page") is { } page) SwitchPageByName(page); else if (Commands.Value(args, "--page") is { } page) SwitchPageByName(page);
else if (Commands.WantsWindow(args)) Win.ShowFromBackground(); else if (Commands.WantsWindow(args)) Win.ShowFromBackground();
} }
@@ -138,6 +143,17 @@ public partial class App : Application
else TrayIcon?.Notify("Seite nicht gefunden", $"Es gibt keine Desktop-Seite „{page}“."); else TrayIcon?.Notify("Seite nicht gefunden", $"Es gibt keine Desktop-Seite „{page}“.");
} }
public static bool QuickSearchRegistered { get; private set; }
/// <summary>Tastenkürzel der Schnellstart-Suche neu registrieren. false = von anderem Programm belegt.</summary>
public static bool ApplyQuickSearchHotkey()
{
if (_quickSearch is null) return false;
var s = AppSettings.Current;
if (!s.QuickSearchEnabled) { _quickSearch.Unregister(); return QuickSearchRegistered = true; }
return QuickSearchRegistered = _quickSearch.Register(s.QuickSearchModifiers, s.QuickSearchKey);
}
/// <summary>Tastenkürzel neu registrieren. false = von anderem Programm belegt.</summary> /// <summary>Tastenkürzel neu registrieren. false = von anderem Programm belegt.</summary>
public static bool ApplyHotkey() public static bool ApplyHotkey()
{ {
@@ -172,6 +188,8 @@ public partial class App : Application
DesktopToggle.RestoreOnExit(); DesktopToggle.RestoreOnExit();
_peekHotkey?.Dispose(); _peekHotkey?.Dispose();
_pagePrev?.Dispose(); _pagePrev?.Dispose();
_quickSearch?.Dispose();
Usage.Flush();
_pageNext?.Dispose(); _pageNext?.Dispose();
Win?.Undo.FlushAll(); Win?.Undo.FlushAll();
FenceManager.CloseAll(); FenceManager.CloseAll();
+190
View File
@@ -0,0 +1,190 @@
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
namespace DeskCore;
/// <summary>
/// Auswahl nach dem Aufziehen einer Box: Welche Kategorie (oder welcher Ordner) soll hinein?
/// Ein eigenes Fenster statt eines Kontextmenüs – ein Menü ohne aktives Fenster schließt sich sonst gelegentlich von selbst.
/// </summary>
public sealed class BoxPicker : Window
{
private static BoxPicker? _open;
private readonly long _shownAt = Environment.TickCount64;
private bool _done;
private static AppSettings S => AppSettings.Current;
/// <param name="area">Fläche der neuen Box in WPF-Einheiten.</param>
/// <param name="create">Legt die Box mit dem gewählten Tab an.</param>
public static void Show(Rect area, Action<FenceTab> create)
{
_open?.Finish(null);
_open = new BoxPicker(area, create);
_open.Show();
}
private readonly Action<FenceTab> _create;
private BoxPicker(Rect area, Action<FenceTab> create)
{
_create = create;
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
Background = Brushes.Transparent;
ResizeMode = ResizeMode.NoResize;
ShowInTaskbar = false;
Topmost = true;
SizeToContent = SizeToContent.WidthAndHeight;
Title = "Neue Box";
var res = Application.Current.Resources;
var list = new StackPanel();
list.Children.Add(new TextBlock
{
Text = "Neue Box hier", FontSize = 14, FontWeight = FontWeights.SemiBold, Margin = new Thickness(12, 8, 12, 0),
Foreground = (Brush)res["TextBrush"],
});
list.Children.Add(new TextBlock
{
Text = "Was soll hinein?", FontSize = 12, Margin = new Thickness(12, 2, 12, 6),
Foreground = (Brush)res["TextSecondaryBrush"],
});
var cats = Storage.Categories();
var catPanel = new StackPanel();
foreach (var c in cats)
{
var meta = S.Categories.GetValueOrDefault(c);
// Pro Seite steht eine Kategorie nur in einer Box – andere Seiten behalten ihre
string? hint = S.HasFence(c) ? "wird aus ihrer Box auf dieser Seite hierher geholt" : null;
catPanel.Children.Add(Row(meta?.Glyph ?? "", CategoryColors.Brush(meta?.Color), c, hint,
() => Pick(new FenceTab { Value = c })));
}
if (cats.Count > 0)
{
list.Children.Add(new ScrollViewer
{
Content = catPanel, MaxHeight = 360, VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
});
list.Children.Add(Divider());
}
list.Children.Add(Row("", null, "Zuletzt benutzt", "zeigt, was du zuletzt gestartet hast",
() => Pick(new FenceTab { Kind = FenceTab.RecentKind })));
list.Children.Add(Row("", null, "Neue Kategorie …", null, () => PickLater(() =>
App.Win?.CreateCategoryInteractive() is { } name ? new FenceTab { Value = name } : null)));
list.Children.Add(Row("", null, "Ordner-Portal …", null, () => PickLater(() =>
FenceManager.PickFolder() is { } folder ? new FenceTab { Kind = FenceTab.FolderKind, Value = folder } : null)));
list.Children.Add(Divider());
list.Children.Add(Row("", null, "Abbrechen", null, () => Finish(null)));
Content = new Border
{
Margin = new Thickness(14), Padding = new Thickness(4), MinWidth = 240, CornerRadius = new CornerRadius(8),
Background = (Brush)res["FlyoutBrush"], BorderBrush = (Brush)res["FlyoutStrokeBrush"], BorderThickness = new Thickness(1),
Effect = (System.Windows.Media.Effects.Effect)res["FlyoutShadow"],
Child = list,
};
// Neben dem Mauszeiger öffnen, aber immer ganz auf dem Bildschirm
Loaded += (_, _) =>
{
var mouse = GetCursorPos(out var p) ? new Point(p.X / Screens.Scale, p.Y / Screens.Scale) : area.BottomRight;
var wa = Screens.WorkAreaAt(mouse);
Left = Math.Clamp(mouse.X - 14, wa.Left, Math.Max(wa.Left, wa.Right - ActualWidth));
Top = Math.Clamp(mouse.Y - 14, wa.Top, Math.Max(wa.Top, wa.Bottom - ActualHeight));
BringToFront();
};
KeyDown += (_, e) => { if (e.Key == Key.Escape) Finish(null); };
// Klick daneben schließt – aber nicht durch ein kurzes Flackern direkt nach dem Öffnen
Deactivated += (_, _) =>
{
if (_done) return;
if (Environment.TickCount64 - _shownAt < 400) BringToFront();
else Finish(null);
};
Closed += (_, _) => { if (_open == this) _open = null; DrawBoxOverlay.Hide(); };
}
private Button Row(string glyph, Brush? glyphBrush, string text, string? hint, Action click)
{
var res = Application.Current.Resources;
var content = new DockPanel();
var icon = new TextBlock
{
Text = glyph, FontFamily = (FontFamily)res["IconFont"], FontSize = 15, Width = 20, Margin = new Thickness(0, 0, 12, 0),
VerticalAlignment = VerticalAlignment.Center, Foreground = glyphBrush ?? (Brush)res["TextBrush"],
};
DockPanel.SetDock(icon, Dock.Left);
content.Children.Add(icon);
var texts = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
texts.Children.Add(new TextBlock { Text = text, Foreground = (Brush)res["TextBrush"] });
if (hint is not null)
texts.Children.Add(new TextBlock { Text = hint, FontSize = 11, Foreground = (Brush)res["TextTertiaryBrush"] });
content.Children.Add(texts);
var b = new Button
{
Style = (Style)res["SubtleButton"], Content = content, HorizontalContentAlignment = HorizontalAlignment.Stretch,
Padding = new Thickness(10, 7, 10, 7), Focusable = true,
};
b.Click += (_, _) => click();
return b;
}
private static Border Divider() => new()
{
Height = 1, Margin = new Thickness(4, 4, 4, 4), Background = (Brush)Application.Current.Resources["ControlStrokeBrush"],
};
private void Pick(FenceTab tab) => Finish(tab);
/// <summary>Für Auswahlen mit eigenem Dialog: erst die Auswahl schließen, Rahmen aber stehen lassen.</summary>
private void PickLater(Func<FenceTab?> ask)
{
_done = true;
Hide();
var tab = ask();
if (tab is not null) _create(tab);
Close();
}
private void Finish(FenceTab? tab)
{
if (_done) return;
_done = true;
Close();
if (tab is not null) _create(tab);
}
/// <summary>
/// DeskCore läuft im Hintergrund – Windows lässt es nicht ohne Weiteres nach vorne. Ein kurzer Alt-Tastendruck
/// gibt die Erlaubnis (bewährter Weg, der auch von Windows selbst so akzeptiert wird).
/// </summary>
private void BringToFront()
{
var hwnd = new WindowInteropHelper(this).Handle;
if (GetForegroundWindow() != hwnd)
{
// Loslassen erst nach dem Wechsel – so landet es bei uns und öffnet nirgends ein Menü
keybd_event(VK_MENU, 0, 0, UIntPtr.Zero);
SetForegroundWindow(hwnd);
keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
Activate();
Focus();
}
private const byte VK_MENU = 0x12;
private const uint KEYEVENTF_KEYUP = 0x2;
[StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; }
[DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT p);
[DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hwnd);
[DllImport("user32.dll")] private static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra);
}
+3 -2
View File
@@ -81,10 +81,11 @@ public static class DesktopIcons
return changed; return changed;
} }
public static bool HitsIcon(int x, int y) => Cached.Any(i => /// <param name="margin">Zusätzlicher Rand in Pixeln, damit Klicks knapp neben Bild oder Beschriftung auch zählen.</param>
public static bool HitsIcon(int x, int y, int margin = 0) => Cached.Any(i =>
{ {
var (l, t, w, h) = CellRect(i); var (l, t, w, h) = CellRect(i);
return x >= l && x < l + w && y >= t && y < t + h; return x >= l - margin && x < l + w + margin && y >= t - margin && y < t + h + margin;
}); });
// ---------- Desktop-Ansicht finden ---------- // ---------- Desktop-Ansicht finden ----------
+263 -261
View File
@@ -1,261 +1,263 @@
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
using System.Windows.Shapes; using System.Windows.Shapes;
using System.Windows.Threading; using System.Windows.Threading;
namespace DeskCore; namespace DeskCore;
/// <summary>Gemeinsame Hilfen für klickdurchlässige Overlay-Fenster.</summary> /// <summary>Gemeinsame Hilfen für klickdurchlässige Overlay-Fenster.</summary>
internal static class Overlay internal static class Overlay
{ {
public static Window Create(bool topmost) public static Window Create(bool topmost)
{ {
var w = new Window var w = new Window
{ {
WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent,
ShowInTaskbar = false, ShowActivated = false, Topmost = topmost, ResizeMode = ResizeMode.NoResize, ShowInTaskbar = false, ShowActivated = false, Topmost = topmost, ResizeMode = ResizeMode.NoResize,
Focusable = false, IsHitTestVisible = false, Focusable = false, IsHitTestVisible = false,
}; };
w.SourceInitialized += (_, _) => w.SourceInitialized += (_, _) =>
{ {
var hwnd = new WindowInteropHelper(w).Handle; var hwnd = new WindowInteropHelper(w).Handle;
long ex = GetWindowLongPtr(hwnd, GWL_EXSTYLE).ToInt64(); long ex = GetWindowLongPtr(hwnd, GWL_EXSTYLE).ToInt64();
SetWindowLongPtr(hwnd, GWL_EXSTYLE, new IntPtr(ex | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE)); SetWindowLongPtr(hwnd, GWL_EXSTYLE, new IntPtr(ex | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE));
}; };
return w; return w;
} }
/// <summary>Wie die Boxen: Besitzer = Desktop, immer ganz unten (über den Desktop-Icons, unter allen Fenstern).</summary> /// <summary>Wie die Boxen: Besitzer = Desktop, immer ganz unten (über den Desktop-Icons, unter allen Fenstern).</summary>
public static void PinToDesktop(Window w) public static void PinToDesktop(Window w)
{ {
var hwnd = new WindowInteropHelper(w).Handle; var hwnd = new WindowInteropHelper(w).Handle;
var progman = FindWindow("Progman", null); var progman = FindWindow("Progman", null);
if (progman != IntPtr.Zero) SetWindowLongPtr(hwnd, GWLP_HWNDPARENT, progman); if (progman != IntPtr.Zero) SetWindowLongPtr(hwnd, GWLP_HWNDPARENT, progman);
HwndSource.FromHwnd(hwnd).AddHook((IntPtr h, int msg, IntPtr wp, IntPtr lp, ref bool handled) => HwndSource.FromHwnd(hwnd).AddHook((IntPtr h, int msg, IntPtr wp, IntPtr lp, ref bool handled) =>
{ {
if (msg == WM_WINDOWPOSCHANGING) if (msg == WM_WINDOWPOSCHANGING)
{ {
var pos = Marshal.PtrToStructure<WINDOWPOS>(lp); var pos = Marshal.PtrToStructure<WINDOWPOS>(lp);
pos.hwndInsertAfter = new IntPtr(1); // HWND_BOTTOM pos.hwndInsertAfter = new IntPtr(1); // HWND_BOTTOM
pos.flags &= ~SWP_NOZORDER; pos.flags &= ~SWP_NOZORDER;
Marshal.StructureToPtr(pos, lp, false); Marshal.StructureToPtr(pos, lp, false);
} }
return IntPtr.Zero; return IntPtr.Zero;
}); });
} }
private const int GWL_EXSTYLE = -20, GWLP_HWNDPARENT = -8, WM_WINDOWPOSCHANGING = 0x46; private const int GWL_EXSTYLE = -20, GWLP_HWNDPARENT = -8, WM_WINDOWPOSCHANGING = 0x46;
private const long WS_EX_TRANSPARENT = 0x20, WS_EX_LAYERED = 0x80000, WS_EX_TOOLWINDOW = 0x80, WS_EX_NOACTIVATE = 0x8000000; private const long WS_EX_TRANSPARENT = 0x20, WS_EX_LAYERED = 0x80000, WS_EX_TOOLWINDOW = 0x80, WS_EX_NOACTIVATE = 0x8000000;
private const uint SWP_NOZORDER = 0x4; private const uint SWP_NOZORDER = 0x4;
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
private struct WINDOWPOS { public IntPtr hwnd, hwndInsertAfter; public int x, y, cx, cy; public uint flags; } 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", 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 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 IntPtr SetWindowLongPtr(IntPtr h, int i, IntPtr v);
} }
// ========================================================================= // =========================================================================
// Seitenanzeige beim Wechseln der Desktop-Seite // Seitenanzeige beim Wechseln der Desktop-Seite
// ========================================================================= // =========================================================================
public static class PageIndicator public static class PageIndicator
{ {
private static Window? _w; private static Window? _w;
private static TextBlock? _name; private static TextBlock? _name;
private static StackPanel? _dots; private static StackPanel? _dots;
private static DispatcherTimer? _hide; private static DispatcherTimer? _hide;
public static void Show(string name, int index, int count) public static void Show(string name, int index, int count)
{ {
if (_w is null) if (_w is null)
{ {
_w = Overlay.Create(topmost: true); _w = Overlay.Create(topmost: true);
_w.SizeToContent = SizeToContent.WidthAndHeight; _w.SizeToContent = SizeToContent.WidthAndHeight;
_name = new TextBlock { FontSize = 15, FontWeight = FontWeights.SemiBold, HorizontalAlignment = HorizontalAlignment.Center }; _name = new TextBlock { FontSize = 15, FontWeight = FontWeights.SemiBold, HorizontalAlignment = HorizontalAlignment.Center };
_dots = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 8, 0, 0) }; _dots = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 8, 0, 0) };
_w.Content = new Border _w.Content = new Border
{ {
Padding = new Thickness(22, 12, 22, 12), CornerRadius = new CornerRadius(10), Margin = new Thickness(12), Padding = new Thickness(22, 12, 22, 12), CornerRadius = new CornerRadius(10), Margin = new Thickness(12),
Child = new StackPanel { Children = { _name, _dots } }, Child = new StackPanel { Children = { _name, _dots } },
}; };
_hide = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1300) }; _hide = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1300) };
_hide.Tick += (_, _) => _hide.Tick += (_, _) =>
{ {
_hide.Stop(); _hide.Stop();
var fade = new DoubleAnimation(0, TimeSpan.FromMilliseconds(350)); var fade = new DoubleAnimation(0, TimeSpan.FromMilliseconds(350));
fade.Completed += (_, _) => { if (_w.Opacity == 0) _w.Hide(); }; fade.Completed += (_, _) => { if (_w.Opacity == 0) _w.Hide(); };
_w.BeginAnimation(UIElement.OpacityProperty, fade); _w.BeginAnimation(UIElement.OpacityProperty, fade);
}; };
} }
var res = Application.Current.Resources; var res = Application.Current.Resources;
var border = (Border)_w.Content; var border = (Border)_w.Content;
border.Background = (Brush)res["FlyoutBrush"]; border.Background = (Brush)res["FlyoutBrush"];
border.BorderBrush = (Brush)res["FlyoutStrokeBrush"]; border.BorderBrush = (Brush)res["FlyoutStrokeBrush"];
border.BorderThickness = new Thickness(1); border.BorderThickness = new Thickness(1);
border.Effect = (System.Windows.Media.Effects.Effect)res["FlyoutShadow"]; border.Effect = (System.Windows.Media.Effects.Effect)res["FlyoutShadow"];
_name!.Text = name; _name!.Text = name;
_name.Foreground = (Brush)res["TextBrush"]; _name.Foreground = (Brush)res["TextBrush"];
_dots!.Children.Clear(); _dots!.Children.Clear();
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
_dots.Children.Add(new Ellipse _dots.Children.Add(new Ellipse
{ {
Width = 7, Height = 7, Margin = new Thickness(4, 0, 4, 0), Width = 7, Height = 7, Margin = new Thickness(4, 0, 4, 0),
Fill = (Brush)res[i == index ? "AccentBrush" : "ControlStrongStrokeBrush"], Fill = (Brush)res[i == index ? "AccentBrush" : "ControlStrongStrokeBrush"],
}); });
_w.BeginAnimation(UIElement.OpacityProperty, null); _w.BeginAnimation(UIElement.OpacityProperty, null);
_w.Opacity = 1; _w.Opacity = 1;
_w.Show(); _w.Show();
_w.UpdateLayout(); _w.UpdateLayout();
var wa = Screens.PrimaryWorkArea; var wa = Screens.PrimaryWorkArea;
_w.Left = wa.Left + (wa.Width - _w.ActualWidth) / 2; _w.Left = wa.Left + (wa.Width - _w.ActualWidth) / 2;
_w.Top = wa.Bottom - _w.ActualHeight - 40; _w.Top = wa.Bottom - _w.ActualHeight - 40;
_hide!.Stop(); _hide!.Stop();
_hide.Start(); _hide.Start();
} }
} }
// ========================================================================= // =========================================================================
// Rahmen beim Aufziehen einer neuen Box (rechte Maustaste auf dem Desktop) // Rahmen beim Aufziehen einer neuen Box (rechte Maustaste auf dem Desktop)
// ========================================================================= // =========================================================================
public static class DrawBoxOverlay public static class DrawBoxOverlay
{ {
private static Window? _w; private static Window? _w;
public static void Begin() public static void Begin()
{ {
if (_w is null) if (_w is null)
{ {
_w = Overlay.Create(topmost: true); _w = Overlay.Create(topmost: true);
var res = Application.Current.Resources; var res = Application.Current.Resources;
var accent = ((SolidColorBrush)res["AccentBrush"]).Color; var accent = ((SolidColorBrush)res["AccentBrush"]).Color;
_w.Content = new Grid _w.Content = new Grid
{ {
Children = Children =
{ {
new Rectangle new Rectangle
{ {
RadiusX = 8, RadiusY = 8, StrokeThickness = 2, StrokeDashArray = [4, 3], RadiusX = 8, RadiusY = 8, StrokeThickness = 2, StrokeDashArray = [4, 3],
Stroke = new SolidColorBrush(accent), Fill = new SolidColorBrush(Color.FromArgb(0x38, accent.R, accent.G, accent.B)), Stroke = new SolidColorBrush(accent), Fill = new SolidColorBrush(Color.FromArgb(0x38, accent.R, accent.G, accent.B)),
}, },
new TextBlock new TextBlock
{ {
Text = "Neue Box", Foreground = Brushes.White, FontSize = 14, FontWeight = FontWeights.SemiBold, Text = "Neue Box", Foreground = Brushes.White, FontSize = 14, FontWeight = FontWeights.SemiBold,
HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 6, ShadowDepth = 0, Opacity = 0.8 }, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 6, ShadowDepth = 0, Opacity = 0.8 },
}, },
}, },
}; };
} }
_w.Width = _w.Height = 1; _w.Width = _w.Height = 1;
_w.Show(); _w.Show();
} }
/// <summary>Rechteck in Bildschirm-Pixeln.</summary> /// <summary>Rechteck in Bildschirm-Pixeln.</summary>
public static void Update(Rect px) public static void Update(Rect px)
{ {
if (_w is null) return; if (_w is null) return;
double s = Screens.Scale; double s = Screens.Scale;
_w.Left = px.X / s; _w.Left = px.X / s;
_w.Top = px.Y / s; _w.Top = px.Y / s;
_w.Width = Math.Max(1, px.Width / s); _w.Width = Math.Max(1, px.Width / s);
_w.Height = Math.Max(1, px.Height / s); _w.Height = Math.Max(1, px.Height / s);
} }
public static void Finish(Rect px) public static void Finish(Rect px)
{ {
_w?.Hide(); double s = Screens.Scale;
double s = Screens.Scale; var dip = new Rect(px.X / s, px.Y / s, px.Width / s, px.Height / s);
var dip = new Rect(px.X / s, px.Y / s, px.Width / s, px.Height / s); if (dip.Width < 120 || dip.Height < 60) { Hide(); return; } // zu klein – war wohl nur ein Versehen
if (dip.Width < 120 || dip.Height < 60) return; // zu klein – war wohl nur ein Versehen Update(px); // Rahmen bleibt stehen, bis etwas ausgewählt ist
FenceManager.ShowCreateMenu(dip); FenceManager.ShowCreateMenu(dip);
} }
}
public static void Hide() => _w?.Hide();
// ========================================================================= }
// Tönung der echten Desktop-Icons
// ========================================================================= // =========================================================================
// Tönung der echten Desktop-Icons
/// <summary> // =========================================================================
/// Legt eine klickdurchlässige Farbschicht exakt über die Bilder der Desktop-Icons. Die Positionen stammen aus der
/// offiziellen Shell-Schnittstelle; am Explorer selbst wird nichts verändert. Icons unter Boxen bleiben ausgespart. /// <summary>
/// </summary> /// Legt eine klickdurchlässige Farbschicht exakt über die Bilder der Desktop-Icons. Die Positionen stammen aus der
public static class DesktopTint /// offiziellen Shell-Schnittstelle; am Explorer selbst wird nichts verändert. Icons unter Boxen bleiben ausgespart.
{ /// </summary>
private static Window? _w; public static class DesktopTint
private static Canvas? _canvas; {
private static string _signature = ""; private static Window? _w;
private static Canvas? _canvas;
private static AppSettings S => AppSettings.Current; private static string _signature = "";
/// <summary>Schicht neu aufbauen, wenn sich Icons, Boxen oder Einstellungen geändert haben.</summary> private static AppSettings S => AppSettings.Current;
public static void Refresh(bool force = false)
{ /// <summary>Schicht neu aufbauen, wenn sich Icons, Boxen oder Einstellungen geändert haben.</summary>
bool on = S.DesktopTint is not null && !FenceManager.Hidden && DesktopToggle.IconsVisible(); public static void Refresh(bool force = false)
if (!on) {
{ bool on = S.DesktopTint is not null && !FenceManager.Hidden && DesktopToggle.IconsVisible();
_w?.Hide(); if (!on)
_signature = ""; {
return; _w?.Hide();
} _signature = "";
var icons = DesktopIcons.Cached.Where(i => i.Path.Length > 0).ToList(); return;
var fences = FenceManager.OpenRects(); }
var sig = $"{S.DesktopTint}|{S.DesktopTintStrength}|{DesktopIcons.IconTopOffset}|" + var icons = DesktopIcons.Cached.Where(i => i.Path.Length > 0).ToList();
string.Join(";", icons.Select(i => $"{i.X},{i.Y},{i.IconSize}")) + "|" + string.Join(";", fences.Select(r => r.ToString())); var fences = FenceManager.OpenRects();
if (!force && sig == _signature && _w?.IsVisible == true) return; var sig = $"{S.DesktopTint}|{S.DesktopTintStrength}|{DesktopIcons.IconTopOffset}|" +
_signature = sig; string.Join(";", icons.Select(i => $"{i.X},{i.Y},{i.IconSize}")) + "|" + string.Join(";", fences.Select(r => r.ToString()));
if (!force && sig == _signature && _w?.IsVisible == true) return;
if (_w is null) _signature = sig;
{
_w = Overlay.Create(topmost: false); if (_w is null)
_canvas = new Canvas(); {
_w.Content = _canvas; _w = Overlay.Create(topmost: false);
_w.SourceInitialized += (_, _) => Overlay.PinToDesktop(_w); _canvas = new Canvas();
} _w.Content = _canvas;
_w.SourceInitialized += (_, _) => Overlay.PinToDesktop(_w);
// Fenster über die gesamte Desktop-Fläche (alle Bildschirme) }
double s = Screens.Scale;
var vs = new Rect(SystemParameters.VirtualScreenLeft, SystemParameters.VirtualScreenTop, // Fenster über die gesamte Desktop-Fläche (alle Bildschirme)
SystemParameters.VirtualScreenWidth, SystemParameters.VirtualScreenHeight); double s = Screens.Scale;
_w.Left = vs.Left; var vs = new Rect(SystemParameters.VirtualScreenLeft, SystemParameters.VirtualScreenTop,
_w.Top = vs.Top; SystemParameters.VirtualScreenWidth, SystemParameters.VirtualScreenHeight);
_w.Width = vs.Width; _w.Left = vs.Left;
_w.Height = vs.Height; _w.Top = vs.Top;
_w.Width = vs.Width;
var tint = CategoryColors.Brush(S.DesktopTint) ?? Brushes.Transparent; _w.Height = vs.Height;
_canvas!.Children.Clear();
foreach (var icon in icons) var tint = CategoryColors.Brush(S.DesktopTint) ?? Brushes.Transparent;
{ _canvas!.Children.Clear();
var (l, t, size) = DesktopIcons.IconRect(icon); foreach (var icon in icons)
var r = new Rect(l / s, t / s, size / s, size / s); {
if (fences.Any(f => f.IntersectsWith(r))) continue; // liegt unter einer Box var (l, t, size) = DesktopIcons.IconRect(icon);
var item = new IconItem { Path = icon.Path }; var r = new Rect(l / s, t / s, size / s, size / s);
var rect = new Rectangle if (fences.Any(f => f.IntersectsWith(r))) continue; // liegt unter einer Box
{ var item = new IconItem { Path = icon.Path };
Width = r.Width, Height = r.Height, Fill = tint, Opacity = Math.Clamp(S.DesktopTintStrength, 0.1, 0.95), var rect = new Rectangle
Visibility = Visibility.Hidden, // erst zeigen, wenn die Form (das Icon) geladen ist {
}; Width = r.Width, Height = r.Height, Fill = tint, Opacity = Math.Clamp(S.DesktopTintStrength, 0.1, 0.95),
void Apply() Visibility = Visibility.Hidden, // erst zeigen, wenn die Form (das Icon) geladen ist
{ };
if (item.Icon is null) return; void Apply()
rect.OpacityMask = new ImageBrush(item.Icon) { Stretch = Stretch.Fill }; {
rect.Visibility = Visibility.Visible; if (item.Icon is null) return;
} rect.OpacityMask = new ImageBrush(item.Icon) { Stretch = Stretch.Fill };
item.PropertyChanged += (_, _) => Apply(); rect.Visibility = Visibility.Visible;
Canvas.SetLeft(rect, r.X - vs.Left); }
Canvas.SetTop(rect, r.Y - vs.Top); item.PropertyChanged += (_, _) => Apply();
_canvas.Children.Add(rect); Canvas.SetLeft(rect, r.X - vs.Left);
IconLoader.Load(item, size); Canvas.SetTop(rect, r.Y - vs.Top);
Apply(); _canvas.Children.Add(rect);
} IconLoader.Load(item, size);
if (!_w.IsVisible) _w.Show(); Apply();
} }
} if (!_w.IsVisible) _w.Show();
}
}
+12 -3
View File
@@ -68,7 +68,10 @@ public static class DesktopToggle
{ {
_lastDownTick = 0; _lastDownTick = 0;
var pt = info.pt; var pt = info.pt;
dispatcher?.BeginInvoke(() => OnDoubleClick(pt)); var first = _lastDownPt;
// Schon im Hook (ohne COM) aussortieren: Doppelklick auf ein Icon = Programmstart
if (!DesktopIcons.HitsIcon(pt.x, pt.y, margin: 8) && !DesktopIcons.HitsIcon(first.x, first.y, margin: 8))
dispatcher?.BeginInvoke(() => OnDoubleClick(pt, first));
} }
else else
{ {
@@ -144,9 +147,15 @@ public static class DesktopToggle
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>()); SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
} }
private static void OnDoubleClick(POINT pt) private static void OnDoubleClick(POINT pt, POINT first)
{ {
if (!IsEmptyDesktopSpot(pt)) return; if (!IsEmptyDesktopSpot(pt) || !IsEmptyDesktopSpot(first)) return;
// Positionen frisch lesen – der Zwischenspeicher kann ein paar Sekunden alt sein (Icon gerade verschoben)
if (IconsVisible())
{
try { DesktopIcons.RefreshCache(); } catch { return; }
if (DesktopIcons.HitsIcon(pt.x, pt.y, margin: 8) || DesktopIcons.HitsIcon(first.x, first.y, margin: 8)) return;
}
Toggle(); Toggle();
} }
+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<!-- Eine Versionsnummer für App und Installer – nur hier ändern --> <!-- Eine Versionsnummer für App und Installer – nur hier ändern -->
<PropertyGroup> <PropertyGroup>
<Version>1.0.2</Version> <Version>1.0.4</Version>
<!-- keine Git-Kennung an die Produktversion anhängen (sonst "1.0.1+a1b2c3…") --> <!-- keine Git-Kennung an die Produktversion anhängen (sonst "1.0.1+a1b2c3…") -->
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup> </PropertyGroup>
+50 -41
View File
@@ -73,14 +73,30 @@ public static class FenceManager
App.Win?.RefreshFromFences(); App.Win?.RefreshFromFences();
} }
/// <summary>Kategorie aus allen Boxen (außer except) entfernen – eine Kategorie soll nur in einer Box stehen.</summary> /// <summary>
public static void RemoveCategoryTab(string category, FenceDef? except) /// Kategorie aus den anderen Boxen derselben Seite entfernen – pro Seite steht eine Kategorie nur in einer Box.
/// Auf anderen Seiten darf sie zusätzlich liegen (z. B. „Internet“ auf „Arbeit“ und auf „Freizeit“).
/// </summary>
public static void RemoveCategoryTab(string category, FenceDef? except, int page)
{ {
foreach (var f in S.Fences.Where(f => f != except)) foreach (var f in S.Fences.Where(f => f != except && f.Page == page))
f.Tabs.RemoveAll(t => !t.IsFolder && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase)); f.Tabs.RemoveAll(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase));
S.Fences.RemoveAll(f => f != except && f.Tabs.Count == 0); S.Fences.RemoveAll(f => f != except && f.Tabs.Count == 0);
} }
/// <summary>Nach Verschieben/Zusammenlegen: doppelte Kategorien auf einer Seite auflösen – „keep“ behält seine.</summary>
private static void ResolveDuplicates(int page, FenceDef? keep)
{
if (keep is not null)
foreach (var t in keep.Tabs.Where(t => t.IsCategory).ToList())
RemoveCategoryTab(t.Value, keep, page);
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var f in S.Fences.Where(f => f.Page == page))
f.Tabs.RemoveAll(t => t.IsCategory && !seen.Add(t.Value));
S.Fences.RemoveAll(f => f.Tabs.Count == 0);
foreach (var f in S.Fences) f.ActiveTab = Math.Clamp(f.ActiveTab, 0, Math.Max(0, f.Tabs.Count - 1));
}
/// <summary>Tab als eigene Box daneben lösen.</summary> /// <summary>Tab als eigene Box daneben lösen.</summary>
public static void DetachTab(FenceDef def, int index) public static void DetachTab(FenceDef def, int index)
{ {
@@ -117,18 +133,12 @@ public static class FenceManager
public static List<Rect> OpenRects() => public static List<Rect> OpenRects() =>
_open.Where(w => w.IsVisible).Select(w => new Rect(w.Left, w.Top, w.ActualWidth, w.ActualHeight)).ToList(); _open.Where(w => w.IsVisible).Select(w => new Rect(w.Left, w.Top, w.ActualWidth, w.ActualHeight)).ToList();
/// <summary>Nach dem Aufziehen: Menü "Neue Box hier" an der Mausposition.</summary> /// <summary>Nach dem Aufziehen: Auswahl "Neue Box hier" an der Mausposition.</summary>
public static void ShowCreateMenu(Rect area) public static void ShowCreateMenu(Rect area)
{ {
var menu = new ContextMenu { Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint }; BoxPicker.Show(area, tab =>
var header = MainWindow.MenuItem("Neue Box hier für …", "\uE8A1", null, bold: true);
header.IsEnabled = false;
menu.Items.Add(header);
menu.Items.Add(new Separator());
void Create(FenceTab tab)
{ {
if (!tab.IsFolder) RemoveCategoryTab(tab.Value, except: null); if (tab.IsCategory) RemoveCategoryTab(tab.Value, except: null, S.CurrentPage);
S.Fences.Add(new FenceDef S.Fences.Add(new FenceDef
{ {
Page = S.CurrentPage, Tabs = [tab], Page = S.CurrentPage, Tabs = [tab],
@@ -138,35 +148,9 @@ public static class FenceManager
S.Save(); S.Save();
Sync(); Sync();
App.Win?.RefreshFromFences(); App.Win?.RefreshFromFences();
} });
foreach (var c in Storage.Categories())
{
var label = S.HasFence(c) ? $"{c} (aus anderer Box hierher holen)" : c;
menu.Items.Add(MainWindow.MenuItem(label, S.Categories.GetValueOrDefault(c)?.Glyph ?? "\uF12B",
() => Create(new FenceTab { Value = c })));
}
if (Storage.Categories().Count > 0) menu.Items.Add(new Separator());
menu.Items.Add(MainWindow.MenuItem("Neue Kategorie …", "\uE710", () =>
{
if (App.Win?.CreateCategoryInteractive() is { } name) Create(new FenceTab { Value = name });
}));
menu.Items.Add(MainWindow.MenuItem("Ordner-Portal …", "\uF12B", () =>
{
if (PickFolder() is { } folder) Create(new FenceTab { Kind = FenceTab.FolderKind, Value = folder });
}));
menu.Items.Add(new Separator());
menu.Items.Add(MainWindow.MenuItem("Abbrechen", "\uE711", () => { }));
// Damit das Menü bei Klick daneben zugeht, muss es im Vordergrund sein
menu.Opened += (_, _) =>
{
if (PresentationSource.FromVisual(menu) is System.Windows.Interop.HwndSource src) SetForegroundWindow(src.Handle);
};
menu.IsOpen = true;
} }
[System.Runtime.InteropServices.DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hwnd);
// ===================================================================== // =====================================================================
// Desktop-Seiten // Desktop-Seiten
// ===================================================================== // =====================================================================
@@ -186,6 +170,7 @@ public static class FenceManager
S.CurrentPage = index; S.CurrentPage = index;
S.Save(); S.Save();
if (changed) Sync(); if (changed) Sync();
PageScheduler.ApplyWallpaper(index);
PageIndicator.Show(S.Pages[index], index, S.Pages.Count); PageIndicator.Show(S.Pages[index], index, S.Pages.Count);
App.Win?.RefreshFromFences(); App.Win?.RefreshFromFences();
} }
@@ -193,8 +178,24 @@ public static class FenceManager
public static void MoveToPage(FenceDef def, int page) public static void MoveToPage(FenceDef def, int page)
{ {
def.Page = Math.Clamp(page, 0, S.Pages.Count - 1); def.Page = Math.Clamp(page, 0, S.Pages.Count - 1);
ResolveDuplicates(def.Page, keep: def);
S.Save(); S.Save();
Sync(); Sync();
App.Win?.RefreshFromFences();
}
/// <summary>Box mit gleichem Inhalt und Aussehen zusätzlich auf eine andere Seite legen.</summary>
public static void CopyToPage(FenceDef def, int page)
{
var copy = AppSettings.CloneFences([def])[0];
copy.Id = Guid.NewGuid().ToString("N");
copy.Page = Math.Clamp(page, 0, S.Pages.Count - 1);
S.Fences.Add(copy);
ResolveDuplicates(copy.Page, keep: copy);
S.Save();
Sync();
App.Win?.RefreshFromFences();
App.TrayIcon?.Notify("Box kopiert", $"Die Box liegt jetzt auch auf „{S.Pages[copy.Page]}“ – an derselben Stelle.");
} }
/// <summary>Seite löschen: ihre Boxen wandern auf die Seite davor.</summary> /// <summary>Seite löschen: ihre Boxen wandern auf die Seite davor.</summary>
@@ -205,6 +206,8 @@ public static class FenceManager
foreach (var f in S.Fences) foreach (var f in S.Fences)
if (f.Page == index) f.Page = Math.Max(0, index - 1); if (f.Page == index) f.Page = Math.Max(0, index - 1);
else if (f.Page > index) f.Page--; else if (f.Page > index) f.Page--;
// Kategorien, die es auf beiden Seiten gab, nicht doppelt zeigen
ResolveDuplicates(Math.Max(0, index - 1), keep: null);
if (S.CurrentPage >= index && S.CurrentPage > 0) S.CurrentPage--; if (S.CurrentPage >= index && S.CurrentPage > 0) S.CurrentPage--;
S.Save(); S.Save();
CloseAll(); CloseAll();
@@ -267,10 +270,16 @@ public static class FenceManager
v => def.IconSize = Enum.Parse<ViewMode>(v)), v => def.IconSize = Enum.Parse<ViewMode>(v)),
}); });
panel.Children.Add(Label("Sortieren nach")); panel.Children.Add(Label("Sortieren nach"));
panel.Children.Add(new TextBlock
{
Text = "„Eigene“: Icons in der Box einfach an die gewünschte Stelle ziehen.", FontSize = 12, Margin = new Thickness(0, -4, 0, 8),
TextWrapping = TextWrapping.Wrap, Foreground = (Brush)res["TextSecondaryBrush"],
});
panel.Children.Add(new Border panel.Children.Add(new Border
{ {
Style = (Style)res["SegmentGroup"], HorizontalAlignment = HorizontalAlignment.Left, Style = (Style)res["SegmentGroup"], HorizontalAlignment = HorizontalAlignment.Left,
Child = Segments("sort", [("Name", "Name"), ("Typ", "Typ"), ("Datum", "Datum")], def.Sort, v => def.Sort = v), Child = Segments("sort", [("Name", "Name"), ("Typ", "Typ"), ("Datum", "Datum"), ("Meistgenutzt", "Häufig"), ("Eigene", "Eigene")],
def.Sort, v => def.Sort = v),
}); });
// Icon-Tönung // Icon-Tönung
+6 -2
View File
@@ -53,7 +53,7 @@
<Grid x:Name="Body"> <Grid x:Name="Body">
<ListBox x:Name="Items" Padding="6,0,6,8" SelectionMode="Extended" ItemContainerStyle="{StaticResource TileItem}" <ListBox x:Name="Items" Padding="6,0,6,8" SelectionMode="Extended" ItemContainerStyle="{StaticResource TileItem}"
AllowDrop="True" DragOver="Items_DragOver" Drop="Items_Drop" AllowDrop="True" DragOver="Items_DragOver" DragLeave="Items_DragLeave" Drop="Items_Drop"
MouseDoubleClick="Items_MouseDoubleClick" KeyDown="Items_KeyDown" MouseDoubleClick="Items_MouseDoubleClick" KeyDown="Items_KeyDown"
PreviewMouseLeftButtonDown="Items_PreviewMouseLeftButtonDown" PreviewMouseMove="Items_PreviewMouseMove" PreviewMouseLeftButtonDown="Items_PreviewMouseLeftButtonDown" PreviewMouseMove="Items_PreviewMouseMove"
ContextMenuOpening="Items_ContextMenuOpening"> ContextMenuOpening="Items_ContextMenuOpening">
@@ -84,7 +84,11 @@
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
<TextBlock x:Name="EmptyText" Text="Leer – Icons hierher ziehen" HorizontalAlignment="Center" VerticalAlignment="Center" <TextBlock x:Name="EmptyText" Text="Leer – Icons hierher ziehen" HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource TextTertiaryBrush}" IsHitTestVisible="False" Margin="0,0,0,10"/> Foreground="{DynamicResource TextTertiaryBrush}" IsHitTestVisible="False" Margin="16,0,16,10"
TextWrapping="Wrap" TextAlignment="Center"/>
<!-- Einfügestelle beim Umsortieren per Ziehen -->
<Rectangle x:Name="DropMarker" Width="3" RadiusX="1.5" RadiusY="1.5" Fill="{DynamicResource AccentBrush}"
HorizontalAlignment="Left" VerticalAlignment="Top" IsHitTestVisible="False" Visibility="Collapsed"/>
</Grid> </Grid>
</DockPanel> </DockPanel>
</Border> </Border>
+119 -21
View File
@@ -55,7 +55,8 @@ public partial class FenceWindow : Window
SourceInitialized += (_, _) => PinToDesktop(); SourceInitialized += (_, _) => PinToDesktop();
MouseEnter += (_, _) => OnHover(true); MouseEnter += (_, _) => OnHover(true);
MouseLeave += (_, _) => OnHover(false); MouseLeave += (_, _) => OnHover(false);
Closed += (_, _) => _portalWatcher?.Dispose(); Usage.Changed += OnUsageChanged;
Closed += (_, _) => { _portalWatcher?.Dispose(); Usage.Changed -= OnUsageChanged; };
Refresh(); Refresh();
} }
@@ -215,29 +216,52 @@ public partial class FenceWindow : Window
WatchPortal(portal ? folder : null); WatchPortal(portal ? folder : null);
List<IconItem> items = []; List<IconItem> items = [];
if (folder is not null && Directory.Exists(folder)) 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 = (portal ? PortalEntries(folder) : Storage.Entries(folder)).Select(p => new IconItem { Path = p }).ToList();
items = Sort(items); items = Sort(items, tab);
int px = (int)Math.Round((double)Resources["FIcon"] * VisualTreeHelper.GetDpi(this).DpiScaleX); int px = (int)Math.Round((double)Resources["FIcon"] * VisualTreeHelper.GetDpi(this).DpiScaleX);
foreach (var i in items) IconLoader.Load(i, px); foreach (var i in items) IconLoader.Load(i, px);
Items.ItemsSource = items; Items.ItemsSource = items;
CountText.Text = items.Count.ToString(); CountText.Text = items.Count.ToString();
EmptyText.Text = folder is not null && !Directory.Exists(folder) ? "Ordner nicht gefunden" : "Leer – Icons hierher ziehen"; 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; EmptyText.Visibility = items.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
} }
/// <summary>Ordner-Portal zeigt alles außer versteckten/System-Dateien.</summary> /// <summary>Ordner-Portal zeigt alles außer versteckten/System-Dateien.</summary>
private static IEnumerable<string> PortalEntries(string dir) => Storage.Entries(dir); private static IEnumerable<string> PortalEntries(string dir) => Storage.Entries(dir);
private List<IconItem> Sort(List<IconItem> items) /// <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 folders = items.Where(i => Directory.Exists(i.Path));
var files = 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 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), "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; } }), "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), _ => x.OrderBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase),
}; };
return Order(folders).Concat(Order(files)).ToList(); return Order(folders).Concat(Order(files)).ToList();
@@ -250,13 +274,13 @@ public partial class FenceWindow : Window
{ {
var t = Def.Tabs[i]; var t = Def.Tabs[i];
bool active = i == Math.Clamp(Def.ActiveTab, 0, Def.Tabs.Count - 1); bool active = i == Math.Clamp(Def.ActiveTab, 0, Def.Tabs.Count - 1);
var meta = t.IsFolder ? null : S.Categories.GetValueOrDefault(t.Value); var meta = t.IsCategory ? S.Categories.GetValueOrDefault(t.Value) : null;
var glyph = new TextBlock var glyph = new TextBlock
{ {
Text = t.IsFolder ? "" : meta?.Glyph ?? "", Text = t.IsRecent ? "" : t.IsFolder ? "" : meta?.Glyph ?? "",
FontFamily = (FontFamily)FindResource("IconFont"), FontSize = 13, VerticalAlignment = VerticalAlignment.Center, FontFamily = (FontFamily)FindResource("IconFont"), FontSize = 13, VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 7, 0), Margin = new Thickness(0, 0, 7, 0),
Foreground = CategoryColors.Brush(meta?.Color) ?? (Brush)FindResource(t.IsFolder ? "AccentBrush" : "TextBrush"), Foreground = CategoryColors.Brush(meta?.Color) ?? (Brush)FindResource(t.IsCategory ? "TextBrush" : "AccentBrush"),
}; };
var label = new TextBlock var label = new TextBlock
{ {
@@ -275,7 +299,7 @@ public partial class FenceWindow : Window
{ {
Style = (Style)FindResource("SubtleButton"), Padding = new Thickness(6, 4, 6, 4), MinHeight = 0, Tag = i, 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), Content = new Grid { Children = { content, underline } }, Margin = new Thickness(0, 0, 2, 0),
ToolTip = t.IsFolder ? t.Value : $"Kategorie „{t.Value}“", 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 WindowChrome.SetIsHitTestVisibleInChrome(btn, Def.Tabs.Count > 1); // bei nur einem Tab: Titel = Ziehfläche
btn.Click += (_, _) => SwitchTab((int)btn.Tag); btn.Click += (_, _) => SwitchTab((int)btn.Tag);
@@ -344,7 +368,7 @@ public partial class FenceWindow : Window
Refresh(); Refresh();
return; return;
} }
try { Process.Start(new ProcessStartInfo(i.Path) { UseShellExecute = true }); } try { Usage.Start(i.Path); }
catch { } catch { }
} }
@@ -383,7 +407,7 @@ public partial class FenceWindow : Window
bool inStorage = sel.All(i => Storage.IsInStorage(i.Path)); bool inStorage = sel.All(i => Storage.IsInStorage(i.Path));
var moveTo = MainWindow.MenuItem(inStorage ? "Verschieben nach" : "Sichern in", inStorage ? "" : "", null); var moveTo = MainWindow.MenuItem(inStorage ? "Verschieben nach" : "Sichern in", inStorage ? "" : "", null);
var ownCategory = Tab is { IsFolder: false } t ? t.Value : 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))) 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 ?? "", moveTo.Items.Add(MainWindow.MenuItem(c, S.Categories.GetValueOrDefault(c)?.Glyph ?? "",
() => win.TransferAll(sel.Select(i => i.Path), Storage.CategoryPath(c)))); () => win.TransferAll(sel.Select(i => i.Path), Storage.CategoryPath(c))));
@@ -423,18 +447,85 @@ public partial class FenceWindow : Window
private void Items_DragOver(object sender, DragEventArgs e) private void Items_DragOver(object sender, DragEventArgs e)
{ {
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) && !_dragging && CurrentFolder is not null if (_dragging)
? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move) {
: DragDropEffects.None; // 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; e.Handled = true;
} }
private void Items_DragLeave(object sender, DragEventArgs e) => ShowDropMarker(null);
private void Items_Drop(object sender, DragEventArgs e) private void Items_Drop(object sender, DragEventArgs e)
{ {
if (!_dragging && CurrentFolder is { } folder && e.Data.GetData(DataFormats.FileDrop) is string[] files) 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); 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 // Menüs
// ===================================================================== // =====================================================================
@@ -448,15 +539,17 @@ public partial class FenceWindow : Window
private void MenuButton_Click(object sender, RoutedEventArgs e) private void MenuButton_Click(object sender, RoutedEventArgs e)
{ {
FenceMenu.Items.Clear(); FenceMenu.Items.Clear();
if (Tab is { IsFolder: false } t) if (Tab is { IsCategory: true } t)
FenceMenu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value))); FenceMenu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value)));
else if (Tab is { IsFolder: true } pt) else if (Tab is { IsFolder: true } pt)
FenceMenu.Items.Add(MainWindow.MenuItem("Im Explorer öffnen", "", () => Process.Start("explorer.exe", $"\"{CurrentFolder ?? pt.Value}\""))); FenceMenu.Items.Add(MainWindow.MenuItem("Im Explorer öffnen", "", () => Process.Start("explorer.exe", $"\"{CurrentFolder ?? pt.Value}\"")));
// Tabs // Tabs
var add = MainWindow.MenuItem("Tab hinzufügen", "", null); var add = MainWindow.MenuItem("Tab hinzufügen", "", null);
foreach (var c in Storage.Categories().Where(c => !Def.Tabs.Any(x => !x.IsFolder && string.Equals(x.Value, c, StringComparison.OrdinalIgnoreCase)))) 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 }))); 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()); if (add.Items.Count > 0) add.Items.Add(new Separator());
add.Items.Add(MainWindow.MenuItem("Ordner-Portal…", "", () => add.Items.Add(MainWindow.MenuItem("Ordner-Portal…", "", () =>
{ {
@@ -475,14 +568,19 @@ public partial class FenceWindow : Window
if (S.Pages.Count > 1) if (S.Pages.Count > 1)
{ {
var move = MainWindow.MenuItem("Auf Seite verschieben", "\uE8AB", null); 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++) for (int p = 0; p < S.Pages.Count; p++)
{ {
int page = p; int page = p;
var mi = MainWindow.MenuItem(S.Pages[p], "\uE8A1", () => FenceManager.MoveToPage(Def, page)); var mi = MainWindow.MenuItem(S.Pages[p], "\uE8A1", () => FenceManager.MoveToPage(Def, page));
mi.IsEnabled = page != Def.Page; mi.IsEnabled = page != Def.Page;
move.Items.Add(mi); 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(move);
FenceMenu.Items.Add(copy);
} }
FenceMenu.Items.Add(new Separator()); FenceMenu.Items.Add(new Separator());
FenceMenu.Items.Add(MainWindow.MenuItem("Box schließen", "", () => FenceManager.Remove(Def))); FenceMenu.Items.Add(MainWindow.MenuItem("Box schließen", "", () => FenceManager.Remove(Def)));
@@ -495,8 +593,8 @@ public partial class FenceWindow : Window
if (index < 0 || index >= Def.Tabs.Count) return; if (index < 0 || index >= Def.Tabs.Count) return;
var t = Def.Tabs[index]; var t = Def.Tabs[index];
var menu = new ContextMenu(); var menu = new ContextMenu();
if (!t.IsFolder) menu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value))); if (t.IsCategory) menu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value)));
else menu.Items.Add(MainWindow.MenuItem("Im Explorer öffnen", "", () => Process.Start("explorer.exe", $"\"{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) if (Def.Tabs.Count > 1)
{ {
menu.Items.Add(MainWindow.MenuItem("Als eigene Box lösen", "", () => FenceManager.DetachTab(Def, index))); menu.Items.Add(MainWindow.MenuItem("Als eigene Box lösen", "", () => FenceManager.DetachTab(Def, index)));
@@ -507,8 +605,8 @@ public partial class FenceWindow : Window
private void AddTab(FenceTab tab) private void AddTab(FenceTab tab)
{ {
// Kategorie steckt schon in einer anderen Box? Dann dorthin verschieben statt doppelt anzeigen // Kategorie steckt auf dieser Seite schon in einer anderen Box? Dann hierher holen statt doppelt anzeigen
if (!tab.IsFolder) FenceManager.RemoveCategoryTab(tab.Value, except: Def); if (tab.IsCategory) FenceManager.RemoveCategoryTab(tab.Value, except: Def, Def.Page);
Def.Tabs.Add(tab); Def.Tabs.Add(tab);
Def.ActiveTab = Def.Tabs.Count - 1; Def.ActiveTab = Def.Tabs.Count - 1;
_portalSub = null; _portalSub = null;
+249 -244
View File
@@ -1,244 +1,249 @@
using System.Diagnostics; using System.Diagnostics;
using System.Windows; using System.Windows;
using System.Windows.Threading; using System.Windows.Threading;
namespace DeskCore; namespace DeskCore;
/// <summary>Fortschrittsanzeige für lange Vorgänge, Fensterposition merken und Update-Hinweis.</summary> /// <summary>Fortschrittsanzeige für lange Vorgänge, Fensterposition merken und Update-Hinweis.</summary>
public partial class MainWindow public partial class MainWindow
{ {
// ===================================================================== // =====================================================================
// Fortschritt mit Abbrechen // Fortschritt mit Abbrechen
// ===================================================================== // =====================================================================
/// <summary>Meldet Fortschritt aus dem Hintergrund (threadsicher).</summary> /// <summary>Meldet Fortschritt aus dem Hintergrund (threadsicher).</summary>
public sealed class ProgressSink public sealed class ProgressSink
{ {
private long _done, _total; private long _done, _total;
public long Done => Interlocked.Read(ref _done); public long Done => Interlocked.Read(ref _done);
public long Total => Interlocked.Read(ref _total); public long Total => Interlocked.Read(ref _total);
public void Add(long bytes) => Interlocked.Add(ref _done, bytes); public void Add(long bytes) => Interlocked.Add(ref _done, bytes);
public void SetTotal(long total) => Interlocked.Exchange(ref _total, total); public void SetTotal(long total) => Interlocked.Exchange(ref _total, total);
public void SetDone(long done) => Interlocked.Exchange(ref _done, done); public void SetDone(long done) => Interlocked.Exchange(ref _done, done);
public volatile string? Detail; public volatile string? Detail;
} }
private CancellationTokenSource? _busyCts; private CancellationTokenSource? _busyCts;
private Task? _busyTask; private Task? _busyTask;
public bool IsBusy => _busyCts is not null; public bool IsBusy => _busyCts is not null;
/// <summary> /// <summary>
/// Führt einen langen Vorgang im Hintergrund aus. Das Fenster bleibt bedienbar; nach kurzer Zeit erscheint /// Führt einen langen Vorgang im Hintergrund aus. Das Fenster bleibt bedienbar; nach kurzer Zeit erscheint
/// eine Fortschrittsanzeige mit Abbrechen-Knopf (bei schnellen Vorgängen gar nicht erst). /// eine Fortschrittsanzeige mit Abbrechen-Knopf (bei schnellen Vorgängen gar nicht erst).
/// </summary> /// </summary>
private async Task<T> RunWithProgress<T>(string title, string detail, Func<ProgressSink, CancellationToken, Task<T>> work) private async Task<T> RunWithProgress<T>(string title, string detail, Func<ProgressSink, CancellationToken, Task<T>> work)
{ {
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
_busyCts = cts; _busyCts = cts;
var sink = new ProgressSink { Detail = detail }; var sink = new ProgressSink { Detail = detail };
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
ProgressTitle.Text = title; ProgressTitle.Text = title;
ProgressDetail.Text = detail; ProgressDetail.Text = detail;
ProgressStats.Text = ""; ProgressStats.Text = "";
ProgressFill.Width = 0; ProgressFill.Width = 0;
ProgressCancel.IsEnabled = true; ProgressCancel.IsEnabled = true;
ProgressCancel.Content = "Abbrechen"; ProgressCancel.Content = "Abbrechen";
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) }; var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
timer.Tick += (_, _) => timer.Tick += (_, _) =>
{ {
if (ProgressOverlay.Visibility != Visibility.Visible && sw.ElapsedMilliseconds > 500) if (ProgressOverlay.Visibility != Visibility.Visible && sw.ElapsedMilliseconds > 500)
ProgressOverlay.Visibility = Visibility.Visible; ProgressOverlay.Visibility = Visibility.Visible;
if (sink.Detail is { } d) ProgressDetail.Text = d; if (sink.Detail is { } d) ProgressDetail.Text = d;
long done = sink.Done, total = sink.Total; long done = sink.Done, total = sink.Total;
if (total > 0) if (total > 0)
{ {
double frac = Math.Min(1, done / (double)total); double frac = Math.Min(1, done / (double)total);
ProgressFill.Width = ProgressTrack.ActualWidth * frac; ProgressFill.Width = ProgressTrack.ActualWidth * frac;
ProgressStats.Text = $"{Backup.FormatSize(done)} von {Backup.FormatSize(total)}{Remaining(done, total, sw.Elapsed)}"; ProgressStats.Text = $"{Backup.FormatSize(done)} von {Backup.FormatSize(total)}{Remaining(done, total, sw.Elapsed)}";
} }
else if (done > 0) ProgressStats.Text = Backup.FormatSize(done); else if (done > 0) ProgressStats.Text = Backup.FormatSize(done);
}; };
timer.Start(); timer.Start();
var task = Task.Run(() => work(sink, cts.Token)); var task = Task.Run(() => work(sink, cts.Token));
_busyTask = task; _busyTask = task;
try try
{ {
return await task; return await task;
} }
finally finally
{ {
timer.Stop(); timer.Stop();
ProgressOverlay.Visibility = Visibility.Collapsed; ProgressOverlay.Visibility = Visibility.Collapsed;
_busyCts = null; _busyCts = null;
_busyTask = null; _busyTask = null;
cts.Dispose(); cts.Dispose();
} }
} }
private static string Remaining(long done, long total, TimeSpan elapsed) private static string Remaining(long done, long total, TimeSpan elapsed)
{ {
if (done <= 0 || elapsed.TotalSeconds < 2) return ""; if (done <= 0 || elapsed.TotalSeconds < 2) return "";
var rest = TimeSpan.FromSeconds((total - done) / (done / elapsed.TotalSeconds)); var rest = TimeSpan.FromSeconds((total - done) / (done / elapsed.TotalSeconds));
return rest.TotalSeconds < 5 ? " · gleich fertig" return rest.TotalSeconds < 5 ? " · gleich fertig"
: rest.TotalMinutes < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalSeconds / 5) * 5:0} Sek." : rest.TotalMinutes < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalSeconds / 5) * 5:0} Sek."
: rest.TotalHours < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalMinutes):0} Min." : rest.TotalHours < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalMinutes):0} Min."
: $" · noch ca. {rest.TotalHours:0.#} Std."; : $" · noch ca. {rest.TotalHours:0.#} Std.";
} }
private void ProgressCancel_Click(object sender, RoutedEventArgs e) private void ProgressCancel_Click(object sender, RoutedEventArgs e)
{ {
_busyCts?.Cancel(); _busyCts?.Cancel();
ProgressCancel.IsEnabled = false; ProgressCancel.IsEnabled = false;
ProgressCancel.Content = "Wird abgebrochen …"; ProgressCancel.Content = "Wird abgebrochen …";
} }
/// <summary>Beim Beenden: laufenden Vorgang abbrechen und kurz aufs Aufräumen warten.</summary> /// <summary>Beim Beenden: laufenden Vorgang abbrechen und kurz aufs Aufräumen warten.</summary>
public void CancelBusyAndWait() public void CancelBusyAndWait()
{ {
if (_busyCts is null) return; if (_busyCts is null) return;
_busyCts.Cancel(); _busyCts.Cancel();
try { _busyTask?.Wait(TimeSpan.FromSeconds(5)); } catch { } try { _busyTask?.Wait(TimeSpan.FromSeconds(5)); } catch { }
} }
// ===================================================================== // =====================================================================
// Fensterposition merken // Fensterposition merken
// ===================================================================== // =====================================================================
private void RestorePlacement() private void RestorePlacement()
{ {
if (S.WindowLeft is double l && S.WindowTop is double t && S.WindowWidth is double w && S.WindowHeight is double h) if (S.WindowLeft is double l && S.WindowTop is double t && S.WindowWidth is double w && S.WindowHeight is double h)
{ {
var r = new Rect(l, t, Math.Max(MinWidth, w), Math.Max(MinHeight, h)); var r = new Rect(l, t, Math.Max(MinWidth, w), Math.Max(MinHeight, h));
// nur übernehmen, wenn das Fenster dort noch erreichbar ist (Monitor evtl. abgesteckt) // nur übernehmen, wenn das Fenster dort noch erreichbar ist (Monitor evtl. abgesteckt)
if (Screens.IsReachable(r)) if (Screens.IsReachable(r))
{ {
WindowStartupLocation = WindowStartupLocation.Manual; WindowStartupLocation = WindowStartupLocation.Manual;
Left = r.Left; Left = r.Left;
Top = r.Top; Top = r.Top;
Width = r.Width; Width = r.Width;
Height = r.Height; Height = r.Height;
} }
} }
if (S.WindowMaximized) WindowState = WindowState.Maximized; if (S.WindowMaximized) WindowState = WindowState.Maximized;
} }
public void SavePlacement() public void SavePlacement()
{ {
if (!IsLoaded) return; // nie angezeigt (Start im Infobereich) → nichts zu merken if (!IsLoaded) return; // nie angezeigt (Start im Infobereich) → nichts zu merken
var r = WindowState == WindowState.Normal ? new Rect(Left, Top, ActualWidth, ActualHeight) : RestoreBounds; var r = WindowState == WindowState.Normal ? new Rect(Left, Top, ActualWidth, ActualHeight) : RestoreBounds;
if (r.IsEmpty || r.Width < 100) return; if (r.IsEmpty || r.Width < 100) return;
S.WindowLeft = r.Left; S.WindowLeft = r.Left;
S.WindowTop = r.Top; S.WindowTop = r.Top;
S.WindowWidth = r.Width; S.WindowWidth = r.Width;
S.WindowHeight = r.Height; S.WindowHeight = r.Height;
S.WindowMaximized = WindowState == WindowState.Maximized; S.WindowMaximized = WindowState == WindowState.Maximized;
S.Save(); S.Save();
} }
/// <summary>Bildschirm abgesteckt/umgestellt: Fenster zurückholen, falls es nicht mehr erreichbar ist.</summary> /// <summary>Bildschirm abgesteckt/umgestellt: Fenster zurückholen, falls es nicht mehr erreichbar ist.</summary>
public void EnsureOnScreen() public void EnsureOnScreen()
{ {
if (WindowState != WindowState.Normal) return; if (WindowState != WindowState.Normal) return;
if (Screens.IsReachable(new Rect(Left, Top, ActualWidth, ActualHeight))) return; if (Screens.IsReachable(new Rect(Left, Top, ActualWidth, ActualHeight))) return;
var wa = Screens.PrimaryWorkArea; var wa = Screens.PrimaryWorkArea;
Width = Math.Min(Width, wa.Width); Width = Math.Min(Width, wa.Width);
Height = Math.Min(Height, wa.Height); Height = Math.Min(Height, wa.Height);
Left = wa.Left + (wa.Width - Width) / 2; Left = wa.Left + (wa.Width - Width) / 2;
Top = wa.Top + (wa.Height - Height) / 2; Top = wa.Top + (wa.Height - Height) / 2;
} }
// ===================================================================== // =====================================================================
// Updates // Updates
// ===================================================================== // =====================================================================
private UpdateInfo? _update; private UpdateInfo? _update;
/// <summary>Beim Start: höchstens einmal am Tag nachsehen, leise im Hintergrund.</summary> /// <summary>Beim Start: höchstens einmal am Tag nachsehen, leise im Hintergrund.</summary>
public async void CheckForUpdatesOnStartup() public async void CheckForUpdatesOnStartup()
{ {
if (!S.AutoUpdateCheck) return; if (!S.AutoUpdateCheck) return;
if (S.LastUpdateCheck is { } last && DateTime.Now - last < TimeSpan.FromHours(20)) return; if (S.LastUpdateCheck is { } last && DateTime.Now - last < TimeSpan.FromHours(20)) return;
await Task.Delay(TimeSpan.FromSeconds(8)); // Start nicht ausbremsen await Task.Delay(TimeSpan.FromSeconds(8)); // Start nicht ausbremsen
await CheckForUpdates(manual: false); await CheckForUpdates(manual: false);
} }
private async Task CheckForUpdates(bool manual) private async Task CheckForUpdates(bool manual)
{ {
if (manual) if (manual)
{ {
CheckUpdateButton.IsEnabled = false; CheckUpdateButton.IsEnabled = false;
UpdateStatusText.Text = "Suche nach Updates …"; UpdateStatusText.Text = "Suche nach Updates …";
} }
try try
{ {
var info = await UpdateChecker.CheckAsync(); var info = await UpdateChecker.CheckAsync();
S.LastUpdateCheck = DateTime.Now; S.LastUpdateCheck = DateTime.Now;
S.Save(); S.Save();
if (info is null) if (info is null)
{ {
if (manual) UpdateStatusText.Text = $"Du hast die neueste Version ({UpdateChecker.Current})."; if (manual) UpdateStatusText.Text = $"Du hast die neueste Version ({UpdateChecker.Current}).";
return; return;
} }
if (!manual && S.SkippedVersion == info.Version.ToString()) return; if (!manual && S.SkippedVersion == info.Version.ToString()) return;
_update = info; _update = info;
UpdateText.Text = $"DeskCore {info.Version} ist verfügbar – du hast Version {UpdateChecker.Current}."; UpdateText.Text = $"DeskCore {info.Version} ist verfügbar – du hast Version {UpdateChecker.Current}.";
UpdateNowButton.Content = info.SetupUrl is not null ? "Jetzt aktualisieren" : "Zur Download-Seite"; UpdateNowButton.Content = info.SetupUrl is not null ? "Jetzt aktualisieren" : "Zur Download-Seite";
UpdateBar.Visibility = Visibility.Visible; UpdateBar.Visibility = Visibility.Visible;
if (manual) UpdateStatusText.Text = $"Version {info.Version} ist verfügbar – oben auf der Startseite aktualisieren."; if (manual) UpdateStatusText.Text = $"Version {info.Version} ist verfügbar – oben auf der Startseite aktualisieren.";
else if (!IsVisible) App.TrayIcon?.Notify("Update verfügbar", $"DeskCore {info.Version} ist verfügbar. Zum Aktualisieren DeskCore öffnen."); else if (!IsVisible) App.TrayIcon?.Notify("Update verfügbar", $"DeskCore {info.Version} ist verfügbar. Zum Aktualisieren DeskCore öffnen.");
} }
catch (Exception ex) catch (Exception ex)
{ {
if (manual) UpdateStatusText.Text = $"Konnte nicht prüfen: {ex.Message}"; if (manual) UpdateStatusText.Text = $"Konnte nicht prüfen: {ex.Message}";
} }
finally finally
{ {
if (manual) CheckUpdateButton.IsEnabled = true; if (manual) CheckUpdateButton.IsEnabled = true;
} }
} }
private async void CheckUpdate_Click(object sender, RoutedEventArgs e) => await CheckForUpdates(manual: true); private async void CheckUpdate_Click(object sender, RoutedEventArgs e) => await CheckForUpdates(manual: true);
private void AutoUpdateToggle_Click(object sender, RoutedEventArgs e) private void AutoUpdateToggle_Click(object sender, RoutedEventArgs e)
{ {
S.AutoUpdateCheck = AutoUpdateToggle.IsChecked == true; S.AutoUpdateCheck = AutoUpdateToggle.IsChecked == true;
S.Save(); S.Save();
} }
private void UpdateLater_Click(object sender, RoutedEventArgs e) => UpdateBar.Visibility = Visibility.Collapsed; private void UpdateLater_Click(object sender, RoutedEventArgs e) => UpdateBar.Visibility = Visibility.Collapsed;
private void UpdateSkip_Click(object sender, RoutedEventArgs e) private void UpdateSkip_Click(object sender, RoutedEventArgs e)
{ {
if (_update is not null) if (_update is not null)
{ {
S.SkippedVersion = _update.Version.ToString(); S.SkippedVersion = _update.Version.ToString();
S.Save(); S.Save();
} }
UpdateBar.Visibility = Visibility.Collapsed; UpdateBar.Visibility = Visibility.Collapsed;
} }
private async void UpdateNow_Click(object sender, RoutedEventArgs e) private async void UpdateNow_Click(object sender, RoutedEventArgs e)
{ {
if (_update is not { } info || IsBusy) return; if (_update is not { } info || IsBusy) return;
if (info.SetupUrl is null) if (info.SetupUrl is null)
{ {
Process.Start(new ProcessStartInfo(info.PageUrl) { UseShellExecute = true }); Process.Start(new ProcessStartInfo(info.PageUrl) { UseShellExecute = true });
return; return;
} }
try try
{ {
var setup = await RunWithProgress($"Update auf {info.Version}", "Neue Version wird heruntergeladen …", var setup = await RunWithProgress($"Update auf {info.Version}", "Neue Version wird heruntergeladen …",
(sink, ct) => UpdateChecker.DownloadSetupAsync(info, (done, total) => { sink.SetTotal(total); sink.SetDone(done); }, ct)); (sink, ct) => UpdateChecker.DownloadSetupAsync(info, (done, total) => { sink.SetTotal(total); sink.SetDone(done); }, ct));
// Installer still ausführen und DeskCore danach automatisch neu starten // Installer still ausführen und DeskCore danach automatisch neu starten.
Process.Start(new ProcessStartInfo(setup, "/S /launch") { UseShellExecute = true }); // Windows fragt dabei nach Administratorrechten (der Installer kümmert sich auch um die .NET-Laufzeit).
App.Quit(); Process.Start(new ProcessStartInfo(setup, "/S /launch") { UseShellExecute = true });
} App.Quit();
catch (OperationCanceledException) { ShowToast("Update abgebrochen", "", info: true); } }
catch (Exception ex) { Error("Update fehlgeschlagen", $"{ex.Message}\n\nDu kannst die neue Version auch selbst herunterladen:\n{info.PageUrl}"); } catch (OperationCanceledException) { ShowToast("Update abgebrochen", "", info: true); }
} catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 1223)
} {
ShowToast("Update abgebrochen – bitte die Windows-Abfrage mit „Ja“ bestätigen", "", info: true);
}
catch (Exception ex) { Error("Update fehlgeschlagen", $"{ex.Message}\n\nDu kannst die neue Version auch selbst herunterladen:\n{info.PageUrl}"); }
}
}
+3 -1
View File
@@ -62,6 +62,7 @@ public partial class MainWindow
ShowToast($"„{g.Name}“ ist noch leer", "", info: true); ShowToast($"„{g.Name}“ ist noch leer", "", info: true);
return; return;
} }
Usage.Record(Usage.GroupPrefix + g.Name); // damit die Schnellstart-Suche häufige Gruppen weiter oben zeigt
int started = 0; int started = 0;
var problems = new List<string>(); var problems = new List<string>();
var items = g.Items.ToList(); var items = g.Items.ToList();
@@ -72,7 +73,7 @@ public partial class MainWindow
if (!File.Exists(p) && !Directory.Exists(p)) { problems.Add($"„{Storage.DisplayName(p)}“ gibt es nicht mehr"); continue; } if (!File.Exists(p) && !Directory.Exists(p)) { problems.Add($"„{Storage.DisplayName(p)}“ gibt es nicht mehr"); continue; }
try try
{ {
Process.Start(new ProcessStartInfo(p) { UseShellExecute = true }); Usage.Start(p);
started++; started++;
} }
catch (Exception ex) { problems.Add($"„{Storage.DisplayName(p)}“: {ex.Message}"); } catch (Exception ex) { problems.Add($"„{Storage.DisplayName(p)}“: {ex.Message}"); }
@@ -206,6 +207,7 @@ public partial class MainWindow
if (form is null) return; if (form is null) return;
var before = (g.Name, g.DelaySeconds, g.Color); var before = (g.Name, g.DelaySeconds, g.Color);
(g.Name, g.DelaySeconds, g.Color) = form.Value; (g.Name, g.DelaySeconds, g.Color) = form.Value;
Usage.Rename(Usage.GroupPrefix + before.Name, Usage.GroupPrefix + g.Name);
S.Save(); S.Save();
var entry = new UndoEntry($"Start-Gruppe „{before.Name}“ bearbeiten"); var entry = new UndoEntry($"Start-Gruppe „{before.Name}“ bearbeiten");
entry.Add(() => { (g.Name, g.DelaySeconds, g.Color) = before; S.Save(); return null; }); entry.Add(() => { (g.Name, g.DelaySeconds, g.Color) = before; S.Save(); return null; });
+132 -2
View File
@@ -1,3 +1,4 @@
using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
@@ -23,6 +24,10 @@ public partial class MainWindow
{ {
int boxes = S.Fences.Count(f => f.Page == i); int boxes = S.Fences.Count(f => f.Page == i);
var info = (boxes == 1 ? "1 Box" : $"{boxes} Boxen") + (i == S.CurrentPage ? " · wird gerade angezeigt" : ""); var info = (boxes == 1 ? "1 Box" : $"{boxes} Boxen") + (i == S.CurrentPage ? " · wird gerade angezeigt" : "");
var o = S.PageOptions.GetValueOrDefault(name);
if (o?.Scheduled == true) info += " · " + PageScheduler.Describe(o);
if (string.Equals(S.PageFallback, name, StringComparison.OrdinalIgnoreCase)) info += " · sonst immer";
if (o?.Wallpaper is not null) info += " · eigenes Hintergrundbild";
return new PageRow(i, name, info, (Brush)res[i == S.CurrentPage ? "AccentBrush" : "ControlStrongStrokeBrush"]); return new PageRow(i, name, info, (Brush)res[i == S.CurrentPage ? "AccentBrush" : "ControlStrongStrokeBrush"]);
}).ToList(); }).ToList();
@@ -46,7 +51,7 @@ public partial class MainWindow
DesktopTintSwatches.Children.Add(rb); DesktopTintSwatches.Children.Add(rb);
} }
AddSwatch(null, Brushes.Transparent, "Keine Tönung"); AddSwatch(null, Brushes.Transparent, "Keine Tönung");
foreach (var (name, hex) in CategoryColors.All.Concat([("Grau", "#8A8A8A"), ("Weiß", "#F5F5F5")])) foreach (var (name, hex) in CategoryColors.All.Concat([("Weiß", "#F5F5F5")]))
AddSwatch(hex, CategoryColors.Brush(hex)!, name); AddSwatch(hex, CategoryColors.Brush(hex)!, name);
DesktopTintStrength.Value = Math.Round(S.DesktopTintStrength * 100); DesktopTintStrength.Value = Math.Round(S.DesktopTintStrength * 100);
DesktopTintStrength.IsEnabled = S.DesktopTint is not null; DesktopTintStrength.IsEnabled = S.DesktopTint is not null;
@@ -106,11 +111,134 @@ public partial class MainWindow
if ((sender as FrameworkElement)?.Tag is not int i || i >= S.Pages.Count) return; if ((sender as FrameworkElement)?.Tag is not int i || i >= S.Pages.Count) return;
var name = Dialog.Prompt(this, "Seite umbenennen", "Neuer Name:", S.Pages[i], "Umbenennen", t => ValidatePageName(t, i)); var name = Dialog.Prompt(this, "Seite umbenennen", "Neuer Name:", S.Pages[i], "Umbenennen", t => ValidatePageName(t, i));
if (name is null) return; if (name is null) return;
S.Pages[i] = name.Trim(); S.RenamePage(i, name.Trim());
S.Save(); S.Save();
LoadDesktopSettings(); LoadDesktopSettings();
} }
// ---------- Zeitplan & Hintergrundbild ----------
private void PageSchedule_Click(object sender, RoutedEventArgs e)
{
if ((sender as FrameworkElement)?.Tag is not int i || i >= S.Pages.Count) return;
var page = S.Pages[i];
var o = S.PageOption(page);
var res = Application.Current.Resources;
var panel = new StackPanel();
TextBlock Label(string t, double top = 18) => new()
{
Text = t, Margin = new Thickness(0, top, 0, 8), FontWeight = FontWeights.SemiBold, FontSize = 13,
};
TextBlock Hint(string t) => new()
{
Text = t, FontSize = 12, TextWrapping = TextWrapping.Wrap, Foreground = (Brush)res["TextSecondaryBrush"],
Margin = new Thickness(0, 4, 0, 0),
};
// Zeitplan
var scheduled = new CheckBox { Content = "Diese Seite automatisch anzeigen", IsChecked = o.Scheduled };
panel.Children.Add(scheduled);
var details = new StackPanel { Margin = new Thickness(28, 0, 0, 0) };
details.Children.Add(Label("An diesen Tagen", 12));
var days = new WrapPanel();
string[] dayNames = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
var dayBoxes = new List<CheckBox>();
for (int d = 0; d < 7; d++)
{
var cb = new CheckBox { Content = dayNames[d], IsChecked = (o.Days & (1 << d)) != 0, Margin = new Thickness(0, 0, 14, 4) };
dayBoxes.Add(cb);
days.Children.Add(cb);
}
details.Children.Add(days);
details.Children.Add(Label("Von – bis"));
var from = new TextBox { Text = o.From, Width = 76, HorizontalContentAlignment = HorizontalAlignment.Center };
var to = new TextBox { Text = o.To, Width = 76, HorizontalContentAlignment = HorizontalAlignment.Center };
details.Children.Add(new StackPanel
{
Orientation = Orientation.Horizontal,
Children =
{
from,
new TextBlock { Text = "bis", Margin = new Thickness(12, 0, 12, 0), VerticalAlignment = VerticalAlignment.Center },
to,
new TextBlock { Text = "Uhr", Margin = new Thickness(12, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center },
},
});
details.Children.Add(Hint("Über Mitternacht geht auch, z. B. 22:00 bis 06:00."));
panel.Children.Add(details);
void UpdateEnabled() => details.IsEnabled = scheduled.IsChecked == true;
scheduled.Click += (_, _) => UpdateEnabled();
UpdateEnabled();
var fallback = new CheckBox
{
Content = "Zu allen anderen Zeiten diese Seite anzeigen", Margin = new Thickness(0, 18, 0, 0),
IsChecked = string.Equals(S.PageFallback, page, StringComparison.OrdinalIgnoreCase),
};
panel.Children.Add(fallback);
panel.Children.Add(Hint("Beispiel: „Arbeit“ Mo–Fr 08:00–17:00 und bei „Freizeit“ diesen Haken setzen."));
// Hintergrundbild
panel.Children.Add(Label("Hintergrundbild", 22));
string? wallpaper = o.Wallpaper;
var wpText = new TextBlock { TextTrimming = TextTrimming.CharacterEllipsis, VerticalAlignment = VerticalAlignment.Center };
var wpRemove = new Button { Content = "Entfernen", Margin = new Thickness(8, 0, 0, 0) };
void UpdateWallpaper()
{
wpText.Text = wallpaper is null ? "Keins – der Hintergrund bleibt, wie er ist" : Path.GetFileName(wallpaper);
wpText.ToolTip = wallpaper;
wpRemove.IsEnabled = wallpaper is not null;
}
var wpPick = new Button { Content = "Bild wählen …" };
wpPick.Click += (_, _) =>
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Title = $"Hintergrundbild für „{page}“", Filter = "Bilder|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.webp|Alle Dateien|*.*",
};
if (wallpaper is not null) dlg.InitialDirectory = Path.GetDirectoryName(wallpaper);
if (dlg.ShowDialog() == true) { wallpaper = dlg.FileName; UpdateWallpaper(); }
};
wpRemove.Click += (_, _) => { wallpaper = null; UpdateWallpaper(); };
UpdateWallpaper();
var wpRow = new DockPanel();
var wpButtons = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(12, 0, 0, 0), Children = { wpPick, wpRemove } };
DockPanel.SetDock(wpButtons, Dock.Right);
wpRow.Children.Add(wpButtons);
wpRow.Children.Add(wpText);
panel.Children.Add(wpRow);
panel.Children.Add(Hint("Wechselst du auf eine Seite ohne eigenes Bild, kommt dein ursprünglicher Hintergrund zurück."));
string? Validate()
{
if (scheduled.IsChecked != true) return null;
if (!TimeSpan.TryParse(from.Text.Trim(), out var f) || f.TotalHours >= 24 ||
!TimeSpan.TryParse(to.Text.Trim(), out var t) || t.TotalHours >= 24)
return "Bitte die Uhrzeiten so eingeben: 08:00";
if (f == t) return "Start und Ende dürfen nicht gleich sein.";
if (!dayBoxes.Any(b => b.IsChecked == true)) return "Bitte mindestens einen Tag auswählen.";
return null;
}
if (!Dialog.Show(this, $"Seite „{page}“", panel, "Speichern", "Abbrechen", Validate)) return;
o.Scheduled = scheduled.IsChecked == true;
o.Days = dayBoxes.Select((b, d) => b.IsChecked == true ? 1 << d : 0).Sum();
if (TimeSpan.TryParse(from.Text.Trim(), out var ff)) o.From = ff.ToString(@"hh\:mm");
if (TimeSpan.TryParse(to.Text.Trim(), out var tt)) o.To = tt.ToString(@"hh\:mm");
if (fallback.IsChecked == true) S.PageFallback = page;
else if (string.Equals(S.PageFallback, page, StringComparison.OrdinalIgnoreCase)) S.PageFallback = null;
bool wallpaperChanged = !string.Equals(o.Wallpaper, wallpaper, StringComparison.OrdinalIgnoreCase);
o.Wallpaper = wallpaper;
S.Save();
if (wallpaperChanged && i == S.CurrentPage) PageScheduler.ApplyWallpaper(i);
PageScheduler.Reset();
PageScheduler.Tick();
LoadDesktopSettings();
ShowToast($"Seite „{page}“ gespeichert", "");
}
private void DeletePage_Click(object sender, RoutedEventArgs e) private void DeletePage_Click(object sender, RoutedEventArgs e)
{ {
if ((sender as FrameworkElement)?.Tag is not int i || i >= S.Pages.Count) return; if ((sender as FrameworkElement)?.Tag is not int i || i >= S.Pages.Count) return;
@@ -126,6 +254,8 @@ public partial class MainWindow
"Löschen")) "Löschen"))
return; return;
FenceManager.DeletePage(i); FenceManager.DeletePage(i);
S.ForgetPage(name);
S.Save();
DesktopToggle.Apply(); DesktopToggle.Apply();
Refresh(); Refresh();
LoadDesktopSettings(); LoadDesktopSettings();
+58
View File
@@ -0,0 +1,58 @@
using System.Windows;
using System.Windows.Input;
namespace DeskCore;
/// <summary>Einstellungen der Schnellstart-Suche.</summary>
public partial class MainWindow
{
private void LoadQuickSearchSettings()
{
QuickSearchToggle.IsChecked = S.QuickSearchEnabled;
QuickSearchHotkeyBox.Text = Hotkey.Describe(S.QuickSearchModifiers, S.QuickSearchKey);
QuickSearchHotkeyBox.IsEnabled = S.QuickSearchEnabled;
QuickSearchStartMenuBox.IsChecked = S.QuickSearchStartMenu;
UpdateQuickSearchHint(App.QuickSearchRegistered);
}
private void QuickSearchToggle_Click(object sender, RoutedEventArgs e)
{
S.QuickSearchEnabled = QuickSearchToggle.IsChecked == true;
S.Save();
App.ApplyQuickSearchHotkey();
LoadQuickSearchSettings();
}
private void QuickSearchStartMenu_Click(object sender, RoutedEventArgs e)
{
S.QuickSearchStartMenu = QuickSearchStartMenuBox.IsChecked == true;
S.Save();
}
private void QuickSearchHotkeyBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
var key = e.Key == Key.System ? e.SystemKey : e.Key;
if (key is Key.Tab) return;
e.Handled = true;
if (key is Key.LeftCtrl or Key.RightCtrl or Key.LeftAlt or Key.RightAlt or Key.LeftShift or Key.RightShift
or Key.LWin or Key.RWin or Key.Escape) return;
int mods = (int)Keyboard.Modifiers; // Alt=1, Strg=2, Umschalt=4, Win=8 – gleiche Bits wie RegisterHotKey
if ((mods & (Hotkey.ModCtrl | Hotkey.ModAlt | Hotkey.ModWin)) == 0)
{
QuickSearchHint.Text = "Bitte mit Strg, Alt oder Win kombinieren.";
return;
}
S.QuickSearchModifiers = mods;
S.QuickSearchKey = KeyInterop.VirtualKeyFromKey(key);
S.Save();
QuickSearchHotkeyBox.Text = Hotkey.Describe(S.QuickSearchModifiers, S.QuickSearchKey);
UpdateQuickSearchHint(App.ApplyQuickSearchHotkey());
}
private void UpdateQuickSearchHint(bool ok) =>
QuickSearchHint.Text = !S.QuickSearchEnabled ? "Ausgeschaltet"
: ok ? $"{Hotkey.Describe(S.QuickSearchModifiers, S.QuickSearchKey)} öffnet von überall eine Suchleiste: ein paar Buchstaben tippen, Enter – fertig. " +
"Was du oft startest, steht weiter oben."
: "⚠ Diese Kombination ist schon von einem anderen Programm belegt – bitte ins Feld klicken und eine andere drücken.";
}
+24 -3
View File
@@ -27,7 +27,8 @@
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <!-- Anzahl bzw. Zieh-Griff teilen sich diese Spalte – feste Mindestbreite, damit der Griff nicht ins Box-Symbol ragt -->
<ColumnDefinition Width="Auto" MinWidth="30"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="{Binding Glyph}" FontFamily="{DynamicResource IconFont}" FontSize="16" <TextBlock Text="{Binding Glyph}" FontFamily="{DynamicResource IconFont}" FontSize="16"
VerticalAlignment="Center" Margin="0,0,14,0"> VerticalAlignment="Center" Margin="0,0,14,0">
@@ -74,7 +75,7 @@
</TextBlock> </TextBlock>
<!-- Griff zum Umsortieren (nur beim Überfahren) --> <!-- Griff zum Umsortieren (nur beim Überfahren) -->
<TextBlock Grid.Column="3" Text="&#xE700;" FontFamily="{DynamicResource IconFont}" FontSize="14" Cursor="SizeNS" <TextBlock Grid.Column="3" Text="&#xE700;" FontFamily="{DynamicResource IconFont}" FontSize="14" Cursor="SizeNS"
Foreground="{DynamicResource TextSecondaryBrush}" VerticalAlignment="Center" HorizontalAlignment="Right" Foreground="{DynamicResource TextSecondaryBrush}" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="10,0,0,0"
ToolTip="Ziehen zum Umsortieren (oder Alt+↑/↓)"> ToolTip="Ziehen zum Umsortieren (oder Alt+↑/↓)">
<TextBlock.Style> <TextBlock.Style>
<Style TargetType="TextBlock"> <Style TargetType="TextBlock">
@@ -648,6 +649,23 @@
</StackPanel> </StackPanel>
</Border> </Border>
<TextBlock Text="Schnellstart-Suche" Style="{StaticResource SectionTitle}"/>
<Border Style="{StaticResource Card}">
<StackPanel>
<DockPanel>
<CheckBox x:Name="QuickSearchToggle" DockPanel.Dock="Right" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="QuickSearchToggle_Click"/>
<TextBox x:Name="QuickSearchHotkeyBox" DockPanel.Dock="Right" Width="200" IsReadOnly="True" Margin="0,0,16,0" IsReadOnlyCaretVisible="False"
VerticalAlignment="Center" PreviewKeyDown="QuickSearchHotkeyBox_PreviewKeyDown" ToolTip="Hier klicken und neue Tastenkombination drücken"/>
<TextBlock DockPanel.Dock="Left" Text="&#xE721;" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
<StackPanel VerticalAlignment="Center" Margin="0,0,16,0">
<TextBlock Text="Programme blitzschnell starten"/>
<TextBlock x:Name="QuickSearchHint" FontSize="12" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
</StackPanel>
</DockPanel>
<CheckBox x:Name="QuickSearchStartMenuBox" Content="Auch Programme aus dem Windows-Startmenü finden" Margin="34,12,0,0" Click="QuickSearchStartMenu_Click"/>
</StackPanel>
</Border>
<TextBlock Text="Desktop-Boxen" Style="{StaticResource SectionTitle}"/> <TextBlock Text="Desktop-Boxen" Style="{StaticResource SectionTitle}"/>
<Border Style="{StaticResource Card}"> <Border Style="{StaticResource Card}">
<DockPanel> <DockPanel>
@@ -700,7 +718,7 @@
<StackPanel VerticalAlignment="Center" Margin="0,0,16,0"> <StackPanel VerticalAlignment="Center" Margin="0,0,16,0">
<TextBlock Text="Desktop-Seiten"/> <TextBlock Text="Desktop-Seiten"/>
<TextBlock x:Name="PagesHint" FontSize="12" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}" <TextBlock x:Name="PagesHint" FontSize="12" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"
Text="Jede Seite hat ihre eigenen Boxen. Wechseln mit Win + Alt + ← / →, mit dem Mausrad über dem freien Desktop oder über das Symbol neben der Uhr."/> Text="Jede Seite hat ihre eigenen Boxen. Wechseln mit Win + Alt + ← / →, mit dem Mausrad über dem freien Desktop oder über das Symbol neben der Uhr. Über die Uhr bei einer Seite stellst du einen Zeitplan und ein eigenes Hintergrundbild ein."/>
</StackPanel> </StackPanel>
</DockPanel> </DockPanel>
<ItemsControl x:Name="PageList" Margin="34,10,0,0"> <ItemsControl x:Name="PageList" Margin="34,10,0,0">
@@ -709,6 +727,9 @@
<DockPanel Margin="0,4,0,0"> <DockPanel Margin="0,4,0,0">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal"> <StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Button Content="Anzeigen" Click="ShowPage_Click" Tag="{Binding Index}" Margin="0,0,6,0"/> <Button Content="Anzeigen" Click="ShowPage_Click" Tag="{Binding Index}" Margin="0,0,6,0"/>
<Button Style="{StaticResource SubtleButton}" Click="PageSchedule_Click" Tag="{Binding Index}" ToolTip="Zeitplan und Hintergrundbild">
<TextBlock Text="&#xE823;" FontFamily="{DynamicResource IconFont}" FontSize="14"/>
</Button>
<Button Style="{StaticResource SubtleButton}" Click="RenamePage_Click" Tag="{Binding Index}" ToolTip="Umbenennen"> <Button Style="{StaticResource SubtleButton}" Click="RenamePage_Click" Tag="{Binding Index}" ToolTip="Umbenennen">
<TextBlock Text="&#xE70F;" FontFamily="{DynamicResource IconFont}" FontSize="14"/> <TextBlock Text="&#xE70F;" FontFamily="{DynamicResource IconFont}" FontSize="14"/>
</Button> </Button>
+12 -2
View File
@@ -314,7 +314,9 @@ public partial class MainWindow : Window
SetEmpty(cat.Glyph, "Noch leer", $"Ziehe Icons vom Desktop hierher oder auf „{cat.Name}“ in der Seitenleiste."); SetEmpty(cat.Glyph, "Noch leer", $"Ziehe Icons vom Desktop hierher oder auf „{cat.Name}“ in der Seitenleiste.");
} }
items = items.OrderBy(i => !Directory.Exists(i.Path)) items = (S.MainSort == "Häufig"
? items.OrderBy(i => !Directory.Exists(i.Path)).ThenByDescending(i => Usage.Score(i.Path))
: items.OrderBy(i => !Directory.Exists(i.Path)))
.ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase).ToList(); .ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase).ToList();
var px = IconPixels; var px = IconPixels;
foreach (var item in items) IconLoader.Load(item, px); foreach (var item in items) IconLoader.Load(item, px);
@@ -456,6 +458,13 @@ public partial class MainWindow : Window
if (S.View == mode) mi.InputGestureText = "✓"; if (S.View == mode) mi.InputGestureText = "✓";
ViewMenu.Items.Add(mi); ViewMenu.Items.Add(mi);
} }
ViewMenu.Items.Add(new Separator());
foreach (var (sort, label) in new[] { ("Name", "Nach Name sortieren"), ("Häufig", "Meistgenutzt zuerst") })
{
var mi = MenuItem(label, "", () => { S.MainSort = sort; S.Save(); Refresh(); }, bold: S.MainSort == sort);
if (S.MainSort == sort) mi.InputGestureText = "✓";
ViewMenu.Items.Add(mi);
}
ViewMenu.PlacementTarget = ViewButton; ViewMenu.PlacementTarget = ViewButton;
ViewMenu.IsOpen = true; ViewMenu.IsOpen = true;
} }
@@ -803,7 +812,7 @@ public partial class MainWindow : Window
private void Open(string path) private void Open(string path)
{ {
try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); } try { Usage.Start(path); }
catch (Exception ex) { Error("Öffnen fehlgeschlagen", $"„{Path.GetFileName(path)}“ konnte nicht geöffnet werden.\n\n{ex.Message}"); } catch (Exception ex) { Error("Öffnen fehlgeschlagen", $"„{Path.GetFileName(path)}“ konnte nicht geöffnet werden.\n\n{ex.Message}"); }
} }
@@ -1208,6 +1217,7 @@ public partial class MainWindow : Window
DeadShortcutsToggle.IsChecked = S.RemoveDeadShortcuts; DeadShortcutsToggle.IsChecked = S.RemoveDeadShortcuts;
LoadFenceSettings(); LoadFenceSettings();
LoadDesktopSettings(); LoadDesktopSettings();
LoadQuickSearchSettings();
AutoRemoveToggle.IsChecked = S.AutoSortRemoveFromDesktop; AutoRemoveToggle.IsChecked = S.AutoSortRemoveFromDesktop;
RulesList.ItemsSource = null; RulesList.ItemsSource = null;
RulesList.ItemsSource = S.Rules; RulesList.ItemsSource = S.Rules;
+97
View File
@@ -0,0 +1,97 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace DeskCore;
/// <summary>
/// Wechselt die Desktop-Seite nach Zeitplan (z. B. Mo–Fr 8–17 Uhr „Arbeit“, sonst „Freizeit“)
/// und setzt das Hintergrundbild einer Seite.
/// </summary>
public static class PageScheduler
{
/// <summary>Seite, die der Zeitplan zuletzt wollte. Gewechselt wird nur, wenn sich das ändert –
/// von Hand gewählte Seiten bleiben also bis zum nächsten Zeitplan-Wechsel stehen.</summary>
private static string? _lastWanted = "\0";
private static AppSettings S => AppSettings.Current;
/// <summary>Gibt es überhaupt einen Zeitplan?</summary>
public static bool Active => S.Pages.Count > 1 && S.Pages.Any(p => S.PageOptions.GetValueOrDefault(p)?.Scheduled == true);
/// <summary>Welche Seite laut Zeitplan jetzt dran ist (null = keine Vorgabe).</summary>
public static string? Wanted(DateTime now)
{
if (!Active) return null;
foreach (var p in S.Pages)
if (S.PageOptions.GetValueOrDefault(p)?.Matches(now) == true) return p;
return S.PageFallback is { } f && S.Pages.Contains(f, StringComparer.OrdinalIgnoreCase) ? f : null;
}
/// <summary>Regelmäßig aufrufen (alle paar Sekunden).</summary>
public static void Tick()
{
var wanted = Wanted(DateTime.Now);
if (string.Equals(wanted, _lastWanted, StringComparison.OrdinalIgnoreCase)) return;
_lastWanted = wanted;
if (wanted is null) return;
int index = S.Pages.FindIndex(p => string.Equals(p, wanted, StringComparison.OrdinalIgnoreCase));
if (index >= 0 && index != S.CurrentPage) FenceManager.SwitchPage(index);
}
/// <summary>Zeitplan geändert → beim nächsten Tick sofort anwenden.</summary>
public static void Reset() => _lastWanted = "\0";
/// <summary>Text für die Einstellungen, z. B. „Mo–Fr · 08:00–17:00“.</summary>
public static string Describe(PageOption o)
{
string[] names = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
var days = Enumerable.Range(0, 7).Where(i => (o.Days & (1 << i)) != 0).ToList();
string d = days.Count == 7 ? "Täglich"
: days.SequenceEqual([0, 1, 2, 3, 4]) ? "Mo–Fr"
: days.SequenceEqual([5, 6]) ? "Wochenende"
: days.Count > 2 && days.Last() - days.First() == days.Count - 1 ? $"{names[days.First()]}–{names[days.Last()]}"
: string.Join(", ", days.Select(i => names[i]));
return $"{d} · {o.From}–{o.To} Uhr";
}
// =====================================================================
// Hintergrundbild
// =====================================================================
/// <summary>Beim Seitenwechsel: Bild der Seite setzen – oder das ursprüngliche zurückholen.</summary>
public static void ApplyWallpaper(int pageIndex)
{
if (pageIndex < 0 || pageIndex >= S.Pages.Count) return;
var wanted = S.PageOptions.GetValueOrDefault(S.Pages[pageIndex])?.Wallpaper;
if (wanted is not null && !File.Exists(wanted)) wanted = null;
string? target;
if (wanted is not null)
{
// Vor dem ersten Wechsel das bisherige Bild merken, um es später zurückzuholen
S.SavedWallpaper ??= CurrentWallpaper() ?? "";
target = wanted;
}
else if (S.SavedWallpaper is { } saved)
{
target = saved;
S.SavedWallpaper = null;
}
else return; // Seiten ohne eigenes Bild lassen den Hintergrund in Ruhe
S.Save();
if (string.Equals(CurrentWallpaper(), target, StringComparison.OrdinalIgnoreCase)) return;
Task.Run(() => SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, target, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE));
}
public static string? CurrentWallpaper()
{
var sb = new StringBuilder(520);
return SystemParametersInfo(SPI_GETDESKWALLPAPER, (uint)sb.Capacity, sb, 0) ? sb.ToString() : null;
}
private const uint SPI_GETDESKWALLPAPER = 0x73, SPI_SETDESKWALLPAPER = 0x14, SPIF_UPDATEINIFILE = 0x1, SPIF_SENDCHANGE = 0x2;
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool SystemParametersInfo(uint action, uint param, string value, uint flags);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool SystemParametersInfo(uint action, uint param, StringBuilder value, uint flags);
}
+397
View File
@@ -0,0 +1,397 @@
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;
namespace DeskCore;
/// <summary>
/// Schnellstart-Suche: Tastenkürzel → Suchleiste mitten auf dem Bildschirm, tippen, Enter, läuft.
/// Durchsucht Kategorien, Desktop, Startmenü, Start-Gruppen, Kategorien und Desktop-Seiten.
/// Was du oft startest, steht weiter oben.
/// </summary>
public sealed class QuickSearch : Window
{
private const int MaxResults = 8;
private sealed record Entry(string Name, string Sub, string? Path, string Glyph, string Key, Action Run, Action? Locate = null)
{
/// <summary>Für die Ähnlichkeit: Name ohne Endungen wie „- Verknüpfung“.</summary>
public string SearchName { get; } = Name.Replace(" - Verknüpfung", "", StringComparison.OrdinalIgnoreCase);
}
private static QuickSearch? _open;
private static List<Entry>? _startMenu;
private static DateTime _startMenuRead;
private readonly TextBox _box;
private readonly ListBox _list;
private readonly TextBlock _hint;
private List<Entry> _all = [];
private List<Entry> _shown = [];
private bool _closing;
private static AppSettings S => AppSettings.Current;
/// <summary>Per Tastenkürzel: öffnen bzw. wieder schließen.</summary>
public static void Toggle()
{
if (_open is not null) { _open.CloseNow(); return; }
_open = new QuickSearch();
_open.Show();
}
private QuickSearch()
{
var res = Application.Current.Resources;
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
Background = Brushes.Transparent;
ResizeMode = ResizeMode.NoResize;
ShowInTaskbar = false;
Topmost = true;
Width = 680;
SizeToContent = SizeToContent.Height;
Title = "DeskCore Suche";
FontFamily = (FontFamily)res["UiFont"];
Foreground = (Brush)res["TextBrush"];
// Suchzeile
_box = new TextBox
{
FontSize = 20, BorderThickness = new Thickness(0), Background = Brushes.Transparent, Padding = new Thickness(0),
VerticalContentAlignment = VerticalAlignment.Center, Foreground = (Brush)res["TextBrush"],
CaretBrush = (Brush)res["TextBrush"],
};
var placeholder = new TextBlock
{
Text = "Programm, Datei, Start-Gruppe oder Kategorie suchen …", FontSize = 20, IsHitTestVisible = false,
Foreground = (Brush)res["TextTertiaryBrush"], VerticalAlignment = VerticalAlignment.Center,
};
_box.TextChanged += (_, _) =>
{
placeholder.Visibility = _box.Text.Length == 0 ? Visibility.Visible : Visibility.Collapsed;
Filter();
};
var searchRow = new DockPanel { Margin = new Thickness(18, 14, 18, 14) };
var glass = new TextBlock
{
Text = "", FontFamily = (FontFamily)res["IconFont"], FontSize = 20, Margin = new Thickness(0, 0, 14, 0),
VerticalAlignment = VerticalAlignment.Center, Foreground = (Brush)res["AccentBrush"],
};
DockPanel.SetDock(glass, Dock.Left);
searchRow.Children.Add(glass);
searchRow.Children.Add(new Grid { Children = { placeholder, _box } });
_list = new ListBox
{
BorderThickness = new Thickness(0), Background = Brushes.Transparent, Margin = new Thickness(6, 0, 6, 6),
Focusable = false, Visibility = Visibility.Collapsed,
};
ScrollViewer.SetHorizontalScrollBarVisibility(_list, ScrollBarVisibility.Disabled);
_list.MouseDoubleClick += (_, _) => RunSelected(locate: false);
_list.PreviewMouseLeftButtonUp += (_, e) =>
{
// Ein Klick genügt – wie im Startmenü
if (MainWindow.FindParent<ListBoxItem>(e.OriginalSource as DependencyObject) is not null) RunSelected(locate: false);
};
_hint = new TextBlock
{
FontSize = 12, Margin = new Thickness(18, 8, 18, 10), Foreground = (Brush)res["TextTertiaryBrush"],
Text = "Enter Starten · Strg + Enter Speicherort öffnen · Esc Schließen",
};
var divider = new Border { Height = 1, Background = (Brush)res["ControlStrokeBrush"] };
var footer = new Border { Child = _hint, BorderBrush = (Brush)res["ControlStrokeBrush"], BorderThickness = new Thickness(0, 1, 0, 0) };
var stack = new StackPanel { Children = { searchRow, divider, _list, footer } };
Content = new Border
{
Margin = new Thickness(24), CornerRadius = new CornerRadius(10),
Background = (Brush)res["FlyoutBrush"], BorderBrush = (Brush)res["FlyoutStrokeBrush"], BorderThickness = new Thickness(1),
Effect = (System.Windows.Media.Effects.Effect)res["DialogShadow"], Child = stack,
};
PreviewKeyDown += OnKey;
Deactivated += (_, _) => CloseNow();
Closed += (_, _) => { if (_open == this) _open = null; };
Loaded += (_, _) =>
{
Position();
BringToFront();
_box.Focus();
BuildIndex();
};
}
// =====================================================================
// Position & Fokus
// =====================================================================
private void Position()
{
var mouse = GetCursorPos(out var p) ? new Point(p.X / Screens.Scale, p.Y / Screens.Scale) : new Point(0, 0);
var wa = Screens.WorkAreaAt(mouse);
Left = wa.Left + (wa.Width - Width) / 2;
Top = wa.Top + wa.Height * 0.22;
}
private void BringToFront()
{
// Das Tastenkürzel erlaubt DeskCore, nach vorne zu kommen
var hwnd = new WindowInteropHelper(this).Handle;
SetForegroundWindow(hwnd);
Activate();
}
private void CloseNow()
{
if (_closing) return;
_closing = true;
Close();
}
// =====================================================================
// Was durchsucht wird
// =====================================================================
private async void BuildIndex()
{
var list = new List<Entry>();
// Start-Gruppen
foreach (var g in S.LaunchGroups)
{
var group = g;
list.Add(new Entry(g.Name, $"Start-Gruppe · {g.Items.Count} {(g.Items.Count == 1 ? "Programm" : "Programme")}", null, "",
Usage.GroupPrefix + g.Name, () => App.Win?.LaunchGroupNow(group)));
}
// Kategorien selbst (öffnet DeskCore dort) und Desktop-Seiten
foreach (var c in Storage.Categories())
{
var cat = c;
list.Add(new Entry(c, "Kategorie in DeskCore öffnen", null, S.Categories.GetValueOrDefault(c)?.Glyph ?? "",
"kategorie:" + c, () => { App.Win?.ShowFromBackground(); App.Win?.ShowCategory(cat); }));
}
if (S.Pages.Count > 1)
for (int i = 0; i < S.Pages.Count; i++)
{
int page = i;
list.Add(new Entry(S.Pages[i], "Desktop-Seite anzeigen", null, "", "seite:" + S.Pages[i], () => FenceManager.SwitchPage(page)));
}
_all = list;
Filter();
// Dateien aus Kategorien, Desktop und Startmenü im Hintergrund sammeln (Startmenü 10 Minuten zwischengespeichert)
bool withStartMenu = S.QuickSearchStartMenu;
var categories = Storage.Categories();
var files = await Task.Run(() =>
{
var l = new List<Entry>();
foreach (var c in categories)
foreach (var p in Storage.Entries(Storage.CategoryPath(c)))
l.Add(FileEntry(p, c));
foreach (var p in Storage.DesktopEntries())
l.Add(FileEntry(p, "Desktop"));
if (withStartMenu)
{
if (_startMenu is null || DateTime.Now - _startMenuRead > TimeSpan.FromMinutes(10))
{
_startMenu = ReadStartMenu();
_startMenuRead = DateTime.Now;
}
l.AddRange(_startMenu);
}
return Dedupe(l);
});
if (_closing) return;
_all = list.Concat(files).ToList();
Filter();
}
private static Entry FileEntry(string path, string where) =>
new(Storage.DisplayName(path), where, path, "", path, () => Usage.Start(path),
() => Process.Start("explorer.exe", $"/select,\"{path}\""));
private static List<Entry> ReadStartMenu()
{
var result = new List<Entry>();
foreach (var root in new[]
{
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
})
{
if (!Directory.Exists(root)) continue;
IEnumerable<string> files;
try { files = Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories).ToList(); }
catch { continue; }
foreach (var f in files)
{
var ext = Path.GetExtension(f).ToLowerInvariant();
if (ext is not (".lnk" or ".url" or ".appref-ms")) continue;
var name = Path.GetFileNameWithoutExtension(f);
// Deinstallations- und Hilfe-Verknüpfungen will niemand per Suche starten
if (name.Contains("uninstall", StringComparison.OrdinalIgnoreCase) || name.Contains("deinstall", StringComparison.OrdinalIgnoreCase))
continue;
result.Add(new Entry(name, "Startmenü", f, "", f, () => Usage.Start(f),
() => Process.Start("explorer.exe", $"/select,\"{f}\"")));
}
}
return result;
}
/// <summary>
/// Gleiches Programm aus Kategorie, Desktop und Startmenü nur einmal zeigen – die Reihenfolge der Quellen
/// entscheidet (Kategorie vor Desktop vor Startmenü). Gleicher Name gilt als gleiches Programm.
/// </summary>
private static List<Entry> Dedupe(IEnumerable<Entry> entries)
{
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
return entries.Where(e => e.Path is null || seen.Add(e.SearchName.Trim())).ToList();
}
// =====================================================================
// Suchen & Anzeigen
// =====================================================================
/// <summary>Wie gut passt der Name zur Eingabe? -1 = gar nicht.</summary>
private static int Match(string name, string q)
{
if (q.Length == 0) return 0;
var n = name.ToLowerInvariant();
if (n == q) return 1000;
if (n.StartsWith(q)) return 800;
var words = n.Split([' ', '-', '_', '.', '(', ')', '&'], StringSplitOptions.RemoveEmptyEntries);
if (words.Any(w => w.StartsWith(q))) return 650;
if (q.Length >= 2 && string.Concat(words.Select(w => w[0])).StartsWith(q)) return 600; // "vsc" → Visual Studio Code
if (n.Contains(q)) return 450;
// Buchstaben in der richtigen Reihenfolge ("fzla" → FileZilla) – aber nur ab einem Wortanfang,
// sonst passt fast alles irgendwie ("ets" → Malwarebytes)
if (q.Length < 3) return -1;
for (int start = 0; start < n.Length; start++)
{
if (n[start] != q[0] || (start > 0 && char.IsLetterOrDigit(n[start - 1]))) continue;
int k = 0;
for (int j = start; j < n.Length && k < q.Length; j++) if (n[j] == q[k]) k++;
if (k == q.Length) return 200;
}
return -1;
}
private void Filter()
{
var q = _box.Text.Trim().ToLowerInvariant();
if (q.Length == 0)
{
// Ohne Eingabe: zuletzt/oft benutzt
_shown = _all.Where(e => Usage.Score(e.Key) > 0)
.OrderByDescending(e => Usage.Score(e.Key)).Take(MaxResults).ToList();
}
else
{
_shown = _all.Select(e => (e, m: Match(e.SearchName, q)))
.Where(x => x.m >= 0)
.OrderByDescending(x => x.m + Math.Min(300, Usage.Score(x.e.Key) * 40))
.ThenBy(x => x.e.Name.Length)
.Take(MaxResults).Select(x => x.e).ToList();
}
ShowResults(q.Length > 0);
}
private void ShowResults(bool searching)
{
var res = Application.Current.Resources;
_list.Items.Clear();
int px = (int)Math.Round(32 * VisualTreeHelper.GetDpi(this).DpiScaleX);
foreach (var e in _shown)
{
FrameworkElement icon;
if (e.Path is not null)
{
var img = new Image { Width = 32, Height = 32 };
var item = new IconItem { Path = e.Path };
item.PropertyChanged += (_, _) => img.Source = item.Icon;
IconLoader.Load(item, px);
img.Source = item.Icon;
icon = img;
}
else
{
var meta = e.Key.StartsWith("kategorie:") ? S.Categories.GetValueOrDefault(e.Name) : null;
icon = new TextBlock
{
Text = e.Glyph, FontFamily = (FontFamily)res["IconFont"], FontSize = 22, Width = 32, TextAlignment = TextAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Foreground = CategoryColors.Brush(meta?.Color) ?? (Brush)res["AccentBrush"],
};
}
icon.Margin = new Thickness(0, 0, 14, 0);
DockPanel.SetDock(icon, Dock.Left);
var texts = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
texts.Children.Add(new TextBlock { Text = e.Name, FontSize = 15, TextTrimming = TextTrimming.CharacterEllipsis });
texts.Children.Add(new TextBlock { Text = e.Sub, FontSize = 12, Foreground = (Brush)res["TextSecondaryBrush"] });
_list.Items.Add(new DockPanel { Margin = new Thickness(8, 6, 8, 6), Children = { icon, texts } });
}
if (_shown.Count == 0 && searching)
_list.Items.Add(new TextBlock
{
Text = "Nichts gefunden", Margin = new Thickness(12, 10, 12, 10), Foreground = (Brush)res["TextSecondaryBrush"],
IsHitTestVisible = false,
});
_list.Visibility = _list.Items.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
if (_shown.Count > 0) _list.SelectedIndex = 0;
}
private void OnKey(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape:
CloseNow();
e.Handled = true;
break;
case Key.Down or Key.Up when _shown.Count > 0:
int i = _list.SelectedIndex + (e.Key == Key.Down ? 1 : -1);
_list.SelectedIndex = (i + _shown.Count) % _shown.Count;
_list.ScrollIntoView(_list.SelectedItem);
e.Handled = true;
break;
case Key.Enter:
RunSelected(locate: Keyboard.Modifiers.HasFlag(ModifierKeys.Control));
e.Handled = true;
break;
}
}
private void RunSelected(bool locate)
{
if (_list.SelectedIndex < 0 || _list.SelectedIndex >= _shown.Count) return;
var entry = _shown[_list.SelectedIndex];
CloseNow(); // zuerst schließen, damit das gestartete Programm nach vorne kommt
try
{
if (locate && entry.Locate is not null) entry.Locate();
else
{
entry.Run();
// Kategorien/Seiten mitzählen (Dateien zählt Usage.Start, Gruppen LaunchGroupNow selbst)
if (entry.Path is null && !entry.Key.StartsWith(Usage.GroupPrefix)) Usage.Record(entry.Key);
}
}
catch (Exception ex)
{
App.TrayIcon?.Notify("Starten fehlgeschlagen", $"„{entry.Name}“ konnte nicht geöffnet werden.\n{ex.Message}");
}
}
[StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; }
[DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT p);
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hwnd);
}
+487 -419
View File
@@ -1,419 +1,487 @@
<p align="center"> <p align="center">
<img src="app.png" width="110" alt="DeskCore-Logo"> <img src="app.png" width="110" alt="DeskCore-Logo">
</p> </p>
<h1 align="center">DeskCore</h1> <h1 align="center">DeskCore</h1>
<p align="center"> <p align="center">
<b>Endlich ein aufgeräumter Desktop.</b><br> <b>Endlich ein aufgeräumter Desktop.</b><br>
Sortiere deine Desktop-Icons in eigene Kategorien – übersichtlich, schnell und im Windows-11-Look. Sortiere deine Desktop-Icons in eigene Kategorien – übersichtlich, schnell und im Windows-11-Look.
</p> </p>
<p align="center"> <p align="center">
<a href="https://git.viper.ipv64.net/M_Viper/DeskCore/releases/latest"><b>⬇️ DeskCore herunterladen</b></a> <a href="https://git.viper.ipv64.net/M_Viper/DeskCore/releases/latest"><b>⬇️ DeskCore herunterladen</b></a>
&nbsp;·&nbsp; Windows 10 &amp; 11 &nbsp;·&nbsp; kostenlos für private Nutzung &nbsp;·&nbsp; Windows 10 &amp; 11 &nbsp;·&nbsp; kostenlos für private Nutzung
</p> </p>
<p align="center"> <p align="center">
<img src="docs/desktop.png" width="820" alt="DeskCore – Übersicht deines Desktops"> <img src="docs/desktop.png" width="820" alt="DeskCore – Übersicht deines Desktops">
<br><sub>Alle Icons deines Desktops auf einen Blick. Der grüne Haken ✓ zeigt, was schon in einer Kategorie gesichert ist.</sub> <br><sub>Alle Icons deines Desktops auf einen Blick. Der grüne Haken ✓ zeigt, was schon in einer Kategorie gesichert ist.</sub>
</p> </p>
Mit DeskCore sortierst du deine Desktop-Icons in eigene Gruppen, zum Beispiel „Spiele“, „Arbeit“ oder „Tools“. Mit DeskCore sortierst du deine Desktop-Icons in eigene Gruppen, zum Beispiel „Spiele“, „Arbeit“ oder „Tools“.
Danach kannst du die Icons vom Desktop löschen. In DeskCore bleiben sie erhalten und lassen sich mit einem Doppelklick starten. Danach kannst du die Icons vom Desktop löschen. In DeskCore bleiben sie erhalten und lassen sich mit einem Doppelklick starten.
--- ---
## Inhalt ## Inhalt
- [Auf einen Blick](#auf-einen-blick) - [Auf einen Blick](#auf-einen-blick)
- [Was du brauchst](#was-du-brauchst) - [Was du brauchst](#was-du-brauchst)
- [Installieren](#installieren) - [Installieren](#installieren)
- [So räumst du deinen Desktop auf](#so-räumst-du-deinen-desktop-auf) - [So räumst du deinen Desktop auf](#so-räumst-du-deinen-desktop-auf)
- [Programme starten](#programme-starten) - [Programme starten](#programme-starten)
- [Start-Gruppen – mehrere Programme mit einem Klick](#start-gruppen--mehrere-programme-mit-einem-klick) - [Schnellstart-Suche](#schnellstart-suche)
- [Boxen auf dem Desktop](#boxen-auf-dem-desktop) - [Start-Gruppen – mehrere Programme mit einem Klick](#start-gruppen--mehrere-programme-mit-einem-klick)
- [Desktop-Seiten](#desktop-seiten) - [Boxen auf dem Desktop](#boxen-auf-dem-desktop)
- [Den Desktop schick machen](#den-desktop-schick-machen) - [Desktop-Seiten](#desktop-seiten)
- [Automatisch sortieren](#automatisch-sortieren) - [Den Desktop schick machen](#den-desktop-schick-machen)
- [Weitere Extras](#weitere-extras) - [Automatisch sortieren](#automatisch-sortieren)
- [Alle Tastenkürzel](#alle-tastenkürzel) - [Weitere Extras](#weitere-extras)
- [Hoppla, falsch geklickt?](#hoppla-falsch-geklickt) - [Alle Tastenkürzel](#alle-tastenkürzel)
- [Häufige Fragen](#häufige-fragen) - [Hoppla, falsch geklickt?](#hoppla-falsch-geklickt)
- [Deinstallieren](#deinstallieren) - [Häufige Fragen](#häufige-fragen)
- [Lizenz](#lizenz) - [Deinstallieren](#deinstallieren)
- [Lizenz](#lizenz)
---
---
## Auf einen Blick
## Auf einen Blick
| | |
|---|---| | | |
| 🗂️ **Eigene Kategorien** | mit Symbol und Farbe, Reihenfolge frei wählbar | |---|---|
| ✅ **Sicher aufräumen** | Icons werden gesichert, bevor du sie vom Desktop löschst | | 🗂️ **Eigene Kategorien** | mit Symbol und Farbe, Reihenfolge frei wählbar |
| ▶️ **Start-Gruppen** | ein Klick startet mehrere Programme auf einmal | | ✅ **Sicher aufräumen** | Icons werden gesichert, bevor du sie vom Desktop löschst |
| 🧱 **Desktop-Boxen** | Kategorien oder Ordner als Kästen direkt auf dem Desktop, mit Tabs und eigenem Aussehen | | 🚀 **Schnellstart-Suche** | Alt + Umschalt + Leertaste, ein paar Buchstaben, Enter – läuft |
| 📑 **Desktop-Seiten** | mehrere Desktops zum Umschalten, z. B. „Arbeit“ und „Freizeit“ | | ▶️ **Start-Gruppen** | ein Klick startet mehrere Programme auf einmal |
| 🎨 **Icons einfärben** | alle Desktop-Icons in einem einheitlichen Farbton | | 🧱 **Desktop-Boxen** | Kategorien oder Ordner als Kästen direkt auf dem Desktop, mit Tabs und eigenem Aussehen |
| 👁️ **Schnell verstecken** | Doppelklick auf den Desktop – alles weg, noch ein Doppelklick – alles wieder da | | 📑 **Desktop-Seiten** | mehrere Desktops zum Umschalten, z. B. „Arbeit“ und „Freizeit“ – auch automatisch nach Uhrzeit |
| ⚡ **Automatisch sortieren** | neue Icons landen per Regel von selbst in der richtigen Kategorie | | ⭐ **Meistgenutzt zuerst** | DeskCore merkt sich, was du oft startest, und sortiert danach |
| 🧹 **Räumt selbst auf** | deinstallierte Programme verschwinden auch aus DeskCore | | 🎨 **Icons einfärben** | alle Desktop-Icons in einem einheitlichen Farbton |
| 🔍 **Suche** | jedes Programm in Sekunden finden | | 👁️ **Schnell verstecken** | Doppelklick auf den Desktop – alles weg, noch ein Doppelklick – alles wieder da |
| ↩️ **Rückgängig** | Strg + Z macht jeden Schritt rückgängig | | ⚡ **Automatisch sortieren** | neue Icons landen per Regel von selbst in der richtigen Kategorie |
| 🌙 **Hell &amp; Dunkel** | passt sich Windows an oder nach deinem Geschmack | | 🧹 **Räumt selbst auf** | deinstallierte Programme verschwinden auch aus DeskCore |
| 💾 **Sicherung** | alles als ZIP sichern, z. B. für einen neuen PC | | 🔍 **Suche** | jedes Programm in Sekunden finden |
| 🔄 **Updates** | DeskCore sagt Bescheid, wenn es eine neue Version gibt | | ↩️ **Rückgängig** | Strg + Z macht jeden Schritt rückgängig |
| 🌙 **Hell &amp; Dunkel** | passt sich Windows an oder nach deinem Geschmack |
--- | 💾 **Sicherung** | alles als ZIP sichern, z. B. für einen neuen PC |
| 🔄 **Updates** | DeskCore sagt Bescheid, wenn es eine neue Version gibt |
## Was du brauchst
---
- Windows 10 oder Windows 11 (64 Bit)
- Das war's. Falls DeskCore noch etwas von Microsoft braucht („.NET“), erledigt das der Installer automatisch. ## Was du brauchst
--- - Windows 10 oder Windows 11 (64 Bit)
- Das war's. Falls DeskCore noch etwas von Microsoft braucht („.NET“), erledigt das der Installer automatisch.
## Installieren
---
1. Doppelklick auf **`DeskCore-Setup.exe`**
2. **Weiter** klicken ## Installieren
3. Die Lizenz lesen, den Haken bei **„Ich akzeptiere …“** setzen, dann **Weiter**
4. Bei Bedarf noch auswählen: 1. Doppelklick auf **`DeskCore-Setup.exe`**
- Verknüpfung auf dem Desktop (empfohlen) 2. Windows fragt: **„Möchten Sie zulassen, dass durch diese App Änderungen an Ihrem Gerät vorgenommen werden?“** → **Ja**
- Mit Windows starten 3. **Weiter** klicken
5. **Installieren** klicken, und fertig. 4. Die Lizenz lesen, den Haken bei **„Ich akzeptiere …“** setzen, dann **Weiter**
5. Bei Bedarf noch auswählen:
Du brauchst dafür **keine Administratorrechte**. - Verknüpfung auf dem Desktop (empfohlen)
Nur wenn „.NET“ fehlt und installiert wird, fragt Windows einmal nach. Einfach mit **Ja** bestätigen. - Mit Windows starten
6. **Installieren** klicken, und fertig.
> **Windows warnt beim Start des Installers?**
> Bei neuen Programmen zeigt Windows manchmal „Der Computer wurde durch Windows geschützt“. Der Installer braucht **Administratorrechte**, weil er bei Bedarf die von DeskCore benötigte Microsoft-Laufzeit („.NET“) gleich mitinstalliert.
> Klick dann auf **„Weitere Informationen“** und danach auf **„Trotzdem ausführen“**. DeskCore selbst läuft danach ganz normal **ohne** Administratorrechte.
**Update auf eine neue Version:** Einfach den neuen Installer starten. Deine Kategorien und Einstellungen bleiben erhalten. > **Windows warnt beim Start des Installers?**
> Bei neuen Programmen zeigt Windows manchmal „Der Computer wurde durch Windows geschützt“.
--- > Klick dann auf **„Weitere Informationen“** und danach auf **„Trotzdem ausführen“**.
## So räumst du deinen Desktop auf **Update auf eine neue Version:** Einfach den neuen Installer starten – oder in DeskCore auf **„Jetzt aktualisieren“** klicken und die Windows-Abfrage mit **Ja** bestätigen. Deine Kategorien und Einstellungen bleiben erhalten.
### 1. Kategorie anlegen ---
Unten links auf **„+ Neue Kategorie“** klicken und einen Namen eingeben, z. B. **Spiele**.
Auf Wunsch suchst du dir noch ein Symbol und eine Farbe aus. ## So räumst du deinen Desktop auf
### 2. Icons hineinziehen ### 1. Kategorie anlegen
Zieh ein Icon **vom Desktop** mit der Maus auf deine Kategorie links. Unten links auf **„+ Neue Kategorie“** klicken und einen Namen eingeben, z. B. **Spiele**.
Es wird jetzt in DeskCore **gesichert**. Auf dem Desktop bleibt es vorerst liegen. Auf Wunsch suchst du dir noch ein Symbol und eine Farbe aus.
> Tipp: Du kannst auch mehrere Icons auf einmal markieren und ziehen. ### 2. Icons hineinziehen
Zieh ein Icon **vom Desktop** mit der Maus auf deine Kategorie links.
<p align="center"> Es wird jetzt in DeskCore **gesichert**. Auf dem Desktop bleibt es vorerst liegen.
<img src="docs/kategorie.png" width="820" alt="Eine Kategorie in DeskCore">
<br><sub>Eine fertige Kategorie: Alle Programme ordentlich beisammen, starten per Doppelklick.</sub> > Tipp: Du kannst auch mehrere Icons auf einmal markieren und ziehen.
</p>
<p align="center">
### 3. Icons vom Desktop löschen <img src="docs/kategorie.png" width="820" alt="Eine Kategorie in DeskCore">
Klick links auf **„Desktop“**. Icons mit einem **grünen Haken ✓** sind schon gesichert. <br><sub>Eine fertige Kategorie: Alle Programme ordentlich beisammen, starten per Doppelklick.</sub>
Die kannst du ohne Sorge vom Desktop löschen: </p>
**Rechtsklick → „Vom Desktop löschen“** oder ganz normal auf dem Desktop löschen.
### 3. Icons vom Desktop löschen
**Fertig!** Dein Desktop ist leer, und deine Programme findest du jetzt in DeskCore. Klick links auf **„Desktop“**. Icons mit einem **grünen Haken ✓** sind schon gesichert.
Die kannst du ohne Sorge vom Desktop löschen:
### Kategorien verwalten **Rechtsklick → „Vom Desktop löschen“** oder ganz normal auf dem Desktop löschen.
- **Umbenennen, Symbol oder Farbe ändern:** Kategorie anklicken → oben **„Bearbeiten“**
- **Reihenfolge ändern:** Kategorie links mit der Maus **nach oben oder unten ziehen** (oder Rechtsklick → **Nach oben / Nach unten**) **Fertig!** Dein Desktop ist leer, und deine Programme findest du jetzt in DeskCore.
- **Ordner im Explorer ansehen:** oben **„Im Explorer öffnen“**
- **Löschen:** oben **„Kategorie löschen“** – die Icons landen im Papierkorb, Strg + Z holt alles zurück ### Kategorien verwalten
- **Umbenennen, Symbol oder Farbe ändern:** Kategorie anklicken → oben **„Bearbeiten“**
--- - **Reihenfolge ändern:** Kategorie links mit der Maus **nach oben oder unten ziehen** (oder Rechtsklick → **Nach oben / Nach unten**)
- **Ordner im Explorer ansehen:** oben **„Im Explorer öffnen“**
## Programme starten - **Löschen:** oben **„Kategorie löschen“** – die Icons landen im Papierkorb, Strg + Z holt alles zurück
- Kategorie links anklicken ---
- **Doppelklick** auf das Icon
## Programme starten
Oder schneller: **Suchfeld** oben benutzen (Strg + F), den Namen eintippen und Doppelklick.
Die Suche findet Programme in allen Kategorien und auf dem Desktop. - Kategorie links anklicken
- **Doppelklick** auf das Icon
Auch über das **DeskCore-Symbol neben der Uhr** kommst du per Rechtsklick direkt an deine Kategorien.
Oder schneller: **Suchfeld** oben benutzen (Strg + F), den Namen eintippen und Doppelklick.
--- Die Suche findet Programme in allen Kategorien und auf dem Desktop.
## Start-Gruppen – mehrere Programme mit einem Klick Auch über das **DeskCore-Symbol neben der Uhr** kommst du per Rechtsklick direkt an deine Kategorien.
Du startest jeden Tag dieselben Programme, z. B. Browser, Mail und Teams? Pack sie in eine **Start-Gruppe**. **Meistgenutzt zuerst:** DeskCore merkt sich, was du wie oft startest. Über den **Ansicht-Knopf** oben rechts →
Ein Klick, und alle gehen nacheinander auf. **„Meistgenutzt zuerst“** stehen deine Lieblingsprogramme in jeder Kategorie ganz vorne.
Was du lange nicht mehr benutzt hast, rutscht mit der Zeit von selbst wieder nach hinten.
**Gruppe anlegen**
1. Links unten bei **„Start-Gruppen“** auf das **+** klicken ---
2. Namen eingeben, z. B. **Feierabend** oder **Arbeit**
3. Einstellen, wie viele Sekunden zwischen den Programmen liegen sollen (oder „gleichzeitig“) ## Schnellstart-Suche
**Programme hinzufügen** Programme starten, ohne DeskCore zu öffnen – ähnlich wie die Spotlight-Suche am Mac:
- Ein Icon mit der Maus **auf die Start-Gruppe ziehen**, oder
- Rechtsklick auf ein Icon → **„Zur Start-Gruppe hinzufügen“** 1. **Alt + Umschalt + Leertaste** drücken – egal, was gerade offen ist
2. Ein paar Buchstaben tippen
**Starten** 3. **Enter** – das Programm startet
- Auf das kleine **▶** neben der Gruppe klicken, oder
- Gruppe anklicken → **„Alle starten“**, oder Gefunden wird alles aus deinen **Kategorien**, vom **Desktop** und aus dem **Windows-Startmenü**,
- Rechtsklick auf das DeskCore-Symbol neben der Uhr → **„… starten“** dazu deine **Start-Gruppen**, **Kategorien** (öffnet DeskCore dort) und **Desktop-Seiten**.
Was du oft startest, steht weiter oben. Ohne Eingabe siehst du direkt deine meistgenutzten Programme.
**Noch bequemer:** Gruppe anklicken → **„Verknüpfung auf den Desktop“**.
Dann liegt ein Symbol auf dem Desktop, das die ganze Gruppe startet. Die Suche ist großzügig: **„vsc“** findet **V**isual **S**tudio **C**ode, **„fzla“** findet **F**ile**Z**i**l**l**a**.
Die Reihenfolge änderst du per Rechtsklick auf ein Programm → **„Früher starten“ / „Später starten“**. | Taste | Was passiert |
Ein Programm aus der Gruppe nehmen: Rechtsklick → **„Aus Start-Gruppe entfernen“**. Das Programm selbst bleibt dabei unangetastet. |---|---|
| **↑ / ↓** | Treffer auswählen |
--- | **Enter** | Starten |
| **Strg + Enter** | Speicherort im Explorer öffnen |
## Boxen auf dem Desktop | **Esc** | Schließen (oder einfach woanders hinklicken) |
Eine **Box** ist ein Kasten direkt auf deinem Desktop, in dem die Icons einer Kategorie liegen. Das Tastenkürzel änderst du unter **Einstellungen → Schnellstart-Suche** (ins Feld klicken, neue Kombination drücken).
So hast du deine Programme auf dem Desktop – aber ordentlich. Dort kannst du auch einstellen, ob das Windows-Startmenü mit durchsucht wird.
### Box anlegen ---
Es gibt drei Wege:
- Kategorie in DeskCore anklicken → oben **„Als Box auf den Desktop“** ## Start-Gruppen – mehrere Programme mit einem Klick
- Mit **gedrückter rechter Maustaste** auf einer freien Stelle des Desktops **einen Rahmen ziehen**. Danach wählst du, welche Kategorie hinein soll – oder legst gleich eine neue an.
(Ein ganz normaler Rechtsklick öffnet weiterhin das gewohnte Windows-Menü.) Du startest jeden Tag dieselben Programme, z. B. Browser, Mail und Teams? Pack sie in eine **Start-Gruppe**.
- **Einstellungen → Desktop-Boxen → Boxen auf dem Desktop anzeigen** schaltet alle Boxen auf einmal ein oder aus Ein Klick, und alle gehen nacheinander auf.
### Mit Boxen arbeiten **Gruppe anlegen**
- **Verschieben:** an der Titelleiste ziehen 1. Links unten bei **„Start-Gruppen“** auf das **+** klicken
- **Größe ändern:** am Rand ziehen 2. Namen eingeben, z. B. **Feierabend** oder **Arbeit**
- **Einklappen:** Doppelklick auf den Titel – noch ein Doppelklick klappt sie wieder auf. Fährst du mit der Maus über eine eingeklappte Box, geht sie kurz auf. 3. Einstellen, wie viele Sekunden zwischen den Programmen liegen sollen (oder „gleichzeitig“)
- **Programm starten:** Doppelklick auf das Icon
- **Rechtsklick auf ein Icon:** Öffnen, Umbenennen, Löschen, in eine andere Kategorie verschieben oder **„Zurück auf den Desktop legen“** **Programme hinzufügen**
- Icons lassen sich auch einfach in eine Box hinein- oder aus ihr herausziehen - Ein Icon mit der Maus **auf die Start-Gruppe ziehen**, oder
- Rechtsklick auf ein Icon → **„Zur Start-Gruppe hinzufügen“**
Das **Menü einer Box** (die drei Punkte ⋯ oben rechts) bietet:
**Starten**
| Menüpunkt | Was passiert | - Auf das kleine **▶** neben der Gruppe klicken, oder
|---|---| - Gruppe anklicken → **„Alle starten“**, oder
| **Tab hinzufügen** | Eine weitere Kategorie in dieselbe Box packen. Oben erscheinen dann Reiter zum Umschalten. | - Rechtsklick auf das DeskCore-Symbol neben der Uhr → **„… starten“**
| **Ordner-Portal …** | Einen Ordner als Reiter hinzufügen (siehe unten) |
| **Aussehen …** | Farbe, Deckkraft, Milchglas, Icon-Größe, Sortierung und Icon-Tönung der Box | **Noch bequemer:** Gruppe anklicken → **„Verknüpfung auf den Desktop“**.
| **Sperren** | Die Box lässt sich nicht mehr versehentlich verschieben oder in der Größe ändern | Dann liegt ein Symbol auf dem Desktop, das die ganze Gruppe startet.
| **Auf Seite verschieben** | Die Box auf eine andere [Desktop-Seite](#desktop-seiten) schicken |
| **Box schließen** | Die Box verschwindet vom Desktop. Die Kategorie in DeskCore bleibt natürlich erhalten. | Die Reihenfolge änderst du per Rechtsklick auf ein Programm → **„Früher starten“ / „Später starten“**.
Ein Programm aus der Gruppe nehmen: Rechtsklick → **„Aus Start-Gruppe entfernen“**. Das Programm selbst bleibt dabei unangetastet.
Rechtsklick auf einen **Reiter**: **„Als eigene Box lösen“** macht daraus wieder eine eigene Box, **„Tab entfernen“** nimmt ihn heraus.
---
### Ordner-Portal
Ein Portal ist eine Box, die **live einen beliebigen Ordner** zeigt, z. B. **Downloads** oder einen Projektordner. ## Boxen auf dem Desktop
Neue Dateien erscheinen sofort darin, und Unterordner öffnest du direkt in der Box.
Anlegen: **Einstellungen → Desktop-Boxen → Ordner-Portal → „Ordner wählen …“** oder beim Aufziehen einer Box **„Ordner-Portal …“** wählen. Eine **Box** ist ein Kasten direkt auf deinem Desktop, in dem die Icons einer Kategorie liegen.
So hast du deine Programme auf dem Desktop – aber ordentlich.
### Peek – Boxen nach vorne holen
Alle Fenster sind offen und verdecken den Desktop? Drück **Win + Umschalt + B**. ### Box anlegen
Die Boxen kommen nach vorne, über alle Fenster. Nochmal drücken, **Esc** oder woanders hinklicken schickt sie zurück. Es gibt drei Wege:
Ein- und ausschalten unter **Einstellungen → Peek**. - Kategorie in DeskCore anklicken → oben **„Als Box auf den Desktop“**
- Mit **gedrückter rechter Maustaste** auf einer freien Stelle des Desktops **einen Rahmen ziehen**. Danach wählst du, welche Kategorie hinein soll – oder legst gleich eine neue an.
### Anordnungen speichern (Snapshots) (Ein ganz normaler Rechtsklick öffnet weiterhin das gewohnte Windows-Menü.)
Du hast deine Boxen perfekt angeordnet? Speicher dir das: - **Einstellungen → Desktop-Boxen → Boxen auf dem Desktop anzeigen** schaltet alle Boxen auf einmal ein oder aus
**Einstellungen → Snapshots → „Aktuelle Anordnung speichern“**.
Mit **„Wiederherstellen“** ist später alles wieder genau so – auch über das DeskCore-Symbol neben der Uhr. ### Mit Boxen arbeiten
- **Verschieben:** an der Titelleiste ziehen
Arbeitest du mal am Laptop allein und mal mit Monitor? DeskCore merkt sich die Positionen der Boxen **für jede Bildschirm-Anordnung automatisch**. - **Größe ändern:** am Rand ziehen
- **Einklappen:** Doppelklick auf den Titel – noch ein Doppelklick klappt sie wieder auf. Fährst du mit der Maus über eine eingeklappte Box, geht sie kurz auf.
--- - **Programm starten:** Doppelklick auf das Icon
- **Rechtsklick auf ein Icon:** Öffnen, Umbenennen, Löschen, in eine andere Kategorie verschieben oder **„Zurück auf den Desktop legen“**
## Desktop-Seiten - Icons lassen sich auch einfach in eine Box hinein- oder aus ihr herausziehen
- **Eigene Reihenfolge:** Zieh ein Icon innerhalb der Box an die gewünschte Stelle – ein blauer Strich zeigt, wo es landet.
Du kannst **mehrere Desktop-Seiten** anlegen, z. B. **Arbeit** und **Freizeit** – jede Seite hat ihre eigenen Boxen. Die Box merkt sich die Reihenfolge (Sortierung „Eigene“).
So siehst du nach Feierabend nur deine Spiele und tagsüber nur deine Arbeitsprogramme.
Das **Menü einer Box** (die drei Punkte ⋯ oben rechts) bietet:
**Seiten anlegen:** **Einstellungen → Desktop-Seiten → „Seite hinzufügen“**.
Dort kannst du Seiten auch umbenennen (Stift) oder löschen (Mülleimer). Die Boxen einer gelöschten Seite wandern auf die Seite davor, es geht also nichts verloren. | Menüpunkt | Was passiert |
|---|---|
**Zwischen Seiten wechseln:** | **Tab hinzufügen** | Eine weitere Kategorie, ein Ordner-Portal oder **„Zuletzt benutzt“** in dieselbe Box packen. Oben erscheinen dann Reiter zum Umschalten. |
- **Win + Alt + ←** oder **→** | **Ordner-Portal …** | Einen Ordner als Reiter hinzufügen (siehe unten) |
- **Mausrad** drehen, während die Maus über einer freien Stelle des Desktops ist | **Aussehen …** | Farbe, Deckkraft, Milchglas, Icon-Größe, Icon-Tönung und Sortierung der Box: nach Name, Typ, Datum, **Meistgenutzt** oder **Eigene** Reihenfolge |
- Rechtsklick auf das DeskCore-Symbol neben der Uhr → **„Desktop-Seite“** | **Sperren** | Die Box lässt sich nicht mehr versehentlich verschieben oder in der Größe ändern |
- in den Einstellungen bei der Seite auf **„Anzeigen“** | **Auf Seite verschieben** | Die Box auf eine andere [Desktop-Seite](#desktop-seiten) schicken |
| **Auf Seite kopieren** | Die gleiche Box zusätzlich auf eine andere Seite legen – z. B. „Internet“ auf „Arbeit“ **und** „Freizeit“ |
Beim Wechseln zeigt DeskCore kurz unten am Bildschirm, auf welcher Seite du bist. | **Box schließen** | Die Box verschwindet vom Desktop. Die Kategorie in DeskCore bleibt natürlich erhalten. |
Neue Boxen landen immer auf der Seite, die gerade angezeigt wird. Eine Box auf eine andere Seite schicken: Box-Menü ⋯ → **„Auf Seite verschieben“**.
Rechtsklick auf einen **Reiter**: **„Als eigene Box lösen“** macht daraus wieder eine eigene Box, **„Tab entfernen“** nimmt ihn heraus.
> Die Icons, die direkt auf dem Desktop liegen (nicht in einer Box), siehst du auf jeder Seite.
### Box „Zuletzt benutzt“
--- Eine Box, die sich von selbst füllt: Sie zeigt, was du zuletzt über DeskCore gestartet hast – neueste zuerst.
Anlegen: beim **Aufziehen einer Box** „Zuletzt benutzt“ wählen, oder im Box-Menü ⋯ → **Tab hinzufügen → Zuletzt benutzt**.
## Den Desktop schick machen Mitgezählt wird alles, was du über DeskCore startest: aus dem Fenster, aus Boxen, über die Schnellstart-Suche und über Start-Gruppen.
### Desktop-Icons einfärben ### Ordner-Portal
**Einstellungen → Desktop-Icons tönen:** eine Farbe anklicken, und alle Icons auf dem Desktop bekommen denselben Farbton. Ein Portal ist eine Box, die **live einen beliebigen Ordner** zeigt, z. B. **Downloads** oder einen Projektordner.
Mit dem Regler **„Stärke“** stellst du ein, wie kräftig. Der erste, leere Kreis schaltet die Tönung wieder aus. Neue Dateien erscheinen sofort darin, und Unterordner öffnest du direkt in der Box.
Die Icons in den Boxen färbst du getrennt über das Box-Menü ⋯ → **„Aussehen …“**. Anlegen: **Einstellungen → Desktop-Boxen → Ordner-Portal → „Ordner wählen …“** oder beim Aufziehen einer Box **„Ordner-Portal …“** wählen.
### Schnell verstecken ### Peek – Boxen nach vorne holen
**Doppelklick auf eine freie Stelle des Desktops** blendet alle Icons und Boxen aus – perfekt für ein sauberes Hintergrundbild oder eine Bildschirmfreigabe. Alle Fenster sind offen und verdecken den Desktop? Drück **Win + Umschalt + B**.
Noch ein Doppelklick holt alles zurück. Einschalten unter **Einstellungen → Schnell verstecken**. Die Boxen kommen nach vorne, über alle Fenster. Nochmal drücken, **Esc** oder woanders hinklicken schickt sie zurück.
Ein- und ausschalten unter **Einstellungen → Peek**.
### Chamäleon
Die Boxen werden fast unsichtbar und erscheinen erst, wenn du mit der Maus darüberfährst. ### Anordnungen speichern (Snapshots)
Einschalten unter **Einstellungen → Chamäleon**. Du hast deine Boxen perfekt angeordnet? Speicher dir das:
**Einstellungen → Snapshots → „Aktuelle Anordnung speichern“**.
### Hell oder Dunkel Mit **„Wiederherstellen“** ist später alles wieder genau so – auch über das DeskCore-Symbol neben der Uhr.
Oben rechts auf die **Sonne bzw. den Mond** klicken und auswählen – oder **„Wie Windows“**, dann passt sich DeskCore automatisch an.
Arbeitest du mal am Laptop allein und mal mit Monitor? DeskCore merkt sich die Positionen der Boxen **für jede Bildschirm-Anordnung automatisch**.
### Ansicht
Oben rechts gibt es den **Ansicht-Knopf**: kleine, mittlere oder große Symbole oder eine Liste. ---
--- ## Desktop-Seiten
## Automatisch sortieren Du kannst **mehrere Desktop-Seiten** anlegen, z. B. **Arbeit** und **Freizeit** – jede Seite hat ihre eigenen Boxen.
So siehst du nach Feierabend nur deine Spiele und tagsüber nur deine Arbeitsprogramme.
DeskCore kann neue Icons selbst einsortieren. Sobald z. B. ein Installer eine Verknüpfung auf den Desktop legt, wandert sie in die richtige Kategorie.
**Seiten anlegen:** **Einstellungen → Desktop-Seiten → „Seite hinzufügen“**.
So geht's: **Einstellungen → Automatisch sortieren → „Regel hinzufügen“**. Es gibt zwei Arten von Regeln Dort kannst du Seiten auch umbenennen (Stift) oder löschen (Mülleimer). Die Boxen einer gelöschten Seite wandern auf die Seite davor, es geht also nichts verloren.
(zum Umschalten auf die Art der Regel klicken):
**Zwischen Seiten wechseln:**
| Regel | Beispiel | - **Win + Alt + ←** oder **→**
|---|---| - **Mausrad** drehen, während die Maus über einer freien Stelle des Desktops ist
| **Name enthält** | alles mit „Steam“ im Namen → **Spiele** | - Rechtsklick auf das DeskCore-Symbol neben der Uhr → **„Desktop-Seite“**
| **Dateityp ist** | Bilder, Videos, Musik, Dokumente, Archive, Programme, Ordner – oder Endungen wie „pdf, docx“ → **Dokumente** | - in den Einstellungen bei der Seite auf **„Anzeigen“**
Die erste passende Regel gewinnt. Groß- und Kleinschreibung ist egal. Beim Wechseln zeigt DeskCore kurz unten am Bildschirm, auf welcher Seite du bist.
Mit **„Danach vom Desktop entfernen“** verschwindet das Original gleich vom Desktop. Neue Boxen landen immer auf der Seite, die gerade angezeigt wird. Eine Box auf eine andere Seite schicken: Box-Menü ⋯ → **„Auf Seite verschieben“**.
**„Jetzt auf den Desktop anwenden“** sortiert alles ein, was schon auf dem Desktop liegt.
**Eine Kategorie auf mehreren Seiten:** Jede Seite darf ihre eigene Box mit derselben Kategorie haben – z. B. „Internet“ auf „Arbeit“ und auf „Freizeit“.
--- Am schnellsten geht das über das Box-Menü ⋯ → **„Auf Seite kopieren“**. Es sind dieselben Icons: Was du in der einen Box änderst, siehst du auch in der anderen.
Nur **auf derselben Seite** steht eine Kategorie in höchstens einer Box.
## Weitere Extras
> Die Icons, die direkt auf dem Desktop liegen (nicht in einer Box), siehst du auf jeder Seite.
### DeskCore schnell öffnen
- **Win + Umschalt + D** drücken, von überall aus (änderbar unter **Einstellungen → Tastenkürzel**) ### Seiten automatisch wechseln (Zeitplan)
- oder unten rechts neben der Uhr auf das **DeskCore-Symbol** klicken Klick in den Einstellungen bei einer Seite auf die **Uhr** 🕒:
Wenn du das Fenster mit dem **X** schließt, läuft DeskCore unten rechts weiter – deine Boxen bleiben also da. - **„Diese Seite automatisch anzeigen“** – Tage und Uhrzeit wählen, z. B. **Mo–Fr, 08:00 bis 17:00**
Richtig beenden: **Rechtsklick auf das Symbol neben der Uhr → Beenden** - **„Zu allen anderen Zeiten diese Seite anzeigen“** – für die Seite, die sonst gelten soll
### Das Symbol neben der Uhr Beispiel: „Arbeit“ bekommt Mo–Fr 08:00–17:00, bei „Freizeit“ setzt du den Haken für „alle anderen Zeiten“.
Ein Rechtsklick darauf bietet dir alles Wichtige, ohne das Fenster zu öffnen: Morgens um acht wechselt DeskCore dann von selbst auf „Arbeit“, um fünf zurück auf „Freizeit“.
deine Kategorien, Start-Gruppen, gespeicherte Anordnungen, Desktop-Seiten und **„Desktop ausblenden“**. Wechselst du zwischendurch von Hand, bleibt deine Wahl bis zum nächsten geplanten Wechsel stehen.
Zeiträume über Mitternacht (z. B. 22:00 bis 06:00) gehen auch.
### Mit Windows starten
**Einstellungen → Mit Windows starten.** DeskCore startet dann unauffällig neben der Uhr, und Boxen sowie Auto-Sortierung sind sofort da. ### Eigenes Hintergrundbild je Seite
Im selben Fenster (**Uhr** 🕒) unter **„Hintergrundbild“** → **„Bild wählen …“**.
### Deinstallierte Programme verschwinden von selbst Beim Wechsel auf die Seite stellt DeskCore das Bild ein. Wechselst du auf eine Seite ohne eigenes Bild,
Deinstallierst du ein Programm, entfernt DeskCore dessen Icon nach etwa einer Minute auch aus deinen Kategorien – auch bei **Steam- und Epic-Spielen**. kommt dein ursprünglicher Hintergrund zurück.
Strg + Z holt es zurück. Ausschalten unter **Einstellungen → Deinstallierte Programme entfernen**.
---
### Sicherung
**Einstellungen → Sicherung → „Sichern …“** speichert alle Kategorien, Boxen, Start-Gruppen und Einstellungen in eine ZIP-Datei. ## Den Desktop schick machen
Mit **„Wiederherstellen …“** holst du alles zurück, z. B. auf einem neuen PC.
### Desktop-Icons einfärben
### Updates **Einstellungen → Desktop-Icons tönen:** eine Farbe anklicken, und alle Icons auf dem Desktop bekommen denselben Farbton.
DeskCore schaut einmal am Tag nach, ob es eine neue Version gibt. Mit dem Regler **„Stärke“** stellst du ein, wie kräftig. Der erste, leere Kreis schaltet die Tönung wieder aus.
Wenn ja, erscheint oben ein Hinweis. Ein Klick auf **„Jetzt aktualisieren“** genügt, und DeskCore startet danach von selbst neu. Die Icons in den Boxen färbst du getrennt über das Box-Menü ⋯ → **„Aussehen …“**.
Prüfen von Hand: **Einstellungen → Updates → „Jetzt prüfen“**.
### Schnell verstecken
--- **Doppelklick auf eine freie Stelle des Desktops** blendet alle Icons und Boxen aus – perfekt für ein sauberes Hintergrundbild oder eine Bildschirmfreigabe.
Noch ein Doppelklick holt alles zurück. Einschalten unter **Einstellungen → Schnell verstecken**.
## Alle Tastenkürzel
### Chamäleon
**Überall in Windows** Die Boxen werden fast unsichtbar und erscheinen erst, wenn du mit der Maus darüberfährst.
Einschalten unter **Einstellungen → Chamäleon**.
| Taste | Was passiert |
|---|---| ### Hell oder Dunkel
| **Win + Umschalt + D** | DeskCore öffnen / verstecken | Oben rechts auf die **Sonne bzw. den Mond** klicken und auswählen – oder **„Wie Windows“**, dann passt sich DeskCore automatisch an.
| **Win + Umschalt + B** | Peek: Boxen nach vorne holen |
| **Win + Alt + ← / →** | Desktop-Seite wechseln | ### Ansicht
Oben rechts gibt es den **Ansicht-Knopf**: kleine, mittlere oder große Symbole oder eine Liste.
**Im DeskCore-Fenster**
---
| Taste | Was passiert |
|---|---| ## Automatisch sortieren
| **Strg + F** | Suchen |
| **Esc** | Suche leeren | DeskCore kann neue Icons selbst einsortieren. Sobald z. B. ein Installer eine Verknüpfung auf den Desktop legt, wandert sie in die richtige Kategorie.
| **Strg + A** | Alles markieren |
| **Enter** | Öffnen | So geht's: **Einstellungen → Automatisch sortieren → „Regel hinzufügen“**. Es gibt zwei Arten von Regeln
| **F2** | Umbenennen | (zum Umschalten auf die Art der Regel klicken):
| **Entf** | Löschen |
| **Strg + Z** | Rückgängig | | Regel | Beispiel |
| **Strg + N** | Neue Kategorie | |---|---|
| **Alt + ↑ / ↓** | Kategorie nach oben / unten verschieben | | **Name enthält** | alles mit „Steam“ im Namen → **Spiele** |
| **Strg + 1 … 9** | Zur Kategorie 1 … 9 springen | | **Dateityp ist** | Bilder, Videos, Musik, Dokumente, Archive, Programme, Ordner – oder Endungen wie „pdf, docx“ → **Dokumente** |
| **F5** | Aktualisieren |
Die erste passende Regel gewinnt. Groß- und Kleinschreibung ist egal.
--- Mit **„Danach vom Desktop entfernen“** verschwindet das Original gleich vom Desktop.
**„Jetzt auf den Desktop anwenden“** sortiert alles ein, was schon auf dem Desktop liegt.
## Hoppla, falsch geklickt?
---
**Strg + Z** macht den letzten Schritt rückgängig – auch mehrmals hintereinander.
Unten im Fenster erscheint nach vielen Aktionen auch kurz ein Knopf **„Rückgängig“**. ## Weitere Extras
--- ### DeskCore schnell öffnen
- **Win + Umschalt + D** drücken, von überall aus (änderbar unter **Einstellungen → Tastenkürzel**)
## Häufige Fragen - oder unten rechts neben der Uhr auf das **DeskCore-Symbol** klicken
**Wo liegen meine Icons eigentlich?** Wenn du das Fenster mit dem **X** schließt, läuft DeskCore unten rechts weiter – deine Boxen bleiben also da.
Im Ordner `C:\Benutzer\DEIN-NAME\DeskCore`. Jede Kategorie ist dort ein ganz normaler Ordner. Richtig beenden: **Rechtsklick auf das Symbol neben der Uhr → Beenden**
Auch wenn DeskCore mal nicht läuft, ist also nichts weg.
### Das Symbol neben der Uhr
**Ich habe etwas gelöscht. Ist es für immer weg?** Ein Rechtsklick darauf bietet dir alles Wichtige, ohne das Fenster zu öffnen:
Nein. Erst geht **Strg + Z**, und danach liegt es noch im **Papierkorb** von Windows. deine Kategorien, Start-Gruppen, gespeicherte Anordnungen, Desktop-Seiten und **„Desktop ausblenden“**.
**Ich habe ein Programm deinstalliert – verschwindet es auch aus DeskCore?** ### Mit Windows starten
Ja, automatisch nach etwa einer Minute – auch bei **Steam- und Epic-Spielen**. Hast du es dir anders überlegt: **Strg + Z** holt es zurück. **Einstellungen → Mit Windows starten.** DeskCore startet dann unauffällig neben der Uhr, und Boxen sowie Auto-Sortierung sind sofort da.
Programme auf einem gerade abgesteckten USB- oder Netzlaufwerk bleiben natürlich erhalten.
### Deinstallierte Programme verschwinden von selbst
**Mein Rechtsklick auf den Desktop fühlt sich anders an.** Deinstallierst du ein Programm, entfernt DeskCore dessen Icon nach etwa einer Minute auch aus deinen Kategorien – auch bei **Steam- und Epic-Spielen**.
Damit du Boxen aufziehen kannst, schaut DeskCore kurz, ob du ziehst oder nur klickst. Das Windows-Menü erscheint deshalb erst beim **Loslassen** der Taste. Strg + Z holt es zurück. Ausschalten unter **Einstellungen → Deinstallierte Programme entfernen**.
Stört dich das: **Einstellungen → Box auf dem Desktop aufziehen** ausschalten.
### Sicherung
**Das Mausrad wechselt ungewollt die Desktop-Seite.** **Einstellungen → Sicherung → „Sichern …“** speichert alle Kategorien, Boxen, Start-Gruppen und Einstellungen in eine ZIP-Datei.
Unter **Einstellungen → Desktop-Seiten** den Haken bei **„Mit dem Mausrad … wechseln“** entfernen. Mit **„Wiederherstellen …“** holst du alles zurück, z. B. auf einem neuen PC.
Mit gedrückter **Strg**-Taste ändert das Mausrad wie gewohnt die Icon-Größe des Desktops.
### Updates
**Die Farbe der Icon-Tönung sitzt nicht genau auf den Icons.** DeskCore schaut einmal am Tag nach, ob es eine neue Version gibt.
Kurz warten – DeskCore liest die Positionen alle zwei Sekunden neu. Hast du gerade die Bildschirmskalierung geändert, hilft ein Neustart von DeskCore. Wenn ja, erscheint oben ein Hinweis. Ein Klick auf **„Jetzt aktualisieren“** genügt, und DeskCore startet danach von selbst neu.
Prüfen von Hand: **Einstellungen → Updates → „Jetzt prüfen“**.
**Ein Icon lässt sich nicht verschieben („Keine Berechtigung“).**
Manche Icons gehören allen Benutzern des PCs. Dafür DeskCore einmal mit ---
**Rechtsklick → „Als Administrator ausführen“** starten.
## Alle Tastenkürzel
**Ich habe einen großen Ordner hineingezogen. Ist der jetzt doppelt da?**
DeskCore fragt bei großen Ordnern nach. Wählst du **„Verschieben“**, liegt er nur einmal da und belegt keinen doppelten Platz. **Überall in Windows**
Beim Kopieren siehst du einen Fortschrittsbalken und kannst jederzeit **abbrechen**. Dann bleibt alles wie vorher.
| Taste | Was passiert |
**Ich habe einen Bildschirm abgesteckt und das Fenster ist weg.** |---|---|
Keine Sorge: DeskCore und die Desktop-Boxen holen sich automatisch auf den verbliebenen Bildschirm zurück. | **Alt + Umschalt + Leertaste** | Schnellstart-Suche |
Steckst du den Bildschirm wieder an, kommen die Boxen an ihre alten Plätze. | **Win + Umschalt + D** | DeskCore öffnen / verstecken |
| **Win + Umschalt + B** | Peek: Boxen nach vorne holen |
**Ich bekomme einen neuen PC. Was jetzt?** | **Win + Alt + ← / →** | Desktop-Seite wechseln |
**Einstellungen → Sicherung → „Sichern …“** speichert alles in eine ZIP-Datei.
Auf dem neuen PC installierst du DeskCore und wählst **„Wiederherstellen …“**. **Im DeskCore-Fenster**
**Ein Tastenkürzel funktioniert nicht.** | Taste | Was passiert |
Ein anderes Programm benutzt es vielleicht schon. Unter **Einstellungen → Tastenkürzel** ins Feld klicken und eine andere Kombination drücken. |---|---|
| **Strg + F** | Suchen |
**Kann ich DeskCore auch per Verknüpfung oder Skript steuern?** | **Esc** | Suche leeren |
Ja, für Fortgeschrittene: | **Strg + A** | Alles markieren |
| **Enter** | Öffnen |
| Befehl | Was passiert | | **F2** | Umbenennen |
|---|---| | **Entf** | Löschen |
| `DeskCore.exe --launch "Feierabend"` | Start-Gruppe „Feierabend“ starten | | **Strg + Z** | Rückgängig |
| `DeskCore.exe --snapshot "Arbeit"` | Gespeicherte Anordnung „Arbeit“ wiederherstellen | | **Strg + N** | Neue Kategorie |
| `DeskCore.exe --page "Freizeit"` | Zur Desktop-Seite „Freizeit“ wechseln (oder `--page 2`) | | **Alt + ↑ / ↓** | Kategorie nach oben / unten verschieben |
| `DeskCore.exe --toggle-desktop` | Desktop aus- / einblenden | | **Strg + 1 … 9** | Zur Kategorie 1 … 9 springen |
| `DeskCore.exe --peek` | Boxen nach vorne holen | | **F5** | Aktualisieren |
--- ---
## Deinstallieren ## Hoppla, falsch geklickt?
**Windows-Einstellungen → Apps → DeskCore → Deinstallieren** **Strg + Z** macht den letzten Schritt rückgängig – auch mehrmals hintereinander.
Unten im Fenster erscheint nach vielen Aktionen auch kurz ein Knopf **„Rückgängig“**.
Deine Kategorien bleiben dabei **standardmäßig erhalten**.
Willst du sie auch entfernen, setz beim Deinstallieren den Haken. Dann landen sie im Papierkorb. ---
--- ## Häufige Fragen
## Lizenz **Wo liegen meine Icons eigentlich?**
Im Ordner `C:\Benutzer\DEIN-NAME\DeskCore`. Jede Kategorie ist dort ein ganz normaler Ordner.
DeskCore ist nur für den **privaten, nicht-kommerziellen** Gebrauch. Auch wenn DeskCore mal nicht läuft, ist also nichts weg.
Weitergeben, Verändern und Verkaufen ist nicht erlaubt.
Den vollständigen Text findest du in der Datei `LICENSE.txt` oder in DeskCore unter **Einstellungen → Info → Lizenz**. **Ich habe etwas gelöscht. Ist es für immer weg?**
Nein. Erst geht **Strg + Z**, und danach liegt es noch im **Papierkorb** von Windows.
© M_Viper · [m-viper.de](https://m-viper.de)
**Ich habe ein Programm deinstalliert – verschwindet es auch aus DeskCore?**
Ja, automatisch nach etwa einer Minute – auch bei **Steam- und Epic-Spielen**. Hast du es dir anders überlegt: **Strg + Z** holt es zurück.
Programme auf einem gerade abgesteckten USB- oder Netzlaufwerk bleiben natürlich erhalten.
**Mein Rechtsklick auf den Desktop fühlt sich anders an.**
Damit du Boxen aufziehen kannst, schaut DeskCore kurz, ob du ziehst oder nur klickst. Das Windows-Menü erscheint deshalb erst beim **Loslassen** der Taste.
Stört dich das: **Einstellungen → Box auf dem Desktop aufziehen** ausschalten.
**Das Mausrad wechselt ungewollt die Desktop-Seite.**
Unter **Einstellungen → Desktop-Seiten** den Haken bei **„Mit dem Mausrad … wechseln“** entfernen.
Mit gedrückter **Strg**-Taste ändert das Mausrad wie gewohnt die Icon-Größe des Desktops.
**Die Farbe der Icon-Tönung sitzt nicht genau auf den Icons.**
Kurz warten – DeskCore liest die Positionen alle zwei Sekunden neu. Hast du gerade die Bildschirmskalierung geändert, hilft ein Neustart von DeskCore.
**Ein Icon lässt sich nicht verschieben („Keine Berechtigung“).**
Manche Icons gehören allen Benutzern des PCs. Dafür DeskCore einmal mit
**Rechtsklick → „Als Administrator ausführen“** starten.
**Ich habe einen großen Ordner hineingezogen. Ist der jetzt doppelt da?**
DeskCore fragt bei großen Ordnern nach. Wählst du **„Verschieben“**, liegt er nur einmal da und belegt keinen doppelten Platz.
Beim Kopieren siehst du einen Fortschrittsbalken und kannst jederzeit **abbrechen**. Dann bleibt alles wie vorher.
**Ich habe einen Bildschirm abgesteckt und das Fenster ist weg.**
Keine Sorge: DeskCore und die Desktop-Boxen holen sich automatisch auf den verbliebenen Bildschirm zurück.
Steckst du den Bildschirm wieder an, kommen die Boxen an ihre alten Plätze.
**Ich bekomme einen neuen PC. Was jetzt?**
**Einstellungen → Sicherung → „Sichern …“** speichert alles in eine ZIP-Datei.
Auf dem neuen PC installierst du DeskCore und wählst **„Wiederherstellen …“**.
**Merkt sich DeskCore, welche Programme ich starte? Geht das irgendwohin?**
Nur auf deinem PC: in der Datei `usage.json` im DeskCore-Einstellungsordner. Gezählt wird nur, was du **über DeskCore** startest.
Nichts davon verlässt deinen Rechner.
**Ein Tastenkürzel funktioniert nicht.**
Ein anderes Programm benutzt es vielleicht schon. Unter **Einstellungen → Tastenkürzel** ins Feld klicken und eine andere Kombination drücken.
**Kann ich DeskCore auch per Verknüpfung oder Skript steuern?**
Ja, für Fortgeschrittene:
| Befehl | Was passiert |
|---|---|
| `DeskCore.exe --launch "Feierabend"` | Start-Gruppe „Feierabend“ starten |
| `DeskCore.exe --snapshot "Arbeit"` | Gespeicherte Anordnung „Arbeit“ wiederherstellen |
| `DeskCore.exe --page "Freizeit"` | Zur Desktop-Seite „Freizeit“ wechseln (oder `--page 2`) |
| `DeskCore.exe --toggle-desktop` | Desktop aus- / einblenden |
| `DeskCore.exe --peek` | Boxen nach vorne holen |
| `DeskCore.exe --search` | Schnellstart-Suche öffnen |
---
## Deinstallieren
**Windows-Einstellungen → Apps → DeskCore → Deinstallieren**
Deine Kategorien bleiben dabei **standardmäßig erhalten**.
Willst du sie auch entfernen, setz beim Deinstallieren den Haken. Dann landen sie im Papierkorb.
---
## Lizenz
DeskCore ist nur für den **privaten, nicht-kommerziellen** Gebrauch.
Weitergeben, Verändern und Verkaufen ist nicht erlaubt.
Den vollständigen Text findest du in der Datei `LICENSE.txt` oder in DeskCore unter **Einstellungen → Info → Lizenz**.
© M_Viper · [m-viper.de](https://m-viper.de)
+9
View File
@@ -26,6 +26,15 @@ public static class Screens
public static Rect PrimaryWorkArea => SystemParameters.WorkArea; public static Rect PrimaryWorkArea => SystemParameters.WorkArea;
/// <summary>Arbeitsbereich des Bildschirms, auf dem der Punkt liegt (sonst der nächstgelegene).</summary>
public static Rect WorkAreaAt(Point p)
{
var areas = WorkAreas();
return areas.FirstOrDefault(a => a.Contains(p), areas.OrderBy(a =>
Math.Pow(Math.Clamp(p.X, a.Left, a.Right) - p.X, 2) + Math.Pow(Math.Clamp(p.Y, a.Top, a.Bottom) - p.Y, 2))
.FirstOrDefault(PrimaryWorkArea));
}
/// <summary>Ist genug vom Fenster (Titelleiste) auf irgendeinem Bildschirm sichtbar, um es greifen zu können?</summary> /// <summary>Ist genug vom Fenster (Titelleiste) auf irgendeinem Bildschirm sichtbar, um es greifen zu können?</summary>
public static bool IsReachable(Rect window) public static bool IsReachable(Rect window)
{ {
+91 -10
View File
@@ -40,15 +40,45 @@ public class SortRule
[JsonIgnore] public string KindText => Kind == "Typ" ? "Dateityp ist" : "Name enthält"; [JsonIgnore] public string KindText => Kind == "Typ" ? "Dateityp ist" : "Name enthält";
} }
/// <summary>Ein Tab einer Desktop-Box: eine Kategorie oder ein beliebiger Ordner ("Ordner-Portal").</summary> /// <summary>Ein Tab einer Desktop-Box: eine Kategorie, ein beliebiger Ordner ("Ordner-Portal") oder "Zuletzt benutzt".</summary>
public class FenceTab public class FenceTab
{ {
public const string CategoryKind = "Kategorie", FolderKind = "Ordner"; public const string CategoryKind = "Kategorie", FolderKind = "Ordner", RecentKind = "Zuletzt";
public string Kind { get; set; } = CategoryKind; public string Kind { get; set; } = CategoryKind;
/// <summary>Kategoriename bzw. Ordnerpfad.</summary> /// <summary>Kategoriename bzw. Ordnerpfad (bei "Zuletzt benutzt" leer).</summary>
public string Value { get; set; } = ""; public string Value { get; set; } = "";
/// <summary>Eigene Reihenfolge (Dateinamen), wenn die Box nach "Eigene" sortiert ist.</summary>
public List<string> Order { get; set; } = [];
[JsonIgnore] public bool IsFolder => Kind == FolderKind; [JsonIgnore] public bool IsFolder => Kind == FolderKind;
[JsonIgnore] public string Title => IsFolder ? Path.GetFileName(Value.TrimEnd('\\')) is { Length: > 0 } n ? n : Value : Value; [JsonIgnore] public bool IsRecent => Kind == RecentKind;
[JsonIgnore] public bool IsCategory => Kind == CategoryKind;
[JsonIgnore] public string Title => IsRecent ? "Zuletzt benutzt"
: IsFolder ? Path.GetFileName(Value.TrimEnd('\\')) is { Length: > 0 } n ? n : Value : Value;
}
/// <summary>Zusatzeinstellungen einer Desktop-Seite: Zeitplan und Hintergrundbild.</summary>
public class PageOption
{
/// <summary>Seite automatisch zu bestimmten Zeiten anzeigen.</summary>
public bool Scheduled { get; set; }
/// <summary>Wochentage als Bits: Montag = 1, Dienstag = 2 … Sonntag = 64.</summary>
public int Days { get; set; } = 0b0011111; // Mo–Fr
public string From { get; set; } = "08:00";
public string To { get; set; } = "17:00";
/// <summary>Eigenes Hintergrundbild für diese Seite (null = nicht ändern).</summary>
public string? Wallpaper { get; set; }
public static int DayBit(DayOfWeek d) => 1 << (((int)d + 6) % 7); // Montag = Bit 0
/// <summary>Gilt der Zeitplan zu diesem Zeitpunkt? Zeiträume über Mitternacht (z. B. 22:00–06:00) gehen auch.</summary>
public bool Matches(DateTime now)
{
if (!Scheduled || !TimeSpan.TryParse(From, out var from) || !TimeSpan.TryParse(To, out var to)) return false;
var t = now.TimeOfDay;
if (from <= to) return (Days & DayBit(now.DayOfWeek)) != 0 && t >= from && t < to;
// über Mitternacht: der Tag zählt, an dem der Zeitraum beginnt
return t >= from ? (Days & DayBit(now.DayOfWeek)) != 0 : t < to && (Days & DayBit(now.AddDays(-1).DayOfWeek)) != 0;
}
} }
/// <summary>Eine Desktop-Box mit Position, Tabs und Aussehen.</summary> /// <summary>Eine Desktop-Box mit Position, Tabs und Aussehen.</summary>
@@ -71,7 +101,7 @@ public class FenceDef
/// <summary>Milchglas-Effekt (Hintergrund verschwommen).</summary> /// <summary>Milchglas-Effekt (Hintergrund verschwommen).</summary>
public bool Blur { get; set; } public bool Blur { get; set; }
public ViewMode IconSize { get; set; } = ViewMode.Medium; public ViewMode IconSize { get; set; } = ViewMode.Medium;
/// <summary>"Name", "Typ" oder "Datum"</summary> /// <summary>"Name", "Typ", "Datum", "Häufig" (meistgenutzt zuerst) oder "Eigene" (per Ziehen angeordnet)</summary>
public string Sort { get; set; } = "Name"; public string Sort { get; set; } = "Name";
/// <summary>Icon-Tönung (#RRGGBB) oder null.</summary> /// <summary>Icon-Tönung (#RRGGBB) oder null.</summary>
public string? Tint { get; set; } public string? Tint { get; set; }
@@ -128,6 +158,22 @@ public class AppSettings
public int CurrentPage { get; set; } public int CurrentPage { get; set; }
/// <summary>Mausrad über einer freien Desktop-Stelle wechselt die Seite.</summary> /// <summary>Mausrad über einer freien Desktop-Stelle wechselt die Seite.</summary>
public bool PageWheel { get; set; } = true; public bool PageWheel { get; set; } = true;
/// <summary>Zeitplan und Hintergrundbild je Seite (Schlüssel = Seitenname).</summary>
public Dictionary<string, PageOption> PageOptions { get; set; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Seite, die außerhalb aller Zeitpläne angezeigt wird (null = keine).</summary>
public string? PageFallback { get; set; }
/// <summary>Hintergrundbild, das vor dem ersten Seiten-Hintergrund eingestellt war (zum Zurückholen).</summary>
public string? SavedWallpaper { get; set; }
/// <summary>Schnellstart-Suche per Tastenkürzel.</summary>
public bool QuickSearchEnabled { get; set; } = true;
public int QuickSearchModifiers { get; set; } = Hotkey.ModAlt | Hotkey.ModShift;
public int QuickSearchKey { get; set; } = 0x20; // Leertaste
/// <summary>Programme aus dem Windows-Startmenü mit durchsuchen.</summary>
public bool QuickSearchStartMenu { get; set; } = true;
/// <summary>Sortierung im Hauptfenster: "Name" oder "Häufig".</summary>
public string MainSort { get; set; } = "Name";
/// <summary>Rechte Maustaste auf freier Desktop-Fläche ziehen → neue Box.</summary> /// <summary>Rechte Maustaste auf freier Desktop-Fläche ziehen → neue Box.</summary>
public bool DrawFences { get; set; } = true; public bool DrawFences { get; set; } = true;
@@ -191,6 +237,8 @@ public class AppSettings
s.Fences ??= []; s.Fences ??= [];
s.Snapshots ??= []; s.Snapshots ??= [];
s.ScreenLayouts ??= []; s.ScreenLayouts ??= [];
s.PageOptions = new(s.PageOptions ?? [], StringComparer.OrdinalIgnoreCase);
foreach (var t in s.Fences.SelectMany(f => f.Tabs)) t.Order ??= [];
if (s.Pages is null || s.Pages.Count == 0) s.Pages = ["Seite 1"]; if (s.Pages is null || s.Pages.Count == 0) s.Pages = ["Seite 1"];
s.CurrentPage = Math.Clamp(s.CurrentPage, 0, s.Pages.Count - 1); s.CurrentPage = Math.Clamp(s.CurrentPage, 0, s.Pages.Count - 1);
@@ -257,11 +305,12 @@ public class AppSettings
g.Items[i] = newDir + g.Items[i][oldDir.Length..]; g.Items[i] = newDir + g.Items[i][oldDir.Length..];
} }
/// <summary>Wird diese Kategorie in irgendeiner Box angezeigt?</summary> /// <summary>Liegt diese Kategorie auf der gerade angezeigten Desktop-Seite in einer Box?</summary>
public bool HasFence(string category) => public bool HasFence(string category) =>
Fences.Any(f => f.Tabs.Any(t => !t.IsFolder && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase))); Fences.Any(f => f.Page == CurrentPage &&
f.Tabs.Any(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase)));
/// <summary>Kategorie als eigene Box anzeigen bzw. aus allen Boxen entfernen.</summary> /// <summary>Kategorie auf der aktuellen Seite als eigene Box anzeigen bzw. von dieser Seite entfernen (andere Seiten bleiben).</summary>
public void SetFence(string category, bool on) public void SetFence(string category, bool on)
{ {
if (on) if (on)
@@ -271,8 +320,8 @@ public class AppSettings
} }
else else
{ {
foreach (var f in Fences) foreach (var f in Fences.Where(f => f.Page == CurrentPage))
f.Tabs.RemoveAll(t => !t.IsFolder && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase)); f.Tabs.RemoveAll(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase));
Fences.RemoveAll(f => f.Tabs.Count == 0); Fences.RemoveAll(f => f.Tabs.Count == 0);
} }
} }
@@ -288,9 +337,41 @@ public class AppSettings
foreach (var g in LaunchGroups) foreach (var g in LaunchGroups)
for (int i = 0; i < g.Items.Count; i++) for (int i = 0; i < g.Items.Count; i++)
if (string.Equals(g.Items[i], oldPath, StringComparison.OrdinalIgnoreCase)) { g.Items[i] = newPath; changed = true; } if (string.Equals(g.Items[i], oldPath, StringComparison.OrdinalIgnoreCase)) { g.Items[i] = newPath; changed = true; }
// Umbenannt im selben Ordner → Platz in der eigenen Box-Reihenfolge behalten
if (Storage.SamePath(Path.GetDirectoryName(oldPath), Path.GetDirectoryName(newPath)))
{
string oldName = Path.GetFileName(oldPath), newName = Path.GetFileName(newPath);
foreach (var t in Fences.SelectMany(f => f.Tabs))
for (int i = 0; i < t.Order.Count; i++)
if (string.Equals(t.Order[i], oldName, StringComparison.OrdinalIgnoreCase)) { t.Order[i] = newName; changed = true; }
}
Usage.Rename(oldPath, newPath);
if (changed) Save(); if (changed) Save();
} }
// ---------- Desktop-Seiten ----------
public PageOption PageOption(string page)
{
if (!PageOptions.TryGetValue(page, out var o)) PageOptions[page] = o = new PageOption();
return o;
}
public void RenamePage(int index, string newName)
{
var old = Pages[index];
Pages[index] = newName;
if (PageOptions.Remove(old, out var o)) PageOptions[newName] = o;
if (string.Equals(PageFallback, old, StringComparison.OrdinalIgnoreCase)) PageFallback = newName;
}
/// <summary>Aufräumen nach dem Löschen einer Seite (Boxen verschiebt der FenceManager).</summary>
public void ForgetPage(string name)
{
PageOptions.Remove(name);
if (string.Equals(PageFallback, name, StringComparison.OrdinalIgnoreCase)) PageFallback = null;
}
public void RemoveCategory(string name) public void RemoveCategory(string name)
{ {
foreach (var f in Fences) foreach (var f in Fences)
+120
View File
@@ -0,0 +1,120 @@
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Windows.Threading;
namespace DeskCore;
public class UsageEntry
{
public int Count { get; set; }
public DateTime Last { get; set; }
}
/// <summary>
/// Merkt sich, was wie oft und wann zuletzt über DeskCore gestartet wurde (für „Meistgenutzt“, „Zuletzt benutzt“
/// und die Schnellstart-Suche). Liegt getrennt von den Einstellungen in usage.json, weil es sich ständig ändert.
/// </summary>
public static class Usage
{
/// <summary>Schlüssel-Präfix für Start-Gruppen (die keinen Dateipfad haben).</summary>
public const string GroupPrefix = "gruppe:";
private static readonly string FilePath = Path.Combine(AppSettings.Folder, "usage.json");
private static readonly Dictionary<string, UsageEntry> _data = Load();
private static DispatcherTimer? _saveTimer;
/// <summary>Etwas wurde gestartet (z. B. damit „Zuletzt benutzt“-Boxen sich aktualisieren).</summary>
public static event Action? Changed;
private static Dictionary<string, UsageEntry> Load()
{
try
{
if (File.Exists(FilePath) &&
JsonSerializer.Deserialize<Dictionary<string, UsageEntry>>(File.ReadAllText(FilePath)) is { } d)
return new(d, StringComparer.OrdinalIgnoreCase);
}
catch { }
return new(StringComparer.OrdinalIgnoreCase);
}
/// <summary>Programm/Datei starten und den Start mitzählen. Wirft bei Fehlern wie Process.Start.</summary>
public static void Start(string path)
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
Record(path);
}
public static void Record(string key)
{
if (!_data.TryGetValue(key, out var e)) _data[key] = e = new UsageEntry();
e.Count++;
e.Last = DateTime.Now;
ScheduleSave();
Changed?.Invoke();
}
public static int Count(string key) => _data.TryGetValue(key, out var e) ? e.Count : 0;
/// <summary>
/// Wie „beliebt“ etwas ist: Anzahl der Starts, mit der Zeit abnehmend gewichtet –
/// was du früher oft, aber seit Wochen nicht mehr benutzt hast, rutscht langsam nach hinten.
/// </summary>
public static double Score(string key)
{
if (!_data.TryGetValue(key, out var e)) return 0;
double days = Math.Max(0, (DateTime.Now - e.Last).TotalDays);
return e.Count / (1 + days / 30);
}
/// <summary>Zuletzt gestartete Dateien, die es noch gibt (neueste zuerst).</summary>
public static List<string> Recent(int max) => _data
.Where(kv => !kv.Key.StartsWith(GroupPrefix) && (File.Exists(kv.Key) || Directory.Exists(kv.Key)))
.OrderByDescending(kv => kv.Value.Last)
.Take(max)
.Select(kv => kv.Key)
.ToList();
/// <summary>Datei wurde umbenannt/verschoben → Zählung mitnehmen.</summary>
public static void Rename(string oldKey, string newKey)
{
if (!_data.Remove(oldKey, out var e)) return;
if (_data.TryGetValue(newKey, out var existing))
{
existing.Count += e.Count;
if (e.Last > existing.Last) existing.Last = e.Last;
}
else _data[newKey] = e;
ScheduleSave();
}
public static void Forget(string key)
{
if (_data.Remove(key)) { ScheduleSave(); Changed?.Invoke(); }
}
private static void ScheduleSave()
{
_saveTimer ??= new DispatcherTimer(TimeSpan.FromSeconds(2), DispatcherPriority.Background, (_, _) => Flush(),
Dispatcher.CurrentDispatcher);
_saveTimer.Stop();
_saveTimer.Start();
}
/// <summary>Sofort speichern (z. B. beim Beenden).</summary>
public static void Flush()
{
_saveTimer?.Stop();
try
{
// Nicht mehr vorhandene Einträge, die lange nicht benutzt wurden, aufräumen
foreach (var key in _data.Where(kv => kv.Value.Last < DateTime.Now.AddDays(-180) && !kv.Key.StartsWith(GroupPrefix)
&& !File.Exists(kv.Key) && !Directory.Exists(kv.Key)).Select(kv => kv.Key).ToList())
_data.Remove(key);
Directory.CreateDirectory(AppSettings.Folder);
File.WriteAllText(FilePath, JsonSerializer.Serialize(_data));
}
catch { }
}
}
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,7 @@
"compilationOptions": {}, "compilationOptions": {},
"targets": { "targets": {
".NETCoreApp,Version=v9.0": { ".NETCoreApp,Version=v9.0": {
"DeskCore/1.1.0": { "DeskCore/1.0.3": {
"runtime": { "runtime": {
"DeskCore.dll": {} "DeskCore.dll": {}
} }
@@ -14,7 +14,7 @@
} }
}, },
"libraries": { "libraries": {
"DeskCore/1.1.0": { "DeskCore/1.0.3": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
Binary file not shown.