forked from BornToBeRoot/NETworkManager
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNotificationWindow.xaml.cs
More file actions
195 lines (149 loc) · 5.93 KB
/
Copy pathNotificationWindow.xaml.cs
File metadata and controls
195 lines (149 loc) · 5.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
using MahApps.Metro.IconPacks;
using NETworkManager.Utilities;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Threading;
namespace NETworkManager;
/// <summary>
/// A generic, stackable notification popup shown in the bottom-right corner of the primary
/// screen. Contains no feature-specific knowledge — icon, color, title and message are all
/// supplied by the caller (via <see cref="NotificationManager"/>).
/// </summary>
public partial class NotificationWindow : INotifyPropertyChanged
{
#region PropertyChangedEventHandler
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region Variables
private readonly DispatcherTimer _timer = new(DispatcherPriority.Render);
private readonly Stopwatch _stopwatch = new();
// Bound properties — all set once in the constructor, no change notifications needed.
// Note: the header property is named NotificationTitle (not Title) to avoid clashing with
// the inherited Window.Title property, which a "{Binding Title}" would otherwise resolve to.
public PackIconMaterialKind IconKind { get; }
public string IconColor { get; }
public string NotificationTitle { get; }
public string Message { get; }
// Timestamp shown in the header — captured when the notification is created, which is the
// moment the status change occurred.
public string Time { get; }
public double TimeMax { get; }
public double TimeRemaining
{
get;
private set
{
if (Math.Abs(value - field) < 0.001)
return;
field = value;
OnPropertyChanged();
}
}
// Command — initialized once in the constructor, not recreated on every binding access
public ICommand CloseCommand { get; }
#endregion
#region Constructor
public NotificationWindow(PackIconMaterialKind iconKind, string iconColor, string title, string message, int closeTimeSeconds)
{
InitializeComponent();
DataContext = this;
IconKind = iconKind;
IconColor = iconColor;
NotificationTitle = title;
Message = message;
Time = DateTime.Now.ToString("HH:mm:ss");
TimeMax = closeTimeSeconds;
TimeRemaining = closeTimeSeconds;
CloseCommand = new RelayCommand(_ => CloseWindow());
// The window auto-sizes its height to the content (SizeToContent=Height), so when the
// message wraps to more lines the whole stack must re-anchor to the bottom edge.
SizeChanged += (_, _) => NotificationManager.RepositionAll();
_timer.Interval = TimeSpan.FromMilliseconds(33);
_timer.Tick += Timer_Tick;
}
#endregion
#region ICommands & Actions
private void ShowMainWindow()
{
CloseWindow();
if (System.Windows.Application.Current.MainWindow is MainWindow mainWindow &&
mainWindow.ShowWindowCommand.CanExecute(null))
mainWindow.ShowWindowCommand.Execute(null);
}
#endregion
#region Methods
/// <summary>
/// Called by <see cref="NotificationManager"/> when the stack changes (a sibling closes or
/// resizes) and this window may need to move.
/// </summary>
internal void Reposition()
{
ApplyPosition();
}
private void ApplyPosition()
{
if (Screen.PrimaryScreen == null)
return;
var scale = System.Windows.Media.VisualTreeHelper.GetDpi(this).DpiScaleX;
var area = Screen.PrimaryScreen.WorkingArea;
// Offset = total height (incl. margins) of all windows stacked below this one. Using the
// actual heights keeps variable-height popups (wrapped messages) bottom-anchored.
var offset = NotificationManager.GetStackOffset(this);
Left = area.Right / scale - Width - NotificationManager.WindowMargin;
Top = area.Bottom / scale - ActualHeight - NotificationManager.WindowMargin - offset;
}
private void CloseWindow()
{
_timer.Stop();
_stopwatch.Stop();
Closing -= MetroWindow_Closing;
Close(); // fires Window.Closed → NotificationManager removes from stack and repositions
}
#endregion
#region Events
// Lifecycle — the HWND exists here, so DPI is safe to read for positioning
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
ApplyPosition();
_stopwatch.Restart();
_timer.Start();
}
// Clicking anywhere on the popup opens the main window. The close button handles its own
// mouse events, so clicking [×] does not bubble up here.
private void Root_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ShowMainWindow();
}
// Intercept Alt+F4 / OS close so the stack cleanup always runs through CloseWindow()
private void MetroWindow_Closing(object sender, CancelEventArgs e)
{
e.Cancel = true;
CloseWindow();
}
private async void Timer_Tick(object sender, EventArgs e)
{
// Use a local for the close decision — the TimeRemaining setter ignores sub-0.001 changes
// (to throttle the progress bar), so reading the property back could stay stuck at a tiny
// positive value and the window would never close.
var remaining = Math.Max(0.0, TimeMax - _stopwatch.Elapsed.TotalSeconds);
TimeRemaining = remaining;
if (remaining > 0)
return;
_timer.Stop();
_stopwatch.Stop();
await Task.Delay(250); // let the bar visually reach zero before closing
CloseWindow();
}
#endregion
}