mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
275 lines
9.0 KiB
C#
275 lines
9.0 KiB
C#
|
|
using Avalonia.Controls;
|
||
|
|
using Avalonia.Media;
|
||
|
|
using Avalonia.Threading;
|
||
|
|
using FluentAvalonia.UI.Windowing;
|
||
|
|
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
|
||
|
|
{
|
||
|
|
private readonly AirAppLaunchOptions _options;
|
||
|
|
private readonly AirAppWindowDescriptor _descriptor;
|
||
|
|
private WhiteboardWidget? _whiteboardWidget;
|
||
|
|
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);
|
||
|
|
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;
|
||
|
|
WindowState = WindowState.Normal;
|
||
|
|
WindowRoot.Background = this.TryFindResource("AirAppWindowBackgroundBrush", out var brush) && brush is IBrush backgroundBrush
|
||
|
|
? backgroundBrush
|
||
|
|
: Brushes.White;
|
||
|
|
ConfigureTitleBar(descriptor);
|
||
|
|
|
||
|
|
switch (descriptor.ChromeMode)
|
||
|
|
{
|
||
|
|
case AirAppWindowChromeMode.Standard:
|
||
|
|
WindowDecorations = WindowDecorations.Full;
|
||
|
|
TitleBar.ExtendsContentIntoTitleBar = false;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AirAppWindowChromeMode.Borderless:
|
||
|
|
WindowDecorations = WindowDecorations.None;
|
||
|
|
TitleBar.ExtendsContentIntoTitleBar = true;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AirAppWindowChromeMode.FullScreen:
|
||
|
|
WindowDecorations = WindowDecorations.None;
|
||
|
|
TitleBar.ExtendsContentIntoTitleBar = true;
|
||
|
|
ShowAsDialog = false;
|
||
|
|
WindowState = WindowState.FullScreen;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AirAppWindowChromeMode.Tool:
|
||
|
|
WindowDecorations = WindowDecorations.Full;
|
||
|
|
TitleBar.ExtendsContentIntoTitleBar = false;
|
||
|
|
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)
|
||
|
|
{
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
_whiteboardWidget = widget;
|
||
|
|
widget.SetComponentPlacementContext(componentId, _options.SourcePlacementId);
|
||
|
|
widget.SetSurfaceMode(
|
||
|
|
WhiteboardWidgetSurfaceMode.AirApp,
|
||
|
|
() =>
|
||
|
|
{
|
||
|
|
widget.ForceSaveNote();
|
||
|
|
Close();
|
||
|
|
});
|
||
|
|
|
||
|
|
ContentHost.Content = widget;
|
||
|
|
AppLogger.Info(
|
||
|
|
"AirAppWindow",
|
||
|
|
$"Whiteboard content created. ComponentId='{componentId}'; PlacementId='{_options.SourcePlacementId ?? string.Empty}'.");
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override void OnClosed(EventArgs e)
|
||
|
|
{
|
||
|
|
SaveAndDisposeWhiteboard();
|
||
|
|
_ = UnregisterWithLauncherAsync();
|
||
|
|
base.OnClosed(e);
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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";
|
||
|
|
}
|
||
|
|
|
||
|
|
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}";
|
||
|
|
}
|
||
|
|
}
|