Files
notify/Notify/Views/ToastWindow.axaml.cs
T
chuan f390bcd133 feat: 提示音、调用方图标与消息清洗(P2)
- 提示音:winmm 从内存异步播放打包 wav,可在设置开关,带防连环音
- 调用方图标:进程树上溯识别编辑器/终端,ExtractIconEx + GDI 转 Avalonia 位图显示
- 进程树跳过 shell/运行时,取不到图标回退默认 Claude 图标
- 消息清洗:折叠换行/多余空白为单行,避免撑乱 toast 布局
- 全程 LibraryImport/源生成,AOT 友好
2026-06-22 16:00:59 +08:00

197 lines
5.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
{
}
public ToastWindow(ToastViewModel vm, ToastSettings settings, bool sticky, long targetHwnd, string? wtRuntimeId, string? iconPath)
{
_settings = settings;
_targetHwnd = targetHwnd;
_wtRuntimeId = wtRuntimeId;
// 常驻:请求显式 Sticky,或全局停留时长 <= 0
_sticky = sticky || settings.DurationSeconds <= 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, settings.DurationSeconds)),
};
_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 可能还没给它登记 ApplicationViewGetViewForHwnd 报
/// 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();
}
}