diff --git a/Actions/SetVolume.cs b/Actions/SetVolume.cs
index 1a01d7b1..f783166e 100644
--- a/Actions/SetVolume.cs
+++ b/Actions/SetVolume.cs
@@ -167,7 +167,18 @@ internal class MMDeviceEnumeratorWrapper
public MMDeviceEnumeratorWrapper()
{
var type = Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"));
- _enumerator = (IMMDeviceEnumerator)Activator.CreateInstance(type);
+ if (type == null)
+ {
+ throw new InvalidOperationException("无法获取 MMDeviceEnumerator 类型。");
+ }
+
+ var instance = Activator.CreateInstance(type);
+ if (instance == null)
+ {
+ throw new InvalidOperationException("无法创建 MMDeviceEnumerator 实例。");
+ }
+
+ _enumerator = (IMMDeviceEnumerator)instance;
}
public IMMDevice GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role)
diff --git a/Actions/ShowFloatingWindowAction.cs b/Actions/ShowFloatingWindowAction.cs
index d304d392..53fc1693 100644
--- a/Actions/ShowFloatingWindowAction.cs
+++ b/Actions/ShowFloatingWindowAction.cs
@@ -26,14 +26,26 @@ protected override async Task OnInvoke()
try
{
var shouldShow = Settings.ShowFloatingWindow;
+ var config = GlobalConstants.MainConfig?.Data;
+
+ // 如果没有可用的悬浮窗组件,则强制隐藏且不允许显示
+ if (_floatingWindowService.Entries.Count == 0)
+ {
+ shouldShow = false;
+ _logger.LogDebug("没有可用的悬浮窗组件,强制隐藏悬浮窗");
+ }
if (IsRevertable)
{
PreviousStates[ActionSet.Guid] = GlobalConstants.MainConfig!.Data.ShowFloatingWindow;
}
- GlobalConstants.MainConfig!.Data.ShowFloatingWindow = shouldShow;
- GlobalConstants.MainConfig.Save();
+ if (config != null)
+ {
+ config.ShowFloatingWindow = shouldShow;
+ GlobalConstants.MainConfig?.Save();
+ }
+
_floatingWindowService.UpdateWindowState();
_logger.LogInformation("悬浮窗状态已更新为: {State}", shouldShow ? "开启" : "关闭");
diff --git a/Actions/SwitchFloatingWindowThemeAction.cs b/Actions/SwitchFloatingWindowThemeAction.cs
new file mode 100644
index 00000000..f6414961
--- /dev/null
+++ b/Actions/SwitchFloatingWindowThemeAction.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Threading.Tasks;
+using ClassIsland.Core.Abstractions.Automation;
+using ClassIsland.Core.Attributes;
+using ClassIsland.Shared;
+using Microsoft.Extensions.Logging;
+using SystemTools.Services;
+using SystemTools.Settings;
+
+namespace SystemTools.Actions;
+
+///
+/// 切换悬浮窗主题行动
+///
+[ActionInfo("SystemTools.SwitchFloatingWindowTheme", "切换悬浮窗主题", "\uE790", false)]
+public class SwitchFloatingWindowThemeAction(ILogger logger) : ActionBase
+{
+ private readonly ILogger _logger = logger;
+
+ protected override async Task OnInvoke()
+ {
+ _logger.LogDebug("SwitchFloatingWindowThemeAction OnInvoke 开始");
+
+ try
+ {
+ var service = IAppHost.GetService();
+
+ if (Settings.TargetTheme >= 0)
+ {
+ service.SetWindowTheme(Settings.TargetTheme);
+ _logger.LogInformation("已设置悬浮窗主题为: {Theme}", GetThemeName(Settings.TargetTheme));
+ }
+ else
+ {
+ service.ToggleWindowTheme();
+ _logger.LogInformation("已切换到下一个悬浮窗主题");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "切换悬浮窗主题失败");
+ throw;
+ }
+
+ await base.OnInvoke();
+ _logger.LogDebug("SwitchFloatingWindowThemeAction OnInvoke 完成");
+ }
+
+ private static string GetThemeName(int theme)
+ {
+ return theme switch
+ {
+ 0 => "跟随系统",
+ 1 => "浅色",
+ 2 => "深色",
+ _ => "未知"
+ };
+ }
+}
diff --git a/Actions/ToggleFloatingWindowLayerAction.cs b/Actions/ToggleFloatingWindowLayerAction.cs
new file mode 100644
index 00000000..1382ece2
--- /dev/null
+++ b/Actions/ToggleFloatingWindowLayerAction.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Threading.Tasks;
+using ClassIsland.Core.Abstractions.Automation;
+using ClassIsland.Core.Attributes;
+using ClassIsland.Shared;
+using Microsoft.Extensions.Logging;
+using SystemTools.Services;
+using SystemTools.Settings;
+
+namespace SystemTools.Actions;
+
+///
+/// 切换悬浮窗层级行动
+///
+[ActionInfo("SystemTools.ToggleFloatingWindowLayer", "切换悬浮窗层级", "\uE9A8", false)]
+public class ToggleFloatingWindowLayerAction(ILogger logger) : ActionBase
+{
+ private readonly ILogger _logger = logger;
+
+ protected override async Task OnInvoke()
+ {
+ _logger.LogDebug("ToggleFloatingWindowLayerAction OnInvoke 开始");
+
+ try
+ {
+ var service = IAppHost.GetService();
+
+ // 根据设置决定是切换还是设置到指定层级
+ // TargetLayer: -1=切换, 0=置顶, 1=置底
+ if (Settings.TargetLayer >= 0)
+ {
+ service.SetWindowLayer(Settings.TargetLayer);
+ _logger.LogInformation("已设置悬浮窗层级为: {Layer}", Settings.TargetLayer == 0 ? "置顶" : "置底");
+ }
+ else
+ {
+ service.ToggleWindowLayer();
+ _logger.LogInformation("已切换悬浮窗层级状态");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "切换悬浮窗层级失败");
+ throw;
+ }
+
+ await base.OnInvoke();
+ _logger.LogDebug("ToggleFloatingWindowLayerAction OnInvoke 完成");
+ }
+}
diff --git a/Actions/ToggleFloatingWindowProfileAction.cs b/Actions/ToggleFloatingWindowProfileAction.cs
new file mode 100644
index 00000000..4f870525
--- /dev/null
+++ b/Actions/ToggleFloatingWindowProfileAction.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Threading.Tasks;
+using ClassIsland.Core.Abstractions.Automation;
+using ClassIsland.Core.Attributes;
+using ClassIsland.Shared;
+using Microsoft.Extensions.Logging;
+using SystemTools.Services;
+using SystemTools.Settings;
+
+namespace SystemTools.Actions;
+
+///
+/// 切换悬浮窗配置方案行动
+///
+[ActionInfo("SystemTools.ToggleFloatingWindowProfile", "切换悬浮窗配置方案", "\uE9A8", false)]
+public class ToggleFloatingWindowProfileAction(ILogger logger) : ActionBase
+{
+ private readonly ILogger _logger = logger;
+
+ protected override async Task OnInvoke()
+ {
+ _logger.LogDebug("ToggleFloatingWindowProfileAction OnInvoke 开始");
+
+ try
+ {
+ var service = IAppHost.GetService();
+
+ // 根据设置决定是切换到下一个还是切换到指定方案
+ // TargetProfileName: null=切换到下一个, 其他=指定方案名称
+ if (!string.IsNullOrWhiteSpace(Settings.TargetProfileName))
+ {
+ service.SwitchToProfile(Settings.TargetProfileName);
+ _logger.LogInformation("已切换到悬浮窗配置方案: {Name}", Settings.TargetProfileName);
+ }
+ else
+ {
+ service.ToggleWindowProfile();
+ _logger.LogInformation("已切换到下一个悬浮窗配置方案");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "切换悬浮窗配置方案失败");
+ throw;
+ }
+
+ await base.OnInvoke();
+ _logger.LogDebug("ToggleFloatingWindowProfileAction OnInvoke 完成");
+ }
+}
diff --git a/Actions/ToggleWorkflowAction.cs b/Actions/ToggleWorkflowAction.cs
index 3dac4fbf..86fef99d 100644
--- a/Actions/ToggleWorkflowAction.cs
+++ b/Actions/ToggleWorkflowAction.cs
@@ -12,7 +12,7 @@
namespace SystemTools.Actions;
-[ActionInfo("SystemTools.ToggleWorkflow", "开关自动化", "\uE9A8", false)]
+[ActionInfo("SystemTools.ToggleWorkflow", "开关自动化", "\uE8B8", false)]
public class ToggleWorkflowAction(ILogger logger) : ActionBase
{
private readonly ILogger _logger = logger;
@@ -98,6 +98,13 @@ protected override async Task OnRevert()
{
await base.OnRevert();
+ if (Settings == null || !Settings.RevertToOriginal)
+ {
+ _logger.LogDebug("RevertToOriginal 为 false,跳过恢复");
+ PreviousSnapshots.TryRemove(ActionSet.Guid, out _);
+ return;
+ }
+
if (!PreviousSnapshots.TryRemove(ActionSet.Guid, out var snapshot))
{
_logger.LogWarning("未找到触发前状态,跳过恢复。ActionSet={ActionSetGuid}", ActionSet.Guid);
diff --git a/ConfigHandlers/ButtonRulesetConfig.cs b/ConfigHandlers/ButtonRulesetConfig.cs
new file mode 100644
index 00000000..9a045695
--- /dev/null
+++ b/ConfigHandlers/ButtonRulesetConfig.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Serialization;
+using ClassIsland.Core.Models.Ruleset;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace SystemTools.ConfigHandlers;
+
+///
+/// 悬浮窗按钮的规则集配置
+///
+public partial class ButtonRulesetConfig : ObservableObject
+{
+ [ObservableProperty]
+ [JsonPropertyName("isVisible")]
+ private bool _isVisible = true;
+
+ [ObservableProperty]
+ [JsonPropertyName("hideOnRule")]
+ private bool _hideOnRule;
+
+ [ObservableProperty]
+ [JsonPropertyName("hidingRules")]
+ private Ruleset _hidingRules = new();
+}
diff --git a/ConfigHandlers/FloatingWindowProfile.cs b/ConfigHandlers/FloatingWindowProfile.cs
new file mode 100644
index 00000000..3a4f88ae
--- /dev/null
+++ b/ConfigHandlers/FloatingWindowProfile.cs
@@ -0,0 +1,108 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json.Serialization;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace SystemTools.ConfigHandlers;
+
+///
+/// 悬浮窗配置方案,保存一套完整的悬浮窗布局和外观配置。
+/// 注意:显示状态(ShowFloatingWindow)和规则集(HideOnRule/HidingRules)是全局设置,不随方案切换。
+///
+public partial class FloatingWindowProfile : ObservableObject
+{
+ [ObservableProperty]
+ [JsonPropertyName("name")]
+ private string _name = "Default";
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowHorizontal")]
+ private bool _floatingWindowHorizontal;
+
+ [JsonPropertyName("floatingWindowButtonOrder")]
+ public List FloatingWindowButtonOrder { get; set; } = new();
+
+ [JsonPropertyName("floatingWindowButtonRows")]
+ public List> FloatingWindowButtonRows { get; set; } = new();
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowScale")]
+ private double _floatingWindowScale = 1.0;
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowIconSize")]
+ private int _floatingWindowIconSize = 22;
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowTextSize")]
+ private int _floatingWindowTextSize = 12;
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowOpacity")]
+ private int _floatingWindowOpacity = 80;
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowPositionX")]
+ private int _floatingWindowPositionX = 100;
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowPositionY")]
+ private int _floatingWindowPositionY = 100;
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowLayer")]
+ private int _floatingWindowLayer = 1;
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowLayerRecheckMode")]
+ private int _floatingWindowLayerRecheckMode = 1;
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowShadowEnabled")]
+ private bool _floatingWindowShadowEnabled = true;
+
+ [ObservableProperty]
+ [JsonPropertyName("floatingWindowDragHandleAlwaysVisible")]
+ private bool _floatingWindowDragHandleAlwaysVisible;
+
+ [JsonPropertyName("floatingWindowButtonRulesets")]
+ public Dictionary FloatingWindowButtonRulesets { get; set; } = new();
+
+ [JsonPropertyName("floatingWindowRowRulesets")]
+ public List FloatingWindowRowRulesets { get; set; } = new();
+
+ ///
+ /// 清理不存在的按钮ID,返回是否有变更
+ ///
+ public bool PruneInvalidButtonIds(IEnumerable validButtonIds)
+ {
+ var validSet = validButtonIds.ToHashSet();
+ var changed = false;
+
+ var newOrder = FloatingWindowButtonOrder.Where(id => validSet.Contains(id)).ToList();
+ if (newOrder.Count != FloatingWindowButtonOrder.Count)
+ {
+ FloatingWindowButtonOrder = newOrder;
+ changed = true;
+ }
+
+ var newRows = FloatingWindowButtonRows
+ .Select(row => row.Where(id => validSet.Contains(id)).ToList())
+ .ToList();
+ if (newRows.Count != FloatingWindowButtonRows.Count ||
+ newRows.Zip(FloatingWindowButtonRows, (a, b) => a.SequenceEqual(b)).Any(x => !x))
+ {
+ FloatingWindowButtonRows = newRows;
+ changed = true;
+ }
+
+ var invalidButtonConfigs = FloatingWindowButtonRulesets.Keys.Where(id => !validSet.Contains(id)).ToList();
+ foreach (var id in invalidButtonConfigs)
+ {
+ FloatingWindowButtonRulesets.Remove(id);
+ changed = true;
+ }
+
+ return changed;
+ }
+}
diff --git a/ConfigHandlers/FloatingWindowProfileManager.cs b/ConfigHandlers/FloatingWindowProfileManager.cs
new file mode 100644
index 00000000..caa08950
--- /dev/null
+++ b/ConfigHandlers/FloatingWindowProfileManager.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ClassIsland.Shared.Helpers;
+using SystemTools.Shared;
+
+namespace SystemTools.ConfigHandlers;
+
+///
+/// 管理悬浮窗配置方案的存储和加载,每个方案为独立的 JSON 文件。
+///
+public class FloatingWindowProfileManager
+{
+ private readonly string _profilesDirectory;
+ private FloatingWindowProfile _currentProfile = new();
+ private string _currentProfileName = "Default";
+
+ public static FloatingWindowProfile DefaultProfile { get; } = new()
+ {
+ Name = "Default",
+ FloatingWindowScale = 1.0,
+ FloatingWindowIconSize = 22,
+ FloatingWindowTextSize = 12,
+ FloatingWindowOpacity = 80,
+ FloatingWindowPositionX = 100,
+ FloatingWindowPositionY = 100,
+ FloatingWindowLayer = 1,
+ FloatingWindowLayerRecheckMode = 1,
+ FloatingWindowShadowEnabled = true,
+ FloatingWindowButtonOrder = new List(),
+ FloatingWindowButtonRows = new List>(),
+ FloatingWindowButtonRulesets = new Dictionary(),
+ FloatingWindowRowRulesets = new List()
+ };
+
+ public FloatingWindowProfileManager()
+ {
+ _profilesDirectory = Path.Combine(DependencyPaths.GetDependencyRoot(), "FloatingWindowProfiles");
+ if (!Directory.Exists(_profilesDirectory))
+ {
+ Directory.CreateDirectory(_profilesDirectory);
+ }
+ }
+
+ ///
+ /// 从旧版 MainConfigData 迁移配置到文件存储
+ ///
+ public void MigrateFromLegacyConfig(MainConfigData legacyData)
+ {
+ var defaultPath = GetProfilePath("Default");
+ if (File.Exists(defaultPath))
+ {
+ return;
+ }
+
+ var profile = new FloatingWindowProfile
+ {
+ Name = "Default",
+ FloatingWindowHorizontal = legacyData.FloatingWindowHorizontal,
+ FloatingWindowButtonOrder = new List(legacyData.FloatingWindowButtonOrder ?? []),
+ FloatingWindowButtonRows = (legacyData.FloatingWindowButtonRows ?? []).Select(r => new List(r)).ToList(),
+ FloatingWindowScale = legacyData.FloatingWindowScale,
+ FloatingWindowIconSize = legacyData.FloatingWindowIconSize,
+ FloatingWindowTextSize = legacyData.FloatingWindowTextSize,
+ FloatingWindowOpacity = legacyData.FloatingWindowOpacity,
+ FloatingWindowPositionX = legacyData.FloatingWindowPositionX,
+ FloatingWindowPositionY = legacyData.FloatingWindowPositionY,
+ FloatingWindowLayer = legacyData.FloatingWindowLayer,
+ FloatingWindowLayerRecheckMode = legacyData.FloatingWindowLayerRecheckMode,
+ FloatingWindowShadowEnabled = legacyData.FloatingWindowShadowEnabled,
+ FloatingWindowDragHandleAlwaysVisible = legacyData.FloatingWindowDragHandleAlwaysVisible,
+ FloatingWindowButtonRulesets = new Dictionary(legacyData.FloatingWindowButtonRulesets ?? []),
+ FloatingWindowRowRulesets = new List(legacyData.FloatingWindowRowRulesets ?? [])
+ };
+
+ ConfigureFileHelper.SaveConfig(defaultPath, profile);
+ }
+
+ public string ProfilesDirectory => _profilesDirectory;
+
+ public FloatingWindowProfile CurrentProfile => _currentProfile;
+
+ public string CurrentProfileName
+ {
+ get => _currentProfileName;
+ private set
+ {
+ if (_currentProfileName == value) return;
+ _currentProfileName = value;
+ CurrentProfile.Name = value;
+ }
+ }
+
+ ///
+ /// 获取所有可用的方案名称列表
+ ///
+ public IReadOnlyList GetProfileNames()
+ {
+ if (!Directory.Exists(_profilesDirectory))
+ {
+ return new List { "Default" };
+ }
+
+ var names = Directory.GetFiles(_profilesDirectory, "*.json")
+ .Select(Path.GetFileNameWithoutExtension)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x!)
+ .ToList();
+
+ if (names.Count == 0)
+ {
+ names.Add("Default");
+ }
+
+ return names;
+ }
+
+ ///
+ /// 加载指定名称的方案
+ ///
+ public void LoadProfile(string profileName)
+ {
+ if (string.IsNullOrWhiteSpace(profileName))
+ {
+ profileName = "Default";
+ }
+
+ var path = GetProfilePath(profileName);
+ if (!File.Exists(path))
+ {
+ _currentProfile = ConfigureFileHelper.CopyObject(DefaultProfile);
+ _currentProfile.Name = profileName;
+ SaveProfile();
+ }
+ else
+ {
+ _currentProfile = ConfigureFileHelper.LoadConfig(path);
+ _currentProfile.Name = profileName;
+ }
+
+ _currentProfileName = profileName;
+ }
+
+ ///
+ /// 保存当前方案
+ ///
+ public void SaveProfile()
+ {
+ var path = GetProfilePath(_currentProfileName);
+ ConfigureFileHelper.SaveConfig(path, _currentProfile);
+ }
+
+ ///
+ /// 创建新方案,基于当前方案或默认方案
+ ///
+ public string CreateProfile(string? name = null)
+ {
+ var baseName = name?.Trim();
+ if (string.IsNullOrWhiteSpace(baseName))
+ {
+ baseName = $"Profile {GetProfileNames().Count + 1}";
+ }
+
+ var profileName = baseName;
+ var counter = 1;
+ while (File.Exists(GetProfilePath(profileName)))
+ {
+ profileName = $"{baseName} ({counter})";
+ counter++;
+ }
+
+ var newProfile = ConfigureFileHelper.CopyObject(_currentProfile);
+ newProfile.Name = profileName;
+
+ var path = GetProfilePath(profileName);
+ ConfigureFileHelper.SaveConfig(path, newProfile);
+
+ return profileName;
+ }
+
+ ///
+ /// 删除指定方案
+ ///
+ public bool RemoveProfile(string profileName)
+ {
+ if (string.Equals(profileName, "Default", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ var path = GetProfilePath(profileName);
+ if (!File.Exists(path))
+ {
+ return false;
+ }
+
+ try
+ {
+ File.Delete(path);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 重命名方案
+ ///
+ public bool RenameProfile(string oldName, string newName)
+ {
+ if (string.IsNullOrWhiteSpace(newName) || string.Equals(oldName, newName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ var oldPath = GetProfilePath(oldName);
+ var newPath = GetProfilePath(newName);
+
+ if (!File.Exists(oldPath) || File.Exists(newPath))
+ {
+ return false;
+ }
+
+ try
+ {
+ File.Move(oldPath, newPath);
+ if (string.Equals(_currentProfileName, oldName, StringComparison.OrdinalIgnoreCase))
+ {
+ _currentProfileName = newName;
+ _currentProfile.Name = newName;
+ }
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private string GetProfilePath(string profileName)
+ {
+ return Path.Combine(_profilesDirectory, $"{profileName}.json");
+ }
+}
diff --git a/ConfigHandlers/MainConfigData.cs b/ConfigHandlers/MainConfigData.cs
index 52b04d82..d764aae5 100644
--- a/ConfigHandlers/MainConfigData.cs
+++ b/ConfigHandlers/MainConfigData.cs
@@ -1,9 +1,10 @@
-using System;
+using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
+using ClassIsland.Core.Models.Ruleset;
namespace SystemTools.ConfigHandlers;
@@ -345,6 +346,57 @@ public int FloatingWindowLayerRecheckMode
}
}
+ string _currentFloatingWindowProfile = "Default";
+
+ [JsonPropertyName("currentFloatingWindowProfile")]
+ public string CurrentFloatingWindowProfile
+ {
+ get => _currentFloatingWindowProfile;
+ set
+ {
+ if (string.Equals(value, _currentFloatingWindowProfile, StringComparison.Ordinal)) return;
+ _currentFloatingWindowProfile = value;
+ OnPropertyChanged();
+ }
+ }
+
+ bool _floatingWindowRulesetEnabled = false;
+
+ [JsonPropertyName("floatingWindowRulesetEnabled")]
+ public bool FloatingWindowRulesetEnabled
+ {
+ get => _floatingWindowRulesetEnabled;
+ set
+ {
+ if (value == _floatingWindowRulesetEnabled) return;
+ _floatingWindowRulesetEnabled = value;
+ OnPropertyChanged();
+ }
+ }
+
+ bool _floatingWindowDragHandleAlwaysVisible = false;
+
+ [JsonPropertyName("floatingWindowDragHandleAlwaysVisible")]
+ public bool FloatingWindowDragHandleAlwaysVisible
+ {
+ get => _floatingWindowDragHandleAlwaysVisible;
+ set
+ {
+ if (value == _floatingWindowDragHandleAlwaysVisible) return;
+ _floatingWindowDragHandleAlwaysVisible = value;
+ OnPropertyChanged();
+ }
+ }
+
+ [JsonPropertyName("floatingWindowRuleset")]
+ public Ruleset FloatingWindowRuleset { get; set; } = new();
+
+ [JsonPropertyName("floatingWindowButtonRulesets")]
+ public Dictionary FloatingWindowButtonRulesets { get; set; } = new();
+
+ [JsonPropertyName("floatingWindowRowRulesets")]
+ public List FloatingWindowRowRulesets { get; set; } = new();
+
// 行动功能启用状态(Key: 行动ID, Value: 是否启用)
[JsonPropertyName("enabledActions")] public Dictionary EnabledActions { get; set; } = new();
diff --git a/ConfigHandlers/RowRulesetConfig.cs b/ConfigHandlers/RowRulesetConfig.cs
new file mode 100644
index 00000000..64f157a3
--- /dev/null
+++ b/ConfigHandlers/RowRulesetConfig.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Serialization;
+using ClassIsland.Core.Models.Ruleset;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace SystemTools.ConfigHandlers;
+
+///
+/// 悬浮窗行的规则集配置
+///
+public partial class RowRulesetConfig : ObservableObject
+{
+ [ObservableProperty]
+ [JsonPropertyName("isVisible")]
+ private bool _isVisible = true;
+
+ [ObservableProperty]
+ [JsonPropertyName("hideOnRule")]
+ private bool _hideOnRule;
+
+ [ObservableProperty]
+ [JsonPropertyName("hidingRules")]
+ private Ruleset _hidingRules = new();
+}
diff --git a/Controls/Components/BetterCarouselContainerComponent.axaml.cs b/Controls/Components/BetterCarouselContainerComponent.axaml.cs
index 9f0bd426..a5176608 100644
--- a/Controls/Components/BetterCarouselContainerComponent.axaml.cs
+++ b/Controls/Components/BetterCarouselContainerComponent.axaml.cs
@@ -22,8 +22,8 @@ namespace SystemTools.Controls.Components;
[ComponentInfo("A7C3455E-6A4E-4D4D-9D0D-7C6FCB5E1E3A", "更好的轮播容器", "\uF0DB", "带有可单独设置组件显示时长等高级功能的轮播容器")]
public partial class BetterCarouselContainerComponent : ComponentBase, INotifyPropertyChanged
{
- private readonly ILessonsService _lessonsService;
- private readonly IRulesetService _rulesetService;
+ private readonly IRulesetService? _rulesetService;
+ private readonly ILessonsService? _lessonsService;
private readonly Random _random = new();
private Animation? _slideInAnimation;
@@ -83,6 +83,12 @@ public int SelectedIndex
public new event PropertyChangedEventHandler? PropertyChanged;
+ public BetterCarouselContainerComponent()
+ {
+ InitializeComponent();
+ InitializeAnimations();
+ }
+
public BetterCarouselContainerComponent(IRulesetService rulesetService, ILessonsService lessonsService)
{
_rulesetService = rulesetService;
diff --git a/Controls/Components/ClipboardContentComponent.axaml.cs b/Controls/Components/ClipboardContentComponent.axaml.cs
index 45fc118d..40124c47 100644
--- a/Controls/Components/ClipboardContentComponent.axaml.cs
+++ b/Controls/Components/ClipboardContentComponent.axaml.cs
@@ -81,7 +81,13 @@ private async System.Threading.Tasks.Task RefreshClipboardAsync()
{
try
{
- var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
+ var topLevel = TopLevel.GetTopLevel(this);
+ if (topLevel == null)
+ {
+ return;
+ }
+
+ var clipboard = topLevel.Clipboard;
if (clipboard == null)
{
return;
diff --git a/Controls/Components/LocalQuoteComponent.axaml.cs b/Controls/Components/LocalQuoteComponent.axaml.cs
index 5efb7924..fe6a92a4 100644
--- a/Controls/Components/LocalQuoteComponent.axaml.cs
+++ b/Controls/Components/LocalQuoteComponent.axaml.cs
@@ -30,11 +30,11 @@ public partial class LocalQuoteComponent : ComponentBase, IN
{
private const double SwapMotionOffset = 20;
- private readonly DispatcherTimer _carouselTimer;
- private readonly ILessonsService _lessonsService;
+ private readonly DispatcherTimer? _carouselTimer;
+ private readonly ILessonsService? _lessonsService;
private readonly List _quotes = [];
- private readonly Animation _swapOutAnimation;
- private readonly Animation _swapInAnimation;
+ private readonly Animation? _swapOutAnimation;
+ private readonly Animation? _swapInAnimation;
private readonly Random _random = new();
private int _currentIndex = -1;
private string _loadedPath = string.Empty;
@@ -70,6 +70,11 @@ static LocalQuoteComponent()
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
+ public LocalQuoteComponent()
+ {
+ InitializeComponent();
+ }
+
public LocalQuoteComponent(ILessonsService lessonsService)
{
_lessonsService = lessonsService;
diff --git a/Controls/Components/LyricsDisplayComponent.axaml.cs b/Controls/Components/LyricsDisplayComponent.axaml.cs
index 9c325676..2b4a8796 100644
--- a/Controls/Components/LyricsDisplayComponent.axaml.cs
+++ b/Controls/Components/LyricsDisplayComponent.axaml.cs
@@ -208,9 +208,11 @@ private void CaptureLyricsWindow()
using (Bitmap croppedBmp = originalBmp.Clone(cropArea, PixelFormat.Format32bppArgb))
{
ProcessBlackPixels(croppedBmp);
- LyricsBitmap = ConvertToAvaloniaBitmap(croppedBmp);
- _originalWidth = croppedBmp.Width;
- _originalHeight = croppedBmp.Height;
+ var oldBitmap = _lyricsBitmap;
+ LyricsBitmap = ConvertToAvaloniaBitmap(croppedBmp);
+ oldBitmap?.Dispose();
+ _originalWidth = croppedBmp.Width;
+ _originalHeight = croppedBmp.Height;
}
}
@@ -230,8 +232,8 @@ private IntPtr FindWindowByClassPrefix(string classPrefix, string? windowTitle)
PInvoke.EnumWindows((hWnd, lParam) =>
{
Span buffer = stackalloc char[256];
- int length=PInvoke.GetClassName(hWnd, buffer);
- if (length == 0) return false;
+ int length = PInvoke.GetClassName(hWnd, buffer);
+ if (length == 0) return true;
string className = new(buffer.Slice(0, length));
if (className.StartsWith(classPrefix))
@@ -239,14 +241,15 @@ private IntPtr FindWindowByClassPrefix(string classPrefix, string? windowTitle)
if (windowTitle != null)
{
Span buffer2 = stackalloc char[256];
- int length2=PInvoke.GetWindowText(hWnd, buffer2);
- if (length2 == 0) return false;
- string title = new(buffer2.Slice(0, length2));
-
- if (title == windowTitle)
+ int length2 = PInvoke.GetWindowText(hWnd, buffer2);
+ if (length2 > 0)
{
- foundHandle = hWnd;
- return false;
+ string title = new(buffer2.Slice(0, length2));
+ if (title == windowTitle)
+ {
+ foundHandle = hWnd;
+ return false;
+ }
}
}
else
diff --git a/Controls/Components/NetworkStatusComponent.axaml.cs b/Controls/Components/NetworkStatusComponent.axaml.cs
index 47baf0d8..b4f655e3 100644
--- a/Controls/Components/NetworkStatusComponent.axaml.cs
+++ b/Controls/Components/NetworkStatusComponent.axaml.cs
@@ -85,7 +85,7 @@ private void NetworkStatusComponent_OnUnloaded(object? sender, RoutedEventArgs e
{
Settings.PropertyChanged -= OnSettingsPropertyChanged;
_timer.Stop();
- _httpClient.Dispose();
+ try { _httpClient.Dispose(); } catch { }
}
private void OnSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -223,14 +223,22 @@ private async Task TryHttpPingAsync(string url)
var stopwatch = Stopwatch.StartNew();
- using var response = await _httpClient.SendAsync(
- new HttpRequestMessage(HttpMethod.Head, httpUrl),
- HttpCompletionOption.ResponseHeadersRead);
+ try
+ {
+ using var response = await _httpClient.SendAsync(
+ new HttpRequestMessage(HttpMethod.Head, httpUrl),
+ HttpCompletionOption.ResponseHeadersRead);
- stopwatch.Stop();
- response.EnsureSuccessStatusCode();
+ stopwatch.Stop();
+ response.EnsureSuccessStatusCode();
- return stopwatch.ElapsedMilliseconds;
+ return stopwatch.ElapsedMilliseconds;
+ }
+ catch
+ {
+ stopwatch.Stop();
+ throw;
+ }
}
private void UpdateStatus(long delay)
diff --git a/Controls/Components/NextClassDisplayComponent.axaml.cs b/Controls/Components/NextClassDisplayComponent.axaml.cs
index 498106e6..a2f9db16 100644
--- a/Controls/Components/NextClassDisplayComponent.axaml.cs
+++ b/Controls/Components/NextClassDisplayComponent.axaml.cs
@@ -21,9 +21,9 @@ public partial class NextClassDisplayComponent : ComponentBase 0 && FirstTextBlock.Bounds.Height > 0) break;
await Task.Delay(100, token);
}
@@ -79,6 +81,11 @@ private void UpdateMarquee()
double textHeight = FirstTextBlock.Bounds.Height;
double maxWidth = Settings.ComponentWidth;
+ if (textWidth <= 0 || textHeight <= 0)
+ {
+ return;
+ }
+
double finalWidth = Math.Min(textWidth + 24, maxWidth);
LayoutRoot.Width = finalWidth;
@@ -114,7 +121,10 @@ private void UpdateMarquee()
}
}
catch (OperationCanceledException) { }
- catch (Exception) { }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[ScrollingText] UpdateMarquee error: {ex.Message}");
+ }
}, DispatcherPriority.Loaded);
}
diff --git a/Controls/KillProcessSettingsControl.cs b/Controls/KillProcessSettingsControl.cs
index 888b3c39..75e4c45c 100644
--- a/Controls/KillProcessSettingsControl.cs
+++ b/Controls/KillProcessSettingsControl.cs
@@ -186,7 +186,7 @@ private async Task ShowProcessListWindow(string processList)
};
copyButton.Click += async (s, e) =>
{
- if (TopLevel.GetTopLevel(this) is { } topLevel)
+ if (TopLevel.GetTopLevel(this) is { } topLevel && topLevel.Clipboard != null)
{
await topLevel.Clipboard.SetTextAsync(processList);
}
diff --git a/Controls/ProcessRunningRuleSettingsControl.cs b/Controls/ProcessRunningRuleSettingsControl.cs
index 592354dd..0f970e5e 100644
--- a/Controls/ProcessRunningRuleSettingsControl.cs
+++ b/Controls/ProcessRunningRuleSettingsControl.cs
@@ -152,7 +152,7 @@ private async Task ShowProcessListWindow(string processList)
};
copyButton.Click += async (_, _) =>
{
- if (TopLevel.GetTopLevel(this) is { } topLevel)
+ if (TopLevel.GetTopLevel(this) is { } topLevel && topLevel.Clipboard != null)
{
await topLevel.Clipboard.SetTextAsync(processList);
}
diff --git a/Controls/SwitchFloatingWindowThemeSettingsControl.cs b/Controls/SwitchFloatingWindowThemeSettingsControl.cs
new file mode 100644
index 00000000..e0ce4fbd
--- /dev/null
+++ b/Controls/SwitchFloatingWindowThemeSettingsControl.cs
@@ -0,0 +1,77 @@
+using Avalonia.Controls;
+using ClassIsland.Core.Abstractions.Controls;
+using SystemTools.Settings;
+
+namespace SystemTools.Controls;
+
+///
+/// 切换悬浮窗主题行动的设置控件
+///
+public class SwitchFloatingWindowThemeSettingsControl : ActionSettingsControlBase
+{
+ private ComboBox _themeComboBox;
+
+ public SwitchFloatingWindowThemeSettingsControl()
+ {
+ var panel = new StackPanel { Spacing = 10, Margin = new(10) };
+
+ panel.Children.Add(new TextBlock
+ {
+ Text = "目标主题:",
+ FontWeight = Avalonia.Media.FontWeight.Bold
+ });
+
+ _themeComboBox = new ComboBox
+ {
+ HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch
+ };
+ _themeComboBox.Items.Add(new ComboBoxItem { Content = "切换到下一个", Tag = -1 });
+ _themeComboBox.Items.Add(new ComboBoxItem { Content = "跟随系统", Tag = 0 });
+ _themeComboBox.Items.Add(new ComboBoxItem { Content = "浅色", Tag = 1 });
+ _themeComboBox.Items.Add(new ComboBoxItem { Content = "深色", Tag = 2 });
+ _themeComboBox.SelectedIndex = 0;
+
+ panel.Children.Add(_themeComboBox);
+
+ panel.Children.Add(new TextBlock
+ {
+ Text = "提示:选择\"切换到下一个\"会按 跟随系统→浅色→深色→跟随系统 循环切换,选择具体主题会直接设置到该主题。",
+ TextWrapping = Avalonia.Media.TextWrapping.Wrap,
+ Opacity = 0.7,
+ FontSize = 12
+ });
+
+ Content = panel;
+ }
+
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+
+ _themeComboBox.SelectionChanged += OnThemeSelectionChanged;
+
+ RestoreSettings();
+ }
+
+ private void RestoreSettings()
+ {
+ if (Settings == null) return;
+
+ var index = Settings.TargetTheme switch
+ {
+ 0 => 1,
+ 1 => 2,
+ 2 => 3,
+ _ => 0
+ };
+ _themeComboBox.SelectedIndex = index;
+ }
+
+ private void OnThemeSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (_themeComboBox.SelectedItem is ComboBoxItem item && item.Tag is int theme)
+ {
+ Settings.TargetTheme = theme;
+ }
+ }
+}
diff --git a/Controls/ToggleFloatingWindowLayerSettingsControl.cs b/Controls/ToggleFloatingWindowLayerSettingsControl.cs
new file mode 100644
index 00000000..c6692fdb
--- /dev/null
+++ b/Controls/ToggleFloatingWindowLayerSettingsControl.cs
@@ -0,0 +1,75 @@
+using Avalonia.Controls;
+using ClassIsland.Core.Abstractions.Controls;
+using SystemTools.Settings;
+
+namespace SystemTools.Controls;
+
+///
+/// 切换悬浮窗层级行动的设置控件
+///
+public class ToggleFloatingWindowLayerSettingsControl : ActionSettingsControlBase
+{
+ private ComboBox _layerComboBox;
+
+ public ToggleFloatingWindowLayerSettingsControl()
+ {
+ var panel = new StackPanel { Spacing = 10, Margin = new(10) };
+
+ panel.Children.Add(new TextBlock
+ {
+ Text = "目标层级:",
+ FontWeight = Avalonia.Media.FontWeight.Bold
+ });
+
+ _layerComboBox = new ComboBox
+ {
+ HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch
+ };
+ _layerComboBox.Items.Add(new ComboBoxItem { Content = "切换(置顶↔置底)", Tag = -1 });
+ _layerComboBox.Items.Add(new ComboBoxItem { Content = "置顶", Tag = 0 });
+ _layerComboBox.Items.Add(new ComboBoxItem { Content = "置底", Tag = 1 });
+ _layerComboBox.SelectedIndex = 0;
+
+ panel.Children.Add(_layerComboBox);
+
+ panel.Children.Add(new TextBlock
+ {
+ Text = "提示:选择\"切换\"会根据当前状态在置顶和置底之间切换,选择具体层级会直接设置到该层级。",
+ TextWrapping = Avalonia.Media.TextWrapping.Wrap,
+ Opacity = 0.7,
+ FontSize = 12
+ });
+
+ Content = panel;
+ }
+
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+
+ _layerComboBox.SelectionChanged += OnLayerSelectionChanged;
+
+ RestoreSettings();
+ }
+
+ private void RestoreSettings()
+ {
+ if (Settings == null) return;
+
+ var index = Settings.TargetLayer switch
+ {
+ 0 => 1,
+ 1 => 2,
+ _ => 0
+ };
+ _layerComboBox.SelectedIndex = index;
+ }
+
+ private void OnLayerSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (_layerComboBox.SelectedItem is ComboBoxItem item && item.Tag is int layer)
+ {
+ Settings.TargetLayer = layer;
+ }
+ }
+}
diff --git a/Controls/ToggleFloatingWindowProfileSettingsControl.cs b/Controls/ToggleFloatingWindowProfileSettingsControl.cs
new file mode 100644
index 00000000..29127e72
--- /dev/null
+++ b/Controls/ToggleFloatingWindowProfileSettingsControl.cs
@@ -0,0 +1,109 @@
+using Avalonia.Controls;
+using ClassIsland.Core.Abstractions.Controls;
+using SystemTools.Settings;
+using SystemTools.Services;
+using ClassIsland.Shared;
+
+namespace SystemTools.Controls;
+
+///
+/// 切换悬浮窗配置方案行动的设置控件
+///
+public class ToggleFloatingWindowProfileSettingsControl : ActionSettingsControlBase
+{
+ private ComboBox _profileComboBox;
+
+ public ToggleFloatingWindowProfileSettingsControl()
+ {
+ var panel = new StackPanel { Spacing = 10, Margin = new(10) };
+
+ panel.Children.Add(new TextBlock
+ {
+ Text = "目标配置方案:",
+ FontWeight = Avalonia.Media.FontWeight.Bold
+ });
+
+ _profileComboBox = new ComboBox
+ {
+ HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch
+ };
+
+ LoadProfiles();
+
+ panel.Children.Add(_profileComboBox);
+
+ panel.Children.Add(new TextBlock
+ {
+ Text = "提示:选择\"切换到下一个\"会按顺序循环切换方案,选择具体方案会直接切换到该方案。",
+ TextWrapping = Avalonia.Media.TextWrapping.Wrap,
+ Opacity = 0.7,
+ FontSize = 12
+ });
+
+ Content = panel;
+ }
+
+ private void LoadProfiles()
+ {
+ _profileComboBox.Items.Clear();
+ _profileComboBox.Items.Add(new ComboBoxItem { Content = "切换到下一个", Tag = null });
+
+ try
+ {
+ var profileManager = IAppHost.GetService().ProfileManager;
+ var profileNames = profileManager.GetProfileNames();
+
+ foreach (var name in profileNames)
+ {
+ _profileComboBox.Items.Add(new ComboBoxItem
+ {
+ Content = name,
+ Tag = name
+ });
+ }
+ }
+ catch
+ {
+ // 服务可能尚未初始化
+ }
+
+ _profileComboBox.SelectedIndex = 0;
+ }
+
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+
+ _profileComboBox.SelectionChanged += OnProfileSelectionChanged;
+
+ RestoreSettings();
+ }
+
+ private void RestoreSettings()
+ {
+ if (Settings == null) return;
+
+ var targetName = Settings.TargetProfileName;
+ if (!string.IsNullOrWhiteSpace(targetName))
+ {
+ for (int i = 1; i < _profileComboBox.Items.Count; i++)
+ {
+ if (_profileComboBox.Items[i] is ComboBoxItem item && item.Tag is string name && name == targetName)
+ {
+ _profileComboBox.SelectedIndex = i;
+ return;
+ }
+ }
+ }
+
+ _profileComboBox.SelectedIndex = 0;
+ }
+
+ private void OnProfileSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (_profileComboBox.SelectedItem is ComboBoxItem item)
+ {
+ Settings.TargetProfileName = item.Tag as string;
+ }
+ }
+}
diff --git a/DriveUtils.cs b/DriveUtils.cs
index bf36781d..02b46022 100644
--- a/DriveUtils.cs
+++ b/DriveUtils.cs
@@ -11,6 +11,10 @@ public static class DriveUtils
private static string GetDriveJsonPath()
{
var pluginDir = Path.GetDirectoryName(typeof(DriveUtils).Assembly.Location);
+ if (string.IsNullOrWhiteSpace(pluginDir))
+ {
+ pluginDir = AppContext.BaseDirectory;
+ }
return Path.Combine(pluginDir, "drive.json");
}
diff --git a/Plugin.cs b/Plugin.cs
index ab6d2fd4..65e9624b 100644
--- a/Plugin.cs
+++ b/Plugin.cs
@@ -46,7 +46,6 @@ public partial class Plugin : PluginBase
{
private ILogger? _logger;
private NativeMenuItem? _toggleFloatingWindowMenuItem;
- private int _toggleMenuRegisterRetryCount;
private bool _faceRecognitionRegistered = false;
private bool _ffmpegDisabledDueToMissingDependency;
private bool _faceRecognitionDisabledDueToMissingDependency;
@@ -68,6 +67,7 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s
services.AddLogging();
services.AddSingleton(GlobalConstants.MainConfig);
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -76,7 +76,7 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s
// ========== 注册可选人脸识别 ==========
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- if (GlobalConstants.MainConfig.Data.EnableFaceRecognition)
+ if (GlobalConstants.MainConfig?.Data.EnableFaceRecognition == true)
{
if (DependencyPaths.HasFaceRecognitionDependencies())
{
@@ -93,7 +93,7 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s
// ========== 注册设置页面 ==========
services.AddSettingsPage();
services.AddSettingsPage();
- if (GlobalConstants.MainConfig.Data.EnableFloatingWindowFeature)
+ if (GlobalConstants.MainConfig?.Data.EnableFloatingWindowFeature == true)
{
services.AddSettingsPage();
}
@@ -108,11 +108,14 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s
RegisterBaseRules(services);
RegisterBaseComponents(services);
- var experimentalEnabled = GlobalConstants.MainConfig.Data.EnableExperimentalFeatures;
- var ffmpegEnabled = GlobalConstants.MainConfig.Data.EnableFfmpegFeatures;
+ var experimentalEnabled = GlobalConstants.MainConfig?.Data.EnableExperimentalFeatures ?? false;
+ var ffmpegEnabled = GlobalConstants.MainConfig?.Data.EnableFfmpegFeatures ?? false;
AppBase.Current.AppStarted += (o, args) =>
{
+ // 迁移旧版悬浮窗配置到文件存储
+ IAppHost.GetService().MigrateFromLegacyConfig(GlobalConstants.MainConfig!.Data);
+
if (GlobalConstants.MainConfig?.Data.EnableFloatingWindowFeature == true)
{
IAppHost.GetService().Start();
@@ -129,7 +132,7 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s
_logger?.LogWarning("[SystemTools]FFmpeg 功能已自动关闭:缺少依赖文件 ffmpeg.exe。");
}
- if (GlobalConstants.MainConfig.Data.EnableFaceRecognition)
+ if (GlobalConstants.MainConfig?.Data.EnableFaceRecognition == true)
{
if (_faceRecognitionRegistered)
{
@@ -286,6 +289,12 @@ private void RegisterBaseActions(IServiceCollection services)
{
RegisterActionIfEnabled(services, config,
"SystemTools.ShowFloatingWindow");
+ RegisterActionIfEnabled(services, config,
+ "SystemTools.ToggleFloatingWindowLayer");
+ RegisterActionIfEnabled(services, config,
+ "SystemTools.ToggleFloatingWindowProfile");
+ RegisterActionIfEnabled(services, config,
+ "SystemTools.SwitchFloatingWindowTheme");
}
// 其他工具
@@ -525,7 +534,9 @@ private void BuildBaseActionTree()
}
// 悬浮窗设置
- if (config.EnableFloatingWindowFeature && config.IsActionEnabled("SystemTools.ShowFloatingWindow"))
+ if (config.EnableFloatingWindowFeature && HasAnyActionEnabled(config, "SystemTools.ShowFloatingWindow",
+ "SystemTools.ToggleFloatingWindowLayer", "SystemTools.ToggleFloatingWindowProfile",
+ "SystemTools.SwitchFloatingWindowTheme"))
{
IActionService.ActionMenuTree["SystemTools 行动"].Add(new ActionMenuTreeGroup("悬浮窗设置…", "\uEA37"));
BuildFloatingWindowMenu(config);
@@ -720,6 +731,12 @@ private void BuildFloatingWindowMenu(MainConfigData config)
if (config.EnableFloatingWindowFeature && config.IsActionEnabled("SystemTools.ShowFloatingWindow"))
items.Add(new ActionMenuTreeItem("SystemTools.ShowFloatingWindow", "显示悬浮窗", "\uEA37"));
+ if (config.EnableFloatingWindowFeature && config.IsActionEnabled("SystemTools.ToggleFloatingWindowLayer"))
+ items.Add(new ActionMenuTreeItem("SystemTools.ToggleFloatingWindowLayer", "切换悬浮窗层级", "\uE9A8"));
+ if (config.EnableFloatingWindowFeature && config.IsActionEnabled("SystemTools.ToggleFloatingWindowProfile"))
+ items.Add(new ActionMenuTreeItem("SystemTools.ToggleFloatingWindowProfile", "切换悬浮窗配置方案", "\uE9A8"));
+ if (config.EnableFloatingWindowFeature && config.IsActionEnabled("SystemTools.SwitchFloatingWindowTheme"))
+ items.Add(new ActionMenuTreeItem("SystemTools.SwitchFloatingWindowTheme", "切换悬浮窗主题", "\uE790"));
if (items.Count > 0)
{
@@ -753,7 +770,7 @@ private void BuildClassIslandMenu(MainConfigData config)
if (config.IsActionEnabled("SystemTools.OpenClassSwapWindow"))
items.Add(new ActionMenuTreeItem("SystemTools.OpenClassSwapWindow", "打开换课窗口", "\uE13B"));
if (config.IsActionEnabled("SystemTools.ToggleWorkflow"))
- items.Add(new ActionMenuTreeItem("SystemTools.ToggleWorkflow", "开关自动化", "\uE9A8"));
+ items.Add(new ActionMenuTreeItem("SystemTools.ToggleWorkflow", "开关自动化", "\uE8B8"));
if (items.Count > 0)
{
@@ -829,13 +846,18 @@ private void RegisterOrUpdateFloatingWindowTrayMenu()
return;
}
- data.ShowFloatingWindow = !data.ShowFloatingWindow;
+ var config = GlobalConstants.MainConfig?.Data;
+ if (config != null)
+ {
+ config.ShowFloatingWindow = !config.ShowFloatingWindow;
+ GlobalConstants.MainConfig?.Save();
+ }
IAppHost.GetService().UpdateWindowState();
UpdateFloatingWindowTrayMenuHeader();
- GlobalConstants.MainConfig?.Save();
};
- config.PropertyChanged += OnMainConfigDataPropertyChanged;
+ // 监听主配置变化以更新托盘菜单
+ config.PropertyChanged += OnMainConfigDataPropertyChanged;
}
if (!config.EnableFloatingWindowFeature)
@@ -874,7 +896,7 @@ private void UnregisterFloatingWindowTrayMenu()
private void OnMainConfigDataPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName is not (nameof(MainConfigData.ShowFloatingWindow) or nameof(MainConfigData.EnableFloatingWindowFeature)))
+ if (e.PropertyName is not nameof(MainConfigData.EnableFloatingWindowFeature))
{
return;
}
@@ -889,7 +911,8 @@ private void UpdateFloatingWindowTrayMenuHeader()
return;
}
- _toggleFloatingWindowMenuItem.Header = GlobalConstants.MainConfig?.Data.ShowFloatingWindow == true
+ var config = GlobalConstants.MainConfig?.Data;
+ _toggleFloatingWindowMenuItem.Header = config is { ShowFloatingWindow: true }
? "隐藏悬浮窗"
: "显示悬浮窗";
}
diff --git a/Services/FloatingWindowService.cs b/Services/FloatingWindowService.cs
index 516a46b6..865e3f03 100644
--- a/Services/FloatingWindowService.cs
+++ b/Services/FloatingWindowService.cs
@@ -1,4 +1,4 @@
-using Avalonia;
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@@ -7,7 +7,9 @@
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.VisualTree;
+using ClassIsland.Core.Abstractions.Services;
using ClassIsland.Core.Controls;
+using ClassIsland.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -37,6 +39,7 @@ public class FloatingWindowService
private static readonly TimeSpan TouchLikeMouseGracePeriod = TimeSpan.FromMilliseconds(250);
private readonly MainConfigHandler _configHandler;
+ private readonly FloatingWindowProfileManager _profileManager;
private readonly Dictionary _entries = new();
private Window? _window;
private StackPanel? _stackPanel;
@@ -50,6 +53,7 @@ public class FloatingWindowService
private readonly Dictionary _buttonWidthCache = new();
private bool _allowWindowClose;
private bool _restoringFromMinimized;
+ private bool _isStopped;
private bool _isTouchDeviceDetected;
private bool _touchDragAllowed;
private PixelPoint _touchDragStartScreenPoint;
@@ -87,21 +91,26 @@ private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPt
public event EventHandler? EntriesChanged;
- public FloatingWindowService(MainConfigHandler configHandler)
+ public FloatingWindowService(MainConfigHandler configHandler, FloatingWindowProfileManager profileManager)
{
_configHandler = configHandler;
+ _profileManager = profileManager;
}
public IReadOnlyList Entries => _entries.Values.ToList();
+ public FloatingWindowProfileManager ProfileManager => _profileManager;
+
public void Start()
{
Dispatcher.UIThread.Post(() =>
{
+ _profileManager.LoadProfile(_configHandler.Data.CurrentFloatingWindowProfile);
EnsureWindow();
EnsureLayerRecheckHooks();
EnsureGlobalInputHooks();
SubscribeThemeChanged();
+ SubscribeRulesetStatusChanged();
ApplyVisibility();
RefreshLayerRecheckMode();
RecheckWindowLayer();
@@ -111,6 +120,7 @@ public void Start()
public void Stop()
{
+ _isStopped = true;
Dispatcher.UIThread.Post(() =>
{
if (_window != null)
@@ -125,6 +135,7 @@ public void Stop()
RemoveLayerRecheckHooks();
RemoveGlobalInputHooks();
UnsubscribeThemeChanged();
+ UnsubscribeRulesetStatusChanged();
});
}
@@ -187,8 +198,10 @@ public void UnregisterTrigger(FloatingWindowTrigger trigger)
public void UpdateWindowState()
{
+ if (_isStopped) return;
Dispatcher.UIThread.Post(() =>
{
+ if (_isStopped) return;
ApplyVisibility();
RefreshLayerRecheckMode();
RecheckWindowLayer();
@@ -199,8 +212,10 @@ public void UpdateWindowState()
private void NotifyEntriesChanged()
{
EntriesChanged?.Invoke(this, EventArgs.Empty);
+ if (_isStopped) return;
Dispatcher.UIThread.Post(() =>
{
+ if (_isStopped) return;
ApplyVisibility();
RecheckWindowLayer();
RefreshWindowButtons();
@@ -253,9 +268,35 @@ private bool IsLightTheme()
return ResolveWindowThemeVariant() == ThemeVariant.Light;
}
+ ///
+ /// 设置悬浮窗主题
+ ///
+ /// 0=跟随系统, 1=浅色, 2=深色
+ public void SetWindowTheme(int theme)
+ {
+ var normalized = theme is 1 or 2 ? theme : 0;
+ if (_configHandler.Data.FloatingWindowTheme == normalized)
+ {
+ return;
+ }
+
+ _configHandler.Data.FloatingWindowTheme = normalized;
+ _configHandler.Save();
+ Dispatcher.UIThread.Post(RefreshWindowButtons);
+ }
+
+ ///
+ /// 切换到下一个悬浮窗主题
+ ///
+ public void ToggleWindowTheme()
+ {
+ var next = (_configHandler.Data.FloatingWindowTheme + 1) % 3;
+ SetWindowTheme(next);
+ }
+
private void EnsureWindow()
{
- if (_window != null)
+ if (_window != null || _isStopped)
{
return;
}
@@ -264,10 +305,10 @@ private void EnsureWindow()
_stackPanel = new StackPanel { Margin = new Thickness(6), Spacing = 6 };
_window = new Window
{
- Width = 1,
- Height = 1,
+ Width = 64,
+ Height = 64,
ShowActivated = false,
- Topmost = _configHandler.Data.FloatingWindowLayer == 1,
+ Topmost = _profileManager.CurrentProfile.FloatingWindowLayer == 1,
SystemDecorations = SystemDecorations.None,
Background = Brushes.Transparent,
CanResize = false,
@@ -275,7 +316,7 @@ private void EnsureWindow()
SizeToContent = SizeToContent.WidthAndHeight,
Content = _windowContainer = new Border
{
- Background = new SolidColorBrush(Color.Parse("#CC1F1F1F")),
+ Background = TryParseColor("#CC1F1F1F") ?? new SolidColorBrush(Color.FromArgb(0xCC, 0x1F, 0x1F, 0x1F)),
CornerRadius = new CornerRadius(8),
Child = _stackPanel
}
@@ -290,10 +331,7 @@ private void EnsureWindow()
if (!_allowWindowClose)
{
e.Cancel = true;
- if (_window is { IsVisible: false })
- {
- _window.Show();
- }
+ // 不在 Closing 事件中调用 Show(),窗口可能处于关闭过程中
}
};
_window.PropertyChanged += OnWindowPropertyChanged;
@@ -316,7 +354,7 @@ private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEven
private void RestoreWindowFromMinimized()
{
- if (_window == null || _restoringFromMinimized)
+ if (_window == null || _restoringFromMinimized || _isStopped)
{
return;
}
@@ -327,17 +365,26 @@ private void RestoreWindowFromMinimized()
{
try
{
- if (_window == null)
+ if (_window == null || _isStopped)
{
return;
}
if (!_window.IsVisible)
{
- _window.Show();
+ try { _window.Show(); }
+ catch (InvalidOperationException)
+ {
+ _window = null;
+ _stackPanel = null;
+ _windowContainer = null;
+ }
}
- _window.WindowState = WindowState.Normal;
+ if (_window != null)
+ {
+ _window.WindowState = WindowState.Normal;
+ }
}
finally
{
@@ -353,24 +400,223 @@ private void OnWindowLoaded(object? sender, RoutedEventArgs e)
RecheckWindowLayer();
}
+ private bool _rulesetHidingWindow = false;
+ private readonly HashSet _rulesetHiddenButtons = new();
+ private readonly HashSet _rulesetHiddenRows = new();
+
+ private void SubscribeRulesetStatusChanged()
+ {
+ var rulesetService = IAppHost.TryGetService();
+ if (rulesetService == null)
+ {
+ return;
+ }
+
+ rulesetService.StatusUpdated -= OnRulesetStatusUpdated;
+ rulesetService.StatusUpdated += OnRulesetStatusUpdated;
+ }
+
+ private void UnsubscribeRulesetStatusChanged()
+ {
+ var rulesetService = IAppHost.TryGetService();
+ if (rulesetService == null)
+ {
+ return;
+ }
+
+ rulesetService.StatusUpdated -= OnRulesetStatusUpdated;
+ }
+
+ private void OnRulesetStatusUpdated(object? sender, EventArgs e)
+ {
+ CheckFloatingWindowRuleset();
+ CheckButtonRulesets();
+ CheckRowRulesets();
+ }
+
+ private void CheckFloatingWindowRuleset()
+ {
+ var profile = _profileManager.CurrentProfile;
+ if (!_configHandler.Data.FloatingWindowRulesetEnabled)
+ {
+ if (_rulesetHidingWindow)
+ {
+ _rulesetHidingWindow = false;
+ ApplyVisibility();
+ }
+ return;
+ }
+
+ var rulesetService = IAppHost.TryGetService();
+ if (rulesetService == null)
+ {
+ return;
+ }
+
+ var isSatisfied = rulesetService.IsRulesetSatisfied(_configHandler.Data.FloatingWindowRuleset);
+ var shouldHide = isSatisfied;
+
+ if (shouldHide != _rulesetHidingWindow)
+ {
+ _rulesetHidingWindow = shouldHide;
+ ApplyVisibility();
+ }
+ }
+
+ private void CheckButtonRulesets()
+ {
+ var profile = _profileManager.CurrentProfile;
+ var rulesetService = IAppHost.TryGetService();
+ if (rulesetService == null)
+ {
+ return;
+ }
+
+ var changed = false;
+ foreach (var entry in _entries.Values)
+ {
+ if (!profile.FloatingWindowButtonRulesets.TryGetValue(entry.ButtonId, out var config))
+ {
+ continue;
+ }
+
+ var shouldHide = false;
+ if (!config.IsVisible)
+ {
+ shouldHide = true;
+ }
+ else if (config.HideOnRule)
+ {
+ shouldHide = rulesetService.IsRulesetSatisfied(config.HidingRules);
+ }
+
+ var wasHidden = _rulesetHiddenButtons.Contains(entry.ButtonId);
+ if (shouldHide != wasHidden)
+ {
+ if (shouldHide)
+ {
+ _rulesetHiddenButtons.Add(entry.ButtonId);
+ }
+ else
+ {
+ _rulesetHiddenButtons.Remove(entry.ButtonId);
+ }
+ changed = true;
+ }
+ }
+
+ if (changed)
+ {
+ Dispatcher.UIThread.Post(RefreshWindowButtons);
+ }
+ }
+
+ private void CheckRowRulesets()
+ {
+ var profile = _profileManager.CurrentProfile;
+ var rowConfigs = profile.FloatingWindowRowRulesets;
+ if (rowConfigs == null || rowConfigs.Count == 0)
+ {
+ if (_rulesetHiddenRows.Count > 0)
+ {
+ _rulesetHiddenRows.Clear();
+ Dispatcher.UIThread.Post(RefreshWindowButtons);
+ }
+ return;
+ }
+
+ var rulesetService = IAppHost.TryGetService();
+ if (rulesetService == null)
+ {
+ return;
+ }
+
+ var changed = false;
+ for (int i = 0; i < rowConfigs.Count; i++)
+ {
+ var config = rowConfigs[i];
+ var shouldHide = false;
+ if (!config.IsVisible)
+ {
+ shouldHide = true;
+ }
+ else if (config.HideOnRule)
+ {
+ shouldHide = rulesetService.IsRulesetSatisfied(config.HidingRules);
+ }
+
+ var wasHidden = _rulesetHiddenRows.Contains(i);
+ if (shouldHide != wasHidden)
+ {
+ if (shouldHide)
+ {
+ _rulesetHiddenRows.Add(i);
+ }
+ else
+ {
+ _rulesetHiddenRows.Remove(i);
+ }
+ changed = true;
+ }
+ }
+
+ if (changed)
+ {
+ Dispatcher.UIThread.Post(RefreshWindowButtons);
+ }
+ }
+
private void ApplyVisibility()
{
+ if (_isStopped) return;
EnsureWindow();
if (_window == null)
{
return;
}
- if (_configHandler.Data.ShowFloatingWindow && _entries.Count > 0)
+ var profile = _profileManager.CurrentProfile;
+ var shouldShow = _configHandler.Data.ShowFloatingWindow && _entries.Count > 0 && !_rulesetHidingWindow;
+
+ if (shouldShow)
{
if (!_window.IsVisible)
{
- _window.Show();
+ try
+ {
+ _window.Show();
+ }
+ catch (InvalidOperationException)
+ {
+ // 窗口已关闭(被外部关闭或竞态条件),需要重建
+ _window = null;
+ _stackPanel = null;
+ _windowContainer = null;
+ if (_isStopped) return;
+ EnsureWindow();
+ if (_window != null)
+ {
+ try { _window.Show(); }
+ catch (InvalidOperationException) { /* 放弃重建 */ }
+ }
+ }
}
}
else
{
- _window.Hide();
+ if (_window != null && _window.IsVisible)
+ {
+ try
+ {
+ _window.Hide();
+ }
+ catch (InvalidOperationException)
+ {
+ _window = null;
+ _stackPanel = null;
+ _windowContainer = null;
+ }
+ }
}
}
@@ -381,10 +627,11 @@ private void RefreshWindowButtons()
return;
}
- var scale = Math.Clamp(_configHandler.Data.FloatingWindowScale, 0.5, 2.0);
- var iconSize = Math.Clamp(_configHandler.Data.FloatingWindowIconSize, 15, 50) * scale;
- var textSize = Math.Clamp(_configHandler.Data.FloatingWindowTextSize, 8, 30) * scale;
- var opacity = Math.Clamp(_configHandler.Data.FloatingWindowOpacity, 10, 100);
+ var profile = _profileManager.CurrentProfile;
+ var scale = Math.Clamp(profile.FloatingWindowScale, 0.5, 2.0);
+ var iconSize = Math.Clamp(profile.FloatingWindowIconSize, 15, 50) * scale;
+ var textSize = Math.Clamp(profile.FloatingWindowTextSize, 8, 30) * scale;
+ var opacity = Math.Clamp(profile.FloatingWindowOpacity, 10, 100);
var alpha = (byte)Math.Round(255 * (opacity / 100.0));
var isLightTheme = IsLightTheme();
var windowBackground = isLightTheme
@@ -395,7 +642,7 @@ private void RefreshWindowButtons()
if (_windowContainer != null)
{
_windowContainer.Background = windowBackground;
- _windowContainer.BoxShadow = _configHandler.Data.FloatingWindowShadowEnabled
+ _windowContainer.BoxShadow = profile.FloatingWindowShadowEnabled
? new BoxShadows(new BoxShadow
{
OffsetX = 0,
@@ -424,8 +671,15 @@ private void RefreshWindowButtons()
_touchDragHandle = null;
}
+ int rowIndex = 0;
foreach (var rowEntries in GetOrderedRows())
{
+ if (_rulesetHiddenRows.Contains(rowIndex))
+ {
+ rowIndex++;
+ continue;
+ }
+
var rowPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
@@ -493,11 +747,11 @@ private void RefreshWindowButtons()
}
else
{
- // 保持自动布局,允许文本变更/缩放后重新测量自然宽度。
button.Width = double.NaN;
}
- button.LayoutUpdated += (_, _) =>
+ EventHandler? layoutUpdatedHandler = null;
+ layoutUpdatedHandler = (_, _) =>
{
if (entry.IsRevertStyleActive)
{
@@ -508,8 +762,10 @@ private void RefreshWindowButtons()
if (width > 0)
{
_buttonWidthCache[entry.ButtonId] = width;
+ button.LayoutUpdated -= layoutUpdatedHandler;
}
};
+ button.LayoutUpdated += layoutUpdatedHandler;
button.PointerPressed += (_, e) =>
{
@@ -529,64 +785,46 @@ private void RefreshWindowButtons()
rowPanel.Children.Add(button);
}
- if (rowPanel.Children.Count > 0)
- {
- _stackPanel.Children.Add(rowPanel);
- }
+ _stackPanel.Children.Add(rowPanel);
+
+ rowIndex++;
}
}
private List> GetOrderedRows()
{
- EnsureUniqueButtonIds();
+ var profile = _profileManager.CurrentProfile;
+ var validButtonIds = _entries.Values.Select(x => x.ButtonId).ToHashSet();
+
+ // 清理不存在的按钮ID
+ if (profile.PruneInvalidButtonIds(validButtonIds))
+ {
+ _profileManager.SaveProfile();
+ }
+
var values = _entries.Values
+ .Where(x => !_rulesetHiddenButtons.Contains(x.ButtonId))
.GroupBy(x => x.ButtonId)
- .ToDictionary(x => x.Key, x => x.First());
- var order = _configHandler.Data.FloatingWindowButtonOrder ?? [];
-
- var orderedIds = values.Keys
- .OrderBy(id =>
- {
- var index = order.IndexOf(id);
- return index < 0 ? int.MaxValue : index;
- })
- .ThenBy(id => id)
- .ToList();
+ .ToDictionary(g => g.Key, g => g.First());
- var used = new HashSet();
var rows = new List>();
- foreach (var row in _configHandler.Data.FloatingWindowButtonRows ?? [])
+ foreach (var row in profile.FloatingWindowButtonRows ?? [])
{
- var items = row
- .Where(id => values.ContainsKey(id) && used.Add(id))
- .Select(id => values[id])
- .ToList();
+ var items = new List();
+ foreach (var id in row)
+ {
+ if (values.TryGetValue(id, out var entry))
+ {
+ items.Add(entry);
+ }
+ }
if (items.Count > 0)
{
rows.Add(items);
}
}
- var missing = orderedIds
- .Where(id => !used.Contains(id))
- .Select(id => values[id])
- .ToList();
-
- if (rows.Count == 0)
- {
- rows.Add(missing);
- }
- else
- {
- rows[0].AddRange(missing);
- }
-
- if (rows.Count == 0)
- {
- rows.Add([]);
- }
-
return rows;
}
@@ -864,7 +1102,6 @@ private IntPtr OnLowLevelMouse(int nCode, IntPtr wParam, IntPtr lParam)
}
else if (message == WmLButtonDown || message == WmRButtonDown)
{
- // 仅在明确的鼠标点击操作时切回鼠标模式,避免触屏后被背景鼠标移动事件自动恢复。
SetTouchInputMode(false);
}
@@ -1022,7 +1259,8 @@ private void RemoveLayerRecheckHooks()
private void RefreshLayerRecheckMode()
{
- var mode = _configHandler.Data.FloatingWindowLayerRecheckMode;
+ var profile = _profileManager.CurrentProfile;
+ var mode = profile.FloatingWindowLayerRecheckMode;
var useReorderHook = mode == 0;
var useForegroundHook = mode == 1;
@@ -1106,7 +1344,7 @@ private void RemoveReorderHook()
private void OnLayerRecheck50MsTimerTick(object? sender, EventArgs e)
{
- if (_configHandler.Data.FloatingWindowLayerRecheckMode == 2)
+ if (_profileManager.CurrentProfile.FloatingWindowLayerRecheckMode == 2)
{
RecheckWindowLayer();
}
@@ -1114,7 +1352,7 @@ private void OnLayerRecheck50MsTimerTick(object? sender, EventArgs e)
private void OnLayerRecheck1MsTimerTick(object? sender, EventArgs e)
{
- if (_configHandler.Data.FloatingWindowLayerRecheckMode == 3)
+ if (_profileManager.CurrentProfile.FloatingWindowLayerRecheckMode == 3)
{
RecheckWindowLayer();
}
@@ -1128,7 +1366,7 @@ private void OnWinEvent(IntPtr hWinEventHook, uint @event, IntPtr hwnd, int idOb
return;
}
- var mode = _configHandler.Data.FloatingWindowLayerRecheckMode;
+ var mode = _profileManager.CurrentProfile.FloatingWindowLayerRecheckMode;
var shouldRecheck = (@event == EventObjectReorder && mode == 0) ||
(@event == EventSystemForeground && mode == 1);
if (!shouldRecheck)
@@ -1161,7 +1399,7 @@ private void RecheckWindowLayer()
SET_WINDOW_POS_FLAGS.SWP_NOSENDCHANGING;
var hwnd = new HWND(handle);
- if (_configHandler.Data.FloatingWindowLayer == 0)
+ if (_profileManager.CurrentProfile.FloatingWindowLayer == 0)
{
_window.Topmost = false;
PInvoke.SetWindowPos(hwnd, HwndBottom, 0, 0, 0, 0, flags);
@@ -1172,9 +1410,108 @@ private void RecheckWindowLayer()
PInvoke.SetWindowPos(hwnd, HwndTopmost, 0, 0, 0, 0, flags);
}
+ public void ToggleWindowLayer()
+ {
+ var profile = _profileManager.CurrentProfile;
+ profile.FloatingWindowLayer = profile.FloatingWindowLayer == 1 ? 0 : 1;
+ _profileManager.SaveProfile();
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (_window != null)
+ {
+ _window.Topmost = profile.FloatingWindowLayer == 1;
+ }
+ RecheckWindowLayer();
+ RefreshLayerRecheckMode();
+ });
+ }
+
+ public void SetWindowLayer(int layer)
+ {
+ var profile = _profileManager.CurrentProfile;
+ profile.FloatingWindowLayer = layer == 1 ? 1 : 0;
+ _profileManager.SaveProfile();
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (_window != null)
+ {
+ _window.Topmost = profile.FloatingWindowLayer == 1;
+ }
+ RecheckWindowLayer();
+ RefreshLayerRecheckMode();
+ });
+ }
+
+ public void ToggleWindowProfile()
+ {
+ var names = _profileManager.GetProfileNames();
+ if (names.Count <= 1)
+ {
+ return;
+ }
+
+ var currentName = _profileManager.CurrentProfileName;
+ var currentIndex = -1;
+ for (int i = 0; i < names.Count; i++)
+ {
+ if (string.Equals(names[i], currentName, StringComparison.OrdinalIgnoreCase))
+ {
+ currentIndex = i;
+ break;
+ }
+ }
+ if (currentIndex < 0)
+ {
+ currentIndex = 0;
+ }
+
+ var newIndex = (currentIndex + 1) % names.Count;
+ var newName = names[newIndex];
+ SwitchToProfile(newName);
+ }
+
+ public void SwitchToProfile(string profileName)
+ {
+ if (string.IsNullOrWhiteSpace(profileName))
+ {
+ return;
+ }
+
+ var names = _profileManager.GetProfileNames();
+ if (!names.Contains(profileName))
+ {
+ return;
+ }
+
+ _profileManager.SaveProfile();
+ _profileManager.LoadProfile(profileName);
+ _configHandler.Data.CurrentFloatingWindowProfile = profileName;
+ _configHandler.Save();
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ RefreshWindowButtons();
+ ApplyVisibility();
+ RecheckWindowLayer();
+ RefreshLayerRecheckMode();
+ });
+ }
+
+ private static IBrush? TryParseColor(string colorString)
+ {
+ try
+ {
+ return new SolidColorBrush(Color.Parse(colorString));
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
public static string ConvertIcon(string raw)
{
- if (string.IsNullOrWhiteSpace(raw)) return "?";
+ if (string.IsNullOrWhiteSpace(raw)) return "\uEA37";
var v = raw.Trim();
if (v.StartsWith("/u", StringComparison.OrdinalIgnoreCase) || v.StartsWith("\\u", StringComparison.OrdinalIgnoreCase))
{
diff --git a/Settings/FloatingWindowTriggerSettings.cs b/Settings/FloatingWindowTriggerSettings.cs
index 55e2c3dc..6976a4d3 100644
--- a/Settings/FloatingWindowTriggerSettings.cs
+++ b/Settings/FloatingWindowTriggerSettings.cs
@@ -109,7 +109,7 @@ private Control BuildIconPickerContent(ObservableCollection rows)
VerticalAlignment = VerticalAlignment.Stretch
};
- listBox.ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel());
+ listBox.ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel());
listBox.ItemTemplate = new FuncDataTemplate((row, _) => BuildIconRow(row));
listBox.Height = 520;
diff --git a/Settings/SwitchFloatingWindowThemeSettings.cs b/Settings/SwitchFloatingWindowThemeSettings.cs
new file mode 100644
index 00000000..cf2c251d
--- /dev/null
+++ b/Settings/SwitchFloatingWindowThemeSettings.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace SystemTools.Settings;
+
+///
+/// 切换悬浮窗主题行动的设置
+///
+public class SwitchFloatingWindowThemeSettings
+{
+ ///
+ /// 目标主题。0=跟随系统, 1=浅色, 2=深色。
+ ///
+ [JsonPropertyName("targetTheme")]
+ public int TargetTheme { get; set; } = -1;
+}
diff --git a/Settings/ToggleFloatingWindowLayerSettings.cs b/Settings/ToggleFloatingWindowLayerSettings.cs
new file mode 100644
index 00000000..2dc3579b
--- /dev/null
+++ b/Settings/ToggleFloatingWindowLayerSettings.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace SystemTools.Settings;
+
+///
+/// 切换悬浮窗层级行动的设置
+///
+public class ToggleFloatingWindowLayerSettings
+{
+ ///
+ /// 目标层级。-1 表示切换,0 表示置顶,1 表示置底。
+ ///
+ [JsonPropertyName("targetLayer")]
+ public int TargetLayer { get; set; } = -1;
+}
diff --git a/Settings/ToggleFloatingWindowProfileSettings.cs b/Settings/ToggleFloatingWindowProfileSettings.cs
new file mode 100644
index 00000000..0c20a256
--- /dev/null
+++ b/Settings/ToggleFloatingWindowProfileSettings.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace SystemTools.Settings;
+
+///
+/// 切换悬浮窗配置方案行动的设置
+///
+public class ToggleFloatingWindowProfileSettings
+{
+ ///
+ /// 目标配置方案名称。null 表示切换到下一个,其他值表示指定方案名称。
+ ///
+ [JsonPropertyName("targetProfileName")]
+ public string? TargetProfileName { get; set; }
+}
diff --git a/SettingsPage/AboutSettingsPage.axaml.cs b/SettingsPage/AboutSettingsPage.axaml.cs
index b77c3b85..00ebec4a 100644
--- a/SettingsPage/AboutSettingsPage.axaml.cs
+++ b/SettingsPage/AboutSettingsPage.axaml.cs
@@ -41,7 +41,10 @@ private void UriNavigationCommands_OnClick(object sender, RoutedEventArgs e)
Button s => s.CommandParameter?.ToString(),
_ => "classisland://app/test/"
};
- IAppHost.TryGetService()?.NavigateWrapped(new Uri(url));
+ if (!string.IsNullOrWhiteSpace(url))
+ {
+ IAppHost.TryGetService()?.NavigateWrapped(new Uri(url));
+ }
}
private void CheckAutoSwitchTab()
diff --git a/SettingsPage/FloatingWindowEditorSettingsPage.axaml b/SettingsPage/FloatingWindowEditorSettingsPage.axaml
index d6a42817..075e6f0c 100644
--- a/SettingsPage/FloatingWindowEditorSettingsPage.axaml
+++ b/SettingsPage/FloatingWindowEditorSettingsPage.axaml
@@ -6,9 +6,15 @@
xmlns:ci="http://classisland.tech/schemas/xaml/core"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:controls1="clr-namespace:SystemTools.Controls"
+ xmlns:ruleset="clr-namespace:ClassIsland.Core.Controls.Ruleset;assembly=ClassIsland.Core"
+ xmlns:local="clr-namespace:SystemTools"
mc:Ignorable="d"
- d:DesignHeight="450"
+ d:DesignHeight="800"
d:DesignWidth="800">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+ Header="悬浮窗按钮布局"
+ Description="您可在下方通过拖拽调整行内按钮顺序,或添加行以实现纵列排布等功能"
+ IsExpanded="True">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ IsExpanded="False">
+ Value="{Binding ViewModel.CurrentFloatingWindowProfile.FloatingWindowScale, Mode=TwoWay}" />
-
@@ -68,16 +231,16 @@
Maximum="50"
TickFrequency="1"
IsSnapToTickEnabled="True"
- Value="{Binding ViewModel.Settings.FloatingWindowIconSize, Mode=TwoWay}" />
+ Value="{Binding ViewModel.CurrentFloatingWindowProfile.FloatingWindowIconSize, Mode=TwoWay}" />
-
@@ -87,16 +250,16 @@
Maximum="25"
TickFrequency="1"
IsSnapToTickEnabled="True"
- Value="{Binding ViewModel.Settings.FloatingWindowTextSize, Mode=TwoWay}" />
+ Value="{Binding ViewModel.CurrentFloatingWindowProfile.FloatingWindowTextSize, Mode=TwoWay}" />
-
@@ -106,8 +269,8 @@
Maximum="100"
TickFrequency="1"
IsSnapToTickEnabled="True"
- Value="{Binding ViewModel.Settings.FloatingWindowOpacity, Mode=TwoWay}" />
-
+
@@ -136,18 +299,26 @@
Content="阴影效果"
Description="悬浮窗阴影效果">
-
+
-
+
+
+
+
+
+
+
+ IsExpanded="False">
-
+
@@ -161,7 +332,7 @@
Description="在什么时候重新设置悬浮窗层级。请注意,较高的频率可能会产生较高的性能占用并导致悬浮窗闪烁。">
+ SelectedIndex="{Binding ViewModel.CurrentFloatingWindowProfile.FloatingWindowLayerRecheckMode, Mode=TwoWay}">
窗口层级变化时
前台窗口变化时
@@ -191,91 +362,25 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/SettingsPage/FloatingWindowEditorSettingsPage.axaml.cs b/SettingsPage/FloatingWindowEditorSettingsPage.axaml.cs
index 82ce4366..c4ea4ca8 100644
--- a/SettingsPage/FloatingWindowEditorSettingsPage.axaml.cs
+++ b/SettingsPage/FloatingWindowEditorSettingsPage.axaml.cs
@@ -1,4 +1,5 @@
-using System;
+using System;
+using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
@@ -11,6 +12,7 @@
using ClassIsland.Core.Abstractions;
using ClassIsland.Core.Abstractions.Controls;
using ClassIsland.Core.Attributes;
+using ClassIsland.Core.Controls.Ruleset;
using ClassIsland.Shared;
using SystemTools.ConfigHandlers;
using SystemTools.Services;
@@ -36,13 +38,34 @@ public FloatingWindowEditorSettingsPage()
DataContext = this;
InitializeComponent();
+ ViewModel.RefreshFloatingWindowProfiles();
ViewModel.RefreshFloatingTriggers();
+ ViewModel.CurrentFloatingWindowProfile.PropertyChanged += OnProfilePropertyChanged;
ViewModel.Settings.PropertyChanged += OnSettingsPropertyChanged;
+ ViewModel.ProfileChanged += OnViewModelProfileChanged;
+
+ // 注册全局设置变更监听(ShowFloatingWindow 和规则集不随方案切换)
+ RegisterHidingRulesEvents();
}
public SystemToolsSettingsViewModel ViewModel { get; }
+
private bool _isDisposed;
+ private Point? _floatingDragStartPoint;
+ private Border? _floatingDragSourceBorder;
+
+ // ===== 规则集 Drawer 状态 =====
+ private enum RulesetTargetType { Button, Row, Window }
+ private RulesetTargetType _currentRulesetTarget;
+ private FloatingTriggerItem? _currentButtonTarget;
+ private FloatingTriggerRow? _currentRowTarget;
+
+ // Drawer 内的控件引用
+ private ToggleSwitch? _drawerIsVisibleToggle;
+ private ToggleSwitch? _drawerHideOnRuleToggle;
+ private RulesetControl? _drawerRulesetControl;
+
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
@@ -52,40 +75,150 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e
return;
}
+ ViewModel.CurrentFloatingWindowProfile.PropertyChanged -= OnProfilePropertyChanged;
ViewModel.Settings.PropertyChanged -= OnSettingsPropertyChanged;
+ ViewModel.ProfileChanged -= OnViewModelProfileChanged;
+
+ UnregisterHidingRulesEvents();
+
ViewModel.Dispose();
_isDisposed = true;
}
+ private void OnProfilePropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName is nameof(FloatingWindowProfile.FloatingWindowScale)
+ or nameof(FloatingWindowProfile.FloatingWindowIconSize)
+ or nameof(FloatingWindowProfile.FloatingWindowTextSize)
+ or nameof(FloatingWindowProfile.FloatingWindowOpacity)
+ or nameof(FloatingWindowProfile.FloatingWindowShadowEnabled)
+ or nameof(FloatingWindowProfile.FloatingWindowLayer)
+ or nameof(FloatingWindowProfile.FloatingWindowLayerRecheckMode)
+ or nameof(FloatingWindowProfile.FloatingWindowDragHandleAlwaysVisible)
+ or nameof(FloatingWindowProfile.FloatingWindowHorizontal))
+ {
+ IAppHost.GetService().ProfileManager.SaveProfile();
+ IAppHost.GetService().UpdateWindowState();
+ }
+ }
+
+ ///
+ /// 重新注册 Profile 属性变更事件监听(切换方案后需要重新注册)
+ ///
+ public void ReattachProfilePropertyChanged()
+ {
+ ViewModel.CurrentFloatingWindowProfile.PropertyChanged -= OnProfilePropertyChanged;
+ ViewModel.CurrentFloatingWindowProfile.PropertyChanged += OnProfilePropertyChanged;
+
+ // 重新注册悬浮窗规则集变更监听
+ UnregisterHidingRulesEvents();
+ RegisterHidingRulesEvents();
+ }
+
+ private void RegisterHidingRulesEvents()
+ {
+ if (ViewModel.Settings.FloatingWindowRuleset is INotifyPropertyChanged hidingRules)
+ {
+ hidingRules.PropertyChanged += OnHidingRulesPropertyChanged;
+ }
+ }
+
+ private void UnregisterHidingRulesEvents()
+ {
+ if (ViewModel.Settings.FloatingWindowRuleset is INotifyPropertyChanged hidingRules)
+ {
+ hidingRules.PropertyChanged -= OnHidingRulesPropertyChanged;
+ }
+ }
+
private void OnSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName is nameof(MainConfigData.ShowFloatingWindow)
- or nameof(MainConfigData.FloatingWindowScale)
- or nameof(MainConfigData.FloatingWindowIconSize)
- or nameof(MainConfigData.FloatingWindowTextSize)
- or nameof(MainConfigData.FloatingWindowOpacity)
- or nameof(MainConfigData.FloatingWindowTheme)
- or nameof(MainConfigData.FloatingWindowShadowEnabled)
- or nameof(MainConfigData.FloatingWindowLayer)
- or nameof(MainConfigData.FloatingWindowLayerRecheckMode))
+ if (e.PropertyName is nameof(MainConfigData.FloatingWindowTheme))
{
+ GlobalConstants.MainConfig?.Save();
IAppHost.GetService().UpdateWindowState();
}
+ else if (e.PropertyName is nameof(MainConfigData.ShowFloatingWindow)
+ or nameof(MainConfigData.FloatingWindowRulesetEnabled))
+ {
+ GlobalConstants.MainConfig?.Save();
+ IAppHost.GetService().UpdateWindowState();
+ }
+ else if (e.PropertyName == nameof(MainConfigData.FloatingWindowRuleset))
+ {
+ // Ruleset 对象被替换时,重新注册事件
+ UnregisterHidingRulesEvents();
+ RegisterHidingRulesEvents();
+ GlobalConstants.MainConfig?.Save();
+ }
}
- private void OnFloatingWindowConfigChanged(object? sender, RoutedEventArgs e)
+ private void OnViewModelProfileChanged(object? sender, EventArgs e)
{
- if (!ViewModel.HasFloatingTriggerEntries)
+ ReattachProfilePropertyChanged();
+ }
+
+ private void OnHidingRulesPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ GlobalConstants.MainConfig?.Save();
+ }
+
+ private void OnFloatingWindowVisibleToggleChanged(object? sender, RoutedEventArgs e)
+ {
+ if (sender is not ToggleSwitch toggle)
{
- ViewModel.Settings.ShowFloatingWindow = false;
+ return;
}
+ var service = IAppHost.GetService();
+ var config = ViewModel.Settings;
+
+ // 没有可用按钮时强制隐藏
+ var shouldShow = toggle.IsChecked == true && service.Entries.Count > 0;
+ config.ShowFloatingWindow = shouldShow;
+
+ // 同步 ToggleSwitch 状态(可能被强制隐藏)
+ if (toggle.IsChecked != shouldShow)
+ {
+ toggle.IsChecked = shouldShow;
+ }
+
+ GlobalConstants.MainConfig?.Save();
+ service.UpdateWindowState();
+ }
+
+ private void OnFloatingWindowProfileSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not ComboBox comboBox || comboBox.SelectedItem is not string profileName)
+ {
+ return;
+ }
+
+ ViewModel.SwitchFloatingWindowProfile(profileName);
+ }
+
+ private void OnToggleFloatingWindowProfileClick(object? sender, RoutedEventArgs e)
+ {
+ IAppHost.GetService().ToggleWindowProfile();
+ ViewModel.RefreshFloatingWindowProfiles();
ViewModel.RefreshFloatingTriggers();
- IAppHost.GetService().UpdateWindowState();
}
- private Point? _floatingDragStartPoint;
- private Border? _floatingDragSourceBorder;
+ private void OnAddFloatingWindowProfileClick(object? sender, RoutedEventArgs e)
+ {
+ ViewModel.AddFloatingWindowProfile();
+ }
+
+ private void OnRemoveCurrentProfileClick(object? sender, RoutedEventArgs e)
+ {
+ var currentName = ViewModel.SelectedFloatingWindowProfile;
+ if (string.IsNullOrWhiteSpace(currentName))
+ {
+ return;
+ }
+
+ ViewModel.RemoveFloatingWindowProfile(currentName);
+ }
private void OnAddFloatingTriggerRowClick(object? sender, RoutedEventArgs e)
{
@@ -107,6 +240,121 @@ private void OnRemoveFloatingTriggerRowClick(object? sender, RoutedEventArgs e)
_ = ViewModel.RemoveFloatingTriggerRow(row);
}
+ private void ButtonOpenFloatingWindowRuleset_OnClick(object? sender, RoutedEventArgs e)
+ {
+ _currentRulesetTarget = RulesetTargetType.Window;
+ _currentButtonTarget = null;
+ _currentRowTarget = null;
+
+ var config = ViewModel.Settings;
+ OpenRulesetDrawer(config.FloatingWindowRuleset, true, config.FloatingWindowRulesetEnabled);
+ }
+
+ ///
+ /// 打开规则集 Drawer,包含 IsVisible/HideOnRule 开关和规则集编辑器(参照 ClassIsland)
+ ///
+ private void OpenRulesetDrawer(ClassIsland.Core.Models.Ruleset.Ruleset ruleset, bool isVisible, bool hideOnRule)
+ {
+ // 每次打开时动态构建 Drawer 内容,避免资源单例问题
+ var panel = new StackPanel { Spacing = 8, Margin = new Thickness(0, 8, 0, 0) };
+
+ // 开关面板
+ var togglesPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 16, Margin = new Thickness(0, 0, 0, 8) };
+
+ _drawerIsVisibleToggle = new ToggleSwitch
+ {
+ OnContent = "显示",
+ OffContent = "隐藏",
+ IsChecked = isVisible,
+ IsVisible = _currentRulesetTarget != RulesetTargetType.Window
+ };
+ ToolTip.SetTip(_drawerIsVisibleToggle, "控制此项目是否显示");
+ _drawerIsVisibleToggle.IsCheckedChanged += OnDrawerIsVisibleChanged;
+
+ _drawerHideOnRuleToggle = new ToggleSwitch
+ {
+ OnContent = "按规则隐藏",
+ OffContent = "禁用规则",
+ IsChecked = hideOnRule
+ };
+ ToolTip.SetTip(_drawerHideOnRuleToggle, "启用后,满足规则集条件时自动隐藏");
+ _drawerHideOnRuleToggle.IsCheckedChanged += OnDrawerHideOnRuleChanged;
+
+ togglesPanel.Children.Add(_drawerIsVisibleToggle);
+ togglesPanel.Children.Add(_drawerHideOnRuleToggle);
+ panel.Children.Add(togglesPanel);
+
+ // 规则集编辑器
+ _drawerRulesetControl = new RulesetControl { Classes = { "in-drawer" }, Ruleset = ruleset };
+ panel.Children.Add(_drawerRulesetControl);
+
+ // 将内容放入 Resources 并打开 Drawer
+ this.Resources["RulesetDrawerContent"] = panel;
+ OpenDrawer("RulesetDrawerContent");
+ }
+
+ private void OnDrawerIsVisibleChanged(object? sender, RoutedEventArgs e)
+ {
+ var value = _drawerIsVisibleToggle?.IsChecked == true;
+
+ switch (_currentRulesetTarget)
+ {
+ case RulesetTargetType.Button when _currentButtonTarget != null:
+ _currentButtonTarget.Config.IsVisible = value;
+ break;
+ case RulesetTargetType.Row when _currentRowTarget != null:
+ _currentRowTarget.RowRuleset.IsVisible = value;
+ break;
+ }
+
+ IAppHost.GetService().ProfileManager.SaveProfile();
+ IAppHost.GetService().UpdateWindowState();
+ }
+
+ private void OnDrawerHideOnRuleChanged(object? sender, RoutedEventArgs e)
+ {
+ var value = _drawerHideOnRuleToggle?.IsChecked == true;
+
+ switch (_currentRulesetTarget)
+ {
+ case RulesetTargetType.Button when _currentButtonTarget != null:
+ _currentButtonTarget.Config.HideOnRule = value;
+ break;
+ case RulesetTargetType.Row when _currentRowTarget != null:
+ _currentRowTarget.RowRuleset.HideOnRule = value;
+ break;
+ case RulesetTargetType.Window:
+ ViewModel.Settings.FloatingWindowRulesetEnabled = value;
+ GlobalConstants.MainConfig?.Save();
+ break;
+ }
+
+ IAppHost.GetService().ProfileManager.SaveProfile();
+ IAppHost.GetService().UpdateWindowState();
+ }
+
+ // ===== 选中状态处理 =====
+
+ private void OnAvailableItemSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not ListBox listBox || listBox.SelectedItem is not FloatingTriggerItem item)
+ {
+ return;
+ }
+
+ var buttonId = item.ButtonId;
+ listBox.SelectedItem = null;
+
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ if (ViewModel.FloatingTriggerRows.Count == 0)
+ {
+ ViewModel.AddFloatingTriggerRow();
+ }
+ ViewModel.AddTriggerFromPool(buttonId, 0, ViewModel.FloatingTriggerRows[0].Buttons.Count);
+ });
+ }
+
private void OnFloatingTriggerItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender is not Border border || !e.GetCurrentPoint(border).Properties.IsLeftButtonPressed)
@@ -280,4 +528,4 @@ private void OnFloatingTriggerItemDrop(object? sender, DragEventArgs e)
ViewModel.MoveFloatingTrigger(buttonId, rowIndex, targetIndex);
}
-}
\ No newline at end of file
+}
diff --git a/SettingsPage/SystemToolsSettingsPage.axaml.cs b/SettingsPage/SystemToolsSettingsPage.axaml.cs
index 8a2c6101..70e37fca 100644
--- a/SettingsPage/SystemToolsSettingsPage.axaml.cs
+++ b/SettingsPage/SystemToolsSettingsPage.axaml.cs
@@ -64,11 +64,7 @@ private void OnRestartPropertyChanged(object? sender, EventArgs e)
private void OnSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName is nameof(MainConfigData.ShowFloatingWindow)
- or nameof(MainConfigData.FloatingWindowScale))
- {
- IAppHost.GetService().UpdateWindowState();
- }
+ // 主设置页面不再直接监听悬浮窗属性变化,由 FloatingWindowEditorSettingsPage 处理
}
diff --git a/SettingsPage/SystemToolsSettingsViewModel.cs b/SettingsPage/SystemToolsSettingsViewModel.cs
index a127cc33..c468eec7 100644
--- a/SettingsPage/SystemToolsSettingsViewModel.cs
+++ b/SettingsPage/SystemToolsSettingsViewModel.cs
@@ -7,6 +7,7 @@
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System;
+using System.ComponentModel;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
@@ -46,11 +47,30 @@ public partial class FloatingTriggerItem : ObservableObject
[ObservableProperty] private string _buttonId = string.Empty;
[ObservableProperty] private string _icon = string.Empty;
[ObservableProperty] private string _buttonName = string.Empty;
+ [ObservableProperty] private bool _isRulesetExpanded = false;
+ [ObservableProperty] private ButtonRulesetConfig _config = new();
+
+ ///
+ /// FluentIconSource,供 IconSourceElement 使用
+ ///
+ public ClassIsland.Core.Controls.FluentIconSource? IconSource
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(Icon)) return null;
+ return new ClassIsland.Core.Controls.FluentIconSource { Glyph = Icon };
+ }
+ }
+
+ partial void OnIconChanged(string value) { OnPropertyChanged(nameof(IconSource)); }
}
public partial class FloatingTriggerRow : ObservableObject
{
[ObservableProperty] private ObservableCollection _buttons = new();
+ [ObservableProperty] private int _rowIndex = 0;
+ [ObservableProperty] private RowRulesetConfig _rowRuleset = new();
+ [ObservableProperty] private bool _isRulesetExpanded = false;
}
public partial class SystemToolsSettingsViewModel : ObservableObject, IDisposable
@@ -77,6 +97,17 @@ public partial class SystemToolsSettingsViewModel : ObservableObject, IDisposabl
[ObservableProperty] private ObservableCollection _floatingTriggerRows = new();
[ObservableProperty] private bool _hasFloatingTriggerEntries;
+ // 选中状态
+ [ObservableProperty] private FloatingTriggerRow? _selectedFloatingTriggerRow;
+ [ObservableProperty] private FloatingTriggerItem? _selectedFloatingTriggerItem;
+
+ // 可用按钮池(未添加到悬浮窗的按钮)
+ [ObservableProperty] private ObservableCollection _availableFloatingTriggerItems = new();
+
+ // 悬浮窗配置方案
+ [ObservableProperty] private ObservableCollection _floatingWindowProfileNames = new();
+ [ObservableProperty] private string _selectedFloatingWindowProfile = "Default";
+
private const string DownloadUrl =
"https://livefile.xesimg.com/programme/python_assets/f94fcfa40c9de41d6df09566a51e3130.exe";
private const string ExpectedMd5 = "f94fcfa40c9de41d6df09566a51e3130";
@@ -174,7 +205,7 @@ public void InitializeFeatureItems()
("SystemTools.WindowOperation", "窗口操作", "模拟操作"),
("SystemTools.AltF4", "按下 Alt+F4", "常用模拟键"),
("SystemTools.AltTab", "按下 Alt+Tab", "常用模拟键"),
- ("SystemTools.AltTab", "按下 Ctrl+Z", "常用模拟键"),
+ ("SystemTools.CtrlZ", "按下 Ctrl+Z", "常用模拟键"),
("SystemTools.EnterKey", "按下 Enter 键", "常用模拟键"),
("SystemTools.EscKey", "按下 Esc 键", "常用模拟键"),
("SystemTools.F11Key", "按下 F11 键", "常用模拟键"),
@@ -220,6 +251,8 @@ public void InitializeFeatureItems()
if (Settings.EnableFloatingWindowFeature)
{
actions.Add(("SystemTools.ShowFloatingWindow", "显示悬浮窗", "悬浮窗设置"));
+ actions.Add(("SystemTools.ToggleFloatingWindowLayer", "切换悬浮窗层级", "悬浮窗设置"));
+ actions.Add(("SystemTools.ToggleFloatingWindowProfile", "切换悬浮窗配置方案", "悬浮窗设置"));
}
foreach (var (id, name, group) in actions)
@@ -259,7 +292,18 @@ public void SaveFeatureSettings()
_configHandler.Save();
}
+ public FloatingWindowProfile CurrentFloatingWindowProfile => _floatingWindowService.ProfileManager.CurrentProfile;
+ public void RefreshFloatingWindowProfiles()
+ {
+ var names = _floatingWindowService.ProfileManager.GetProfileNames();
+ FloatingWindowProfileNames.Clear();
+ foreach (var name in names)
+ {
+ FloatingWindowProfileNames.Add(name);
+ }
+ SelectedFloatingWindowProfile = _floatingWindowService.ProfileManager.CurrentProfileName;
+ }
public void RefreshFloatingTriggers()
{
@@ -269,74 +313,221 @@ public void RefreshFloatingTriggers()
.ToDictionary(x => x.Key, x => x.First());
HasFloatingTriggerEntries = entries.Count > 0;
- if (!HasFloatingTriggerEntries && Settings.ShowFloatingWindow)
+ var profile = CurrentFloatingWindowProfile;
+ var globalShow = _configHandler.Data.ShowFloatingWindow;
+ if (!HasFloatingTriggerEntries && globalShow)
{
- Settings.ShowFloatingWindow = false;
+ _configHandler.Data.ShowFloatingWindow = false;
_configHandler.Save();
+ _floatingWindowService.UpdateWindowState();
}
- var legacyOrder = Settings.FloatingWindowButtonOrder ?? [];
- var orderedIds = entries.Keys
- .OrderBy(id =>
- {
- var i = legacyOrder.IndexOf(id);
- return i < 0 ? int.MaxValue : i;
- })
- .ThenBy(id => id)
- .ToList();
+ // 清理不存在的按钮ID
+ if (profile.PruneInvalidButtonIds(entries.Keys))
+ {
+ _floatingWindowService.ProfileManager.SaveProfile();
+ }
- var used = new HashSet();
- var normalizedRows = new List>();
+ // 收集已配置的按钮ID
+ var configuredIds = new HashSet();
+ foreach (var row in profile.FloatingWindowButtonRows ?? [])
+ {
+ foreach (var id in row)
+ {
+ configuredIds.Add(id);
+ }
+ }
- foreach (var row in Settings.FloatingWindowButtonRows ?? [])
+ // 如果没有任何按钮被配置到行中,自动将所有可用按钮添加到第一行
+ // 这样用户首次使用或从旧版本迁移时,按钮默认会显示出来
+ if (configuredIds.Count == 0 && entries.Count > 0)
{
- var normalizedRow = row
- .Where(id => entries.ContainsKey(id) && used.Add(id))
- .ToList();
- if (normalizedRow.Count > 0)
+ var allButtonIds = entries.Values.Select(e => e.ButtonId).ToList();
+ if (profile.FloatingWindowButtonRows == null || profile.FloatingWindowButtonRows.Count == 0)
+ {
+ profile.FloatingWindowButtonRows = [allButtonIds];
+ }
+ else
{
- normalizedRows.Add(normalizedRow);
+ profile.FloatingWindowButtonRows[0] = allButtonIds;
}
+ foreach (var id in allButtonIds)
+ {
+ configuredIds.Add(id);
+ }
+ _floatingWindowService.ProfileManager.SaveProfile();
}
- var missing = orderedIds.Where(id => !used.Contains(id)).ToList();
- if (normalizedRows.Count == 0)
+ // 注销旧对象上的事件处理程序,避免重复注册和内存泄漏
+ foreach (var oldRow in FloatingTriggerRows)
{
- normalizedRows.Add(missing);
+ oldRow.RowRuleset.PropertyChanged -= OnRowRulesetPropertyChanged;
+ if (oldRow.RowRuleset.HidingRules is INotifyPropertyChanged oldRowHidingRules)
+ {
+ oldRowHidingRules.PropertyChanged -= OnRowRulesetPropertyChanged;
+ }
+ foreach (var oldItem in oldRow.Buttons)
+ {
+ oldItem.Config.PropertyChanged -= OnButtonConfigPropertyChanged;
+ if (oldItem.Config.HidingRules is INotifyPropertyChanged oldBtnHidingRules)
+ {
+ oldBtnHidingRules.PropertyChanged -= OnButtonConfigPropertyChanged;
+ }
+ }
}
- else
+
+ // 构建已配置的行显示
+ FloatingTriggerRows.Clear();
+ var rowConfigs = profile.FloatingWindowRowRulesets;
+ var rowIndex = 0;
+ var needSave = false;
+ foreach (var row in profile.FloatingWindowButtonRows ?? [])
{
- normalizedRows[0].AddRange(missing);
+ while (rowConfigs.Count <= rowIndex)
+ {
+ rowConfigs.Add(new RowRulesetConfig());
+ needSave = true;
+ }
+ var vmRow = new FloatingTriggerRow
+ {
+ RowIndex = rowIndex + 1,
+ RowRuleset = rowConfigs[rowIndex]
+ };
+ vmRow.RowRuleset.PropertyChanged += OnRowRulesetPropertyChanged;
+ if (vmRow.RowRuleset.HidingRules is INotifyPropertyChanged rowHidingRules)
+ {
+ rowHidingRules.PropertyChanged += OnRowRulesetPropertyChanged;
+ }
+ foreach (var id in row)
+ {
+ if (!entries.TryGetValue(id, out var entry))
+ {
+ continue;
+ }
+ if (!profile.FloatingWindowButtonRulesets.TryGetValue(entry.ButtonId, out var btnConfig))
+ {
+ btnConfig = new ButtonRulesetConfig();
+ profile.FloatingWindowButtonRulesets[entry.ButtonId] = btnConfig;
+ needSave = true;
+ }
+ var item = new FloatingTriggerItem
+ {
+ ButtonId = entry.ButtonId,
+ Icon = FloatingWindowService.ConvertIcon(entry.Icon),
+ ButtonName = entry.LayoutName,
+ Config = btnConfig
+ };
+ item.Config.PropertyChanged += OnButtonConfigPropertyChanged;
+ if (item.Config.HidingRules is INotifyPropertyChanged btnHidingRules)
+ {
+ btnHidingRules.PropertyChanged += OnButtonConfigPropertyChanged;
+ }
+ vmRow.Buttons.Add(item);
+ }
+ FloatingTriggerRows.Add(vmRow);
+ rowIndex++;
}
- if (normalizedRows.Count == 0)
+ if (FloatingTriggerRows.Count == 0)
{
- normalizedRows.Add([]);
+ if (rowConfigs.Count == 0)
+ {
+ rowConfigs.Add(new RowRulesetConfig());
+ needSave = true;
+ }
+ var emptyRow = new FloatingTriggerRow
+ {
+ RowIndex = 1,
+ RowRuleset = rowConfigs[0]
+ };
+ emptyRow.RowRuleset.PropertyChanged += OnRowRulesetPropertyChanged;
+ if (emptyRow.RowRuleset.HidingRules is INotifyPropertyChanged emptyRowHidingRules)
+ {
+ emptyRowHidingRules.PropertyChanged += OnRowRulesetPropertyChanged;
+ }
+ FloatingTriggerRows.Add(emptyRow);
}
- FloatingTriggerRows.Clear();
- foreach (var row in normalizedRows)
+ // 构建可用按钮池(未配置的按钮)
+ AvailableFloatingTriggerItems.Clear();
+ foreach (var entry in entries.Values)
{
- var vmRow = new FloatingTriggerRow();
- foreach (var id in row)
+ if (!configuredIds.Contains(entry.ButtonId))
{
- var entry = entries[id];
- vmRow.Buttons.Add(new FloatingTriggerItem
+ AvailableFloatingTriggerItems.Add(new FloatingTriggerItem
{
ButtonId = entry.ButtonId,
- Icon = entry.Icon,
+ Icon = FloatingWindowService.ConvertIcon(entry.Icon),
ButtonName = entry.LayoutName
});
}
- FloatingTriggerRows.Add(vmRow);
}
- PersistFloatingTriggerRows(updateWindow: false, forceSave: false);
+ // 如果有新创建的默认配置,确保保存
+ if (needSave)
+ {
+ _floatingWindowService.ProfileManager.SaveProfile();
+ }
+
+ }
+
+ private void OnButtonConfigPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ _floatingWindowService.ProfileManager.SaveProfile();
+ _floatingWindowService.UpdateWindowState();
+ }
+
+ private void OnRowRulesetPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ _floatingWindowService.ProfileManager.SaveProfile();
+ _floatingWindowService.UpdateWindowState();
}
public void AddFloatingTriggerRow()
{
- FloatingTriggerRows.Add(new FloatingTriggerRow());
+ var profile = CurrentFloatingWindowProfile;
+ var rowRulesets = profile.FloatingWindowRowRulesets;
+ var newRowRuleset = new RowRulesetConfig();
+ rowRulesets.Add(newRowRuleset);
+ var newRow = new FloatingTriggerRow
+ {
+ RowIndex = FloatingTriggerRows.Count + 1,
+ RowRuleset = newRowRuleset
+ };
+ newRow.RowRuleset.PropertyChanged += OnRowRulesetPropertyChanged;
+ if (newRow.RowRuleset.HidingRules is INotifyPropertyChanged rowHidingRules)
+ {
+ rowHidingRules.PropertyChanged += OnRowRulesetPropertyChanged;
+ }
+ FloatingTriggerRows.Add(newRow);
+ PersistFloatingTriggerRows();
+ }
+
+ public void InsertFloatingTriggerRow(int insertIndex)
+ {
+ var profile = CurrentFloatingWindowProfile;
+ var rowRulesets = profile.FloatingWindowRowRulesets;
+ insertIndex = Math.Clamp(insertIndex, 0, FloatingTriggerRows.Count);
+ var newRowRuleset = new RowRulesetConfig();
+ rowRulesets.Insert(insertIndex, newRowRuleset);
+ var newRow = new FloatingTriggerRow
+ {
+ RowIndex = insertIndex + 1,
+ RowRuleset = newRowRuleset
+ };
+ newRow.RowRuleset.PropertyChanged += OnRowRulesetPropertyChanged;
+ if (newRow.RowRuleset.HidingRules is INotifyPropertyChanged rowHidingRules)
+ {
+ rowHidingRules.PropertyChanged += OnRowRulesetPropertyChanged;
+ }
+ FloatingTriggerRows.Insert(insertIndex, newRow);
+
+ // 重新计算后续行的索引
+ for (int i = insertIndex; i < FloatingTriggerRows.Count; i++)
+ {
+ FloatingTriggerRows[i].RowIndex = i + 1;
+ }
+
PersistFloatingTriggerRows();
}
@@ -348,13 +539,28 @@ public bool RemoveFloatingTriggerRow(FloatingTriggerRow row)
return false;
}
+ // 注销被移除行的事件处理程序
+ row.RowRuleset.PropertyChanged -= OnRowRulesetPropertyChanged;
+ if (row.RowRuleset.HidingRules is INotifyPropertyChanged rowHidingRules)
+ {
+ rowHidingRules.PropertyChanged -= OnRowRulesetPropertyChanged;
+ }
+
var targetRow = index > 0 ? FloatingTriggerRows[index - 1] : FloatingTriggerRows[index + 1];
foreach (var item in row.Buttons)
{
+ // 按钮的 Config 事件监听保持不变(对象引用不变,事件仍有效)
targetRow.Buttons.Add(item);
}
FloatingTriggerRows.RemoveAt(index);
+
+ // 重新计算行索引
+ for (int i = 0; i < FloatingTriggerRows.Count; i++)
+ {
+ FloatingTriggerRows[i].RowIndex = i + 1;
+ }
+
PersistFloatingTriggerRows();
return true;
}
@@ -368,9 +574,11 @@ public bool MoveFloatingTrigger(string buttonId, int targetRowIndex, int targetI
targetRowIndex = Math.Clamp(targetRowIndex, 0, FloatingTriggerRows.Count - 1);
var sourceRow = FloatingTriggerRows.FirstOrDefault(r => r.Buttons.Any(b => b.ButtonId == buttonId));
+
+ // 如果按钮不在任何行中(如在按钮池中),尝试从按钮池添加
if (sourceRow == null)
{
- return false;
+ return AddTriggerFromPool(buttonId, targetRowIndex, targetIndex);
}
var item = sourceRow.Buttons.First(b => b.ButtonId == buttonId);
@@ -401,8 +609,82 @@ public bool MoveFloatingTrigger(string buttonId, int targetRowIndex, int targetI
return true;
}
+ ///
+ /// 从可用按钮池添加按钮到指定行
+ ///
+ public bool AddTriggerFromPool(string buttonId, int targetRowIndex, int targetIndex)
+ {
+ if (string.IsNullOrWhiteSpace(buttonId) || FloatingTriggerRows.Count == 0)
+ {
+ return false;
+ }
+
+ var poolItem = AvailableFloatingTriggerItems.FirstOrDefault(x => x.ButtonId == buttonId);
+ if (poolItem == null)
+ {
+ return false;
+ }
+
+ targetRowIndex = Math.Clamp(targetRowIndex, 0, FloatingTriggerRows.Count - 1);
+ var destinationRow = FloatingTriggerRows[targetRowIndex];
+ targetIndex = Math.Clamp(targetIndex, 0, destinationRow.Buttons.Count);
+
+ AvailableFloatingTriggerItems.Remove(poolItem);
+
+ // 确保按钮有 Config(池项可能没有),并注册事件监听
+ var profile = CurrentFloatingWindowProfile;
+ if (!profile.FloatingWindowButtonRulesets.TryGetValue(buttonId, out var btnConfig))
+ {
+ btnConfig = new ButtonRulesetConfig();
+ profile.FloatingWindowButtonRulesets[buttonId] = btnConfig;
+ }
+ poolItem.Config = btnConfig;
+ poolItem.Config.PropertyChanged += OnButtonConfigPropertyChanged;
+ if (poolItem.Config.HidingRules is INotifyPropertyChanged btnHidingRules)
+ {
+ btnHidingRules.PropertyChanged += OnButtonConfigPropertyChanged;
+ }
+
+ destinationRow.Buttons.Insert(targetIndex, poolItem);
+ PersistFloatingTriggerRows();
+ return true;
+ }
+
+ ///
+ /// 将按钮从行中移除,放回可用按钮池
+ ///
+ public bool RemoveTriggerToPool(string buttonId)
+ {
+ if (string.IsNullOrWhiteSpace(buttonId))
+ {
+ return false;
+ }
+
+ foreach (var row in FloatingTriggerRows)
+ {
+ var item = row.Buttons.FirstOrDefault(x => x.ButtonId == buttonId);
+ if (item != null)
+ {
+ // 注销事件处理程序
+ item.Config.PropertyChanged -= OnButtonConfigPropertyChanged;
+ if (item.Config.HidingRules is INotifyPropertyChanged btnHidingRules)
+ {
+ btnHidingRules.PropertyChanged -= OnButtonConfigPropertyChanged;
+ }
+
+ row.Buttons.Remove(item);
+ AvailableFloatingTriggerItems.Add(item);
+ PersistFloatingTriggerRows();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public void PersistFloatingTriggerRows(bool updateWindow = true, bool forceSave = true)
{
+ var profile = CurrentFloatingWindowProfile;
var newRows = FloatingTriggerRows
.Select(row => row.Buttons.Select(x => x.ButtonId).ToList())
.ToList();
@@ -410,22 +692,68 @@ public void PersistFloatingTriggerRows(bool updateWindow = true, bool forceSave
.SelectMany(row => row)
.ToList();
- var rowsChanged = !AreRowsEqual(Settings.FloatingWindowButtonRows, newRows);
- var orderChanged = !(Settings.FloatingWindowButtonOrder ?? []).SequenceEqual(newOrder);
+ var rowsChanged = !AreRowsEqual(profile.FloatingWindowButtonRows, newRows);
+ var orderChanged = !(profile.FloatingWindowButtonOrder ?? []).SequenceEqual(newOrder);
if (rowsChanged)
{
- Settings.FloatingWindowButtonRows = newRows;
+ profile.FloatingWindowButtonRows = newRows;
}
if (orderChanged)
{
- Settings.FloatingWindowButtonOrder = newOrder;
+ profile.FloatingWindowButtonOrder = newOrder;
}
- if (forceSave && (rowsChanged || orderChanged))
+ // 同步行规则集:确保 FloatingWindowRowRulesets 与行数一致
+ var rowRulesets = profile.FloatingWindowRowRulesets;
+ while (rowRulesets.Count < FloatingTriggerRows.Count)
{
- _configHandler.Save();
+ rowRulesets.Add(new RowRulesetConfig());
+ }
+ while (rowRulesets.Count > FloatingTriggerRows.Count)
+ {
+ // 注销被移除行规则集的事件
+ var removedRowRuleset = rowRulesets[rowRulesets.Count - 1];
+ removedRowRuleset.PropertyChanged -= OnRowRulesetPropertyChanged;
+ if (removedRowRuleset.HidingRules is INotifyPropertyChanged removedHidingRules)
+ {
+ removedHidingRules.PropertyChanged -= OnRowRulesetPropertyChanged;
+ }
+ rowRulesets.RemoveAt(rowRulesets.Count - 1);
+ }
+ // 同步每行的 RowRuleset 引用(确保ViewModel中的修改反映到profile)
+ for (int i = 0; i < FloatingTriggerRows.Count; i++)
+ {
+ var vmRow = FloatingTriggerRows[i];
+ if (!ReferenceEquals(vmRow.RowRuleset, rowRulesets[i]))
+ {
+ // RowRuleset 引用变更时,重新注册事件
+ vmRow.RowRuleset.PropertyChanged -= OnRowRulesetPropertyChanged;
+ if (vmRow.RowRuleset.HidingRules is INotifyPropertyChanged oldHidingRules)
+ {
+ oldHidingRules.PropertyChanged -= OnRowRulesetPropertyChanged;
+ }
+ vmRow.RowRuleset = rowRulesets[i];
+ vmRow.RowRuleset.PropertyChanged += OnRowRulesetPropertyChanged;
+ if (vmRow.RowRuleset.HidingRules is INotifyPropertyChanged newHidingRules)
+ {
+ newHidingRules.PropertyChanged += OnRowRulesetPropertyChanged;
+ }
+ }
+ }
+
+ // 清理不再使用的按钮规则集配置
+ var usedButtonIds = new HashSet(newOrder);
+ var staleButtonIds = profile.FloatingWindowButtonRulesets.Keys.Where(id => !usedButtonIds.Contains(id)).ToList();
+ foreach (var staleId in staleButtonIds)
+ {
+ profile.FloatingWindowButtonRulesets.Remove(staleId);
+ }
+
+ if (forceSave)
+ {
+ _floatingWindowService.ProfileManager.SaveProfile();
}
if (updateWindow)
@@ -434,6 +762,52 @@ public void PersistFloatingTriggerRows(bool updateWindow = true, bool forceSave
}
}
+ public void AddFloatingWindowProfile()
+ {
+ var newName = _floatingWindowService.ProfileManager.CreateProfile();
+ RefreshFloatingWindowProfiles();
+ SelectedFloatingWindowProfile = newName;
+ SwitchFloatingWindowProfile(newName);
+ }
+
+ public void RemoveFloatingWindowProfile(string profileName)
+ {
+ if (string.IsNullOrWhiteSpace(profileName))
+ {
+ return;
+ }
+
+ if (_floatingWindowService.ProfileManager.RemoveProfile(profileName))
+ {
+ RefreshFloatingWindowProfiles();
+ // 如果删除的是当前方案,切换到 Default
+ if (string.Equals(SelectedFloatingWindowProfile, profileName, StringComparison.OrdinalIgnoreCase))
+ {
+ SwitchFloatingWindowProfile("Default");
+ }
+ }
+ }
+
+ public void SwitchFloatingWindowProfile(string profileName)
+ {
+ if (string.IsNullOrWhiteSpace(profileName))
+ {
+ return;
+ }
+
+ _floatingWindowService.SwitchToProfile(profileName);
+ SelectedFloatingWindowProfile = profileName;
+ RefreshFloatingTriggers();
+
+ // 通知 UI 重新注册 Profile 属性变更事件监听
+ ProfileChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Profile 对象发生变化时触发(切换方案后需要重新注册事件监听)
+ ///
+ public event EventHandler? ProfileChanged;
+
public void Dispose()
{
_floatingWindowService.EntriesChanged -= _entriesChangedHandler;
diff --git a/Triggers/FloatingWindowTrigger.cs b/Triggers/FloatingWindowTrigger.cs
index 34776997..51d91e10 100644
--- a/Triggers/FloatingWindowTrigger.cs
+++ b/Triggers/FloatingWindowTrigger.cs
@@ -104,7 +104,7 @@ public string GetButtonName()
public string GetLayoutButtonName()
{
- return IsRevertEnabled() ? $"{Settings.ButtonName}(启用恢复)" : Settings.ButtonName;
+ return Settings.ButtonName;
}
public bool ShouldUseRevertStyle()
diff --git a/Triggers/FloatingWindowTriggerConfig.cs b/Triggers/FloatingWindowTriggerConfig.cs
index 91f9f706..30fc70d0 100644
--- a/Triggers/FloatingWindowTriggerConfig.cs
+++ b/Triggers/FloatingWindowTriggerConfig.cs
@@ -1,11 +1,31 @@
using System;
+using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SystemTools.Triggers;
-public partial class FloatingWindowTriggerConfig : ObservableRecipient
+///
+/// 悬浮窗触发器的配置
+///
+public partial class FloatingWindowTriggerConfig : ObservableObject
{
- [ObservableProperty] private string _buttonId = Guid.NewGuid().ToString("N");
- [ObservableProperty] private string _icon = "/uEA37";
- [ObservableProperty] private string _buttonName = "触发按钮 1";
+ [ObservableProperty]
+ [JsonPropertyName("buttonId")]
+ private string _buttonId = Guid.NewGuid().ToString("N");
+
+ [ObservableProperty]
+ [JsonPropertyName("icon")]
+ private string _icon = "/uEA37";
+
+ [ObservableProperty]
+ [JsonPropertyName("buttonName")]
+ private string _buttonName = "触发按钮 1";
+
+ [ObservableProperty]
+ [JsonPropertyName("isVisible")]
+ private bool _isVisible = true;
+
+ [ObservableProperty]
+ [JsonPropertyName("position")]
+ private int _position = -1;
}