From b8dcbc43873b69328208b2b5a4b5bdd477f06420 Mon Sep 17 00:00:00 2001 From: Mahdi Hosseini Date: Thu, 30 Nov 2023 23:35:29 +0330 Subject: [PATCH] Update TitleBar Control (sync with Labs) --- .../Controls/TitleBar/InfoHelper.cs | 34 +++ .../Controls/TitleBar/NativeMethods.cs | 94 ++++++--- .../Controls/TitleBar/TitleBar.WASDK.cs | 198 ++++++++++++------ .../Controls/TitleBar/TitleBar.cs | 40 +++- .../Controls/TitleBar/TitleBar.xaml | 12 +- .../Controls/TitleBar/WndProcHelper.cs | 51 +++++ 6 files changed, 330 insertions(+), 99 deletions(-) create mode 100644 CommunityToolkit.App.Shared/Controls/TitleBar/InfoHelper.cs create mode 100644 CommunityToolkit.App.Shared/Controls/TitleBar/WndProcHelper.cs diff --git a/CommunityToolkit.App.Shared/Controls/TitleBar/InfoHelper.cs b/CommunityToolkit.App.Shared/Controls/TitleBar/InfoHelper.cs new file mode 100644 index 00000000..ff1672ae --- /dev/null +++ b/CommunityToolkit.App.Shared/Controls/TitleBar/InfoHelper.cs @@ -0,0 +1,34 @@ +using Windows.ApplicationModel; +using Windows.Storage; +using Windows.System.Profile; + +namespace CommunityToolkit.App.Shared.Controls; +internal static class InfoHelper +{ + public static Version AppVersion { get; } = new Version( + Package.Current.Id.Version.Major, + Package.Current.Id.Version.Minor, + Package.Current.Id.Version.Build, + Package.Current.Id.Version.Revision + ); + + public static Version SystemVersion { get; } + + public static SystemDataPaths SystemDataPath { get; } = SystemDataPaths.GetDefault(); + + public static UserDataPaths UserDataPath { get; } = UserDataPaths.GetDefault(); + + public static string AppInstalledLocation { get; } = Package.Current.InstalledLocation.Path; + + static InfoHelper() + { + string systemVersion = AnalyticsInfo.VersionInfo.DeviceFamilyVersion; + ulong version = ulong.Parse(systemVersion); + SystemVersion = new Version( + (int)((version & 0xFFFF000000000000L) >> 48), + (int)((version & 0x0000FFFF00000000L) >> 32), + (int)((version & 0x00000000FFFF0000L) >> 16), + (int)(version & 0x000000000000FFFFL) + ); + } +} \ No newline at end of file diff --git a/CommunityToolkit.App.Shared/Controls/TitleBar/NativeMethods.cs b/CommunityToolkit.App.Shared/Controls/TitleBar/NativeMethods.cs index 6e50ae3c..35bdbef4 100644 --- a/CommunityToolkit.App.Shared/Controls/TitleBar/NativeMethods.cs +++ b/CommunityToolkit.App.Shared/Controls/TitleBar/NativeMethods.cs @@ -6,42 +6,88 @@ #pragma warning disable CA1060 using System.Runtime.InteropServices; -using WinRT.Interop; -using Microsoft.UI; -using Microsoft.UI.Windowing; namespace CommunityToolkit.App.Shared.Controls; - -public partial class TitleBar : Control +internal static class NativeMethods { - [DllImport("Shcore.dll", SetLastError = true)] - internal static extern int GetDpiForMonitor(IntPtr hmonitor, Monitor_DPI_Type dpiType, out uint dpiX, out uint dpiY); + public enum WindowMessage : int + { + WM_NCLBUTTONDOWN = 0x00A1, + WM_NCRBUTTONDOWN = 0x00A4, + WM_SYSCOMMAND = 0x0112, + WM_SYSMENU = 0x0313, + WM_GETMINMAXINFO = 0x0024 + } + [Flags] + public enum WindowStyle : uint + { + WS_SYSMENU = 0x80000 + } - internal enum Monitor_DPI_Type : int + [Flags] + public enum WindowLongIndexFlags : int { - MDT_Effective_DPI = 0, - MDT_Angular_DPI = 1, - MDT_Raw_DPI = 2, - MDT_Default = MDT_Effective_DPI + GWL_WNDPROC = -4, + GWL_STYLE = -16 } - private double GetScaleAdjustment() + [Flags] + public enum SetWindowPosFlags : uint + { + /// + /// Retains the current position (ignores X and Y parameters). + /// + SWP_NOMOVE = 0x0002 + } + + public enum SystemCommand + { + SC_MOUSEMENU = 0xF090, + SC_KEYMENU = 0xF100 + } + + [DllImport("user32.dll", EntryPoint = "GetWindowLongW", SetLastError = false)] + public static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "GetWindowLongPtrW", SetLastError = false)] + public static extern int GetWindowLongPtr(IntPtr hWnd, int nIndex); + + public static int GetWindowLongAuto(IntPtr hWnd, int nIndex) { - IntPtr hWnd = WindowNative.GetWindowHandle(this.Window); - WindowId wndId = Win32Interop.GetWindowIdFromWindow(hWnd); - DisplayArea displayArea = DisplayArea.GetFromWindowId(wndId, DisplayAreaFallback.Primary); - IntPtr hMonitor = Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId); - - // Get DPI. - int result = GetDpiForMonitor(hMonitor, Monitor_DPI_Type.MDT_Default, out uint dpiX, out uint _); - if (result != 0) + if (IntPtr.Size is 8) { - throw new Exception("Could not get DPI for monitor."); + return GetWindowLongPtr(hWnd, nIndex); } + else + { + return GetWindowLong(hWnd, nIndex); + } + } + + [DllImport("user32.dll", EntryPoint = "FindWindowExW", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpszClass, string lpszWindow); + + + [DllImport("user32.dll", EntryPoint = "SetWindowLongW", SetLastError = false)] + public static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = false)] + public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); - uint scaleFactorPercent = (uint)(((long)dpiX * 100 + (96 >> 1)) / 96); - return scaleFactorPercent / 100.0; + public static IntPtr SetWindowLongAuto(IntPtr hWnd, int nIndex, IntPtr dwNewLong) + { + if (IntPtr.Size is 8) + { + return SetWindowLongPtr(hWnd, nIndex, dwNewLong); + } + else + { + return SetWindowLong(hWnd, nIndex, dwNewLong); + } } + + [DllImport("user32.dll")] + public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, WindowMessage Msg, IntPtr wParam, IntPtr lParam); } #pragma warning restore CA1060 #endif diff --git a/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.WASDK.cs b/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.WASDK.cs index 41c10c2c..9734a000 100644 --- a/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.WASDK.cs +++ b/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.WASDK.cs @@ -4,29 +4,21 @@ #if WINDOWS_WINAPPSDK using Microsoft.UI; +using Microsoft.UI.Input; using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml.Media; namespace CommunityToolkit.App.Shared.Controls; -[TemplatePart(Name = nameof(PART_ButtonsHolderColumn), Type = typeof(ColumnDefinition))] -[TemplatePart(Name = nameof(PART_IconColumn), Type = typeof(ColumnDefinition))] -[TemplatePart(Name = nameof(PART_TitleColumn), Type = typeof(ColumnDefinition))] -[TemplatePart(Name = nameof(PART_LeftDragColumn), Type = typeof(ColumnDefinition))] -[TemplatePart(Name = nameof(PART_ContentColumn), Type = typeof(ColumnDefinition))] -[TemplatePart(Name = nameof(PART_FooterColumn), Type = typeof(ColumnDefinition))] -[TemplatePart(Name = nameof(PART_RightDragColumn), Type = typeof(ColumnDefinition))] -[TemplatePart(Name = nameof(PART_TitleHolder), Type = typeof(StackPanel))] +[TemplatePart(Name = nameof(PART_FooterPresenter), Type = typeof(ContentPresenter))] +[TemplatePart(Name = nameof(PART_ContentPresenter), Type = typeof(ContentPresenter))] public partial class TitleBar : Control { - ColumnDefinition? PART_ButtonsHolderColumn; - ColumnDefinition? PART_IconColumn; - ColumnDefinition? PART_TitleColumn; - ColumnDefinition? PART_LeftDragColumn; - ColumnDefinition? PART_ContentColumn; - ColumnDefinition? PART_FooterColumn; - ColumnDefinition? PART_RightDragColumn; - StackPanel? PART_TitleHolder; + WndProcHelper WndProcHelper; + MenuFlyout MenuFlyout; + ContentPresenter? PART_ContentPresenter; + ContentPresenter? PART_FooterPresenter; private void SetWASDKTitleBar() { @@ -39,6 +31,16 @@ private void SetWASDKTitleBar() { Window.AppWindow.TitleBar.ExtendsContentIntoTitleBar = true; + if (this.ContextFlyout != null && this.ContextFlyout is MenuFlyout menuFlyout) + { + this.MenuFlyout = menuFlyout; + WndProcHelper = new WndProcHelper(this.Window); + WndProcHelper.RegisterWndProc(WindowWndProc); + WndProcHelper.RegisterInputNonClientPointerSourceWndProc(InputNonClientPointerSourceWndProc); + } + + this.Window.SizeChanged -= Window_SizeChanged; + this.Window.SizeChanged += Window_SizeChanged; this.Window.Activated -= Window_Activated; this.Window.Activated += Window_Activated; @@ -51,15 +53,8 @@ private void SetWASDKTitleBar() }; } - // Set the width of padding columns in the UI. - PART_ButtonsHolderColumn = GetTemplateChild(nameof(PART_ButtonsHolderColumn)) as ColumnDefinition; - PART_IconColumn = GetTemplateChild(nameof(PART_IconColumn)) as ColumnDefinition; - PART_TitleColumn = GetTemplateChild(nameof(PART_TitleColumn)) as ColumnDefinition; - PART_LeftDragColumn = GetTemplateChild(nameof(PART_LeftDragColumn)) as ColumnDefinition; - PART_ContentColumn = GetTemplateChild(nameof(PART_ContentColumn)) as ColumnDefinition; - PART_RightDragColumn = GetTemplateChild(nameof(PART_RightDragColumn)) as ColumnDefinition; - PART_FooterColumn = GetTemplateChild(nameof(PART_FooterColumn)) as ColumnDefinition; - PART_TitleHolder = GetTemplateChild(nameof(PART_TitleHolder)) as StackPanel; + PART_ContentPresenter = GetTemplateChild(nameof(PART_ContentPresenter)) as ContentPresenter; + PART_FooterPresenter = GetTemplateChild(nameof(PART_FooterPresenter)) as ContentPresenter; // Get caption button occlusion information. int CaptionButtonOcclusionWidthRight = Window.AppWindow.TitleBar.RightInset; @@ -83,6 +78,11 @@ private void SetWASDKTitleBar() } } + private void Window_SizeChanged(object sender, WindowSizeChangedEventArgs args) + { + UpdateVisualStateAndDragRegion(args.Size); + } + private void UpdateCaptionButtons(FrameworkElement rootElement) { Window.AppWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent; @@ -108,6 +108,7 @@ private void ResetWASDKTitleBar() } Window.AppWindow.TitleBar.ExtendsContentIntoTitleBar = false; + this.Window.SizeChanged -= Window_SizeChanged; this.Window.Activated -= Window_Activated; SizeChanged -= this.TitleBar_SizeChanged; Window.AppWindow.TitleBar.ResetToDefault(); @@ -125,46 +126,119 @@ private void Window_Activated(object sender, WindowActivatedEventArgs args) } } - private void SetDragRegionForCustomTitleBar() + public void SetDragRegionForCustomTitleBar() + { + if (AutoConfigureCustomTitleBar && Window != null) + { + ClearDragRegions(NonClientRegionKind.Passthrough); + SetDragRegion(NonClientRegionKind.Passthrough, PART_ContentPresenter, PART_FooterPresenter, PART_ButtonHolder); + } + } + + public double GetRasterizationScaleForElement(UIElement element) + { + if (element.XamlRoot != null) + { + return element.XamlRoot.RasterizationScale; + } + return 0.0; + } + + public void SetDragRegion(NonClientRegionKind nonClientRegionKind, params FrameworkElement[] frameworkElements) + { + var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(Window.AppWindow.Id); + List rects = new List(); + var scale = GetRasterizationScaleForElement(this); + + foreach (var frameworkElement in frameworkElements) + { + if (frameworkElement == null) + { + continue; + } + GeneralTransform transformElement = frameworkElement.TransformToVisual(null); + Windows.Foundation.Rect bounds = transformElement.TransformBounds(new Windows.Foundation.Rect(0, 0, frameworkElement.ActualWidth, frameworkElement.ActualHeight)); + var transparentRect = new Windows.Graphics.RectInt32( + _X: (int)Math.Round(bounds.X * scale), + _Y: (int)Math.Round(bounds.Y * scale), + _Width: (int)Math.Round(bounds.Width * scale), + _Height: (int)Math.Round(bounds.Height * scale) + ); + rects.Add(transparentRect); + } + if (rects.Count > 0) + { + nonClientInputSrc.SetRegionRects(nonClientRegionKind, rects.ToArray()); + } + } + + public void ClearDragRegions(NonClientRegionKind nonClientRegionKind) + { + var noninputsrc = InputNonClientPointerSource.GetForWindowId(Window.AppWindow.Id); + noninputsrc.ClearRegionRects(nonClientRegionKind); + } + + private IntPtr InputNonClientPointerSourceWndProc(IntPtr hWnd, NativeMethods.WindowMessage Msg, IntPtr wParam, IntPtr lParam) + { + switch (Msg) + { + case NativeMethods.WindowMessage.WM_NCLBUTTONDOWN: + { + if (MenuFlyout.IsOpen) + { + MenuFlyout.Hide(); + } + break; + } + case NativeMethods.WindowMessage.WM_NCRBUTTONDOWN: + { + PointInt32 pt = new PointInt32(lParam.ToInt32() & 0xFFFF, lParam.ToInt32() >> 16); + FlyoutShowOptions options = new FlyoutShowOptions(); + options.ShowMode = FlyoutShowMode.Standard; + options.Position = InfoHelper.SystemVersion.Build >= 22000 ? + new Windows.Foundation.Point((pt.X - this.Window.AppWindow.Position.X - 8) / XamlRoot.RasterizationScale, (pt.Y - this.Window.AppWindow.Position.Y) / XamlRoot.RasterizationScale) : + new Windows.Foundation.Point(pt.X - this.Window.AppWindow.Position.X - 8, pt.Y - this.Window.AppWindow.Position.Y); + + MenuFlyout.ShowAt(this, options); + return (IntPtr)0; + } + } + return WndProcHelper.CallInputNonClientPointerSourceWindowProc(hWnd, Msg, wParam, lParam); + } + + private IntPtr WindowWndProc(IntPtr hWnd, NativeMethods.WindowMessage Msg, IntPtr wParam, IntPtr lParam) { - if (AutoConfigureCustomTitleBar && Window != null && PART_RightPaddingColumn != null && PART_LeftPaddingColumn != null) + switch (Msg) { - double scaleAdjustment = GetScaleAdjustment(); - - PART_RightPaddingColumn.Width = new GridLength(Window.AppWindow.TitleBar.RightInset / scaleAdjustment); - PART_LeftPaddingColumn.Width = new GridLength(Window.AppWindow.TitleBar.LeftInset / scaleAdjustment); - - List dragRectsList = new(); - - Windows.Graphics.RectInt32 dragRectL; - dragRectL.X = (int)((PART_LeftPaddingColumn.ActualWidth - + PART_ButtonsHolderColumn!.ActualWidth) - * scaleAdjustment); - dragRectL.Y = 0; - dragRectL.Height = (int)(this.ActualHeight * scaleAdjustment); - dragRectL.Width = (int)((PART_IconColumn!.ActualWidth - + PART_TitleColumn!.ActualWidth - + PART_LeftDragColumn!.ActualWidth) - * scaleAdjustment); - dragRectsList.Add(dragRectL); - - Windows.Graphics.RectInt32 dragRectR; - dragRectR.X = (int)((PART_LeftPaddingColumn.ActualWidth - + PART_IconColumn.ActualWidth - + PART_ButtonsHolderColumn!.ActualWidth - + PART_TitleHolder!.ActualWidth - + PART_LeftDragColumn.ActualWidth - + PART_ContentColumn!.ActualWidth) - * scaleAdjustment); - dragRectR.Y = 0; - dragRectR.Height = (int)(this.ActualHeight * scaleAdjustment); - dragRectR.Width = (int)(PART_RightDragColumn!.ActualWidth * scaleAdjustment); - dragRectsList.Add(dragRectR); - - Windows.Graphics.RectInt32[] dragRects = dragRectsList.ToArray(); - - Window.AppWindow.TitleBar.SetDragRectangles(dragRects); + case NativeMethods.WindowMessage.WM_SYSMENU: + { + return (IntPtr)0; + } + + case NativeMethods.WindowMessage.WM_SYSCOMMAND: + { + NativeMethods.SystemCommand sysCommand = (NativeMethods.SystemCommand)(wParam.ToInt32() & 0xFFF0); + + if (sysCommand is NativeMethods.SystemCommand.SC_MOUSEMENU) + { + FlyoutShowOptions options = new FlyoutShowOptions(); + options.Position = new Windows.Foundation.Point(0, 15); + options.ShowMode = FlyoutShowMode.Standard; + MenuFlyout.ShowAt(null, options); + return (IntPtr)0; + } + else if (sysCommand is NativeMethods.SystemCommand.SC_KEYMENU) + { + FlyoutShowOptions options = new FlyoutShowOptions(); + options.Position = new Windows.Foundation.Point(0, 45); + options.ShowMode = FlyoutShowMode.Standard; + MenuFlyout.ShowAt(null, options); + return (IntPtr)0; + } + break; + } } + return WndProcHelper.CallWindowProc(hWnd, Msg, wParam, lParam); } } #endif diff --git a/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.cs b/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.cs index 0e19b451..7814cd45 100644 --- a/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.cs +++ b/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.cs @@ -24,6 +24,7 @@ namespace CommunityToolkit.App.Shared.Controls; [TemplatePart(Name = PartPaneButton, Type = typeof(Button))] [TemplatePart(Name = nameof(PART_LeftPaddingColumn), Type = typeof(ColumnDefinition))] [TemplatePart(Name = nameof(PART_RightPaddingColumn), Type = typeof(ColumnDefinition))] +[TemplatePart(Name = nameof(PART_ButtonHolder), Type = typeof(StackPanel))] public partial class TitleBar : Control { @@ -64,6 +65,7 @@ public partial class TitleBar : Control ColumnDefinition? PART_LeftPaddingColumn; ColumnDefinition? PART_RightPaddingColumn; + StackPanel? PART_ButtonHolder; public TitleBar() { @@ -74,6 +76,7 @@ protected override void OnApplyTemplate() { PART_LeftPaddingColumn = GetTemplateChild(nameof(PART_LeftPaddingColumn)) as ColumnDefinition; PART_RightPaddingColumn = GetTemplateChild(nameof(PART_RightPaddingColumn)) as ColumnDefinition; + ConfigureButtonHolder(); Configure(); if (GetTemplateChild(PartBackButton) is Button backButton) { @@ -97,7 +100,12 @@ protected override void OnApplyTemplate() private void TitleBar_SizeChanged(object sender, SizeChangedEventArgs e) { - if (e.NewSize.Width <= CompactStateBreakpoint) + UpdateVisualStateAndDragRegion(e.NewSize); + } + + private void UpdateVisualStateAndDragRegion(Windows.Foundation.Size size) + { + if (size.Width <= CompactStateBreakpoint) { if (Content != null || Footer != null) { @@ -109,7 +117,7 @@ private void TitleBar_SizeChanged(object sender, SizeChangedEventArgs e) VisualStateManager.GoToState(this, WideState, true); } -#if WINDOWS_WINAPPSDK +#if WINAPPSDK SetDragRegionForCustomTitleBar(); #endif } @@ -124,12 +132,34 @@ private void PaneButton_Click(object sender, RoutedEventArgs e) PaneButtonClick?.Invoke(this, new RoutedEventArgs()); } + private void ConfigureButtonHolder() + { + if (PART_ButtonHolder != null) + { + PART_ButtonHolder.SizeChanged -= PART_ButtonHolder_SizeChanged; + } + + PART_ButtonHolder = GetTemplateChild(nameof(PART_ButtonHolder)) as StackPanel; + + if(PART_ButtonHolder != null) + { + PART_ButtonHolder.SizeChanged += PART_ButtonHolder_SizeChanged; + } + } + + private void PART_ButtonHolder_SizeChanged(object sender, SizeChangedEventArgs e) + { +#if WINAPPSDK + SetDragRegionForCustomTitleBar(); +#endif + } + private void Configure() { #if WINDOWS_UWP && !HAS_UNO SetUWPTitleBar(); #endif -#if WINDOWS_WINAPPSDK +#if WINAPPSDK SetWASDKTitleBar(); #endif } @@ -139,7 +169,7 @@ public void Reset() #if WINDOWS_UWP && !HAS_UNO ResetUWPTitleBar(); #endif -#if WINDOWS_WINAPPSDK +#if WINAPPSDK ResetWASDKTitleBar(); #endif } @@ -185,7 +215,7 @@ private void Update() VisualStateManager.GoToState(this, FooterCollapsedState, true); } -#if WINDOWS_WINAPPSDK +#if WINAPPSDK SetDragRegionForCustomTitleBar(); #endif } diff --git a/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.xaml b/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.xaml index 820dcc25..ce7942e8 100644 --- a/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.xaml +++ b/CommunityToolkit.App.Shared/Controls/TitleBar/TitleBar.xaml @@ -19,7 +19,7 @@ @@ -165,7 +165,7 @@ Grid.Column="2" Grid.ColumnSpan="6" Background="Transparent" /> -