diff --git a/.trae/documents/floating-window-button-ruleset-inline-ui.md b/.trae/documents/floating-window-button-ruleset-inline-ui.md
new file mode 100644
index 0000000..143c590
--- /dev/null
+++ b/.trae/documents/floating-window-button-ruleset-inline-ui.md
@@ -0,0 +1,262 @@
+# 悬浮窗按钮规则集内联UI优化计划
+
+## 需求概述
+
+用户提出两个核心问题:
+1. **按钮规则集UI应该像ClassIsland组件一样**:点击按钮后,可以在按钮旁边直接调整规则集设置,而不是在底部的折叠面板中集中管理
+2. **拖拽反馈不明确**:拖动按钮时无法确定自己是否正在拖动,缺少视觉反馈
+
+用户特别叮嘱:
+- **性能优化**:避免不必要的UI渲染和重复保存
+- **去掉单独的按钮显示开关**:不需要 `IsVisible` 开关,因为删除按钮即可实现隐藏
+- **配置文件保存**:确保修改后及时、正确地写入JSON文件
+
+## 现状分析
+
+### 当前按钮规则集UI
+- 位置:在"规则集设置"折叠面板中,所有按钮的规则集集中显示
+- 问题:按钮和规则集配置分离,用户需要滚动到页面底部才能配置,不直观
+
+### 当前拖拽机制
+- 触发方式:`PointerPressed`/`PointerMoved`/`PointerReleased` 在拖拽把手上
+- 问题:
+ - 只有 `⋮` 把手区域可以触发拖拽,但把手太小(4px padding)
+ - 拖拽开始时没有视觉反馈(如半透明、阴影、光标变化)
+ - 拖拽过程中没有预览效果
+ - 按钮池中的卡片也是整个区域触发拖拽,但没有视觉提示
+
+## 设计方案
+
+### 一、按钮规则集内联配置(核心改动)
+
+参考ClassIsland组件配置方式:
+- 每个按钮在行内显示时,右侧添加一个"规则集"按钮(齿轮图标)
+- 点击后在该按钮下方展开**精简的规则集配置面板**
+- 面板只包含:**启用规则集开关** + **规则集编辑器**(去掉 `IsVisible` 开关,因为删除按钮即可隐藏)
+
+#### XAML结构变更
+
+**行内按钮模板修改:**
+```xml
+
+
+
+
+
+ ...
+
+ ...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#### ViewModel变更
+
+**FloatingTriggerItem新增属性:**
+```csharp
+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(); // 新增:直接引用规则集配置
+}
+```
+
+**重构RefreshFloatingTriggers:**
+- 为每个按钮查找并绑定对应的 `ButtonRulesetConfig`
+- 如果字典中不存在,自动创建新的配置并加入字典
+- 移除底部集中的 `FloatingTriggerButtonConfigs` 集合及相关代码
+
+#### 代码后置事件
+
+```csharp
+private void OnButtonRulesetClick(object? sender, RoutedEventArgs e)
+{
+ // 找到对应的 FloatingTriggerItem
+ // 切换 IsRulesetExpanded 状态
+ // 关闭其他按钮的规则集面板(单开模式,避免同时展开多个占用空间)
+}
+```
+
+#### 配置文件保存策略
+
+```csharp
+// 规则集修改后自动保存
+// 利用 ButtonRulesetConfig 的 PropertyChanged 事件
+// 或者由 RulesetControl 的变更触发保存
+
+// 在 ViewModel 中订阅 Config 属性变更
+private void SubscribeButtonConfigChanges()
+{
+ foreach (var row in FloatingTriggerRows)
+ {
+ foreach (var button in row.Buttons)
+ {
+ button.Config.PropertyChanged += OnButtonConfigPropertyChanged;
+ }
+ }
+}
+
+private void OnButtonConfigPropertyChanged(object? sender, PropertyChangedEventArgs e)
+{
+ // 直接保存整个 profile,确保 JSON 文件更新
+ _floatingWindowService.ProfileManager.SaveProfile();
+ // 通知悬浮窗更新显示状态
+ _floatingWindowService.UpdateWindowState();
+}
+```
+
+### 二、移除底部按钮规则集面板
+
+- 删除 XAML 中"规则集设置"折叠面板内的"按钮规则集"区域
+- 删除 `FloatingTriggerButtonConfigItem` 类
+- 删除 `FloatingTriggerButtonConfigs` 集合
+- 删除 `RefreshFloatingTriggerButtonConfigs` 方法
+- 保留"悬浮窗规则集"(整体控制悬浮窗显示/隐藏)
+
+### 三、拖拽视觉反馈优化
+
+#### 1. 拖拽把手扩大热区
+```xml
+
+
+
+```
+
+#### 2. 拖拽开始时添加视觉反馈
+```csharp
+private Border? _dragVisualBorder;
+
+private void OnFloatingTriggerItemPointerPressed(object? sender, PointerPressedEventArgs e)
+{
+ // ... 现有逻辑 ...
+
+ // 记录原始视觉效果
+ _dragVisualBorder = border;
+ _dragOriginalOpacity = border.Opacity;
+}
+
+private void OnFloatingTriggerItemPointerMoved(object? sender, PointerEventArgs e)
+{
+ // ... 现有逻辑(距离判断)...
+
+ // 一旦判定为拖拽,立即添加视觉反馈
+ if (_dragVisualBorder != null && _isDragging)
+ {
+ _dragVisualBorder.Opacity = 0.6;
+ _dragVisualBorder.Classes.Add("dragging");
+ }
+}
+
+private void OnFloatingTriggerItemPointerReleased(object? sender, PointerReleasedEventArgs e)
+{
+ // 恢复视觉效果
+ if (_dragVisualBorder != null)
+ {
+ _dragVisualBorder.Opacity = _dragOriginalOpacity;
+ _dragVisualBorder.Classes.Remove("dragging");
+ _dragVisualBorder = null;
+ }
+ // ... 现有逻辑 ...
+}
+```
+
+#### 3. 添加拖拽样式(在XAML Resources中)
+```xml
+
+```
+
+#### 4. 按钮池卡片拖拽反馈
+- 鼠标悬停在按钮池卡片上时显示 `Cursor="SizeAll"`
+- 拖拽开始时卡片透明度降至 0.5
+- 拖拽把手(`⋮`)使用更深的颜色,始终清晰可见
+
+## 实施步骤
+
+### 步骤1:修改数据模型(SystemToolsSettingsViewModel.cs)
+- [ ] 给 `FloatingTriggerItem` 添加 `IsRulesetExpanded` 和 `Config` 属性
+- [ ] 修改 `RefreshFloatingTriggers`:为每个按钮查找/创建 `ButtonRulesetConfig`
+- [ ] 删除 `FloatingTriggerButtonConfigItem` 类
+- [ ] 删除 `FloatingTriggerButtonConfigs` 属性
+- [ ] 删除 `RefreshFloatingTriggerButtonConfigs` 方法
+
+### 步骤2:修改行内按钮XAML(FloatingWindowEditorSettingsPage.axaml)
+- [ ] 将按钮卡片改为 `Grid RowDefinitions="Auto,Auto"`
+- [ ] 第一行添加"规则集"按钮(``)
+- [ ] 第二行添加规则集展开面板(仅包含启用开关和规则集编辑器)
+- [ ] 移除 `IsVisible` ToggleSwitch(用户不需要单独的显示开关)
+
+### 步骤3:添加事件处理(FloatingWindowEditorSettingsPage.axaml.cs)
+- [ ] 添加 `OnButtonRulesetClick` 方法
+- [ ] 实现单开逻辑(展开一个时关闭其他按钮的规则集面板)
+
+### 步骤4:移除底部按钮规则集面板(FloatingWindowEditorSettingsPage.axaml)
+- [ ] 删除"规则集设置"折叠面板中的"按钮规则集"区域
+
+### 步骤5:优化拖拽视觉反馈
+- [ ] 扩大拖拽把手热区(padding 4,2 → 8,6)
+- [ ] 添加 `Cursor="SizeAll"` 到拖拽把手
+- [ ] 在 `PointerMoved` 中判定拖拽后添加 `dragging` 样式类
+- [ ] 在 `PointerReleased` 中移除 `dragging` 样式类
+- [ ] 添加拖拽样式到 XAML Resources
+- [ ] 为按钮池卡片添加悬停光标提示
+
+### 步骤6:配置文件保存
+- [ ] 在 `FloatingTriggerItem` 的 `Config` 属性变更时,调用 `SaveProfile()`
+- [ ] 确保 `ButtonRulesetConfig` 的 `PropertyChanged` 能正确传播到 profile 保存
+- [ ] 测试:修改规则集后检查 JSON 文件是否及时更新
+
+### 步骤7:测试验证
+- [ ] 测试点击规则集按钮展开/折叠
+- [ ] 测试规则集修改后实时生效且 JSON 保存正确
+- [ ] 测试拖拽按钮时的视觉反馈(透明度变化、阴影)
+- [ ] 测试按钮池拖拽到行中
+- [ ] 测试删除按钮后配置文件中对应规则集是否被清理
+
+## 性能优化策略
+
+1. **懒加载规则集编辑器**:`RulesetControl` 只在展开时初始化,折叠时释放资源
+2. **批量保存**:如果连续快速修改多个属性,使用防抖(debounce)延迟保存,避免频繁写入磁盘
+3. **避免重复刷新**:`UpdateWindowState()` 只在必要属性变更时调用,而非每次保存都调用
+4. **移除冗余集合**:删除 `FloatingTriggerButtonConfigs` 减少内存占用和绑定开销
+
+## 配置文件保存注意事项
+
+1. **保存时机**:
+ - 规则集配置修改后立即保存(通过 `PropertyChanged` 监听)
+ - 按钮拖拽排序后保存(已有 `PersistFloatingTriggerRows`)
+ - 按钮添加/删除后保存
+
+2. **保存内容**:
+ - `FloatingWindowButtonRulesets` 字典中只保留当前存在的按钮ID
+ - 删除按钮时同步清理对应的规则集配置(在 `PruneInvalidButtonIds` 中已实现)
+
+3. **线程安全**:
+ - `SaveProfile()` 使用 `ConfigureFileHelper.SaveConfig`,确保线程安全
+ - UI 线程上的修改通过 `Dispatcher.UIThread.Post` 触发保存
diff --git a/.trae/documents/floating-window-classisland-component-ui-redesign.md b/.trae/documents/floating-window-classisland-component-ui-redesign.md
new file mode 100644
index 0000000..14c62ba
--- /dev/null
+++ b/.trae/documents/floating-window-classisland-component-ui-redesign.md
@@ -0,0 +1,265 @@
+# 悬浮窗编辑页面 ClassIsland 组件库风格重构设计
+
+## 概述
+
+将悬浮窗编辑页面(FloatingWindowEditorSettingsPage)的 UI 和拖拽交互完全重构为 ClassIsland ComponentsSettingsPage 风格,复用 ClassIsland.Core 的拖拽框架(AdvancedManagedContextDragBehavior、TouchDragThumb、ManagedDragDropService、DragPreviewService),并针对低性能设备(学校白板)做性能平衡优化。
+
+## 当前状态分析
+
+### 当前布局
+- 使用 `SettingsExpander` 包裹按钮布局区域
+- 行列表使用 `ItemsControl` + `DataTemplate`
+- 按钮池使用 `ItemsControl` + `WrapPanel`
+- 拖拽使用 Avalonia 原生 `DragDrop.DoDragDrop`(OS 级拖拽)
+- 手动实现 `PointerPressed/Moved/Released` 事件
+- 规则集面板内联在按钮下方
+
+### 当前问题
+1. **OS DragDrop 触摸体验差**:触摸屏上拖拽不流畅,需要长按才能触发
+2. **无拖拽预览**:拖拽时看不到被拖动的元素
+3. **布局不够直观**:SettingsExpander 嵌套过深,按钮池和行列表混在一起
+4. **行操作按钮拥挤**:规则集、插入行、删除行按钮挤在一行
+5. **规则集面板占用空间大**:展开后挤压其他按钮
+
+### ClassIsland ComponentsSettingsPage 参考架构
+- **行列表**:`ListBox` + 垂直排列,每行内嵌水平 `ListBox`(`VirtualizingStackPanel`)
+- **组件库**:`ListBox` + `WrapPanel`(`WrapPanelAutoResizeBehavior`),标记为 `drag-source`
+- **拖拽**:`AdvancedManagedContextDragBehavior`(进程内拖拽)+ `ManagedContextDropBehavior`
+- **拖拽把手**:`TouchDragThumb`(触摸模式自动显示)
+- **拖拽预览**:`DragPreviewService`(半透明跟随窗口)
+- **设置面板**:`TabControl`(组件库 Tab + 组件设置 Tab + 高级设置 Tab + 行设置 Tab)
+- **行操作**:选中行时在右侧浮出操作按钮(主行标记、通知标记、插入行、删除行)
+
+## 设计方案
+
+### 1. 整体布局结构
+
+```
+┌─────────────────────────────────────────────────┐
+│ 方案管理 + 显示开关(保持现有设计) │
+├─────────────────────────────────────────────────┤
+│ 提示文字:"以下按钮将显示在悬浮窗上..." │
+├─────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────┐ │
+│ │ 行 1: [拖拽] [按钮A] [按钮B] [按钮C] [操作] │ │ ← ListBox(垂直)
+│ │ 行 2: [拖拽] [按钮D] [按钮E] [操作] │ │
+│ └─────────────────────────────────────────────┘ │
+├─────────────── GridSplitter ────────────────────┤
+│ ┌──────────┬──────────┬──────────┐ │
+│ │ 组件库 │ 按钮设置 │ 行设置 │ │ ← TabControl
+│ ├──────────┼──────────┼──────────┤ │
+│ │ [拖拽] 组件1 │ 缩放: ──●── │ 规则集: │ │
+│ │ [拖拽] 组件2 │ 图标: ──●── │ 透明度: │ │
+│ │ [拖拽] 组件3 │ 透明: ──●── │ ... │ │
+│ └──────────┴──────────┴──────────┘ │
+├─────────────────────────────────────────────────┤
+│ 外观设置(SettingsExpander,保持现有) │
+│ 层级设置(SettingsExpander,保持现有) │
+│ 规则集设置(SettingsExpander,保持现有) │
+└─────────────────────────────────────────────────┘
+```
+
+### 2. 行列表区域
+
+**实现方式**:`ListBox` + `DataTemplate`
+
+参照 ClassIsland ComponentsSettingsPage 的行列表设计:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**行内按钮模板**:
+```xml
+
+
+
+
+
+
+
+```
+
+### 3. 组件库区域
+
+**实现方式**:`ListBox` + `WrapPanel`(标记为 `drag-source`)
+
+参照 ClassIsland 的组件库设计:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 4. 拖拽系统
+
+**核心组件**(均来自 ClassIsland.Core,无需自研):
+
+| 组件 | 用途 |
+|------|------|
+| `AdvancedManagedContextDragBehavior` | 进程内拖拽发起,支持触摸和鼠标 |
+| `ManagedContextDropBehavior` | 进程内拖拽接收 |
+| `ManagedDragDropService` | 拖拽上下文管理(单例) |
+| `DragPreviewService` | 拖拽预览窗口管理 |
+| `TouchDragThumb` | 触摸模式自动显示的拖拽把手 |
+| `AdvancedItemDragBehavior` | 行级拖拽排序 |
+
+**拖拽数据格式**:
+
+创建 `FloatingWindowButtonDragData` 类,参照 ClassIsland 的 `EditableComponentsListBoxDragData`:
+
+```csharp
+public class FloatingWindowButtonDragData
+{
+ public FloatingTriggerItem Item { get; set; }
+ public ObservableCollection SourceCollection { get; set; }
+
+ public static FloatingWindowButtonDragData Create(FloatingTriggerItem item, ObservableCollection source)
+ => new() { Item = item, SourceCollection = source };
+}
+```
+
+**DropHandler**:
+
+创建 `FloatingWindowDropHandler` 实现 `IManagedDropHandler`:
+
+```csharp
+public class FloatingWindowDropHandler : IManagedDropHandler
+{
+ public bool ValidateDrop(object? context, object? data, DragDropEffects effects) { ... }
+ public void Drop(object? context, object? data, DragDropEffects effects) { ... }
+}
+```
+
+### 5. 性能优化
+
+| 优化项 | 措施 | 效果 |
+|--------|------|------|
+| 拖拽预览尺寸 | 预览窗口缩放为原尺寸 70% | 减少渲染面积 |
+| 预览透明度 | `PreviewOpacity = 0.5`(默认 0.65) | 降低混合计算 |
+| 行内按钮面板 | 使用 `VirtualizingStackPanel` | 虚拟化减少渲染 |
+| 禁用过渡动画 | 拖拽把手 `Transitions` 设为空 | 减少动画计算 |
+| 预览渲染方式 | 使用 `VisualBrush` 截图而非克隆控件树 | 减少控件实例化 |
+
+### 6. TabControl 设置面板
+
+**Tab 1 - 组件库**:可用按钮列表(WrapPanel + drag-source)
+
+**Tab 2 - 按钮设置**:选中按钮的规则集和配置
+- 规则集开关 + RulesetControl
+- 满足规则时隐藏 ToggleSwitch
+
+**Tab 3 - 行设置**:选中行的规则集和配置
+- 行可见性 ToggleSwitch
+- 行规则集开关 + RulesetControl
+- 满足规则时隐藏 ToggleSwitch
+
+### 7. 需要修改的文件
+
+| 文件 | 变更内容 |
+|------|---------|
+| `FloatingWindowEditorSettingsPage.axaml` | 完全重写布局:ListBox 行列表 + TabControl 设置面板 |
+| `FloatingWindowEditorSettingsPage.axaml.cs` | 重写拖拽逻辑,移除 OS DragDrop,改用 ManagedDragDrop;添加 DropHandler |
+| `SystemToolsSettingsViewModel.cs` | 添加 SelectedFloatingTriggerRow、SelectedFloatingTriggerItem 属性;添加 FloatingWindowDropHandler;适配新拖拽接口 |
+| `FloatingTriggerItem.cs` | 添加 IconSource 属性(FluentIconSource 类型,供 IconSourceElement 使用) |
+| `FloatingTriggerRow.cs` | 添加 IsSelected 相关属性 |
+
+### 8. 不变的部分
+
+以下部分保持现有设计不变:
+- 方案管理 + 显示开关(顶部栏)
+- 外观设置(SettingsExpander)
+- 层级设置(SettingsExpander)
+- 规则集设置(SettingsExpander)
+- FloatingWindowService.cs(悬浮窗服务逻辑不变)
+- FloatingWindowProfile.cs(配置数据模型不变)
+- 所有配置保存逻辑不变
+
+### 9. 实施步骤
+
+1. **添加拖拽数据类和 DropHandler**
+ - 创建 `FloatingWindowButtonDragData`
+ - 创建 `FloatingWindowDropHandler`
+
+2. **修改 ViewModel**
+ - 添加选中行/选中按钮属性
+ - 添加 DropHandler 实例
+ - 修改 `MoveFloatingTrigger`/`AddTriggerFromPool` 适配新拖拽接口
+
+3. **重写 XAML 布局**
+ - 行列表改为 ListBox + TouchDragThumb
+ - 按钮池改为 TabControl 内的 ListBox + WrapPanel
+ - 按钮设置/行设置改为 TabControl 内的面板
+
+4. **重写 code-behind 拖拽逻辑**
+ - 移除所有 `DragDrop.DoDragDrop` 相关代码
+ - 移除手动 `PointerPressed/Moved/Released` 事件处理
+ - 改用 `AdvancedManagedContextDragBehavior` + `ManagedContextDropBehavior`
+
+5. **性能调优**
+ - 设置拖拽预览参数
+ - 验证虚拟化面板工作正常
+
+### 10. 验证标准
+
+- [ ] 触摸屏上拖拽按钮流畅(无卡顿)
+- [ ] 拖拽预览正常显示(半透明跟随)
+- [ ] 从组件库拖拽按钮到行内正常工作
+- [ ] 行内按钮拖拽排序正常
+- [ ] 行排序(上下移动行)正常
+- [ ] 点击组件库按钮可添加到行
+- [ ] 删除按钮后正确回到组件库
+- [ ] 规则集设置面板正常工作
+- [ ] 配置保存及时性不变
+- [ ] 在低性能设备上拖拽预览不卡顿
diff --git a/Actions/BackgroundPlayAudioAction.cs b/Actions/BackgroundPlayAudioAction.cs
index 3c20efd..af53882 100644
--- a/Actions/BackgroundPlayAudioAction.cs
+++ b/Actions/BackgroundPlayAudioAction.cs
@@ -6,7 +6,6 @@
using System;
using System.IO;
using System.Threading.Tasks;
-using ClassIsland.Shared;
using SystemTools.Settings;
namespace SystemTools.Actions;
diff --git a/Actions/SetVolume.cs b/Actions/SetVolume.cs
index 1a01d7b..f783166 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 ca5fab4..1bb98f8 100644
--- a/Actions/ShowFloatingWindowAction.cs
+++ b/Actions/ShowFloatingWindowAction.cs
@@ -24,8 +24,21 @@ protected override async Task OnInvoke()
try
{
var shouldShow = Settings.ShowFloatingWindow;
- GlobalConstants.MainConfig!.Data.ShowFloatingWindow = shouldShow;
- GlobalConstants.MainConfig.Save();
+ var config = GlobalConstants.MainConfig?.Data;
+
+ // 如果没有可用的悬浮窗组件,则强制隐藏且不允许显示
+ if (_floatingWindowService.Entries.Count == 0)
+ {
+ shouldShow = false;
+ _logger.LogDebug("没有可用的悬浮窗组件,强制隐藏悬浮窗");
+ }
+
+ 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 0000000..f641496
--- /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 0000000..1382ece
--- /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 0000000..4f87052
--- /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 3dac4fb..86fef99 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 0000000..9a04569
--- /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 0000000..3a4f88a
--- /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 0000000..caa0895
--- /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 c0e0eb8..313af51 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;
@@ -330,6 +331,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 0000000..64f157a
--- /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 f3705b4..8399468 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;
@@ -82,6 +82,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 45fc118..40124c4 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 5efb792..fe6a92a 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 9c32567..2b4a879 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 47baf0d..b4f655e 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 498106e..a2f9db1 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 888b3c3..75e4c45 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 592354d..0f970e5 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 0000000..e0ce4fb
--- /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 0000000..c6692fd
--- /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 0000000..29127e7
--- /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 bf36781..02b4602 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 e0bd5b5..18b7be3 100644
--- a/Plugin.cs
+++ b/Plugin.cs
@@ -48,7 +48,6 @@ public class Plugin : PluginBase
{
private ILogger? _logger;
private NativeMenuItem? _toggleFloatingWindowMenuItem;
- private int _toggleMenuRegisterRetryCount;
private bool _faceRecognitionRegistered = false;
private bool _ffmpegDisabledDueToMissingDependency;
private bool _faceRecognitionDisabledDueToMissingDependency;
@@ -70,6 +69,7 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s
services.AddLogging();
services.AddSingleton(GlobalConstants.MainConfig);
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -77,7 +77,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())
{
@@ -94,7 +94,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();
}
@@ -109,11 +109,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)
{
@@ -283,6 +286,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");
}
// 其他工具
@@ -522,7 +531,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);
@@ -821,6 +832,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)
{
@@ -854,7 +871,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)
{
@@ -929,13 +946,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)
@@ -974,7 +996,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;
}
@@ -989,7 +1011,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 0f22fa4..39a7228 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();
});
}
@@ -155,8 +166,10 @@ public void UnregisterTrigger(FloatingWindowTrigger trigger)
public void UpdateWindowState()
{
+ if (_isStopped) return;
Dispatcher.UIThread.Post(() =>
{
+ if (_isStopped) return;
ApplyVisibility();
RefreshLayerRecheckMode();
RecheckWindowLayer();
@@ -167,8 +180,10 @@ public void UpdateWindowState()
private void NotifyEntriesChanged()
{
EntriesChanged?.Invoke(this, EventArgs.Empty);
+ if (_isStopped) return;
Dispatcher.UIThread.Post(() =>
{
+ if (_isStopped) return;
ApplyVisibility();
RecheckWindowLayer();
RefreshWindowButtons();
@@ -221,9 +236,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;
}
@@ -232,10 +273,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,
@@ -243,7 +284,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
}
@@ -258,10 +299,7 @@ private void EnsureWindow()
if (!_allowWindowClose)
{
e.Cancel = true;
- if (_window is { IsVisible: false })
- {
- _window.Show();
- }
+ // 不在 Closing 事件中调用 Show(),窗口可能处于关闭过程中
}
};
_window.PropertyChanged += OnWindowPropertyChanged;
@@ -284,7 +322,7 @@ private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEven
private void RestoreWindowFromMinimized()
{
- if (_window == null || _restoringFromMinimized)
+ if (_window == null || _restoringFromMinimized || _isStopped)
{
return;
}
@@ -295,17 +333,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
{
@@ -321,24 +368,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;
+ }
+ }
}
}
@@ -349,10 +595,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
@@ -363,7 +610,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,
@@ -382,7 +629,7 @@ private void RefreshWindowButtons()
_stackPanel.Children.Clear();
- if (_isTouchDeviceDetected)
+ if (_isTouchDeviceDetected || profile.FloatingWindowDragHandleAlwaysVisible)
{
_touchDragHandle = CreateTouchDragHandle(scale, contentForeground);
_stackPanel.Children.Add(_touchDragHandle);
@@ -392,8 +639,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,
@@ -461,11 +715,11 @@ private void RefreshWindowButtons()
}
else
{
- // 保持自动布局,允许文本变更/缩放后重新测量自然宽度。
button.Width = double.NaN;
}
- button.LayoutUpdated += (_, _) =>
+ EventHandler? layoutUpdatedHandler = null;
+ layoutUpdatedHandler = (_, _) =>
{
if (entry.IsRevertStyleActive)
{
@@ -476,8 +730,10 @@ private void RefreshWindowButtons()
if (width > 0)
{
_buttonWidthCache[entry.ButtonId] = width;
+ button.LayoutUpdated -= layoutUpdatedHandler;
}
};
+ button.LayoutUpdated += layoutUpdatedHandler;
button.PointerPressed += (_, e) =>
{
@@ -497,61 +753,45 @@ private void RefreshWindowButtons()
rowPanel.Children.Add(button);
}
- if (rowPanel.Children.Count > 0)
- {
- _stackPanel.Children.Add(rowPanel);
- }
+ _stackPanel.Children.Add(rowPanel);
+
+ rowIndex++;
}
}
private List> GetOrderedRows()
{
- var values = _entries.Values.ToDictionary(x => x.ButtonId, x => x);
- var order = _configHandler.Data.FloatingWindowButtonOrder ?? [];
+ var profile = _profileManager.CurrentProfile;
+ var validButtonIds = _entries.Values.Select(x => x.ButtonId).ToHashSet();
- var orderedIds = values.Keys
- .OrderBy(id =>
- {
- var index = order.IndexOf(id);
- return index < 0 ? int.MaxValue : index;
- })
- .ThenBy(id => id)
- .ToList();
+ // 清理不存在的按钮ID
+ if (profile.PruneInvalidButtonIds(validButtonIds))
+ {
+ _profileManager.SaveProfile();
+ }
+
+ var values = _entries.Values
+ .Where(x => !_rulesetHiddenButtons.Contains(x.ButtonId))
+ .ToDictionary(x => x.ButtonId, x => x);
- 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;
}
@@ -829,7 +1069,6 @@ private IntPtr OnLowLevelMouse(int nCode, IntPtr wParam, IntPtr lParam)
}
else if (message == WmLButtonDown || message == WmRButtonDown)
{
- // 仅在明确的鼠标点击操作时切回鼠标模式,避免触屏后被背景鼠标移动事件自动恢复。
SetTouchInputMode(false);
}
@@ -927,7 +1166,8 @@ private void EnsureWindowPositionVisibleOnStartup()
return;
}
- var configured = new PixelPoint(_configHandler.Data.FloatingWindowPositionX, _configHandler.Data.FloatingWindowPositionY);
+ var profile = _profileManager.CurrentProfile;
+ var configured = new PixelPoint(profile.FloatingWindowPositionX, profile.FloatingWindowPositionY);
var rect = GetWindowRect(configured);
var target = IsWindowInsideAnyScreen(rect) ? ClampToVisibleScreen(configured) : GetCenteredPositionOnPrimaryScreen();
@@ -937,23 +1177,24 @@ private void EnsureWindowPositionVisibleOnStartup()
private void SavePosition(PixelPoint position, bool forceSave = false)
{
+ var profile = _profileManager.CurrentProfile;
var changed = false;
- if (_configHandler.Data.FloatingWindowPositionX != position.X)
+ if (profile.FloatingWindowPositionX != position.X)
{
- _configHandler.Data.FloatingWindowPositionX = position.X;
+ profile.FloatingWindowPositionX = position.X;
changed = true;
}
- if (_configHandler.Data.FloatingWindowPositionY != position.Y)
+ if (profile.FloatingWindowPositionY != position.Y)
{
- _configHandler.Data.FloatingWindowPositionY = position.Y;
+ profile.FloatingWindowPositionY = position.Y;
changed = true;
}
if (forceSave || changed)
{
- _configHandler.Save();
+ _profileManager.SaveProfile();
}
}
@@ -987,7 +1228,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;
@@ -1071,7 +1313,7 @@ private void RemoveReorderHook()
private void OnLayerRecheck50MsTimerTick(object? sender, EventArgs e)
{
- if (_configHandler.Data.FloatingWindowLayerRecheckMode == 2)
+ if (_profileManager.CurrentProfile.FloatingWindowLayerRecheckMode == 2)
{
RecheckWindowLayer();
}
@@ -1079,7 +1321,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();
}
@@ -1093,7 +1335,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)
@@ -1126,7 +1368,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);
@@ -1137,9 +1379,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 55e2c3d..6976a4d 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 0000000..cf2c251
--- /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 0000000..2dc3579
--- /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 0000000..0c20a25
--- /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 b77c3b8..00ebec4 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 d6a4281..e2fad1b 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">
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ IsExpanded="False">
+ Value="{Binding ViewModel.CurrentFloatingWindowProfile.FloatingWindowScale, Mode=TwoWay}" />
-
@@ -68,16 +311,16 @@
Maximum="50"
TickFrequency="1"
IsSnapToTickEnabled="True"
- Value="{Binding ViewModel.Settings.FloatingWindowIconSize, Mode=TwoWay}" />
+ Value="{Binding ViewModel.CurrentFloatingWindowProfile.FloatingWindowIconSize, Mode=TwoWay}" />
-
@@ -87,16 +330,16 @@
Maximum="25"
TickFrequency="1"
IsSnapToTickEnabled="True"
- Value="{Binding ViewModel.Settings.FloatingWindowTextSize, Mode=TwoWay}" />
+ Value="{Binding ViewModel.CurrentFloatingWindowProfile.FloatingWindowTextSize, Mode=TwoWay}" />
-
@@ -106,8 +349,8 @@
Maximum="100"
TickFrequency="1"
IsSnapToTickEnabled="True"
- Value="{Binding ViewModel.Settings.FloatingWindowOpacity, Mode=TwoWay}" />
-
+
@@ -136,18 +379,26 @@
Content="阴影效果"
Description="悬浮窗阴影效果">
-
+
-
+
+
+
+
+
+
+
+ IsExpanded="False">
-
+
@@ -161,7 +412,7 @@
Description="在什么时候重新设置悬浮窗层级。请注意,较高的频率可能会产生较高的性能占用并导致悬浮窗闪烁。">
+ SelectedIndex="{Binding ViewModel.CurrentFloatingWindowProfile.FloatingWindowLayerRecheckMode, Mode=TwoWay}">
窗口层级变化时
前台窗口变化时
@@ -191,91 +442,25 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/SettingsPage/FloatingWindowEditorSettingsPage.axaml.cs b/SettingsPage/FloatingWindowEditorSettingsPage.axaml.cs
index 82ce436..e886869 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,33 @@ 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;
+ // ===== 拖拽状态(已移除——拖拽现在直接在 PointerPressed 中启动) =====
+
+ // ===== 规则集 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,232 +74,613 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e
return;
}
+ // 页面卸载时确保所有配置(包括规则集)都已保存
+ IAppHost.GetService().ProfileManager.SaveProfile();
+
+ ViewModel.CurrentFloatingWindowProfile.PropertyChanged -= OnProfilePropertyChanged;
ViewModel.Settings.PropertyChanged -= OnSettingsPropertyChanged;
+ ViewModel.ProfileChanged -= OnViewModelProfileChanged;
+
+ UnregisterHidingRulesEvents();
+
ViewModel.Dispose();
_isDisposed = true;
}
- private void OnSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ private void OnProfilePropertyChanged(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(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();
}
}
- private void OnFloatingWindowConfigChanged(object? sender, RoutedEventArgs e)
+ ///
+ /// 重新注册 Profile 属性变更事件监听(切换方案后需要重新注册)
+ ///
+ public void ReattachProfilePropertyChanged()
+ {
+ ViewModel.CurrentFloatingWindowProfile.PropertyChanged -= OnProfilePropertyChanged;
+ ViewModel.CurrentFloatingWindowProfile.PropertyChanged += OnProfilePropertyChanged;
+
+ // 重新注册悬浮窗规则集变更监听
+ UnregisterHidingRulesEvents();
+ RegisterHidingRulesEvents();
+ }
+
+ private void RegisterHidingRulesEvents()
{
- if (!ViewModel.HasFloatingTriggerEntries)
+ if (ViewModel.Settings.FloatingWindowRuleset is INotifyPropertyChanged hidingRules)
{
- ViewModel.Settings.ShowFloatingWindow = false;
+ hidingRules.PropertyChanged += OnHidingRulesPropertyChanged;
}
+ }
- ViewModel.RefreshFloatingTriggers();
- IAppHost.GetService().UpdateWindowState();
+ private void UnregisterHidingRulesEvents()
+ {
+ if (ViewModel.Settings.FloatingWindowRuleset is INotifyPropertyChanged hidingRules)
+ {
+ hidingRules.PropertyChanged -= OnHidingRulesPropertyChanged;
+ }
}
- private Point? _floatingDragStartPoint;
- private Border? _floatingDragSourceBorder;
+ private void OnSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ 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 OnAddFloatingTriggerRowClick(object? sender, RoutedEventArgs e)
+ private void OnViewModelProfileChanged(object? sender, EventArgs e)
{
- ViewModel.AddFloatingTriggerRow();
+ ReattachProfilePropertyChanged();
}
- private void OnRemoveFloatingTriggerRowClick(object? sender, RoutedEventArgs e)
+ private void OnHidingRulesPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- if (sender is not Control { DataContext: FloatingTriggerRow row })
+ GlobalConstants.MainConfig?.Save();
+ }
+
+ private void OnFloatingWindowVisibleToggleChanged(object? sender, RoutedEventArgs e)
+ {
+ if (sender is not ToggleSwitch toggle)
{
return;
}
- if (ViewModel.FloatingTriggerRows.Count <= 1)
+ 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)
{
- return;
+ toggle.IsChecked = shouldShow;
}
- _ = ViewModel.RemoveFloatingTriggerRow(row);
+ GlobalConstants.MainConfig?.Save();
+ service.UpdateWindowState();
}
- private void OnFloatingTriggerItemPointerPressed(object? sender, PointerPressedEventArgs e)
+ private void OnFloatingWindowProfileSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
- if (sender is not Border border || !e.GetCurrentPoint(border).Properties.IsLeftButtonPressed)
+ if (sender is not ComboBox comboBox || comboBox.SelectedItem is not string profileName)
{
return;
}
- _floatingDragSourceBorder = border;
- _floatingDragStartPoint = e.GetPosition(border);
- e.Handled = e.Pointer.Type is PointerType.Touch or PointerType.Pen;
+ ViewModel.SwitchFloatingWindowProfile(profileName);
}
- private void OnFloatingTriggerItemPointerReleased(object? sender, PointerReleasedEventArgs e)
+ private void OnToggleFloatingWindowProfileClick(object? sender, RoutedEventArgs e)
{
- _floatingDragSourceBorder = null;
- _floatingDragStartPoint = null;
+ IAppHost.GetService().ToggleWindowProfile();
+ ViewModel.RefreshFloatingWindowProfiles();
+ ViewModel.RefreshFloatingTriggers();
}
- private async void OnFloatingTriggerItemPointerMoved(object? sender, PointerEventArgs e)
+ private void OnAddFloatingWindowProfileClick(object? sender, RoutedEventArgs e)
{
- if (sender is not Border border || _floatingDragSourceBorder != border || _floatingDragStartPoint == null)
+ ViewModel.AddFloatingWindowProfile();
+ }
+
+ private void OnRemoveCurrentProfileClick(object? sender, RoutedEventArgs e)
+ {
+ var currentName = ViewModel.SelectedFloatingWindowProfile;
+ if (string.IsNullOrWhiteSpace(currentName))
{
return;
}
- if (!e.GetCurrentPoint(border).Properties.IsLeftButtonPressed)
+ ViewModel.RemoveFloatingWindowProfile(currentName);
+ }
+
+ private void OnInsertRowBelowClick(object? sender, RoutedEventArgs e)
+ {
+ if (sender is not Control control)
{
return;
}
- var now = e.GetPosition(border);
- if (Math.Abs(now.X - _floatingDragStartPoint.Value.X) + Math.Abs(now.Y - _floatingDragStartPoint.Value.Y) < 4)
+ var row = control.DataContext as FloatingTriggerRow;
+ if (row == null)
{
return;
}
- if (border.Tag is not string buttonId || string.IsNullOrWhiteSpace(buttonId))
+ var index = ViewModel.FloatingTriggerRows.IndexOf(row);
+ if (index < 0)
{
return;
}
- var data = new DataObject();
- data.Set("FloatingTriggerButtonId", buttonId);
+ ViewModel.InsertFloatingTriggerRow(index + 1);
+ }
- _floatingDragSourceBorder = null;
- _floatingDragStartPoint = null;
- await DragDrop.DoDragDrop(e, data, DragDropEffects.Move);
- e.Handled = e.Pointer.Type is PointerType.Touch or PointerType.Pen;
+ private void OnAddFloatingTriggerRowClick(object? sender, RoutedEventArgs e)
+ {
+ ViewModel.AddFloatingTriggerRow();
}
- private static bool TryGetDragButtonId(DragEventArgs e, out string buttonId)
+ private void OnRemoveFloatingTriggerRowClick(object? sender, RoutedEventArgs e)
{
- buttonId = string.Empty;
- if (!e.Data.Contains("FloatingTriggerButtonId"))
+ if (sender is not Control { DataContext: FloatingTriggerRow row })
+ {
+ return;
+ }
+
+ if (ViewModel.FloatingTriggerRows.Count <= 1)
{
- return false;
+ return;
}
- buttonId = e.Data.Get("FloatingTriggerButtonId") as string ?? string.Empty;
- return !string.IsNullOrWhiteSpace(buttonId);
+ _ = ViewModel.RemoveFloatingTriggerRow(row);
}
- private int GetRowIndexFromControl(Control? control)
+ private void OnRemoveTriggerFromRowClick(object? sender, RoutedEventArgs e)
{
- var current = control;
- while (current != null)
+ if (sender is not Button button || button.Tag is not string buttonId)
{
- if (current.DataContext is FloatingTriggerRow row)
- {
- return ViewModel.FloatingTriggerRows.IndexOf(row);
- }
-
- current = current.GetVisualParent() as Control;
+ return;
}
- return -1;
+ ViewModel.RemoveTriggerToPool(buttonId);
}
- private int GetRowInsertIndex(Control sender, FloatingTriggerRow row, DragEventArgs e)
+ // ===== 规则集 Drawer(参照 ClassIsland,含 IsVisible/HideOnRule 开关) =====
+
+ ///
+ /// 按钮规则集按钮点击:打开该按钮的规则集 Drawer
+ ///
+ private void OnButtonRulesetClick(object? sender, RoutedEventArgs e)
{
- if (row.Buttons.Count == 0)
+ if (sender is not Button button || button.Tag is not string buttonId)
+ return;
+
+ // 在所有行中查找该按钮
+ var item = ViewModel.FloatingTriggerRows
+ .SelectMany(r => r.Buttons)
+ .FirstOrDefault(b => b.ButtonId == buttonId);
+ if (item == null) return;
+
+ ViewModel.SelectedFloatingTriggerItem = item;
+ _currentRulesetTarget = RulesetTargetType.Button;
+ _currentButtonTarget = item;
+ _currentRowTarget = null;
+
+ OpenRulesetDrawer(item.Config.HidingRules, item.Config.IsVisible, item.Config.HideOnRule);
+ }
+
+ ///
+ /// 行规则集按钮点击:打开该行的规则集 Drawer
+ ///
+ private void OnRowRulesetClick(object? sender, RoutedEventArgs e)
+ {
+ if (sender is not Control control)
+ return;
+
+ // 通过 DataContext 获取所属行
+ var row = control.DataContext as FloatingTriggerRow;
+ if (row == null) return;
+
+ ViewModel.SelectedFloatingTriggerRow = row;
+ _currentRulesetTarget = RulesetTargetType.Row;
+ _currentRowTarget = row;
+ _currentButtonTarget = null;
+
+ OpenRulesetDrawer(row.RowRuleset.HidingRules, row.RowRuleset.IsVisible, row.RowRuleset.HideOnRule);
+ }
+
+ 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
{
- return 0;
- }
+ 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");
+ }
- var pointer = e.GetPosition(sender);
- var itemBorders = sender.GetVisualDescendants()
- .OfType()
- .Where(x => x.DataContext is FloatingTriggerItem)
- .OrderBy(x => x.TranslatePoint(new Point(0, 0), sender)?.X ?? double.MaxValue)
- .ToList();
+ private void OnDrawerIsVisibleChanged(object? sender, RoutedEventArgs e)
+ {
+ var value = _drawerIsVisibleToggle?.IsChecked == true;
- for (var i = 0; i < itemBorders.Count; i++)
+ switch (_currentRulesetTarget)
{
- var topLeft = itemBorders[i].TranslatePoint(new Point(0, 0), sender);
- if (topLeft == null)
- {
- continue;
- }
+ case RulesetTargetType.Button when _currentButtonTarget != null:
+ _currentButtonTarget.Config.IsVisible = value;
+ break;
+ case RulesetTargetType.Row when _currentRowTarget != null:
+ _currentRowTarget.RowRuleset.IsVisible = value;
+ break;
+ }
- var center = topLeft.Value.X + itemBorders[i].Bounds.Width / 2;
- if (pointer.X <= center)
- {
- return i;
- }
+ 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;
}
- return row.Buttons.Count;
+ IAppHost.GetService().ProfileManager.SaveProfile();
+ IAppHost.GetService().UpdateWindowState();
}
- private void OnFloatingTriggerRowDragOver(object? sender, DragEventArgs e)
+ // ===== 选中状态处理 =====
+
+ private void OnRowSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
- e.DragEffects = TryGetDragButtonId(e, out _) ? DragDropEffects.Move : DragDropEffects.None;
- e.Handled = true;
+ if (sender is ListBox listBox && listBox.SelectedItem is FloatingTriggerRow row)
+ {
+ ViewModel.SelectedFloatingTriggerRow = row;
+ }
}
- private void OnFloatingTriggerRowDrop(object? sender, DragEventArgs e)
+ private void OnButtonSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
- if (!TryGetDragButtonId(e, out var buttonId) || sender is not Control senderControl)
+ if (sender is ListBox listBox && listBox.SelectedItem is FloatingTriggerItem item)
{
- return;
+ ViewModel.SelectedFloatingTriggerItem = item;
}
+ }
- var rowIndex = GetRowIndexFromControl(senderControl);
- if (rowIndex < 0)
+ private void OnAvailableItemSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not ListBox listBox || listBox.SelectedItem is not FloatingTriggerItem item)
{
return;
}
- var row = ViewModel.FloatingTriggerRows[rowIndex];
- var insertIndex = GetRowInsertIndex(senderControl, row, e);
- ViewModel.MoveFloatingTrigger(buttonId, rowIndex, insertIndex);
+ // 先清除选中状态,避免移除项时选择模型与集合冲突(ArgumentOutOfRangeException)
+ var buttonId = item.ButtonId;
+ listBox.SelectedItem = null;
+
+ // 延迟执行添加操作,确保 SelectionChanged 事件处理完成后再修改集合
+ // 否则 AvailableFloatingTriggerItems.Remove 会在选择模型迭代期间触发集合变更,导致 ArgumentOutOfRangeException
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ // 点击组件库项:添加到第一行末尾
+ if (ViewModel.FloatingTriggerRows.Count == 0)
+ {
+ ViewModel.AddFloatingTriggerRow();
+ }
+ ViewModel.AddTriggerFromPool(buttonId, 0, ViewModel.FloatingTriggerRows[0].Buttons.Count);
+ });
}
- private void OnFloatingTriggerItemDragOver(object? sender, DragEventArgs e)
+ // ===== 拖拽处理(标准 Avalonia DragDrop) =====
+
+ ///
+ /// 行拖拽把手按下:开始行拖拽
+ ///
+ private void OnRowDragThumbPointerPressed(object? sender, PointerPressedEventArgs e)
{
- e.DragEffects = TryGetDragButtonId(e, out _) ? DragDropEffects.Move : DragDropEffects.None;
+ if (sender is not Control control) return;
+
+ // 通过 DataContext 获取所属行
+ var row = control.DataContext as FloatingTriggerRow;
+ if (row == null) return;
+
+ e.Handled = true;
+
+ // 直接在 PointerPressed 中启动拖拽(TouchDragThumb 可能消费 PointerMoved 事件,
+ // 导致页面级别的 OnPointerMoved 收不到,因此不依赖 OnPointerMoved)
+ var data = new DataObject();
+ data.Set("FloatingWindowRow", row);
+ DragDrop.DoDragDrop(e, data, DragDropEffects.Move);
+ }
+
+ ///
+ /// 行内按钮按下:开始按钮拖拽
+ ///
+ private void OnButtonPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (sender is not Control control || control.DataContext is not FloatingTriggerItem item) return;
+
+ // 通过 ViewModel 查找所属行的 Buttons 集合
+ var row = ViewModel.FloatingTriggerRows.FirstOrDefault(r => r.Buttons.Contains(item));
+ if (row == null) return;
+
e.Handled = true;
+
+ // 直接启动拖拽(不依赖 OnPointerMoved,原因同行拖拽)
+ var data = new DataObject();
+ data.Set("FloatingWindowButtonId", item.ButtonId);
+ data.Set("FloatingWindowButtonSource", row.Buttons!);
+ DragDrop.DoDragDrop(e, data, DragDropEffects.Move);
}
- private void OnFloatingTriggerItemDrop(object? sender, DragEventArgs e)
+ // ===== 行区域拖放处理 =====
+
+ private void OnRowDropBorderDragOver(object? sender, DragEventArgs e)
{
- if (sender is not Border border || border.DataContext is not FloatingTriggerItem targetItem)
+ if (e.Data.Contains("FloatingWindowButtonId") || e.Data.Contains("FloatingWindowRow"))
{
- return;
+ e.DragEffects = DragDropEffects.Move;
+ e.Handled = true;
+ }
+ else
+ {
+ e.DragEffects = DragDropEffects.None;
}
+ }
- if (!TryGetDragButtonId(e, out var buttonId))
+ private void OnRowDropBorderDrop(object? sender, DragEventArgs e)
+ {
+ e.Handled = true;
+
+ // 处理行拖拽排序
+ if (e.Data.Contains("FloatingWindowRow"))
{
+ var sourceRow = e.Data.Get("FloatingWindowRow") as FloatingTriggerRow;
+ if (sourceRow == null) return;
+
+ var rowTargetIndex = FindTargetRowIndex(e, sender as Control);
+ if (rowTargetIndex < 0) return;
+
+ var sourceIndex = ViewModel.FloatingTriggerRows.IndexOf(sourceRow);
+ if (sourceIndex < 0 || sourceIndex == rowTargetIndex) return;
+
+ // 移动行
+ ViewModel.FloatingTriggerRows.RemoveAt(sourceIndex);
+ if (rowTargetIndex > sourceIndex) rowTargetIndex--;
+ ViewModel.FloatingTriggerRows.Insert(rowTargetIndex, sourceRow);
+
+ // 重新计算行索引
+ for (int i = 0; i < ViewModel.FloatingTriggerRows.Count; i++)
+ {
+ ViewModel.FloatingTriggerRows[i].RowIndex = i + 1;
+ }
+
+ ViewModel.PersistFloatingTriggerRows();
return;
}
- var rowIndex = GetRowIndexFromControl(border);
- if (rowIndex < 0)
+ // 处理按钮拖拽
+ if (!e.Data.Contains("FloatingWindowButtonId")) return;
+
+ var buttonId = e.Data.Get("FloatingWindowButtonId") as string;
+ if (string.IsNullOrEmpty(buttonId)) return;
+
+ var sourceCollection = e.Data.Get("FloatingWindowButtonSource") as ObservableCollection;
+
+ // 确定目标行和位置
+ if (ViewModel.FloatingTriggerRows.Count == 0)
{
- return;
+ ViewModel.AddFloatingTriggerRow();
}
- var row = ViewModel.FloatingTriggerRows[rowIndex];
- var targetIndex = row.Buttons.IndexOf(targetItem);
- if (targetIndex < 0)
+ var targetRowIndex = FindTargetRowIndex(e, sender as Control);
+ if (targetRowIndex < 0) targetRowIndex = 0;
+ targetRowIndex = System.Math.Clamp(targetRowIndex, 0, ViewModel.FloatingTriggerRows.Count - 1);
+ var btnTargetIndex = ViewModel.FloatingTriggerRows[targetRowIndex].Buttons.Count;
+
+ if (sourceCollection == null)
{
- return;
+ // 从组件库拖入
+ ViewModel.AddTriggerFromPool(buttonId, targetRowIndex, btnTargetIndex);
}
+ else
+ {
+ // 从其他行拖入
+ ViewModel.MoveFloatingTrigger(buttonId, targetRowIndex, btnTargetIndex);
+ }
+ }
- var pos = e.GetPosition(border);
- if (pos.X > border.Bounds.Width / 2)
+ ///
+ /// 根据拖放位置确定目标行索引
+ ///
+ private int FindTargetRowIndex(DragEventArgs e, Control? targetControl)
+ {
+ if (targetControl == null) return -1;
+
+ var pos = e.GetPosition(targetControl);
+ if (this.FindControl("ListBoxRows") is not ListBox rowsList)
+ return -1;
+
+ for (int i = 0; i < ViewModel.FloatingTriggerRows.Count; i++)
{
- targetIndex += 1;
+ if (rowsList.ContainerFromIndex(i) is ListBoxItem lbi)
+ {
+ var transform = lbi.TransformToVisual(rowsList);
+ if (transform == null) continue;
+ var itemPos = transform.Value.Transform(new Point(0, 0));
+ var itemBounds = lbi.Bounds;
+ if (pos.Y >= itemPos.Y && pos.Y <= itemPos.Y + itemBounds.Height)
+ {
+ return i;
+ }
+ }
}
- ViewModel.MoveFloatingTrigger(buttonId, rowIndex, targetIndex);
+ return -1;
+ }
+
+ // ===== 行内按钮拖放处理 =====
+
+ private void OnInnerButtonDragOver(object? sender, DragEventArgs e)
+ {
+ if (e.Data.Contains("FloatingWindowButtonId"))
+ {
+ e.DragEffects = DragDropEffects.Move;
+ e.Handled = true;
+ }
+ else
+ {
+ e.DragEffects = DragDropEffects.None;
+ }
+ }
+
+ private void OnInnerButtonDrop(object? sender, DragEventArgs e)
+ {
+ if (!e.Data.Contains("FloatingWindowButtonId")) return;
+
+ var buttonId = e.Data.Get("FloatingWindowButtonId") as string;
+ if (string.IsNullOrEmpty(buttonId)) return;
+
+ var sourceCollection = e.Data.Get("FloatingWindowButtonSource") as ObservableCollection;
+
+ // 通过 DataContext 获取目标行
+ if (sender is not Control targetControl) return;
+ var targetRow = targetControl.DataContext as FloatingTriggerRow;
+ if (targetRow == null) return;
+
+ var targetRowIndex = ViewModel.FloatingTriggerRows.IndexOf(targetRow);
+ if (targetRowIndex < 0) return;
+
+ // 尝试确定精确的插入位置
+ var targetIndex = targetRow.Buttons.Count;
+ if (sender is ListBox listBox)
+ {
+ var pos = e.GetPosition(listBox);
+ for (int i = 0; i < targetRow.Buttons.Count; i++)
+ {
+ if (listBox.ContainerFromIndex(i) is ListBoxItem lbi)
+ {
+ var transform = lbi.TransformToVisual(listBox);
+ if (transform == null) continue;
+ var itemPos = transform.Value.Transform(new Point(0, 0));
+ var itemBounds = lbi.Bounds;
+ if (pos.X >= itemPos.X && pos.X <= itemPos.X + itemBounds.Width / 2)
+ {
+ targetIndex = i;
+ break;
+ }
+ if (pos.X <= itemPos.X + itemBounds.Width && i == targetRow.Buttons.Count - 1)
+ {
+ targetIndex = i + 1;
+ }
+ }
+ }
+ }
+
+ e.Handled = true;
+
+ if (sourceCollection == null)
+ {
+ // 从组件库拖入
+ ViewModel.AddTriggerFromPool(buttonId, targetRowIndex, targetIndex);
+ }
+ else if (!ReferenceEquals(sourceCollection, targetRow.Buttons))
+ {
+ // 跨行移动
+ ViewModel.MoveFloatingTrigger(buttonId, targetRowIndex, targetIndex);
+ }
+ else
+ {
+ // 行内排序
+ var item = targetRow.Buttons.FirstOrDefault(b => b.ButtonId == buttonId);
+ if (item == null) return;
+ var sourceIndex = targetRow.Buttons.IndexOf(item);
+ if (sourceIndex < 0 || sourceIndex == targetIndex) return;
+
+ targetRow.Buttons.Move(sourceIndex, System.Math.Clamp(targetIndex, 0, targetRow.Buttons.Count - 1));
+ ViewModel.PersistFloatingTriggerRows();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/SettingsPage/SystemToolsSettingsPage.axaml.cs b/SettingsPage/SystemToolsSettingsPage.axaml.cs
index 8a2c610..70e37fc 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 b39978b..9ea9c7d 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 键", "常用模拟键"),
@@ -218,6 +249,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)
@@ -257,81 +290,239 @@ 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()
{
var entries = _floatingWindowService.Entries.ToDictionary(x => x.ButtonId, x => x);
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();
}
@@ -343,13 +534,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;
}
@@ -363,9 +569,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);
@@ -396,8 +604,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();
@@ -405,22 +687,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)
@@ -429,6 +757,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/FloatingWindowTriggerConfig.cs b/Triggers/FloatingWindowTriggerConfig.cs
index 91f9f70..30fc70d 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;
}
diff --git a/classisland b/classisland
new file mode 160000
index 0000000..0ea2cc5
--- /dev/null
+++ b/classisland
@@ -0,0 +1 @@
+Subproject commit 0ea2cc550310a7717b8b074862940b13d5534715
diff --git "a/\346\226\260\345\273\272\346\226\207\346\234\254\346\226\207\346\241\243.txt" "b/\346\226\260\345\273\272\346\226\207\346\234\254\346\226\207\346\241\243.txt"
new file mode 100644
index 0000000..ddddd62
--- /dev/null
+++ "b/\346\226\260\345\273\272\346\226\207\346\234\254\346\226\207\346\241\243.txt"
@@ -0,0 +1,41 @@
+ύʱ뱣Ϣ
+TraceID: b273d7f1676845099d6d730214ad9657
+================================
+² ClassIsland ߷ǰ²Ŀ߷⣺
+- SystemTools [SystemTools,2.5.0.106]
+================================
+System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
+ at System.Collections.Generic.List`1.get_Item(Int32 index)
+ at Avalonia.Controls.ItemsSourceView`1.GetAt(Int32 index)
+ at Avalonia.Controls.Selection.SelectedItems`1.GetEnumerator()+MoveNext()
+ at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items)
+ at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source)
+ at Avalonia.Controls.Selection.InternalSelectionModel.OnSelectionChanged(Object sender, SelectionModelSelectionChangedEventArgs e)
+ at Avalonia.Controls.Selection.SelectionModel`1.CommitOperation(Operation operation, Boolean raisePropertyChanged)
+ at Avalonia.Controls.Selection.SelectionModel`1.OnSelectionRemoved(Int32 index, Int32 count, IReadOnlyList`1 deselectedItems)
+ at Avalonia.Controls.Selection.SelectionNodeBase`1.OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
+ at Avalonia.Controls.Selection.SelectionModel`1.OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
+ at Avalonia.Controls.Utils.CollectionChangedEventManager.Entry..OnEvent>g__Notify|6_0(INotifyCollectionChanged incc, NotifyCollectionChangedEventArgs args, WeakReference`1[] listeners)
+ at Avalonia.Utilities.WeakEvent`2.Subscription.OnEvent(Object sender, TEventArgs eventArgs)
+ at System.Collections.ObjectModel.ObservableCollection`1.OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+ at System.Collections.ObjectModel.Collection`1.Remove(T item)
+ at SystemTools.SystemToolsSettingsViewModel.AddTriggerFromPool(String buttonId, Int32 targetRowIndex, Int32 targetIndex) in D:\a\SystemTools\SystemTools\SettingsPage\SystemToolsSettingsViewModel.cs:line 625
+ at SystemTools.FloatingWindowEditorSettingsPage.OnAvailableItemSelectionChanged(Object sender, SelectionChangedEventArgs e) in D:\a\SystemTools\SystemTools\SettingsPage\FloatingWindowEditorSettingsPage.axaml.cs:line 333
+ at Avalonia.Interactivity.EventRoute.RaiseEventImpl(RoutedEventArgs e)
+ at Avalonia.Interactivity.EventRoute.RaiseEvent(Interactive source, RoutedEventArgs e)
+ at Avalonia.Interactivity.Interactive.RaiseEvent(RoutedEventArgs e)
+ at Avalonia.Controls.Primitives.SelectingItemsControl.OnSelectionModelSelectionChanged(Object sender, SelectionModelSelectionChangedEventArgs e)
+ at Avalonia.Controls.Selection.SelectionModel`1.CommitOperation(Operation operation, Boolean raisePropertyChanged)
+ at Avalonia.Controls.Selection.SelectionModelExtensions.BatchUpdateOperation.Dispose()
+ at Avalonia.Controls.Primitives.SelectingItemsControl.UpdateSelection(Int32 index, Boolean select, Boolean rangeModifier, Boolean toggleModifier, Boolean rightButton, Boolean fromFocus)
+ at Avalonia.Controls.Primitives.SelectingItemsControl.UpdateSelection(Control container, Boolean select, Boolean rangeModifier, Boolean toggleModifier, Boolean rightButton, Boolean fromFocus)
+ at Avalonia.Controls.Primitives.SelectingItemsControl.UpdateSelectionFromEventSource(Object eventSource, Boolean select, Boolean rangeModifier, Boolean toggleModifier, Boolean rightButton, Boolean fromFocus)
+ at Avalonia.Controls.ListBox.UpdateSelectionFromPointerEvent(Control source, PointerEventArgs e)
+ at Avalonia.Controls.ListBoxItem.OnPointerReleased(PointerReleasedEventArgs e)
+ at Avalonia.Reactive.LightweightObservableBase`1.PublishNext(T value)
+ at Avalonia.Interactivity.EventRoute.RaiseEventImpl(RoutedEventArgs e)
+ at Avalonia.Interactivity.EventRoute.RaiseEvent(Interactive source, RoutedEventArgs e)
+ at Avalonia.Interactivity.Interactive.RaiseEvent(RoutedEventArgs e)
+ at Avalonia.Input.TouchDevice.ProcessRawEvent(RawInputEventArgs ev)
+ at Avalonia.Controls.TopLevel.<>c.b__150_0(Object state)
+ at Avalonia.Threading.Dispatcher.Send(SendOrPostCallback action, Object arg, Nullable`1 priority)
\ No newline at end of file