Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .nuget/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.275" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.3.1" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" Condition="'$(MSBuildProjectName)' == 'DesktopFlyouts.Wasdk' Or '$(MSBuildProjectName)' == 'DesktopFlyouts.Wasdk.Sample.App'" />
<PackageVersion Include="XamlHostingKit" Version="0.1.0" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.2" />
<PackageVersion Include="Tmds.DBus.Generator" Version="0.94.2" />
<PackageVersion Include="Svg.Skia" Version="3.2.1" />
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<a style="text-decoration:none" href="https://www.nuget.org/packages/DesktopFlyouts.WinUI"><img src="https://img.shields.io/nuget/v/DesktopFlyouts.WinUI" alt="NuGet badge" /></a>
Expand Down
5 changes: 4 additions & 1 deletion docs/focus-and-activation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
40 changes: 40 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
```

```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
Expand Down
34 changes: 1 addition & 33 deletions samples/DesktopFlyouts.Uwp.Sample.TrayHost/App.xaml
Original file line number Diff line number Diff line change
@@ -1,36 +1,4 @@
<Application
x:Class="DesktopFlyouts.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:DesktopFlyouts">

<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>

<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />

<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="FlyoutOverlayBackgroundBrush">#09FFFFFF</SolidColorBrush>
<SolidColorBrush x:Key="FlyoutSeparatorPrimaryBackgroundBrush">#15FFFFFF</SolidColorBrush>
<SolidColorBrush x:Key="FlyoutSeparatorSecondaryBackgroundBrush">#18000000</SolidColorBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="FlyoutOverlayBackgroundBrush">#40FFFFFF</SolidColorBrush>
<SolidColorBrush x:Key="FlyoutSeparatorPrimaryBackgroundBrush">#0F000000</SolidColorBrush>
<SolidColorBrush x:Key="FlyoutSeparatorSecondaryBackgroundBrush">#10000000</SolidColorBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="FlyoutOverlayBackgroundBrush">#09FFFFFF</SolidColorBrush>
<SolidColorBrush x:Key="FlyoutSeparatorPrimaryBackgroundBrush">#15FFFFFF</SolidColorBrush>
<SolidColorBrush x:Key="FlyoutSeparatorSecondaryBackgroundBrush">#18000000</SolidColorBrush>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

</Application>
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" />
233 changes: 162 additions & 71 deletions samples/DesktopFlyouts.Uwp.Sample.TrayHost/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,172 @@
// Licensed under the MIT license.

using System;
using System.IO;
using System.Threading;
using DesktopFlyouts.Shared;
using Windows.ApplicationModel.Activation;
using Windows.System;
using Windows.UI;
using Windows.UI.Core;
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;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

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));
}
}
public partial class App : Application
{
private static SystemTrayIcon? _systemTrayIcon;
private static DesktopFlyout? _desktopFlyout;
private static volatile DesktopMenuFlyout? _desktopMenuFlyout;

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
SynchronizationContext.SetSynchronizationContext(
new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()));

_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 = CreateDesktopFlyout();

// 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(_ =>
{
SynchronizationContext.SetSynchronizationContext(
new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()));
_desktopMenuFlyout = CreateDesktopMenuFlyout();
});
}

private static DesktopFlyout CreateDesktopFlyout()
{
var flyout = new DesktopFlyout
{
Width = 360,
};

flyout.Islands.Add(CreateIsland(new Button
{
Content = "A",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
}));

flyout.Islands.Add(CreateIsland(new TextBlock
{
Text = "Island 2",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
}));

var options = new ComboBox
{
HorizontalAlignment = HorizontalAlignment.Stretch,
};
options.Items.Add("Option 1");
options.Items.Add("Option 2");
options.Items.Add("Option 3");

var panel = new StackPanel
{
Padding = new Thickness(16),
Spacing = 8,
};
panel.Children.Add(new TextBlock
{
Text = "Island 3",
FontSize = 20,
FontWeight = Windows.UI.Text.FontWeights.SemiBold,
});
panel.Children.Add(new TextBlock { Text = "Select option" });
panel.Children.Add(options);
panel.Children.Add(new CalendarDatePicker());
flyout.Islands.Add(CreateIsland(panel));

return flyout;
}

private static DesktopFlyoutIsland CreateIsland(UIElement content)
{
return new()
{
Height = 180,
Background = new SolidColorBrush(Color.FromArgb(245, 32, 32, 32)),
Content = content,
};
}

private static DesktopMenuFlyout CreateDesktopMenuFlyout()
{
var menu = new DesktopMenuFlyout();
var settings = new MenuFlyoutSubItem
{
Text = "Settings",
Icon = new FontIcon { Glyph = "\uE115" },
};
settings.Items.Add(new MenuFlyoutItem { Text = "Theme" });
settings.Items.Add(new MenuFlyoutItem { Text = "Language" });
settings.Items.Add(new MenuFlyoutItem { Text = "Privacy" });

var devices = new MenuFlyoutItem
{
Text = "Devices",
Icon = new FontIcon { Glyph = "\uE975" },
};
devices.Click += async (_, _) => await Launcher.LaunchUriAsync(new Uri("tif-secondaryapp:"));

var exit = new MenuFlyoutItem
{
Text = "Exit",
Icon = new FontIcon { Glyph = "\uE8BB" },
};
exit.Click += (_, _) =>
{
var dispatcher = _desktopFlyout?.Dispatcher;
if (dispatcher is not null)
_ = dispatcher.RunAsync(CoreDispatcherPriority.Normal, Current.Exit);
};

menu.Items.Add(settings);
menu.Items.Add(devices);
menu.Items.Add(new MenuFlyoutSeparator());
menu.Items.Add(exit);
return menu;
}

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)
{
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));
});
}
}
}
Loading
Loading