diff --git a/src/Core/src/Handlers/Window/WindowHandler.Windows.cs b/src/Core/src/Handlers/Window/WindowHandler.Windows.cs index e40a69397e17..b57d6fbdf4fe 100644 --- a/src/Core/src/Handlers/Window/WindowHandler.Windows.cs +++ b/src/Core/src/Handlers/Window/WindowHandler.Windows.cs @@ -1,5 +1,6 @@ using System; using Microsoft.UI.Xaml.Controls; +using Microsoft.Maui.ApplicationModel; namespace Microsoft.Maui.Handlers { diff --git a/src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs b/src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs index d6814ea51f9c..ac1d618950b2 100644 --- a/src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs +++ b/src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs @@ -62,10 +62,6 @@ internal static MauiAppBuilder UseEssentials(this MauiAppBuilder builder) })); #elif WINDOWS life.AddWindows(windows => windows - .OnPlatformMessage((window, args) => - { - ApplicationModel.Platform.OnWindowMessage(args.Hwnd, args.MessageId, args.WParam, args.LParam); - }) .OnActivated((window, args) => { ApplicationModel.Platform.OnActivated(window, args); diff --git a/src/Core/src/Platform/Windows/MauiWinUIWindow.cs b/src/Core/src/Platform/Windows/MauiWinUIWindow.cs index aa4ec68c2b08..174fdf1398a9 100644 --- a/src/Core/src/Platform/Windows/MauiWinUIWindow.cs +++ b/src/Core/src/Platform/Windows/MauiWinUIWindow.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.Devices; using Microsoft.Maui.LifecycleEvents; using Microsoft.UI; @@ -10,11 +11,15 @@ namespace Microsoft.Maui { public class MauiWinUIWindow : UI.Xaml.Window { + readonly WindowMessageManager _windowManager; + IntPtr _windowIcon; bool _enableResumeEvent; public MauiWinUIWindow() { + _windowManager = WindowMessageManager.Get(this); + Activated += OnActivated; Closed += OnClosedPrivate; VisibilityChanged += OnVisibilityChanged; @@ -62,43 +67,26 @@ protected virtual void OnVisibilityChanged(object sender, UI.Xaml.WindowVisibili MauiWinUIApplication.Current.Services?.InvokeLifecycleEvents(del => del(this, args)); } - #region Platform Window - - IntPtr _hwnd = IntPtr.Zero; - - /// - /// Returns a pointer to the underlying platform window handle (hWnd). - /// - public IntPtr WindowHandle - { - get - { - if (_hwnd == IntPtr.Zero) - _hwnd = this.GetWindowHandle(); - return _hwnd; - } - } - - PlatformMethods.WindowProc? newWndProc = null; - IntPtr oldWndProc = IntPtr.Zero; + public IntPtr WindowHandle => _windowManager.WindowHandle; void SubClassingWin32() { MauiWinUIApplication.Current.Services?.InvokeLifecycleEvents( del => del(this, new WindowsPlatformWindowSubclassedEventArgs(WindowHandle))); - newWndProc = new PlatformMethods.WindowProc(NewWindowProc); - oldWndProc = PlatformMethods.SetWindowLongPtr(WindowHandle, PlatformMethods.WindowLongFlags.GWL_WNDPROC, newWndProc); + _windowManager.WindowMessage += OnWindowMessage; - IntPtr NewWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + void OnWindowMessage(object? sender, WindowMessageEventArgs e) { - if (msg == WindowsPlatformMessageIds.WM_SETTINGCHANGE || msg == WindowsPlatformMessageIds.WM_THEMECHANGE) + if (e.MessageId == PlatformMethods.MessageIds.WM_SETTINGCHANGE || + e.MessageId == PlatformMethods.MessageIds.WM_THEMECHANGE) + { MauiWinUIApplication.Current.Application?.ThemeChanged(); - - if (msg == WindowsPlatformMessageIds.WM_DPICHANGED) + } + else if (e.MessageId == PlatformMethods.MessageIds.WM_DPICHANGED) { - var dpiX = (short)(long)wParam; - var dpiY = (short)((long)wParam >> 16); + var dpiX = (short)(long)e.WParam; + var dpiY = (short)((long)e.WParam >> 16); var window = this.GetWindow(); if (window is not null) @@ -106,14 +94,10 @@ IntPtr NewWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) } MauiWinUIApplication.Current.Services?.InvokeLifecycleEvents( - m => m.Invoke(this, new WindowsPlatformMessageEventArgs(hWnd, msg, wParam, lParam))); - - return PlatformMethods.CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam); + m => m.Invoke(this, new WindowsPlatformMessageEventArgs(e.Hwnd, e.MessageId, e.WParam, e.LParam))); } } - #endregion - /// /// Default the Window Icon to the icon stored in the .exe, if any. /// diff --git a/src/Core/src/Platform/Windows/NavigationRootManager.cs b/src/Core/src/Platform/Windows/NavigationRootManager.cs index d5a658e39f63..85307bd148bc 100644 --- a/src/Core/src/Platform/Windows/NavigationRootManager.cs +++ b/src/Core/src/Platform/Windows/NavigationRootManager.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Maui.ApplicationModel; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; diff --git a/src/Core/src/Platform/Windows/WindowExtensions.cs b/src/Core/src/Platform/Windows/WindowExtensions.cs index f2073894d8fa..572b37501f82 100644 --- a/src/Core/src/Platform/Windows/WindowExtensions.cs +++ b/src/Core/src/Platform/Windows/WindowExtensions.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.Devices; using System.Threading.Tasks; using Microsoft.Maui.Media; diff --git a/src/Core/src/Platform/Windows/WindowsPlatformMessageIds.cs b/src/Core/src/Platform/Windows/WindowsPlatformMessageIds.cs deleted file mode 100644 index 6f2e36348d88..000000000000 --- a/src/Core/src/Platform/Windows/WindowsPlatformMessageIds.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.Maui.Platform -{ - internal static class WindowsPlatformMessageIds - { - public const int WM_DPICHANGED = 0x02E0; - public const int WM_DISPLAYCHANGE = 0x007E; - public const int WM_SETTINGCHANGE = 0x001A; - public const int WM_THEMECHANGE = 0x031A; - } -} \ No newline at end of file diff --git a/src/Essentials/src/AppInfo/AppInfo.uwp.cs b/src/Essentials/src/AppInfo/AppInfo.uwp.cs index f81b90962695..99e01f236278 100644 --- a/src/Essentials/src/AppInfo/AppInfo.uwp.cs +++ b/src/Essentials/src/AppInfo/AppInfo.uwp.cs @@ -1,13 +1,9 @@ using System; -using System.Globalization; -using Windows.ApplicationModel; using System.Diagnostics; +using System.Globalization; using System.Reflection; -#if WINDOWS using Microsoft.UI.Xaml; -#else -using Windows.UI.Xaml; -#endif +using Windows.ApplicationModel; namespace Microsoft.Maui.ApplicationModel { @@ -19,11 +15,13 @@ class AppInfoImplementation : IAppInfo ApplicationTheme? _applicationTheme; + readonly ActiveWindowTracker _activeWindowTracker; + public AppInfoImplementation() { - // TODO: NET7 use new public events - if (WindowStateManager.Default is WindowStateManagerImplementation impl) - impl.ActiveWindowThemeChanged += OnActiveWindowThemeChanged; + _activeWindowTracker = new(WindowStateManager.Default); + _activeWindowTracker.Start(); + _activeWindowTracker.WindowMessage += OnWindowMessage; if (MainThread.IsMainThread) OnActiveWindowThemeChanged(); @@ -78,7 +76,14 @@ public AppTheme RequestedTheme public LayoutDirection RequestedLayoutDirection => CultureInfo.CurrentCulture.TextInfo.IsRightToLeft ? LayoutDirection.RightToLeft : LayoutDirection.LeftToRight; - void OnActiveWindowThemeChanged(object sender = null, EventArgs e = null) + void OnWindowMessage(object sender, WindowMessageEventArgs e) + { + if (e.MessageId == PlatformMethods.MessageIds.WM_SETTINGCHANGE || + e.MessageId == PlatformMethods.MessageIds.WM_THEMECHANGE) + OnActiveWindowThemeChanged(); + } + + void OnActiveWindowThemeChanged() { if (Application.Current is Application app) _applicationTheme = app.RequestedTheme; diff --git a/src/Essentials/src/DeviceDisplay/DeviceDisplay.uwp.cs b/src/Essentials/src/DeviceDisplay/DeviceDisplay.uwp.cs index 6bdd06d9adcf..508fc5ce73e5 100644 --- a/src/Essentials/src/DeviceDisplay/DeviceDisplay.uwp.cs +++ b/src/Essentials/src/DeviceDisplay/DeviceDisplay.uwp.cs @@ -11,8 +11,16 @@ namespace Microsoft.Maui.Devices partial class DeviceDisplayImplementation { readonly object locker = new object(); + readonly ActiveWindowTracker _activeWindowTracker; + DisplayRequest? displayRequest; + public DeviceDisplayImplementation() + { + _activeWindowTracker = new(WindowStateManager.Default); + _activeWindowTracker.WindowMessage += OnWindowMessage; + } + protected override bool GetKeepScreenOn() { lock (locker) @@ -44,8 +52,6 @@ protected override void SetKeepScreenOn(bool keepScreenOn) } } - AppWindow? _currentAppWindowListeningTo; - protected override DisplayInfo GetMainDisplayInfo() { if (WindowStateManager.Default.GetActiveAppWindow(false) is not AppWindow appWindow) @@ -102,47 +108,22 @@ protected override DisplayInfo GetMainDisplayInfo() return null; } - protected override void StartScreenMetricsListeners() - { - MainThread.BeginInvokeOnMainThread(() => - { - WindowStateManager.Default.ActiveWindowDisplayChanged += OnWindowDisplayChanged; - WindowStateManager.Default.ActiveWindowChanged += OnCurrentWindowChanged; + protected override void StartScreenMetricsListeners() => + MainThread.BeginInvokeOnMainThread(_activeWindowTracker.Start); - _currentAppWindowListeningTo = WindowStateManager.Default.GetActiveAppWindow(true)!; - _currentAppWindowListeningTo.Changed += OnAppWindowChanged; - }); - } + protected override void StopScreenMetricsListeners() => + MainThread.BeginInvokeOnMainThread(_activeWindowTracker.Stop); - protected override void StopScreenMetricsListeners() + // Currently there isn't a way to detect Orientation Changes unless you subclass the WinUI.Window and watch the messages. + // This is the "subtlest" way to currently wire this together. + // Hopefully there will be a more public API for this down the road so we can just use that directly from Essentials + void OnWindowMessage(object? sender, WindowMessageEventArgs e) { - MainThread.BeginInvokeOnMainThread(() => - { - WindowStateManager.Default.ActiveWindowChanged -= OnCurrentWindowChanged; - WindowStateManager.Default.ActiveWindowDisplayChanged -= OnWindowDisplayChanged; - - if (_currentAppWindowListeningTo != null) - _currentAppWindowListeningTo.Changed -= OnAppWindowChanged; - - _currentAppWindowListeningTo = null; - }); - } - - void OnCurrentWindowChanged(object? sender, EventArgs e) - { - if (_currentAppWindowListeningTo != null) - _currentAppWindowListeningTo.Changed -= OnAppWindowChanged; - - _currentAppWindowListeningTo = WindowStateManager.Default.GetActiveAppWindow(true)!; - _currentAppWindowListeningTo.Changed += OnAppWindowChanged; + if (e.MessageId == PlatformMethods.MessageIds.WM_DISPLAYCHANGE || + e.MessageId == PlatformMethods.MessageIds.WM_DPICHANGED) + OnMainDisplayInfoChanged(); } - void OnWindowDisplayChanged(object? sender, EventArgs e) => - OnMainDisplayInfoChanged(); - - void OnAppWindowChanged(AppWindow sender, AppWindowChangedEventArgs args) => - OnMainDisplayInfoChanged(); - static DisplayRotation CalculateRotation(DisplayOrientations orientation) => orientation switch { diff --git a/src/Essentials/src/Platform/ActiveWindowTracker.uwp.cs b/src/Essentials/src/Platform/ActiveWindowTracker.uwp.cs new file mode 100644 index 000000000000..8bafa17572ad --- /dev/null +++ b/src/Essentials/src/Platform/ActiveWindowTracker.uwp.cs @@ -0,0 +1,58 @@ +#nullable enable +using System; + +namespace Microsoft.Maui.ApplicationModel +{ + class ActiveWindowTracker + { + readonly IWindowStateManager _windowStateManager; + + WindowMessageManager? _currentWindowManager; + + public ActiveWindowTracker(IWindowStateManager windowStateManager) + { + _windowStateManager = windowStateManager; + } + + public event EventHandler? WindowMessage; + + public void Start() + { + var window = _windowStateManager.GetActiveWindow(); + OnActiveWindowChanged(window); + + _windowStateManager.ActiveWindowChanged += OnActiveWindowChanged; + } + + public void Stop() + { + OnActiveWindowChanged(null); + + _windowStateManager.ActiveWindowChanged -= OnActiveWindowChanged; + } + + void OnActiveWindowChanged(object? sender, EventArgs e) + { + var window = _windowStateManager?.GetActiveWindow(); + OnActiveWindowChanged(window); + } + + void OnActiveWindowChanged(UI.Xaml.Window? window) + { + if (_currentWindowManager is not null) + { + _currentWindowManager.WindowMessage -= OnWindowMessage; + _currentWindowManager = null; + } + + if (window is not null) + { + _currentWindowManager = WindowMessageManager.Get(window); + _currentWindowManager.WindowMessage += OnWindowMessage; + } + } + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) => + WindowMessage?.Invoke(sender, e); + } +} diff --git a/src/Essentials/src/Platform/Platform.shared.cs b/src/Essentials/src/Platform/Platform.shared.cs index e25aae751190..ecdf3a90b549 100644 --- a/src/Essentials/src/Platform/Platform.shared.cs +++ b/src/Essentials/src/Platform/Platform.shared.cs @@ -82,9 +82,6 @@ public static void OnLaunched(UI.Xaml.LaunchActivatedEventArgs e) => public static void OnActivated(UI.Xaml.Window window, UI.Xaml.WindowActivatedEventArgs args) => WindowStateManager.Default.OnActivated(window, args); - public static void OnWindowMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) => - WindowStateManager.Default.OnWindowMessage(hWnd, msg, wParam, lParam); - #elif TIZEN public static Tizen.Applications.Package CurrentPackage { diff --git a/src/Core/src/Platform/Windows/PlatformMethods.cs b/src/Essentials/src/Platform/PlatformMethods.uwp.cs similarity index 80% rename from src/Core/src/Platform/Windows/PlatformMethods.cs rename to src/Essentials/src/Platform/PlatformMethods.uwp.cs index c65d7043a9ce..56f63e8aa5ae 100644 --- a/src/Core/src/Platform/Windows/PlatformMethods.cs +++ b/src/Essentials/src/Platform/PlatformMethods.uwp.cs @@ -1,11 +1,19 @@ -#nullable enable +#nullable enable using System; using System.Runtime.InteropServices; -namespace Microsoft.Maui.Platform +namespace Microsoft.Maui.ApplicationModel { static class PlatformMethods { + public static class MessageIds + { + public const int WM_DPICHANGED = 0x02E0; + public const int WM_DISPLAYCHANGE = 0x007E; + public const int WM_SETTINGCHANGE = 0x001A; + public const int WM_THEMECHANGE = 0x031A; + } + public delegate IntPtr WindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); public static IntPtr SetWindowLongPtr(IntPtr hWnd, WindowLongFlags nIndex, WindowProc dwNewLong) @@ -22,6 +30,20 @@ public static IntPtr SetWindowLongPtr(IntPtr hWnd, WindowLongFlags nIndex, Windo static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, WindowLongFlags nIndex, WindowProc dwNewLong); } + public static IntPtr SetWindowLongPtr(IntPtr hWnd, WindowLongFlags nIndex, IntPtr dwNewLong) + { + if (IntPtr.Size == 8) + return SetWindowLongPtr64(hWnd, nIndex, dwNewLong); + else + return new IntPtr(SetWindowLong32(hWnd, nIndex, dwNewLong)); + + [DllImport("user32.dll", EntryPoint = "SetWindowLong")] + static extern int SetWindowLong32(IntPtr hWnd, WindowLongFlags nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] + static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, WindowLongFlags nIndex, IntPtr dwNewLong); + } + public static IntPtr SetWindowLongPtr(IntPtr hWnd, WindowLongFlags nIndex, long dwNewLong) { if (IntPtr.Size == 8) @@ -51,15 +73,17 @@ public static long GetWindowLongPtr(IntPtr hWnd, WindowLongFlags nIndex) } [DllImport("user32.dll")] - public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int width, int height, SetWindowPosFlags uFlags); + [DllImport("comctl32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr DefSubclassProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); + [DllImport("user32.dll")] public static extern uint GetDpiForWindow(IntPtr hWnd); - [DllImport("User32", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); @@ -156,4 +180,4 @@ struct RECT public int Bottom; } } -} \ No newline at end of file +} diff --git a/src/Essentials/src/Platform/WindowMessageEventArgs.uwp.cs b/src/Essentials/src/Platform/WindowMessageEventArgs.uwp.cs new file mode 100644 index 000000000000..d817a4925c27 --- /dev/null +++ b/src/Essentials/src/Platform/WindowMessageEventArgs.uwp.cs @@ -0,0 +1,28 @@ +#nullable enable +using System; + +namespace Microsoft.Maui.ApplicationModel +{ + class WindowMessageEventArgs : EventArgs + { + public WindowMessageEventArgs(IntPtr hwnd, uint messageId, IntPtr wParam, IntPtr lParam) + { + Hwnd = hwnd; + MessageId = messageId; + WParam = wParam; + LParam = lParam; + } + + public IntPtr Hwnd { get; } + + public uint MessageId { get; } + + public IntPtr WParam { get; } + + public IntPtr LParam { get; } + + public IntPtr Result { get; set; } + + public bool Handled { get; set; } + } +} diff --git a/src/Essentials/src/Platform/WindowMessageManager.uwp.cs b/src/Essentials/src/Platform/WindowMessageManager.uwp.cs new file mode 100644 index 000000000000..f47b7d45284d --- /dev/null +++ b/src/Essentials/src/Platform/WindowMessageManager.uwp.cs @@ -0,0 +1,156 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.UI.Xaml; + +namespace Microsoft.Maui.ApplicationModel +{ + class WindowMessageManager : IDisposable + { + readonly static Dictionary> _managers = new(); + readonly static PlatformMethods.WindowProc _newWndProc = new(NewWindowProc); + + readonly object _locker = new(); + + IntPtr _windowHandle; + IntPtr _oldWndProc; + + bool _isDisposed; + + event EventHandler? WindowMessageInternal; + + WindowMessageManager(Window window) + { + _windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(window); + } + + public IntPtr WindowHandle => _windowHandle; + + public bool IsAttached => _oldWndProc != IntPtr.Zero; + + public static IEnumerable GetAll() + { + foreach (var weakManager in _managers.Values.ToArray()) + { + if (weakManager.TryGetTarget(out var manager)) + yield return manager; + } + } + + public event EventHandler WindowMessage + { + add + { + if (WindowMessageInternal is null) + Attach(); + + WindowMessageInternal += value; + } + remove + { + WindowMessageInternal -= value; + + if (WindowMessageInternal is null) + Detach(); + } + } + + void Attach() + { + lock (_locker) + { + if (_oldWndProc == IntPtr.Zero) + { + _oldWndProc = PlatformMethods.SetWindowLongPtr(_windowHandle, PlatformMethods.WindowLongFlags.GWL_WNDPROC, _newWndProc); + } + } + } + + void Detach() + { + lock (_locker) + { + if (_oldWndProc != IntPtr.Zero) + { + PlatformMethods.SetWindowLongPtr(_windowHandle, PlatformMethods.WindowLongFlags.GWL_WNDPROC, _oldWndProc); + _oldWndProc = IntPtr.Zero; + } + } + } + + static IntPtr NewWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam) + { + if (_managers.TryGetValue(hWnd, out var weakManager) && weakManager.TryGetTarget(out var manager)) + { + var evt = manager.WindowMessageInternal; + if (evt is not null) + { + var args = new WindowMessageEventArgs(hWnd, uMsg, wParam, lParam); + + evt.Invoke(manager, args); + + if (args.Handled) + return args.Result; + } + + return PlatformMethods.CallWindowProc(manager._oldWndProc, hWnd, uMsg, wParam, lParam); + } + + // this technically should never happen + return PlatformMethods.DefSubclassProc(hWnd, uMsg, wParam, lParam); + } + + public static WindowMessageManager Get(Window window) + { + var handle = WinRT.Interop.WindowNative.GetWindowHandle(window); + + if (_managers.TryGetValue(handle, out var weakManager) && + weakManager.TryGetTarget(out var manager) && + !manager._isDisposed) + return manager; + + var newManager = new WindowMessageManager(window); + + _managers[handle] = new WeakReference(newManager); + + return newManager; + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + // dispose managed state (managed objects) + + if (_managers.ContainsKey(_windowHandle)) + _managers.Remove(_windowHandle); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + + Detach(); + + // set large fields to null + + _windowHandle = IntPtr.Zero; + _oldWndProc = IntPtr.Zero; + + _isDisposed = true; + } + } + + ~WindowMessageManager() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Essentials/src/Platform/WindowStateManager.uwp.cs b/src/Essentials/src/Platform/WindowStateManager.uwp.cs index 74a454a16cd5..e6848ecab142 100644 --- a/src/Essentials/src/Platform/WindowStateManager.uwp.cs +++ b/src/Essentials/src/Platform/WindowStateManager.uwp.cs @@ -9,16 +9,9 @@ public interface IWindowStateManager { event EventHandler ActiveWindowChanged; - event EventHandler ActiveWindowDisplayChanged; - - // TODO: NET7 make this public - // event EventHandler ActiveWindowThemeChanged; - Window? GetActiveWindow(); void OnActivated(Window window, WindowActivatedEventArgs args); - - void OnWindowMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); } public static class WindowStateManager @@ -76,20 +69,10 @@ public static IntPtr GetActiveWindowHandle(this IWindowStateManager manager, boo class WindowStateManagerImplementation : IWindowStateManager { - const uint WM_DISPLAYCHANGE = 0x7E; - const uint WM_DPICHANGED = 0x02E0; - const uint WM_SETTINGCHANGE = 0x001A; - const uint WM_THEMECHANGE = 0x031A; - Window? _activeWindow; - IntPtr _activeWindowHandle; public event EventHandler? ActiveWindowChanged; - public event EventHandler? ActiveWindowDisplayChanged; - - public event EventHandler? ActiveWindowThemeChanged; - public Window? GetActiveWindow() => _activeWindow; @@ -99,9 +82,6 @@ void SetActiveWindow(Window window) return; _activeWindow = window; - _activeWindowHandle = window is null - ? IntPtr.Zero - : WinRT.Interop.WindowNative.GetWindowHandle(window); ActiveWindowChanged?.Invoke(window, EventArgs.Empty); } @@ -111,21 +91,5 @@ public void OnActivated(Window window, WindowActivatedEventArgs args) if (args.WindowActivationState != WindowActivationState.Deactivated) SetActiveWindow(window); } - - // Currently there isn't a way to detect Orientation Changes unless you subclass the WinUI.Window and watch the messages - // Maui.Core forwards these messages to here so that WinUI can react accordingly. - // This is the "subtlest" way to currently wire this together. - // Hopefully there will be a more public API for this down the road so we can just use that directly from Essentials - public void OnWindowMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - // only track events if they come from the active window - if (_activeWindow is null || hWnd != _activeWindowHandle) - return; - - if (msg == WM_SETTINGCHANGE || msg == WM_THEMECHANGE) - ActiveWindowThemeChanged?.Invoke(_activeWindow, EventArgs.Empty); - else if (msg == WM_DISPLAYCHANGE || msg == WM_DPICHANGED) - ActiveWindowDisplayChanged?.Invoke(_activeWindow, EventArgs.Empty); - } } } diff --git a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 47042d23f092..d593804388c9 100644 --- a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1,4 +1,7 @@ #nullable enable +*REMOVED*Microsoft.Maui.ApplicationModel.IWindowStateManager.ActiveWindowDisplayChanged -> System.EventHandler! +*REMOVED*Microsoft.Maui.ApplicationModel.IWindowStateManager.OnWindowMessage(System.IntPtr hWnd, uint msg, System.IntPtr wParam, System.IntPtr lParam) -> void +*REMOVED*static Microsoft.Maui.ApplicationModel.Platform.OnWindowMessage(System.IntPtr hWnd, uint msg, System.IntPtr wParam, System.IntPtr lParam) -> void override Microsoft.Maui.Devices.Sensors.Location.GetHashCode() -> int ~override Microsoft.Maui.Devices.Sensors.Location.Equals(object obj) -> bool ~static Microsoft.Maui.Devices.Sensors.Location.operator !=(Microsoft.Maui.Devices.Sensors.Location left, Microsoft.Maui.Devices.Sensors.Location right) -> bool diff --git a/src/Essentials/test/DeviceTests/Tests/Windows/ActiveWindowTracker_Tests.cs b/src/Essentials/test/DeviceTests/Tests/Windows/ActiveWindowTracker_Tests.cs new file mode 100644 index 000000000000..784f16955b71 --- /dev/null +++ b/src/Essentials/test/DeviceTests/Tests/Windows/ActiveWindowTracker_Tests.cs @@ -0,0 +1,163 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Maui.ApplicationModel; +using Microsoft.Maui.Platform; +using Xunit; + +namespace Microsoft.Maui.Essentials.DeviceTests.Shared +{ + [Category("Windows ActiveWindowTracker")] + public class Windows_ActiveWindowTracker_Tests : BaseWindowMessageManager_Tests + { + [Fact] + public Task StoppedTrackerDoesNotTrack() => + Utils.OnMainThread(async () => + { + var messages = new List(); + + var window = new UI.Xaml.Window(); + + var wsm = new TestWindowStateManager(); + var tracker = new ActiveWindowTracker(wsm); + tracker.WindowMessage += OnWindowMessage; + + wsm.OnActivated(window); + tracker.Start(); + tracker.Stop(); + + await PostTestMessageAsync(window); + + Assert.DoesNotContain(TEST_MESSAGE, messages); + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + messages.Add(e.MessageId); + } + }); + + [Fact] + public Task ActivatedBeforeStartRecievesMessage() => + Utils.OnMainThread(async () => + { + var messages = new List(); + + var window = new UI.Xaml.Window(); + + var wsm = new TestWindowStateManager(); + var tracker = new ActiveWindowTracker(wsm); + tracker.WindowMessage += OnWindowMessage; + + wsm.OnActivated(window); + tracker.Start(); + + await PostTestMessageAsync(window); + + tracker.Stop(); + + Assert.Contains(TEST_MESSAGE, messages); + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + messages.Add(e.MessageId); + } + }); + + [Fact] + public Task StartBeforeActivatedRecievesMessage() => + Utils.OnMainThread(async () => + { + var messages = new List(); + + var window = new UI.Xaml.Window(); + + var wsm = new TestWindowStateManager(); + wsm.OnActivated(window); + + var tracker = new ActiveWindowTracker(wsm); + tracker.Start(); + tracker.WindowMessage += OnWindowMessage; + + await PostTestMessageAsync(window); + + Assert.Contains(TEST_MESSAGE, messages); + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + messages.Add(e.MessageId); + } + }); + + [Fact] + public Task SwitchingWindowsPostsToTheNewWindow() => + Utils.OnMainThread(async () => + { + var messages = new List<(IntPtr, uint)>(); + + var window1 = new UI.Xaml.Window(); + var window2 = new UI.Xaml.Window(); + + var wsm = new TestWindowStateManager(); + var tracker = new ActiveWindowTracker(wsm); + tracker.Start(); + tracker.WindowMessage += OnWindowMessage; + + wsm.OnActivated(window1); + wsm.OnActivated(window2); + + await PostTestMessageAsync(window2); + + Assert.DoesNotContain((window1.GetWindowHandle(), TEST_MESSAGE), messages); + Assert.Contains((window2.GetWindowHandle(), TEST_MESSAGE), messages); + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + messages.Add((e.Hwnd, e.MessageId)); + } + }); + + [Fact] + public Task SwitchingWindowsDoesNotPostToTheOldWindow() => + Utils.OnMainThread(async () => + { + var messages = new List<(IntPtr, uint)>(); + + var window1 = new UI.Xaml.Window(); + var window2 = new UI.Xaml.Window(); + + var wsm = new TestWindowStateManager(); + var tracker = new ActiveWindowTracker(wsm); + tracker.Start(); + tracker.WindowMessage += OnWindowMessage; + + wsm.OnActivated(window1); + wsm.OnActivated(window2); + + await PostTestMessageAsync(window1); + + Assert.DoesNotContain((window1.GetWindowHandle(), TEST_MESSAGE), messages); + Assert.DoesNotContain((window2.GetWindowHandle(), TEST_MESSAGE), messages); + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + messages.Add((e.Hwnd, e.MessageId)); + } + }); + + class TestWindowStateManager : IWindowStateManager + { + UI.Xaml.Window? _window; + + public event EventHandler? ActiveWindowChanged; + + public UI.Xaml.Window? GetActiveWindow() => _window; + + public void OnActivated(UI.Xaml.Window window, UI.Xaml.WindowActivatedEventArgs? args = null) + { + _window = window; + ActiveWindowChanged?.Invoke(window, EventArgs.Empty); + } + } + } +} diff --git a/src/Essentials/test/DeviceTests/Tests/Windows/BaseWindowMessageManager_Tests.cs b/src/Essentials/test/DeviceTests/Tests/Windows/BaseWindowMessageManager_Tests.cs new file mode 100644 index 000000000000..1f78985b34fe --- /dev/null +++ b/src/Essentials/test/DeviceTests/Tests/Windows/BaseWindowMessageManager_Tests.cs @@ -0,0 +1,27 @@ +#nullable enable +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.Maui.Platform; + +namespace Microsoft.Maui.Essentials.DeviceTests.Shared +{ + public abstract class BaseWindowMessageManager_Tests + { + protected const uint WM_APP = 0x8000; + protected const uint TEST_MESSAGE = WM_APP + 1; + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + protected static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + protected async Task PostTestMessageAsync(UI.Xaml.Window window) + { + var handle = window.GetWindowHandle(); + + PostMessage(handle, TEST_MESSAGE, IntPtr.Zero, IntPtr.Zero); + + await Task.Delay(100); + } + } +} diff --git a/src/Essentials/test/DeviceTests/Tests/Windows/WindowMessageManager_Tests.cs b/src/Essentials/test/DeviceTests/Tests/Windows/WindowMessageManager_Tests.cs new file mode 100644 index 000000000000..8c327f7af987 --- /dev/null +++ b/src/Essentials/test/DeviceTests/Tests/Windows/WindowMessageManager_Tests.cs @@ -0,0 +1,216 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Maui.ApplicationModel; +using Microsoft.Maui.Platform; +using Xunit; + +namespace Microsoft.Maui.Essentials.DeviceTests.Shared +{ + [Category("Windows WindowMessageManager")] + public class Windows_WindowMessageManager_Tests : BaseWindowMessageManager_Tests + { + [Fact] + public Task NewWindowIsNotFound() => + Utils.OnMainThread(() => + { + var window = new UI.Xaml.Window(); + var handle = window.GetWindowHandle(); + + var allManagers = WindowMessageManager.GetAll().ToArray(); + var allHandles = allManagers.Select(m => m.WindowHandle).ToArray(); + + Assert.DoesNotContain(handle, allHandles); + }); + + [Fact] + public Task RegisteredWindowIsFound() => + Utils.OnMainThread(() => + { + var window = new UI.Xaml.Window(); + var handle = window.GetWindowHandle(); + + var manager = WindowMessageManager.Get(window); + + var allManagers = WindowMessageManager.GetAll().ToArray(); + var allHandles = allManagers.Select(m => m.WindowHandle).ToArray(); + + manager.Dispose(); + + Assert.Contains(handle, allHandles); + Assert.Contains(manager, allManagers); + }); + + [Fact] + public Task RegisteredWindowIsNotAttached() => + Utils.OnMainThread(() => + { + var window = new UI.Xaml.Window(); + var handle = window.GetWindowHandle(); + + var manager = WindowMessageManager.Get(window); + + var isAttached = manager.IsAttached; + + manager.Dispose(); + + Assert.False(isAttached); + }); + + [Fact] + public Task SubscribedWindowIsAttached() => + Utils.OnMainThread(() => + { + var window = new UI.Xaml.Window(); + var handle = window.GetWindowHandle(); + + var manager = WindowMessageManager.Get(window); + manager.WindowMessage += OnWindowMessage; + + var isAttached = manager.IsAttached; + + manager.Dispose(); + + Assert.True(isAttached); + + static void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + } + }); + + [Fact] + public Task UnsubscribedWindowIsNotAttached() => + Utils.OnMainThread(() => + { + var window = new UI.Xaml.Window(); + var handle = window.GetWindowHandle(); + + var manager = WindowMessageManager.Get(window); + manager.WindowMessage += OnWindowMessage; + manager.WindowMessage -= OnWindowMessage; + + var isAttached = manager.IsAttached; + + manager.Dispose(); + + Assert.False(isAttached); + + static void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + } + }); + + [Fact] + public Task DisposedManagerIsNotFound() => + Utils.OnMainThread(() => + { + var window = new UI.Xaml.Window(); + var handle = window.GetWindowHandle(); + + var manager = WindowMessageManager.Get(window); + + manager.Dispose(); + + var allManagers = WindowMessageManager.GetAll().ToArray(); + var allHandles = allManagers.Select(m => m.WindowHandle).ToArray(); + + Assert.DoesNotContain(handle, allHandles); + Assert.DoesNotContain(manager, allManagers); + }); + + [Fact] + public Task PostingMessagesReachesEvent() => + Utils.OnMainThread(async () => + { + var messages = new List(); + + var window = new UI.Xaml.Window(); + + var manager = WindowMessageManager.Get(window); + manager.WindowMessage += OnWindowMessage; + + await PostTestMessageAsync(window); + + manager.Dispose(); + + Assert.Contains(TEST_MESSAGE, messages); + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + messages.Add(e.MessageId); + } + }); + + [Fact] + public Task PostingMessagesToDisposedManagerDoesNotReachEvent() => + Utils.OnMainThread(async () => + { + var messages = new List(); + + var window = new UI.Xaml.Window(); + + var manager = WindowMessageManager.Get(window); + manager.WindowMessage += OnWindowMessage; + manager.Dispose(); + + await PostTestMessageAsync(window); + + Assert.DoesNotContain(TEST_MESSAGE, messages); + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + messages.Add(e.MessageId); + } + }); + + [Fact] + public Task PostingMessagesToUnsubscribedManagerDoesNotReachEvent() => + Utils.OnMainThread(async () => + { + var messages = new List(); + + var window = new UI.Xaml.Window(); + + using var manager = WindowMessageManager.Get(window); + manager.WindowMessage += OnWindowMessage; + manager.WindowMessage -= OnWindowMessage; + + await PostTestMessageAsync(window); + + manager.Dispose(); + + Assert.DoesNotContain(TEST_MESSAGE, messages); + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + messages.Add(e.MessageId); + } + }); + + [Fact] + public Task PostingMessagesToMultipleSubscribedManagerReachesEvent() => + Utils.OnMainThread(async () => + { + var messages = new List(); + + var window = new UI.Xaml.Window(); + + using var manager = WindowMessageManager.Get(window); + manager.WindowMessage += OnWindowMessage; + manager.WindowMessage += OnWindowMessage; + manager.WindowMessage -= OnWindowMessage; + + await PostTestMessageAsync(window); + + manager.Dispose(); + + Assert.Contains(TEST_MESSAGE, messages); + + void OnWindowMessage(object? sender, WindowMessageEventArgs e) + { + messages.Add(e.MessageId); + } + }); + } +} diff --git a/src/Essentials/test/DeviceTests/Utils.cs b/src/Essentials/test/DeviceTests/Utils.cs index ecf712244881..e1bdb812f08f 100644 --- a/src/Essentials/test/DeviceTests/Utils.cs +++ b/src/Essentials/test/DeviceTests/Utils.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Maui.ApplicationModel; namespace Microsoft.Maui.Essentials.DeviceTests { @@ -7,101 +8,10 @@ public class Utils { public static void Unused(params object[] obj) { } -#if WINDOWS_UWP || WINDOWS - public static async Task OnMainThread(global::Windows.UI.Core.DispatchedHandler action) - { - var mainView = global::Windows.ApplicationModel.Core.CoreApplication.MainView; - var normal = global::Windows.UI.Core.CoreDispatcherPriority.Normal; - await mainView.CoreWindow.Dispatcher.RunAsync(normal, action); - } + public static Task OnMainThread(Action action) => + MainThread.InvokeOnMainThreadAsync(() => action()); - public static Task OnMainThread(Func action) - { - var tcs = new TaskCompletionSource(); - var mainView = global::Windows.ApplicationModel.Core.CoreApplication.MainView; - var normal = global::Windows.UI.Core.CoreDispatcherPriority.Normal; -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - mainView.CoreWindow.Dispatcher.RunAsync(normal, async () => - { - try - { - await action(); - tcs.SetResult(true); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - }); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - return tcs.Task; - } -#elif __ANDROID__ - public static Task OnMainThread(Action action) - { - var tcs = new TaskCompletionSource(); - var looper = Android.OS.Looper.MainLooper; - var handler = new Android.OS.Handler(looper); - handler.Post(() => - { - try - { - action(); - tcs.SetResult(true); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - }); - return tcs.Task; - } - - public static Task OnMainThread(Func action) - { - var tcs = new TaskCompletionSource(); - var looper = Android.OS.Looper.MainLooper; - var handler = new Android.OS.Handler(looper); - handler.Post(async () => - { - try - { - await action(); - tcs.SetResult(true); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - }); - return tcs.Task; - } -#elif __IOS__ - public static Task OnMainThread(Action action) - { - var obj = new Foundation.NSObject(); - obj.InvokeOnMainThread(action); - return Task.FromResult(true); - } - - public static Task OnMainThread(Func action) - { - var tcs = new TaskCompletionSource(); - var obj = new Foundation.NSObject(); - obj.InvokeOnMainThread(async () => - { - try - { - await action(); - tcs.SetResult(true); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - }); - return tcs.Task; - } -#endif + public static Task OnMainThread(Func action) => + MainThread.InvokeOnMainThreadAsync(() => action()); } }