Compare commits

...

2 Commits

Author SHA1 Message Date
lincube
1c3cc76f21 fead.做了状态栏文字组件,支持了位置放置。 2026-04-03 13:14:20 +08:00
lincube
44b87ba12e fead.桌面组件 2026-04-03 11:42:00 +08:00
26 changed files with 2179 additions and 363 deletions

View File

@@ -149,6 +149,11 @@ public partial class App : Application
LinuxDesktopEntryInstaller.EnsureInstalled(); LinuxDesktopEntryInstaller.EnsureInstalled();
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell); DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
if (!Design.IsDesignMode && OperatingSystem.IsWindows())
{
FusedDesktopManagerServiceFactory.GetOrCreate().Initialize();
}
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
@@ -226,6 +231,9 @@ public partial class App : Application
AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows."); AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows.");
return; return;
} }
// 切换进入编辑模式,隐藏常态零散的小部件
FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode();
// 确保透明覆盖层窗口存在并显示 // 确保透明覆盖层窗口存在并显示
EnsureTransparentOverlayWindow(); EnsureTransparentOverlayWindow();
@@ -248,6 +256,19 @@ public partial class App : Application
window.SetOverlayWindow(_transparentOverlayWindow); window.SetOverlayWindow(_transparentOverlayWindow);
} }
// 当组件库关闭时,退出编辑态
window.Closed += (s, ev) =>
{
if (_transparentOverlayWindow is not null)
{
// 触发画布保存,并隐藏画布
_transparentOverlayWindow.SaveLayoutAndHide();
}
// 让管理器根据已存储的最新快照重建生成所有实体小组件
FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode();
};
window.Show(); window.Show();
window.Activate(); window.Activate();
} }

View File

@@ -388,6 +388,18 @@
"settings.status_bar.clock_format_label": "Clock format", "settings.status_bar.clock_format_label": "Clock format",
"settings.status_bar.clock_format.hm": "Hour:Minute", "settings.status_bar.clock_format.hm": "Hour:Minute",
"settings.status_bar.clock_format.hms": "Hour:Minute:Second", "settings.status_bar.clock_format.hms": "Hour:Minute:Second",
"settings.status_bar.clock_position_label": "Clock position",
"settings.status_bar.clock_position.left": "Left",
"settings.status_bar.clock_position.center": "Center",
"settings.status_bar.clock_position.right": "Right",
"settings.status_bar.text_capsule_header": "Text Capsule",
"settings.status_bar.text_capsule_description": "Display custom text on the status bar with Markdown support.",
"settings.status_bar.text_capsule_position_label": "Text capsule position",
"settings.status_bar.text_capsule_position.left": "Left",
"settings.status_bar.text_capsule_position.center": "Center",
"settings.status_bar.text_capsule_position.right": "Right",
"settings.status_bar.text_capsule_content_label": "Text content (Markdown supported)",
"settings.status_bar.text_capsule_transparent_background_label": "Transparent background",
"settings.components.title": "Components", "settings.components.title": "Components",
"settings.components.description": "Adjust component layout and corner design.", "settings.components.description": "Adjust component layout and corner design.",
"settings.components.grid_header": "Grid Settings", "settings.components.grid_header": "Grid Settings",

View File

@@ -331,6 +331,18 @@
"settings.status_bar.clock_format_label": "時計の形式", "settings.status_bar.clock_format_label": "時計の形式",
"settings.status_bar.clock_format.hm": "時:分", "settings.status_bar.clock_format.hm": "時:分",
"settings.status_bar.clock_format.hms": "時:分:秒", "settings.status_bar.clock_format.hms": "時:分:秒",
"settings.status_bar.clock_position_label": "時計の位置",
"settings.status_bar.clock_position.left": "左",
"settings.status_bar.clock_position.center": "中央",
"settings.status_bar.clock_position.right": "右",
"settings.status_bar.text_capsule_header": "テキストカプセル",
"settings.status_bar.text_capsule_description": "ステータスバーにMarkdown形式のカスタムテキストを表示します。",
"settings.status_bar.text_capsule_position_label": "テキストカプセルの位置",
"settings.status_bar.text_capsule_position.left": "左",
"settings.status_bar.text_capsule_position.center": "中央",
"settings.status_bar.text_capsule_position.right": "右",
"settings.status_bar.text_capsule_content_label": "テキスト内容Markdown対応",
"settings.status_bar.text_capsule_transparent_background_label": "透明な背景",
"settings.components.title": "コンポーネント", "settings.components.title": "コンポーネント",
"settings.components.description": "コンポーネントのレイアウトとコーナーデザインを調整します。", "settings.components.description": "コンポーネントのレイアウトとコーナーデザインを調整します。",
"settings.components.grid_header": "グリッド設定", "settings.components.grid_header": "グリッド設定",

View File

@@ -377,6 +377,18 @@
"settings.status_bar.clock_format_label": "시계 형식", "settings.status_bar.clock_format_label": "시계 형식",
"settings.status_bar.clock_format.hm": "시:분", "settings.status_bar.clock_format.hm": "시:분",
"settings.status_bar.clock_format.hms": "시:분:초", "settings.status_bar.clock_format.hms": "시:분:초",
"settings.status_bar.clock_position_label": "시계 위치",
"settings.status_bar.clock_position.left": "왼쪽",
"settings.status_bar.clock_position.center": "가욍데",
"settings.status_bar.clock_position.right": "오른쪽",
"settings.status_bar.text_capsule_header": "텍스트 캡슐",
"settings.status_bar.text_capsule_description": "Markdown 형식의 사용자 정의 텍스트를 상태 표시줄에 표시합니다.",
"settings.status_bar.text_capsule_position_label": "텍스트 캡슐 위치",
"settings.status_bar.text_capsule_position.left": "왼쪽",
"settings.status_bar.text_capsule_position.center": "가욍데",
"settings.status_bar.text_capsule_position.right": "오른쪽",
"settings.status_bar.text_capsule_content_label": "텍스트 내용 (Markdown 지원)",
"settings.status_bar.text_capsule_transparent_background_label": "투명 배경",
"settings.components.title": "컴포넌트", "settings.components.title": "컴포넌트",
"settings.components.description": "컴포넌트 레이아웃과 모서리 디자인을 조정합니다.", "settings.components.description": "컴포넌트 레이아웃과 모서리 디자인을 조정합니다.",
"settings.components.grid_header": "그리드 설정", "settings.components.grid_header": "그리드 설정",

View File

@@ -383,6 +383,18 @@
"settings.status_bar.clock_format_label": "时钟格式", "settings.status_bar.clock_format_label": "时钟格式",
"settings.status_bar.clock_format.hm": "时:分", "settings.status_bar.clock_format.hm": "时:分",
"settings.status_bar.clock_format.hms": "时:分:秒", "settings.status_bar.clock_format.hms": "时:分:秒",
"settings.status_bar.clock_position_label": "时钟位置",
"settings.status_bar.clock_position.left": "靠左",
"settings.status_bar.clock_position.center": "居中",
"settings.status_bar.clock_position.right": "靠右",
"settings.status_bar.text_capsule_header": "文字胶囊",
"settings.status_bar.text_capsule_description": "在状态栏显示自定义文字,支持 Markdown 格式。",
"settings.status_bar.text_capsule_position_label": "文字胶囊位置",
"settings.status_bar.text_capsule_position.left": "靠左",
"settings.status_bar.text_capsule_position.center": "居中",
"settings.status_bar.text_capsule_position.right": "靠右",
"settings.status_bar.text_capsule_content_label": "文字内容(支持 Markdown",
"settings.status_bar.text_capsule_transparent_background_label": "透明背景",
"settings.components.title": "组件", "settings.components.title": "组件",
"settings.components.description": "调整组件布局与圆角设计。", "settings.components.description": "调整组件布局与圆角设计。",
"settings.components.grid_header": "网格设置", "settings.components.grid_header": "网格设置",

View File

@@ -112,6 +112,16 @@ public sealed class AppSettingsSnapshot
public bool StatusBarClockTransparentBackground { get; set; } public bool StatusBarClockTransparentBackground { get; set; }
public string ClockPosition { get; set; } = "Left"; // Left, Center, Right
public bool ShowTextCapsule { get; set; } = false;
public string TextCapsuleContent { get; set; } = "**Hello** World!";
public string TextCapsulePosition { get; set; } = "Right"; // Left, Center, Right
public bool TextCapsuleTransparentBackground { get; set; } = false;
public string StatusBarSpacingMode { get; set; } = "Relaxed"; public string StatusBarSpacingMode { get; set; } = "Relaxed";
public int StatusBarCustomSpacingPercent { get; set; } = 12; public int StatusBarCustomSpacingPercent { get; set; } = 12;

View File

@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Views;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Services;
/// <summary>
/// 融合桌面中央管理器服务接口
/// </summary>
public interface IFusedDesktopManagerService
{
void Initialize();
void EnterEditMode();
void ExitEditMode();
void ReloadWidgets();
}
/// <summary>
/// 融合桌面中央管理器服务实现。用于管理常态下的各个小窗口实体。
/// </summary>
internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService
{
private readonly IFusedDesktopLayoutService _layoutService;
private readonly ISettingsFacadeService _settingsFacade;
private readonly Dictionary<string, DesktopWidgetWindow> _widgetWindows = [];
// 基础服务依赖
private readonly IWeatherInfoService _weatherDataService;
private readonly TimeZoneService _timeZoneService;
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
private ComponentRegistry? _componentRegistry;
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
private bool _isEditMode;
private const double DefaultCellSize = 100;
public FusedDesktopManagerService(
IFusedDesktopLayoutService layoutService,
ISettingsFacadeService settingsFacade)
{
_layoutService = layoutService;
_settingsFacade = settingsFacade;
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
}
public void Initialize()
{
if (!OperatingSystem.IsWindows()) return;
EnsureRegistries();
ReloadWidgets();
}
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);
}
public void EnterEditMode()
{
if (_isEditMode) return;
_isEditMode = true;
// 隐藏所有底层小窗口
foreach (var window in _widgetWindows.Values)
{
window.Hide();
}
}
public void ExitEditMode()
{
if (!_isEditMode) return;
_isEditMode = false;
// 编辑完成,重新加载布局(可能已发生更改)并显示
ReloadWidgets();
}
public void ReloadWidgets()
{
if (_isEditMode) return; // 编辑模式下不渲染小窗口
var layout = _layoutService.Load();
var existingIds = new HashSet<string>(_widgetWindows.Keys);
foreach (var placement in layout.ComponentPlacements)
{
existingIds.Remove(placement.PlacementId);
if (_widgetWindows.TryGetValue(placement.PlacementId, out var existingWindow))
{
// 已存在,可能只更新位置或尺寸
existingWindow.Position = new Avalonia.PixelPoint((int)placement.X, (int)placement.Y);
if (existingWindow.IsVisible == false)
{
existingWindow.Show();
}
}
else
{
// 新组件,生成窗口
try
{
var window = CreateWidgetWindow(placement);
if (window != null)
{
_widgetWindows[placement.PlacementId] = window;
window.Show();
window.Position = new Avalonia.PixelPoint((int)placement.X, (int)placement.Y);
}
}
catch (Exception ex)
{
AppLogger.Warn("FusedDesktopMgr", $"Failed to render tiny window for {placement.ComponentId}", ex);
}
}
}
// 移除被删除的组件
foreach (var id in existingIds)
{
if (_widgetWindows.Remove(id, out var windowToRemove))
{
windowToRemove.Close();
}
}
}
private DesktopWidgetWindow? CreateWidgetWindow(FusedDesktopComponentPlacementSnapshot placement)
{
EnsureRegistries();
if (_componentRuntimeRegistry is null || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor))
{
AppLogger.Warn("FusedDesktopMgr", $"Unknown component: {placement.ComponentId}");
return null;
}
var control = descriptor.CreateControl(
DefaultCellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
placement.PlacementId);
// 将组件包装到一个具有准确宽高的容器内(如果组件自身没有设置宽度)
control.Width = placement.Width;
control.Height = placement.Height;
var window = new DesktopWidgetWindow(control);
return window;
}
}
/// <summary>
/// 工厂
/// </summary>
public static class FusedDesktopManagerServiceFactory
{
private static IFusedDesktopManagerService? _instance;
private static readonly object _lock = new();
public static IFusedDesktopManagerService GetOrCreate()
{
if (_instance is not null) return _instance;
lock (_lock)
{
var layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
var settings = HostSettingsFacadeProvider.GetOrCreate();
_instance ??= new FusedDesktopManagerService(layoutService, settings);
return _instance;
}
}
}

