Toasts – Blame information for rev 35
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | // =====COPYRIGHT===== |
2 | // Code originally retrieved from http://www.vbforums.com/showthread.php?t=547778 - no license information supplied |
||
3 | // =====COPYRIGHT===== |
||
4 | |||
34 | office | 5 | using Markdig.Extensions.Tables; |
1 | office | 6 | using System; |
15 | office | 7 | using System.Collections.Concurrent; |
16 | office | 8 | using System.Collections.Generic; |
9 | using System.Diagnostics; |
||
1 | office | 10 | using System.Drawing; |
8 | office | 11 | using System.IO; |
15 | office | 12 | using System.Linq; |
8 | office | 13 | using System.Media; |
34 | office | 14 | using System.Runtime.InteropServices; |
16 | office | 15 | using System.Threading; |
16 | using System.Threading.Tasks; |
||
1 | office | 17 | using System.Windows.Forms; |
34 | office | 18 | using TheArtOfDev.HtmlRenderer.WinForms; |
11 | office | 19 | using Toasts.Properties; |
34 | office | 20 | using static System.Windows.Forms.VisualStyles.VisualStyleElement; |
1 | office | 21 | |
22 | namespace Toasts |
||
23 | { |
||
2 | office | 24 | public partial class ToastForm : Form |
1 | office | 25 | { |
24 | office | 26 | #region Public Fields and Properties |
25 | office | 27 | |
24 | office | 28 | public bool EnableChime { get; set; } = true; |
29 | |||
30 | public int LingerTime { get; set; } = 5000; |
||
31 | |||
32 | public FormAnimator.AnimationMethod AnimationMethod { get; set; } = FormAnimator.AnimationMethod.Slide; |
||
33 | |||
34 | public FormAnimator.AnimationDirection AnimationDirection { get; set; } = FormAnimator.AnimationDirection.Up; |
||
35 | |||
36 | public int AnimationDuration { get; set; } = 500; |
||
37 | |||
35 | office | 38 | public string ContentType { get; internal set; } = "text/plain"; |
39 | |||
26 | office | 40 | public byte[] Chime |
41 | { |
||
42 | get => _chime; |
||
43 | set |
||
44 | { |
||
45 | if (value is null) |
||
46 | { |
||
47 | return; |
||
48 | } |
||
24 | office | 49 | |
26 | office | 50 | _chime = value; |
51 | } |
||
52 | } |
||
53 | |||
24 | office | 54 | /// <summary> |
55 | /// |
||
56 | /// </summary> |
||
57 | /// <remarks>clone the image for safety</remarks> |
||
58 | public Image Image |
||
59 | { |
||
34 | office | 60 | get |
61 | { |
||
62 | if (imageBox.Image == null) |
||
63 | { |
||
64 | return null; |
||
65 | } |
||
66 | return new Bitmap(imageBox.Image); |
||
67 | } |
||
33 | office | 68 | set => imageBox.Image = value; |
24 | office | 69 | } |
70 | |||
71 | #endregion |
||
72 | |||
1 | office | 73 | #region Static Fields and Constants |
74 | |||
16 | office | 75 | private static readonly object OpenNotificationsLock = new object(); |
1 | office | 76 | |
16 | office | 77 | private static readonly HashSet<ToastForm> OpenNotifications = new HashSet<ToastForm>(); |
78 | |||
1 | office | 79 | #endregion |
80 | |||
12 | office | 81 | #region Private Overrides |
82 | |||
83 | protected override bool ShowWithoutActivation => true; |
||
84 | protected override CreateParams CreateParams |
||
85 | { |
||
86 | get |
||
87 | { |
||
34 | office | 88 | |
12 | office | 89 | var baseParams = base.CreateParams; |
34 | office | 90 | |
12 | office | 91 | const int WS_EX_NOACTIVATE = 0x08000000; |
92 | const int WS_EX_TOOLWINDOW = 0x00000080; |
||
93 | const int WS_EX_TOPMOST = 0x00000008; |
||
94 | baseParams.ExStyle |= WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST; |
||
34 | office | 95 | |
12 | office | 96 | return baseParams; |
97 | } |
||
98 | } |
||
99 | |||
35 | office | 100 | |
101 | |||
12 | office | 102 | #endregion |
103 | |||
1 | office | 104 | #region Constructors, Destructors and Finalizers |
105 | |||
11 | office | 106 | private ToastForm() |
7 | office | 107 | { |
108 | InitializeComponent(); |
||
34 | office | 109 | // round rectangles |
110 | //Region = Region.FromHrgn(NativeMethods.CreateRoundRectRgn(0, 0, Width - 5, Height - 5, 20, 20)); |
||
111 | |||
16 | office | 112 | _screenWidth = Screen.PrimaryScreen.WorkingArea.Width; |
113 | _screenHeight = Screen.PrimaryScreen.WorkingArea.Height; |
||
15 | office | 114 | |
24 | office | 115 | _formAnimator = new FormAnimator(this, AnimationMethod, AnimationDirection, AnimationDuration); |
16 | office | 116 | |
117 | using (var memoryStream = new MemoryStream()) |
||
15 | office | 118 | { |
16 | office | 119 | Resources.normal.CopyTo(memoryStream); |
120 | |||
121 | memoryStream.Position = 0L; |
||
122 | |||
24 | office | 123 | Chime = memoryStream.ToArray(); |
15 | office | 124 | } |
125 | |||
18 | office | 126 | _showFormTaskCompletionSource = new TaskCompletionSource<object>(); |
127 | |||
25 | office | 128 | _toastTimer = new System.Timers.Timer { Enabled = false, AutoReset = false, Interval = LingerTime }; |
15 | office | 129 | _toastTimer.Elapsed += ToastTimer_Elapsed; |
7 | office | 130 | } |
11 | office | 131 | |
1 | office | 132 | /// <summary> |
15 | office | 133 | /// Display a toast with a title, body and a logo indefinitely on screen. |
1 | office | 134 | /// </summary> |
15 | office | 135 | /// <param name="title">the toast title</param> |
136 | /// <param name="body">the toast body</param> |
||
24 | office | 137 | public ToastForm(string title, string body) : this() |
15 | office | 138 | { |
139 | labelTitle.Text = title; |
||
27 | office | 140 | htmlPanel1.Text = body; |
15 | office | 141 | } |
142 | |||
143 | /// <summary> |
||
11 | office | 144 | /// Clean up any resources being used. |
7 | office | 145 | /// </summary> |
146 | /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> |
||
147 | protected override void Dispose(bool disposing) |
||
148 | { |
||
16 | office | 149 | if (disposing && components != null) |
33 | office | 150 | { |
151 | components.Dispose(); |
||
152 | } |
||
153 | |||
154 | if (_toastTimer != null) |
||
16 | office | 155 | { |
33 | office | 156 | _toastTimer.Dispose(); |
157 | _toastTimer = null; |
||
158 | } |
||
16 | office | 159 | |
33 | office | 160 | if (Image != null) |
161 | { |
||
24 | office | 162 | Image.Dispose(); |
33 | office | 163 | Image = null; |
16 | office | 164 | } |
33 | office | 165 | |
7 | office | 166 | base.Dispose(disposing); |
167 | } |
||
168 | |||
1 | office | 169 | #endregion |
170 | |||
171 | #region Private Delegates, Events, Enums, Properties, Indexers and Fields |
||
172 | |||
16 | office | 173 | private readonly FormAnimator _formAnimator; |
1 | office | 174 | |
16 | office | 175 | private System.Timers.Timer _toastTimer; |
1 | office | 176 | |
16 | office | 177 | private readonly int _screenWidth; |
178 | |||
179 | private readonly int _screenHeight; |
||
21 | office | 180 | |
18 | office | 181 | private readonly TaskCompletionSource<object> _showFormTaskCompletionSource; |
16 | office | 182 | |
26 | office | 183 | private byte[] _chime; |
184 | |||
1 | office | 185 | #endregion |
186 | |||
187 | #region Event Handlers |
||
188 | |||
25 | office | 189 | private void ToastForm_Load(object sender, EventArgs e) |
1 | office | 190 | { |
24 | office | 191 | if (EnableChime) |
16 | office | 192 | { |
24 | office | 193 | Task.Factory.StartNew(() => |
9 | office | 194 | { |
24 | office | 195 | using (var memoryStream = new MemoryStream(Chime)) |
16 | office | 196 | { |
24 | office | 197 | using (var player = new SoundPlayer(memoryStream)) |
198 | { |
||
199 | player.Play(); |
||
200 | } |
||
16 | office | 201 | } |
24 | office | 202 | }); |
203 | } |
||
8 | office | 204 | |
16 | office | 205 | try |
8 | office | 206 | { |
16 | office | 207 | lock (OpenNotificationsLock) |
8 | office | 208 | { |
35 | office | 209 | // if not image is provided, just collapse the panel for the extra space |
210 | if (imageBox.Image == null) |
||
211 | { |
||
212 | splitContainer1.Panel1Collapsed = true; |
||
213 | } |
||
214 | |||
34 | office | 215 | Invoke(new MethodInvoker(() => |
216 | { |
||
217 | // compute notification height from body |
||
218 | var maxWidth = tableLayoutPanel4.Width; |
||
219 | using (var m_Bitmap = new Bitmap(64, 64)) |
||
220 | { |
||
221 | using (var graphics = Graphics.FromImage(m_Bitmap)) |
||
222 | { |
||
35 | office | 223 | switch(ContentType) |
224 | { |
||
225 | case "text/markdown": |
||
226 | var htmlDocument = new HtmlAgilityPack.HtmlDocument(); |
||
227 | htmlDocument.LoadHtml(htmlPanel1.Text); |
||
228 | foreach (var node in htmlDocument.DocumentNode.SelectNodes("//img")) |
||
229 | { |
||
230 | node.SetAttributeValue("style", $"max-width: {maxWidth}px"); |
||
231 | } |
||
232 | |||
233 | htmlPanel1.Text = htmlDocument.DocumentNode.WriteTo(); |
||
234 | break; |
||
235 | default: |
||
236 | break; |
||
237 | } |
||
238 | |||
239 | PointF point = new PointF(0, 0); |
||
240 | SizeF maxSize = new SizeF(maxWidth, Screen.PrimaryScreen.WorkingArea.Height); |
||
241 | var renderSize = HtmlRender.Render(graphics, htmlPanel1.Text, point, maxSize); |
||
34 | office | 242 | // total height = height of text fitting in rectangle + the height of the title on top + one extra line for the last line wrap |
35 | office | 243 | var computedOptimalHeight = (int)Math.Round(renderSize.Height) + labelTitle.Height + (int)Math.Round(htmlPanel1.Font.GetHeight()); |
244 | // keep the default height of a notification constant |
||
245 | if(computedOptimalHeight > Height) |
||
246 | { |
||
247 | Height = computedOptimalHeight; |
||
248 | } |
||
34 | office | 249 | } |
250 | } |
||
251 | })); |
||
252 | |||
16 | office | 253 | var notifications = OpenNotifications.ToArray(); |
8 | office | 254 | |
16 | office | 255 | foreach (var openForm in notifications) |
15 | office | 256 | { |
16 | office | 257 | if (openForm == null) |
258 | { |
||
259 | continue; |
||
260 | } |
||
7 | office | 261 | |
16 | office | 262 | if (openForm.IsDisposed) |
263 | { |
||
264 | OpenNotifications.Remove(openForm); |
||
15 | office | 265 | |
16 | office | 266 | continue; |
267 | } |
||
1 | office | 268 | |
16 | office | 269 | if (openForm.IsHandleCreated != true) |
270 | { |
||
271 | continue; |
||
272 | } |
||
4 | office | 273 | |
16 | office | 274 | // Move each open form upwards to make room for this one |
275 | var handle = Handle; |
||
276 | openForm.BeginInvoke(new MethodInvoker(() => |
||
277 | { |
||
278 | try |
||
279 | { |
||
280 | if (openForm.Handle == handle) |
||
281 | { |
||
282 | return; |
||
283 | } |
||
1 | office | 284 | |
16 | office | 285 | openForm.Top -= Height; |
286 | } |
||
287 | catch |
||
288 | { |
||
289 | Debug.WriteLine("Error while moving forms up."); |
||
290 | } |
||
291 | })); |
||
292 | } |
||
15 | office | 293 | |
16 | office | 294 | Invoke(new MethodInvoker(() => |
295 | { |
||
34 | office | 296 | // set the location of the toast |
16 | office | 297 | Location = new Point(_screenWidth - Width, _screenHeight - Height); |
298 | })); |
||
299 | |||
300 | OpenNotifications.Add(this); |
||
301 | |||
25 | office | 302 | _toastTimer.Enabled = true; |
24 | office | 303 | _toastTimer.Start(); |
16 | office | 304 | } |
305 | } |
||
306 | catch |
||
15 | office | 307 | { |
16 | office | 308 | Debug.WriteLine("Error on form load event."); |
15 | office | 309 | } |
1 | office | 310 | } |
311 | |||
25 | office | 312 | private void ToastForm_FormClosed(object sender, FormClosedEventArgs e) |
1 | office | 313 | { |
16 | office | 314 | try |
1 | office | 315 | { |
16 | office | 316 | lock (OpenNotificationsLock) |
7 | office | 317 | { |
16 | office | 318 | OpenNotifications.Remove(this); |
7 | office | 319 | } |
16 | office | 320 | } |
321 | catch |
||
322 | { |
||
323 | Debug.WriteLine("Error in form closed event."); |
||
324 | } |
||
325 | } |
||
15 | office | 326 | |
25 | office | 327 | private void ToastForm_Shown(object sender, EventArgs e) |
16 | office | 328 | { |
329 | // Reverse animation direction for hiding. |
||
330 | switch (_formAnimator.Direction) |
||
331 | { |
||
332 | case FormAnimator.AnimationDirection.Up: |
||
333 | _formAnimator.Direction = FormAnimator.AnimationDirection.Down; |
||
334 | break; |
||
335 | case FormAnimator.AnimationDirection.Down: |
||
336 | _formAnimator.Direction = FormAnimator.AnimationDirection.Up; |
||
337 | break; |
||
338 | case FormAnimator.AnimationDirection.Left: |
||
339 | _formAnimator.Direction = FormAnimator.AnimationDirection.Right; |
||
340 | break; |
||
341 | case FormAnimator.AnimationDirection.Right: |
||
342 | _formAnimator.Direction = FormAnimator.AnimationDirection.Left; |
||
343 | break; |
||
1 | office | 344 | } |
34 | office | 345 | |
18 | office | 346 | _showFormTaskCompletionSource.TrySetResult(new { }); |
1 | office | 347 | } |
348 | |||
18 | office | 349 | private async void ToastTimer_Elapsed(object sender, EventArgs e) |
1 | office | 350 | { |
18 | office | 351 | await _showFormTaskCompletionSource.Task; |
352 | |||
16 | office | 353 | if (IsDisposed) |
15 | office | 354 | { |
355 | return; |
||
356 | } |
||
1 | office | 357 | |
16 | office | 358 | try |
359 | { |
||
360 | _toastTimer.Stop(); |
||
361 | BeginInvoke(new MethodInvoker(() => |
||
362 | { |
||
363 | lock (OpenNotificationsLock) |
||
364 | { |
||
365 | Close(); |
||
366 | } |
||
367 | })); |
||
368 | } |
||
369 | catch |
||
370 | { |
||
371 | Debug.WriteLine("Error in timer elapsed event."); |
||
372 | } |
||
1 | office | 373 | } |
374 | |||
15 | office | 375 | private void Toast_Click(object sender, EventArgs e) |
1 | office | 376 | { |
16 | office | 377 | try |
378 | { |
||
21 | office | 379 | BeginInvoke(new MethodInvoker(() => |
16 | office | 380 | { |
21 | office | 381 | lock (OpenNotificationsLock) |
382 | { |
||
383 | Close(); |
||
384 | } |
||
385 | })); |
||
16 | office | 386 | } |
387 | catch |
||
388 | { |
||
389 | Debug.WriteLine("Error in form click event."); |
||
390 | } |
||
1 | office | 391 | } |
392 | |||
393 | #endregion |
||
35 | office | 394 | |
395 | private void htmlPanel1_TextChanged(object sender, EventArgs e) |
||
396 | { |
||
397 | |||
398 | } |
||
1 | office | 399 | } |
400 | } |