Files
LanMountainDesktop/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs
2026-04-19 17:02:53 +08:00

806 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views;
/// <summary>
/// 透明覆盖层窗口 - 作为"负一屏"显示在 Windows 桌面上
/// 支持在系统桌面上自由摆放组件
/// </summary>
public partial class TransparentOverlayWindow : Window
{
private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate();
private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate();
// 滑动状态
private bool _isSwipeActive;
private bool _isSwipeDirectionLocked;
private Point _swipeStartPoint;
private Point _swipeCurrentPoint;
private Point _swipeLastPoint;
private double _swipeVelocityX;
private long _swipeLastTimestamp;
// 三指/右键拖动状态
private bool _isThreeFingerOrRightDragSwipeActive;
private readonly HashSet<int> _activePointerIds = [];
// 组件管理
private readonly Dictionary<string, Border> _componentHosts = [];
private readonly List<Rect> _interactiveRegions = [];
private FusedDesktopLayoutSnapshot _layout = new();
private ComponentRegistry? _componentRegistry;
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
// 基础服务
private readonly IWeatherInfoService _weatherDataService;
private readonly TimeZoneService _timeZoneService;
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
// 渲染参数
private const double DefaultCellSize = 100;
private double _currentDesktopCellSize;
// 拖拽与缩放状态
private bool _isDragging;
private bool _isResizing;
private string? _interactionPlacementId;
private Point _interactionStartPoint;
private double _interactionOriginalX;
private double _interactionOriginalY;
private double _interactionOriginalWidth;
private double _interactionOriginalHeight;
private Border? _interactionHost;
// 选中状态
private Border? _selectedHost;
public event EventHandler? RestoreMainWindowRequested;
public TransparentOverlayWindow()
{
InitializeComponent();
var facade = HostSettingsFacadeProvider.GetOrCreate();
_weatherDataService = facade.Weather.GetWeatherInfoService();
_timeZoneService = facade.Region.GetTimeZoneService();
_settingsFacade = facade;
if (OperatingSystem.IsWindows())
{
_bottomMostService.SetupBottomMost(this);
}
}
private readonly ISettingsFacadeService _settingsFacade;
public void SaveLayoutAndHide()
{
SaveLayout();
_regionPassthroughService.ClearInteractiveRegions(this);
Hide();
// Remove all components so that next time we open it builds fresh from snapshot
if (Content is Canvas canvas)
{
canvas.Children.Clear();
}
_componentHosts.Clear();
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if (Screens.Primary is { } primaryScreen)
{
// 避开系统任务栏
var workArea = primaryScreen.WorkingArea;
var scaling = primaryScreen.Scaling;
Position = new PixelPoint(workArea.X, workArea.Y);
Width = workArea.Width / scaling;
Height = workArea.Height / scaling;
// 基于设置计算单元格尺寸
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var shortCells = Math.Clamp(appSnapshot.GridShortSideCells > 0 ? appSnapshot.GridShortSideCells : 12, 6, 96);
_currentDesktopCellSize = Height / shortCells;
}
else
{
_currentDesktopCellSize = DefaultCellSize;
}
if (Content is Canvas canvas)
{
// 保证透明区域也能被抓取事件
canvas.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
}
// 确保注册表已初始化
EnsureRegistries();
// 加载布局并渲染
_layout = _layoutService.Load();
RenderAllComponents();
AppLogger.Info("TransparentOverlay", $"Opened with {_layout.ComponentPlacements.Count} components.");
if (OperatingSystem.IsWindows())
{
_bottomMostService.SendToBottom(this);
}
}
/// <summary>
/// 确保组件运行时注册表已初始化
/// </summary>
private void EnsureRegistries()
{
if (_componentRuntimeRegistry is not null) return;
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry,
pluginRuntimeService,
_settingsFacade);
}
/// <summary>
/// 渲染所有布局中的组件
/// </summary>
private void RenderAllComponents()
{
if (Content is not Canvas canvas) return;
canvas.Children.Clear();
_componentHosts.Clear();
_selectedHost = null;
foreach (var placement in _layout.ComponentPlacements)
{
try
{
RenderComponentInternal(placement);
}
catch (Exception ex)
{
AppLogger.Warn("TransparentOverlay", $"Failed to render component {placement.ComponentId}", ex);
}
}
UpdateInteractiveRegions();
}
protected override void OnClosed(EventArgs e)
{
SaveLayout();
base.OnClosed(e);
}
/// <summary>
/// 更新可交互区域
/// </summary>
private void UpdateInteractiveRegions()
{
_interactiveRegions.Clear();
foreach (var host in _componentHosts.Values)
{
var left = Canvas.GetLeft(host);
var top = Canvas.GetTop(host);
var width = host.Width > 0 ? host.Width : host.Bounds.Width;
var height = host.Height > 0 ? host.Height : host.Bounds.Height;
if (width <= 0 || height <= 0)
{
continue;
}
// 稍微向外扩一圈,确保拖拽和右下角缩放手柄也能命中。
_interactiveRegions.Add(new Rect(left - 12, top - 12, width + 24, height + 24));
}
_regionPassthroughService.SetInteractiveRegions(this, _interactiveRegions);
}
/// <summary>
/// 保存布局
/// </summary>
private void SaveLayout()
{
_layoutService.Save(_layout);
}
/// <summary>
/// 添加组件(供外部调用)
/// </summary>
public void AddComponent(string componentId, double x, double y, double? width = null, double? height = null)
{
EnsureRegistries();
if (_componentRegistry == null || !_componentRegistry.TryGetDefinition(componentId, out var definition))
{
AppLogger.Warn("TransparentOverlay", $"Cannot add unknown component: {componentId}");
return;
}
var finalWidth = width ?? (definition.MinWidthCells * _currentDesktopCellSize);
var finalHeight = height ?? (definition.MinHeightCells * _currentDesktopCellSize);
// 对齐网格
x = Math.Round(x / _currentDesktopCellSize) * _currentDesktopCellSize;
y = Math.Round(y / _currentDesktopCellSize) * _currentDesktopCellSize;
finalWidth = Math.Round(finalWidth / _currentDesktopCellSize) * _currentDesktopCellSize;
finalHeight = Math.Round(finalHeight / _currentDesktopCellSize) * _currentDesktopCellSize;
var placementId = Guid.NewGuid().ToString("N");
var placement = new FusedDesktopComponentPlacementSnapshot
{
PlacementId = placementId,
ComponentId = componentId,
X = x,
Y = y,
Width = finalWidth,
Height = finalHeight,
ZIndex = _layout.ComponentPlacements.Count
};
_layout.ComponentPlacements.Add(placement);
// 立即渲染
try
{
RenderComponentInternal(placement);
UpdateInteractiveRegions();
SaveLayout();
AppLogger.Info("TransparentOverlay", $"Added component: {componentId} at ({x}, {y}) size ({finalWidth}x{finalHeight})");
}
catch (Exception ex)
{
AppLogger.Warn("TransparentOverlay", $"Failed to add component {componentId}", ex);
_layout.ComponentPlacements.Remove(placement);
}
}
/// <summary>
/// 内部渲染单个组件
/// </summary>
private void RenderComponentInternal(FusedDesktopComponentPlacementSnapshot placement)
{
if (_componentRuntimeRegistry is null || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor))
{
AppLogger.Warn("TransparentOverlay", $"Unknown component: {placement.ComponentId}");
return;
}
// 【修复问题3】尝试从现有窗口中获取组件实例避免重新创建导致状态丢失
var control = TryGetExistingControl(placement.PlacementId);
if (control is null)
{
// 如果没有现有实例,才创建新的
control = descriptor.CreateControl(
_currentDesktopCellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
placement.PlacementId);
}
RenderComponent(placement.PlacementId, control, placement.X, placement.Y, placement.Width, placement.Height);
}
/// <summary>
/// 【修复问题3】尝试从现有的小窗口中获取组件控件实例
/// </summary>
private Control? TryGetExistingControl(string placementId)
{
try
{
var manager = FusedDesktopManagerServiceFactory.GetOrCreate();
// 通过反射或公共 API 获取现有窗口中的控件
// 这里需要 FusedDesktopManagerService 提供获取控件的方法
// 暂时返回 null后续需要扩展接口
return null;
}
catch
{
return null;
}
}
/// <summary>
/// 移除组件
/// </summary>
public void RemoveComponent(string placementId)
{
if (_componentHosts.TryGetValue(placementId, out var host))
{
if (Content is Canvas canvas)
{
canvas.Children.Remove(host);
}
_componentHosts.Remove(placementId);
}
_layout.ComponentPlacements.RemoveAll(p => p.PlacementId == placementId);
UpdateInteractiveRegions();
SaveLayout();
}
/// <summary>
/// 渲染组件(从外部传入控件)
/// </summary>
public void RenderComponent(string placementId, Control component, double x, double y, double width, double height)
{
var grid = new Grid();
grid.Children.Add(component);
var resizeHandle = new Border
{
Width = 24,
Height = 24,
Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#3B82F6")),
CornerRadius = new Avalonia.CornerRadius(12),
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
Margin = new Avalonia.Thickness(0, 0, -12, -12),
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.BottomRightCorner),
Tag = "desktop-component-resize-handle",
IsVisible = false
};
grid.Children.Add(resizeHandle);
var host = new Border
{
Tag = placementId,
Width = width,
Height = height,
Background = Avalonia.Media.Brushes.Transparent,
CornerRadius = new Avalonia.CornerRadius(12),
ClipToBounds = false, // 允许把手溢出
BorderBrush = Avalonia.Media.Brushes.Transparent,
BorderThickness = new Avalonia.Thickness(3),
Child = grid,
Classes = { "desktop-component-host" }
};
Canvas.SetLeft(host, x);
Canvas.SetTop(host, y);
host.PointerPressed += OnComponentPointerPressed;
host.PointerMoved += OnInteractionPointerMoved;
host.PointerReleased += OnInteractionPointerReleased;
// 右键上下文菜单(删除组件)
host.ContextRequested += OnComponentContextRequested;
if (Content is Canvas canvas)
{
canvas.Children.Add(host);
}
_componentHosts[placementId] = host;
UpdateInteractiveRegions();
}
// 组件右键上下文菜单(删除)
private void OnComponentContextRequested(object? sender, ContextRequestedEventArgs e)
{
if (sender is not Border host || host.Tag is not string placementId) return;
// 构建上下文菜单
var deleteItem = new MenuItem
{
Header = "移除组件",
Icon = new Avalonia.Controls.TextBlock { Text = "🗑" }
};
deleteItem.Click += (_, _) =>
{
RemoveComponent(placementId);
AppLogger.Info("TransparentOverlay", $"Component removed via context menu: {placementId}");
};
var menu = new ContextMenu
{
Items = { deleteItem }
};
// 显示在当前控件上
menu.Open(host);
e.Handled = true;
}
// 取消选中
private void OnCanvasPointerPressed(object? sender, PointerPressedEventArgs e)
{
DeselectComponent();
}
// 选中组件
private void SelectComponent(Border host)
{
if (_selectedHost == host) return;
DeselectComponent();
_selectedHost = host;
// 渲染选中边框和把手
host.BorderBrush = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#3B82F6"));
host.Classes.Add("desktop-component-host-selected");
if (host.Child is Grid grid)
{
foreach (var child in grid.Children)
{
if (child is Control c && c.Tag is string tg && tg == "desktop-component-resize-handle")
{
c.IsVisible = true;
break;
}
}
}
}
private void DeselectComponent()
{
if (_selectedHost != null)
{
_selectedHost.BorderBrush = Avalonia.Media.Brushes.Transparent;
_selectedHost.Classes.Remove("desktop-component-host-selected");
if (_selectedHost.Child is Grid grid)
{
foreach (var child in grid.Children)
{
if (child is Control c && c.Tag is string tg && tg == "desktop-component-resize-handle")
{
c.IsVisible = false;
break;
}
}
}
}
_selectedHost = null;
}
// 组件拖拽与缩放处理
private void OnComponentPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender is not Border host || host.Tag is not string placementId) return;
var point = e.GetCurrentPoint(this);
if (!point.Properties.IsLeftButtonPressed) return;
SelectComponent(host);
_interactionPlacementId = placementId;
_interactionHost = host;
_interactionStartPoint = e.GetPosition(this);
// 这里必须用未吸附的原始屏幕位置计算 delta
_interactionOriginalX = Canvas.GetLeft(host);
_interactionOriginalY = Canvas.GetTop(host);
_interactionOriginalWidth = host.Width;
_interactionOriginalHeight = host.Height;
if (e.Source is Control sourceControl && sourceControl.Tag is string tag && tag == "desktop-component-resize-handle")
{
_isResizing = true;
_isDragging = false;
}
else
{
_isDragging = true;
_isResizing = false;
}
e.Pointer.Capture(host);
e.Handled = true;
}
private void OnInteractionPointerMoved(object? sender, PointerEventArgs e)
{
if ((!_isDragging && !_isResizing) || _interactionHost is null) return;
var currentPoint = e.GetPosition(this);
var deltaX = currentPoint.X - _interactionStartPoint.X;
var deltaY = currentPoint.Y - _interactionStartPoint.Y;
if (_isDragging)
{
var rawX = _interactionOriginalX + deltaX;
var rawY = _interactionOriginalY + deltaY;
var snapX = Math.Round(rawX / _currentDesktopCellSize) * _currentDesktopCellSize;
var snapY = Math.Round(rawY / _currentDesktopCellSize) * _currentDesktopCellSize;
Canvas.SetLeft(_interactionHost, snapX);
Canvas.SetTop(_interactionHost, snapY);
}
else if (_isResizing)
{
var rawWidth = _interactionOriginalWidth + deltaX;
var rawHeight = _interactionOriginalHeight + deltaY;
var snapWidth = Math.Round(rawWidth / _currentDesktopCellSize) * _currentDesktopCellSize;
var snapHeight = Math.Round(rawHeight / _currentDesktopCellSize) * _currentDesktopCellSize;
// 防溢出与极小值保护
snapWidth = Math.Max(_currentDesktopCellSize, snapWidth);
snapHeight = Math.Max(_currentDesktopCellSize, snapHeight);
_interactionHost.Width = snapWidth;
_interactionHost.Height = snapHeight;
}
e.Handled = true;
}
private void OnInteractionPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if ((!_isDragging && !_isResizing) || _interactionHost is null || _interactionPlacementId is null)
{
_isDragging = false;
_isResizing = false;
return;
}
// 更新布局中的位置与尺寸
var placement = _layout.ComponentPlacements.Find(p => p.PlacementId == _interactionPlacementId);
if (placement is not null)
{
placement.X = Canvas.GetLeft(_interactionHost);
placement.Y = Canvas.GetTop(_interactionHost);
placement.Width = _interactionHost.Width;
placement.Height = _interactionHost.Height;
}
UpdateInteractiveRegions();
SaveLayout();
_isDragging = false;
_isResizing = false;
_interactionPlacementId = null;
_interactionHost = null;
e.Pointer.Capture(null);
e.Handled = true;
}
// 三指滑动处理
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
if (!appSnapshot.EnableThreeFingerSwipe)
{
base.OnPointerPressed(e);
return;
}
if (!TryGetPointerPosition(e, out var pointerPos))
{
base.OnPointerPressed(e);
return;
}
var currentPoint = e.GetCurrentPoint(this);
var pointerId = e.Pointer?.Id ?? 0;
var isRightButtonPressed = currentPoint.Properties.IsRightButtonPressed;
var isLeftButtonPressed = currentPoint.Properties.IsLeftButtonPressed;
if (isLeftButtonPressed || isRightButtonPressed)
{
_activePointerIds.Add(pointerId);
}
var isThreeFinger = _activePointerIds.Count >= 3;
var isRightDrag = isRightButtonPressed;
if (isThreeFinger || isRightDrag)
{
_isSwipeActive = true;
_isThreeFingerOrRightDragSwipeActive = true;
_isSwipeDirectionLocked = false;
_swipeStartPoint = pointerPos;
_swipeCurrentPoint = pointerPos;
_swipeLastPoint = pointerPos;
_swipeVelocityX = 0;
_swipeLastTimestamp = Stopwatch.GetTimestamp();
e.Handled = true;
}
else
{
base.OnPointerPressed(e);
}
}
protected override void OnPointerMoved(PointerEventArgs e)
{
if (!_isSwipeActive)
{
base.OnPointerMoved(e);
return;
}
if (!TryGetPointerPosition(e, out var pointerPos))
{
base.OnPointerMoved(e);
return;
}
_swipeCurrentPoint = pointerPos;
UpdateSwipeVelocity(pointerPos);
var deltaX = _swipeCurrentPoint.X - _swipeStartPoint.X;
var deltaY = _swipeCurrentPoint.Y - _swipeStartPoint.Y;
if (!_isSwipeDirectionLocked)
{
const double activationThreshold = 14;
const double horizontalBias = 1.15;
var absDeltaX = Math.Abs(deltaX);
var absDeltaY = Math.Abs(deltaY);
if (absDeltaY >= activationThreshold && absDeltaY > absDeltaX * horizontalBias)
{
CancelSwipeInteraction(e.Pointer);
base.OnPointerMoved(e);
return;
}
if (absDeltaX < activationThreshold || absDeltaX <= absDeltaY * horizontalBias)
{
base.OnPointerMoved(e);
return;
}
_isSwipeDirectionLocked = true;
if (e.Pointer?.Captured != this)
{
e.Pointer?.Capture(this);
}
}
e.Handled = true;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
var pointerId = e.Pointer?.Id ?? 0;
_activePointerIds.Remove(pointerId);
if (_isSwipeActive)
{
if (EndSwipeInteraction(e.Pointer))
{
e.Handled = true;
return;
}
}
base.OnPointerReleased(e);
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
var pointerId = e.Pointer?.Id ?? 0;
_activePointerIds.Remove(pointerId);
if (_isSwipeActive)
{
EndSwipeInteraction(e.Pointer);
}
base.OnPointerCaptureLost(e);
}
private bool TryGetPointerPosition(PointerEventArgs e, out Point point)
{
try
{
point = e.GetPosition(this);
return true;
}
catch
{
point = default;
return false;
}
}
private void UpdateSwipeVelocity(Point currentPoint)
{
var now = Stopwatch.GetTimestamp();
var elapsed = Stopwatch.GetElapsedTime(_swipeLastTimestamp, now).TotalSeconds;
if (elapsed > 0)
{
var dx = currentPoint.X - _swipeLastPoint.X;
_swipeVelocityX = dx / elapsed;
}
_swipeLastPoint = currentPoint;
_swipeLastTimestamp = now;
}
private void CancelSwipeInteraction(IPointer? pointer)
{
if (!_isSwipeActive) return;
if (pointer?.Captured == this)
{
pointer?.Capture(null);
}
_isSwipeActive = false;
_isSwipeDirectionLocked = false;
_isThreeFingerOrRightDragSwipeActive = false;
_activePointerIds.Clear();
_swipeVelocityX = 0;
_swipeLastTimestamp = 0;
}
private bool EndSwipeInteraction(IPointer? pointer)
{
if (!_isSwipeActive) return false;
var wasDirectionLocked = _isSwipeDirectionLocked;
var wasThreeFingerOrRightDrag = _isThreeFingerOrRightDragSwipeActive;
_isSwipeActive = false;
_isSwipeDirectionLocked = false;
_isThreeFingerOrRightDragSwipeActive = false;
_activePointerIds.Clear();
if (pointer?.Captured == this)
{
pointer?.Capture(null);
}
_swipeLastTimestamp = 0;
if (!wasDirectionLocked)
{
_swipeVelocityX = 0;
return false;
}
var deltaX = _swipeCurrentPoint.X - _swipeStartPoint.X;
var deltaY = _swipeCurrentPoint.Y - _swipeStartPoint.Y;
var absDeltaX = Math.Abs(deltaX);
var distanceThreshold = Math.Max(48, this.Bounds.Width * 0.14);
var velocityThreshold = Math.Max(860, this.Bounds.Width * 1.08);
var hasDistanceIntent = absDeltaX >= distanceThreshold && absDeltaX > Math.Abs(deltaY) * 1.05;
var hasVelocityIntent = Math.Abs(_swipeVelocityX) >= velocityThreshold;
// 向左滑动回到第一页
if (wasThreeFingerOrRightDrag && deltaX < 0 && (hasDistanceIntent || hasVelocityIntent))
{
RestoreMainWindowRequested?.Invoke(this, EventArgs.Empty);
_swipeVelocityX = 0;
return true;
}
_swipeVelocityX = 0;
return hasDistanceIntent || hasVelocityIntent;
}
}