View File

@@ -41,8 +41,20 @@ public sealed record StatusBarSettingsState(
string TaskbarLayoutMode, string TaskbarLayoutMode,
string ClockDisplayFormat, string ClockDisplayFormat,
bool ClockTransparentBackground, bool ClockTransparentBackground,
string ClockPosition,
bool ShowTextCapsule,
string TextCapsuleContent,
string TextCapsulePosition,
bool TextCapsuleTransparentBackground,
string SpacingMode, string SpacingMode,
int CustomSpacingPercent); int CustomSpacingPercent);
public sealed record TextCapsuleSettingsState(
bool ShowTextCapsule,
string Content,
string Position,
bool TransparentBackground);
public sealed record WeatherSettingsState( public sealed record WeatherSettingsState(
string LocationMode, string LocationMode,
string LocationKey, string LocationKey,
@@ -274,6 +286,12 @@ public interface IStatusBarSettingsService
void Save(StatusBarSettingsState state); void Save(StatusBarSettingsState state);
} }
public interface ITextCapsuleSettingsService
{
TextCapsuleSettingsState Get();
void Save(TextCapsuleSettingsState state);
}
public interface IWeatherProvider public interface IWeatherProvider
{ {
Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync( Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync(
@@ -385,6 +403,7 @@ public interface ISettingsFacadeService
IWallpaperMediaService WallpaperMedia { get; } IWallpaperMediaService WallpaperMedia { get; }
IThemeAppearanceService Theme { get; } IThemeAppearanceService Theme { get; }
IStatusBarSettingsService StatusBar { get; } IStatusBarSettingsService StatusBar { get; }
ITextCapsuleSettingsService TextCapsule { get; }
IWeatherSettingsService Weather { get; } IWeatherSettingsService Weather { get; }
IRegionSettingsService Region { get; } IRegionSettingsService Region { get; }
IPrivacySettingsService Privacy { get; } IPrivacySettingsService Privacy { get; }

View File

@@ -386,6 +386,11 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
snapshot.TaskbarLayoutMode, snapshot.TaskbarLayoutMode,
snapshot.ClockDisplayFormat, snapshot.ClockDisplayFormat,
snapshot.StatusBarClockTransparentBackground, snapshot.StatusBarClockTransparentBackground,
snapshot.ClockPosition,
snapshot.ShowTextCapsule,
snapshot.TextCapsuleContent,
snapshot.TextCapsulePosition,
snapshot.TextCapsuleTransparentBackground,
snapshot.StatusBarSpacingMode, snapshot.StatusBarSpacingMode,
snapshot.StatusBarCustomSpacingPercent); snapshot.StatusBarCustomSpacingPercent);
} }
@@ -399,6 +404,11 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode; snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode;
snapshot.ClockDisplayFormat = state.ClockDisplayFormat; snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
snapshot.StatusBarClockTransparentBackground = state.ClockTransparentBackground; snapshot.StatusBarClockTransparentBackground = state.ClockTransparentBackground;
snapshot.ClockPosition = state.ClockPosition;
snapshot.ShowTextCapsule = state.ShowTextCapsule;
snapshot.TextCapsuleContent = state.TextCapsuleContent;
snapshot.TextCapsulePosition = state.TextCapsulePosition;
snapshot.TextCapsuleTransparentBackground = state.TextCapsuleTransparentBackground;
snapshot.StatusBarSpacingMode = state.SpacingMode; snapshot.StatusBarSpacingMode = state.SpacingMode;
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent; snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
_settingsService.SaveSnapshot( _settingsService.SaveSnapshot(
@@ -412,12 +422,56 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
nameof(AppSettingsSnapshot.TaskbarLayoutMode), nameof(AppSettingsSnapshot.TaskbarLayoutMode),
nameof(AppSettingsSnapshot.ClockDisplayFormat), nameof(AppSettingsSnapshot.ClockDisplayFormat),
nameof(AppSettingsSnapshot.StatusBarClockTransparentBackground), nameof(AppSettingsSnapshot.StatusBarClockTransparentBackground),
nameof(AppSettingsSnapshot.ClockPosition),
nameof(AppSettingsSnapshot.ShowTextCapsule),
nameof(AppSettingsSnapshot.TextCapsuleContent),
nameof(AppSettingsSnapshot.TextCapsulePosition),
nameof(AppSettingsSnapshot.TextCapsuleTransparentBackground),
nameof(AppSettingsSnapshot.StatusBarSpacingMode), nameof(AppSettingsSnapshot.StatusBarSpacingMode),
nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent) nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent)
]); ]);
} }
} }
internal sealed class TextCapsuleSettingsService : ITextCapsuleSettingsService
{
private readonly ISettingsService _settingsService;
public TextCapsuleSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public TextCapsuleSettingsState Get()
{
var snapshot = _settingsService.Load();
return new TextCapsuleSettingsState(
snapshot.ShowTextCapsule,
snapshot.TextCapsuleContent,
snapshot.TextCapsulePosition,
snapshot.TextCapsuleTransparentBackground);
}
public void Save(TextCapsuleSettingsState state)
{
var snapshot = _settingsService.Load();
snapshot.ShowTextCapsule = state.ShowTextCapsule;
snapshot.TextCapsuleContent = state.Content;
snapshot.TextCapsulePosition = state.Position;
snapshot.TextCapsuleTransparentBackground = state.TransparentBackground;
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.ShowTextCapsule),
nameof(AppSettingsSnapshot.TextCapsuleContent),
nameof(AppSettingsSnapshot.TextCapsulePosition),
nameof(AppSettingsSnapshot.TextCapsuleTransparentBackground)
]);
}
}
internal sealed class WeatherProviderAdapter : IWeatherProvider, IWeatherInfoService, IDisposable internal sealed class WeatherProviderAdapter : IWeatherProvider, IWeatherInfoService, IDisposable
{ {
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService(); private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
@@ -1198,6 +1252,7 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
WallpaperMedia = new WallpaperMediaService(); WallpaperMedia = new WallpaperMediaService();
Theme = new ThemeAppearanceService(Settings); Theme = new ThemeAppearanceService(Settings);
StatusBar = new StatusBarSettingsService(Settings); StatusBar = new StatusBarSettingsService(Settings);
TextCapsule = new TextCapsuleSettingsService(Settings);
_weatherSettingsService = new WeatherSettingsService(Settings); _weatherSettingsService = new WeatherSettingsService(Settings);
Weather = _weatherSettingsService; Weather = _weatherSettingsService;
Region = new RegionSettingsService(Settings); Region = new RegionSettingsService(Settings);
@@ -1227,6 +1282,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
public IStatusBarSettingsService StatusBar { get; } public IStatusBarSettingsService StatusBar { get; }
public ITextCapsuleSettingsService TextCapsule { get; }
public IWeatherSettingsService Weather { get; } public IWeatherSettingsService Weather { get; }
public IRegionSettingsService Region { get; } public IRegionSettingsService Region { get; }

View File

@@ -21,6 +21,8 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode); _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
ClockFormats = CreateClockFormats(); ClockFormats = CreateClockFormats();
ClockPositions = CreateClockPositions();
TextCapsulePositions = CreateTextCapsulePositions();
SpacingModes = CreateSpacingModes(); SpacingModes = CreateSpacingModes();
RefreshLocalizedText(); RefreshLocalizedText();
@@ -31,6 +33,10 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
public IReadOnlyList<SelectionOption> ClockFormats { get; } public IReadOnlyList<SelectionOption> ClockFormats { get; }
public IReadOnlyList<SelectionOption> ClockPositions { get; }
public IReadOnlyList<SelectionOption> TextCapsulePositions { get; }
public IReadOnlyList<SelectionOption> SpacingModes { get; } public IReadOnlyList<SelectionOption> SpacingModes { get; }
[ObservableProperty] [ObservableProperty]
@@ -42,6 +48,9 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private bool _clockTransparentBackground; private bool _clockTransparentBackground;
[ObservableProperty]
private SelectionOption _selectedClockPosition = new("Left", "Left");
[ObservableProperty] [ObservableProperty]
private SelectionOption _selectedSpacingMode = new("Relaxed", "Relaxed"); private SelectionOption _selectedSpacingMode = new("Relaxed", "Relaxed");
@@ -75,6 +84,36 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private string _clockTransparentBackgroundDescription = string.Empty; private string _clockTransparentBackgroundDescription = string.Empty;
[ObservableProperty]
private string _clockPositionLabel = string.Empty;
[ObservableProperty]
private string _textCapsuleHeader = string.Empty;
[ObservableProperty]
private string _textCapsuleDescription = string.Empty;
[ObservableProperty]
private bool _showTextCapsule;
[ObservableProperty]
private string _textCapsuleContent = "**Hello** World!";
[ObservableProperty]
private SelectionOption _selectedTextCapsulePosition = new("Right", "Right");
[ObservableProperty]
private bool _textCapsuleTransparentBackground;
[ObservableProperty]
private string _textCapsulePositionLabel = string.Empty;
[ObservableProperty]
private string _textCapsuleContentLabel = string.Empty;
[ObservableProperty]
private string _textCapsuleTransparentBackgroundLabel = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _spacingHeader = string.Empty; private string _spacingHeader = string.Empty;
@@ -99,6 +138,20 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
?? ClockFormats[1]; ?? ClockFormats[1];
ClockTransparentBackground = state.ClockTransparentBackground; ClockTransparentBackground = state.ClockTransparentBackground;
var clockPosition = NormalizeClockPosition(state.ClockPosition);
SelectedClockPosition = ClockPositions.FirstOrDefault(option =>
string.Equals(option.Value, clockPosition, StringComparison.OrdinalIgnoreCase))
?? ClockPositions[0];
// 文字胶囊设置
ShowTextCapsule = state.ShowTextCapsule;
TextCapsuleContent = state.TextCapsuleContent ?? "**Hello** World!";
var textCapsulePosition = NormalizeTextCapsulePosition(state.TextCapsulePosition);
SelectedTextCapsulePosition = TextCapsulePositions.FirstOrDefault(option =>
string.Equals(option.Value, textCapsulePosition, StringComparison.OrdinalIgnoreCase))
?? TextCapsulePositions[2]; // 默认靠右
TextCapsuleTransparentBackground = state.TextCapsuleTransparentBackground;
var spacingMode = NormalizeSpacingMode(state.SpacingMode); var spacingMode = NormalizeSpacingMode(state.SpacingMode);
SelectedSpacingMode = SpacingModes.FirstOrDefault(option => SelectedSpacingMode = SpacingModes.FirstOrDefault(option =>
string.Equals(option.Value, spacingMode, StringComparison.OrdinalIgnoreCase)) string.Equals(option.Value, spacingMode, StringComparison.OrdinalIgnoreCase))
@@ -137,6 +190,56 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
Save(); Save();
} }
partial void OnSelectedClockPositionChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
Save();
}
partial void OnShowTextCapsuleChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnTextCapsuleContentChanged(string value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnSelectedTextCapsulePositionChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
Save();
}
partial void OnTextCapsuleTransparentBackgroundChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnSelectedSpacingModeChanged(SelectionOption value) partial void OnSelectedSpacingModeChanged(SelectionOption value)
{ {
IsCustomSpacingVisible = string.Equals(value?.Value, "Custom", StringComparison.OrdinalIgnoreCase); IsCustomSpacingVisible = string.Equals(value?.Value, "Custom", StringComparison.OrdinalIgnoreCase);
@@ -184,6 +287,11 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
state.TaskbarLayoutMode, state.TaskbarLayoutMode,
SelectedClockFormat.Value, SelectedClockFormat.Value,
ClockTransparentBackground, ClockTransparentBackground,
SelectedClockPosition.Value,
ShowTextCapsule,
TextCapsuleContent ?? "**Hello** World!",
SelectedTextCapsulePosition?.Value ?? "Right",
TextCapsuleTransparentBackground,
NormalizeSpacingMode(SelectedSpacingMode.Value), NormalizeSpacingMode(SelectedSpacingMode.Value),
Math.Clamp(CustomSpacingPercent, 0, 30))); Math.Clamp(CustomSpacingPercent, 0, 30)));
} }
@@ -197,6 +305,26 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
]; ];
} }
private IReadOnlyList<SelectionOption> CreateClockPositions()
{
return
[
new SelectionOption("Left", L("settings.status_bar.clock_position.left", "Left")),
new SelectionOption("Center", L("settings.status_bar.clock_position.center", "Center")),
new SelectionOption("Right", L("settings.status_bar.clock_position.right", "Right"))
];
}
private IReadOnlyList<SelectionOption> CreateTextCapsulePositions()
{
return
[
new SelectionOption("Left", L("settings.status_bar.text_capsule_position.left", "Left")),
new SelectionOption("Center", L("settings.status_bar.text_capsule_position.center", "Center")),
new SelectionOption("Right", L("settings.status_bar.text_capsule_position.right", "Right"))
];
}
private IReadOnlyList<SelectionOption> CreateSpacingModes() private IReadOnlyList<SelectionOption> CreateSpacingModes()
{ {
return return
@@ -217,6 +345,12 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock format"); ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock format");
ClockTransparentBackgroundLabel = L("settings.status_bar.clock_transparent_background_label", "Transparent background"); ClockTransparentBackgroundLabel = L("settings.status_bar.clock_transparent_background_label", "Transparent background");
ClockTransparentBackgroundDescription = L("settings.status_bar.clock_transparent_background_desc", "Remove the capsule background and keep only the clock text."); ClockTransparentBackgroundDescription = L("settings.status_bar.clock_transparent_background_desc", "Remove the capsule background and keep only the clock text.");
ClockPositionLabel = L("settings.status_bar.clock_position_label", "Clock position");
TextCapsuleHeader = L("settings.status_bar.text_capsule_header", "Text Capsule");
TextCapsuleDescription = L("settings.status_bar.text_capsule_description", "Display custom text with Markdown support on the status bar.");
TextCapsulePositionLabel = L("settings.status_bar.text_capsule_position_label", "Text capsule position");
TextCapsuleContentLabel = L("settings.status_bar.text_capsule_content_label", "Text content (Markdown supported)");
TextCapsuleTransparentBackgroundLabel = L("settings.status_bar.text_capsule_transparent_background_label", "Transparent background");
SpacingHeader = L("settings.status_bar.spacing_header", "Component Spacing"); SpacingHeader = L("settings.status_bar.spacing_header", "Component Spacing");
SpacingDescription = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components."); SpacingDescription = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
CustomSpacingLabel = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)"); CustomSpacingLabel = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
@@ -232,6 +366,26 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
}; };
} }
private static string NormalizeClockPosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ when string.Equals(value, "Right", StringComparison.OrdinalIgnoreCase) => "Right",
_ => "Left"
};
}
private static string NormalizeTextCapsulePosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Left", StringComparison.OrdinalIgnoreCase) => "Left",
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ => "Right"
};
}
private string L(string key, string fallback) private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback); => _localizationService.GetString(_languageCode, key, fallback);
} }

