using System.Runtime.InteropServices; using System.Windows; using System.Windows.Forms; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; namespace OmniNotify; public partial class PopupWindow : Window { private const double ShellBorderThickness = 1; private const double BodyTopMargin = 8; private const int GwlExStyle = -20; private const int WsExTransparent = 0x20; private const int WsExNoActivate = 0x08000000; private const int WsExToolWindow = 0x00000080; private readonly NotificationProfile _profile; private readonly DispatcherTimer _timer = new(); private double _bodyViewportHeight; private int _slot; public PopupWindow(NotificationProfile profile, IncomingMessage message, int slot) { InitializeComponent(); _profile = profile; _slot = slot; Width = profile.Width; MaxHeight = profile.MaxHeight; Opacity = profile.OverallOpacity; TitleText.Text = message.Title; BodyText.Text = message.Body; ApplyProfile(); _timer.Interval = TimeSpan.FromSeconds(Math.Max(0.5, GetInitialLifetimeSeconds(profile))); Loaded += (_, _) => { ApplyClickThrough(); LayoutBodyText(); StartOverflowBehavior(); MoveToSlot(_slot, false); AnimateIn(); }; _timer.Tick += (_, _) => { _timer.Stop(); AnimateOut(); }; } public void MoveToSlot(int slot, bool animated = true) { _slot = slot; var (left, top) = CalculatePosition(slot); if (!animated) { Left = left; Top = top; return; } BeginAnimation(LeftProperty, new DoubleAnimation(left, TimeSpan.FromMilliseconds(180)) { EasingFunction = new CubicEase() }); BeginAnimation(TopProperty, new DoubleAnimation(top, TimeSpan.FromMilliseconds(180)) { EasingFunction = new CubicEase() }); } public void CloseImmediately() { _timer.Stop(); Close(); } private void ApplyProfile() { Shell.BorderThickness = new Thickness(ShellBorderThickness); Shell.Padding = new Thickness(_profile.Padding); Shell.Background = Brush(_profile.BackgroundColor, _profile.BackgroundOpacity); Shell.BorderBrush = Brush(_profile.BorderColor, _profile.BorderOpacity); Shell.ClipToBounds = true; ContentPanel.ClipToBounds = true; ContentPanel.MaxHeight = Math.Max(80, GetPanelMaxHeight(_profile)); TitleText.Foreground = Brush(_profile.TextColor, 1); BodyText.Foreground = Brush(_profile.TextColor, 0.9); TitleText.FontFamily = new System.Windows.Media.FontFamily(_profile.FontFamily); BodyText.FontFamily = new System.Windows.Media.FontFamily(_profile.FontFamily); TitleText.FontSize = _profile.TitleFontSize; BodyText.FontSize = _profile.BodyFontSize; BodyText.LineStackingStrategy = LineStackingStrategy.BlockLineHeight; BodyText.LineHeight = GetBodyLineHeight(_profile); _bodyViewportHeight = GetBodyViewportHeight(_profile); BodyText.RenderTransform = null; BodyCanvas.RenderTransform = null; switch (_profile.OverflowMode) { case OverflowMode.VerticalScroll: BodyViewport.Height = _bodyViewportHeight; BodyViewport.MaxHeight = _bodyViewportHeight; BodyText.TextWrapping = TextWrapping.Wrap; BodyText.TextTrimming = TextTrimming.None; break; case OverflowMode.Split: BodyViewport.Height = _bodyViewportHeight; BodyViewport.MaxHeight = _bodyViewportHeight; BodyText.TextWrapping = TextWrapping.Wrap; BodyText.TextTrimming = TextTrimming.None; break; default: BodyViewport.Height = Math.Max(_profile.BodyFontSize * 1.6, _profile.BodyFontSize + 4); BodyViewport.MaxHeight = BodyViewport.Height; BodyText.TextWrapping = TextWrapping.NoWrap; BodyText.TextTrimming = TextTrimming.CharacterEllipsis; break; } } private void StartOverflowBehavior() { if (_profile.OverflowMode != OverflowMode.VerticalScroll) { return; } var overflow = BodyText.DesiredSize.Height - BodyViewport.ActualHeight; var hold = TimeSpan.FromSeconds(Math.Max(0, _profile.VerticalScrollHoldSeconds)); if (overflow <= 1) { _timer.Interval = ClampTimerInterval(hold + hold); return; } var transform = new TranslateTransform(); BodyCanvas.RenderTransform = transform; var speed = Math.Max(1, _profile.VerticalScrollSpeed); var duration = TimeSpan.FromSeconds(overflow / speed); _timer.Interval = ClampTimerInterval(hold + duration + hold); var animation = new DoubleAnimation { From = 0, To = -overflow, BeginTime = hold, Duration = new Duration(duration), FillBehavior = FillBehavior.HoldEnd }; transform.BeginAnimation(TranslateTransform.YProperty, animation); } private void LayoutBodyText() { var viewportWidth = Math.Max(1, BodyViewport.ActualWidth); if (viewportWidth <= 1) { viewportWidth = GetBodyContentWidth(_profile); } _bodyViewportHeight = GetBodyViewportHeight(_profile, TitleText.ActualHeight); BodyText.Width = viewportWidth; BodyCanvas.Width = viewportWidth; BodyText.Measure(new System.Windows.Size(viewportWidth, double.PositiveInfinity)); var textHeight = Math.Ceiling(BodyText.DesiredSize.Height + 2); if (_profile.OverflowMode == OverflowMode.Split) { BodyViewport.Height = Math.Min(_bodyViewportHeight, textHeight); BodyViewport.MaxHeight = BodyViewport.Height; BodyCanvas.Height = textHeight; } else if (_profile.OverflowMode == OverflowMode.VerticalScroll) { BodyViewport.Height = _bodyViewportHeight; BodyViewport.MaxHeight = _bodyViewportHeight; BodyCanvas.Height = textHeight; } else { BodyCanvas.Height = Math.Max(BodyViewport.ActualHeight, textHeight); } BodyText.Arrange(new Rect(0, 0, viewportWidth, textHeight)); } private void AnimateIn() { switch (_profile.EnterAnimation) { case PopupAnimation.Zoom: RenderTransformOrigin = new System.Windows.Point(0.5, 0.5); RenderTransform = new ScaleTransform(0.92, 0.92); RenderTransform.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(180))); RenderTransform.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(180))); break; case PopupAnimation.Slide: Left += 18; BeginAnimation(LeftProperty, new DoubleAnimation(Left - 18, TimeSpan.FromMilliseconds(180)) { EasingFunction = new CubicEase() }); break; } BeginAnimation(OpacityProperty, new DoubleAnimation(0, _profile.OverallOpacity, TimeSpan.FromMilliseconds(160))); _timer.Start(); } private void AnimateOut() { var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(160)); animation.Completed += (_, _) => Close(); BeginAnimation(OpacityProperty, animation); } private (double Left, double Top) CalculatePosition(int slot) { var screens = Screen.AllScreens; var screen = GetWorkingAreaInDips(screens[Math.Clamp(_profile.ScreenIndex, 0, screens.Length - 1)]); var height = ActualHeight > 1 ? ActualHeight : _profile.MaxHeight; var gap = 12; var step = height + gap; var x = _profile.Anchor switch { ScreenAnchor.TopLeft or ScreenAnchor.MiddleLeft or ScreenAnchor.BottomLeft => screen.Left + _profile.MarginX, ScreenAnchor.TopCenter or ScreenAnchor.Center or ScreenAnchor.BottomCenter => screen.Left + (screen.Width - Width) / 2, _ => screen.Right - Width - _profile.MarginX }; var y = _profile.Anchor switch { ScreenAnchor.TopLeft or ScreenAnchor.TopCenter or ScreenAnchor.TopRight => screen.Top + _profile.MarginY + slot * step, ScreenAnchor.MiddleLeft or ScreenAnchor.Center or ScreenAnchor.MiddleRight => screen.Top + (screen.Height - height) / 2 + slot * step, _ => screen.Bottom - height - _profile.MarginY - slot * step }; return ( Math.Clamp(x, screen.Left, Math.Max(screen.Left, screen.Right - Width)), Math.Clamp(y, screen.Top, Math.Max(screen.Top, screen.Bottom - height)) ); } private Rect GetWorkingAreaInDips(Screen screen) { var source = PresentationSource.FromVisual(this); var transform = source?.CompositionTarget?.TransformFromDevice ?? Matrix.Identity; var area = screen.WorkingArea; var topLeft = transform.Transform(new System.Windows.Point(area.Left, area.Top)); var bottomRight = transform.Transform(new System.Windows.Point(area.Right, area.Bottom)); return new Rect(topLeft, bottomRight); } private static System.Windows.Media.Brush Brush(string color, double opacity) { try { var brush = (SolidColorBrush)new BrushConverter().ConvertFromString(color)!; brush.Opacity = Math.Clamp(opacity, 0, 1); return brush; } catch { return new SolidColorBrush(Colors.White) { Opacity = Math.Clamp(opacity, 0, 1) }; } } public static double GetBodyViewportHeight(NotificationProfile profile) { return GetBodyViewportHeight(profile, profile.TitleFontSize * 1.6); } public static double GetBodyViewportHeight(NotificationProfile profile, double titleHeight) { return Math.Max( profile.BodyFontSize * 1.8, profile.MaxHeight - (ShellBorderThickness * 2) - (profile.Padding * 2) - titleHeight - BodyTopMargin); } public static double GetBodyLineHeight(NotificationProfile profile) { return Math.Ceiling(profile.BodyFontSize * 1.35); } public static double GetBodyContentWidth(NotificationProfile profile) { return Math.Max(1, profile.Width - (ShellBorderThickness * 2) - (profile.Padding * 2)); } private static double GetPanelMaxHeight(NotificationProfile profile) { return profile.MaxHeight - (ShellBorderThickness * 2) - (profile.Padding * 2); } private static double GetInitialLifetimeSeconds(NotificationProfile profile) { return profile.OverflowMode == OverflowMode.VerticalScroll ? profile.VerticalScrollHoldSeconds * 2 : profile.LifetimeSeconds; } private static TimeSpan ClampTimerInterval(TimeSpan interval) { return interval < TimeSpan.FromMilliseconds(500) ? TimeSpan.FromMilliseconds(500) : interval; } private void ApplyClickThrough() { var handle = new WindowInteropHelper(this).Handle; var style = GetWindowLong(handle, GwlExStyle); SetWindowLong(handle, GwlExStyle, style | WsExTransparent | WsExNoActivate | WsExToolWindow); } [DllImport("user32.dll")] private static extern int GetWindowLong(IntPtr hwnd, int index); [DllImport("user32.dll")] private static extern int SetWindowLong(IntPtr hwnd, int index, int value); }