mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-25 10:40:57 +08:00
refactor: standardize property names for visibility and mode across components
This commit is contained in:
@@ -1,242 +0,0 @@
|
||||
# ColorPicker Popup 重构方案:TriggerType 与箭头管理
|
||||
|
||||
## 背景
|
||||
|
||||
ColorPicker 已完成从 Flyout 到直接 Popup 的基本迁移(删除了 `AbstractColorPickerFlyout`、`ColorPickerFlyout`、`GradientColorPickerFlyout`),但 TriggerType 的实现和箭头管理做得不好:
|
||||
|
||||
- TriggerType 当前通过简单的 `OnClick`/`OnPointerEntered`/`OnPointerExited` 重写实现,缺少延迟定时器、全局指针追踪、焦点范围检测
|
||||
- 箭头缺少 `IsShowArrowEffective`、`ArrowPosition`、翻转追踪等关键逻辑
|
||||
- `IsLightDismissEnabled` 硬编码为 `True`,应根据 TriggerType 动态设置
|
||||
|
||||
## 参考实现
|
||||
|
||||
- `FlyoutStateHelper.cs` — TriggerType 的完整实现模式(定时器 + InputManager + 范围检测)
|
||||
- `Flyout.cs` — 箭头管理模式(IsShowArrowEffective / ArrowPosition / flip tracking)
|
||||
- `PopupUtils.cs` — 箭头计算工具方法
|
||||
|
||||
## 重构内容
|
||||
|
||||
### 1. TriggerType 重写
|
||||
|
||||
**当前问题**:
|
||||
- Hover 模式没有 `MouseEnterDelay`/`MouseLeaveDelay` 定时器
|
||||
- Hover 模式没有通过 `InputManager.Process` 订阅全局指针移动来检测指针是否在 Popup 内
|
||||
- Focus 模式没有根级 PointerPressed 处理(点击外部关闭)
|
||||
- Focus 模式没有焦点范围检测(`IsFocusWithinFlyoutScope`)
|
||||
- Click 模式没有正确设置 `IsLightDismissEnabled`
|
||||
|
||||
**方案**:在 `AbstractColorPicker` 中内联 `FlyoutStateHelper` 的核心逻辑:
|
||||
|
||||
```csharp
|
||||
// 新增属性
|
||||
public static readonly StyledProperty<int> MouseEnterDelayProperty =
|
||||
AvaloniaProperty.Register<AbstractColorPicker, int>(nameof(MouseEnterDelay), 200);
|
||||
|
||||
public static readonly StyledProperty<int> MouseLeaveDelayProperty =
|
||||
AvaloniaProperty.Register<AbstractColorPicker, int>(nameof(MouseLeaveDelay), 200);
|
||||
|
||||
// 新增字段
|
||||
private DispatcherTimer? _mouseEnterDelayTimer;
|
||||
private DispatcherTimer? _mouseLeaveDelayTimer;
|
||||
private IDisposable? _popupPointerSubscription;
|
||||
private TopLevel? _registeredTopLevel;
|
||||
private bool _isPickerShowing;
|
||||
```
|
||||
|
||||
**Hover 模式实现要点**:
|
||||
1. 监听 `IsPointerOverProperty` 变化(通过 `GetObservable`)
|
||||
2. 指针进入 → 启动 `_mouseEnterDelayTimer`,到期后打开 Popup
|
||||
3. 指针离开 anchor → 检查 Popup 内是否有指针,没有则启动 `_mouseLeaveDelayTimer`
|
||||
4. Popup 打开后 → 通过 `InputManager.Process` 订阅全局指针移动事件
|
||||
5. 全局指针移动 → hit test 判断是否在 flyout scope 内(anchor 或 popup child)
|
||||
6. 离开 scope → 启动关闭定时器;回到 scope → 取消关闭定时器
|
||||
|
||||
**Click 模式实现要点**:
|
||||
1. 设置 `IsLightDismissEnabled = true`(仅 Click 模式需要)
|
||||
2. 通过 `AddHandler(PointerPressedEvent, ..., handledEventsToo: true)` 处理点击
|
||||
3. 点击时 toggle `IsPickerOpen`
|
||||
|
||||
**Focus 模式实现要点**:
|
||||
1. 监听 `GotFocus` → 打开 Popup
|
||||
2. 监听 `LostFocus` → `Dispatcher.Post` 延迟检查焦点是否仍在 flyout scope 内
|
||||
3. Popup 打开后 → 注册根级 `PointerPressedEvent`(Tunnel 路由)
|
||||
4. 根级点击 → 判断点击目标是否在 flyout scope 内,不在则关闭
|
||||
5. Flyout scope = anchor 及其子元素 + popup child 及其逻辑子元素
|
||||
|
||||
**Scope 检测方法**:
|
||||
```csharp
|
||||
private bool IsVisualInPickerScope(Visual visual)
|
||||
{
|
||||
// 在 anchor 内
|
||||
if (visual == this || this.IsVisualAncestorOf(visual))
|
||||
return true;
|
||||
// 在 popup 内
|
||||
if (_popup is { Child: Visual popupChild } &&
|
||||
(visual == popupChild || popupChild.IsLogicalAncestorOf(visual)))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
**生命周期管理**:
|
||||
- `OnAttachedToVisualTree` → `SetupTriggerHandler()`
|
||||
- `OnDetachedFromVisualTree` → 停止所有定时器、取消订阅、清理
|
||||
- `TriggerTypeProperty` 变化 → 重新 `SetupTriggerHandler()`
|
||||
|
||||
**删除当前的**:
|
||||
- `OnClick()` 重写中的 TriggerType 逻辑
|
||||
- `OnPointerEntered()` / `OnPointerExited()` 重写
|
||||
- `_isPointerInPopup` 字段
|
||||
- `HandlePopupPointerEntered` / `HandlePopupPointerExited`
|
||||
- `ClosePickerWithDelayIfNeeded()`
|
||||
- `HandleGotFocus` / `HandleLostFocus` 中的简单实现
|
||||
|
||||
### 2. 箭头管理重写
|
||||
|
||||
**当前问题**:
|
||||
- 没有 `IsShowArrowEffective` 计算(某些 Placement 不支持箭头)
|
||||
- 没有 `ArrowPosition` 属性(箭头方向)
|
||||
- 没有翻转追踪(Popup 被翻转时箭头方向需要跟着翻转)
|
||||
- Theme 模板中 `ArrowDecoratedBox` 没有绑定 `ArrowPosition` 和 `IsShowArrow`
|
||||
|
||||
**方案**:参照 `Flyout.cs` 的模式:
|
||||
|
||||
```csharp
|
||||
// 新增内部属性
|
||||
internal static readonly DirectProperty<AbstractColorPicker, bool> IsShowArrowEffectiveProperty = ...;
|
||||
internal static readonly DirectProperty<AbstractColorPicker, bool> IsPopupHorizontalFlippedProperty = ...;
|
||||
internal static readonly DirectProperty<AbstractColorPicker, bool> IsPopupVerticalFlippedProperty = ...;
|
||||
internal static readonly StyledProperty<ArrowPosition> ArrowPositionProperty =
|
||||
ArrowDecoratedBox.ArrowPositionProperty.AddOwner<AbstractColorPicker>();
|
||||
```
|
||||
|
||||
**箭头计算逻辑**:
|
||||
```csharp
|
||||
private void ConfigureShowArrowEffective()
|
||||
{
|
||||
if (!IsShowArrow)
|
||||
{
|
||||
IsShowArrowEffective = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var placement = Placement;
|
||||
IsShowArrowEffective = PopupUtils.CanEnabledArrow(placement, PlacementAnchor, PlacementGravity);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureArrowPosition()
|
||||
{
|
||||
var placement = Placement;
|
||||
var arrowPosition = PopupUtils.CalculateArrowPosition(placement, PlacementAnchor, PlacementGravity);
|
||||
if (arrowPosition.HasValue)
|
||||
{
|
||||
arrowPosition = ArrowPositionUtils.FlipArrowPosition(
|
||||
arrowPosition.Value, IsPopupHorizontalFlipped, IsPopupVerticalFlipped);
|
||||
SetCurrentValue(ArrowPositionProperty, arrowPosition);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**触发时机**:
|
||||
- `IsShowArrowProperty` / `PlacementProperty` / `PlacementGravityProperty` 变化 → `ConfigureShowArrowEffective()`
|
||||
- `PlacementProperty` / `IsPopupHorizontalFlippedProperty` / `IsPopupVerticalFlippedProperty` 变化 → `ConfigureArrowPosition()`
|
||||
|
||||
**Popup 翻转绑定**(在 `OnApplyTemplate` 中):
|
||||
```csharp
|
||||
this[!IsPopupHorizontalFlippedProperty] = _popup[!PopupControl.IsHorizontalFlippedProperty];
|
||||
this[!IsPopupVerticalFlippedProperty] = _popup[!PopupControl.IsVerticalFlippedProperty];
|
||||
```
|
||||
|
||||
### 3. Theme 模板修复
|
||||
|
||||
**ColorPickerTheme.axaml / GradientColorPickerTheme.axaml**:
|
||||
|
||||
```xml
|
||||
<atom:Popup Name="PART_Popup"
|
||||
WindowManagerAddShadowHint="False"
|
||||
IsLightDismissEnabled="{TemplateBinding IsLightDismissEnabled}"
|
||||
IsMotionEnabled="{TemplateBinding IsMotionEnabled}"
|
||||
ShouldUseOverlayLayer="{TemplateBinding ShouldUseOverlayPopup}"
|
||||
RequestedPlacement="{TemplateBinding Placement}"
|
||||
PlacementGravity="{TemplateBinding PlacementGravity}"
|
||||
IsPointAtCenter="{TemplateBinding IsPointAtCenter}"
|
||||
IsOpen="{TemplateBinding IsPickerOpen, Mode=TwoWay}"
|
||||
MarginToAnchor="{TemplateBinding MarginToAnchor}">
|
||||
<atom:ArrowDecoratedBox Content="{TemplateBinding PickerPresenter}"
|
||||
IsShowArrow="{TemplateBinding IsShowArrowEffective}"
|
||||
ArrowPosition="{TemplateBinding ArrowPosition}"
|
||||
IsMotionEnabled="{TemplateBinding IsMotionEnabled}"/>
|
||||
</atom:Popup>
|
||||
```
|
||||
|
||||
**关键变化**:
|
||||
- `IsLightDismissEnabled` → TemplateBinding(动态,由 TriggerType 决定)
|
||||
- `ShouldUseOverlayLayer` → TemplateBinding(新增公共属性)
|
||||
- 新增 `PlacementGravity`、`IsPointAtCenter` 绑定
|
||||
- `ArrowDecoratedBox` 新增 `IsShowArrow` 和 `ArrowPosition` 绑定
|
||||
|
||||
### 4. 新增公共属性
|
||||
|
||||
```csharp
|
||||
public static readonly StyledProperty<bool> ShouldUseOverlayPopupProperty =
|
||||
AvaloniaProperty.Register<AbstractColorPicker, bool>(nameof(ShouldUseOverlayPopup), true);
|
||||
|
||||
public static readonly StyledProperty<bool> IsLightDismissEnabledProperty =
|
||||
AvaloniaProperty.Register<AbstractColorPicker, bool>(nameof(IsLightDismissEnabled), true);
|
||||
|
||||
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
|
||||
Popup.PlacementAnchorProperty.AddOwner<AbstractColorPicker>();
|
||||
```
|
||||
|
||||
`IsLightDismissEnabled` 在 `SetupTriggerHandler()` 中根据 TriggerType 自动设置:
|
||||
```csharp
|
||||
SetCurrentValue(IsLightDismissEnabledProperty, TriggerType == FlyoutTriggerType.Click);
|
||||
```
|
||||
|
||||
### 5. 命名清理
|
||||
|
||||
| 旧名称 | 新名称 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `NotifyFlyoutOpened()` | `NotifyPickerOpened()` | 子类重写点 |
|
||||
| `NotifyFlyoutClosed()` | `NotifyPickerClosed()` | 子类重写点 |
|
||||
| `ClosePickerFlyout()` | `ClosePicker()` | 公共 API |
|
||||
| `UpdatePseudoClasses` 中 `:flyout-open` | 保持不变 | 兼容现有样式 |
|
||||
|
||||
### 6. Popup Overlay 配置
|
||||
|
||||
Hover 模式下需要特殊配置以避免 Popup overlay 拦截指针事件:
|
||||
```csharp
|
||||
if (_popup != null)
|
||||
{
|
||||
_popup.OverlayInputPassThroughElement = this;
|
||||
_popup.OverlayDismissEventPassThrough = true;
|
||||
}
|
||||
```
|
||||
|
||||
这确保:
|
||||
- 指针事件能穿透 overlay 到达 anchor(不会因为 overlay 导致 PointerExited)
|
||||
- overlay 的 dismiss 事件不会直接关闭 Popup(由我们的逻辑控制关闭时机)
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 在 `AbstractColorPicker` 中添加新属性(MouseEnterDelay、MouseLeaveDelay、箭头相关、ShouldUseOverlayPopup、IsLightDismissEnabled)
|
||||
2. 删除当前的 TriggerType 简单实现(OnClick/OnPointerEntered/OnPointerExited 等)
|
||||
3. 实现 `SetupTriggerHandler()` + 三种模式的完整逻辑
|
||||
4. 实现箭头管理(ConfigureShowArrowEffective / ConfigureArrowPosition)
|
||||
5. 更新 `OnApplyTemplate` 中的 Popup 绑定(翻转追踪、overlay 配置)
|
||||
6. 更新两个 Theme AXAML 模板
|
||||
7. 重命名 `NotifyFlyoutOpened`/`NotifyFlyoutClosed` → `NotifyPickerOpened`/`NotifyPickerClosed`
|
||||
8. 更新 `ColorPicker.cs` 和 `GradientColorPicker.cs` 中的重写方法名
|
||||
9. 构建验证
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] Click 模式:点击打开/关闭,点击外部关闭(light dismiss)
|
||||
- [ ] Hover 模式:延迟打开,指针移到 Popup 内不关闭,离开后延迟关闭
|
||||
- [ ] Focus 模式:获得焦点打开,失去焦点关闭,点击外部关闭
|
||||
- [ ] 箭头:正确显示/隐藏,方向正确,翻转后方向跟随
|
||||
- [ ] IsPointAtCenter:箭头指向 anchor 中心
|
||||
- [ ] 窗口失活时关闭
|
||||
- [ ] Disabled 状态不响应触发
|
||||
- [ ] GradientColorPicker 同样正常工作
|
||||
@@ -1,820 +0,0 @@
|
||||
# Dialog Window/Overlay implementation plan for release/6.0
|
||||
|
||||
## 背景与目标
|
||||
|
||||
`release/5.0` 的 `Dialog` 是一套完整的弹窗系统:外部通过 `Dialog`、`DialogOptions`、`ShowDialog*` 静态 API、`IDialog`、`IDialogHostProvider` 等接口使用;内部根据 `DialogHostType` 分为 `Window Dialog` 与 `Overlay Dialog` 两种承载方式。
|
||||
|
||||
`release/6.0` 已引入新的 `Window` 架构与新的 `Popup`/Overlay 架构:
|
||||
|
||||
- `Window` 模板内使用 `VisualLayerManager`、`FullscreenPopoverLayer`、新的标题栏和新的 resizer。
|
||||
- `Popup` 移除了旧的 `PopupBuddyLayer`/`OverlayPopupContent`,改为 Avalonia 12 的 PopupHost/OverlayPopupHost 路线,并通过 `PopupMotionActor`、`ShadowsAwareContainer`、`ShouldUseOverlayLayer` 统一动画与阴影。
|
||||
|
||||
本计划的目标是基于 `release/6.0` 的新架构重新完成:
|
||||
|
||||
1. `Window Dialog`
|
||||
2. `Overlay Dialog`
|
||||
|
||||
同时保持 `release/5.0` 的外部接口、交互语义和 UI 效果不变。
|
||||
|
||||
重要原则:以 `release/5.0` 的对外 API、行为语义和 UI 效果为基线,在 `release/6.0` 新 `Window` 和新 `Popup` 架构上重新实现 Dialog。
|
||||
|
||||
## 必须保持不变的对外行为
|
||||
|
||||
### Public API
|
||||
|
||||
需要保持或恢复以下 API:
|
||||
|
||||
- `Dialog`
|
||||
- `DialogOptions`
|
||||
- `DialogHostType`
|
||||
- `DialogPlacement` 中的 `DialogHorizontalAnchor`、`DialogVerticalAnchor`
|
||||
- `DialogStandardButton` / `DialogStandardButtons`
|
||||
- `DialogButton`
|
||||
- `DialogButtonRole`
|
||||
- `IDialog`
|
||||
- `IDialogActionResult`
|
||||
- `IDialogAwareDataContext`
|
||||
- `IDialogHost`
|
||||
- `IDialogHostProvider`
|
||||
- `Dialog.ShowDialog*`
|
||||
- `Dialog.ShowDialogModal*`
|
||||
- `Dialog.ShowDialogAsync*`
|
||||
- `Dialog.ShowDialogModalAsync*`
|
||||
|
||||
为了“接口不变”,需要提供这些静态 API。内部实现不再依赖 `GlobalDialogManager`:Window 类型基于新 `Window`,Overlay 类型基于新 `Popup` 自己的 OverlayLayer。
|
||||
|
||||
### 行为语义
|
||||
|
||||
以下行为需要与 `release/5.0` 保持一致:
|
||||
|
||||
- `DialogHostType.Window` 使用独立窗口承载。
|
||||
- `DialogHostType.Overlay` 在当前 `TopLevel` 的 overlay 层内承载。
|
||||
- `IsModal=true` 时,异步打开应等待 Dialog 关闭后完成。
|
||||
- `IsModal=false` 时,异步打开应在 Host 显示后完成,关闭结果通过回调或事件获取。
|
||||
- `Open()` 仍通过局部 Dispatcher frame 实现同步等待。
|
||||
- `Accept()` 设置 `Result = DialogCode.Accepted` 并触发关闭。
|
||||
- `Reject()` 设置 `Result = DialogCode.Rejected` 并触发关闭。
|
||||
- `Done(object? result)` 设置结果并关闭。
|
||||
- `Closing` 可取消关闭。
|
||||
- `IsConfirmLoading=true` 时禁止确认关闭,并同步到确认类按钮 loading 状态。
|
||||
- `IDialogAwareDataContext.NotifyAttachedToDialog/NotifyDetachedFromDialog/NotifyClosed` 生命周期保持。
|
||||
- `CustomButtons` 打开前后动态增删保持同步。
|
||||
- `ButtonsConfigure` 在按钮同步后仍可配置按钮。
|
||||
- light dismiss、遮罩事件穿透、父窗口关闭时关闭 Dialog 的语义保持。
|
||||
|
||||
### UI 效果
|
||||
|
||||
需要保持 Ant Design 5.0 Dialog 视觉效果:
|
||||
|
||||
- header/content/footer padding、间距、字体、背景和阴影继续由 `DialogToken` 控制。
|
||||
- Overlay Dialog 保持圆角、阴影、modal mask、标题栏、关闭按钮、最大化按钮、resize handle、拖拽移动。
|
||||
- Window Dialog 保持与 AtomUI Window 一致的新标题栏、系统平台适配和窗口阴影,同时保留 Dialog footer/button box。
|
||||
- `DialogButtonBox` 的按钮布局、本地化文本、默认按钮、Esc 按钮、确认按钮 loading 不变。
|
||||
|
||||
## 架构取舍
|
||||
|
||||
实现时需要遵循以下取舍:
|
||||
|
||||
- 可从 5.0 拿回 API、按钮系统、Token、Localization 和视觉模板结构。
|
||||
- 不直接拿回 5.0 的旧 `GlobalDialogManager`、旧 `DialogLayer`、旧 `ManagedDialogPositioner` 作为最终架构。
|
||||
- 以 6.0 的 `Window` 和新 `Popup`/OverlayPopupHost 作为底座。
|
||||
- Window Dialog 不复制或修改 Window 的复杂 ControlTheme,直接使用新 `Window` 的原生模板能力,通过设置 Window `Content` / `ContentTemplate` 承载 Dialog 自己的内容包装控件。
|
||||
|
||||
因此实现重点是将 5.0 的 API 和视觉行为重新映射到 6.0 的 Window/Popup 架构。
|
||||
|
||||
## 总体架构
|
||||
|
||||
### 组件关系
|
||||
|
||||
```text
|
||||
Dialog
|
||||
├── public API / lifecycle / events / result
|
||||
├── static ShowDialog* APIs
|
||||
├── DialogOpenState
|
||||
├── Relay bindings to host
|
||||
│
|
||||
├── Window Dialog
|
||||
│ └── DialogHost : AtomUI.Desktop.Controls.Window
|
||||
│ ├── uses release/6.0 Window chrome/titlebar/resizer
|
||||
│ ├── owns DialogButtonBox footer
|
||||
│ └── relays close/minimize/maximize/footer/loading/buttons
|
||||
│
|
||||
└── Overlay Dialog
|
||||
└── OverlayDialogHost : ContentControl
|
||||
├── hosted as Popup content
|
||||
├── Popup.ShouldUseOverlayLayer = true
|
||||
├── optional OverlayDialogMask
|
||||
├── OverlayDialogHeader
|
||||
├── DialogButtonBox footer
|
||||
├── OverlayDialogResizer
|
||||
└── drag/maximize/restore/z-index/motion
|
||||
```
|
||||
|
||||
### Host 创建策略
|
||||
|
||||
`Dialog.OpenAsync` 根据 `DialogHostType` 分发:
|
||||
|
||||
```text
|
||||
DialogHostType.Window
|
||||
TopLevel parent = TopLevel.GetTopLevel(PlacementTarget)
|
||||
DialogHost host = CreateDialogHost(parent, this)
|
||||
host.SetChild(ContentControl)
|
||||
relay Dialog properties
|
||||
host.Show() or ShowDialog-like modal loop
|
||||
|
||||
DialogHostType.Overlay
|
||||
Popup popup = CreateOverlayPopup(PlacementTarget or topLevel)
|
||||
popup.ShouldUseOverlayLayer = true
|
||||
OverlayDialogHost host = CreateOverlayDialogHost(popup, this)
|
||||
host is assigned as popup child/content
|
||||
relay Dialog properties
|
||||
popup.IsOpen = true
|
||||
```
|
||||
|
||||
`Dialog` 自身继续不参与 measure/arrange,仍作为逻辑控制器存在。
|
||||
|
||||
## Window Dialog 设计
|
||||
|
||||
### 承载类
|
||||
|
||||
保留:
|
||||
|
||||
```csharp
|
||||
internal class DialogHost : Window, IDialogHost, IStyleHost, IMotionAwareControl
|
||||
```
|
||||
|
||||
但需要明确它是基于 `release/6.0` 新 `Window` 的 Dialog 特化,而不是 5.0 旧 Window 模板副本。
|
||||
|
||||
### Window 属性同步
|
||||
|
||||
`Dialog` 打开 Window Host 时需要同步:
|
||||
|
||||
- `Title`
|
||||
- `TitleIcon` 到 Window 的 `Icon`/`Logo` 或 DialogHost 自定义 title content
|
||||
- `Content`
|
||||
- `ContentTemplate`
|
||||
- `Width`
|
||||
- `Height`
|
||||
- `MinWidth`
|
||||
- `MinHeight`
|
||||
- `MaxWidth`
|
||||
- `MaxHeight`
|
||||
- `Topmost`
|
||||
- `IsResizable`
|
||||
- `IsClosable`
|
||||
- `IsMaximizable`
|
||||
- `IsMinimizable`
|
||||
- `IsModal`
|
||||
- `IsMotionEnabled`
|
||||
- `IsLoading`
|
||||
- `IsConfirmLoading`
|
||||
- `StandardButtons`
|
||||
- `DefaultStandardButton`
|
||||
- `EscapeStandardButton`
|
||||
- `IsFooterVisible`
|
||||
|
||||
`IsModal=true` 时:
|
||||
|
||||
- `EffectiveMinimizable=false`
|
||||
- Window host 不显示 minimize caption button
|
||||
- 关闭通过 Dialog `Closing` 事件统一拦截
|
||||
|
||||
### Window Content 包装策略
|
||||
|
||||
Window 的 ControlTheme 处理跨平台 CSD、标题栏、阴影、resizer、`VisualLayerManager`、`FullscreenPopoverLayer` 等复杂逻辑。Window Dialog 不应复制、覆盖或修改 Window 的 ControlTheme。
|
||||
|
||||
首选方案:`DialogHost` 继续继承 release/6.0 的 `Window`,并直接使用 Window 原有 `ControlTheme`。Dialog 专属 UI 通过设置 Window 的 `Content` 和 `ContentTemplate` 实现。
|
||||
|
||||
建议新增内部内容包装控件,例如:
|
||||
|
||||
```text
|
||||
DialogWindowContent
|
||||
├── Skeleton loading wrapper
|
||||
├── user content presenter
|
||||
└── DialogButtonBox footer
|
||||
```
|
||||
|
||||
`DialogHost.SetChild(Control? control)` 不应直接把用户内容设置给 Window `Content`,而应:
|
||||
|
||||
1. 保存用户内容和 `ContentTemplate`。
|
||||
2. 创建/更新 `DialogWindowContent`。
|
||||
3. 将 `DialogWindowContent` 设置为 Window `Content`。
|
||||
4. 由 `DialogWindowContent` 内部承载用户内容、loading skeleton、footer 和 button box。
|
||||
|
||||
包装控件可以是内部 `TemplatedControl` 或轻量 `ContentControl`:
|
||||
|
||||
- 若需要 token style 和 template selector,使用 `TemplatedControl`。
|
||||
- 若实现足够简单,可使用 `ContentControl` + 专用 `ControlTheme`。
|
||||
|
||||
包装控件需要暴露或接收:
|
||||
|
||||
- `DialogContent`
|
||||
- `DialogContentTemplate`
|
||||
- `IsLoading`
|
||||
- `IsConfirmLoading`
|
||||
- `IsFooterVisible`
|
||||
- `IsEffectiveFooterVisible`
|
||||
- `StandardButtons`
|
||||
- `DefaultStandardButton`
|
||||
- `EscapeStandardButton`
|
||||
- `CustomButtons`
|
||||
- `IsMotionEnabled`
|
||||
|
||||
它的模板只负责 Dialog 内容区:
|
||||
|
||||
```xml
|
||||
<DockPanel LastChildFill="True">
|
||||
<Border Name="FooterFrame" DockPanel.Dock="Bottom"
|
||||
IsVisible="{TemplateBinding IsEffectiveFooterVisible}">
|
||||
<atom:DialogButtonBox ... />
|
||||
</Border>
|
||||
<Border Name="ContentFrame" Padding="{TemplateBinding Padding}">
|
||||
<atom:Skeleton IsLoading="{TemplateBinding IsLoading}" ... />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
```
|
||||
|
||||
这样 Window Dialog 可以完整复用新 Window 的 ControlTheme,并把 Dialog 的 header/footer/content 逻辑限制在 Window content 区域内,避免后续 Window 模板变化导致 DialogHost 模板同步成本。
|
||||
|
||||
`DialogHostTheme.axaml` 应尽量保持很薄:
|
||||
|
||||
- 只 `BasedOn` 新 `WindowTheme`。
|
||||
- 设置 DialogHost 默认属性,例如 `SizeToContent`、`Padding`、caption button 可见性。
|
||||
- 不定义完整 `ControlTemplate`。
|
||||
- 不选择 Window 内部复杂 template part,除非该 part 是稳定公开常量。
|
||||
|
||||
### Dialog 定位抽象
|
||||
|
||||
不要使用 `release/5.0` 的 `ManagedDialogPositioner`。它为旧 Popup/DialogLayer 场景处理了大量 anchor、gravity、screen constraint、flip 等复杂逻辑,但 Dialog 当前只需要 Ant Design Dialog 的启动位置和 offset 语义。Window 类型已经可以直接设置窗口位置;Popup 类型固定使用 Dialog 自己的 `CustomPopupPlacementCallback`,不考虑通用 `Placement`,因为 Dialog 的 Overlay 场景里 placement 没有实际意义。
|
||||
|
||||
建议抽象一个轻量定位接口,统一 Window Dialog 和 Overlay Dialog 的定位输入,但分别适配不同宿主:
|
||||
|
||||
```csharp
|
||||
internal interface IDialogPlacementController
|
||||
{
|
||||
void Attach(Dialog dialog, IDialogHost host, TopLevel topLevel, Control placementTarget);
|
||||
void Detach();
|
||||
void UpdatePlacement();
|
||||
void NotifyHostMeasured(Size hostSize, Rect ownerBounds);
|
||||
}
|
||||
```
|
||||
|
||||
或使用更小的计算型接口:
|
||||
|
||||
```csharp
|
||||
internal interface IDialogPlacementCalculator
|
||||
{
|
||||
Point Calculate(Dialog dialog, Size hostSize, Size ownerSize);
|
||||
}
|
||||
```
|
||||
|
||||
推荐组合:
|
||||
|
||||
- `DialogPlacementCalculator`:只负责根据 `HorizontalStartupLocation`、`VerticalStartupLocation`、`HorizontalOffset`、`VerticalOffset` 计算 logical offset。
|
||||
- `WindowDialogPlacementController`:把 logical offset 转成 screen/device position,并设置 `DialogHost.Position`。
|
||||
- `PopupDialogPlacementController`:维护 Dialog offset,并安装 Dialog 专用 `CustomPopupPlacementCallback`;callback 根据当前 offset 返回固定 top-left placement。
|
||||
|
||||
这样定位策略只写一份,宿主适配逻辑分开,避免把 Window 和 Popup 坐标系混在一起。
|
||||
|
||||
### Window Dialog 定位
|
||||
|
||||
Window Dialog 直接设置 Window 位置,不使用 `ManagedDialogPositioner`:
|
||||
|
||||
- startup location 只负责初始位置。
|
||||
- `HorizontalStartupLocation` / `VerticalStartupLocation` 计算 `OffsetX` / `OffsetY`。
|
||||
- Window host 测量完成后调用 `NotifyDialogHostMeasured(Size, Rect)`。
|
||||
- `Rect` 来源:
|
||||
- 优先使用 owner 所在 screen working area。
|
||||
- 无 owner screen 时使用 primary screen working area。
|
||||
- 计算完成后设置 `DialogHost.Position`。
|
||||
|
||||
`WindowDialogPlacementController.UpdatePlacement()`:
|
||||
|
||||
```text
|
||||
Window host:
|
||||
logical point = owner screen top-left + OffsetX/Y
|
||||
device point = logical point * DesktopScaling
|
||||
host.Position = device point
|
||||
```
|
||||
|
||||
Window 类型不需要 placement callback,也不需要复杂避让逻辑。后续如果要支持 owner window 移动时跟随,只在 `WindowDialogPlacementController` 中订阅 owner position/size 变化即可。
|
||||
|
||||
### Window Dialog 关闭
|
||||
|
||||
`DialogHost.OnClosing` 必须拦截用户关闭:
|
||||
|
||||
- 非程序关闭或 caption close button 关闭时取消 `WindowClosingEventArgs`。
|
||||
- 转发到 `Dialog.NotifyDialogHostCloseRequest()`。
|
||||
- 由 `Dialog.NotifyClose()` 统一触发 `Closing`、`Accepted`、`Rejected`、`Finished`、`Closed`。
|
||||
|
||||
程序关闭时由 `DialogOpenState.Dispose()` 调用 `host.Close(callback)`。
|
||||
|
||||
## Overlay Dialog 设计
|
||||
|
||||
### Popup OverlayLayer 承载策略
|
||||
|
||||
Overlay Dialog 应直接使用 `Popup` 自己的 OverlayLayer,不引入 `ScopeAwareOverlayLayer`。
|
||||
|
||||
```text
|
||||
Dialog
|
||||
└── Popup
|
||||
├── ShouldUseOverlayLayer = true
|
||||
├── PlacementTarget = Dialog.PlacementTarget or TopLevel root/content
|
||||
├── CustomPopupPlacementCallback = Dialog-specific callback
|
||||
└── Child/Content = OverlayDialogHost
|
||||
```
|
||||
|
||||
这样 Overlay Dialog 的层级、PopupHost、OverlayPopupHost、motion/shadow 基础设施都由新 Popup 架构负责。
|
||||
|
||||
`Dialog` 不应直接查找或注入 overlay layer。它只创建和配置 `Popup`,由 `Popup` 内部决定使用 OverlayPopupHost 还是 PopupRoot。
|
||||
|
||||
### Overlay Host 结构
|
||||
|
||||
承载类:
|
||||
|
||||
```csharp
|
||||
internal class OverlayDialogHost : ContentControl, IDialogHost, IMotionAwareControl
|
||||
```
|
||||
|
||||
内部职责:
|
||||
|
||||
- 作为 `Popup` 的内容控件存在。
|
||||
- 通过持有 owner `Popup` 或由 `DialogOpenState` 管理 popup open/close。
|
||||
- 管理 `OverlayDialogMask`
|
||||
- 管理 `OverlayDialogHeader`
|
||||
- 管理 `DialogButtonBox`
|
||||
- 管理 `OverlayDialogResizer`
|
||||
- 管理拖拽、最大化、还原
|
||||
- 管理 z-index
|
||||
- 管理打开/关闭动画
|
||||
- 管理 footer 自动显隐
|
||||
|
||||
### Overlay mask
|
||||
|
||||
`IsModal=true`:
|
||||
|
||||
- 在 `OverlayDialogHost` 内显示 mask。
|
||||
- Popup 内容容器应按 owner `TopLevel.ClientSize` 或 placement target root bounds 拉满,使 mask 覆盖当前 Popup OverlayLayer 可视区域。
|
||||
- mask 拦截 pointer wheel 和 pointer press。
|
||||
- `IsLightDismissEnabled=true` 时点击 mask 触发 `Dialog.NotifyClose()`。
|
||||
|
||||
`IsModal=false`:
|
||||
|
||||
- 不显示阻塞 mask。
|
||||
- host 自身保留阴影。
|
||||
- 点击外部不关闭,除非显式配置 light dismiss。
|
||||
|
||||
### Overlay 事件穿透
|
||||
|
||||
保持 5.0 的语义:
|
||||
|
||||
- `OverlayDismissEventPassThrough=true` 时,mask 点击关闭后将 pointer event 转发给 `OverlayInputPassThroughElement` 或原始命中元素。
|
||||
- 如果 release/6.0/Avalonia 12 的 raw input API 不再允许旧方式直接复用,需要实现局部兼容层,不改变公开属性。
|
||||
|
||||
### Overlay 定位与尺寸
|
||||
|
||||
Overlay Dialog 不使用旧 `ManagedDialogPositioner`。使用 Popup owner 的可用尺寸:
|
||||
|
||||
- `TopLevel.ClientSize`
|
||||
- 或 `PlacementTarget` 所在 visual root/client bounds
|
||||
- 作为 `NotifyDialogHostMeasured` 的 bounds
|
||||
|
||||
通用 `DialogPlacementCalculator` 定位规则:
|
||||
|
||||
- `HorizontalStartupLocation.Left` -> `OffsetX=0`
|
||||
- `HorizontalStartupLocation.Center` -> `(ownerWidth - hostWidth) / 2`
|
||||
- `HorizontalStartupLocation.Right` -> `ownerWidth - hostWidth`
|
||||
- `HorizontalStartupLocation.Custom` -> `HorizontalOffset.Resolve(ownerWidth)`
|
||||
- vertical 同理
|
||||
|
||||
拖拽时:
|
||||
|
||||
- 更新 `OffsetX` / `OffsetY`,由 `OverlayDialogHost` 在自己的 full-overlay panel 内重新 arrange frame。
|
||||
- 不回写 `HorizontalStartupLocation` / `VerticalStartupLocation`。
|
||||
|
||||
Popup Dialog 固定使用自己的 `CustomPopupPlacementCallback`:
|
||||
|
||||
- 不使用 Popup 的普通 `Placement` 语义。
|
||||
- 要考虑 Dialog offset:接口实现负责设置/更新 offset。
|
||||
- callback 只处理 Dialog 的 top-left 位置。
|
||||
- callback 根据当前 offset 定位 Dialog。
|
||||
- callback 不实现 5.0 `ManagedDialogPositioner` 那种复杂 flip/constraint 算法。
|
||||
|
||||
```text
|
||||
PopupDialogPlacementController.UpdatePlacement()
|
||||
offset = DialogPlacementCalculator.Calculate(...)
|
||||
popup.InvalidatePosition()
|
||||
|
||||
DialogCustomPopupPlacementCallback(ownerSize, hostSize, current offset)
|
||||
returns top-left custom placement from offset
|
||||
```
|
||||
|
||||
最大化时:
|
||||
|
||||
- 保存 `_originPosition` 和 `_latestSize`。
|
||||
- `WindowState=Maximized` 后 dialog frame 填满 popup owner 可视区域。
|
||||
- `CornerRadius=0`。
|
||||
- resizer 隐藏。
|
||||
|
||||
还原时:
|
||||
|
||||
- 恢复最大化前位置和尺寸。
|
||||
|
||||
### Overlay 与新 Popup 架构的关系
|
||||
|
||||
Overlay Dialog 不应复活旧 `PopupBuddyLayer` 或旧 `DialogLayer`。
|
||||
|
||||
Overlay Dialog 直接使用新 Popup 架构:
|
||||
|
||||
- `Popup.ShouldUseOverlayLayer=true`
|
||||
- `Popup.PlacementTarget = Dialog.PlacementTarget ?? topLevel/root`
|
||||
- `Popup` 负责 OverlayLayer/OverlayPopupHost 生命周期。
|
||||
- `OverlayDialogHost` 负责 Dialog mask、frame、drag、resize、maximize/restore。
|
||||
- `PopupMotionActor`/`MotionActor` 的动画调度模式可以复用,但 Dialog 当前也可继续使用 host transitions,前提是视觉不变。
|
||||
- `ShadowsAwareContainer` 的阴影测量思路可以参考,但 Dialog 当前可继续通过 `ShadowFrame` + token shadow 实现,避免影响 UI。
|
||||
|
||||
Popup 配置必须满足:
|
||||
|
||||
- `Popup.ShouldUseOverlayLayer=true`
|
||||
- `Popup.IsLightDismissEnabled=false`,light dismiss 由 Dialog mask 统一处理。
|
||||
- `Popup.StaysOpen=true` 或等价设置,避免 Popup 自己绕过 Dialog 生命周期关闭。
|
||||
- `Popup` close/cancel 事件必须转发到 `Dialog.NotifyClose()`,不能直接销毁。
|
||||
- Popup content 的 root 尺寸应覆盖 owner 可视区域,以支持 modal mask。
|
||||
- Popup 定位固定通过 Dialog 专用 `CustomPopupPlacementCallback` 实现:controller 设置 offset,callback 根据 offset 定位;不使用通用 `Placement`,不使用 `ManagedDialogPositioner`。
|
||||
|
||||
## Dialog.OpenAsync 实现计划
|
||||
|
||||
### 打开流程
|
||||
|
||||
`OpenAsync` 应补齐为:
|
||||
|
||||
```text
|
||||
if already open/opening -> return
|
||||
resolve placementTarget
|
||||
resolve topLevel
|
||||
create host by DialogHostType
|
||||
set child/content
|
||||
relay properties and token bindings
|
||||
subscribe parent closed/detached/raw input if needed
|
||||
create DialogOpenState
|
||||
set IsOpen=true under ignore scope
|
||||
host.Show()
|
||||
raise Opened
|
||||
if IsModal:
|
||||
await close task
|
||||
else:
|
||||
return after show
|
||||
```
|
||||
|
||||
需要内部增加 close completion:
|
||||
|
||||
```csharp
|
||||
TaskCompletionSource<object?> _closeTcs
|
||||
```
|
||||
|
||||
或放入 `DialogOpenState`。
|
||||
|
||||
### 关闭流程
|
||||
|
||||
`NotifyClose` 应:
|
||||
|
||||
1. 检查 `IsConfirmLoading` 和 `_closing`。
|
||||
2. 触发 `Closing`,允许取消。
|
||||
3. 根据 `Result` 触发 `Accepted` / `Rejected`。
|
||||
4. 触发 `Finished`。
|
||||
5. dispose open state:
|
||||
- 从 overlay layer 移除 mask 和 host,播放关闭动画。
|
||||
- 或关闭 Window host。
|
||||
- 解除所有 bindings/subscriptions。
|
||||
6. 设置 `IsOpen=false`。
|
||||
7. 触发 `Closed`。
|
||||
8. 通知 `IDialogAwareDataContext.NotifyClosed()`。
|
||||
9. 完成 modal await 的 task。
|
||||
|
||||
### 属性 relay
|
||||
|
||||
`Dialog` 到 host 使用 `BindUtils.RelayBind`,避免手动同步遗漏。
|
||||
|
||||
Window Host 和 Overlay Host 共同 relay:
|
||||
|
||||
- title/icon
|
||||
- content/template
|
||||
- size
|
||||
- modal
|
||||
- resizable
|
||||
- closable
|
||||
- maximizable
|
||||
- minimizable/effective minimizable
|
||||
- drag movable
|
||||
- motion
|
||||
- loading
|
||||
- confirm loading
|
||||
- footer visible
|
||||
- standard buttons
|
||||
- default/escape button
|
||||
- topmost
|
||||
|
||||
Overlay-only:
|
||||
|
||||
- light dismiss
|
||||
- overlay event pass-through
|
||||
- overlay input pass-through element
|
||||
- offset x/y
|
||||
|
||||
Window-only:
|
||||
|
||||
- Window titlebar caption button visibility
|
||||
- owner/parent relation
|
||||
- startup position
|
||||
|
||||
## Static API 恢复计划
|
||||
|
||||
恢复 `Dialog.StaticAPI.cs`,但内部实现改为:
|
||||
|
||||
```text
|
||||
FindDialogPlacementRoot(topLevel)
|
||||
topLevel ?? Window.GetMainWindow()
|
||||
throw if null
|
||||
|
||||
CreateDialog(content, dataContext, options)
|
||||
same mapping as release/5.0
|
||||
|
||||
ShowDialog(...)
|
||||
dialog.PlacementTarget = options.PlacementTarget ?? topLevel/root content
|
||||
return dialog.Open()
|
||||
|
||||
ShowDialogAsync(...)
|
||||
attach Closed callback
|
||||
await dialog.OpenAsync(...)
|
||||
```
|
||||
|
||||
不再要求 `GlobalDialogManager` 存在。
|
||||
|
||||
兼容点:
|
||||
|
||||
- 如果用户传入 AtomUI `Window` 或普通 Avalonia `TopLevel`,Overlay Dialog 都通过 `Popup` 自己解析 OverlayLayer。
|
||||
- 如果 Popup 无法在当前 TopLevel 创建 overlay host,应抛出明确异常。
|
||||
|
||||
## 文件级实施清单
|
||||
|
||||
### 建议的实现策略
|
||||
|
||||
以 `release/5.0` 为行为基线,在 `release/6.0` 上实现:
|
||||
|
||||
1. 备份/对照 `release/5.0:src/AtomUI.Desktop.Controls/Dialog` 作为行为基线。
|
||||
2. 保留可直接复用且无架构冲突的文件:
|
||||
- `DialogToken.cs`
|
||||
- `Localization/en_US.cs`
|
||||
- `Localization/zh_CN.cs`
|
||||
- `ButtonBox/*`,但需要按 6.0 Button/Theme API 验证
|
||||
- `DialogActionResult.cs`
|
||||
- `DialogFinishedEventArgs.cs`
|
||||
- `DialogManagerEventArgs.cs`
|
||||
- `DialogHostType.cs`
|
||||
- `DialogPlacement.cs`
|
||||
- `IDialog*.cs`
|
||||
3. 实现以下核心文件:
|
||||
- `Dialog.cs`
|
||||
- `Dialog.StaticAPI.cs`
|
||||
- `WindowHost/DialogHost.cs`
|
||||
- `OverlayHost/OverlayDialogHost.cs`
|
||||
- `OverlayHost/OverlayDialogMask.cs`
|
||||
- `Themes/DialogHostTheme.axaml`
|
||||
- `Themes/OverlayDialogHostTheme.axaml`
|
||||
4. 不恢复旧架构文件:
|
||||
- `GlobalDialogManager.cs`
|
||||
- `DialogPositioning/ManagedDialogPositioner.cs`
|
||||
- `DialogPositioning/IDialogPositioner.cs`
|
||||
- 旧 `DialogLayer` 相关实现
|
||||
5. 新增轻量定位抽象:
|
||||
- `DialogPositioning/IDialogPlacementCalculator.cs`
|
||||
- `DialogPositioning/DialogPlacementCalculator.cs`
|
||||
- `DialogPositioning/WindowDialogPlacementController.cs`
|
||||
- `DialogPositioning/PopupDialogPlacementController.cs`
|
||||
- `DialogPositioning/DialogCustomPopupPlacementCallback.cs`
|
||||
|
||||
### Dialog 主流程
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/Dialog.cs`
|
||||
- 以 5.0 `Dialog` public surface 为基线重写。
|
||||
- 实现 Host 创建方法:`CreateDialogHost`、`CreateOverlayDialogHost`。
|
||||
- 实现 `Open()`、`OpenAsync()`、`NotifyClose()`、modal/non-modal close completion。
|
||||
- 实现 `UpdateHostSizing`,定位委托给轻量 `IDialogPlacementController`。
|
||||
- 实现 `Closed` 与 data context closed notification。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/DialogPositioning/*`
|
||||
- 不恢复 5.0 复杂 positioner。
|
||||
- 新增轻量 `DialogPlacementCalculator` 计算 logical offset。
|
||||
- 新增 Window/Popup 两个 controller,分别设置 Window position 和 Dialog 专用 Popup placement callback。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/Dialog.StaticAPI.cs`
|
||||
- 从 release/5.0 恢复静态 API 签名。
|
||||
- 改用 release/6.0 overlay/window root 解析。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/IDialogHost.cs`
|
||||
- 保持 public surface 不破坏。
|
||||
- 如果必须新增内部方法,优先通过 internal extension 或具体 host 类型实现,避免破坏接口。
|
||||
|
||||
### Window Dialog
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/WindowHost/DialogHost.cs`
|
||||
- 对齐新 `Window` 标题栏和 caption button 属性。
|
||||
- 补齐 position 更新。
|
||||
- 确认 `Close(Action?)` 会执行 callback。
|
||||
- 确认 button/footer/loading relay。
|
||||
- 通过内部 `DialogWindowContent` 设置 Window `Content`,不直接修改 Window ControlTheme。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/Themes/DialogHostTheme.axaml`
|
||||
- 保持薄主题,基于 release/6.0 `WindowTheme`。
|
||||
- 不复制 Window `ControlTemplate`。
|
||||
- 不插入 Window 内部 footer。
|
||||
- 只设置 Window/DialogHost 级默认属性。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/WindowHost/DialogWindowContent.cs`
|
||||
- 新增内部内容包装控件。
|
||||
- 承载用户内容、`Skeleton` loading、footer 和 `DialogButtonBox`。
|
||||
- 向 `DialogHost` 回调按钮点击和按钮同步事件。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/Themes/DialogWindowContentTheme.axaml`
|
||||
- 新增包装控件主题。
|
||||
- 使用 `DialogTokenResource ContentPadding`、`FooterPadding`、`FooterMarginTop`、`FooterBg`。
|
||||
- 视觉效果与 5.0 Window Dialog content/footer 一致。
|
||||
|
||||
### Overlay Dialog
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/OverlayHost/OverlayDialogHost.cs`
|
||||
- 改为作为 `Popup` 内容控件承载。
|
||||
- 补齐 show/close 动画与 popup open/close 协调。
|
||||
- 补齐 drag/maximize/restore resize 边界。
|
||||
- 补齐 z-index 更新。
|
||||
- 补齐 footer/loading/button 同步。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/OverlayHost/OverlayDialogPopup.cs` 或 Dialog 内部 popup factory
|
||||
- 新增或封装 Overlay Dialog 专用 Popup 创建逻辑。
|
||||
- 设置 `ShouldUseOverlayLayer=true`。
|
||||
- 设置 placement target、placement、stays open/light dismiss 策略。
|
||||
- 将 `OverlayDialogHost` 作为 popup content。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/OverlayHost/OverlayDialogMask.cs`
|
||||
- 确认 modal mask 覆盖 Popup owner 可视区域。
|
||||
- 确认 light dismiss 和 pointer wheel 拦截。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/OverlayHost/OverlayDialogResizer.cs`
|
||||
- 确认 resize 事件与 `WindowState=Maximized` 互斥。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/Themes/OverlayDialogHostTheme.axaml`
|
||||
- 保持现有视觉。
|
||||
- 必要时引入 `MotionActor`,但不改变 UI。
|
||||
|
||||
### ButtonBox / Token / Localization
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/ButtonBox/DialogButtonBox.cs`
|
||||
- 保持 release/5.0 行为。
|
||||
- 验证标准按钮顺序、角色分组、默认按钮和 escape 按钮。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/DialogToken.cs`
|
||||
- 保持现有 token,不硬编码颜色和尺寸。
|
||||
|
||||
- `src/AtomUI.Desktop.Controls/Dialog/Localization/*.cs`
|
||||
- 保持按钮文本本地化。
|
||||
|
||||
## 实施阶段
|
||||
|
||||
### 阶段 1:恢复外部 API 与打开/关闭骨架
|
||||
|
||||
目标:
|
||||
|
||||
- 以 release/5.0 为基线重建 `Dialog.cs` 和 `Dialog.StaticAPI.cs`。
|
||||
- 实现 `Dialog.OpenAsync` 的 Host 创建分发。
|
||||
- 补齐 `DialogOpenState` close completion。
|
||||
- 能打开和关闭最小 Window Dialog / Overlay Dialog。
|
||||
|
||||
验收:
|
||||
|
||||
- `ShowDialog` 可同步返回 `DialogCode.Accepted/Rejected`。
|
||||
- `ShowDialogAsync` 非 modal 打开后返回,并可通过 closed callback 拿到 result。
|
||||
- `ShowDialogModalAsync` 等待关闭后返回 result。
|
||||
|
||||
### 阶段 2:Window Dialog 接入新 Window
|
||||
|
||||
目标:
|
||||
|
||||
- `DialogHost` 完整使用 release/6.0 新 Window 行为。
|
||||
- 新增 `DialogWindowContent` 包装控件,使用 Window `Content` / `ContentTemplate` 承载 Dialog 内容和 footer。
|
||||
- `DialogHostTheme.axaml` 保持薄主题,不复制 Window `ControlTemplate`。
|
||||
- 标题栏、caption buttons、resize、平台外观正常。
|
||||
|
||||
验收:
|
||||
|
||||
- Window Dialog 与普通 `Window` 的标题栏风格一致。
|
||||
- footer 按钮显示、隐藏、loading 正常。
|
||||
- modal Window 不显示 minimize。
|
||||
- 关闭按钮触发 Dialog 生命周期,而不是绕过 `Closing`。
|
||||
|
||||
### 阶段 3:Overlay Dialog 接入 Popup OverlayLayer
|
||||
|
||||
目标:
|
||||
|
||||
- Overlay Dialog 不依赖 `GlobalDialogManager`。
|
||||
- 通过 `Popup.ShouldUseOverlayLayer=true` 使用 Popup 自己的 OverlayLayer。
|
||||
- 支持 mask、light dismiss、drag、resize、maximize/restore、z-index。
|
||||
|
||||
验收:
|
||||
|
||||
- Overlay Dialog 居中显示。
|
||||
- overlay mask 覆盖当前 Popup owner 可视区域。
|
||||
- resize/maximize/restore 视觉与 release/5.0 一致。
|
||||
- 多个 overlay dialog 层级正确。
|
||||
|
||||
### 阶段 4:定位、尺寸和 transform 兼容
|
||||
|
||||
目标:
|
||||
|
||||
- 支持 `HorizontalStartupLocation` / `VerticalStartupLocation`。
|
||||
- 支持 `HorizontalOffset` / `VerticalOffset`。
|
||||
- 支持 Window 和 Overlay 的不同坐标系。
|
||||
- 抽象轻量 `IDialogPlacementCalculator` / `IDialogPlacementController`。
|
||||
- Window 类型直接设置 `Window.Position`。
|
||||
- Popup 类型由 controller 设置 offset,并通过 Dialog 专用 `CustomPopupPlacementCallback` 根据 offset 定位,不考虑通用 `Placement`。
|
||||
- 保留 `InheritsTransform` 对 host sizing 的影响。
|
||||
|
||||
验收:
|
||||
|
||||
- top/center/bottom、left/center/right、custom offset 都正确。
|
||||
- owner window 移动或不同 screen working area 下 Window Dialog 初始位置正确。
|
||||
- Overlay Dialog 不越出 Popup owner 可视区域的基本边界。
|
||||
- 不引入 5.0 `ManagedDialogPositioner`。
|
||||
|
||||
### 阶段 5:行为回归
|
||||
|
||||
目标:
|
||||
|
||||
- ButtonBox 行为与 release/5.0 一致。
|
||||
- Loading/ConfirmLoading 行为一致。
|
||||
- DataContext notification 一致。
|
||||
- light dismiss 和 event pass-through 一致。
|
||||
|
||||
验收:
|
||||
|
||||
- 标准按钮角色触发正确 result。
|
||||
- `Closing` 可取消关闭。
|
||||
- `IsConfirmLoading` 阻止确认关闭。
|
||||
- `NotifyClosed` 在最终关闭后触发。
|
||||
|
||||
### 阶段 6:测试和 Gallery 验证
|
||||
|
||||
建议补充或执行:
|
||||
|
||||
- 单元测试:
|
||||
- `DialogStandardButtons.Count`
|
||||
- `DialogButtonBox` 标准按钮构建和 role 分组
|
||||
- `Closing` cancel
|
||||
- `Accept/Reject/Done` result
|
||||
|
||||
- 交互验证:
|
||||
- Overlay modal
|
||||
- Overlay non-modal
|
||||
- Overlay light dismiss
|
||||
- Overlay resize/maximize/restore
|
||||
- Window modal
|
||||
- Window non-modal
|
||||
- 自定义按钮
|
||||
- confirm loading
|
||||
- loading skeleton
|
||||
|
||||
- Gallery:
|
||||
- 如现有 Gallery 无 Dialog showcase,需要补一个最小 showcase,覆盖 Window/Overlay 两种 host。
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
1. **不要恢复旧 PopupBuddyLayer 架构**
|
||||
release/6.0 已迁移到 Avalonia 12 的 PopupHost/OverlayPopupHost 方向,Dialog 不应重新引入旧 Popup buddy layer。
|
||||
|
||||
2. **不要恢复 GlobalDialogManager 依赖**
|
||||
静态 API 可以恢复,但内部 Window Dialog 使用新 `Window`,Overlay Dialog 使用新 `Popup` 自己的 OverlayLayer。
|
||||
|
||||
3. **WindowTheme 模板复制需要谨慎**
|
||||
Window 的 ControlTheme 很复杂,Window Dialog 不应复制或修改它。DialogHost 应复用 Window 原主题,把 Dialog 专属内容放进 Window `Content` 包装控件中。
|
||||
|
||||
4. **Overlay 坐标系与 Window 坐标系不同**
|
||||
Window 使用 screen/device pixel position;Overlay 使用 layer logical coordinates。不要混用。
|
||||
|
||||
5. **不要恢复 ManagedDialogPositioner**
|
||||
5.0 的 positioner 异常复杂,不适合新架构。Window 直接设置位置;Popup 由 controller 设置 offset,再由 Dialog 专用 `CustomPopupPlacementCallback` 根据 offset 定位。需要复用的是“启动位置和 offset 语义”,不是旧 positioner 实现。
|
||||
|
||||
6. **Modal async 语义容易回归**
|
||||
`OpenAsync` 对 modal/non-modal 的完成时机必须严格与 release/5.0 保持一致。
|
||||
|
||||
7. **Avalonia 12 internal API**
|
||||
`release/6.0` 已有若干 reflection extension。新增反射前必须确认是否已有可复用扩展,避免重复和脆弱依赖。
|
||||
|
||||
## 完成标准
|
||||
|
||||
完成后应满足:
|
||||
|
||||
- 5.0 Dialog 外部 API 源码兼容。
|
||||
- Window Dialog 基于 release/6.0 新 `Window`。
|
||||
- Overlay Dialog 基于 release/6.0 新 `Popup` OverlayLayer 架构。
|
||||
- Dialog UI 与 release/5.0 效果一致。
|
||||
- 不引入旧 `GlobalDialogManager`、旧 `DialogLayer`、旧 `PopupBuddyLayer`。
|
||||
- 不破坏 `Popup`、`Window`、`Message`、`Notification`、`Tour` 等现有 overlay/layer 使用者。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +1,35 @@
|
||||
# API 设计一致性分析报告
|
||||
|
||||
> 扫描时间:2026-05-10
|
||||
> 扫描时间:2026-05-10(修订)
|
||||
> 扫描范围:AtomUI.slnx 中所有项目
|
||||
> 分析维度:属性命名、方法/重写模式、枚举/类型、AXAML 模板
|
||||
> 分析维度:属性命名、方法/重写模式、枚举/类型、AXAML 模板、伪类与 Part 常量
|
||||
|
||||
---
|
||||
|
||||
## P0 — 拼写错误 / 命名错误
|
||||
|
||||
### 1. `ChoosingStatueChanged` → `ChoosingStatusChanged`
|
||||
### 1. `ChoosingStatue` → `ChoosingStatus`(21+ 处,覆盖 7 个文件)
|
||||
|
||||
单词拼写错误:`Statue`(雕像)应为 `Status`(状态)。
|
||||
|
||||
**涉及文件(事件定义):**
|
||||
**事件定义:**
|
||||
|
||||
| 文件 | 行号 | 当前命名 |
|
||||
|---|---|---|
|
||||
| `src/AtomUI.Desktop.Controls/TimePicker/TimePickerPresenter.cs` | 129 | `ChoosingStatueChanged` |
|
||||
| `src/AtomUI.Desktop.Controls/DatePicker/DatePickerPresenter.cs` | 119 | `ChoosingStatueChanged` |
|
||||
| 文件 | 当前命名 |
|
||||
|---|---|
|
||||
| `src/AtomUI.Desktop.Controls/TimePicker/TimePickerPresenter.cs` | `ChoosingStatueChanged`(事件,多处) |
|
||||
| `src/AtomUI.Desktop.Controls/DatePicker/DatePickerPresenter.cs` | `ChoosingStatueChanged`(事件)+ `EmitChoosingStatueChanged`(方法) |
|
||||
|
||||
**涉及文件(事件订阅/处理方法):**
|
||||
**事件订阅 / 处理方法:**
|
||||
|
||||
| 文件 | 行号 | 当前命名 |
|
||||
|---|---|---|
|
||||
| `src/AtomUI.Desktop.Controls/TimePicker/TimePicker.cs` | 159, 170 | `HandleChoosingStatueChanged` |
|
||||
| `src/AtomUI.Desktop.Controls/TimePicker/RangeTimePicker.cs` | 209, 220 | `HandleChoosingStatueChanged` |
|
||||
| `src/AtomUI.Desktop.Controls/DatePicker/DatePicker.cs` | 183, 194 | `HandleChoosingStatueChanged` |
|
||||
| `src/AtomUI.Desktop.Controls/DatePicker/RangeDatePicker.cs` | 178, 201 | `HandleChoosingStatueChanged` |
|
||||
| 文件 | 当前命名 |
|
||||
|---|---|
|
||||
| `src/AtomUI.Desktop.Controls/TimePicker/TimePicker.cs` | `HandleChoosingStatueChanged` |
|
||||
| `src/AtomUI.Desktop.Controls/TimePicker/RangeTimePicker.cs` | `HandleChoosingStatueChanged` |
|
||||
| `src/AtomUI.Desktop.Controls/DatePicker/DatePicker.cs` | `HandleChoosingStatueChanged` |
|
||||
| `src/AtomUI.Desktop.Controls/DatePicker/RangeDatePicker.cs` | `HandleChoosingStatueChanged` |
|
||||
| `src/AtomUI.Desktop.Controls/DatePicker/RangeDatePickerPresenter.cs` | `EmitChoosingStatueChanged` 调用 |
|
||||
|
||||
**修复方案:** 全局重命名为 `ChoosingStatusChanged` / `HandleChoosingStatusChanged`。属于公共 API 破坏性变更,需发 breaking change 说明。
|
||||
**修复方案:** 全局重命名为 `ChoosingStatusChanged` / `HandleChoosingStatusChanged` / `EmitChoosingStatusChanged`。属于公共 API 破坏性变更,需发 breaking change 说明。
|
||||
|
||||
---
|
||||
|
||||
@@ -43,28 +44,19 @@
|
||||
| `src/AtomUI.Controls/Timeline/TimelineToken.cs` | `TimelineToken`(Token 类) |
|
||||
| 目录名 | `Timeline/` |
|
||||
|
||||
**修复方案:** 将枚举重命名为 `TimelineMode`。属于公共 API 破坏性变更。
|
||||
**修复方案:** 将枚举重命名为 `TimelineMode`,文件重命名为 `TimelineEnums.cs`。属于公共 API 破坏性变更。
|
||||
|
||||
---
|
||||
|
||||
### 3. 重复的 ToolTip 目录
|
||||
|
||||
存在两个目录,包含同名文件:
|
||||
|
||||
| 目录 | 大小写 |
|
||||
|---|---|
|
||||
| `src/AtomUI.Desktop.Controls/ToolTip/` | PascalCase "Tip" |
|
||||
| `src/AtomUI.Desktop.Controls/Tooltip/` | lowercase "tip" |
|
||||
|
||||
两个目录都包含 `ToolTip.cs`、`ToolTipService.cs`、`ToolTipToken.cs` 和 `Themes/` 子目录。
|
||||
|
||||
**修复方案:** 合并为一个目录,保留 `Tooltip/`(与 Avalonia 一致)。
|
||||
> **说明:** 原报告中"重复 ToolTip 目录"条目已失效 —— 当前仅存 `src/AtomUI.Desktop.Controls/Tooltip/`,无重复。
|
||||
|
||||
---
|
||||
|
||||
## P1 — 布尔属性缺少 `Is` 前缀(14 处)
|
||||
## P1 — 布尔属性缺少 `Is` 前缀(共 22 处)
|
||||
|
||||
Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前缀。以下属性不符合规范:
|
||||
Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前缀。
|
||||
|
||||
### 1.1 裸动词 / 裸名词布尔属性(15 处)
|
||||
|
||||
| 文件 | 行号 | 当前命名 | 建议命名 |
|
||||
|---|---|---|---|
|
||||
@@ -84,25 +76,54 @@ Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前
|
||||
| `src/AtomUI.Desktop.Controls/Transfer/AbstractTransfer.cs` | 312 | `ToTargetButtonEnabledProperty` | `IsToTargetButtonEnabledProperty` |
|
||||
| `src/AtomUI.Desktop.Controls/Transfer/AbstractTransfer.cs` | 317 | `ToSourceButtonEnabledProperty` | `IsToSourceButtonEnabledProperty` |
|
||||
|
||||
**额外说明:** `IsShow*` 模式(如 `IsShowArrow`、`IsShowDescription`)虽然能工作,但在 Avalonia 惯例中更自然的写法是 `IsArrowVisible`、`IsDescriptionVisible`。这属于风格偏好,影响范围更广,可单独评估。
|
||||
### 1.2 `IsShow*` 反模式 → `Is*Visible`(10 处)
|
||||
|
||||
Avalonia 惯例:可见性用 `Is{Subject}Visible` 而非 `IsShow{Subject}`。
|
||||
|
||||
| 文件 | 当前命名 | 建议命名 |
|
||||
|---|---|---|
|
||||
| `src/AtomUI.Controls/Empty/AbstractEmpty.cs` | `IsShowDescriptionProperty` | `IsDescriptionVisibleProperty` |
|
||||
| `src/AtomUI.Controls/ProgressBar/AbstractProgressBar.cs` | `IsShowProgressInfoProperty` | `IsProgressInfoVisibleProperty` |
|
||||
| `src/AtomUI.Controls/Spin/AbstractSpin.cs` | `IsShowTipProperty` | `IsTipVisibleProperty` |
|
||||
| `src/AtomUI.Controls/Primitives/ArrowDecoratedBox/AbstractArrowDecoratedBox.cs` | `IsShowArrowProperty` | `IsArrowVisibleProperty` |
|
||||
| `src/AtomUI.Controls/Badge/AbstractCountBadge.cs` | `IsShowZeroProperty` | `IsZeroVisibleProperty` |
|
||||
| `src/AtomUI.Desktop.Controls.ColorPicker/AbstractColorPicker.cs` | `IsShowArrowProperty` | `IsArrowVisibleProperty` |
|
||||
| `src/AtomUI.Desktop.Controls.ColorPicker/AbstractColorPicker.cs` | `IsShowTextProperty` | `IsTextVisibleProperty` |
|
||||
| `src/AtomUI.Desktop.Controls.DataGrid/DataGrid.cs` | `IsShowFrameBorderProperty` | `IsFrameBorderVisibleProperty` |
|
||||
| `src/AtomUI.Desktop.Controls.DataGrid/DataGrid.cs` | `ShowSorterTooltipProperty` | `IsSorterTooltipVisibleProperty` |
|
||||
| `src/AtomUI.Desktop.Controls/Input/SearchEditDecoratedBox.cs:18` | `SearchButtonLoadingProperty` | `IsSearchButtonLoadingProperty` |
|
||||
|
||||
### 1.3 动词前缀缺 `Is`(7 处)
|
||||
|
||||
| 文件 | 行号 | 当前命名 | 建议命名 |
|
||||
|---|---|---|---|
|
||||
| `src/AtomUI.Desktop.Controls.ColorPicker/ColorSlider/AbstractColorPickerSliderTrack.cs` | 45 | `IgnoreThumbDragProperty` | `IsThumbDragIgnoredProperty` |
|
||||
| `src/AtomUI.Desktop.Controls.ColorPicker/ColorSlider/AbstractColorPickerSliderTrack.cs` | 48 | `DeferThumbDragProperty` | `IsThumbDragDeferredProperty` |
|
||||
| `src/AtomUI.Desktop.Controls.DataGrid/Column/Filters/DataGridFilterIndicator.cs` | 18 | `FilterMultipleProperty` | `IsMultipleFilterEnabledProperty` |
|
||||
|
||||
**总体修复策略:** 所有都是公共 API 破坏性变更,建议随下一个大版本批量处理,提供 `[Obsolete]` 过渡别名 1~2 个版本。
|
||||
|
||||
---
|
||||
|
||||
## P2 — 方法/重写模式不一致
|
||||
|
||||
### 1. `Notify*` vs `On*` vs `Handle*` 混用
|
||||
### 2.1 `Notify*` vs `On*` vs `Handle*` 混用
|
||||
|
||||
主流模式是 `Notify*`(60+ 处),但存在例外:
|
||||
|
||||
| 文件 | 行号 | 当前命名 | 建议 |
|
||||
|---|---|---|---|
|
||||
| `src/AtomUI.Controls/Primitives/Thumb.cs` | 59-67 | `OnDragStarted` / `OnDragDelta` / `OnDragCompleted` | `NotifyDragStarted` 等 |
|
||||
| `src/AtomUI.Controls/Primitives/Thumb.cs` | 59-67 | `OnDragStarted` / `OnDragDelta` / `OnDragCompleted` | 保留(WPF/Avalonia 标准) |
|
||||
| `src/AtomUI.Controls/Badge/AbstractCountBadge.cs` | 197 | `HandleDecoratedTargetChanged` | `NotifyDecoratedTargetChanged` |
|
||||
| `src/AtomUI.Controls/Badge/AbstractDotBadge.cs` | 178 | `HandleDecoratedTargetChanged` | `NotifyDecoratedTargetChanged` |
|
||||
|
||||
> 注:`Thumb` 的 `On*` 命名是 WPF/Avalonia 标准模式,可能有意保留。需确认。
|
||||
### 2.2 事件触发方法不一致:`Emit*` vs 直接 `Invoke`
|
||||
|
||||
### 2. `CreatePickerPresenter` vs `CreatePresenter`
|
||||
少数 Presenter 通过 `EmitXxxChanged()` 方法触发事件(`DatePickerPresenter` 中的 `EmitChoosingStatueChanged`),其他 Presenter 直接内联 `XxxChanged?.Invoke(...)`。两种风格并存且没有统一规则。
|
||||
|
||||
**建议:** 选一种风格统一(推荐内部触发封装为 `RaiseXxxChanged` 或 `OnXxxChanged` 虚方法,便于子类钩入)。
|
||||
|
||||
### 2.3 `CreatePickerPresenter` vs `CreatePresenter`
|
||||
|
||||
同一概念(创建弹出层 Presenter)有两种命名:
|
||||
|
||||
@@ -112,9 +133,9 @@ Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前
|
||||
| `src/AtomUI.Desktop.Controls.DataGrid/Column/Filters/DataGridTreeFilterFlyout.cs:30` | `CreatePresenter()` |
|
||||
| `src/AtomUI.Desktop.Controls.DataGrid/Column/Filters/DataGridMenuFilterFlyout.cs:20` | `CreatePresenter()` |
|
||||
|
||||
**建议:** 统一为 `CreatePresenter()`(更通用)。
|
||||
**建议:** 统一为 `CreatePresenter()`。
|
||||
|
||||
### 3. `abstract` 与 `Notify*` 命名语义冲突
|
||||
### 2.4 `abstract` 与 `Notify*` 命名语义冲突
|
||||
|
||||
`Notify*` 前缀暗示可选重写的钩子(virtual),但以下方法是 `abstract`(强制重写):
|
||||
|
||||
@@ -125,12 +146,12 @@ Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前
|
||||
|
||||
**建议:** `Create*` 用于强制重写(abstract),`Notify*` 用于可选重写(virtual)。
|
||||
|
||||
### 4. Dispose/Cleanup 模式不统一
|
||||
### 2.5 Dispose/Cleanup 模式不统一
|
||||
|
||||
清理逻辑散布在不同位置:
|
||||
- 部分控件在 `OnApplyTemplate` 中替换旧订阅
|
||||
- 部分控件在 `OnApplyTemplate` 中手工解/绑
|
||||
- 部分控件在 `DetachedFromVisualTree` 事件中清理
|
||||
- 部分控件使用 `CompositeDisposable`(如 `AbstractFloatButtonHost`)
|
||||
- 部分控件使用 `CompositeDisposable`(如 `AbstractFloatButtonHost`、`IconPresenter`)
|
||||
- 没有控件在自身实现 `IDisposable`
|
||||
- `CancellationTokenSource` 内联释放,无统一模式
|
||||
|
||||
@@ -140,7 +161,7 @@ Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前
|
||||
|
||||
## P3 — 枚举/类型碎片化
|
||||
|
||||
### 1. Status 枚举碎片化(7 个独立枚举)
|
||||
### 3.1 Status 枚举碎片化(7 个独立枚举)
|
||||
|
||||
| 枚举 | 文件 | 成员 |
|
||||
|---|---|---|
|
||||
@@ -152,7 +173,7 @@ Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前
|
||||
| `QRCodeStatus` | `AtomUI.Controls/QRCode/QRCodeEnums.cs` | Active, Expired, Loading, Scanned |
|
||||
| `StepsItemStatus` | `AtomUI.Desktop.Controls/Steps/Steps.cs` | Wait, Process, Finish, Error |
|
||||
|
||||
**语义碎片化:**
|
||||
**语义碎片:**
|
||||
|
||||
| 概念 | 分散为 |
|
||||
|---|---|
|
||||
@@ -162,56 +183,54 @@ Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前
|
||||
|
||||
**建议:** 提取公共 `ControlStatus` 基础枚举(Default, Success, Warning, Error, Info, Processing),特殊状态由各控件扩展。
|
||||
|
||||
### 2. Placement 枚举碎片化(6 个独立枚举)
|
||||
### 3.2 Placement 枚举碎片化(6 个独立枚举)
|
||||
|
||||
| 枚举 | 文件 | 成员 |
|
||||
|---|---|---|
|
||||
| `FloatButtonPlacement` | `AtomUI.Controls/FloatButton/...` | Top, Bottom, Left, Right, TopLeft, TopRight, BottomLeft, BottomRight, Center |
|
||||
| `DrawerPlacement` | `AtomUI.Desktop.Controls/Drawer/...` | Left, Top, Right, Bottom |
|
||||
| `AutoCompletePlacementMode` | `AtomUI.Desktop.Controls/AutoComplete/...` | Top, Bottom |
|
||||
| `MentionsPlacementMode` | `AtomUI.Desktop.Controls/Mentions/...` | Top, Bottom |
|
||||
| `TourPlacementMode` | `AtomUI.Desktop.Controls/Tour/...` | Center, Left, LeftTop, LeftBottom, Right, RightTop, RightBottom, Top, TopLeft, TopRight, Bottom, BottomLeft, BottomRight |
|
||||
| `SelectPopupPlacement` | `AtomUI.Desktop.Controls/Select/...` | TopEdgeAlignedLeft, TopEdgeAlignedRight, BottomEdgeAlignedLeft, BottomEdgeAlignedRight |
|
||||
| 枚举 | 成员 |
|
||||
|---|---|
|
||||
| `FloatButtonPlacement` | Top, Bottom, Left, Right, TopLeft, TopRight, BottomLeft, BottomRight, Center |
|
||||
| `DrawerPlacement` | Left, Top, Right, Bottom |
|
||||
| `AutoCompletePlacementMode` | Top, Bottom |
|
||||
| `MentionsPlacementMode` | Top, Bottom |
|
||||
| `TourPlacementMode` | Center, Left, LeftTop, LeftBottom, Right, RightTop, RightBottom, Top, TopLeft, TopRight, Bottom, BottomLeft, BottomRight |
|
||||
| `SelectPopupPlacement` | TopEdgeAlignedLeft, TopEdgeAlignedRight, BottomEdgeAlignedLeft, BottomEdgeAlignedRight |
|
||||
|
||||
**命名不一致:** `Placement` vs `PlacementMode` vs `PopupPlacement`。
|
||||
|
||||
**建议:** 对于 Top/Bottom 类弹出位置,统一使用 `PopupPlacementMode`;对于 8 方位 + Center,使用 `Placement`(已有 `Avalonia.Controls.PlacementMode` 可复用)。
|
||||
**建议:** Top/Bottom 类用 `PopupPlacementMode`;8 方位 + Center 用 `Placement`(可复用 `Avalonia.Controls.PlacementMode`)。
|
||||
|
||||
### 3. `CountBadgeSize` 应复用共享 `SizeType`
|
||||
### 3.3 `CountBadgeSize` 应复用共享 `SizeType`
|
||||
|
||||
| 枚举 | 文件 | 成员 |
|
||||
|---|---|---|
|
||||
| `CountBadgeSize` | `AtomUI.Controls/Badge/AbstractCountBadge.cs` | Default, Small |
|
||||
| `SizeType`(共享) | `AtomUI.Core/Common.cs` | Large, Middle, Small |
|
||||
| 枚举 | 成员 |
|
||||
|---|---|
|
||||
| `CountBadgeSize`(`AbstractCountBadge.cs`) | Default, Small |
|
||||
| `SizeType`(`AtomUI.Core/Common.cs`) | Large, Middle, Small |
|
||||
|
||||
`CountBadgeSize` 只有 `Default` 和 `Small`,完全可以复用 `SizeType`。
|
||||
`CountBadgeSize` 完全可以复用 `SizeType`。
|
||||
|
||||
### 4. Token 类访问修饰符不一致
|
||||
### 3.4 Token 类访问修饰符不一致
|
||||
|
||||
| Token 类 | 访问修饰符 |
|
||||
|---|---|
|
||||
| `ColorPickerToken` | `public` |
|
||||
| `ColorPickerToken`(`src/AtomUI.Desktop.Controls.ColorPicker/ColorPickerToken.cs`) | `public` |
|
||||
| 其他 80+ Token 类 | `internal` |
|
||||
|
||||
**文件:** `src/AtomUI.Desktop.Controls.ColorPicker/ColorPickerToken.cs`
|
||||
**建议:** 若无外部消费需要,统一为 `internal`;若必须公开,将 80+ 其余 Token 也公开以保持一致。需要先确认设计意图。
|
||||
|
||||
**建议:** 统一为 `internal`(Token 类是实现细节,不应暴露给外部)。
|
||||
### 3.5 `DataGridSortDirections` 复数命名
|
||||
|
||||
### 5. `DataGridSortDirections` 复数命名
|
||||
`[Flags]` 枚举使用复数 `DataGridSortDirections`,但其他 `[Flags]` 枚举(`DataGridGridLinesVisibility`、`DataGridHeadersVisibility`、`DataGridPaginationVisibility`)使用单数。
|
||||
|
||||
`[Flags]` 枚举使用复数 `DataGridSortDirections`,但其他 `[Flags]` 枚举(如 `DataGridGridLinesVisibility`)使用单数。
|
||||
**文件:** `src/AtomUI.Desktop.Controls.DataGrid/DataGridEnumerations.cs:90`
|
||||
|
||||
**文件:** `src/AtomUI.Desktop.Controls.DataGrid/DataGridEnumerations.cs`
|
||||
|
||||
**建议:** 统一使用单数。
|
||||
**建议:** 重命名为 `DataGridSortDirection` 保持单数统一。
|
||||
|
||||
---
|
||||
|
||||
## P4 — AXAML 模板不一致
|
||||
## P4 — AXAML 模板 / 伪类 / Part 常量不一致
|
||||
|
||||
### 1. PART_ 命名不一致
|
||||
### 4.1 PART_ 前缀缺失
|
||||
|
||||
主流控件全部使用 `PART_` 前缀(577 处),但以下控件使用裸名:
|
||||
主流控件全部使用 `PART_` 前缀(577+ 处),但以下控件使用裸名:
|
||||
|
||||
**ColorPicker 主题:**
|
||||
|
||||
@@ -220,6 +239,8 @@ Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前
|
||||
| `ColorPicker/Themes/ColorBlockTheme.axaml` | `ColorPreviewFrame`, `ColorPreview`, `EmptyColorFrame` |
|
||||
| `ColorPicker/Themes/ColorPickerTheme.axaml` | `Frame`, `ColorText` |
|
||||
| `ColorPicker/Themes/GradientColorPickerTheme.axaml` | `Frame`, `ColorText` |
|
||||
| `ColorPicker/Themes/ColorView/ColorPickerInput.axaml` | `RootLayout`, `HsvValuePanel`, `RgbValuePanel` |
|
||||
| `ColorPicker/Themes/ColorView/ColorSpectrumTheme.axaml` | `Frame` |
|
||||
|
||||
**DataGrid 主题:**
|
||||
|
||||
@@ -234,29 +255,44 @@ Avalonia/WPF 规范要求布尔属性使用 `Is`、`Has`、`Can`、`Should` 前
|
||||
|---|---|
|
||||
| `FloatButton/Themes/FloatButtonItemsControlTheme.axaml` | `ItemsLayout` |
|
||||
|
||||
**建议:** 全部加上 `PART_` 前缀,与 Avalonia 规范一致。
|
||||
**建议:** 全部加上 `PART_` 前缀。
|
||||
|
||||
### 2. `:active` vs `:pressed` 伪类不一致
|
||||
### 4.2 Part 常量覆盖率不足
|
||||
|
||||
| 文件 | 伪类 |
|
||||
|---|---|
|
||||
| `ColorPicker/Themes/ColorSlider/ColorSliderThumbTheme.axaml:39` | `^:active` |
|
||||
| 其他所有主题 | `^:pressed` |
|
||||
全解决方案 137 个文件直接出现 `"PART_..."` 字面量,但只有 75 处定义为 `const string * = "PART_*"`。绝大多数控件仍硬编码 part 名(例如 `Input/SearchEditDecoratedBox.cs` 的 `e.NameScope.Find<Button>("PART_RightAddOn")`),没有集中到 `*ThemeConstants` 类。
|
||||
|
||||
**建议:** 为每个控件定义 `XxxThemeConstants`(或扩展现有的 `AddOnDecoratedBoxThemeConstants`),所有 Part 名通过常量引用。便于重命名和静态分析。
|
||||
|
||||
### 4.3 内联伪类字符串(绕过常量约定)
|
||||
|
||||
代码库里大多数伪类已集中在 `XxxPseudoClass` 常量类(如 `AddOnDecoratedBoxPseudoClass.Outline`),但以下两处直接写字面量:
|
||||
|
||||
| 文件 | 行号 | 字面量 |
|
||||
|---|---|---|
|
||||
| `src/AtomUI.Desktop.Controls.ColorPicker/AbstractColorPicker.cs` | 998 | `PseudoClasses.Set(":flyout-open", ...)` |
|
||||
| `src/AtomUI.Desktop.Controls/Tooltip/ToolTip.cs` | 466 | `PseudoClasses.Set(":open", ...)` |
|
||||
|
||||
**建议:** 提取为 `ColorPickerPseudoClass.FlyoutOpen`、`ToolTipPseudoClass.Open`。
|
||||
|
||||
### 4.4 `:active` vs `:pressed` 伪类不一致
|
||||
|
||||
| 文件 | 行号 | 伪类 |
|
||||
|---|---|---|
|
||||
| `ColorPicker/Themes/ColorSlider/ColorSliderThumbTheme.axaml` | 39 | `^:focus,^:active` |
|
||||
| 其他所有主题 | — | `^:pressed` |
|
||||
|
||||
**建议:** 统一使用 `:pressed`(Avalonia 标准伪类)。
|
||||
|
||||
### 3. `Name=` vs `x:Name=` 混用
|
||||
### 4.5 `Name=` vs `x:Name=` 混用
|
||||
|
||||
| 用法 | 数量 |
|
||||
|---|---|
|
||||
| `Name="PART_*"` | 566 处 |
|
||||
| `x:Name="PART_*"` | 11 处 |
|
||||
|
||||
Avalonia 两种写法等效,但混用不规范。
|
||||
|
||||
**建议:** 统一使用 `Name=`(Avalonia 原生属性)。
|
||||
|
||||
### 4. 5 个 ControlTheme 用字符串 Key
|
||||
### 4.6 字符串 Key 的 ControlTheme(5 处)
|
||||
|
||||
以下主题使用字符串 Key 而非类型 Key:
|
||||
|
||||
@@ -268,7 +304,7 @@ Avalonia 两种写法等效,但混用不规范。
|
||||
| `TabControl/Themes/TabStrip/TabStripItemTheme.axaml` | `x:Key="TabStripItemTheme"` |
|
||||
| `TabControl/Themes/TabStrip/CardTabStripItemTheme.axaml` | `x:Key="CardTabStripItemTheme"` |
|
||||
|
||||
这些都是同一控件类型的变体主题,使用字符串 Key 可能是有意设计(用于主题切换)。需确认是否应该改为类型 Key。
|
||||
这些都是同一控件类型的变体主题,使用字符串 Key 可能是有意设计(主题切换)。**需先确认设计意图**再决定是否迁移为类型 Key。
|
||||
|
||||
---
|
||||
|
||||
@@ -276,15 +312,28 @@ Avalonia 两种写法等效,但混用不规范。
|
||||
|
||||
| 优先级 | 类别 | 数量 | 是否 Breaking |
|
||||
|---|---|---|---|
|
||||
| P0 | 拼写错误 | 3 处 | 是(公共 API) |
|
||||
| P1 | 布尔属性缺 Is 前缀 | 14 处 | 是(公共 API) |
|
||||
| P2 | 方法命名不一致 | 4 类 | 部分 |
|
||||
| P0 | 拼写错误 | 2 处(21+ 引用 / 4 文件 + 1 枚举) | 是(公共 API) |
|
||||
| P1 | 布尔属性命名 | 22 处(1.1=15 + 1.2=10 + 1.3=3,去重) | 是(公共 API) |
|
||||
| P2 | 方法/模式不一致 | 5 类 | 部分 |
|
||||
| P3 | 枚举/类型碎片化 | 5 类 | 是(架构层面) |
|
||||
| P4 | AXAML 模板不一致 | 4 类 | 否(内部实现) |
|
||||
| P4 | AXAML/伪类/Part 常量 | 6 类 | 否(内部实现) |
|
||||
|
||||
**建议处理顺序:**
|
||||
1. P0 拼写错误 — 影响最小,优先修复
|
||||
2. P4 AXAML 模板 — 不影响公共 API,可安全修复
|
||||
3. P1 布尔属性 — Breaking change,需配合版本发布
|
||||
4. P2 方法命名 — 逐步统一
|
||||
5. P3 枚举碎片化 — 架构重构,影响面最大
|
||||
|
||||
1. **P0 拼写错误** — 影响最小、收益明显,优先修复,为 P1 批量重命名做参考样例
|
||||
2. **P4.3 内联伪类字符串** 与 **P4.2 Part 常量提取** — 只是内部重构,不破坏 API,顺手能做
|
||||
3. **P4.1/4.4/4.5** AXAML 统一 — 内部主题,风险可控
|
||||
4. **P1 布尔属性** — Breaking,需配合版本发布并提供 `[Obsolete]` 别名
|
||||
5. **P2 方法命名** — 逐步统一,可与 P1 同批发布
|
||||
6. **P3 枚举碎片化** — 架构重构,影响面最大,需专门设计评审
|
||||
|
||||
---
|
||||
|
||||
## 变更日志
|
||||
|
||||
- **2026-05-10 修订:**
|
||||
- 删除"重复 ToolTip 目录"条目(已修复,当前仅存 `Tooltip/`)
|
||||
- P0-1 受影响文件从 4 个扩充为 7 个,补充 `EmitChoosingStatueChanged` 方法
|
||||
- P1 从 14 处扩充为 22 处,拆分为 1.1/1.2/1.3 三组
|
||||
- P2 新增 2.2 "事件触发方法 `Emit*` vs `Invoke`" 条目
|
||||
- P4 拆分并新增 4.2 "Part 常量覆盖率不足"、4.3 "内联伪类字符串"两条
|
||||
|
||||
Reference in New Issue
Block a user