From f1e4fcb0190c6c01ec6044b78629331113e9aea4 Mon Sep 17 00:00:00 2001 From: SlimeNull <69663231+SlimeNull@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:53:34 +0800 Subject: [PATCH] Add SnapLayout Options support for Win11 (#345) --- dnSpy/dnSpy/Controls/WinSysButton.cs | 57 +- .../Utilities/WindowOption.NativeMethods.cs | 18 + .../EleCho.WpfSuite/Utilities/WindowOption.cs | 588 ++++++++++++++++++ dnSpy/dnSpy/Themes/wpf.styles.templates.xaml | 14 +- 4 files changed, 645 insertions(+), 32 deletions(-) create mode 100644 dnSpy/dnSpy/Lib/EleCho.WpfSuite/Utilities/WindowOption.NativeMethods.cs create mode 100644 dnSpy/dnSpy/Lib/EleCho.WpfSuite/Utilities/WindowOption.cs diff --git a/dnSpy/dnSpy/Controls/WinSysButton.cs b/dnSpy/dnSpy/Controls/WinSysButton.cs index fa9dd54f8e..2c4afbdcfc 100644 --- a/dnSpy/dnSpy/Controls/WinSysButton.cs +++ b/dnSpy/dnSpy/Controls/WinSysButton.cs @@ -58,7 +58,10 @@ public CurrentWinSysType CurrentWinSysType { static WinSysButton() => DefaultStyleKeyProperty.OverrideMetadata(typeof(WinSysButton), new FrameworkPropertyMetadata(typeof(WinSysButton))); - public WinSysButton() => Loaded += WinSysButton_Loaded; + public WinSysButton() { + Loaded += WinSysButton_Loaded; + Click += WinSysButton_Click; + } void WinSysButton_Loaded(object? sender, RoutedEventArgs e) { Loaded -= WinSysButton_Loaded; @@ -67,6 +70,32 @@ void WinSysButton_Loaded(object? sender, RoutedEventArgs e) { window.StateChanged += window_StateChanged; } + void WinSysButton_Click(object sender, RoutedEventArgs e) { + if (window is null) + return; + + switch (CurrentWinSysType) { + case CurrentWinSysType.Minimize: + WindowUtils.Minimize(window); + break; + + case CurrentWinSysType.Maximize: + WindowUtils.Maximize(window); + break; + + case CurrentWinSysType.Restore: + WindowUtils.Restore(window); + break; + + case CurrentWinSysType.Close: + window.Close(); + break; + + default: + throw new ArgumentException("Invalid CurrentWinSysType"); + } + } + void window_StateChanged(object? sender, EventArgs e) => OnWinSysTypeChanged(WinSysType); static void OnWinSysTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((WinSysButton)d).OnWinSysTypeChanged((WinSysType)e.NewValue); @@ -97,31 +126,5 @@ void OnWinSysTypeChanged(WinSysType newValue) { throw new ArgumentException("Invalid WinSysType"); } } - - protected override void OnClick() { - if (window is null) - return; - - switch (CurrentWinSysType) { - case CurrentWinSysType.Minimize: - WindowUtils.Minimize(window); - break; - - case CurrentWinSysType.Maximize: - WindowUtils.Maximize(window); - break; - - case CurrentWinSysType.Restore: - WindowUtils.Restore(window); - break; - - case CurrentWinSysType.Close: - window.Close(); - break; - - default: - throw new ArgumentException("Invalid CurrentWinSysType"); - } - } } } diff --git a/dnSpy/dnSpy/Lib/EleCho.WpfSuite/Utilities/WindowOption.NativeMethods.cs b/dnSpy/dnSpy/Lib/EleCho.WpfSuite/Utilities/WindowOption.NativeMethods.cs new file mode 100644 index 0000000000..38df140a63 --- /dev/null +++ b/dnSpy/dnSpy/Lib/EleCho.WpfSuite/Utilities/WindowOption.NativeMethods.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; + +namespace EleCho.WpfSuite { + public partial class WindowOption { + internal static class NativeDefinition { + public const nint WM_NCHITTEST = 0x0084; + public const nint WM_NCMOUSELEAVE = 0x02A2; + public const nint WM_NCLBUTTONDOWN = 0x00A1; + public const nint WM_NCLBUTTONUP = 0x00A2; + public const nint WM_MOUSEMOVE = 0x0200; + + public const nint HTCLOSE = 20; + public const nint HTMAXBUTTON = 9; + public const nint HTMINBUTTON = 8; + } + } +} diff --git a/dnSpy/dnSpy/Lib/EleCho.WpfSuite/Utilities/WindowOption.cs b/dnSpy/dnSpy/Lib/EleCho.WpfSuite/Utilities/WindowOption.cs new file mode 100644 index 0000000000..7ff483a145 --- /dev/null +++ b/dnSpy/dnSpy/Lib/EleCho.WpfSuite/Utilities/WindowOption.cs @@ -0,0 +1,588 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Shell; +using System.Xml.Linq; +using static EleCho.WpfSuite.WindowOption.NativeDefinition; + +namespace EleCho.WpfSuite { + /// + /// Window options + /// + public partial class WindowOption { + static readonly Version s_versionWindows10_1809 = new Version(10, 0, 17763); + static readonly Version s_versionWindows10 = new Version(10, 0); + + /// + /// DWM Supports Corner, BorderColor, CaptionColor, TextColor + /// + static readonly Version s_versionWindows11_22000 = new Version(10, 0, 22000); + + /// + /// DWM Supports Dark mode and Backdrop property + /// + static readonly Version s_versionWindows11_22621 = new Version(10, 0, 22621); + + static readonly Version s_versionCurrentWindows = Environment.OSVersion.Version; + + static Dictionary? s_maximumButtons; + static Dictionary? s_minimumButtons; + static Dictionary? s_closeButtons; + + static DependencyPropertyKey s_uiElementIsMouseOverPropertyKey = + (DependencyPropertyKey)typeof(UIElement).GetField("IsMouseOverPropertyKey", BindingFlags.NonPublic | BindingFlags.Static)!.GetValue(null)!; + + static DependencyPropertyKey s_buttonIsPressedPropertyKey = + (DependencyPropertyKey)typeof(ButtonBase).GetField("IsPressedPropertyKey", BindingFlags.NonPublic | BindingFlags.Static)!.GetValue(null)!; + + /// + /// Get value of IsMaximumButton property + /// + /// + /// + public static bool GetIsMaximumButton(DependencyObject obj) { + return (bool)obj.GetValue(IsMaximumButtonProperty); + } + + /// + /// Set value of IsMaximumButton property + /// + /// + /// + public static void SetIsMaximumButton(DependencyObject obj, bool value) { + obj.SetValue(IsMaximumButtonProperty, value); + } + + /// + /// Get value of IsMinimumButton property + /// + /// + /// + public static bool GetIsMinimumButton(DependencyObject obj) { + return (bool)obj.GetValue(IsMinimumButtonProperty); + } + + /// + /// Set value of IsMinimumButton property + /// + /// + /// + public static void SetIsMinimumButton(DependencyObject obj, bool value) { + obj.SetValue(IsMinimumButtonProperty, value); + } + + /// + /// Get value of IsCloseButton property + /// + /// + /// + public static bool GetIsCloseButton(DependencyObject obj) { + return (bool)obj.GetValue(IsCloseButtonProperty); + } + + /// + /// Set value of IsCloseButton property + /// + /// + /// + public static void SetIsCloseButton(DependencyObject obj, bool value) { + obj.SetValue(IsCloseButtonProperty, value); + } + + /// + /// The DependencyProperty of IsMaximumButton property + /// + public static readonly DependencyProperty IsMaximumButtonProperty = + DependencyProperty.RegisterAttached("IsMaximumButton", typeof(bool), typeof(WindowOption), new FrameworkPropertyMetadata(false, OnIsMaximumButtonChanged)); + + /// + /// The DependencyProperty of IsMinimumButton property + /// + public static readonly DependencyProperty IsMinimumButtonProperty = + DependencyProperty.RegisterAttached("IsMinimumButton", typeof(bool), typeof(WindowOption), new FrameworkPropertyMetadata(false, OnIsMinimumButtonChanged)); + + /// + /// The DependencyProperty of IsCloseButton property + /// + public static readonly DependencyProperty IsCloseButtonProperty = + DependencyProperty.RegisterAttached("IsCloseButton", typeof(bool), typeof(WindowOption), new FrameworkPropertyMetadata(false, OnIsCloseButtonChanged)); + + #region DependencyProperty Callbacks + + private static void OnIsMaximumButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + if (d is not FrameworkElement frameworkElement) { + throw new InvalidOperationException("Target DependencyObject is not FrameworkElement"); + } + + if (DesignerProperties.GetIsInDesignMode(d)) { + return; + } + + if (Window.GetWindow(d) is Window window) { + DoAfterWindowSourceInitialized(window, () => { + ApplyIsMaximumButton(window, frameworkElement, (bool)e.NewValue); + }); + } + else { + DoAfterElementLoaded(frameworkElement, () => { + if (Window.GetWindow(frameworkElement) is Window loadedWindow) { + DoAfterWindowSourceInitialized(loadedWindow, () => { + ApplyIsMaximumButton(loadedWindow, frameworkElement, (bool)e.NewValue); + }); + } + else { + throw new InvalidOperationException("Cannot find Window of Visual"); + } + }); + } + } + + private static void OnIsMinimumButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + if (d is not FrameworkElement frameworkElement) { + throw new InvalidOperationException("Target DependencyObject is not FrameworkElement"); + } + + if (DesignerProperties.GetIsInDesignMode(d)) { + return; + } + + if (Window.GetWindow(d) is Window window) { + DoAfterWindowSourceInitialized(window, () => { + ApplyIsMinimumButton(window, frameworkElement, (bool)e.NewValue); + }); + } + else { + DoAfterElementLoaded(frameworkElement, () => { + if (Window.GetWindow(frameworkElement) is Window loadedWindow) { + DoAfterWindowSourceInitialized(loadedWindow, () => { + ApplyIsMinimumButton(loadedWindow, frameworkElement, (bool)e.NewValue); + }); + } + else { + throw new InvalidOperationException("Cannot find Window of Visual"); + } + }); + } + } + + private static void OnIsCloseButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + if (d is not FrameworkElement frameworkElement) { + throw new InvalidOperationException("Target DependencyObject is not FrameworkElement"); + } + + if (DesignerProperties.GetIsInDesignMode(d)) { + return; + } + + if (Window.GetWindow(d) is Window window) { + DoAfterWindowSourceInitialized(window, () => { + ApplyIsCloseButton(window, frameworkElement, (bool)e.NewValue); + }); + } + else { + DoAfterElementLoaded(frameworkElement, () => { + if (Window.GetWindow(frameworkElement) is Window loadedWindow) { + DoAfterWindowSourceInitialized(loadedWindow, () => { + ApplyIsCloseButton(loadedWindow, frameworkElement, (bool)e.NewValue); + }); + } + else { + throw new InvalidOperationException("Cannot find Window of Visual"); + } + }); + } + } + + private static IntPtr WindowCaptionButtonsInteropHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { + if (handled) { + return IntPtr.Zero; + } + + switch ((nint)msg) { + case NativeDefinition.WM_NCHITTEST: { + var x = (int)((ulong)lParam & 0x0000FFFF); + var y = (int)((ulong)lParam & 0xFFFF0000) >> 16; + var result = default(IntPtr); + + if (s_maximumButtons is not null && + s_maximumButtons.TryGetValue(hwnd, out var maximumButtonVisual)) { + var relativePoint = maximumButtonVisual.PointFromScreen(new Point(x, y)); + var hitResult = VisualTreeHelper.HitTest(maximumButtonVisual, relativePoint); + + if (hitResult is not null) { + maximumButtonVisual.SetValue(s_uiElementIsMouseOverPropertyKey, true); + + handled = true; + result = NativeDefinition.HTMAXBUTTON; + } + else { + maximumButtonVisual.SetValue(s_uiElementIsMouseOverPropertyKey, false); + + if (maximumButtonVisual is ButtonBase button) { + button.SetValue(s_buttonIsPressedPropertyKey, false); + } + } + } + + if (s_minimumButtons is not null && + s_minimumButtons.TryGetValue(hwnd, out var minimumButtonVisual)) { + var relativePoint = minimumButtonVisual.PointFromScreen(new Point(x, y)); + var hitResult = VisualTreeHelper.HitTest(minimumButtonVisual, relativePoint); + + if (hitResult is not null) { + minimumButtonVisual.SetValue(s_uiElementIsMouseOverPropertyKey, true); + + handled = true; + result = NativeDefinition.HTMINBUTTON; + } + else { + minimumButtonVisual.SetValue(s_uiElementIsMouseOverPropertyKey, false); + + if (minimumButtonVisual is ButtonBase button) { + button.SetValue(s_buttonIsPressedPropertyKey, false); + } + } + } + + if (s_closeButtons is not null && + s_closeButtons.TryGetValue(hwnd, out var closeButtonVisual)) { + var relativePoint = closeButtonVisual.PointFromScreen(new Point(x, y)); + var hitResult = VisualTreeHelper.HitTest(closeButtonVisual, relativePoint); + + if (hitResult is not null) { + closeButtonVisual.SetValue(s_uiElementIsMouseOverPropertyKey, true); + + handled = true; + result = NativeDefinition.HTCLOSE; + } + else { + closeButtonVisual.SetValue(s_uiElementIsMouseOverPropertyKey, false); + + if (closeButtonVisual is ButtonBase button) { + button.SetValue(s_buttonIsPressedPropertyKey, false); + } + } + } + + return result; + } + + case NativeDefinition.WM_NCLBUTTONDOWN: { + var x = (int)((ulong)lParam & 0x0000FFFF); + var y = (int)((ulong)lParam & 0xFFFF0000) >> 16; + + if (s_maximumButtons is not null && + s_maximumButtons.TryGetValue(hwnd, out var maximumButtonVisual)) { + var relativePoint = maximumButtonVisual.PointFromScreen(new Point(x, y)); + var hitResult = VisualTreeHelper.HitTest(maximumButtonVisual, relativePoint); + + if (hitResult is not null) { + if (maximumButtonVisual is ButtonBase button) { + button.SetValue(s_buttonIsPressedPropertyKey, true); + } + + handled = true; + } + } + + if (s_minimumButtons is not null && + s_minimumButtons.TryGetValue(hwnd, out var minimumButtonVisual)) { + var relativePoint = minimumButtonVisual.PointFromScreen(new Point(x, y)); + var hitResult = VisualTreeHelper.HitTest(minimumButtonVisual, relativePoint); + + if (hitResult is not null) { + if (minimumButtonVisual is ButtonBase button) { + button.SetValue(s_buttonIsPressedPropertyKey, true); + } + + handled = true; + } + } + + if (s_closeButtons is not null && + s_closeButtons.TryGetValue(hwnd, out var closeButtonVisual)) { + var relativePoint = closeButtonVisual.PointFromScreen(new Point(x, y)); + var hitResult = VisualTreeHelper.HitTest(closeButtonVisual, relativePoint); + + if (hitResult is not null) { + if (closeButtonVisual is ButtonBase button) { + button.SetValue(s_buttonIsPressedPropertyKey, true); + } + + handled = true; + } + } + + break; + } + + case NativeDefinition.WM_NCLBUTTONUP: { + var x = (int)((ulong)lParam & 0x0000FFFF); + var y = (int)((ulong)lParam & 0xFFFF0000) >> 16; + + if (s_maximumButtons is not null && + s_maximumButtons.TryGetValue(hwnd, out var maximumButtonVisual)) { + if (maximumButtonVisual is ButtonBase button) { + bool shouldClick = false; + if ((bool)button.GetValue(s_buttonIsPressedPropertyKey.DependencyProperty)) { + shouldClick = true; + } + + button.SetValue(s_buttonIsPressedPropertyKey, false); + + if (shouldClick) { + button.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent, button)); + button.Command?.Execute(button.CommandParameter); + } + + handled = true; + } + } + + if (s_minimumButtons is not null && + s_minimumButtons.TryGetValue(hwnd, out var minimumButtonVisual)) { + if (minimumButtonVisual is ButtonBase button) { + bool shouldClick = false; + if ((bool)button.GetValue(s_buttonIsPressedPropertyKey.DependencyProperty)) { + shouldClick = true; + } + + button.SetValue(s_buttonIsPressedPropertyKey, false); + + if (shouldClick) { + button.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent, button)); + button.Command?.Execute(button.CommandParameter); + } + + handled = true; + } + } + + if (s_closeButtons is not null && + s_closeButtons.TryGetValue(hwnd, out var closeButtonVisual)) { + if (closeButtonVisual is ButtonBase button) { + bool shouldClick = false; + if ((bool)button.GetValue(s_buttonIsPressedPropertyKey.DependencyProperty)) { + shouldClick = true; + } + + button.SetValue(s_buttonIsPressedPropertyKey, false); + + if (shouldClick) { + button.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent, button)); + button.Command?.Execute(button.CommandParameter); + } + + handled = true; + } + } + + break; + } + + case NativeDefinition.WM_NCMOUSELEAVE: { + var x = (int)((ulong)lParam & 0x0000FFFF); + var y = (int)((ulong)lParam & 0xFFFF0000) >> 16; + + if (s_maximumButtons is not null && + s_maximumButtons.TryGetValue(hwnd, out var maximumButtonVisual)) { + maximumButtonVisual.SetValue(s_uiElementIsMouseOverPropertyKey, false); + + if (maximumButtonVisual is ButtonBase button) { + button.SetValue(s_buttonIsPressedPropertyKey, false); + } + } + + if (s_minimumButtons is not null && + s_minimumButtons.TryGetValue(hwnd, out var minimumButtonVisual)) { + minimumButtonVisual.SetValue(s_uiElementIsMouseOverPropertyKey, false); + + if (minimumButtonVisual is ButtonBase button) { + button.SetValue(s_buttonIsPressedPropertyKey, false); + } + } + + if (s_closeButtons is not null && + s_closeButtons.TryGetValue(hwnd, out var closeButtonVisual)) { + closeButtonVisual.SetValue(s_uiElementIsMouseOverPropertyKey, false); + + if (closeButtonVisual is ButtonBase button) { + button.SetValue(s_buttonIsPressedPropertyKey, false); + } + } + + break; + } + } + + return IntPtr.Zero; + } + + #endregion + + #region Utilities + + private static void DoAfterWindowSourceInitialized(Window window, Action action) { + var eventHandler = default(EventHandler); + + eventHandler = (s, e) => { + action?.Invoke(); + window.SourceInitialized -= eventHandler; + }; + + window.SourceInitialized += eventHandler; + } + + private static void DoAfterElementLoaded(FrameworkElement element, Action action) { + var eventHandler = default(RoutedEventHandler); + + eventHandler = (s, e) => { + action?.Invoke(); + element.Loaded -= eventHandler; + }; + + element.Loaded += eventHandler; + } + + private static bool HasWindowCaptionButton(nint hwnd) { + if (s_minimumButtons is not null && s_minimumButtons.ContainsKey(hwnd)) + return true; + if (s_maximumButtons is not null && s_maximumButtons.ContainsKey(hwnd)) + return true; + if (s_closeButtons is not null && s_closeButtons.ContainsKey(hwnd)) + return true; + + return false; + } + + #endregion + + #region Final Logic + + private static unsafe void ApplyIsMaximumButton(Window window, Visual visual, bool isMaximumButton) { + var windowInteropHelper = new WindowInteropHelper(window); + var windowHandle = windowInteropHelper.EnsureHandle(); + + var hwndSource = HwndSource.FromHwnd(windowHandle); + + if (isMaximumButton) { + if (s_maximumButtons is null) { + s_maximumButtons = new(); + } + + if (HasWindowCaptionButton(windowHandle)) { + hwndSource.AddHook(WindowCaptionButtonsInteropHook); + } + + if (s_maximumButtons.ContainsKey(windowHandle)) { + throw new InvalidOperationException("MaximumButton is already set to another Visual"); + } + + s_maximumButtons[windowHandle] = visual; + } + else { + if (s_maximumButtons is null) { + return; + } + + s_maximumButtons.Remove(windowHandle); + + if (s_maximumButtons.Count == 0) { + s_maximumButtons = null; + } + + if (!HasWindowCaptionButton(windowHandle)) { + hwndSource.RemoveHook(WindowCaptionButtonsInteropHook); + } + } + } + + private static unsafe void ApplyIsMinimumButton(Window window, Visual visual, bool isMinimumButton) { + var windowInteropHelper = new WindowInteropHelper(window); + var windowHandle = windowInteropHelper.EnsureHandle(); + + var hwndSource = HwndSource.FromHwnd(windowHandle); + + if (isMinimumButton) { + if (s_minimumButtons is null) { + s_minimumButtons = new(); + } + + if (HasWindowCaptionButton(windowHandle)) { + hwndSource.AddHook(WindowCaptionButtonsInteropHook); + } + + if (s_minimumButtons.ContainsKey(windowHandle)) { + throw new InvalidOperationException("MinimumButton is already set to another Visual"); + } + + s_minimumButtons[windowHandle] = visual; + } + else { + if (s_minimumButtons is null) { + return; + } + + s_minimumButtons.Remove(windowHandle); + + if (s_minimumButtons.Count == 0) { + s_minimumButtons = null; + } + + if (!HasWindowCaptionButton(windowHandle)) { + hwndSource.RemoveHook(WindowCaptionButtonsInteropHook); + } + } + } + + private static unsafe void ApplyIsCloseButton(Window window, Visual visual, bool isCloseButton) { + var windowInteropHelper = new WindowInteropHelper(window); + var windowHandle = windowInteropHelper.EnsureHandle(); + + var hwndSource = HwndSource.FromHwnd(windowHandle); + + if (isCloseButton) { + if (s_closeButtons is null) { + s_closeButtons = new(); + } + + if (HasWindowCaptionButton(windowHandle)) { + hwndSource.AddHook(WindowCaptionButtonsInteropHook); + } + + if (s_closeButtons.ContainsKey(windowHandle)) { + throw new InvalidOperationException("MinimumButton is already set to another Visual"); + } + + s_closeButtons[windowHandle] = visual; + } + else { + if (s_closeButtons is null) { + return; + } + + s_closeButtons.Remove(windowHandle); + + if (s_closeButtons.Count == 0) { + s_closeButtons = null; + } + + if (!HasWindowCaptionButton(windowHandle)) { + hwndSource.RemoveHook(WindowCaptionButtonsInteropHook); + } + } + } + + #endregion + } +} diff --git a/dnSpy/dnSpy/Themes/wpf.styles.templates.xaml b/dnSpy/dnSpy/Themes/wpf.styles.templates.xaml index bed4d2a1ab..22538ebd82 100644 --- a/dnSpy/dnSpy/Themes/wpf.styles.templates.xaml +++ b/dnSpy/dnSpy/Themes/wpf.styles.templates.xaml @@ -22,7 +22,8 @@ xmlns:disasmsettingsx86="clr-namespace:dnSpy.Disassembly.X86" xmlns:disasmviewersettings="clr-namespace:dnSpy.Disassembly.Viewer" xmlns:bm="clr-namespace:dnSpy.Bookmarks.Settings" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:ws="clr-namespace:EleCho.WpfSuite">