mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
fead.做了状态栏文字组件,支持了位置放置。
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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": "グリッド設定",
|
||||
|
||||
@@ -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": "그리드 설정",
|
||||
|
||||
@@ -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": "网格设置",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> 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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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<SelectionOption> ClockFormats { get; }
|
||||
|
||||
public IReadOnlyList<SelectionOption> ClockPositions { get; }
|
||||
|
||||
public IReadOnlyList<SelectionOption> TextCapsulePositions { get; }
|
||||
|
||||
public IReadOnlyList<SelectionOption> 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<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()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
22
LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml
Normal file
22
LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml
Normal 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>
|
||||
167
LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml.cs
Normal file
167
LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml.cs
Normal 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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public partial class MainWindow
|
||||
private readonly IComponentPreviewImageService _componentPreviewImageService = new ComponentPreviewImageService();
|
||||
private readonly Dictionary<ComponentPreviewKey, List<ComponentLibraryPreviewVisualTarget>> _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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,12 +364,295 @@ public partial class MainWindow
|
||||
? ClockDisplayFormat.HourMinute
|
||||
: ClockDisplayFormat.HourMinuteSecond;
|
||||
_statusBarClockTransparentBackground = snapshot.StatusBarClockTransparentBackground;
|
||||
_clockPosition = NormalizeClockPosition(snapshot.ClockPosition);
|
||||
|
||||
if (ClockWidget is not null)
|
||||
{
|
||||
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
|
||||
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground);
|
||||
_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)
|
||||
{
|
||||
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"
|
||||
};
|
||||
}
|
||||
|
||||
/// <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()
|
||||
@@ -377,23 +660,282 @@ public partial class MainWindow
|
||||
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
|
||||
var hasVisibleTopStatusComponent = false;
|
||||
|
||||
if (ClockWidget is not null)
|
||||
{
|
||||
ClockWidget.IsVisible = showClock;
|
||||
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground);
|
||||
// 先隐藏所有时钟控件
|
||||
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.SetDisplayFormat(_clockDisplayFormat);
|
||||
var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3;
|
||||
Grid.SetColumnSpan(ClockWidget, columnSpan);
|
||||
var targetPosition = _clockPosition;
|
||||
var canAdd = CanAddComponentAtPosition(targetPosition);
|
||||
|
||||
if (canAdd)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TopStatusBarHost is not null)
|
||||
{
|
||||
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()
|
||||
|
||||
@@ -233,13 +233,47 @@
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="4">
|
||||
<StackPanel x:Name="TopStatusComponentsPanel"
|
||||
<Grid ColumnDefinitions="*,Auto,*">
|
||||
<!-- 左侧状态栏组件 -->
|
||||
<StackPanel x:Name="TopStatusLeftPanel"
|
||||
Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="6">
|
||||
<comp:ClockWidget x:Name="ClockWidget"
|
||||
Spacing="6"
|
||||
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 x:Name="BottomTaskbarContainer"
|
||||
|
||||
@@ -135,6 +135,11 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
private string _statusBarSpacingMode = "Relaxed";
|
||||
private int _statusBarCustomSpacingPercent = 12;
|
||||
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 string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
|
||||
private string _languageCode = "zh-CN";
|
||||
@@ -238,9 +243,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
TaskbarProfileButton.IsEnabled = false;
|
||||
TaskbarProfilePopup.IsOpen = false;
|
||||
|
||||
ClockWidget.IsVisible = true;
|
||||
ClockWidget.SetDisplayFormat(ClockDisplayFormat.HourMinute);
|
||||
ClockWidget.SetTransparentBackground(false);
|
||||
ClockWidgetLeft.IsVisible = true;
|
||||
ClockWidgetLeft.SetDisplayFormat(ClockDisplayFormat.HourMinute);
|
||||
ClockWidgetLeft.SetTransparentBackground(false);
|
||||
|
||||
ConfigureDesignTimeDesktopGrid();
|
||||
PopulateDesignTimeDesktopSurface();
|
||||
@@ -288,7 +293,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
DesktopPagesHost.ColumnDefinitions.Clear();
|
||||
DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
|
||||
|
||||
ClockWidget.ApplyCellSize(72);
|
||||
ClockWidgetLeft.ApplyCellSize(72);
|
||||
}
|
||||
|
||||
private void PopulateDesignTimeDesktopSurface()
|
||||
@@ -481,7 +486,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
RebuildDesktopGrid();
|
||||
LoadLauncherEntriesAsync();
|
||||
InitializeTimeZoneSettings();
|
||||
ClockWidget.SetTimeZoneService(_timeZoneService);
|
||||
ClockWidgetLeft.SetTimeZoneService(_timeZoneService);
|
||||
ClockWidgetCenter.SetTimeZoneService(_timeZoneService);
|
||||
ClockWidgetRight.SetTimeZoneService(_timeZoneService);
|
||||
|
||||
_suppressSettingsPersistence = false;
|
||||
PersistSettings();
|
||||
@@ -621,7 +628,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
|
||||
private void ApplyDesktopStatusBarComponentSpacing()
|
||||
{
|
||||
ApplyStatusBarComponentSpacingForPanel(TopStatusComponentsPanel, _currentDesktopCellSize);
|
||||
ApplyStatusBarComponentSpacingForPanel(TopStatusLeftPanel, _currentDesktopCellSize);
|
||||
ApplyStatusBarComponentSpacingForPanel(TopStatusCenterPanel, _currentDesktopCellSize);
|
||||
ApplyStatusBarComponentSpacingForPanel(TopStatusRightPanel, _currentDesktopCellSize);
|
||||
}
|
||||
|
||||
private int ResolveStatusBarSpacingPercent()
|
||||
@@ -697,8 +706,19 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
ApplyUnifiedMainRectangleChrome();
|
||||
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(taskbarCellHeight * 0.16, 6, 14));
|
||||
|
||||
ClockWidget.Margin = new Thickness(0);
|
||||
ClockWidget.ApplyCellSize(cellSize);
|
||||
ClockWidgetLeft.Margin = new Thickness(0);
|
||||
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);
|
||||
|
||||
@@ -737,7 +757,12 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
|
||||
if (_currentDesktopCellSize > 0)
|
||||
{
|
||||
ClockWidget.ApplyCellSize(_currentDesktopCellSize);
|
||||
ClockWidgetLeft.ApplyCellSize(_currentDesktopCellSize);
|
||||
ClockWidgetCenter.ApplyCellSize(_currentDesktopCellSize);
|
||||
ClockWidgetRight.ApplyCellSize(_currentDesktopCellSize);
|
||||
TextCapsuleWidgetLeft.ApplyCellSize(_currentDesktopCellSize);
|
||||
TextCapsuleWidgetCenter.ApplyCellSize(_currentDesktopCellSize);
|
||||
TextCapsuleWidgetRight.ApplyCellSize(_currentDesktopCellSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,78 @@
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</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>
|
||||
|
||||
<Separator Classes="settings-separator" />
|
||||
|
||||
@@ -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<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)
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public void RenderComponent(string placementId, Control component, double x, double y, double width, double height)
|
||||
{
|
||||
var grid = new Grid();
|
||||
grid.Children.Add(component);
|
||||
|
||||
var resizeHandle = new Border
|
||||
{
|
||||
Width = 24,
|
||||
Height = 24,
|
||||
Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#3B82F6")),
|
||||
CornerRadius = new Avalonia.CornerRadius(12),
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
|
||||
Margin = new Avalonia.Thickness(0, 0, -12, -12),
|
||||
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.BottomRightCorner),
|
||||
Tag = "desktop-component-resize-handle",
|
||||
IsVisible = false
|
||||
};
|
||||
grid.Children.Add(resizeHandle);
|
||||
|
||||
var host = new Border
|
||||
{
|
||||
Tag = placementId,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Background = 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;
|
||||
|
||||
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;
|
||||
_draggingPlacementId = placementId;
|
||||
_draggingHost = host;
|
||||
_dragStartPoint = e.GetPosition(this);
|
||||
_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;
|
||||
|
||||
Canvas.SetLeft(_draggingHost, currentX + deltaX);
|
||||
Canvas.SetTop(_draggingHost, currentY + 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;
|
||||
}
|
||||
|
||||
_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;
|
||||
|
||||
Reference in New Issue
Block a user