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 + + + + + + + + + + + + + + + + + + + - \ 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