View File

@@ -220,7 +220,7 @@ public partial class ComponentLibraryWindow : Window
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase)) if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
{ {
return Symbol.Apps; return Symbol.Info;
} }
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase)) if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))

View File

@@ -0,0 +1,22 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:md="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
mc:Ignorable="d"
d:DesignWidth="200"
d:DesignHeight="48"
x:Class="LanMountainDesktop.Views.Components.TextCapsuleWidget">
<Border x:Name="RootBorder"
Classes="surface-translucent-panel"
Padding="0"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}">
<md:MarkdownScrollViewer x:Name="MarkdownViewer"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="12,6"
MaxWidth="400" />
</Border>
</UserControl>

View File

@@ -0,0 +1,167 @@
using System;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using LanMountainDesktop.Services;
using Markdown.Avalonia;
namespace LanMountainDesktop.Views.Components;
public partial class TextCapsuleWidget : UserControl, IDesktopComponentWidget
{
private string _text = string.Empty;
private bool _transparentBackground;
private double _lastAppliedCellSize = 100;
private CancellationTokenSource? _debounceCts;
public TextCapsuleWidget()
{
InitializeComponent();
UpdateDisplay();
}
public string Text
{
get => _text;
set
{
if (_text == value)
{
return;
}
_text = value;
DebouncedUpdateDisplay();
}
}
public bool TransparentBackground
{
get => _transparentBackground;
set
{
if (_transparentBackground == value)
{
return;
}
_transparentBackground = value;
ApplyChrome();
ApplyCellSize(_lastAppliedCellSize);
}
}
public void SetText(string text)
{
Text = text;
}
public void SetTransparentBackground(bool transparentBackground)
{
TransparentBackground = transparentBackground;
}
private void DebouncedUpdateDisplay()
{
// 取消之前的延迟任务
_debounceCts?.Cancel();
_debounceCts?.Dispose();
_debounceCts = new CancellationTokenSource();
var token = _debounceCts.Token;
// 延迟 150ms 后更新显示,避免频繁输入时过度渲染
Dispatcher.UIThread.Post(async () =>
{
try
{
await System.Threading.Tasks.Task.Delay(150, token);
if (!token.IsCancellationRequested)
{
UpdateDisplay();
}
}
catch (OperationCanceledException)
{
// 忽略取消异常
}
});
}
private void UpdateDisplay()
{
try
{
if (string.IsNullOrWhiteSpace(_text))
{
MarkdownViewer.Markdown = "*Empty*";
return;
}
// 使用 Markdown 引擎渲染文本
MarkdownViewer.Markdown = _text;
}
catch (Exception ex)
{
// 错误处理:显示错误信息而不是崩溃
MarkdownViewer.Markdown = $"*Error: {ex.Message}*";
}
}
public void ApplyCellSize(double cellSize)
{
_lastAppliedCellSize = cellSize;
// 计算组件高度:保持与任务栏核心比例一致 (0.74x)
var targetHeight = Math.Clamp(cellSize * 0.74, 34, 74);
RootBorder.Height = targetHeight;
// 主矩形统一到主题主档圆角
RootBorder.CornerRadius = ResolveUnifiedMainRectangle();
RootBorder.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
// 设置最小和最大宽度
RootBorder.MinWidth = cellSize * 1.5;
RootBorder.MaxWidth = cellSize * 6;
if (_transparentBackground)
{
RootBorder.MinWidth = 0;
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 4, 10), 0);
return;
}
// 确保清除可能存在的固定 Padding由代码控制"紧密感"
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15, 12, 24), 0);
}
private void ApplyChrome()
{
if (_transparentBackground)
{
RootBorder.Classes.Remove("glass-panel");
RootBorder.Background = Brushes.Transparent;
RootBorder.BorderBrush = Brushes.Transparent;
RootBorder.BorderThickness = new Thickness(0);
RootBorder.BoxShadow = default;
return;
}
if (!RootBorder.Classes.Contains("glass-panel"))
{
RootBorder.Classes.Add("glass-panel");
}
RootBorder.ClearValue(Border.BackgroundProperty);
RootBorder.ClearValue(Border.BorderBrushProperty);
RootBorder.ClearValue(Border.BorderThicknessProperty);
RootBorder.ClearValue(Border.BoxShadowProperty);
}
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
private static double ResolveUnifiedMainRadiusValue() =>
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
}

View File

@@ -0,0 +1,23 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="LanMountainDesktop.Views.DesktopWidgetWindow"
Title="Desktop Component"
ShowInTaskbar="False"
SystemDecorations="None"
Background="Transparent"
Topmost="False"
SizeToContent="WidthAndHeight"
TransparencyLevelHint="Transparent"
RenderOptions.BitmapInterpolationMode="HighQuality"
CanResize="False">
<Border x:Name="ComponentContainer"
Background="Transparent"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
ClipToBounds="True">
<!-- Component control will be injected here -->
</Border>
</Window>

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using LanMountainDesktop.Services;
using Avalonia.Threading;
namespace LanMountainDesktop.Views;
/// <summary>
/// 表示一个独立的组件挂载窗口。它不含有任何自己的边窗,仅仅负责包裹组件并将自身植入系统最底层。
/// </summary>
public partial class DesktopWidgetWindow : Window
{
private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate();
private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate();
public DesktopWidgetWindow()
{
InitializeComponent();
}
public DesktopWidgetWindow(Control componentContent) : this()
{
ComponentContainer.Child = componentContent;
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if (OperatingSystem.IsWindows())
{
// 通过现有的置底服务将独立的小窗口锁定到底层
_bottomMostService.SetupBottomMost(this);
_bottomMostService.SendToBottom(this);
// 当窗口展示完毕且有了尺寸后,更新可交互区域,使得整个组件都能被点击
Dispatcher.UIThread.Post(UpdateInteractiveRegion, DispatcherPriority.Render);
}
}
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
if (OperatingSystem.IsWindows() && IsVisible)
{
UpdateInteractiveRegion();
}
}
private void UpdateInteractiveRegion()
{
// 既然是一个完全紧贴在组件身上的小窗,它的全部都是可交互的
_regionPassthroughService.SetInteractiveRegions(this, new List<Rect>
{
new(0, 0, Bounds.Width, Bounds.Height)
});
}
}

View File

