mirror of
https://github.com/9ParsonsB/Pulsar.git
synced 2025-07-01 08:23:42 -04:00
feat: cosmetic overhaul of native notification popup (#13)
* feat: cosmetic overhaul of native notification popup * fix: use correct screen default * feat: move notification test from debug build type to button press * fix: not debug anymore * fix: rearrange native notification settings controls * fix: account for display scaling when positioning notification * fix: guard against a screen that no longer exists * fix: safer way to get screens, in case control tree changes
This commit is contained in:
@ -11,6 +11,8 @@ using Avalonia.VisualTree;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
|
||||
namespace Observatory.UI.Views
|
||||
{
|
||||
@ -150,7 +152,35 @@ namespace Observatory.UI.Views
|
||||
|
||||
#region Notification settings
|
||||
|
||||
TextBlock nativeNotifyLabel = new() { Text = "Basic Notification" };
|
||||
Expander notificationExpander = new()
|
||||
{
|
||||
Header = "Basic Notifications",
|
||||
DataContext = Properties.Core.Default,
|
||||
Margin = new Thickness(0, 20),
|
||||
Background = this.Background,
|
||||
BorderThickness = new Thickness(0)
|
||||
};
|
||||
|
||||
Grid notificationGrid = new();
|
||||
|
||||
notificationGrid.ColumnDefinitions = new()
|
||||
{
|
||||
new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Star) },
|
||||
new ColumnDefinition() { Width = new GridLength(3, GridUnitType.Star) },
|
||||
new ColumnDefinition() { Width = new GridLength(3, GridUnitType.Star) }
|
||||
};
|
||||
|
||||
notificationGrid.RowDefinitions = new()
|
||||
{
|
||||
new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) },
|
||||
new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) },
|
||||
new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) },
|
||||
new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) },
|
||||
new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) }
|
||||
};
|
||||
|
||||
TextBlock nativeNotifyLabel = new() { Text = "Enabled" };
|
||||
|
||||
CheckBox nativeNotifyCheckbox = new() { IsChecked = Properties.Core.Default.NativeNotify, Content = nativeNotifyLabel };
|
||||
|
||||
nativeNotifyCheckbox.Checked += (object sender, RoutedEventArgs e) =>
|
||||
@ -164,10 +194,167 @@ namespace Observatory.UI.Views
|
||||
Properties.Core.Default.NativeNotify = false;
|
||||
Properties.Core.Default.Save();
|
||||
};
|
||||
|
||||
Button notifyTestButton = new()
|
||||
{
|
||||
Content = "Test",
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right
|
||||
};
|
||||
|
||||
corePanel.AddControl(nativeNotifyCheckbox, rowTracker.NextIndex(), 0, 2);
|
||||
notifyTestButton.Click += (object sender, RoutedEventArgs e) =>
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
var notifyWindow = new UI.Views.NotificationView() { DataContext = new UI.ViewModels.NotificationViewModel("Test Notification", "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras suscipit hendrerit libero ac scelerisque.") };
|
||||
notifyWindow.Show();
|
||||
});
|
||||
};
|
||||
|
||||
#endregion
|
||||
TextBlock notifyFontLabel = new()
|
||||
{
|
||||
Text = "Font: ",
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
};
|
||||
ComboBox notifyFontDropDown = new()
|
||||
{
|
||||
MinWidth = 200
|
||||
};
|
||||
|
||||
notifyFontDropDown.Items = new System.Drawing.Text.InstalledFontCollection().Families.Select(font => font.Name);
|
||||
|
||||
if (Properties.Core.Default.NativeNotifyFont.Length > 0)
|
||||
{
|
||||
notifyFontDropDown.SelectedItem = Properties.Core.Default.NativeNotifyFont;
|
||||
}
|
||||
|
||||
notifyFontDropDown.SelectionChanged += (object sender, SelectionChangedEventArgs e) =>
|
||||
{
|
||||
var comboBox = (ComboBox)sender;
|
||||
Properties.Core.Default.NativeNotifyFont = comboBox.SelectedItem.ToString();
|
||||
Properties.Core.Default.Save();
|
||||
};
|
||||
|
||||
TextBlock monitorLabel = new()
|
||||
{
|
||||
Text = "Display: ",
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
};
|
||||
ComboBox monitorDropDown = new()
|
||||
{
|
||||
MinWidth = 200
|
||||
};
|
||||
|
||||
List<string> displays = new();
|
||||
displays.Add("Primary");
|
||||
|
||||
var application = (IClassicDesktopStyleApplicationLifetime)Application.Current.ApplicationLifetime;
|
||||
var screens = application.MainWindow.Screens.All;
|
||||
|
||||
if (screens.Count > 1)
|
||||
for (int i = 0; i < screens.Count; i++)
|
||||
{
|
||||
displays.Add((i + 1).ToString());
|
||||
}
|
||||
|
||||
monitorDropDown.Items = displays;
|
||||
|
||||
if (Properties.Core.Default.NativeNotifyScreen == -1)
|
||||
{
|
||||
monitorDropDown.SelectedItem = "Primary";
|
||||
}
|
||||
else
|
||||
{
|
||||
monitorDropDown.SelectedItem = (Properties.Core.Default.NativeNotifyScreen).ToString();
|
||||
}
|
||||
|
||||
monitorDropDown.SelectionChanged += (object sender, SelectionChangedEventArgs e) =>
|
||||
{
|
||||
|
||||
var comboBox = (ComboBox)sender;
|
||||
string selectedItem = comboBox.SelectedItem.ToString();
|
||||
int selectedScreen = selectedItem == "Primary" ? -1 : Int32.Parse(selectedItem);
|
||||
|
||||
Properties.Core.Default.NativeNotifyScreen = selectedScreen;
|
||||
Properties.Core.Default.Save();
|
||||
};
|
||||
|
||||
TextBlock cornerLabel = new()
|
||||
{
|
||||
Text = "Corner: ",
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
};
|
||||
ComboBox cornerDropDown = new()
|
||||
{
|
||||
MinWidth = 200
|
||||
};
|
||||
|
||||
List<string> corners = new()
|
||||
{
|
||||
"Bottom-Right",
|
||||
"Bottom-Left",
|
||||
"Top-Right",
|
||||
"Top-Left"
|
||||
};
|
||||
|
||||
cornerDropDown.Items = corners;
|
||||
|
||||
cornerDropDown.SelectedItem = corners[Properties.Core.Default.NativeNotifyCorner];
|
||||
|
||||
cornerDropDown.SelectionChanged += (object sender, SelectionChangedEventArgs e) =>
|
||||
{
|
||||
var comboBox = (ComboBox)sender;
|
||||
Properties.Core.Default.NativeNotifyCorner = comboBox.SelectedIndex;
|
||||
Properties.Core.Default.Save();
|
||||
};
|
||||
|
||||
TextBlock colourLabel = new()
|
||||
{
|
||||
Text = "Colour: ",
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
};
|
||||
|
||||
BrushConverter brushConverter = new();
|
||||
|
||||
Egorozh.ColorPicker.Dialog.ColorPickerButton colourPickerButton = new()
|
||||
{
|
||||
Width = 25,
|
||||
Height = 25,
|
||||
Color = Color.FromUInt32(Properties.Core.Default.NativeNotifyColour),
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left
|
||||
|
||||
};
|
||||
|
||||
colourPickerButton.PropertyChanged += (object sender, AvaloniaPropertyChangedEventArgs e) =>
|
||||
{
|
||||
if (e.Property.Name == "Color")
|
||||
{
|
||||
Properties.Core.Default.NativeNotifyColour = ((Color)e.NewValue).ToUint32();
|
||||
Properties.Core.Default.Save();
|
||||
}
|
||||
};
|
||||
|
||||
notificationGrid.AddControl(monitorLabel, 0, 0);
|
||||
notificationGrid.AddControl(monitorDropDown, 0, 1, 2);
|
||||
notificationGrid.AddControl(cornerLabel, 1, 0);
|
||||
notificationGrid.AddControl(cornerDropDown, 1, 1, 2);
|
||||
notificationGrid.AddControl(notifyFontLabel, 2, 0);
|
||||
notificationGrid.AddControl(notifyFontDropDown, 2, 1, 2);
|
||||
notificationGrid.AddControl(colourLabel, 3, 0);
|
||||
notificationGrid.AddControl(colourPickerButton, 3, 1);
|
||||
notificationGrid.AddControl(notifyTestButton, 3, 1);
|
||||
notificationGrid.AddControl(nativeNotifyCheckbox, 4, 0, 2);
|
||||
|
||||
|
||||
notificationExpander.Content = notificationGrid;
|
||||
|
||||
|
||||
corePanel.AddControl(notificationExpander, rowTracker.NextIndex(), 0, 2);
|
||||
|
||||
#endregion
|
||||
|
||||
#region System Context Priming setting
|
||||
|
||||
|
@ -5,25 +5,40 @@
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="150"
|
||||
x:Class="Observatory.UI.Views.NotificationView"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="NoChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="-1"
|
||||
Title="Notification"
|
||||
Width="400" Height="150"
|
||||
Topmost="True">
|
||||
<StackPanel DataContext="{Binding Notification}">
|
||||
<TextBlock
|
||||
Padding="10"
|
||||
FontWeight="Bold"
|
||||
FontSize="30"
|
||||
FontFamily="Ebrima"
|
||||
Text="{Binding Title}">
|
||||
Title
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
Padding="20,0"
|
||||
FontWeight="Normal"
|
||||
FontSize="20"
|
||||
FontFamily="Ebrima"
|
||||
Text="{Binding Detail}">
|
||||
Detail
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
MinWidth="400" MinHeight="150"
|
||||
MaxWidth="400" MaxHeight="150"
|
||||
Topmost="True"
|
||||
SizeToContent="Height"
|
||||
TransparencyLevelHint="AcrylicBlur"
|
||||
Background="Transparent"
|
||||
Focusable="False">
|
||||
<Panel DataContext="{Binding Notification}">
|
||||
<Border BorderBrush="{Binding Colour}" BorderThickness="4">
|
||||
<StackPanel Width="400">
|
||||
<TextBlock
|
||||
Name="Title"
|
||||
Padding="10"
|
||||
FontWeight="Normal"
|
||||
FontSize="30"
|
||||
Foreground="{Binding Colour}"
|
||||
Text="{Binding Title}">
|
||||
Title
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
Name="Detail"
|
||||
Padding="20,0"
|
||||
FontWeight="Normal"
|
||||
FontSize="20"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{Binding Colour}"
|
||||
Text="{Binding Detail}">
|
||||
Detail
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Panel>
|
||||
</Window>
|
||||
|
@ -1,6 +1,9 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Observatory.UI.Views
|
||||
{
|
||||
@ -10,11 +13,53 @@ namespace Observatory.UI.Views
|
||||
{
|
||||
InitializeComponent();
|
||||
SystemDecorations = SystemDecorations.None;
|
||||
var screenBounds = Screens.Primary.Bounds;
|
||||
Position = screenBounds.BottomRight - new PixelPoint((int)Width, (int)Height);
|
||||
|
||||
MakeClickThrough(); //Platform specific, currently windows only.
|
||||
|
||||
int screen = Properties.Core.Default.NativeNotifyScreen;
|
||||
int corner = Properties.Core.Default.NativeNotifyCorner;
|
||||
string font = Properties.Core.Default.NativeNotifyFont;
|
||||
|
||||
if (font.Length > 0)
|
||||
{
|
||||
var titleText = this.Find<TextBlock>("Title");
|
||||
var detailText = this.Find<TextBlock>("Detail");
|
||||
var fontFamily = new Avalonia.Media.FontFamily(font);
|
||||
|
||||
titleText.FontFamily = fontFamily;
|
||||
detailText.FontFamily = fontFamily;
|
||||
}
|
||||
|
||||
PixelRect screenBounds;
|
||||
|
||||
if (screen == -1 || screen > Screens.All.Count)
|
||||
screenBounds = Screens.Primary.Bounds;
|
||||
else
|
||||
screenBounds = Screens.All[screen - 1].Bounds;
|
||||
|
||||
double scale = LayoutHelper.GetLayoutScale(this);
|
||||
double scaleWidth = Width * scale;
|
||||
double scaleHeight = Height * scale;
|
||||
|
||||
switch (corner)
|
||||
{
|
||||
default: case 0:
|
||||
Position = screenBounds.BottomRight - new PixelPoint((int)scaleWidth + 50, (int)scaleHeight + 50);
|
||||
break;
|
||||
case 1:
|
||||
Position = screenBounds.BottomLeft - new PixelPoint(-50, (int)scaleHeight + 50);
|
||||
break;
|
||||
case 2:
|
||||
Position = screenBounds.TopRight - new PixelPoint((int)scaleWidth + 50, -50);
|
||||
break;
|
||||
case 3:
|
||||
Position = screenBounds.TopLeft + new PixelPoint(50, 50);
|
||||
break;
|
||||
}
|
||||
|
||||
var timer = new System.Timers.Timer();
|
||||
timer.Elapsed += CloseNotification;
|
||||
timer.Interval = 5000;
|
||||
timer.Interval = 8000;
|
||||
timer.Start();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
@ -33,5 +78,31 @@ namespace Observatory.UI.Views
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user