mirror of
https://github.com/9ParsonsB/Pulsar.git
synced 2025-04-05 17:39:39 -04:00
* Implement click-through windows for X11 * Adjust the "platform specific" comment * Get rid of accidental whitespace * Move else declaration into next line
256 lines
9.4 KiB
C#
256 lines
9.4 KiB
C#
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Markup.Xaml;
|
|
using Observatory.UI.ViewModels;
|
|
using System;
|
|
using System.Reflection;
|
|
using System.Timers;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace Observatory.UI.Views
|
|
{
|
|
public partial class NotificationView : Window
|
|
{
|
|
private readonly double scale;
|
|
private readonly System.Timers.Timer timer;
|
|
private readonly Guid guid;
|
|
private bool defaultPosition = true;
|
|
private PixelPoint originalPosition;
|
|
|
|
public NotificationView() : this(default)
|
|
{ }
|
|
|
|
public NotificationView(Guid guid)
|
|
{
|
|
this.guid = guid;
|
|
InitializeComponent();
|
|
SystemDecorations = SystemDecorations.None;
|
|
ShowActivated = false;
|
|
ShowInTaskbar = false;
|
|
MakeClickThrough(); //Platform specific, currently windows and Linux (X11) only.
|
|
|
|
this.DataContextChanged += NotificationView_DataContextChanged;
|
|
scale = Properties.Core.Default.NativeNotifyScale / 100.0;
|
|
|
|
AdjustText();
|
|
|
|
AdjustPanel();
|
|
|
|
AdjustPosition();
|
|
|
|
timer = new();
|
|
timer.Elapsed += CloseNotification;
|
|
timer.Interval = Properties.Core.Default.NativeNotifyTimeout;
|
|
timer.Start();
|
|
|
|
#if DEBUG
|
|
this.AttachDevTools();
|
|
#endif
|
|
}
|
|
|
|
public Guid Guid { get => guid; }
|
|
|
|
public void AdjustOffset(bool increase)
|
|
{
|
|
if (defaultPosition)
|
|
{
|
|
if (increase || Position != originalPosition)
|
|
{
|
|
var corner = Properties.Core.Default.NativeNotifyCorner;
|
|
|
|
if ((corner >= 2 && increase) || (corner <= 1 && !increase))
|
|
{
|
|
Position += new PixelPoint(0, Convert.ToInt32(Height));
|
|
}
|
|
else
|
|
{
|
|
Position -= new PixelPoint(0, Convert.ToInt32(Height));
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
private void NotificationView_DataContextChanged(object sender, EventArgs e)
|
|
{
|
|
var notification = ((NotificationViewModel)DataContext).Notification;
|
|
|
|
AdjustText();
|
|
|
|
AdjustPanel();
|
|
|
|
AdjustPosition(notification.XPos / 100, notification.YPos / 100);
|
|
|
|
if (notification.Timeout > 0)
|
|
{
|
|
timer.Stop();
|
|
timer.Interval = notification.Timeout;
|
|
timer.Start();
|
|
}
|
|
else if (notification.Timeout == 0)
|
|
{
|
|
timer.Stop();
|
|
}
|
|
}
|
|
|
|
private void AdjustText()
|
|
{
|
|
string font = Properties.Core.Default.NativeNotifyFont;
|
|
var titleText = this.Find<TextBlock>("Title");
|
|
var detailText = this.Find<TextBlock>("Detail");
|
|
|
|
if (font.Length > 0)
|
|
{
|
|
var fontFamily = new Avalonia.Media.FontFamily(font);
|
|
|
|
titleText.FontFamily = fontFamily;
|
|
detailText.FontFamily = fontFamily;
|
|
}
|
|
|
|
titleText.FontSize *= scale;
|
|
detailText.FontSize *= scale;
|
|
}
|
|
|
|
private void AdjustPanel()
|
|
{
|
|
var textPanel = this.Find<StackPanel>("TextPanel");
|
|
Width *= scale;
|
|
Height *= scale;
|
|
textPanel.Width *= scale;
|
|
textPanel.Height *= scale;
|
|
|
|
var textBorder = this.Find<Border>("TextBorder");
|
|
textBorder.BorderThickness *= scale;
|
|
}
|
|
|
|
private void AdjustPosition(double xOverride = -1.0, double yOverride = -1.0)
|
|
{
|
|
PixelRect screenBounds;
|
|
int screen = Properties.Core.Default.NativeNotifyScreen;
|
|
int corner = Properties.Core.Default.NativeNotifyCorner;
|
|
|
|
if (screen == -1 || screen > Screens.All.Count)
|
|
if (Screens.All.Count == 1)
|
|
screenBounds = Screens.All[0].Bounds;
|
|
else
|
|
screenBounds = Screens.Primary.Bounds;
|
|
else
|
|
screenBounds = Screens.All[screen - 1].Bounds;
|
|
|
|
double displayScale = LayoutHelper.GetLayoutScale(this);
|
|
double scaleWidth = Width * displayScale;
|
|
double scaleHeight = Height * displayScale;
|
|
|
|
if (xOverride >= 0 && yOverride >= 0)
|
|
{
|
|
defaultPosition = false;
|
|
Position = screenBounds.TopLeft + new PixelPoint(Convert.ToInt32(screenBounds.Width * xOverride), Convert.ToInt32(screenBounds.Height * yOverride));
|
|
}
|
|
else
|
|
{
|
|
defaultPosition = true;
|
|
switch (corner)
|
|
{
|
|
default:
|
|
case 0:
|
|
Position = screenBounds.BottomRight - new PixelPoint(Convert.ToInt32(scaleWidth) + 50, Convert.ToInt32(scaleHeight) + 50);
|
|
break;
|
|
case 1:
|
|
Position = screenBounds.BottomLeft - new PixelPoint(-50, Convert.ToInt32(scaleHeight) + 50);
|
|
break;
|
|
case 2:
|
|
Position = screenBounds.TopRight - new PixelPoint(Convert.ToInt32(scaleWidth) + 50, -50);
|
|
break;
|
|
case 3:
|
|
Position = screenBounds.TopLeft + new PixelPoint(50, 50);
|
|
break;
|
|
}
|
|
originalPosition = Position;
|
|
}
|
|
}
|
|
|
|
private void CloseNotification(object sender, System.Timers.ElapsedEventArgs e)
|
|
{
|
|
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
Close();
|
|
});
|
|
}
|
|
|
|
private void InitializeComponent()
|
|
{
|
|
AvaloniaXamlLoader.Load(this);
|
|
}
|
|
|
|
private void MakeClickThrough()
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
var style = GetWindowLong(this.PlatformImpl.Handle.Handle, GWL_EXSTYLE);
|
|
|
|
//PlatformImpl not part of formal Avalonia API and may not be available in future versions.
|
|
SetWindowLong(this.PlatformImpl.Handle.Handle, GWL_EXSTYLE, style | WS_EX_LAYERED | WS_EX_TRANSPARENT);
|
|
SetLayeredWindowAttributes(this.PlatformImpl.Handle.Handle, 0, 255, LWA_ALPHA);
|
|
}
|
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
{
|
|
// X11 stuff is not part of official API, we'll have to deal with reflection
|
|
// This solution currently only supports the X11 window system which is used on most systems
|
|
var type = this.PlatformImpl.GetType();
|
|
if (type.FullName is not "Avalonia.X11.X11Window") return;
|
|
|
|
// Get the pointer to the X11 window
|
|
var handlePropInfo = type.GetField("_handle", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
var handle = handlePropInfo?.GetValue(this.PlatformImpl);
|
|
// Get the X11Info instance
|
|
var x11PropInfo = type.GetField("_x11", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
var x11Info = x11PropInfo?.GetValue(this.PlatformImpl);
|
|
// Get the pointer to the X11 display
|
|
var displayPropInfo = x11Info?.GetType().GetProperty("Display");
|
|
var display = displayPropInfo?.GetValue(x11Info);
|
|
|
|
if (display == null || handle == null) return;
|
|
try
|
|
{
|
|
// Create a very tiny region
|
|
var region = XFixesCreateRegion((IntPtr)display, IntPtr.Zero, 0);
|
|
// Set the input shape of the window to our region
|
|
XFixesSetWindowShapeRegion((IntPtr)display, (IntPtr)handle, ShapeInput, 0, 0, region);
|
|
// Cleanup
|
|
XFixesDestroyRegion((IntPtr)display, region);
|
|
}
|
|
catch (DllNotFoundException ignored)
|
|
{
|
|
// libXfixes is not installed for some reason
|
|
}
|
|
}
|
|
}
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
static extern uint GetWindowLong(IntPtr hWnd, int nIndex);
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags);
|
|
[DllImport("user32.dll")]
|
|
static extern uint SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
|
|
[DllImport("user32.dll")]
|
|
static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags);
|
|
|
|
internal const int GWL_EXSTYLE = -20;
|
|
internal const int WS_EX_LAYERED = 0x80000;
|
|
internal const int LWA_ALPHA = 0x2;
|
|
internal const int WS_EX_TRANSPARENT = 0x00000020;
|
|
|
|
[DllImport("libXfixes.so")]
|
|
static extern IntPtr XFixesCreateRegion(IntPtr dpy, IntPtr rectangles, int nrectangles);
|
|
|
|
[DllImport("libXfixes.so")]
|
|
static extern IntPtr XFixesSetWindowShapeRegion(IntPtr dpy, IntPtr win, int shape_kind, int x_off, int y_off, IntPtr region);
|
|
|
|
[DllImport("libXfixes.so")]
|
|
static extern IntPtr XFixesDestroyRegion(IntPtr dpy, IntPtr region);
|
|
|
|
internal const int ShapeInput = 2;
|
|
}
|
|
}
|