@@ -1,15 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:fi="using:FluentIcons.Avalonia" xmlns:fi="using:FluentIcons.Avalonia"
xmlns:local="using:LanMountainDesktop.Views" x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"> x:DataType="vm:ComponentLibraryWindowViewModel">
<Grid ColumnDefinitions="220,*"> <Grid ColumnDefinitions="240,*"
ColumnSpacing="12"
Margin="0">
<!-- 分类列表 (左侧) --> <!-- 分类列表 (左侧) -->
<Border Grid.Column="0" <Border Classes="surface-translucent-panel"
BorderBrush="{DynamicResource AdaptiveBorderBrush}" CornerRadius="{DynamicResource DesignCornerRadiusLg}"
BorderThickness="0,0,1,0" Padding="10">
Padding="12,0,12,12">
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<TextBox x:Name="SearchBox" <TextBox x:Name="SearchBox"
Watermark="搜索组件..." Watermark="搜索组件..."
@@ -27,87 +29,132 @@
Grid.Row="1" Grid.Row="1"
Background="Transparent" Background="Transparent"
BorderThickness="0" BorderThickness="0"
SelectionChanged="OnCategorySelectionChanged"> SelectionChanged="OnCategorySelectionChanged"
<ListBox.Styles> ItemsSource="{Binding Categories}">
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Margin" Value="0,2" />
<Setter Property="Padding" Value="12,10" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="local:LibraryCategoryItem"> <DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
<StackPanel Orientation="Horizontal" Spacing="12"> <Border Padding="10"
<fi:SymbolIcon Symbol="{Binding Icon}" FontSize="18" /> Margin="0,0,0,6"
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" /> CornerRadius="{DynamicResource DesignCornerRadiusSm}"
</StackPanel> Background="{DynamicResource AdaptiveNavItemBackgroundBrush}">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<fi:SymbolIcon Symbol="{Binding Icon}"
IconVariant="Regular"
FontSize="16" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding Title}" />
</Grid>
</Border>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</Grid> </Grid>
</Border> </Border>
<!-- 组件网格 (右侧) --> <!-- 组件网格 (右侧) -->
<ScrollViewer Grid.Column="1" Padding="20"> <Border Grid.Column="1"
<ItemsControl x:Name="ComponentItemsControl"> Classes="surface-translucent-strong"
<ItemsControl.ItemsPanel> CornerRadius="{DynamicResource DesignCornerRadiusLg}"
<ItemsPanelTemplate> Padding="10">
<WrapPanel Orientation="Horizontal" /> <ScrollViewer VerticalScrollBarVisibility="Auto"
</ItemsPanelTemplate> HorizontalScrollBarVisibility="Disabled">
</ItemsControl.ItemsPanel> <ItemsControl x:Name="ComponentItemsControl"
<ItemsControl.ItemTemplate> ItemsSource="{Binding Components}">
<DataTemplate x:DataType="local:LibraryComponentItem"> <ItemsControl.ItemsPanel>
<Button Classes="unstyled-card" <ItemsPanelTemplate>
Width="260" <WrapPanel Orientation="Horizontal" />
Margin="0,0,16,16" </ItemsPanelTemplate>
Padding="0" </ItemsControl.ItemsPanel>
Background="Transparent"
BorderThickness="0" <ItemsControl.ItemTemplate>
Click="OnAddComponentClick" <DataTemplate x:DataType="vm:ComponentLibraryItemViewModel">
Tag="{Binding Id}"> <Border Width="240"
<Border Classes="card" Height="220"
Background="{DynamicResource AdaptiveSurfaceLowBrush}" Margin="6"
BorderBrush="{DynamicResource AdaptiveBorderBrush}" CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
BorderThickness="1" Padding="10"
CornerRadius="24" Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
ClipToBounds="True"> BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
<Grid RowDefinitions="Auto,Auto"> BorderThickness="1">
<!-- 预览区域 (动态填充预览) --> <Grid RowDefinitions="*,Auto,Auto"
<Border x:Name="PreviewHost" RowSpacing="8">
Height="150" <!-- 预览区域 -->
Background="{DynamicResource AdaptiveSurfaceNeutralBrush}" <Border CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Margin="8" Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
CornerRadius="16" BorderThickness="1"
ClipToBounds="True"> BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
<Panel> Padding="8">
<!-- 这里将显示组件的缩放预览 --> <Grid>
<ContentPresenter Content="{Binding PreviewContent}" /> <Image Source="{Binding PreviewBitmap}"
Stretch="Uniform"
<!-- 空状态或加载中图标 --> HorizontalAlignment="Stretch"
<fi:SymbolIcon Symbol="Cube" VerticalAlignment="Stretch"
FontSize="32" RenderOptions.BitmapInterpolationMode="HighQuality"
Opacity="0.1" IsVisible="{Binding IsPreviewReady}" />
IsVisible="{Binding !HasPreview}" />
</Panel> <!-- 加载中状态 -->
<Border IsVisible="{Binding IsPreviewPending}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<ProgressBar Width="96"
IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding PreviewStatusText}" />
</StackPanel>
</Border>
<!-- 失败状态 -->
<Border IsVisible="{Binding IsPreviewFailed}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding PreviewStatusText}" />
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding PreviewErrorMessage}" />
</StackPanel>
</Border>
</Grid>
</Border> </Border>
<!-- 文字说明 --> <!-- 组件名称 -->
<StackPanel Grid.Row="1" Margin="16,8,16,16" Spacing="4"> <TextBlock Grid.Row="1"
<TextBlock Text="{Binding DisplayName}" HorizontalAlignment="Center"
FontWeight="SemiBold" FontWeight="SemiBold"
FontSize="15" /> Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
<TextBlock Text="{Binding Description}" Text="{Binding DisplayName}" />
Opacity="0.6"
FontSize="12" <!-- 添加按钮 -->
TextWrapping="Wrap" <Button Grid.Row="2"
MaxLines="2" /> HorizontalAlignment="Center"
</StackPanel> Padding="12,6"
Tag="{Binding ComponentId}"
Click="OnAddComponentClick">
<TextBlock Text="添加到桌面" />
</Button>
</Grid> </Grid>
</Border> </Border>
</Button> </DataTemplate>
</DataTemplate> </ItemsControl.ItemTemplate>
</ItemsControl.ItemTemplate> </ItemsControl>
</ItemsControl> </ScrollViewer>
</ScrollViewer> </Border>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -1,18 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using FluentIcons.Common;
using Avalonia.Media;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views.Components; using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Models; using Avalonia.Controls.ApplicationLifetimes;
namespace LanMountainDesktop.Views; namespace LanMountainDesktop.Views;
@@ -20,10 +18,9 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
{ {
public event EventHandler<string>? AddComponentRequested; public event EventHandler<string>? AddComponentRequested;
private readonly ObservableCollection<LibraryCategoryItem> _categories = new(); private readonly ComponentLibraryWindowViewModel _viewModel = new();
private readonly ObservableCollection<LibraryComponentItem> _components = new();
private List<DesktopComponentDefinition> _allDefinitions = new(); private List<DesktopComponentDefinition> _allDefinitions = new();
private ComponentRegistry? _componentRegistry; private ComponentRegistry? _componentRegistry;
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry; private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
@@ -35,18 +32,17 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
public FusedDesktopComponentLibraryControl() public FusedDesktopComponentLibraryControl()
{ {
InitializeComponent(); InitializeComponent();
DataContext = _viewModel;
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService(); _weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService(); _timeZoneService = _settingsFacade.Region.GetTimeZoneService();
CategoryListBox.ItemsSource = _categories;
ComponentItemsControl.ItemsSource = _components;
LoadRegistry(); LoadRegistry();
LoadCategories(); LoadCategories();
SearchBox.KeyUp += (s, e) => FilterComponents(); SearchBox.KeyUp += (s, e) => FilterComponents();
// 默认选择第一个分类 // 默认选择第一个分类
if (_categories.Count > 0) if (_viewModel.Categories.Count > 0)
{ {
CategoryListBox.SelectedIndex = 0; CategoryListBox.SelectedIndex = 0;
} }
@@ -60,7 +56,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
_componentRegistry, _componentRegistry,
pluginRuntimeService, pluginRuntimeService,
_settingsFacade); _settingsFacade);
_allDefinitions = _componentRegistry.GetAll() _allDefinitions = _componentRegistry.GetAll()
.Where(d => d.AllowDesktopPlacement) .Where(d => d.AllowDesktopPlacement)
.ToList(); .ToList();
@@ -68,18 +64,27 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
private void LoadCategories() private void LoadCategories()
{ {
_categories.Clear(); _viewModel.Categories.Clear();
_categories.Add(new LibraryCategoryItem("all", "全部组件", "Apps")); _viewModel.Components.Clear();
var categoryMap = new Dictionary<string, (string Display, string Icon)> // 添加"全部组件"分类
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
"all",
"全部组件",
Symbol.Apps,
Array.Empty<ComponentLibraryItemViewModel>()));
var categoryMap = new Dictionary<string, (string Display, Symbol Icon)>
{ {
{ "clock", ("时钟", "Clock") }, { "clock", ("时钟", Symbol.Clock) },
{ "date", ("日历", "Calendar") }, { "date", ("日历", Symbol.CalendarDate) },
{ "weather", ("天气", "WeatherCloudy") }, { "weather", ("天气", Symbol.WeatherSunny) },
{ "info", ("资讯", "News") }, { "board", ("画板", Symbol.Edit) },
{ "calculator", ("工具", "Calculator") }, { "media", ("媒体", Symbol.Play) },
{ "study", ("学习", "Book") }, { "info", ("资讯", Symbol.News) },
{ "file", ("文件", "Document") } { "calculator", ("工具", Symbol.Calculator) },
{ "study", ("学习", Symbol.Hourglass) },
{ "file", ("文件", Symbol.Folder) }
}; };
var usedCategories = _allDefinitions var usedCategories = _allDefinitions
@@ -91,11 +96,62 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
{ {
if (categoryMap.TryGetValue(cat.ToLower(), out var info)) if (categoryMap.TryGetValue(cat.ToLower(), out var info))
{ {
_categories.Add(new LibraryCategoryItem(cat, info.Display, info.Icon)); var categoryComponents = _allDefinitions
.Where(d => string.Equals(d.Category, cat, StringComparison.OrdinalIgnoreCase))
.OrderBy(d => d.DisplayName)
.Select(d => CreateComponentItem(d))
.ToArray();
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
cat,
info.Display,
info.Icon,
categoryComponents));
} }
else }
}
private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition)
{
var previewKey = ComponentPreviewKey.ForComponentType(
definition.Id,
definition.MinWidthCells,
definition.MinHeightCells);
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
ComponentPreviewImageEntry? previewEntry = null;
if (mainWindow is not null)
{
previewEntry = mainWindow.GetPreviewEntry(previewKey);
}
var item = new ComponentLibraryItemViewModel(
definition.Id,
definition.DisplayName,
previewKey,
"正在加载预览...",
"预览不可用",
previewEntry);
if (mainWindow is not null && (previewEntry is null || previewEntry.State == ComponentPreviewImageState.Pending))
{
mainWindow.RequestDetachedLibraryPreview(previewKey);
}
return item;
}
public void UpdatePreviewImage(ComponentPreviewImageEntry entry)
{
foreach (var category in _viewModel.Categories)
{
foreach (var component in category.Components)
{ {
_categories.Add(new LibraryCategoryItem(cat, cat, "Cube")); if (component.PreviewKey.Equals(entry.Key))
{
component.UpdatePreviewImageEntry(entry);
}
} }
} }
} }
@@ -107,7 +163,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
private void FilterComponents() private void FilterComponents()
{ {
var selectedCategory = (CategoryListBox.SelectedItem as LibraryCategoryItem)?.Id; var selectedCategory = (CategoryListBox.SelectedItem as ComponentLibraryCategoryViewModel)?.Id;
var searchText = SearchBox.Text?.ToLower() ?? ""; var searchText = SearchBox.Text?.ToLower() ?? "";
var filtered = _allDefinitions.Where(d => var filtered = _allDefinitions.Where(d =>
@@ -117,57 +173,10 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
return matchesCategory && matchesSearch; return matchesCategory && matchesSearch;
}); });
_components.Clear(); _viewModel.Components.Clear();
foreach (var def in filtered) foreach (var def in filtered)
{ {
_components.Add(new LibraryComponentItem _viewModel.Components.Add(CreateComponentItem(def));
{
Id = def.Id,
DisplayName = def.DisplayName,
Description = GetDescription(def.Id),
PreviewContent = CreatePreview(def.Id)
});
}
}
private string GetDescription(string id)
{
// 简单映射描述信息
return id.Contains("clock") ? "实时显示当前时间与日期。" :
id.Contains("weather") ? "为您提供精准的天气预报。" :
"多功能桌面组件,提升您的操作效率。";
}
private Control? CreatePreview(string id)
{
if (_componentRuntimeRegistry == null || !_componentRuntimeRegistry.TryGetDescriptor(id, out var descriptor))
{
return null;
}
try
{
var control = descriptor.CreateControl(
100, // Previews assume 100px base
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
"preview_" + id);
control.IsHitTestVisible = false;
return new Viewbox
{
Child = control,
Stretch = Stretch.Uniform,
Margin = new Thickness(12)
};
}
catch (Exception ex)
{
return new TextBlock { Text = "无法预览", VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center };
} }
} }
@@ -179,15 +188,3 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
} }
} }
} }
public record LibraryCategoryItem(string Id, string DisplayName, string Icon);
public class LibraryComponentItem
{
public string Id { get; set; } = "";
public string DisplayName { get; set; } = "";
public string Description { get; set; } = "";
public Control? PreviewContent { get; set; }
public bool HasPreview => PreviewContent != null;
}

View File

@@ -1,9 +1,11 @@
using System; using System;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using Avalonia.Controls.ApplicationLifetimes;
namespace LanMountainDesktop.Views; namespace LanMountainDesktop.Views;
@@ -26,6 +28,9 @@ public partial class FusedDesktopComponentLibraryWindow : Window
InitializeComponent(); InitializeComponent();
LibraryControl.AddComponentRequested += OnAddComponentRequested; LibraryControl.AddComponentRequested += OnAddComponentRequested;
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
mainWindow?.RegisterFusedLibraryWindow(this);
} }
/// <summary> /// <summary>
@@ -97,4 +102,16 @@ public partial class FusedDesktopComponentLibraryWindow : Window
{ {
Close(); Close();
} }
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
mainWindow?.UnregisterFusedLibraryWindow(this);
}
public void UpdatePreviewImage(ComponentPreviewImageEntry entry)
{
LibraryControl.UpdatePreviewImage(entry);
}
} }

View File

