diff --git a/.nuget/README.md b/.nuget/README.md index 6c4806a..3fb2961 100644 --- a/.nuget/README.md +++ b/.nuget/README.md @@ -1,5 +1,12 @@ DesktopFlyouts is a WinUI library for showing desktop flyouts from tray icons or programmatically. +## UWP desktop host + +The UWP package uses XamlHostingKit for System XAML initialization and message dispatch. Start the +application with `XamlIslandApplication.Start`, and create every additional flyout on its own XAML +thread with `XamlIslandApplication.CreateWindow`. See the getting-started guide below for the full +entry-point example. + ## Usage ### DesktopFlyout diff --git a/AGENTS.md b/AGENTS.md index 32983cb..b39a500 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,12 +19,8 @@ DesktopFlyouts is a WinUI library for showing lightweight desktop flyouts, menu └───samples ├───DesktopFlyouts.Wasdk.Sample.App │ └───DesktopFlyouts.Wasdk.Sample.App.csproj - ├───DesktopFlyouts.Uwp.Sample.App + └───DesktopFlyouts.Uwp.Sample.App │ └───DesktopFlyouts.Uwp.Sample.App.csproj - ├───DesktopFlyouts.Uwp.Sample.Packaging - │ └───DesktopFlyouts.Uwp.Sample.Packaging.wapproj - └───DesktopFlyouts.Uwp.Sample.TrayHost - └───DesktopFlyouts.Uwp.Sample.TrayHost.csproj ``` Most runtime behavior lives in `src/DesktopFlyouts.Shared` behind `#if WASDK`, `#if UWP`, and `#if HAS_UNO`. Keep all branches building when changing shared files. @@ -55,10 +51,10 @@ dotnet msbuild /restore:false src/DesktopFlyouts.Uno/DesktopFlyouts.Uno.csproj / dotnet msbuild /restore:false samples\DesktopFlyouts.Wasdk.Sample.App\DesktopFlyouts.Wasdk.Sample.App.csproj /p:Configuration=Debug /p:Platform=x64 /p:AppxBundle=Never ``` -3. Build the UWP library (Windows only): +3. Build the UWP sample and library (Windows only): ```powershell -dotnet msbuild /restore:false src\DesktopFlyouts.Uwp\DesktopFlyouts.Uwp.csproj /p:Configuration=Debug /p:Platform=x64 /p:AppxBundle=Never +dotnet msbuild /restore:false samples\DesktopFlyouts.Uwp.Sample.App\DesktopFlyouts.Uwp.Sample.App.csproj /p:Configuration=Debug /p:Platform=x64 /p:AppxBundle=Never ``` 4. Check whitespace and line endings: @@ -118,7 +114,7 @@ Validate one item at a time: 1. `git status --short --branch` to understand the current branch and dirty files. 2. Build the Uno library. 3. Build the WASDK sample (Windows only). -4. Build the UWP library when shared code changed (Windows only). +4. Build the UWP sample and library (Windows only). 5. Launch the WASDK sample as packaged, never by direct `.exe` (Windows only). 6. Confirm the app has a responsive process and real window handle (Windows only). 7. Run `git diff --check`. diff --git a/DesktopFlyouts.slnx b/DesktopFlyouts.slnx index d04044e..51d2006 100644 --- a/DesktopFlyouts.slnx +++ b/DesktopFlyouts.slnx @@ -24,15 +24,6 @@ - - - - - - - - - diff --git a/Directory.Packages.props b/Directory.Packages.props index 4824bf8..1bdccb9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,8 +18,9 @@ - + + diff --git a/README.md b/README.md index c2b7e1b..211a70b 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ The UWP version of sample app is currently under development. Recommend to use W > dotnet add package DesktopFlyouts.Uwp ``` +UWP desktop hosts initialize System XAML and its message loop through XamlHostingKit. See the +[UWP startup instructions](docs/getting-started.md#start-a-uwp-desktop-host); each additional +flyout is created on its own XAML thread with `XamlIslandApplication.CreateWindow`. + ### WinUI (WinAppSDK/WinUI3) NuGet badge diff --git a/docs/focus-and-activation.md b/docs/focus-and-activation.md index ab41e0e..563800d 100644 --- a/docs/focus-and-activation.md +++ b/docs/focus-and-activation.md @@ -48,4 +48,7 @@ Set it to `False` for sticky flyouts. ## UWP message loop integration -UWP desktop-host scenarios expose `TryPreTranslateMessage` on `DesktopFlyout` and `DesktopMenuFlyout`. Call it from the native message loop so keyboard navigation and accelerators can reach the hosted XAML island. +Start UWP desktop hosts with `XamlIslandApplication.Start`. XamlHostingKit owns the CoreWindow +message loop and performs XAML message translation, so application code must not run a second +native message loop. `TryPreTranslateMessage` remains available for source compatibility and +returns `False` with the XamlHostingKit host. diff --git a/docs/getting-started.md b/docs/getting-started.md index b0a8600..6e09e74 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -10,6 +10,46 @@ dotnet add package 0x5BFA.DesktopFlyouts.WinUI --prerelease dotnet add package 0x5BFA.DesktopFlyouts.Uwp --prerelease ``` +### Start a UWP desktop host + +The UWP package uses XamlHostingKit to own System XAML initialization and the desktop message loop. +Disable the generated XAML entry point and start the application through `XamlIslandApplication`: + +```xml +$(DefineConstants);DISABLE_XAML_GENERATED_MAIN +``` + +```csharp +using DesktopFlyouts.Shared; + +[STAThread] +static void Main() +{ + XamlIslandApplication.Start(_ => new App()); +} +``` + +Create the first `DesktopFlyout` or `DesktopMenuFlyout` on the main XAML thread. System XAML +allows one top-level window per thread, so create every additional flyout in its own callback: + +```csharp +private DesktopFlyout? _flyout; +private DesktopMenuFlyout? _menu; + +protected override void OnLaunched(LaunchActivatedEventArgs args) +{ + _flyout = new MainDesktopFlyout(); + + XamlIslandApplication.CreateWindow(_ => + { + _menu = new MainDesktopMenuFlyout(); + }); +} +``` + +XamlHostingKit dispatches each callback on the matching XAML thread. Dispatch later menu or +flyout operations back to the instance's `Dispatcher` when the caller runs on another thread. + Add the DesktopFlyouts namespace to XAML. ```xml diff --git a/samples/DesktopFlyouts.Uwp.Sample.App/App.xaml b/samples/DesktopFlyouts.Uwp.Sample.App/App.xaml new file mode 100644 index 0000000..9163926 --- /dev/null +++ b/samples/DesktopFlyouts.Uwp.Sample.App/App.xaml @@ -0,0 +1,6 @@ + + + diff --git a/samples/DesktopFlyouts.Uwp.Sample.App/App.xaml.cs b/samples/DesktopFlyouts.Uwp.Sample.App/App.xaml.cs new file mode 100644 index 0000000..2a9776f --- /dev/null +++ b/samples/DesktopFlyouts.Uwp.Sample.App/App.xaml.cs @@ -0,0 +1,87 @@ +// Copyright (c) 0x5BFA. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.IO; +using DesktopFlyouts.Shared; +using Windows.ApplicationModel.Activation; +using Windows.UI.Core; +using Windows.UI.Xaml; + +namespace DesktopFlyouts +{ + public partial class App : Application + { + private static SystemTrayIcon? _systemTrayIcon; + private static DesktopFlyout? _desktopFlyout; + private static volatile DesktopMenuFlyout? _desktopMenuFlyout; + + public App() + { + InitializeComponent(); + } + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + _systemTrayIcon = new( + Path.Combine(AppContext.BaseDirectory, "Tray.ico"), + "DesktopFlyouts sample app (UWP)", + new("022F5158-F05A-4FE1-B356-34F14B363625")); + + _systemTrayIcon.LeftClicked += SystemTrayIcon_LeftClicked; + _systemTrayIcon.RightClicked += SystemTrayIcon_RightClicked; + _systemTrayIcon.Show(); + + // The first flyout uses XamlHostingKit's main XamlWindow. + _desktopFlyout = new CustomizableFlyout + { + // XamlHostingKit deactivates its CoreWindow during the first host activation. + HideOnLostFocus = false, + }; + + // System XAML permits one top-level window per thread. The menu therefore uses + // another XamlHostingKit window and is constructed on that window's XAML thread. + XamlIslandApplication.CreateWindow(_ => + { + _desktopMenuFlyout = new MainDesktopMenuFlyout(); + }); + } + + internal static void ExitApplication() + { + var dispatcher = _desktopFlyout?.Dispatcher; + if (dispatcher is not null) + _ = dispatcher.RunAsync(CoreDispatcherPriority.Normal, Current.Exit); + } + + private static void SystemTrayIcon_LeftClicked(object? sender, MouseEventReceivedEventArgs e) + { + var flyout = _desktopFlyout; + if (flyout is null) + return; + + _ = flyout.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + if (flyout.IsOpen) + flyout.Hide(); + else + flyout.Show(); + }); + } + + private static void SystemTrayIcon_RightClicked(object? sender, MouseEventReceivedEventArgs e) + { + var menuFlyout = _desktopMenuFlyout; + if (menuFlyout is null) + return; + + _ = menuFlyout.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + if (menuFlyout.IsOpen) + menuFlyout.Hide(); + + menuFlyout.Show(new(e.Point.X, e.Point.Y - 32)); + }); + } + } +} diff --git a/samples/DesktopFlyouts.Uwp.Sample.App/App2.xaml b/samples/DesktopFlyouts.Uwp.Sample.App/App2.xaml deleted file mode 100644 index bc8ecf5..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.App/App2.xaml +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/samples/DesktopFlyouts.Uwp.Sample.App/App2.xaml.cs b/samples/DesktopFlyouts.Uwp.Sample.App/App2.xaml.cs deleted file mode 100644 index 5cc0681..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.App/App2.xaml.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 0x5BFA. All rights reserved. -// Licensed under the MIT license. - -using System; -using Windows.ApplicationModel; -using Windows.ApplicationModel.Activation; -using Windows.ApplicationModel.Core; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Navigation; -using WinRT; - -namespace DesktopFlyouts -{ - public sealed partial class App2 : Application - { - public App2() - { - InitializeComponent(); - - Suspending += OnSuspending; - } - - protected override void OnActivated(IActivatedEventArgs args) - { - // Do not repeat app initialization when the Window already has content, - // just ensure that the window is active. - if (Window.Current.Content is not Frame rootFrame) - { - // Create a Frame to act as the navigation context and navigate to the first page - rootFrame = new Frame(); - rootFrame.NavigationFailed += OnNavigationFailed; - - if (args.PreviousExecutionState == ApplicationExecutionState.Terminated) - { - // TODO: Load state from previously suspended application - } - - // Place the frame in the current Window - Window.Current.Content = rootFrame; - } - - if (rootFrame.Content == null) - { - CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true; - - rootFrame.Navigate(typeof(MainPage), args.As().Data); - } - - // Ensure the current window is active - Window.Current.Activate(); - } - - private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) - { - throw new Exception($"Failed to load page '{e.SourcePageType.FullName}'."); - } - - private void OnSuspending(object sender, SuspendingEventArgs e) - { - SuspendingDeferral deferral = e.SuspendingOperation.GetDeferral(); - - // TODO: Save application state and stop any background activity - deferral.Complete(); - } - } -} diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-100.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-100.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-100.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-100.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-125.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-125.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-125.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-125.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-150.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-150.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-150.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-150.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-200.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-200.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-200.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-200.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-400.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-400.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LargeTile.scale-400.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/LargeTile.scale-400.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LockScreenLogo.scale-200.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/LockScreenLogo.scale-200.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/LockScreenLogo.scale-200.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/LockScreenLogo.scale-200.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-100.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-100.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-100.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-100.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-125.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-125.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-125.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-125.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-150.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-150.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-150.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-150.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-200.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-200.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-200.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-200.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-400.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-400.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SmallTile.scale-400.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SmallTile.scale-400.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-100.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-100.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-100.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-100.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-125.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-125.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-125.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-125.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-150.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-150.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-150.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-150.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-200.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-200.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-200.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-200.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-400.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-400.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/SplashScreen.scale-400.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/SplashScreen.scale-400.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-100.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-100.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-100.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-100.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-125.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-125.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-125.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-125.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-150.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-150.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-150.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-150.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-200.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-200.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-200.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-200.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-400.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-400.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square150x150Logo.scale-400.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square150x150Logo.scale-400.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-unplated_targetsize-16.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-unplated_targetsize-16.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-unplated_targetsize-16.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-unplated_targetsize-256.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-unplated_targetsize-256.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-unplated_targetsize-256.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-unplated_targetsize-32.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-unplated_targetsize-32.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-unplated_targetsize-32.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-unplated_targetsize-48.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.altform-unplated_targetsize-48.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.altform-unplated_targetsize-48.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-100.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-100.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-100.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-100.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-125.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-125.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-125.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-125.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-150.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-150.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-150.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-150.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-200.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-200.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-200.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-200.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-400.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-400.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.scale-400.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.scale-400.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-16.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-16.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-16.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-16.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-24.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-24.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-24.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-24.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-24_altform-unplated.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-24_altform-unplated.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-24_altform-unplated.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-256.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-256.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-256.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-256.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-32.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-32.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-32.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-32.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-48.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-48.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Square44x44Logo.targetsize-48.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Square44x44Logo.targetsize-48.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-100.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-100.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-100.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-100.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-125.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-125.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-125.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-125.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-150.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-150.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-150.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-150.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-200.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-200.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-200.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-200.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-400.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-400.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/StoreLogo.scale-400.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/StoreLogo.scale-400.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-100.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-100.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-100.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-100.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-125.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-125.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-125.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-125.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-150.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-150.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-150.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-150.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-200.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-200.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-200.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-200.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-400.png b/samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-400.png similarity index 100% rename from samples/DesktopFlyouts.Uwp.Sample.Packaging/Assets/Wide310x150Logo.scale-400.png rename to samples/DesktopFlyouts.Uwp.Sample.App/Assets/Wide310x150Logo.scale-400.png diff --git a/samples/DesktopFlyouts.Uwp.Sample.App/DesktopFlyouts.Uwp.Sample.App.csproj b/samples/DesktopFlyouts.Uwp.Sample.App/DesktopFlyouts.Uwp.Sample.App.csproj index eae07fe..504161a 100644 --- a/samples/DesktopFlyouts.Uwp.Sample.App/DesktopFlyouts.Uwp.Sample.App.csproj +++ b/samples/DesktopFlyouts.Uwp.Sample.App/DesktopFlyouts.Uwp.Sample.App.csproj @@ -1,30 +1,39 @@ - - - - - WinExe - net10.0-windows$(TargetWindowsVersion) - 10.0.17763.0 - win-$(Platform).pubxml - app.manifest - true - - true - false - - - - - - - - - - - - - - - + + + + + WinExe + net10.0-windows$(TargetWindowsVersion) + 10.0.17763.0 + win-$(Platform).pubxml + app.manifest + true + + $(DefineConstants);DISABLE_XAML_GENERATED_MAIN + + true + true + false + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/DesktopFlyouts.Uwp.Sample.App/MainPage.xaml b/samples/DesktopFlyouts.Uwp.Sample.App/MainPage.xaml deleted file mode 100644 index 1eb3d9e..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.App/MainPage.xaml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - + + + + + + + diff --git a/samples/DesktopFlyouts.Uwp.Sample.App/Views/CustomizableFlyout.xaml.cs b/samples/DesktopFlyouts.Uwp.Sample.App/Views/CustomizableFlyout.xaml.cs new file mode 100644 index 0000000..9249b60 --- /dev/null +++ b/samples/DesktopFlyouts.Uwp.Sample.App/Views/CustomizableFlyout.xaml.cs @@ -0,0 +1,30 @@ +// Copyright (c) 0x5BFA. All rights reserved. +// Licensed under the MIT license. + +using Windows.UI.Xaml; + +namespace DesktopFlyouts +{ + public sealed partial class CustomizableFlyout : DesktopFlyout + { + public CustomizableFlyout() + { + InitializeComponent(); + } + + private void CollapseFirstIslandButton_Click(object sender, RoutedEventArgs e) + { + FirstIsland.Visibility = Visibility.Collapsed; + } + + private void CollapseSecondIslandButton_Click(object sender, RoutedEventArgs e) + { + SecondIsland.Visibility = Visibility.Collapsed; + } + + private void CollapseThirdIslandButton_Click(object sender, RoutedEventArgs e) + { + ThirdIsland.Visibility = Visibility.Collapsed; + } + } +} diff --git a/samples/DesktopFlyouts.Uwp.Sample.App/Views/MainDesktopMenuFlyout.xaml b/samples/DesktopFlyouts.Uwp.Sample.App/Views/MainDesktopMenuFlyout.xaml new file mode 100644 index 0000000..18127de --- /dev/null +++ b/samples/DesktopFlyouts.Uwp.Sample.App/Views/MainDesktopMenuFlyout.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopMenuFlyout.xaml.cs b/samples/DesktopFlyouts.Uwp.Sample.App/Views/MainDesktopMenuFlyout.xaml.cs similarity index 60% rename from samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopMenuFlyout.xaml.cs rename to samples/DesktopFlyouts.Uwp.Sample.App/Views/MainDesktopMenuFlyout.xaml.cs index 988477a..eaac94e 100644 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopMenuFlyout.xaml.cs +++ b/samples/DesktopFlyouts.Uwp.Sample.App/Views/MainDesktopMenuFlyout.xaml.cs @@ -1,9 +1,7 @@ // Copyright (c) 0x5BFA. All rights reserved. // Licensed under the MIT license. -using System; - -using Windows.System; +using Windows.UI.Xaml; namespace DesktopFlyouts { @@ -14,9 +12,9 @@ public MainDesktopMenuFlyout() InitializeComponent(); } - private async void OpenSecondaryApp(object sender, Windows.UI.Xaml.RoutedEventArgs e) + private void Exit_Click(object sender, RoutedEventArgs e) { - await Launcher.LaunchUriAsync(new Uri("tif-secondaryapp:")); + App.ExitApplication(); } } } diff --git a/samples/DesktopFlyouts.Uwp.Sample.Packaging/DesktopFlyouts.Uwp.Sample.Packaging.wapproj b/samples/DesktopFlyouts.Uwp.Sample.Packaging/DesktopFlyouts.Uwp.Sample.Packaging.wapproj deleted file mode 100644 index 33552e4..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.Packaging/DesktopFlyouts.Uwp.Sample.Packaging.wapproj +++ /dev/null @@ -1,65 +0,0 @@ - - - - 15.0 - - - - Debug - x86 - - - Release - x86 - - - Debug - x64 - - - Release - x64 - - - Debug - ARM64 - - - Release - ARM64 - - - - $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ - - - - 8f9f53a2-0cda-4bb2-b6e9-060ce608dfb2 - 10.0.26100.0 - 10.0.17763.0 - en-US - false - $(NoWarn);NU1702 - ..\DesktopFlyouts.Uwp.Sample.TrayHost\DesktopFlyouts.Uwp.Sample.TrayHost.csproj - - - - Designer - - - - - - - - - - - - True - True - Properties\PublishProfiles\win-$(Platform).pubxml - - - - diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/App.xaml b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/App.xaml deleted file mode 100644 index 960e994..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/App.xaml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - #09FFFFFF - #15FFFFFF - #18000000 - - - #40FFFFFF - #0F000000 - #10000000 - - - #09FFFFFF - #15FFFFFF - #18000000 - - - - - - - - diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/App.xaml.cs b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/App.xaml.cs deleted file mode 100644 index 7f74b1b..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/App.xaml.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 0x5BFA. All rights reserved. -// Licensed under the MIT license. - -using System; -using System.Threading; -using Windows.System; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Hosting; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.System.WinRT; -using Windows.Win32.UI.WindowsAndMessaging; - -namespace DesktopFlyouts -{ - public partial class App : Application - { - private static SystemTrayIcon? _systemTrayIcon; - private static DesktopFlyout? _desktopFlyout; - private static DesktopMenuFlyout? _desktopMenuFlyout; - - public unsafe App() - { - PInvoke.RoInitialize(RO_INIT_TYPE.RO_INIT_SINGLETHREADED); - - _systemTrayIcon = new( - "Tray.ico", - "DesktopFlyouts sample app (UWP)", - new("022F5158-F05A-4FE1-B356-34F14B363625")); - - _systemTrayIcon.LeftClicked += SystemTrayIcon_LeftClicked; - _systemTrayIcon.RightClicked += SystemTrayIcon_RightClicked; - _systemTrayIcon.Show(); - - // Initialize XAML Island - WindowsXamlManager.InitializeForCurrentThread(); - SynchronizationContext.SetSynchronizationContext(new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread())); - - // Initialize XAML flyouts - _desktopFlyout = new MainDesktopFlyout(); - _desktopMenuFlyout = new MainDesktopMenuFlyout(); - - MSG msg; - while (PInvoke.GetMessage(&msg, HWND.Null, 0U, 0U)) - { - if (!TryPreTranslateMessage(&msg)) - { - PInvoke.TranslateMessage(&msg); - PInvoke.DispatchMessage(&msg); - } - } - } - - private static unsafe bool TryPreTranslateMessage(MSG* msg) - { - return (_desktopFlyout?.TryPreTranslateMessage(msg) ?? false) || - (_desktopMenuFlyout?.TryPreTranslateMessage(msg) ?? false); - } - - private static void SystemTrayIcon_LeftClicked(object? sender, MouseEventReceivedEventArgs e) - { - if (_desktopFlyout is null) - return; - - if (_desktopFlyout.IsOpen) - _desktopFlyout.Hide(); - else - _desktopFlyout.Show(); - } - - private static void SystemTrayIcon_RightClicked(object? sender, MouseEventReceivedEventArgs e) - { - if (_desktopMenuFlyout is null) - return; - - if (_desktopMenuFlyout.IsOpen) - _desktopMenuFlyout.Hide(); - - _desktopMenuFlyout.Show(new(e.Point.X, e.Point.Y - 32)); - } - } -} diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/DesktopFlyouts.Uwp.Sample.TrayHost.csproj b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/DesktopFlyouts.Uwp.Sample.TrayHost.csproj deleted file mode 100644 index ce9021a..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/DesktopFlyouts.Uwp.Sample.TrayHost.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - - WinExe - net10.0-windows$(TargetWindowsVersion) - 10.0.17763.0 - win-$(Platform).pubxml - app.manifest - true - - $(DefineConstants);DISABLE_XAML_GENERATED_MAIN - - true - false - - - - - - - - - - - - - - - - - - diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopFlyout.xaml b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopFlyout.xaml deleted file mode 100644 index 4a04cab..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopFlyout.xaml +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopFlyout.xaml.cs b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopFlyout.xaml.cs deleted file mode 100644 index 2e56d10..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopFlyout.xaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 0x5BFA. All rights reserved. -// Licensed under the MIT license. - -namespace DesktopFlyouts -{ - public sealed partial class MainDesktopFlyout : DesktopFlyout - { - public MainDesktopFlyout() - { - InitializeComponent(); - } - } -} diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopMenuFlyout.xaml b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopMenuFlyout.xaml deleted file mode 100644 index b15804f..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/MainDesktopMenuFlyout.xaml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/PublishProfiles/win-arm64.pubxml b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/PublishProfiles/win-arm64.pubxml deleted file mode 100644 index 3481de2..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/PublishProfiles/win-arm64.pubxml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - FileSystem - ARM64 - win-arm64 - bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ - true - - \ No newline at end of file diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/PublishProfiles/win-x64.pubxml b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/PublishProfiles/win-x64.pubxml deleted file mode 100644 index 4463ecc..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/PublishProfiles/win-x64.pubxml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - FileSystem - x64 - win-x64 - bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ - true - - \ No newline at end of file diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/PublishProfiles/win-x86.pubxml b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/PublishProfiles/win-x86.pubxml deleted file mode 100644 index 31c51d6..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/PublishProfiles/win-x86.pubxml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - FileSystem - x86 - win-x86 - bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ - true - - \ No newline at end of file diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/launchSettings.json b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/launchSettings.json deleted file mode 100644 index 669b6ad..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/Properties/launchSettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "profiles": { - "DesktopFlyouts.Uwp.Sample.TrayHost": { - "commandName": "MsixPackage" - } - } -} \ No newline at end of file diff --git a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/app.manifest b/samples/DesktopFlyouts.Uwp.Sample.TrayHost/app.manifest deleted file mode 100644 index 2f30ef5..0000000 --- a/samples/DesktopFlyouts.Uwp.Sample.TrayHost/app.manifest +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - true - PerMonitorV2 - true - - - - \ No newline at end of file diff --git a/src/DesktopFlyouts.Shared/DesktopFlyout.cs b/src/DesktopFlyouts.Shared/DesktopFlyout.cs index 48a5a76..8844c6b 100644 --- a/src/DesktopFlyouts.Shared/DesktopFlyout.cs +++ b/src/DesktopFlyouts.Shared/DesktopFlyout.cs @@ -70,6 +70,9 @@ public partial class DesktopFlyout : Control, IDisposable private bool _isFocusManagerGettingFocusSubscribed; private bool _isUpdatingOpenFlyoutLayout; private bool _disposed; +#if UWP + private bool _showPending; +#endif private Grid? RootGrid; private ItemsControl? IslandsItemsControl; @@ -85,6 +88,9 @@ public DesktopFlyout() DefaultStyleKey = typeof(DesktopFlyout); _host = new XamlIslandHostWindow(); +#if UWP + _host.ContentAttached += Host_ContentAttached; +#endif _host.SetContent(this); _ = _host.UpdateWindowVisibility(false); #if HAS_UNO @@ -147,7 +153,22 @@ protected override void OnApplyTemplate() /// public void Show() { - if (_disposed || _host?.DesktopWindowXamlSource is null || RootGrid is null || _isPopupAnimationPlaying) + Show(false); + } + + private void Show(bool preserveCustomPlacementWhilePending) + { +#if UWP + if (!_disposed && _host is not null && !_host.IsContentReady) + { + _showPending = true; + if (!preserveCustomPlacementWhilePending) + _customPlacementBottomCenterPoint = null; + + return; + } +#endif + if (_disposed || _host?.IsInitialized is not true || RootGrid is null || _isPopupAnimationPlaying) { _customPlacementBottomCenterPoint = null; return; @@ -222,7 +243,7 @@ public void Show(Point bottomCenterPoint) return; _customPlacementBottomCenterPoint = bottomCenterPoint; - Show(); + Show(true); } /// @@ -234,6 +255,13 @@ public void Show(Point bottomCenterPoint) /// public void Hide() { +#if UWP + if (_showPending) + { + _showPending = false; + _customPlacementBottomCenterPoint = null; + } +#endif Hide(false); } @@ -297,8 +325,8 @@ public void NavigateFocus(XamlSourceFocusNavigationReason reason = XamlSourceFoc /// The native message to process. /// if the message was handled; otherwise, . /// - /// UWP desktop-host scenarios should call this from their native message loop so keyboard - /// navigation and accelerator processing can reach the hosted XAML island. + /// XamlHostingKit owns the UWP desktop message loop and performs XAML message translation. + /// This method remains available for source compatibility and returns . /// public unsafe bool TryPreTranslateMessage(MSG* msg) { @@ -346,7 +374,7 @@ private DesktopFlyoutPopupDirection UpdateFlyoutRegion( FoundationRect? resizeAnchorRegion = null, DesktopFlyoutPopupDirection resizePopupDirection = DesktopFlyoutPopupDirection.Vertical) { - if (_host?.DesktopWindowXamlSource is null || IslandsItemsControl is null) + if (_host?.IsInitialized is not true || IslandsItemsControl is null) return ResolvePopupDirection(PopupDirection, default, WindowHelpers.GetFlyoutWorkAreaRect(_customPlacementBottomCenterPoint)); var customBottomCenterPoint = _customPlacementBottomCenterPoint; @@ -419,7 +447,7 @@ private void UpdateOpenFlyoutLayout() _isPopupAnimationPlaying || _isUpdatingOpenFlyoutLayout || RootGrid is null || - _host?.DesktopWindowXamlSource is null) + _host?.IsInitialized is not true) { return; } @@ -1318,6 +1346,17 @@ private void HostWindow_SystemSettingsChanged(object? sender, EventArgs e) UpdateIslandBackdrops(); } +#if UWP + private void Host_ContentAttached(object? sender, EventArgs e) + { + if (_disposed || !_showPending) + return; + + _showPending = false; + Show(true); + } +#endif + /// public void Dispose() { @@ -1331,6 +1370,9 @@ public void Dispose() _host?.WindowInactivated -= HostWindow_Inactivated; _host?.SystemSettingsChanged -= HostWindow_SystemSettingsChanged; +#if UWP + _host?.ContentAttached -= Host_ContentAttached; +#endif RootGrid?.GettingFocus -= RootGrid_GettingFocus; RootGrid?.PointerPressed -= RootGrid_PointerPressed; RootGrid?.PointerMoved -= RootGrid_PointerMoved; diff --git a/src/DesktopFlyouts.Shared/DesktopFlyouts.Shared.projitems b/src/DesktopFlyouts.Shared/DesktopFlyouts.Shared.projitems index 2aecb33..466344e 100644 --- a/src/DesktopFlyouts.Shared/DesktopFlyouts.Shared.projitems +++ b/src/DesktopFlyouts.Shared/DesktopFlyouts.Shared.projitems @@ -41,6 +41,7 @@ + diff --git a/src/DesktopFlyouts.Shared/DesktopMenuFlyout.cs b/src/DesktopFlyouts.Shared/DesktopMenuFlyout.cs index 2f830aa..bcda84c 100644 --- a/src/DesktopFlyouts.Shared/DesktopMenuFlyout.cs +++ b/src/DesktopFlyouts.Shared/DesktopMenuFlyout.cs @@ -7,6 +7,7 @@ #if UWP +using Windows.Graphics; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Markup; @@ -36,6 +37,9 @@ public partial class DesktopMenuFlyout : ItemsControl, IDisposable private readonly XamlIslandHostWindow? _host; private MenuFlyout? _menuFlyout; private bool _disposed; +#if UWP + private Point? _pendingShowPoint; +#endif private Border? MenuFlyoutTargetControl; @@ -57,6 +61,9 @@ public DesktopMenuFlyout() DefaultStyleKey = typeof(DesktopMenuFlyout); _host = new XamlIslandHostWindow(); +#if UWP + _host.ContentAttached += Host_ContentAttached; +#endif _host.SetContent(this); _ = _host.UpdateWindowVisibility(false); _host.SystemSettingsChanged += HostWindow_SystemSettingsChanged; @@ -101,7 +108,14 @@ protected override void OnItemsChanged(object e) /// public void Show(Point point) { - if (_disposed || _menuFlyout is null) +#if UWP + if (!_disposed && _host is not null && !_host.IsContentReady) + { + _pendingShowPoint = point; + return; + } +#endif + if (_disposed || _host?.IsInitialized is not true || _menuFlyout is null) return; UpdateFlyoutTheme(); @@ -110,6 +124,14 @@ public void Show(Point point) _host?.SetHWndRectRegion(new RectInt32() { Width = 1, Height = 1 }); _ = _host?.UpdateWindowVisibility(true); + ApplyTemplate(); + UpdateLayout(); + if (MenuFlyoutTargetControl?.XamlRoot is null) + { + _ = _host?.UpdateWindowVisibility(false); + return; + } + _menuFlyout.ShowAt(MenuFlyoutTargetControl); IsOpen = true; @@ -123,6 +145,9 @@ public void Show(Point point) /// public void Hide() { +#if UWP + _pendingShowPoint = null; +#endif if (_disposed) return; @@ -147,6 +172,17 @@ private void HostWindow_SystemSettingsChanged(object? sender, EventArgs e) UpdateFlyoutTheme(); } +#if UWP + private void Host_ContentAttached(object? sender, EventArgs e) + { + if (_disposed || _pendingShowPoint is not Point point) + return; + + _pendingShowPoint = null; + Show(point); + } +#endif + #if UWP /// /// Lets the XAML island process a native keyboard message before dispatch. @@ -154,8 +190,8 @@ private void HostWindow_SystemSettingsChanged(object? sender, EventArgs e) /// The native message to process. /// if the message was handled; otherwise, . /// - /// UWP desktop-host scenarios should call this from their native message loop so keyboard - /// navigation and accelerator processing can reach the hosted XAML island. + /// XamlHostingKit owns the UWP desktop message loop and performs XAML message translation. + /// This method remains available for source compatibility and returns . /// public unsafe bool TryPreTranslateMessage(MSG* msg) { @@ -187,6 +223,10 @@ public void Dispose() } _host?.SystemSettingsChanged -= HostWindow_SystemSettingsChanged; +#if UWP + _host?.ContentAttached -= Host_ContentAttached; + _pendingShowPoint = null; +#endif _host?.Dispose(); IsOpen = false; diff --git a/src/DesktopFlyouts.Shared/XamlIslandApplication.cs b/src/DesktopFlyouts.Shared/XamlIslandApplication.cs index 0e51709..f4ffb31 100644 --- a/src/DesktopFlyouts.Shared/XamlIslandApplication.cs +++ b/src/DesktopFlyouts.Shared/XamlIslandApplication.cs @@ -3,7 +3,9 @@ // Licensed under the MIT license. #if UWP +using System; using Windows.UI.Xaml; +using XamlHostingKit; #endif namespace DesktopFlyouts.Shared @@ -18,6 +20,32 @@ namespace DesktopFlyouts.Shared /// public partial class XamlIslandApplication : Application { + /// + /// Starts System XAML through XamlHostingKit and runs its desktop message loop. + /// + /// Creates the application's instance. + public new static void Start(ApplicationInitializationCallback initializationCallback) + { + ArgumentNullException.ThrowIfNull(initializationCallback); + + XamlApplication.Start(initializationCallback); + } + + /// + /// Creates another XamlHostingKit window and invokes a callback on its XAML thread. + /// + /// + /// Creates one or for the new window. + /// + /// + /// System XAML permits one top-level XAML window per thread. Create each additional + /// DesktopFlyouts host inside a separate callback supplied to this method. + /// + public static void CreateWindow(ApplicationInitializationCallback initializationCallback) + { + ArgumentNullException.ThrowIfNull(initializationCallback); + XamlApplication.CreateWindow(new WindowCreationOptions(), initializationCallback); + } } #endif } diff --git a/src/DesktopFlyouts.Shared/XamlIslandHostWindow.Uwp.cs b/src/DesktopFlyouts.Shared/XamlIslandHostWindow.Uwp.cs new file mode 100644 index 0000000..6e65a49 --- /dev/null +++ b/src/DesktopFlyouts.Shared/XamlIslandHostWindow.Uwp.cs @@ -0,0 +1,763 @@ +#if UWP +// Copyright (c) 0x5BFA. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Threading; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Graphics; +using Windows.Graphics.Display; +using Windows.UI.Core; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Hosting; +using Windows.UI.Xaml.Input; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.System.Com; +using Windows.Win32.System.WinRT; +using Windows.Win32.UI.WindowsAndMessaging; +using WinRT; +using XamlHostingKit; + +namespace DesktopFlyouts +{ + internal sealed class CoreDispatcherSynchronizationContext : SynchronizationContext + { + private readonly CoreDispatcher _dispatcher; + + internal CoreDispatcherSynchronizationContext(CoreDispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + public override void Post(SendOrPostCallback callback, object? state) + { + ArgumentNullException.ThrowIfNull(callback); + _ = _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => callback(state)); + } + + public override void Send(SendOrPostCallback callback, object? state) + { + ArgumentNullException.ThrowIfNull(callback); + + if (!_dispatcher.HasThreadAccess) + throw new NotSupportedException("Cross-thread synchronous dispatch is not supported."); + + callback(state); + } + + public override SynchronizationContext CreateCopy() + { + return new CoreDispatcherSynchronizationContext(_dispatcher); + } + } + + internal unsafe partial class XamlIslandHostWindow : IDisposable + { + private static readonly object s_cbtHookTargetsLock = new(); + private static readonly Dictionary> s_cbtHookTargetsByThread = []; + private static readonly object s_claimedWindowsLock = new(); + private static readonly HashSet s_claimedWindows = []; + + private readonly WNDPROC _xamlWndProc; + private readonly UISettings _uiSettings = new(); + private readonly Dictionary _subclassedXamlWndProcs = []; + + private XamlWindow? _xamlWindow; + private CoreWindow? _coreWindow; + private UIElement? _content; + private Grid? _contentRoot; + private HWND _xamlHwnd = default; + private HHOOK _cbtHook = default; + private uint _cbtHookThreadId; + private HWND _preservedForegroundHWnd = default; + private HWND _preservedActiveHWnd = default; + private HWND _preservedFocusHWnd = default; + private DispatcherTimer? _contentReadyFallbackTimer; + private bool _isContentReady; + private bool _isContentReadyQueued; + private bool _hasSeenInitialDeactivation; + private bool _disposed; + private DesktopFlyoutActivationMode _activationMode = DesktopFlyoutActivationMode.Activate; + + internal HWND HWnd { get; private set; } + + internal bool IsInitialized => _xamlWindow is not null; + internal bool IsContentReady => !_disposed && _isContentReady; + + internal Rect WindowSize + { + get + { + RECT rect; + PInvoke.GetWindowRect(HWnd, &rect); + return new(rect.X, rect.Y, rect.Width, rect.Height); + } + } + + internal double XamlIslandRasterizationScale + { + get + { + try + { + return DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel; + } + catch + { + return 1.0D; + } + } + } + + internal event EventHandler? WindowInactivated; + internal event EventHandler? SystemSettingsChanged; + internal event EventHandler? ContentAttached; + + internal XamlIslandHostWindow() + { + _xamlWndProc = new(XamlWndProc); + + _xamlWindow = XamlWindow.Current; + if (_xamlWindow is null) + { + throw new InvalidOperationException( + "UWP DesktopFlyouts must be created inside a XamlHostingKit XamlWindow. " + + "Start the app with XamlIslandApplication.Start and create additional flyouts " + + "inside XamlIslandApplication.CreateWindow callbacks."); + } + + if (_xamlWindow.Content is not null) + { + throw new InvalidOperationException( + "A XamlHostingKit XamlWindow can host only one DesktopFlyout or DesktopMenuFlyout instance."); + } + + _xamlWindow.Title = "DesktopFlyoutHostWindow"; + _xamlWindow.Styles = WindowStyles.Popup; + _xamlWindow.ExtendedStyles = + WindowExtendedStyles.NoRedirectionBitmap | + WindowExtendedStyles.ToolWindow | + WindowExtendedStyles.Topmost; + + HWnd = (HWND)unchecked((nint)_xamlWindow.WindowHandle.Value); + if (HWnd.IsNull) + throw new InvalidOperationException("XamlHostingKit did not provide a host window handle."); + + lock (s_claimedWindowsLock) + { + if (!s_claimedWindows.Add((nint)HWnd.Value)) + { + throw new InvalidOperationException( + "A XamlHostingKit XamlWindow can host only one DesktopFlyout or DesktopMenuFlyout instance."); + } + } + + try + { + _coreWindow = _xamlWindow.CoreWindow; + SynchronizationContext.SetSynchronizationContext( + new CoreDispatcherSynchronizationContext(_coreWindow.Dispatcher)); + InitializeCoreWindowHandle(); + + _coreWindow.Activated += CoreWindow_Activated; + _uiSettings.ColorValuesChanged += UISettings_ColorValuesChanged; + + ApplyActivationModeToWindows(); + } + catch + { + lock (s_claimedWindowsLock) + s_claimedWindows.Remove((nint)HWnd.Value); + + throw; + } + } + + internal void SetContent(UIElement content) + { + if (_disposed || _xamlWindow is null) + return; + + _isContentReady = false; + _isContentReadyQueued = false; + StopContentReadyFallbackTimer(); + _content = content; + var dispatcher = _coreWindow?.Dispatcher; + if (dispatcher is null) + throw new InvalidOperationException("XamlHostingKit did not provide a CoreDispatcher."); + + // A derived control calls InitializeComponent after its base DesktopFlyout constructor. + // Attach it on the next dispatcher turn so XAML parsing completes before Window.Content + // starts loading templates and resources. + _ = dispatcher.RunAsync(CoreDispatcherPriority.High, () => + { + if (!_disposed && _xamlWindow is not null && ReferenceEquals(_content, content)) + { + _contentRoot = new Grid(); + var desktopFlyoutsResources = new DesktopFlyoutResources(); + _contentRoot.Resources.MergedDictionaries.Add(desktopFlyoutsResources); + + if (content is Control control) + { + control.Style = content switch + { + DesktopFlyout => desktopFlyoutsResources.FlyoutStyle, + DesktopMenuFlyout => desktopFlyoutsResources.MenuFlyoutStyle, + _ => throw new InvalidOperationException($"Unsupported flyout type: {content.GetType().Name}."), + }; + } + + _contentRoot.Children.Add(content); + _xamlWindow.Content = _contentRoot; + + var wasVisible = PInvoke.IsWindowVisible(HWnd); + if (!wasVisible) + { + PInvoke.SetWindowPos( + HWnd, + HWND.Null, + -32000, + -32000, + 1, + 1, + SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + PInvoke.ShowWindow(HWnd, SHOW_WINDOW_CMD.SW_SHOWNOACTIVATE); + } + + if (content is Control templatedControl) + { + templatedControl.ApplyTemplate(); + templatedControl.UpdateLayout(); + } + + if (!wasVisible) + PInvoke.ShowWindow(HWnd, SHOW_WINDOW_CMD.SW_HIDE); + + ApplyActivationModeToWindows(); + if (wasVisible || _hasSeenInitialDeactivation) + QueueContentReady(); + else + StartContentReadyFallbackTimer(); + } + }); + } + + private void StartContentReadyFallbackTimer() + { + StopContentReadyFallbackTimer(); + + _contentReadyFallbackTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(100), + }; + _contentReadyFallbackTimer.Tick += ContentReadyFallbackTimer_Tick; + _contentReadyFallbackTimer.Start(); + } + + private void ContentReadyFallbackTimer_Tick(object? sender, object e) + { + StopContentReadyFallbackTimer(); + QueueContentReady(); + } + + private void StopContentReadyFallbackTimer() + { + if (_contentReadyFallbackTimer is null) + return; + + _contentReadyFallbackTimer.Stop(); + _contentReadyFallbackTimer.Tick -= ContentReadyFallbackTimer_Tick; + _contentReadyFallbackTimer = null; + } + + private void QueueContentReady() + { + var dispatcher = _coreWindow?.Dispatcher; + if (_disposed || dispatcher is null || _isContentReady || _isContentReadyQueued) + return; + + StopContentReadyFallbackTimer(); + _isContentReadyQueued = true; + _ = dispatcher.RunAsync(CoreDispatcherPriority.Low, () => + { + _isContentReadyQueued = false; + if (_disposed || _xamlWindow is null || _content is null) + return; + + _isContentReady = _contentRoot is not null && _content.XamlRoot is not null; + if (_isContentReady) + ContentAttached?.Invoke(this, EventArgs.Empty); + }); + } + + internal void PreserveActivationState() + { + if (_disposed) + return; + + _preservedForegroundHWnd = PInvoke.GetForegroundWindow(); + _preservedActiveHWnd = PInvoke.GetActiveWindow(); + _preservedFocusHWnd = PInvoke.GetFocus(); + } + + internal void RestoreActivationState() + { + if (_disposed) + return; + + if (!_preservedForegroundHWnd.IsNull) + PInvoke.SetForegroundWindow(_preservedForegroundHWnd); + + if (!_preservedActiveHWnd.IsNull) + PInvoke.SetActiveWindow(_preservedActiveHWnd); + + if (!_preservedFocusHWnd.IsNull) + PInvoke.SetFocus(_preservedFocusHWnd); + } + + internal void MoveAndResize(RectInt32 rect, bool activate = true) + { + if (_disposed) + return; + + var flags = activate ? 0 : SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE; + PInvoke.SetWindowPos(HWnd, HWND.HWND_TOP, rect.X, rect.Y, rect.Width, rect.Height, flags); + } + + internal void Maximize(System.Drawing.Rectangle workArea, bool activate = true) + { + if (_disposed) + return; + + var flags = activate ? 0 : SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE; + PInvoke.SetWindowPos(HWnd, HWND.HWND_TOP, workArea.X, workArea.Y, workArea.Width, workArea.Height, flags); + } + + internal void SetHWndRectRegion(RectInt32 rect) + { + if (_disposed) + return; + + SetWindowRectRegion(HWnd, rect); + SetWindowRectRegion(_xamlHwnd, rect); + } + + private static void SetWindowRectRegion(HWND hWnd, RectInt32 rect) + { + if (hWnd.IsNull) + return; + + HRGN region = PInvoke.CreateRectRgn(rect.X, rect.Y, rect.X + rect.Width, rect.Y + rect.Height); + if (region.IsNull) + return; + + if (PInvoke.SetWindowRgn(hWnd, region, false) == 0) + PInvoke.DeleteObject(region); + } + + internal ValueTask UpdateWindowVisibility(bool isVisible, bool activate = true) + { + if (_disposed) + return default; + + var command = isVisible + ? activate ? SHOW_WINDOW_CMD.SW_SHOW : SHOW_WINDOW_CMD.SW_SHOWNOACTIVATE + : SHOW_WINDOW_CMD.SW_HIDE; + + PInvoke.ShowWindow(HWnd, command); + + if (isVisible) + ApplyActivationModeToWindows(); + + return default; + } + + internal void SetActivationMode(DesktopFlyoutActivationMode activationMode) + { + if (_disposed) + return; + + _activationMode = activationMode; + ApplyActivationModeToWindows(); + } + + internal bool NavigateFocus(XamlSourceFocusNavigationReason reason = XamlSourceFocusNavigationReason.Programmatic) + { + if (_disposed || !_isContentReady || _xamlHwnd.IsNull || _content is null || _activationMode is DesktopFlyoutActivationMode.NeverActivate) + return false; + + PInvoke.SetFocus(_xamlHwnd); + + var direction = reason switch + { + XamlSourceFocusNavigationReason.Left => FocusNavigationDirection.Left, + XamlSourceFocusNavigationReason.Up => FocusNavigationDirection.Up, + XamlSourceFocusNavigationReason.Right => FocusNavigationDirection.Right, + XamlSourceFocusNavigationReason.Down => FocusNavigationDirection.Down, + _ => FocusNavigationDirection.None, + }; + + if (direction is not FocusNavigationDirection.None && FocusManager.TryMoveFocus(direction)) + return true; + + var focusTarget = reason switch + { + XamlSourceFocusNavigationReason.Last or + XamlSourceFocusNavigationReason.Left or + XamlSourceFocusNavigationReason.Up => FocusManager.FindLastFocusableElement(_content), + _ => FocusManager.FindFirstFocusableElement(_content), + }; + + var focusState = reason is XamlSourceFocusNavigationReason.Programmatic or XamlSourceFocusNavigationReason.Restore + ? FocusState.Programmatic + : FocusState.Keyboard; + + return focusTarget is Control control && control.Focus(focusState); + } + + public bool TryPreTranslateMessage(MSG* msg) + { + // XamlHostingKit owns the CoreWindow message loop and performs XAML message translation. + return false; + } + + private void InitializeCoreWindowHandle() + { + if (_coreWindow is null) + throw new InvalidOperationException("XamlHostingKit did not provide a CoreWindow."); + + void* ppv; + ((IUnknown*)((IWinRTObject)_coreWindow).NativeObject.ThisPtr)->QueryInterface( + (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in IID.IID_ICoreWindowInterop)), &ppv); + + var wrappers = new StrategyBasedComWrappers(); + var interop = (ICoreWindowInterop)wrappers.GetOrCreateObjectForComInstance( + (nint)ppv, + CreateObjectFlags.None); + interop.get_WindowHandle((HWND*)Unsafe.AsPointer(ref _xamlHwnd)); + } + + private void ApplyActivationModeToWindows() + { + var neverActivate = _activationMode is DesktopFlyoutActivationMode.NeverActivate; + UpdateCbtHook(neverActivate); + if (neverActivate) + RefreshXamlWindowSubclasses(); + + SetNoActivateStyle(HWnd, neverActivate); + SetNoActivateStyle(_xamlHwnd, neverActivate); + + foreach (var hWnd in _subclassedXamlWndProcs.Keys) + SetNoActivateStyle((HWND)hWnd, neverActivate); + + if (!neverActivate) + UnsubclassXamlWindows(); + } + + private static void SetNoActivateStyle(HWND hWnd, bool enabled) + { + if (hWnd.IsNull) + return; + + var exStyle = (WINDOW_EX_STYLE)PInvoke.GetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + exStyle = enabled + ? exStyle | WINDOW_EX_STYLE.WS_EX_NOACTIVATE + : exStyle & ~WINDOW_EX_STYLE.WS_EX_NOACTIVATE; + + PInvoke.SetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)exStyle); + PInvoke.SetWindowPos( + hWnd, + HWND.Null, + 0, + 0, + 0, + 0, + SET_WINDOW_POS_FLAGS.SWP_NOMOVE | + SET_WINDOW_POS_FLAGS.SWP_NOSIZE | + SET_WINDOW_POS_FLAGS.SWP_NOZORDER | + SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | + SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED); + } + + private void UpdateCbtHook(bool enabled) + { + if (enabled) + { + EnsureCbtHook(); + return; + } + + RemoveCbtHook(); + } + + private void EnsureCbtHook() + { + if (_cbtHook != HHOOK.Null) + return; + + _cbtHookThreadId = PInvoke.GetCurrentThreadId(); + _cbtHook = PInvoke.SetWindowsHookEx( + WINDOWS_HOOK_ID.WH_CBT, + &CbtHookProc, + HINSTANCE.Null, + _cbtHookThreadId); + + if (_cbtHook != HHOOK.Null) + RegisterCbtHookTarget(_cbtHookThreadId); + } + + private void RemoveCbtHook() + { + if (_cbtHook == HHOOK.Null) + return; + + PInvoke.UnhookWindowsHookEx(_cbtHook); + _cbtHook = default; + UnregisterCbtHookTarget(_cbtHookThreadId); + _cbtHookThreadId = 0; + } + + private void RegisterCbtHookTarget(uint threadId) + { + lock (s_cbtHookTargetsLock) + { + if (!s_cbtHookTargetsByThread.TryGetValue(threadId, out var targets)) + { + targets = []; + s_cbtHookTargetsByThread[threadId] = targets; + } + + if (!targets.Contains(this)) + targets.Add(this); + } + } + + private void UnregisterCbtHookTarget(uint threadId) + { + if (threadId == 0) + return; + + lock (s_cbtHookTargetsLock) + { + if (!s_cbtHookTargetsByThread.TryGetValue(threadId, out var targets)) + return; + + targets.Remove(this); + if (targets.Count == 0) + s_cbtHookTargetsByThread.Remove(threadId); + } + } + + private static XamlIslandHostWindow[] GetCbtHookTargets(uint threadId) + { + lock (s_cbtHookTargetsLock) + { + return s_cbtHookTargetsByThread.TryGetValue(threadId, out var targets) + ? [.. targets] + : []; + } + } + + private void RefreshXamlWindowSubclasses() + { + if (_xamlHwnd.IsNull) + return; + + SubclassXamlWindow(_xamlHwnd); + SubclassChildWindows(HWnd); + SubclassChildWindows(_xamlHwnd); + } + + private void SubclassChildWindows(HWND parentHWnd) + { + for (var childHWnd = PInvoke.GetWindow(parentHWnd, GET_WINDOW_CMD.GW_CHILD); + !childHWnd.IsNull; + childHWnd = PInvoke.GetWindow(childHWnd, GET_WINDOW_CMD.GW_HWNDNEXT)) + { + SubclassXamlWindow(childHWnd); + SubclassChildWindows(childHWnd); + } + } + + private void SubclassXamlWindow(HWND hWnd) + { + if (hWnd.IsNull || _subclassedXamlWndProcs.ContainsKey((nint)hWnd.Value)) + return; + + var previousWndProc = (nint)PInvoke.SetWindowLongPtr( + hWnd, + WINDOW_LONG_PTR_INDEX.GWLP_WNDPROC, + Marshal.GetFunctionPointerForDelegate(_xamlWndProc)); + + _subclassedXamlWndProcs[(nint)hWnd.Value] = previousWndProc; + } + + private void UnsubclassXamlWindows() + { + foreach (var item in _subclassedXamlWndProcs) + { + var hWnd = (HWND)item.Key; + if (!hWnd.IsNull) + PInvoke.SetWindowLongPtr(hWnd, WINDOW_LONG_PTR_INDEX.GWLP_WNDPROC, item.Value); + } + + _subclassedXamlWndProcs.Clear(); + } + + private LRESULT XamlWndProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + switch (uMsg) + { + case PInvoke.WM_MOUSEACTIVATE: + if (_activationMode is DesktopFlyoutActivationMode.NeverActivate) + { + RestoreActivationState(); + return (LRESULT)(int)PInvoke.MA_NOACTIVATE; + } + break; + case PInvoke.WM_SETFOCUS: + if (_activationMode is DesktopFlyoutActivationMode.NeverActivate) + { + RestoreActivationState(); + return (LRESULT)0; + } + break; + } + + return CallPreviousXamlWndProc(hWnd, uMsg, wParam, lParam); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])] + private static LRESULT CbtHookProc(int code, WPARAM wParam, LPARAM lParam) + { + if (code >= 0 && (code == PInvoke.HCBT_ACTIVATE || code == PInvoke.HCBT_SETFOCUS)) + { + var targetHWnd = (HWND)(nint)(nuint)wParam; + foreach (var target in GetCbtHookTargets(PInvoke.GetCurrentThreadId())) + { + if (target._activationMode is DesktopFlyoutActivationMode.NeverActivate && target.IsFlyoutWindow(targetHWnd)) + { + target.RestoreActivationState(); + return (LRESULT)1; + } + } + } + + return PInvoke.CallNextHookEx(HHOOK.Null, code, wParam, lParam); + } + + private bool IsFlyoutWindow(HWND hWnd) + { + if (hWnd.IsNull) + return false; + + if (hWnd == HWnd || hWnd == _xamlHwnd) + return true; + + if (_subclassedXamlWndProcs.ContainsKey((nint)hWnd.Value)) + return true; + + if (!HWnd.IsNull && PInvoke.IsChild(HWnd, hWnd)) + return true; + + if (!_xamlHwnd.IsNull && PInvoke.IsChild(_xamlHwnd, hWnd)) + return true; + + var rootHWnd = PInvoke.GetAncestor(hWnd, GET_ANCESTOR_FLAGS.GA_ROOT); + if (rootHWnd == HWnd || rootHWnd == _xamlHwnd) + return true; + + var rootOwnerHWnd = PInvoke.GetAncestor(hWnd, GET_ANCESTOR_FLAGS.GA_ROOTOWNER); + return rootOwnerHWnd == HWnd || rootOwnerHWnd == _xamlHwnd; + } + + private LRESULT CallPreviousXamlWndProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + if (!_subclassedXamlWndProcs.TryGetValue((nint)hWnd.Value, out var previousWndProc) || previousWndProc == 0) + return PInvoke.DefWindowProc(hWnd, uMsg, wParam, lParam); + + return PInvoke.CallWindowProc( + (delegate* unmanaged[Stdcall])(void*)previousWndProc, + hWnd, + uMsg, + wParam, + lParam); + } + + private void CoreWindow_Activated(CoreWindow sender, WindowActivatedEventArgs args) + { + if (_disposed || args.WindowActivationState is not CoreWindowActivationState.Deactivated) + return; + + if (!_isContentReady) + { + _hasSeenInitialDeactivation = true; + if (_contentRoot is not null) + QueueContentReady(); + + return; + } + + WindowInactivated?.Invoke(this, EventArgs.Empty); + } + + private void UISettings_ColorValuesChanged(UISettings sender, object args) + { + var dispatcher = _coreWindow?.Dispatcher; + if (_disposed || dispatcher is null) + return; + + _ = dispatcher.TryRunAsync( + CoreDispatcherPriority.Normal, + () => SystemSettingsChanged?.Invoke(this, EventArgs.Empty)); + } + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + StopContentReadyFallbackTimer(); + RemoveCbtHook(); + UnsubclassXamlWindows(); + + _uiSettings.ColorValuesChanged -= UISettings_ColorValuesChanged; + if (_coreWindow is not null) + _coreWindow.Activated -= CoreWindow_Activated; + + if (_xamlWindow is not null) + _xamlWindow.Content = null; + + _contentRoot?.Children.Clear(); + _contentRoot = null; + _content = null; + _isContentReady = false; + _isContentReadyQueued = false; + _hasSeenInitialDeactivation = false; + + lock (s_claimedWindowsLock) + s_claimedWindows.Remove((nint)HWnd.Value); + + if (!HWnd.IsNull) + PInvoke.DestroyWindow(HWnd); + + _xamlWindow = null; + _coreWindow = null; + HWnd = default; + _xamlHwnd = default; + + GC.SuppressFinalize(this); + } + } +} +#endif diff --git a/src/DesktopFlyouts.Shared/XamlIslandHostWindow.X11.cs b/src/DesktopFlyouts.Shared/XamlIslandHostWindow.X11.cs index cbe4cd1..7642cd0 100644 --- a/src/DesktopFlyouts.Shared/XamlIslandHostWindow.X11.cs +++ b/src/DesktopFlyouts.Shared/XamlIslandHostWindow.X11.cs @@ -33,6 +33,8 @@ internal partial class XamlIslandHostWindow : IDisposable internal object? DesktopWindowXamlSource { get; private set; } + internal bool IsInitialized => DesktopWindowXamlSource is not null; + internal Rect WindowSize { get diff --git a/src/DesktopFlyouts.Shared/XamlIslandHostWindow.cs b/src/DesktopFlyouts.Shared/XamlIslandHostWindow.cs index 9a96af9..3578cf5 100644 --- a/src/DesktopFlyouts.Shared/XamlIslandHostWindow.cs +++ b/src/DesktopFlyouts.Shared/XamlIslandHostWindow.cs @@ -1,4 +1,4 @@ -#if !HAS_UNO +#if WASDK && !HAS_UNO // Copyright (c) 0x5BFA. All rights reserved. // Licensed under the MIT license. @@ -75,6 +75,8 @@ internal DesktopWindowXamlSource? DesktopWindowXamlSource private set; } + internal bool IsInitialized => DesktopWindowXamlSource is not null; + internal Rect WindowSize { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/DesktopFlyouts.Uwp/DesktopFlyout.xaml b/src/DesktopFlyouts.Uwp/DesktopFlyout.xaml index 5a14ebb..62e89e9 100644 --- a/src/DesktopFlyouts.Uwp/DesktopFlyout.xaml +++ b/src/DesktopFlyouts.Uwp/DesktopFlyout.xaml @@ -3,10 +3,22 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:DesktopFlyouts"> - +