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"> + + + + + + + + + + + + + + - \ 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; }