@@ -24,6 +24,7 @@ public partial class MainWindow
private readonly IComponentPreviewImageService _componentPreviewImageService = new ComponentPreviewImageService(); private readonly IComponentPreviewImageService _componentPreviewImageService = new ComponentPreviewImageService();
private readonly Dictionary<ComponentPreviewKey, List<ComponentLibraryPreviewVisualTarget>> _componentLibraryPreviewVisualTargets = new(ComponentPreviewKeyComparer.Instance); private readonly Dictionary<ComponentPreviewKey, List<ComponentLibraryPreviewVisualTarget>> _componentLibraryPreviewVisualTargets = new(ComponentPreviewKeyComparer.Instance);
private bool _componentLibraryPreviewWarmupStarted; private bool _componentLibraryPreviewWarmupStarted;
private FusedDesktopComponentLibraryWindow? _fusedLibraryWindow;
private sealed record ComponentLibraryPreviewVisualTarget(Image Image, Control Fallback); private sealed record ComponentLibraryPreviewVisualTarget(Image Image, Control Fallback);
@@ -519,6 +520,7 @@ public partial class MainWindow
{ {
ApplyPreviewEntryToEmbeddedVisuals(entry.Key); ApplyPreviewEntryToEmbeddedVisuals(entry.Key);
_detachedComponentLibraryWindow?.UpdatePreviewImage(entry); _detachedComponentLibraryWindow?.UpdatePreviewImage(entry);
_fusedLibraryWindow?.UpdatePreviewImage(entry);
if (entry.Key.Kind == ComponentPreviewKeyKind.PlacementInstance) if (entry.Key.Kind == ComponentPreviewKeyKind.PlacementInstance)
{ {
@@ -597,4 +599,30 @@ public partial class MainWindow
action: "DetachedLibraryRender", action: "DetachedLibraryRender",
forceRefresh: false); forceRefresh: false);
} }
// FusedDesktop 支持
public void RegisterFusedLibraryWindow(FusedDesktopComponentLibraryWindow window)
{
_fusedLibraryWindow = window;
}
public void UnregisterFusedLibraryWindow(FusedDesktopComponentLibraryWindow window)
{
if (ReferenceEquals(_fusedLibraryWindow, window))
{
_fusedLibraryWindow = null;
}
}
public ComponentPreviewImageEntry? GetPreviewEntry(ComponentPreviewKey key)
{
return ResolvePreviewEntry(key);
}
public void RequestDetachedLibraryPreview(ComponentPreviewKey key)
{
RequestDetachedLibraryPreviewWarm(key);
RequestDetachedLibraryPreviewRender(key);
}
} }

View File

@@ -364,12 +364,295 @@ public partial class MainWindow
? ClockDisplayFormat.HourMinute ? ClockDisplayFormat.HourMinute
: ClockDisplayFormat.HourMinuteSecond; : ClockDisplayFormat.HourMinuteSecond;
_statusBarClockTransparentBackground = snapshot.StatusBarClockTransparentBackground; _statusBarClockTransparentBackground = snapshot.StatusBarClockTransparentBackground;
_clockPosition = NormalizeClockPosition(snapshot.ClockPosition);
if (ClockWidget is not null) _showTextCapsule = snapshot.ShowTextCapsule;
_textCapsuleContent = snapshot.TextCapsuleContent ?? "**Hello** World!";
_textCapsulePosition = NormalizeTextCapsulePosition(snapshot.TextCapsulePosition);
_textCapsuleTransparentBackground = snapshot.TextCapsuleTransparentBackground;
ApplyClockSettingsToAllWidgets();
ApplyTextCapsuleSettingsToAllWidgets();
}
private void ApplyClockSettingsToAllWidgets()
{
if (ClockWidgetLeft is not null)
{ {
ClockWidget.SetDisplayFormat(_clockDisplayFormat); ClockWidgetLeft.SetDisplayFormat(_clockDisplayFormat);
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground); ClockWidgetLeft.SetTransparentBackground(_statusBarClockTransparentBackground);
} }
if (ClockWidgetCenter is not null)
{
ClockWidgetCenter.SetDisplayFormat(_clockDisplayFormat);
ClockWidgetCenter.SetTransparentBackground(_statusBarClockTransparentBackground);
}
if (ClockWidgetRight is not null)
{
ClockWidgetRight.SetDisplayFormat(_clockDisplayFormat);
ClockWidgetRight.SetTransparentBackground(_statusBarClockTransparentBackground);
}
}
private static string NormalizeClockPosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ when string.Equals(value, "Right", StringComparison.OrdinalIgnoreCase) => "Right",
_ => "Left"
};
}
private void ApplyTextCapsuleSettingsToAllWidgets()
{
if (TextCapsuleWidgetLeft is not null)
{
TextCapsuleWidgetLeft.SetText(_textCapsuleContent);
TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground);
}
if (TextCapsuleWidgetCenter is not null)
{
TextCapsuleWidgetCenter.SetText(_textCapsuleContent);
TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground);
}
if (TextCapsuleWidgetRight is not null)
{
TextCapsuleWidgetRight.SetText(_textCapsuleContent);
TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground);
}
}
private static string NormalizeTextCapsulePosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ when string.Equals(value, "Left", StringComparison.OrdinalIgnoreCase) => "Left",
_ => "Right"
};
}
/// <summary>
/// 检测状态栏组件是否会发生碰撞
/// </summary>
private bool WouldComponentsCollide()
{
if (TopStatusBarHost is null)
return false;
// 获取各区域当前占用的宽度
var leftWidth = GetLeftPanelOccupiedWidth();
var centerWidth = GetCenterPanelOccupiedWidth();
var rightWidth = GetRightPanelOccupiedWidth();
// 获取状态栏总宽度
var totalWidth = TopStatusBarHost.Bounds.Width;
if (totalWidth <= 0)
return false;
// 计算中间区域的实际位置
// 左列是 *, 中列是 Auto, 右列是 *
// 中间区域居中显示
var centerLeft = (totalWidth - centerWidth) / 2;
var centerRight = centerLeft + centerWidth;
// 安全间距(像素)
const double safetyMargin = 20;
// 检测左侧组件是否会与中间区域碰撞
// 左侧组件右边界 = leftWidth
// 中间区域左边界 = centerLeft
if (leftWidth + safetyMargin > centerLeft)
{
return true;
}
// 检测右侧组件是否会与中间区域碰撞
// 右侧组件左边界 = totalWidth - rightWidth
// 中间区域右边界 = centerRight
if (totalWidth - rightWidth - safetyMargin < centerRight)
{
return true;
}
// 检测中间区域是否会与左右两侧碰撞(中间区域过宽)
if (centerLeft < leftWidth + safetyMargin ||
centerRight > totalWidth - rightWidth - safetyMargin)
{
return true;
}
return false;
}
/// <summary>
/// 获取左侧面板占用的宽度(包括间距)
/// </summary>
private double GetLeftPanelOccupiedWidth()
{
if (TopStatusLeftPanel is null)
return 0;
var spacing = TopStatusLeftPanel.Spacing;
var width = 0.0;
var visibleCount = 0;
foreach (var child in TopStatusLeftPanel.Children)
{
if (child is Control control && control.IsVisible)
{
width += control.Bounds.Width;
visibleCount++;
}
}
// 添加间距
if (visibleCount > 1)
{
width += spacing * (visibleCount - 1);
}
return width;
}
/// <summary>
/// 获取中间面板占用的宽度(包括间距)
/// </summary>
private double GetCenterPanelOccupiedWidth()
{
if (TopStatusCenterPanel is null)
return 0;
var spacing = TopStatusCenterPanel.Spacing;
var width = 0.0;
var visibleCount = 0;
foreach (var child in TopStatusCenterPanel.Children)
{
if (child is Control control && control.IsVisible)
{
width += control.Bounds.Width;
visibleCount++;
}
}
// 添加间距
if (visibleCount > 1)
{
width += spacing * (visibleCount - 1);
}
return width;
}
/// <summary>
/// 获取右侧面板占用的宽度(包括间距)
/// </summary>
private double GetRightPanelOccupiedWidth()
{
if (TopStatusRightPanel is null)
return 0;
var spacing = TopStatusRightPanel.Spacing;
var width = 0.0;
var visibleCount = 0;
foreach (var child in TopStatusRightPanel.Children)
{
if (child is Control control && control.IsVisible)
{
width += control.Bounds.Width;
visibleCount++;
}
}
// 添加间距
if (visibleCount > 1)
{
width += spacing * (visibleCount - 1);
}
return width;
}
/// <summary>
/// 检查是否可以在指定位置添加组件
/// </summary>
private bool CanAddComponentAtPosition(string position)
{
// 先临时显示组件以计算宽度
var wouldCollide = WouldComponentsCollide();
if (!wouldCollide)
return true;
// 如果会发生碰撞,检查是否是因为目标位置导致的
// 获取当前各区域宽度
var leftWidth = GetLeftPanelOccupiedWidth();
var centerWidth = GetCenterPanelOccupiedWidth();
var rightWidth = GetRightPanelOccupiedWidth();
// 估算新组件的宽度(基于当前单元格大小)
var estimatedNewComponentWidth = _currentDesktopCellSize > 0 ? _currentDesktopCellSize * 2 : 120;
// 根据目标位置检查添加后是否会碰撞
return position switch
{
"Left" => CanAddToLeft(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth),
"Center" => CanAddToCenter(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth),
"Right" => CanAddToRight(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth),
_ => false
};
}
private bool CanAddToLeft(double leftWidth, double centerWidth, double rightWidth, double newWidth)
{
if (TopStatusBarHost is null)
return false;
var totalWidth = TopStatusBarHost.Bounds.Width;
if (totalWidth <= 0)
return true;
var newLeftWidth = leftWidth + newWidth + TopStatusLeftPanel?.Spacing ?? 6;
var centerLeft = (totalWidth - centerWidth) / 2;
const double safetyMargin = 20;
return newLeftWidth + safetyMargin <= centerLeft;
}
private bool CanAddToCenter(double leftWidth, double centerWidth, double rightWidth, double newWidth)
{
if (TopStatusBarHost is null)
return false;
var totalWidth = TopStatusBarHost.Bounds.Width;
if (totalWidth <= 0)
return true;
var newCenterWidth = centerWidth + newWidth + TopStatusCenterPanel?.Spacing ?? 6;
var centerLeft = (totalWidth - newCenterWidth) / 2;
var centerRight = centerLeft + newCenterWidth;
const double safetyMargin = 20;
return centerLeft >= leftWidth + safetyMargin &&
centerRight <= totalWidth - rightWidth - safetyMargin;
}
private bool CanAddToRight(double leftWidth, double centerWidth, double rightWidth, double newWidth)
{
if (TopStatusBarHost is null)
return false;
var totalWidth = TopStatusBarHost.Bounds.Width;
if (totalWidth <= 0)
return true;
var newRightWidth = rightWidth + newWidth + TopStatusRightPanel?.Spacing ?? 6;
var centerRight = (totalWidth + centerWidth) / 2;
const double safetyMargin = 20;
return totalWidth - newRightWidth - safetyMargin >= centerRight;
} }
private void ApplyTopStatusComponentVisibility() private void ApplyTopStatusComponentVisibility()
@@ -377,16 +660,113 @@ public partial class MainWindow
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock); var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
var hasVisibleTopStatusComponent = false; var hasVisibleTopStatusComponent = false;
if (ClockWidget is not null) // 先隐藏所有时钟控件
if (ClockWidgetLeft is not null)
ClockWidgetLeft.IsVisible = false;
if (ClockWidgetCenter is not null)
ClockWidgetCenter.IsVisible = false;
if (ClockWidgetRight is not null)
ClockWidgetRight.IsVisible = false;
// 先隐藏所有文字胶囊控件
if (TextCapsuleWidgetLeft is not null)
TextCapsuleWidgetLeft.IsVisible = false;
if (TextCapsuleWidgetCenter is not null)
TextCapsuleWidgetCenter.IsVisible = false;
if (TextCapsuleWidgetRight is not null)
TextCapsuleWidgetRight.IsVisible = false;
// 根据位置设置显示对应的时钟控件(带碰撞检测)
if (showClock)
{ {
ClockWidget.IsVisible = showClock; var targetPosition = _clockPosition;
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground); var canAdd = CanAddComponentAtPosition(targetPosition);
if (showClock)
if (canAdd)
{ {
ClockWidget.SetDisplayFormat(_clockDisplayFormat); var targetClock = targetPosition switch
var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3; {
Grid.SetColumnSpan(ClockWidget, columnSpan); "Center" => ClockWidgetCenter,
hasVisibleTopStatusComponent = true; "Right" => ClockWidgetRight,
_ => ClockWidgetLeft
};
if (targetClock is not null)
{
targetClock.IsVisible = true;
targetClock.SetTransparentBackground(_statusBarClockTransparentBackground);
targetClock.SetDisplayFormat(_clockDisplayFormat);
hasVisibleTopStatusComponent = true;
}
}
else
{
// 如果目标位置无法添加,尝试其他位置
var alternativePosition = FindAlternativePosition(targetPosition);
if (alternativePosition is not null)
{
var targetClock = alternativePosition switch
{
"Center" => ClockWidgetCenter,
"Right" => ClockWidgetRight,
_ => ClockWidgetLeft
};
if (targetClock is not null)
{
targetClock.IsVisible = true;
targetClock.SetTransparentBackground(_statusBarClockTransparentBackground);
targetClock.SetDisplayFormat(_clockDisplayFormat);
hasVisibleTopStatusComponent = true;
}
}
}
}
// 根据位置设置显示对应的文字胶囊控件(带碰撞检测)
if (_showTextCapsule)
{
var targetPosition = _textCapsulePosition;
var canAdd = CanAddComponentAtPosition(targetPosition);
if (canAdd)
{
var targetTextCapsule = targetPosition switch
{
"Left" => TextCapsuleWidgetLeft,
"Center" => TextCapsuleWidgetCenter,
_ => TextCapsuleWidgetRight
};
if (targetTextCapsule is not null)
{
targetTextCapsule.IsVisible = true;
targetTextCapsule.SetTransparentBackground(_textCapsuleTransparentBackground);
targetTextCapsule.SetText(_textCapsuleContent);
hasVisibleTopStatusComponent = true;
}
}
else
{
// 如果目标位置无法添加,尝试其他位置
var alternativePosition = FindAlternativePosition(targetPosition);
if (alternativePosition is not null)
{
var targetTextCapsule = alternativePosition switch
{
"Left" => TextCapsuleWidgetLeft,
"Center" => TextCapsuleWidgetCenter,
_ => TextCapsuleWidgetRight
};
if (targetTextCapsule is not null)
{
targetTextCapsule.IsVisible = true;
targetTextCapsule.SetTransparentBackground(_textCapsuleTransparentBackground);
targetTextCapsule.SetText(_textCapsuleContent);
hasVisibleTopStatusComponent = true;
}
}
} }
} }
@@ -394,6 +774,168 @@ public partial class MainWindow
{ {
TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent; TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
} }
// 延迟检查碰撞并调整
Dispatcher.UIThread.Post(async () =>
{
await System.Threading.Tasks.Task.Delay(50);
AdjustComponentsIfColliding();
});
}
/// <summary>
/// 当组件发生碰撞时,自动调整位置
/// </summary>
private void AdjustComponentsIfColliding()
{
if (!WouldComponentsCollide())
return;
// 获取当前可见的组件
var leftComponents = GetVisibleLeftComponents();
var centerComponents = GetVisibleCenterComponents();
var rightComponents = GetVisibleRightComponents();
// 优先保留时钟,调整文字胶囊位置
if (TextCapsuleWidgetLeft?.IsVisible == true && WouldComponentsCollide())
{
// 尝试将左侧文字胶囊移到中间
if (CanAddComponentAtPosition("Center"))
{
TextCapsuleWidgetLeft.IsVisible = false;
TextCapsuleWidgetCenter!.IsVisible = true;
TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetCenter.SetText(_textCapsuleContent);
}
// 或者移到右侧
else if (CanAddComponentAtPosition("Right"))
{
TextCapsuleWidgetLeft.IsVisible = false;
TextCapsuleWidgetRight!.IsVisible = true;
TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetRight.SetText(_textCapsuleContent);
}
// 如果都无法添加,则隐藏文字胶囊
else
{
TextCapsuleWidgetLeft.IsVisible = false;
}
}
if (TextCapsuleWidgetRight?.IsVisible == true && WouldComponentsCollide())
{
// 尝试将右侧文字胶囊移到中间
if (CanAddComponentAtPosition("Center"))
{
TextCapsuleWidgetRight.IsVisible = false;
TextCapsuleWidgetCenter!.IsVisible = true;
TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetCenter.SetText(_textCapsuleContent);
}
// 或者移到左侧
else if (CanAddComponentAtPosition("Left"))
{
TextCapsuleWidgetRight.IsVisible = false;
TextCapsuleWidgetLeft!.IsVisible = true;
TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetLeft.SetText(_textCapsuleContent);
}
// 如果都无法添加,则隐藏文字胶囊
else
{
TextCapsuleWidgetRight.IsVisible = false;
}
}
if (TextCapsuleWidgetCenter?.IsVisible == true && WouldComponentsCollide())
{
// 尝试将中间文字胶囊移到左侧
if (CanAddComponentAtPosition("Left"))
{
TextCapsuleWidgetCenter.IsVisible = false;
TextCapsuleWidgetLeft!.IsVisible = true;
TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetLeft.SetText(_textCapsuleContent);
}
// 或者移到右侧
else if (CanAddComponentAtPosition("Right"))
{
TextCapsuleWidgetCenter.IsVisible = false;
TextCapsuleWidgetRight!.IsVisible = true;
TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetRight.SetText(_textCapsuleContent);
}
// 如果都无法添加,则隐藏文字胶囊
else
{
TextCapsuleWidgetCenter.IsVisible = false;
}
}
}
/// <summary>
/// 查找可用的替代位置
/// </summary>
private string? FindAlternativePosition(string originalPosition)
{
// 尝试所有可能的位置
var positions = new[] { "Left", "Center", "Right" };
foreach (var position in positions)
{
if (position != originalPosition && CanAddComponentAtPosition(position))
{
return position;
}
}
return null;
}
/// <summary>
/// 获取左侧可见组件列表
/// </summary>
private List<Control> GetVisibleLeftComponents()
{
var result = new List<Control>();
if (TopStatusLeftPanel is null) return result;
foreach (var child in TopStatusLeftPanel.Children)
{
if (child is Control control && control.IsVisible)
result.Add(control);
}
return result;
}
/// <summary>
/// 获取中间可见组件列表
/// </summary>
private List<Control> GetVisibleCenterComponents()
{
var result = new List<Control>();
if (TopStatusCenterPanel is null) return result;
foreach (var child in TopStatusCenterPanel.Children)
{
if (child is Control control && control.IsVisible)
result.Add(control);
}
return result;
}
/// <summary>
/// 获取右侧可见组件列表
/// </summary>
private List<Control> GetVisibleRightComponents()
{
var result = new List<Control>();
if (TopStatusRightPanel is null) return result;
foreach (var child in TopStatusRightPanel.Children)
{
if (child is Control control && control.IsVisible)
result.Add(control);
}
return result;
} }
private TaskbarContext GetCurrentTaskbarContext() private TaskbarContext GetCurrentTaskbarContext()

