b89223fa54
- 定位改为 水平(左/中/右) × 垂直(上/中/下) + 边缘留白,取代四角枚举 - 完成类通知在目标窗口已聚焦时用更短停留(FocusedDurationSeconds) - 托盘左键单击直接打开设置,右键菜单仅保留退出 - 修复 host 拉起用 UseShellExecute=true,脱离钩子 stdout 管道,避免 Claude 卡在 running stop hook
121 lines
3.3 KiB
C#
121 lines
3.3 KiB
C#
using System.Collections.Generic;
|
|
using Avalonia;
|
|
using Avalonia.Platform;
|
|
using Notify.Models;
|
|
using Notify.ViewModels;
|
|
using Notify.Views;
|
|
|
|
namespace Notify.Services;
|
|
|
|
/// <summary>
|
|
/// 在常驻进程内管理所有 toast 窗口:创建、按角落堆叠、关闭后重新排布
|
|
/// 这是 Rust 版"每条通知一进程 + EnumWindows"的替代——进程内一个列表即可
|
|
/// </summary>
|
|
public sealed class ToastManager
|
|
{
|
|
private const int Gap = 8;
|
|
|
|
private readonly SettingsService _settings;
|
|
private readonly List<ToastWindow> _active = [];
|
|
private readonly Queue<ToastRequest> _pending = new();
|
|
|
|
public ToastManager(SettingsService settings) => _settings = settings;
|
|
|
|
public void Show(ToastRequest request)
|
|
{
|
|
var settings = _settings.Current;
|
|
|
|
if (_active.Count >= settings.MaxVisible)
|
|
{
|
|
_pending.Enqueue(request);
|
|
return;
|
|
}
|
|
|
|
var vm = new ToastViewModel(request);
|
|
var window = new ToastWindow(vm, settings, request.Sticky, request.TargetHwnd, request.WtRuntimeId, request.IconPath, request.DurationSecondsOverride);
|
|
window.Closed += OnToastClosed;
|
|
|
|
_active.Add(window);
|
|
// 先显示(拿到尺寸/屏幕信息),再排布
|
|
window.Show();
|
|
Arrange();
|
|
}
|
|
|
|
private void OnToastClosed(object? sender, System.EventArgs e)
|
|
{
|
|
if (sender is ToastWindow w)
|
|
{
|
|
w.Closed -= OnToastClosed;
|
|
_active.Remove(w);
|
|
}
|
|
|
|
Arrange();
|
|
|
|
if (_pending.Count > 0 && _active.Count < _settings.Current.MaxVisible)
|
|
{
|
|
Show(_pending.Dequeue());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 把所有活动 toast 从指定角落沿垂直方向依次堆叠
|
|
/// </summary>
|
|
private void Arrange()
|
|
{
|
|
if (_active.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var anchor = _active[0];
|
|
var screen = anchor.Screens.ScreenFromWindow(anchor) ?? anchor.Screens.Primary;
|
|
if (screen is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var settings = _settings.Current;
|
|
var wa = screen.WorkingArea; // 物理像素
|
|
var scale = anchor.RenderScaling;
|
|
var margin = (int)(settings.Margin * scale);
|
|
var gap = (int)(Gap * scale);
|
|
|
|
// 垂直靠下时向上堆叠,否则从锚点向下堆叠
|
|
var stackUp = settings.Vertical == VEdge.Bottom;
|
|
var cursor = settings.Vertical switch
|
|
{
|
|
VEdge.Top => wa.Y + margin,
|
|
VEdge.Bottom => wa.Bottom - margin,
|
|
_ => wa.Y + wa.Height / 2,
|
|
};
|
|
|
|
foreach (var toast in _active)
|
|
{
|
|
var wPx = (int)(toast.Width * scale);
|
|
var hPx = (int)(toast.Bounds.Height * scale);
|
|
|
|
var x = settings.Horizontal switch
|
|
{
|
|
HEdge.Left => wa.X + margin,
|
|
HEdge.Right => wa.Right - margin - wPx,
|
|
_ => wa.X + (wa.Width - wPx) / 2,
|
|
};
|
|
|
|
int y;
|
|
if (stackUp)
|
|
{
|
|
cursor -= hPx;
|
|
y = cursor;
|
|
cursor -= gap;
|
|
}
|
|
else
|
|
{
|
|
y = cursor;
|
|
cursor += hPx + gap;
|
|
}
|
|
|
|
toast.Position = new PixelPoint(x, y);
|
|
}
|
|
}
|
|
}
|