diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index d4b738d..213185f 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -388,6 +388,18 @@ "settings.status_bar.clock_format_label": "Clock format", "settings.status_bar.clock_format.hm": "Hour:Minute", "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.description": "Adjust component layout and corner design.", "settings.components.grid_header": "Grid Settings", diff --git a/LanMountainDesktop/Localization/ja-JP.json b/LanMountainDesktop/Localization/ja-JP.json index d6ab53e..2d541fc 100644 --- a/LanMountainDesktop/Localization/ja-JP.json +++ b/LanMountainDesktop/Localization/ja-JP.json @@ -331,6 +331,18 @@ "settings.status_bar.clock_format_label": "時計の形式", "settings.status_bar.clock_format.hm": "時:分", "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.description": "コンポーネントのレイアウトとコーナーデザインを調整します。", "settings.components.grid_header": "グリッド設定", diff --git a/LanMountainDesktop/Localization/ko-KR.json b/LanMountainDesktop/Localization/ko-KR.json index a3a6879..2358cdb 100644 --- a/LanMountainDesktop/Localization/ko-KR.json +++ b/LanMountainDesktop/Localization/ko-KR.json @@ -377,6 +377,18 @@ "settings.status_bar.clock_format_label": "시계 형식", "settings.status_bar.clock_format.hm": "시:분", "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.description": "컴포넌트 레이아웃과 모서리 디자인을 조정합니다.", "settings.components.grid_header": "그리드 설정", diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index 389f95b..f5a3097 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -383,6 +383,18 @@ "settings.status_bar.clock_format_label": "时钟格式", "settings.status_bar.clock_format.hm": "时:分", "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.description": "调整组件布局与圆角设计。", "settings.components.grid_header": "网格设置", diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index bef7ada..828d5a7 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -112,6 +112,16 @@ public sealed class AppSettingsSnapshot 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 int StatusBarCustomSpacingPercent { get; set; } = 12; diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs index bf488c8..32b2bf5 100644 --- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs +++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs @@ -41,8 +41,20 @@ public sealed record StatusBarSettingsState( string TaskbarLayoutMode, string ClockDisplayFormat, bool ClockTransparentBackground, + string ClockPosition, + bool ShowTextCapsule, + string TextCapsuleContent, + string TextCapsulePosition, + bool TextCapsuleTransparentBackground, string SpacingMode, int CustomSpacingPercent); + +public sealed record TextCapsuleSettingsState( + bool ShowTextCapsule, + string Content, + string Position, + bool TransparentBackground); + public sealed record WeatherSettingsState( string LocationMode, string LocationKey, @@ -274,6 +286,12 @@ public interface IStatusBarSettingsService void Save(StatusBarSettingsState state); } +public interface ITextCapsuleSettingsService +{ + TextCapsuleSettingsState Get(); + void Save(TextCapsuleSettingsState state); +} + public interface IWeatherProvider { Task>> SearchLocationsAsync( @@ -385,6 +403,7 @@ public interface ISettingsFacadeService IWallpaperMediaService WallpaperMedia { get; } IThemeAppearanceService Theme { get; } IStatusBarSettingsService StatusBar { get; } + ITextCapsuleSettingsService TextCapsule { get; } IWeatherSettingsService Weather { get; } IRegionSettingsService Region { get; } IPrivacySettingsService Privacy { get; } diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs index 12a7704..e895f50 100644 --- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -386,6 +386,11 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService snapshot.TaskbarLayoutMode, snapshot.ClockDisplayFormat, snapshot.StatusBarClockTransparentBackground, + snapshot.ClockPosition, + snapshot.ShowTextCapsule, + snapshot.TextCapsuleContent, + snapshot.TextCapsulePosition, + snapshot.TextCapsuleTransparentBackground, snapshot.StatusBarSpacingMode, snapshot.StatusBarCustomSpacingPercent); } @@ -399,6 +404,11 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode; snapshot.ClockDisplayFormat = state.ClockDisplayFormat; 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.StatusBarCustomSpacingPercent = state.CustomSpacingPercent; _settingsService.SaveSnapshot( @@ -412,12 +422,56 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService nameof(AppSettingsSnapshot.TaskbarLayoutMode), nameof(AppSettingsSnapshot.ClockDisplayFormat), nameof(AppSettingsSnapshot.StatusBarClockTransparentBackground), + nameof(AppSettingsSnapshot.ClockPosition), + nameof(AppSettingsSnapshot.ShowTextCapsule), + nameof(AppSettingsSnapshot.TextCapsuleContent), + nameof(AppSettingsSnapshot.TextCapsulePosition), + nameof(AppSettingsSnapshot.TextCapsuleTransparentBackground), nameof(AppSettingsSnapshot.StatusBarSpacingMode), 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 { private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService(); @@ -1198,6 +1252,7 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl WallpaperMedia = new WallpaperMediaService(); Theme = new ThemeAppearanceService(Settings); StatusBar = new StatusBarSettingsService(Settings); + TextCapsule = new TextCapsuleSettingsService(Settings); _weatherSettingsService = new WeatherSettingsService(Settings); Weather = _weatherSettingsService; Region = new RegionSettingsService(Settings); @@ -1227,6 +1282,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl public IStatusBarSettingsService StatusBar { get; } + public ITextCapsuleSettingsService TextCapsule { get; } + public IWeatherSettingsService Weather { get; } public IRegionSettingsService Region { get; } diff --git a/LanMountainDesktop/ViewModels/StatusBarSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/StatusBarSettingsPageViewModel.cs index a6bea88..161fd18 100644 --- a/LanMountainDesktop/ViewModels/StatusBarSettingsPageViewModel.cs +++ b/LanMountainDesktop/ViewModels/StatusBarSettingsPageViewModel.cs @@ -21,6 +21,8 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode); ClockFormats = CreateClockFormats(); + ClockPositions = CreateClockPositions(); + TextCapsulePositions = CreateTextCapsulePositions(); SpacingModes = CreateSpacingModes(); RefreshLocalizedText(); @@ -31,6 +33,10 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase public IReadOnlyList ClockFormats { get; } + public IReadOnlyList ClockPositions { get; } + + public IReadOnlyList TextCapsulePositions { get; } + public IReadOnlyList SpacingModes { get; } [ObservableProperty] @@ -42,6 +48,9 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase [ObservableProperty] private bool _clockTransparentBackground; + [ObservableProperty] + private SelectionOption _selectedClockPosition = new("Left", "Left"); + [ObservableProperty] private SelectionOption _selectedSpacingMode = new("Relaxed", "Relaxed"); @@ -75,6 +84,36 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase [ObservableProperty] 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] private string _spacingHeader = string.Empty; @@ -99,6 +138,20 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase ?? ClockFormats[1]; 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); SelectedSpacingMode = SpacingModes.FirstOrDefault(option => string.Equals(option.Value, spacingMode, StringComparison.OrdinalIgnoreCase)) @@ -137,6 +190,56 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase 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) { IsCustomSpacingVisible = string.Equals(value?.Value, "Custom", StringComparison.OrdinalIgnoreCase); @@ -184,6 +287,11 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase state.TaskbarLayoutMode, SelectedClockFormat.Value, ClockTransparentBackground, + SelectedClockPosition.Value, + ShowTextCapsule, + TextCapsuleContent ?? "**Hello** World!", + SelectedTextCapsulePosition?.Value ?? "Right", + TextCapsuleTransparentBackground, NormalizeSpacingMode(SelectedSpacingMode.Value), Math.Clamp(CustomSpacingPercent, 0, 30))); } @@ -197,6 +305,26 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase ]; } + private IReadOnlyList 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 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 CreateSpacingModes() { return @@ -217,6 +345,12 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock format"); 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."); + 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"); SpacingDescription = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components."); 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) => _localizationService.GetString(_languageCode, key, fallback); } diff --git a/LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml b/LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml new file mode 100644 index 0000000..7f20c4a --- /dev/null +++ b/LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml.cs b/LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml.cs new file mode 100644 index 0000000..4b1e6a3 --- /dev/null +++ b/LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml.cs @@ -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; +} diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs index 72447d4..507836f 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs @@ -10,6 +10,7 @@ using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; using LanMountainDesktop.ViewModels; using LanMountainDesktop.Views.Components; +using Avalonia.Controls.ApplicationLifetimes; namespace LanMountainDesktop.Views; @@ -117,12 +118,42 @@ public partial class FusedDesktopComponentLibraryControl : UserControl definition.MinWidthCells, definition.MinHeightCells); - return new ComponentLibraryItemViewModel( + 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) + { + if (component.PreviewKey.Equals(entry.Key)) + { + component.UpdatePreviewImageEntry(entry); + } + } + } } private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e) diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs index b6dcc9f..8f34fec 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Interactivity; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; +using Avalonia.Controls.ApplicationLifetimes; namespace LanMountainDesktop.Views; @@ -27,6 +28,9 @@ public partial class FusedDesktopComponentLibraryWindow : Window InitializeComponent(); LibraryControl.AddComponentRequested += OnAddComponentRequested; + + var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow; + mainWindow?.RegisterFusedLibraryWindow(this); } /// @@ -98,4 +102,16 @@ public partial class FusedDesktopComponentLibraryWindow : Window { 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); + } } diff --git a/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs b/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs index aaad000..7bad92c 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs @@ -24,6 +24,7 @@ public partial class MainWindow private readonly IComponentPreviewImageService _componentPreviewImageService = new ComponentPreviewImageService(); private readonly Dictionary> _componentLibraryPreviewVisualTargets = new(ComponentPreviewKeyComparer.Instance); private bool _componentLibraryPreviewWarmupStarted; + private FusedDesktopComponentLibraryWindow? _fusedLibraryWindow; private sealed record ComponentLibraryPreviewVisualTarget(Image Image, Control Fallback); @@ -519,6 +520,7 @@ public partial class MainWindow { ApplyPreviewEntryToEmbeddedVisuals(entry.Key); _detachedComponentLibraryWindow?.UpdatePreviewImage(entry); + _fusedLibraryWindow?.UpdatePreviewImage(entry); if (entry.Key.Kind == ComponentPreviewKeyKind.PlacementInstance) { @@ -597,4 +599,30 @@ public partial class MainWindow action: "DetachedLibraryRender", 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); + } } diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs index 791d270..e647f9b 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs @@ -364,12 +364,295 @@ public partial class MainWindow ? ClockDisplayFormat.HourMinute : ClockDisplayFormat.HourMinuteSecond; _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); - ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground); + ClockWidgetLeft.SetDisplayFormat(_clockDisplayFormat); + 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" + }; + } + + /// + /// 检测状态栏组件是否会发生碰撞 + /// + 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; + } + + /// + /// 获取左侧面板占用的宽度(包括间距) + /// + 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; + } + + /// + /// 获取中间面板占用的宽度(包括间距) + /// + 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; + } + + /// + /// 获取右侧面板占用的宽度(包括间距) + /// + 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; + } + + /// + /// 检查是否可以在指定位置添加组件 + /// + 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() @@ -377,16 +660,113 @@ public partial class MainWindow var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock); 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; - ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground); - if (showClock) + var targetPosition = _clockPosition; + var canAdd = CanAddComponentAtPosition(targetPosition); + + if (canAdd) { - ClockWidget.SetDisplayFormat(_clockDisplayFormat); - var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3; - Grid.SetColumnSpan(ClockWidget, columnSpan); - hasVisibleTopStatusComponent = true; + var targetClock = targetPosition switch + { + "Center" => ClockWidgetCenter, + "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; } + + // 延迟检查碰撞并调整 + Dispatcher.UIThread.Post(async () => + { + await System.Threading.Tasks.Task.Delay(50); + AdjustComponentsIfColliding(); + }); + } + + /// + /// 当组件发生碰撞时,自动调整位置 + /// + 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; + } + } + } + + /// + /// 查找可用的替代位置 + /// + 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; + } + + /// + /// 获取左侧可见组件列表 + /// + private List GetVisibleLeftComponents() + { + var result = new List(); + if (TopStatusLeftPanel is null) return result; + + foreach (var child in TopStatusLeftPanel.Children) + { + if (child is Control control && control.IsVisible) + result.Add(control); + } + return result; + } + + /// + /// 获取中间可见组件列表 + /// + private List GetVisibleCenterComponents() + { + var result = new List(); + if (TopStatusCenterPanel is null) return result; + + foreach (var child in TopStatusCenterPanel.Children) + { + if (child is Control control && control.IsVisible) + result.Add(control); + } + return result; + } + + /// + /// 获取右侧可见组件列表 + /// + private List GetVisibleRightComponents() + { + var result = new List(); + 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() diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml index 4711688..a7b3d2b 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml +++ b/LanMountainDesktop/Views/MainWindow.axaml @@ -233,13 +233,47 @@ Background="Transparent" BorderThickness="0" Padding="4"> - - - + + + + + + + + + + + + + + + + + 0) { - ClockWidget.ApplyCellSize(_currentDesktopCellSize); + ClockWidgetLeft.ApplyCellSize(_currentDesktopCellSize); + ClockWidgetCenter.ApplyCellSize(_currentDesktopCellSize); + ClockWidgetRight.ApplyCellSize(_currentDesktopCellSize); + TextCapsuleWidgetLeft.ApplyCellSize(_currentDesktopCellSize); + TextCapsuleWidgetCenter.ApplyCellSize(_currentDesktopCellSize); + TextCapsuleWidgetRight.ApplyCellSize(_currentDesktopCellSize); } } diff --git a/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml index 5a620d6..7eab8a0 100644 --- a/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml @@ -52,6 +52,78 @@ VerticalAlignment="Center" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs b/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs index 544b9a7..078c99b 100644 --- a/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs +++ b/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs @@ -52,12 +52,21 @@ public partial class TransparentOverlayWindow : Window // 渲染参数 private const double DefaultCellSize = 100; + private double _currentDesktopCellSize; - // 拖拽状态 + // 拖拽与缩放状态 private bool _isDragging; - private string? _draggingPlacementId; - private Point _dragStartPoint; - private Border? _draggingHost; + private bool _isResizing; + private string? _interactionPlacementId; + private Point _interactionStartPoint; + private double _interactionOriginalX; + private double _interactionOriginalY; + private double _interactionOriginalWidth; + private double _interactionOriginalHeight; + private Border? _interactionHost; + + // 选中状态 + private Border? _selectedHost; public event EventHandler? RestoreMainWindowRequested; @@ -97,6 +106,15 @@ public partial class TransparentOverlayWindow : Window Position = new PixelPoint(workArea.X, workArea.Y); Width = workArea.Width / scaling; Height = workArea.Height / scaling; + + // 基于设置计算单元格尺寸 + var appSnapshot = _settingsFacade.Settings.LoadSnapshot(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) @@ -139,6 +157,7 @@ public partial class TransparentOverlayWindow : Window canvas.Children.Clear(); _componentHosts.Clear(); + _selectedHost = null; foreach (var placement in _layout.ComponentPlacements) { @@ -190,9 +209,14 @@ public partial class TransparentOverlayWindow : Window return; } - // 解析尺寸:如果未提供,则使用组件定义的最小尺寸 * 100 - var finalWidth = width ?? (definition.MinWidthCells * DefaultCellSize); - var finalHeight = height ?? (definition.MinHeightCells * DefaultCellSize); + var finalWidth = width ?? (definition.MinWidthCells * _currentDesktopCellSize); + var finalHeight = height ?? (definition.MinHeightCells * _currentDesktopCellSize); + + // 对齐网格 + x = Math.Round(x / _currentDesktopCellSize) * _currentDesktopCellSize; + y = Math.Round(y / _currentDesktopCellSize) * _currentDesktopCellSize; + finalWidth = Math.Round(finalWidth / _currentDesktopCellSize) * _currentDesktopCellSize; + finalHeight = Math.Round(finalHeight / _currentDesktopCellSize) * _currentDesktopCellSize; var placementId = Guid.NewGuid().ToString("N"); var placement = new FusedDesktopComponentPlacementSnapshot @@ -235,7 +259,7 @@ public partial class TransparentOverlayWindow : Window } var control = descriptor.CreateControl( - DefaultCellSize, + _currentDesktopCellSize, _timeZoneService, _weatherDataService, _recommendationInfoService, @@ -270,24 +294,44 @@ public partial class TransparentOverlayWindow : Window /// public void RenderComponent(string placementId, Control component, double x, double y, double width, double height) { + var grid = new Grid(); + grid.Children.Add(component); + + var resizeHandle = new Border + { + Width = 24, + Height = 24, + Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#3B82F6")), + CornerRadius = new Avalonia.CornerRadius(12), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Avalonia.Thickness(0, 0, -12, -12), + Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.BottomRightCorner), + Tag = "desktop-component-resize-handle", + IsVisible = false + }; + grid.Children.Add(resizeHandle); + var host = new Border { Tag = placementId, Width = width, Height = height, - Background = Brushes.Transparent, - CornerRadius = new CornerRadius(12), - ClipToBounds = true, - Child = component + Background = Avalonia.Media.Brushes.Transparent, + CornerRadius = new Avalonia.CornerRadius(12), + ClipToBounds = false, // 允许把手溢出 + BorderBrush = Avalonia.Media.Brushes.Transparent, + BorderThickness = new Avalonia.Thickness(3), + Child = grid, + Classes = { "desktop-component-host" } }; Canvas.SetLeft(host, x); Canvas.SetTop(host, y); - // 添加拖拽支持 host.PointerPressed += OnComponentPointerPressed; - host.PointerMoved += OnComponentPointerMoved; - host.PointerReleased += OnComponentPointerReleased; + host.PointerMoved += OnInteractionPointerMoved; + host.PointerReleased += OnInteractionPointerReleased; // 右键上下文菜单(删除组件) host.ContextRequested += OnComponentContextRequested; @@ -328,7 +372,60 @@ public partial class TransparentOverlayWindow : Window e.Handled = true; } - // 组件拖拽处理 + // 取消选中 + private void OnCanvasPointerPressed(object? sender, PointerPressedEventArgs e) + { + DeselectComponent(); + } + + // 选中组件 + private void SelectComponent(Border host) + { + if (_selectedHost == host) return; + DeselectComponent(); + + _selectedHost = host; + + // 渲染选中边框和把手 + host.BorderBrush = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#3B82F6")); + host.Classes.Add("desktop-component-host-selected"); + + if (host.Child is Grid grid) + { + foreach (var child in grid.Children) + { + if (child is Control c && c.Tag is string tg && tg == "desktop-component-resize-handle") + { + c.IsVisible = true; + break; + } + } + } + } + + private void DeselectComponent() + { + if (_selectedHost != null) + { + _selectedHost.BorderBrush = Avalonia.Media.Brushes.Transparent; + _selectedHost.Classes.Remove("desktop-component-host-selected"); + + if (_selectedHost.Child is Grid grid) + { + foreach (var child in grid.Children) + { + if (child is Control c && c.Tag is string tg && tg == "desktop-component-resize-handle") + { + c.IsVisible = false; + break; + } + } + } + } + _selectedHost = null; + } + + // 组件拖拽与缩放处理 private void OnComponentPointerPressed(object? sender, PointerPressedEventArgs e) { if (sender is not Border host || host.Tag is not string placementId) return; @@ -336,55 +433,97 @@ public partial class TransparentOverlayWindow : Window var point = e.GetCurrentPoint(this); if (!point.Properties.IsLeftButtonPressed) return; - _isDragging = true; - _draggingPlacementId = placementId; - _draggingHost = host; - _dragStartPoint = e.GetPosition(this); + SelectComponent(host); + + _interactionPlacementId = placementId; + _interactionHost = host; + _interactionStartPoint = e.GetPosition(this); + + // 这里必须用未吸附的原始屏幕位置计算 delta + _interactionOriginalX = Canvas.GetLeft(host); + _interactionOriginalY = Canvas.GetTop(host); + _interactionOriginalWidth = host.Width; + _interactionOriginalHeight = host.Height; + + if (e.Source is Control sourceControl && sourceControl.Tag is string tag && tag == "desktop-component-resize-handle") + { + _isResizing = true; + _isDragging = false; + } + else + { + _isDragging = true; + _isResizing = false; + } e.Pointer.Capture(host); e.Handled = true; } - private void 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 deltaX = currentPoint.X - _dragStartPoint.X; - var deltaY = currentPoint.Y - _dragStartPoint.Y; + var deltaX = currentPoint.X - _interactionStartPoint.X; + var deltaY = currentPoint.Y - _interactionStartPoint.Y; - var currentX = Canvas.GetLeft(_draggingHost); - var currentY = Canvas.GetTop(_draggingHost); + if (_isDragging) + { + var rawX = _interactionOriginalX + deltaX; + var rawY = _interactionOriginalY + deltaY; + + var snapX = Math.Round(rawX / _currentDesktopCellSize) * _currentDesktopCellSize; + var snapY = Math.Round(rawY / _currentDesktopCellSize) * _currentDesktopCellSize; + + Canvas.SetLeft(_interactionHost, snapX); + Canvas.SetTop(_interactionHost, snapY); + } + else if (_isResizing) + { + var rawWidth = _interactionOriginalWidth + deltaX; + var rawHeight = _interactionOriginalHeight + deltaY; + + var snapWidth = Math.Round(rawWidth / _currentDesktopCellSize) * _currentDesktopCellSize; + var snapHeight = Math.Round(rawHeight / _currentDesktopCellSize) * _currentDesktopCellSize; + + // 防溢出与极小值保护 + snapWidth = Math.Max(_currentDesktopCellSize, snapWidth); + snapHeight = Math.Max(_currentDesktopCellSize, snapHeight); + + _interactionHost.Width = snapWidth; + _interactionHost.Height = snapHeight; + } - Canvas.SetLeft(_draggingHost, currentX + deltaX); - Canvas.SetTop(_draggingHost, currentY + deltaY); - - _dragStartPoint = currentPoint; 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; + _isResizing = false; return; } - // 更新布局中的位置 - var placement = _layout.ComponentPlacements.Find(p => p.PlacementId == _draggingPlacementId); + // 更新布局中的位置与尺寸 + var placement = _layout.ComponentPlacements.Find(p => p.PlacementId == _interactionPlacementId); if (placement is not null) { - placement.X = Canvas.GetLeft(_draggingHost); - placement.Y = Canvas.GetTop(_draggingHost); + placement.X = Canvas.GetLeft(_interactionHost); + placement.Y = Canvas.GetTop(_interactionHost); + placement.Width = _interactionHost.Width; + placement.Height = _interactionHost.Height; } UpdateInteractiveRegions(); SaveLayout(); _isDragging = false; - _draggingPlacementId = null; - _draggingHost = null; + _isResizing = false; + _interactionPlacementId = null; + _interactionHost = null; e.Pointer.Capture(null); e.Handled = true;