View File

@@ -269,12 +269,6 @@ public partial class MainWindow
LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2; LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2;
LauncherPagePanel.MaxHeight = pageHeight - launcherMargin * 2; LauncherPagePanel.MaxHeight = pageHeight - launcherMargin * 2;
if (LauncherFolderPanel is not null)
{
LauncherFolderPanel.MaxWidth = Math.Max(320, pageWidth - 96);
LauncherFolderPanel.MaxHeight = Math.Max(220, pageHeight - 96);
}
// 更新启动台图标布局 // 更新启动台图标布局
UpdateLauncherTileLayout(); UpdateLauncherTileLayout();
@@ -331,19 +325,6 @@ public partial class MainWindow
} }
} }
// 同样更新文件夹视图的图标尺寸
if (LauncherFolderTilePanel is not null)
{
LauncherFolderTilePanel.Width = availableWidth;
foreach (var child in LauncherFolderTilePanel.Children)
{
if (child is Button button)
{
button.Width = tileWidth;
button.Height = tileHeight;
}
}
}
} }
private void ClampSurfaceIndex() private void ClampSurfaceIndex()
@@ -630,8 +611,12 @@ public partial class MainWindow
foreach (var node in button.GetSelfAndVisualAncestors()) foreach (var node in button.GetSelfAndVisualAncestors())
{ {
if (node is WrapPanel panel && if (node is WrapPanel panel && panel.Name == "LauncherRootTilePanel")
(panel.Name == "LauncherRootTilePanel" || panel.Name == "LauncherFolderTilePanel")) {
return true;
}
if (node is Grid grid && grid.Name == "LauncherFolderGridPanel")
{ {
return true; return true;
} }
@@ -719,8 +704,7 @@ public partial class MainWindow
return false; return false;
} }
return scrollViewer.Name == "LauncherRootScrollViewer" || return scrollViewer.Name == "LauncherRootScrollViewer";
scrollViewer.Name == "LauncherFolderScrollViewer";
} }
private bool TryGetPointerPositionInDesktopViewport(PointerEventArgs e, out Point point) private bool TryGetPointerPositionInDesktopViewport(PointerEventArgs e, out Point point)
@@ -1561,18 +1545,17 @@ public partial class MainWindow
LauncherFolderOverlay.IsVisible = false; LauncherFolderOverlay.IsVisible = false;
} }
if (LauncherFolderTilePanel is not null) if (LauncherFolderGridPanel is not null)
{ {
LauncherFolderTilePanel.Children.Clear(); LauncherFolderGridPanel.Children.Clear();
} }
} }
private void RenderLauncherFolderFromStack() private void RenderLauncherFolderFromStack()
{ {
if (LauncherFolderOverlay is null || if (LauncherFolderOverlay is null ||
LauncherFolderTilePanel is null || LauncherFolderGridPanel is null ||
LauncherFolderTitleTextBlock is null || LauncherFolderTitleTextBlock is null)
LauncherFolderBackButton is null)
{ {
return; return;
} }
@@ -1587,38 +1570,230 @@ public partial class MainWindow
var folder = _launcherFolderStack.Peek(); var folder = _launcherFolderStack.Peek();
LauncherFolderOverlay.IsVisible = true; LauncherFolderOverlay.IsVisible = true;
LauncherFolderTitleTextBlock.Text = folder.Name; LauncherFolderTitleTextBlock.Text = folder.Name;
LauncherFolderBackButton.IsVisible = _launcherFolderStack.Count > 1;
LauncherFolderTilePanel.Children.Clear(); LauncherFolderGridPanel.Children.Clear();
foreach (var subFolder in folder.Folders)
const int maxCols = 4;
const int maxRows = 3;
const int maxItems = maxCols * maxRows;
var visibleFolders = folder.Folders.Where(IsLauncherFolderVisible).ToList();
var visibleApps = folder.Apps.Where(IsLauncherAppVisible).ToList();
if (visibleFolders.Count == 0 && visibleApps.Count == 0)
{ {
if (!IsLauncherFolderVisible(subFolder)) LauncherFolderGridPanel.Children.Add(CreateLauncherFolderGridHintCell(
L("launcher.empty_folder", "This folder is empty.")));
return;
}
var allItems = new List<(StartMenuFolderNode? Folder, StartMenuAppEntry? App)>();
foreach (var f in visibleFolders)
{
allItems.Add((f, null));
}
foreach (var a in visibleApps)
{
allItems.Add((null, a));
}
var displayCount = Math.Min(allItems.Count, maxItems);
for (var i = 0; i < displayCount; i++)
{
var col = i % maxCols;
var row = i / maxCols;
var (itemFolder, itemApp) = allItems[i];
Control cell;
if (itemFolder is not null)
{
var capturedFolder = itemFolder;
cell = CreateLauncherFolderGridTile(itemFolder.Name, GetLauncherFolderIconBitmap(), () => OpenLauncherFolder(capturedFolder));
}
else if (itemApp is not null)
{
var capturedApp = itemApp;
cell = CreateLauncherFolderGridTile(capturedApp, () => LaunchStartMenuEntry(capturedApp));
}
else
{ {
continue; continue;
} }
LauncherFolderTilePanel.Children.Add(CreateLauncherFolderTile(subFolder)); Grid.SetColumn(cell, col);
Grid.SetRow(cell, row);
LauncherFolderGridPanel.Children.Add(cell);
} }
}
foreach (var app in folder.Apps) private Button CreateLauncherFolderGridTile(StartMenuAppEntry app, Action clickAction)
{ {
if (!IsLauncherAppVisible(app)) var iconBitmap = GetLauncherIconBitmap(app);
var monogram = BuildMonogram(app.DisplayName);
Control iconControl = iconBitmap is not null
? new Image
{ {
continue; Source = iconBitmap,
Width = 32,
Height = 32,
Stretch = Stretch.Uniform
}
: new Border
{
Width = 32,
Height = 32,
CornerRadius = new CornerRadius(8),
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
{
Text = monogram,
FontSize = 13,
FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
var content = new StackPanel
{
Spacing = 6,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Center
};
content.Children.Add(iconControl);
content.Children.Add(new TextBlock
{
Text = app.DisplayName,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
TextAlignment = TextAlignment.Center,
FontSize = 11,
HorizontalAlignment = HorizontalAlignment.Stretch
});
var button = new Button
{
Classes = { "glass-panel" },
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(8, 8, 8, 6),
Content = content
};
button.Click += (_, _) =>
{
if (_isComponentLibraryOpen)
{
return;
} }
LauncherFolderTilePanel.Children.Add(CreateLauncherAppTile(app)); clickAction();
} };
return button;
}
if (LauncherFolderTilePanel.Children.Count == 0) private Button CreateLauncherFolderGridTile(string folderName, Bitmap? iconBitmap, Action clickAction)
{
var monogram = "DIR";
Control iconControl = iconBitmap is not null
? new Image
{
Source = iconBitmap,
Width = 32,
Height = 32,
Stretch = Stretch.Uniform
}
: new Border
{
Width = 32,
Height = 32,
CornerRadius = new CornerRadius(8),
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
{
Text = monogram,
FontSize = 11,
FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
var content = new StackPanel
{ {
LauncherFolderTilePanel.Children.Add(CreateLauncherHintTile( Spacing = 6,
L("launcher.empty_folder", "This folder is empty."), HorizontalAlignment = HorizontalAlignment.Stretch,
string.Empty)); VerticalAlignment = VerticalAlignment.Center
} };
content.Children.Add(iconControl);
content.Children.Add(new TextBlock
{
Text = folderName,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
TextAlignment = TextAlignment.Center,
FontSize = 11,
HorizontalAlignment = HorizontalAlignment.Stretch
});
// 在图标渲染完成后,应用布局计算 var button = new Button
Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background); {
Classes = { "glass-panel" },
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(8, 8, 8, 6),
Content = content
};
button.Click += (_, _) =>
{
if (_isComponentLibraryOpen)
{
return;
}
clickAction();
};
return button;
}
private Control CreateLauncherFolderGridHintCell(string message)
{
return CreateLauncherFolderGridHintCell(message, 0, 0);
}
private Control CreateLauncherFolderGridHintCell(string message, int col, int row)
{
var textBlock = new TextBlock
{
Text = message,
FontSize = 12,
FontWeight = FontWeight.SemiBold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Opacity = 0.6
};
var cell = new Border
{
Classes = { "glass-panel" },
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
CornerRadius = new CornerRadius(12),
Child = textBlock
};
Grid.SetColumn(cell, col);
Grid.SetRow(cell, row);
return cell;
} }
private static string BuildMonogram(string text) private static string BuildMonogram(string text)
@@ -1689,18 +1864,6 @@ public partial class MainWindow
} }
} }
private void OnLauncherFolderBackClick(object? sender, RoutedEventArgs e)
{
if (_launcherFolderStack.Count <= 1)
{
CloseLauncherFolderOverlay();
return;
}
_launcherFolderStack.Pop();
RenderLauncherFolderFromStack();
}
private void OnLauncherFolderOverlayPointerPressed(object? sender, PointerPressedEventArgs e) private void OnLauncherFolderOverlayPointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (LauncherFolderPanel is null) if (LauncherFolderPanel is null)
@@ -1721,11 +1884,6 @@ public partial class MainWindow
e.Handled = true; e.Handled = true;
} }
private void OnLauncherFolderCloseClick(object? sender, RoutedEventArgs e)
{
CloseLauncherFolderOverlay();
}
private void DisposeLauncherResources() private void DisposeLauncherResources()
{ {
foreach (var bitmap in _launcherIconCache.Values) foreach (var bitmap in _launcherIconCache.Values)

View File

@@ -189,50 +189,21 @@
Classes="surface-solid-strong" Classes="surface-solid-strong"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="52" Width="464"
MaxWidth="760" Height="384"
MaxHeight="520" CornerRadius="24"
CornerRadius="36" Padding="16,14,16,12">
Padding="14"> <Grid RowDefinitions="Auto,*">
<Border.RenderTransform> <TextBlock x:Name="LauncherFolderTitleTextBlock"
<TranslateTransform Y="42" /> FontSize="15"
</Border.RenderTransform> FontWeight="SemiBold"
<Grid RowDefinitions="Auto,*" HorizontalAlignment="Center"
RowSpacing="10"> Margin="0,0,0,10" />
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8">
<Button x:Name="LauncherFolderBackButton"
Grid.Column="0"
Width="38"
Height="34"
Padding="0"
Click="OnLauncherFolderBackClick">
<fi:FluentIcon Icon="ArrowLeft"
IconVariant="Regular" />
</Button>
<TextBlock x:Name="LauncherFolderTitleTextBlock"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold" />
<Button x:Name="LauncherFolderCloseButton"
Grid.Column="2"
Width="38"
Height="34"
Padding="0"
Click="OnLauncherFolderCloseClick">
<fi:FluentIcon Icon="Dismiss"
IconVariant="Regular" />
</Button>
</Grid>
<ScrollViewer x:Name="LauncherFolderScrollViewer" <Grid x:Name="LauncherFolderGridPanel"
Grid.Row="1" Grid.Row="1"
VerticalScrollBarVisibility="Auto" ColumnDefinitions="*,*,*,*"
HorizontalScrollBarVisibility="Disabled"> RowDefinitions="*,*,*" />
<WrapPanel x:Name="LauncherFolderTilePanel"
Orientation="Horizontal" />
</ScrollViewer>
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>
@@ -262,13 +233,47 @@
Background="Transparent" Background="Transparent"
BorderThickness="0" BorderThickness="0"
Padding="4"> Padding="4">
<StackPanel x:Name="TopStatusComponentsPanel" <Grid ColumnDefinitions="*,Auto,*">
Orientation="Horizontal" <!-- 左侧状态栏组件 -->
Spacing="6"> <StackPanel x:Name="TopStatusLeftPanel"
<comp:ClockWidget x:Name="ClockWidget" Grid.Column="0"
IsVisible="False" Orientation="Horizontal"
Margin="0" /> Spacing="6"
</StackPanel> HorizontalAlignment="Left">
<comp:ClockWidget x:Name="ClockWidgetLeft"
IsVisible="False"
Margin="0" />
<comp:TextCapsuleWidget x:Name="TextCapsuleWidgetLeft"
IsVisible="False"
Margin="0" />
</StackPanel>
<!-- 中间状态栏组件 -->
<StackPanel x:Name="TopStatusCenterPanel"
Grid.Column="1"
Orientation="Horizontal"
Spacing="6"
HorizontalAlignment="Center">
<comp:ClockWidget x:Name="ClockWidgetCenter"
IsVisible="False"
Margin="0" />
<comp:TextCapsuleWidget x:Name="TextCapsuleWidgetCenter"
IsVisible="False"
Margin="0" />
</StackPanel>
<!-- 右侧状态栏组件 -->
<StackPanel x:Name="TopStatusRightPanel"
Grid.Column="2"
Orientation="Horizontal"
Spacing="6"
HorizontalAlignment="Right">
<comp:ClockWidget x:Name="ClockWidgetRight"
IsVisible="False"
Margin="0" />
<comp:TextCapsuleWidget x:Name="TextCapsuleWidgetRight"
IsVisible="False"
Margin="0" />
</StackPanel>
</Grid>
</Border> </Border>
<Border x:Name="BottomTaskbarContainer" <Border x:Name="BottomTaskbarContainer"

View File

@@ -135,6 +135,11 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
private string _statusBarSpacingMode = "Relaxed"; private string _statusBarSpacingMode = "Relaxed";
private int _statusBarCustomSpacingPercent = 12; private int _statusBarCustomSpacingPercent = 12;
private bool _statusBarClockTransparentBackground; private bool _statusBarClockTransparentBackground;
private string _clockPosition = "Left"; // Left, Center, Right
private bool _showTextCapsule;
private string _textCapsuleContent = "**Hello** World!";
private string _textCapsulePosition = "Right"; // Left, Center, Right
private bool _textCapsuleTransparentBackground;
private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent; private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent;
private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle; private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
private string _languageCode = "zh-CN"; private string _languageCode = "zh-CN";
@@ -238,9 +243,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
TaskbarProfileButton.IsEnabled = false; TaskbarProfileButton.IsEnabled = false;
TaskbarProfilePopup.IsOpen = false; TaskbarProfilePopup.IsOpen = false;
ClockWidget.IsVisible = true; ClockWidgetLeft.IsVisible = true;
ClockWidget.SetDisplayFormat(ClockDisplayFormat.HourMinute); ClockWidgetLeft.SetDisplayFormat(ClockDisplayFormat.HourMinute);
ClockWidget.SetTransparentBackground(false); ClockWidgetLeft.SetTransparentBackground(false);
ConfigureDesignTimeDesktopGrid(); ConfigureDesignTimeDesktopGrid();
PopulateDesignTimeDesktopSurface(); PopulateDesignTimeDesktopSurface();
@@ -288,7 +293,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
DesktopPagesHost.ColumnDefinitions.Clear(); DesktopPagesHost.ColumnDefinitions.Clear();
DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star))); DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
ClockWidget.ApplyCellSize(72); ClockWidgetLeft.ApplyCellSize(72);
} }
private void PopulateDesignTimeDesktopSurface() private void PopulateDesignTimeDesktopSurface()
@@ -481,7 +486,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
RebuildDesktopGrid(); RebuildDesktopGrid();
LoadLauncherEntriesAsync(); LoadLauncherEntriesAsync();
InitializeTimeZoneSettings(); InitializeTimeZoneSettings();
ClockWidget.SetTimeZoneService(_timeZoneService); ClockWidgetLeft.SetTimeZoneService(_timeZoneService);
ClockWidgetCenter.SetTimeZoneService(_timeZoneService);
ClockWidgetRight.SetTimeZoneService(_timeZoneService);
_suppressSettingsPersistence = false; _suppressSettingsPersistence = false;
PersistSettings(); PersistSettings();
@@ -621,7 +628,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
private void ApplyDesktopStatusBarComponentSpacing() private void ApplyDesktopStatusBarComponentSpacing()
{ {
ApplyStatusBarComponentSpacingForPanel(TopStatusComponentsPanel, _currentDesktopCellSize); ApplyStatusBarComponentSpacingForPanel(TopStatusLeftPanel, _currentDesktopCellSize);
ApplyStatusBarComponentSpacingForPanel(TopStatusCenterPanel, _currentDesktopCellSize);
ApplyStatusBarComponentSpacingForPanel(TopStatusRightPanel, _currentDesktopCellSize);
} }
private int ResolveStatusBarSpacingPercent() private int ResolveStatusBarSpacingPercent()
@@ -697,8 +706,19 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
ApplyUnifiedMainRectangleChrome(); ApplyUnifiedMainRectangleChrome();
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(taskbarCellHeight * 0.16, 6, 14)); BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(taskbarCellHeight * 0.16, 6, 14));
ClockWidget.Margin = new Thickness(0); ClockWidgetLeft.Margin = new Thickness(0);
ClockWidget.ApplyCellSize(cellSize); ClockWidgetLeft.ApplyCellSize(cellSize);
ClockWidgetCenter.Margin = new Thickness(0);
ClockWidgetCenter.ApplyCellSize(cellSize);
ClockWidgetRight.Margin = new Thickness(0);
ClockWidgetRight.ApplyCellSize(cellSize);
TextCapsuleWidgetLeft.Margin = new Thickness(0);
TextCapsuleWidgetLeft.ApplyCellSize(cellSize);
TextCapsuleWidgetCenter.Margin = new Thickness(0);
TextCapsuleWidgetCenter.ApplyCellSize(cellSize);
TextCapsuleWidgetRight.Margin = new Thickness(0);
TextCapsuleWidgetRight.ApplyCellSize(cellSize);
var buttonMinWidth = Math.Clamp(taskbarCellHeight * 2.35, 100, 340); var buttonMinWidth = Math.Clamp(taskbarCellHeight * 2.35, 100, 340);
@@ -737,7 +757,12 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
if (_currentDesktopCellSize > 0) if (_currentDesktopCellSize > 0)
{ {
ClockWidget.ApplyCellSize(_currentDesktopCellSize); ClockWidgetLeft.ApplyCellSize(_currentDesktopCellSize);
ClockWidgetCenter.ApplyCellSize(_currentDesktopCellSize);
ClockWidgetRight.ApplyCellSize(_currentDesktopCellSize);
TextCapsuleWidgetLeft.ApplyCellSize(_currentDesktopCellSize);
TextCapsuleWidgetCenter.ApplyCellSize(_currentDesktopCellSize);
TextCapsuleWidgetRight.ApplyCellSize(_currentDesktopCellSize);
} }
} }

