Files
2026-10-04 14:48:53 +00:00

191 lines
8.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.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);
}