b89223fa54
- 定位改为 水平(左/中/右) × 垂直(上/中/下) + 边缘留白,取代四角枚举 - 完成类通知在目标窗口已聚焦时用更短停留(FocusedDurationSeconds) - 托盘左键单击直接打开设置,右键菜单仅保留退出 - 修复 host 拉起用 UseShellExecute=true,脱离钩子 stdout 管道,避免 Claude 卡在 running stop hook
198 lines
5.5 KiB
C#
198 lines
5.5 KiB
C#
using System;
|
||
using Avalonia;
|
||
using Avalonia.Animation;
|
||
using Avalonia.Controls;
|
||
using Avalonia.Input;
|
||
using Avalonia.Interactivity;
|
||
using Avalonia.Media;
|
||
using Avalonia.Styling;
|
||
using Avalonia.Threading;
|
||
using Notify.Models;
|
||
using Notify.ViewModels;
|
||
|
||
namespace Notify.Views;
|
||
|
||
public partial class ToastWindow : Window
|
||
{
|
||
private static readonly Color BorderNormal = Color.Parse("#FF4B64B2");
|
||
private static readonly Color BorderInput = Color.Parse("#FF00CFCF");
|
||
|
||
private readonly ToastSettings _settings;
|
||
private readonly DispatcherTimer _dismissTimer;
|
||
private readonly bool _sticky;
|
||
private readonly long _targetHwnd;
|
||
private readonly string? _wtRuntimeId;
|
||
private Avalonia.Media.Imaging.Bitmap? _appIcon;
|
||
private bool _closing;
|
||
|
||
// 设计器需要的无参构造
|
||
public ToastWindow() : this(new ToastViewModel(new ToastRequest { Title = "Title", Message = "Message" }), new ToastSettings(), false, 0, null, null, null)
|
||
{
|
||
}
|
||
|
||
public ToastWindow(ToastViewModel vm, ToastSettings settings, bool sticky, long targetHwnd, string? wtRuntimeId, string? iconPath, int? durationOverride)
|
||
{
|
||
_settings = settings;
|
||
_targetHwnd = targetHwnd;
|
||
_wtRuntimeId = wtRuntimeId;
|
||
var duration = durationOverride ?? settings.DurationSeconds;
|
||
// 常驻:请求显式 Sticky,或停留时长 <= 0
|
||
_sticky = sticky || duration <= 0;
|
||
InitializeComponent();
|
||
DataContext = vm;
|
||
|
||
Width = settings.Width;
|
||
Opacity = 0;
|
||
|
||
// Opacity 过渡用于淡入/淡出
|
||
Transitions =
|
||
[
|
||
new DoubleTransition
|
||
{
|
||
Property = OpacityProperty,
|
||
Duration = TimeSpan.FromMilliseconds(settings.FadeMilliseconds),
|
||
Easing = new Avalonia.Animation.Easings.CubicEaseOut(),
|
||
},
|
||
];
|
||
|
||
Root.BorderBrush = new SolidColorBrush(vm.InputMode ? BorderInput : BorderNormal);
|
||
|
||
// 调用方 App 图标,取不到则保留默认 Claude 图标
|
||
if (!string.IsNullOrEmpty(iconPath))
|
||
{
|
||
_appIcon = Notify.Interop.AppIcon.Extract(iconPath);
|
||
if (_appIcon is not null)
|
||
{
|
||
IconImage.Source = _appIcon;
|
||
}
|
||
}
|
||
|
||
_dismissTimer = new DispatcherTimer
|
||
{
|
||
Interval = TimeSpan.FromSeconds(Math.Max(1, duration)),
|
||
};
|
||
_dismissTimer.Tick += (_, _) => BeginClose();
|
||
}
|
||
|
||
protected override void OnClosed(EventArgs e)
|
||
{
|
||
base.OnClosed(e);
|
||
_appIcon?.Dispose();
|
||
_appIcon = null;
|
||
}
|
||
|
||
protected override void OnOpened(EventArgs e)
|
||
{
|
||
base.OnOpened(e);
|
||
Opacity = _settings.Opacity; // 触发淡入
|
||
|
||
if (TryGetPlatformHandle()?.Handle is { } hwnd)
|
||
{
|
||
// 工具窗口:从任务栏与 Alt+Tab 中隐藏
|
||
Notify.Interop.Win32.MakeToolWindow(hwnd);
|
||
|
||
// 跨虚拟桌面:把窗口钉到所有桌面(失败自动忽略)
|
||
if (_settings.ShowOnAllDesktops)
|
||
{
|
||
TryPinWithRetry(hwnd);
|
||
}
|
||
}
|
||
|
||
if (!_sticky)
|
||
{
|
||
_dismissTimer.Start();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 窗口刚打开时 shell 可能还没给它登记 ApplicationView(GetViewForHwnd 报
|
||
/// TYPE_E_ELEMENTNOTFOUND),故短间隔重试若干次直到成功
|
||
/// </summary>
|
||
private void TryPinWithRetry(IntPtr hwnd)
|
||
{
|
||
if (Notify.Interop.VirtualDesktopPinner.TryPin(hwnd))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var attempts = 0;
|
||
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(60) };
|
||
timer.Tick += (_, _) =>
|
||
{
|
||
attempts++;
|
||
if (_closing || Notify.Interop.VirtualDesktopPinner.TryPin(hwnd) || attempts >= 15)
|
||
{
|
||
timer.Stop();
|
||
}
|
||
};
|
||
timer.Start();
|
||
}
|
||
|
||
private void OnPointerEntered(object? sender, PointerEventArgs e)
|
||
{
|
||
// 悬停时暂停自动消失
|
||
_dismissTimer.Stop();
|
||
if (!_closing)
|
||
{
|
||
Opacity = _settings.Opacity;
|
||
}
|
||
}
|
||
|
||
private void OnPointerExited(object? sender, PointerEventArgs e)
|
||
{
|
||
if (!_closing && !_sticky)
|
||
{
|
||
_dismissTimer.Start();
|
||
}
|
||
}
|
||
|
||
private void OnBodyPressed(object? sender, PointerPressedEventArgs e)
|
||
{
|
||
// 左键点击主体:激活原窗口后关闭
|
||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||
{
|
||
if (_targetHwnd != 0)
|
||
{
|
||
var target = new IntPtr(_targetHwnd);
|
||
Notify.Interop.WindowActivator.Activate(target);
|
||
|
||
// 目标是 Windows Terminal 则切回原标签
|
||
if (!string.IsNullOrEmpty(_wtRuntimeId))
|
||
{
|
||
Notify.Interop.WinTerminalTabs.SelectTab(target, _wtRuntimeId);
|
||
}
|
||
}
|
||
|
||
BeginClose();
|
||
}
|
||
}
|
||
|
||
private void OnCloseClick(object? sender, RoutedEventArgs e) => BeginClose();
|
||
|
||
/// <summary>
|
||
/// 淡出后再真正关闭
|
||
/// </summary>
|
||
private void BeginClose()
|
||
{
|
||
if (_closing)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_closing = true;
|
||
_dismissTimer.Stop();
|
||
Opacity = 0;
|
||
|
||
var closeTimer = new DispatcherTimer
|
||
{
|
||
Interval = TimeSpan.FromMilliseconds(_settings.FadeMilliseconds),
|
||
};
|
||
closeTimer.Tick += (_, _) =>
|
||
{
|
||
closeTimer.Stop();
|
||
Close();
|
||
};
|
||
closeTimer.Start();
|
||
}
|
||
}
|