View File

@@ -52,6 +52,78 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Grid> </Grid>
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding ClockPositionLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="220"
IsEnabled="{Binding ShowClock}"
ItemsSource="{Binding ClockPositions}"
SelectedItem="{Binding SelectedClockPosition}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{Binding TextCapsuleHeader}"
Description="{Binding TextCapsuleDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="TextQuote" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding ShowTextCapsule}" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding TextCapsuleContentLabel}"
VerticalAlignment="Top"
Margin="0,8,0,0" />
<TextBox Grid.Column="1"
AcceptsReturn="True"
TextWrapping="Wrap"
Height="100"
IsEnabled="{Binding ShowTextCapsule}"
Text="{Binding TextCapsuleContent}"
Watermark="Enter Markdown text..." />
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding TextCapsulePositionLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="220"
IsEnabled="{Binding ShowTextCapsule}"
ItemsSource="{Binding TextCapsulePositions}"
SelectedItem="{Binding SelectedTextCapsulePosition}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="16">
<TextBlock Text="{Binding TextCapsuleTransparentBackgroundLabel}"
VerticalAlignment="Center" />
<ToggleSwitch Grid.Column="1"
IsChecked="{Binding TextCapsuleTransparentBackground}"
IsEnabled="{Binding ShowTextCapsule}"
VerticalAlignment="Center" />
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander> </ui:SettingsExpander>
<Separator Classes="settings-separator" /> <Separator Classes="settings-separator" />

