Files
LanMountainDesktop/LanMountainDesktop.AirAppHost/AirAppWindow.axaml.cs

275 lines
9.0 KiB
C#
Raw Normal View History

2026-05-14 19:44:01 +08:00
using Avalonia.Controls;
using Avalonia.Media;
2026-05-14 19:44:01 +08:00
using Avalonia.Threading;
using FluentAvalonia.UI.Windowing;
2026-05-14 19:44:01 +08:00
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
using LanMountainDesktop.Shared.IPC;
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.AirAppHost;
public sealed partial class AirAppWindow : FAAppWindow
2026-05-14 19:44:01 +08:00
{
private readonly AirAppLaunchOptions _options;
private readonly AirAppWindowDescriptor _descriptor;
2026-05-18 08:30:40 +08:00
private WhiteboardWidget? _whiteboardWidget;
2026-05-14 19:44:01 +08:00
private string _instanceKey = string.Empty;
public AirAppWindow()
: this(AirAppLaunchOptions.Parse([]))
{
}
public AirAppWindow(AirAppLaunchOptions options)
{
_options = options;
_descriptor = AirAppWindowDescriptor.Create(options);
InitializeComponent();
ConfigureWindow();
}
private void ConfigureWindow()
{
ApplyWindowDescriptor(_descriptor);
if (string.Equals(_options.AppId, AirAppLaunchOptions.WorldClockAppId, StringComparison.OrdinalIgnoreCase))
{
ContentHost.Content = new ClockAirAppView(_options);
2026-05-14 19:44:01 +08:00
return;
}
if (string.Equals(_options.AppId, AirAppLaunchOptions.WhiteboardAppId, StringComparison.OrdinalIgnoreCase))
{
ConfigureWhiteboardWindow();
return;
}
ContentHost.Content = new TextBlock
{
Text = $"Unsupported Air APP: {_options.AppId}",
Margin = new Avalonia.Thickness(18)
};
}
private void ApplyWindowDescriptor(AirAppWindowDescriptor descriptor)
{
Title = descriptor.Title;
Width = descriptor.Width;
Height = descriptor.Height;
MinWidth = descriptor.MinWidth;
MinHeight = descriptor.MinHeight;
ShowInTaskbar = descriptor.ShowInTaskbar;
CanResize = descriptor.CanResize;
ShowAsDialog = descriptor.ShowAsDialog;
2026-05-14 19:44:01 +08:00
WindowState = WindowState.Normal;
WindowRoot.Background = this.TryFindResource("AirAppWindowBackgroundBrush", out var brush) && brush is IBrush backgroundBrush
? backgroundBrush
: Brushes.White;
ConfigureTitleBar(descriptor);
2026-05-14 19:44:01 +08:00
switch (descriptor.ChromeMode)
{
case AirAppWindowChromeMode.Standard:
WindowDecorations = WindowDecorations.Full;
TitleBar.ExtendsContentIntoTitleBar = false;
2026-05-14 19:44:01 +08:00
break;
case AirAppWindowChromeMode.Borderless:
WindowDecorations = WindowDecorations.None;
TitleBar.ExtendsContentIntoTitleBar = true;
2026-05-14 19:44:01 +08:00
break;
case AirAppWindowChromeMode.FullScreen:
WindowDecorations = WindowDecorations.None;
TitleBar.ExtendsContentIntoTitleBar = true;
ShowAsDialog = false;
2026-05-14 19:44:01 +08:00
WindowState = WindowState.FullScreen;
break;
case AirAppWindowChromeMode.Tool:
WindowDecorations = WindowDecorations.Full;
TitleBar.ExtendsContentIntoTitleBar = false;
2026-05-14 19:44:01 +08:00
ShowInTaskbar = false;
CanResize = false;
break;
case AirAppWindowChromeMode.BackgroundOnly:
// Reserved for future background-only Air APPs. Keep a normal window for now
// so accidental launches remain visible and debuggable.
break;
}
}
private void ConfigureTitleBar(AirAppWindowDescriptor descriptor)
2026-05-14 19:44:01 +08:00
{
TitleBar.Height = descriptor.ChromeMode == AirAppWindowChromeMode.Tool ? 36 : 40;
TitleBar.BackgroundColor = Colors.Transparent;
TitleBar.ForegroundColor = Color.FromRgb(32, 32, 32);
TitleBar.InactiveBackgroundColor = Colors.Transparent;
TitleBar.InactiveForegroundColor = Color.FromRgb(96, 96, 96);
TitleBar.ButtonBackgroundColor = Colors.Transparent;
TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(23, 0, 0, 0);
TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(52, 0, 0, 0);
TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
TitleBar.ButtonInactiveForegroundColor = Colors.Gray;
2026-05-14 19:44:01 +08:00
}
private void ConfigureWhiteboardWindow()
{
var componentId = string.IsNullOrWhiteSpace(_options.SourceComponentId)
? BuiltInComponentIds.DesktopWhiteboard
: _options.SourceComponentId.Trim();
var baseWidthCells = string.Equals(componentId, BuiltInComponentIds.DesktopBlackboardLandscape, StringComparison.OrdinalIgnoreCase)
? 4
: 2;
var widget = new WhiteboardWidget(baseWidthCells);
2026-05-18 08:30:40 +08:00
_whiteboardWidget = widget;
2026-05-14 19:44:01 +08:00
widget.SetComponentPlacementContext(componentId, _options.SourcePlacementId);
widget.SetSurfaceMode(
WhiteboardWidgetSurfaceMode.AirApp,
() =>
{
widget.ForceSaveNote();
Close();
});
ContentHost.Content = widget;
2026-05-18 08:30:40 +08:00
AppLogger.Info(
"AirAppWindow",
$"Whiteboard content created. ComponentId='{componentId}'; PlacementId='{_options.SourcePlacementId ?? string.Empty}'.");
2026-05-14 19:44:01 +08:00
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
_ = RegisterWithLauncherAsync();
AppLogger.Info(
"AirAppWindow",
$"Opened. WindowRole=AirApp; AppId='{_options.AppId}'; ForegroundActivationRequested=True.");
Dispatcher.UIThread.Post(() =>
{
Activate();
}, DispatcherPriority.Background);
}
protected override void OnClosing(WindowClosingEventArgs e)
{
SaveWhiteboard();
base.OnClosing(e);
}
2026-05-14 19:44:01 +08:00
protected override void OnClosed(EventArgs e)
{
2026-05-18 08:30:40 +08:00
SaveAndDisposeWhiteboard();
2026-05-14 19:44:01 +08:00
_ = UnregisterWithLauncherAsync();
base.OnClosed(e);
}
2026-05-18 08:30:40 +08:00
private void SaveAndDisposeWhiteboard()
{
var widget = _whiteboardWidget;
if (widget is null)
{
return;
}
SaveWhiteboard();
if (ContentHost.Content == widget)
{
ContentHost.Content = null;
}
widget.Dispose();
_whiteboardWidget = null;
}
private void SaveWhiteboard()
{
if (_whiteboardWidget is null)
{
return;
}
try
{
_whiteboardWidget.ForceSaveNote();
}
catch (Exception ex)
{
AppLogger.Warn("AirAppWindow", "Failed to force-save whiteboard before closing Air APP.", ex);
}
}
2026-05-14 19:44:01 +08:00
private async Task RegisterWithLauncherAsync()
{
if (string.IsNullOrWhiteSpace(_options.LauncherPipeName))
{
return;
}
_instanceKey = ResolveInstanceKey();
try
{
using var client = new LanMountainDesktopIpcClient();
await client.ConnectAsync(_options.LauncherPipeName).ConfigureAwait(false);
var proxy = client.CreateProxy<IAirAppLifecycleService>();
_ = await proxy.RegisterAsync(new AirAppRegistrationRequest(
_instanceKey,
_options.AppId,
_options.SessionId,
Environment.ProcessId,
Title ?? "Air APP",
_options.SourceComponentId,
_options.SourcePlacementId)).ConfigureAwait(false);
}
catch
{
// Registration is best-effort; Launcher also tracks the process it started.
}
}
private async Task UnregisterWithLauncherAsync()
{
if (string.IsNullOrWhiteSpace(_options.LauncherPipeName))
{
return;
}
var instanceKey = string.IsNullOrWhiteSpace(_instanceKey) ? ResolveInstanceKey() : _instanceKey;
try
{
using var client = new LanMountainDesktopIpcClient();
await client.ConnectAsync(_options.LauncherPipeName).ConfigureAwait(false);
var proxy = client.CreateProxy<IAirAppLifecycleService>();
_ = await proxy.UnregisterAsync(instanceKey, Environment.ProcessId).ConfigureAwait(false);
}
catch
{
// Unregister is best-effort; Launcher prunes dead processes.
}
}
private string ResolveInstanceKey()
{
if (!string.IsNullOrWhiteSpace(_options.InstanceKey))
{
return _options.InstanceKey.Trim();
}
if (string.Equals(_options.AppId, AirAppLaunchOptions.WorldClockAppId, StringComparison.OrdinalIgnoreCase))
{
return $"{AirAppLaunchOptions.WorldClockAppId}:clock-suite:global";
}
2026-05-14 19:44:01 +08:00
var componentId = string.IsNullOrWhiteSpace(_options.SourceComponentId)
? "none"
: _options.SourceComponentId.Trim();
var placementId = string.IsNullOrWhiteSpace(_options.SourcePlacementId)
? "none"
: _options.SourcePlacementId.Trim();
return $"{_options.AppId}:{componentId}:{placementId}";
}
}