191 lines
8.1 KiB
C#
191 lines
8.1 KiB
C#
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);
|
||
}
|