View File

@@ -1,7 +1,6 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LanMountainDesktop.Views.TransparentOverlayWindow" x:Class="LanMountainDesktop.Views.TransparentOverlayWindow"
WindowState="FullScreen"
SystemDecorations="None" SystemDecorations="None"
CanResize="False" CanResize="False"
ShowInTaskbar="False" ShowInTaskbar="False"

View File

@@ -22,9 +22,6 @@ namespace LanMountainDesktop.Views;
/// </summary> /// </summary>
public partial class TransparentOverlayWindow : Window public partial class TransparentOverlayWindow : Window
{ {
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate();
private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate();
private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate(); private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
// 滑动状态 // 滑动状态
@@ -55,35 +52,75 @@ public partial class TransparentOverlayWindow : Window
// 渲染参数 // 渲染参数
private const double DefaultCellSize = 100; private const double DefaultCellSize = 100;
private double _currentDesktopCellSize;
// 拖拽状态 // 拖拽与缩放状态
private bool _isDragging; private bool _isDragging;
private string? _draggingPlacementId; private bool _isResizing;
private Point _dragStartPoint; private string? _interactionPlacementId;
private Border? _draggingHost; 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 event EventHandler? RestoreMainWindowRequested;
public TransparentOverlayWindow() public TransparentOverlayWindow()
{ {
InitializeComponent(); InitializeComponent();
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService(); var facade = HostSettingsFacadeProvider.GetOrCreate();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService(); _weatherDataService = facade.Weather.GetWeatherInfoService();
_timeZoneService = facade.Region.GetTimeZoneService();
_settingsFacade = facade;
}
private readonly ISettingsFacadeService _settingsFacade;
public void SaveLayoutAndHide()
{
SaveLayout();
Hide();
// 仅在 Windows 上启用置底功能 // Remove all components so that next time we open it builds fresh from snapshot
if (OperatingSystem.IsWindows()) if (Content is Canvas canvas)
{ {
_bottomMostService.SetupBottomMost(this); canvas.Children.Clear();
} }
_componentHosts.Clear();
} }
protected override void OnOpened(EventArgs e) protected override void OnOpened(EventArgs e)
{ {
base.OnOpened(e); base.OnOpened(e);
if (OperatingSystem.IsWindows()) if (Screens.Primary is { } primaryScreen)
{ {
_bottomMostService.SendToBottom(this); // 避开系统任务栏
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));
} }
// 确保注册表已初始化 // 确保注册表已初始化
@@ -120,6 +157,7 @@ public partial class TransparentOverlayWindow : Window
canvas.Children.Clear(); canvas.Children.Clear();
_componentHosts.Clear(); _componentHosts.Clear();
_selectedHost = null;
foreach (var placement in _layout.ComponentPlacements) foreach (var placement in _layout.ComponentPlacements)
{ {
@@ -147,16 +185,7 @@ public partial class TransparentOverlayWindow : Window
/// </summary> /// </summary>
private void UpdateInteractiveRegions() private void UpdateInteractiveRegions()
{ {
_interactiveRegions.Clear(); // 编辑模式下不再需要底层穿透功能计算,这里留空或移除
foreach (var host in _componentHosts.Values)
{
var x = Canvas.GetLeft(host);
var y = Canvas.GetTop(host);
_interactiveRegions.Add(new Rect(x, y, host.Width, host.Height));
}
_regionPassthroughService.SetInteractiveRegions(this, _interactiveRegions);
} }
/// <summary> /// <summary>
@@ -180,9 +209,14 @@ public partial class TransparentOverlayWindow : Window
return; return;
} }
// 解析尺寸:如果未提供,则使用组件定义的最小尺寸 * 100 var finalWidth = width ?? (definition.MinWidthCells * _currentDesktopCellSize);
var finalWidth = width ?? (definition.MinWidthCells * DefaultCellSize); var finalHeight = height ?? (definition.MinHeightCells * _currentDesktopCellSize);
var finalHeight = height ?? (definition.MinHeightCells * DefaultCellSize);
// 对齐网格
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 placementId = Guid.NewGuid().ToString("N");
var placement = new FusedDesktopComponentPlacementSnapshot var placement = new FusedDesktopComponentPlacementSnapshot
@@ -225,7 +259,7 @@ public partial class TransparentOverlayWindow : Window
} }
var control = descriptor.CreateControl( var control = descriptor.CreateControl(
DefaultCellSize, _currentDesktopCellSize,
_timeZoneService, _timeZoneService,
_weatherDataService, _weatherDataService,
_recommendationInfoService, _recommendationInfoService,
@@ -260,24 +294,44 @@ public partial class TransparentOverlayWindow : Window
/// </summary> /// </summary>
public void RenderComponent(string placementId, Control component, double x, double y, double width, double height) 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 var host = new Border
{ {
Tag = placementId, Tag = placementId,
Width = width, Width = width,
Height = height, Height = height,
Background = Brushes.Transparent, Background = Avalonia.Media.Brushes.Transparent,
CornerRadius = new CornerRadius(12), CornerRadius = new Avalonia.CornerRadius(12),
ClipToBounds = true, ClipToBounds = false, // 允许把手溢出
Child = component BorderBrush = Avalonia.Media.Brushes.Transparent,
BorderThickness = new Avalonia.Thickness(3),
Child = grid,
Classes = { "desktop-component-host" }
}; };
Canvas.SetLeft(host, x); Canvas.SetLeft(host, x);
Canvas.SetTop(host, y); Canvas.SetTop(host, y);
// 添加拖拽支持
host.PointerPressed += OnComponentPointerPressed; host.PointerPressed += OnComponentPointerPressed;
host.PointerMoved += OnComponentPointerMoved; host.PointerMoved += OnInteractionPointerMoved;
host.PointerReleased += OnComponentPointerReleased; host.PointerReleased += OnInteractionPointerReleased;
// 右键上下文菜单(删除组件) // 右键上下文菜单(删除组件)
host.ContextRequested += OnComponentContextRequested; host.ContextRequested += OnComponentContextRequested;
@@ -318,7 +372,60 @@ public partial class TransparentOverlayWindow : Window
e.Handled = true; 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) private void OnComponentPointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (sender is not Border host || host.Tag is not string placementId) return; if (sender is not Border host || host.Tag is not string placementId) return;
@@ -326,55 +433,97 @@ public partial class TransparentOverlayWindow : Window
var point = e.GetCurrentPoint(this); var point = e.GetCurrentPoint(this);
if (!point.Properties.IsLeftButtonPressed) return; if (!point.Properties.IsLeftButtonPressed) return;
_isDragging = true; SelectComponent(host);
_draggingPlacementId = placementId;
_draggingHost = host; _interactionPlacementId = placementId;
_dragStartPoint = e.GetPosition(this); _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.Pointer.Capture(host);
e.Handled = true; e.Handled = true;
} }
private void OnComponentPointerMoved(object? sender, PointerEventArgs e) private void OnInteractionPointerMoved(object? sender, PointerEventArgs e)
{ {
if (!_isDragging || _draggingHost is null) return; if ((!_isDragging && !_isResizing) || _interactionHost is null) return;
var currentPoint = e.GetPosition(this); var currentPoint = e.GetPosition(this);
var deltaX = currentPoint.X - _dragStartPoint.X; var deltaX = currentPoint.X - _interactionStartPoint.X;
var deltaY = currentPoint.Y - _dragStartPoint.Y; var deltaY = currentPoint.Y - _interactionStartPoint.Y;
var currentX = Canvas.GetLeft(_draggingHost); if (_isDragging)
var currentY = Canvas.GetTop(_draggingHost); {
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;
}
Canvas.SetLeft(_draggingHost, currentX + deltaX);
Canvas.SetTop(_draggingHost, currentY + deltaY);
_dragStartPoint = currentPoint;
e.Handled = true; e.Handled = true;
} }
private void OnComponentPointerReleased(object? sender, PointerReleasedEventArgs e) private void OnInteractionPointerReleased(object? sender, PointerReleasedEventArgs e)
{ {
if (!_isDragging || _draggingHost is null || _draggingPlacementId is null) if ((!_isDragging && !_isResizing) || _interactionHost is null || _interactionPlacementId is null)
{ {
_isDragging = false; _isDragging = false;
_isResizing = false;
return; return;
} }
// 更新布局中的位置 // 更新布局中的位置与尺寸
var placement = _layout.ComponentPlacements.Find(p => p.PlacementId == _draggingPlacementId); var placement = _layout.ComponentPlacements.Find(p => p.PlacementId == _interactionPlacementId);
if (placement is not null) if (placement is not null)
{ {
placement.X = Canvas.GetLeft(_draggingHost); placement.X = Canvas.GetLeft(_interactionHost);
placement.Y = Canvas.GetTop(_draggingHost); placement.Y = Canvas.GetTop(_interactionHost);
placement.Width = _interactionHost.Width;
placement.Height = _interactionHost.Height;
} }
UpdateInteractiveRegions(); UpdateInteractiveRegions();
SaveLayout(); SaveLayout();
_isDragging = false; _isDragging = false;
_draggingPlacementId = null; _isResizing = false;
_draggingHost = null; _interactionPlacementId = null;
_interactionHost = null;
e.Pointer.Capture(null); e.Pointer.Capture(null);
e.Handled = true; e.Handled = true;