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(); } } /// /// 窗口刚打开时 shell 可能还没给它登记 ApplicationView(GetViewForHwnd 报 /// TYPE_E_ELEMENTNOTFOUND),故短间隔重试若干次直到成功 /// 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(); /// /// 淡出后再真正关闭 /// 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(); } }