Toasts – Blame information for rev 40

Subversion Repositories:
Rev:
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;
40 office 21 using static TheArtOfDev.HtmlRenderer.Adapters.RGraphicsPath;
22 using static Toasts.FormAnimator;
1 office 23  
24 namespace Toasts
25 {
2 office 26 public partial class ToastForm : Form
1 office 27 {
24 office 28 #region Public Fields and Properties
25 office 29  
24 office 30 public bool EnableChime { get; set; } = true;
31  
32 public int LingerTime { get; set; } = 5000;
33  
40 office 34 public FormAnimator.AnimationMethod AnimationMethodDetached { get; set; } = FormAnimator.AnimationMethod.Fade;
35  
36 public FormAnimator.AnimationDirection AnimationDirectionDetached { get; set; } = FormAnimator.AnimationDirection.None;
37  
24 office 38 public FormAnimator.AnimationMethod AnimationMethod { get; set; } = FormAnimator.AnimationMethod.Slide;
39  
40 public FormAnimator.AnimationDirection AnimationDirection { get; set; } = FormAnimator.AnimationDirection.Up;
41  
42 public int AnimationDuration { get; set; } = 500;
43  
35 office 44 public string ContentType { get; internal set; } = "text/plain";
45  
26 office 46 public byte[] Chime
47 {
48 get => _chime;
49 set
50 {
51 if (value is null)
52 {
53 return;
54 }
24 office 55  
26 office 56 _chime = value;
57 }
58 }
59  
24 office 60 /// <summary>
61 ///
62 /// </summary>
63 /// <remarks>clone the image for safety</remarks>
64 public Image Image
65 {
34 office 66 get
67 {
68 if (imageBox.Image == null)
69 {
70 return null;
71 }
72 return new Bitmap(imageBox.Image);
73 }
33 office 74 set => imageBox.Image = value;
24 office 75 }
76  
77 #endregion
78  
1 office 79 #region Static Fields and Constants
80  
16 office 81 private static readonly object OpenNotificationsLock = new object();
1 office 82  
16 office 83 private static readonly HashSet<ToastForm> OpenNotifications = new HashSet<ToastForm>();
84  
1 office 85 #endregion
86  
40 office 87 #region Private Fields and Properties
88  
89 private bool _toastDetached;
90 private object _toastDetachedLock = new object();
91 private bool _mouseDown;
92 private Point _lastLocation;
93  
94 #endregion
95  
12 office 96 #region Private Overrides
97  
98 protected override bool ShowWithoutActivation => true;
99 protected override CreateParams CreateParams
100 {
101 get
102 {
34 office 103  
12 office 104 var baseParams = base.CreateParams;
34 office 105  
40 office 106 //const int WS_EX_NOACTIVATE = 0x08000000;
12 office 107 const int WS_EX_TOOLWINDOW = 0x00000080;
108 const int WS_EX_TOPMOST = 0x00000008;
40 office 109 baseParams.ExStyle |= WS_EX_TOOLWINDOW | WS_EX_TOPMOST;
34 office 110  
12 office 111 return baseParams;
112 }
113 }
114  
40 office 115 [DllImport("user32.dll", EntryPoint = "SetWindowPos")]
116 public static extern IntPtr SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int Y, int cx, int cy, int wFlags);
35 office 117  
12 office 118 #endregion
119  
1 office 120 #region Constructors, Destructors and Finalizers
121  
11 office 122 private ToastForm()
7 office 123 {
124 InitializeComponent();
34 office 125 // round rectangles
126 //Region = Region.FromHrgn(NativeMethods.CreateRoundRectRgn(0, 0, Width - 5, Height - 5, 20, 20));
127  
16 office 128 _screenWidth = Screen.PrimaryScreen.WorkingArea.Width;
129 _screenHeight = Screen.PrimaryScreen.WorkingArea.Height;
15 office 130  
24 office 131 _formAnimator = new FormAnimator(this, AnimationMethod, AnimationDirection, AnimationDuration);
16 office 132  
133 using (var memoryStream = new MemoryStream())
15 office 134 {
16 office 135 Resources.normal.CopyTo(memoryStream);
136  
137 memoryStream.Position = 0L;
138  
24 office 139 Chime = memoryStream.ToArray();
15 office 140 }
141  
18 office 142 _showFormTaskCompletionSource = new TaskCompletionSource<object>();
143  
25 office 144 _toastTimer = new System.Timers.Timer { Enabled = false, AutoReset = false, Interval = LingerTime };
15 office 145 _toastTimer.Elapsed += ToastTimer_Elapsed;
7 office 146 }
11 office 147  
1 office 148 /// <summary>
15 office 149 /// Display a toast with a title, body and a logo indefinitely on screen.
1 office 150 /// </summary>
15 office 151 /// <param name="title">the toast title</param>
152 /// <param name="body">the toast body</param>
24 office 153 public ToastForm(string title, string body) : this()
15 office 154 {
155 labelTitle.Text = title;
27 office 156 htmlPanel1.Text = body;
15 office 157 }
158  
159 /// <summary>
11 office 160 /// Clean up any resources being used.
7 office 161 /// </summary>
162 /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
163 protected override void Dispose(bool disposing)
164 {
16 office 165 if (disposing && components != null)
33 office 166 {
167 components.Dispose();
168 }
169  
170 if (_toastTimer != null)
16 office 171 {
33 office 172 _toastTimer.Dispose();
173 _toastTimer = null;
174 }
16 office 175  
33 office 176 if (Image != null)
177 {
24 office 178 Image.Dispose();
33 office 179 Image = null;
16 office 180 }
33 office 181  
7 office 182 base.Dispose(disposing);
183 }
184  
1 office 185 #endregion
186  
187 #region Private Delegates, Events, Enums, Properties, Indexers and Fields
188  
16 office 189 private readonly FormAnimator _formAnimator;
1 office 190  
16 office 191 private System.Timers.Timer _toastTimer;
1 office 192  
16 office 193 private readonly int _screenWidth;
194  
195 private readonly int _screenHeight;
21 office 196  
18 office 197 private readonly TaskCompletionSource<object> _showFormTaskCompletionSource;
16 office 198  
26 office 199 private byte[] _chime;
200  
1 office 201 #endregion
202  
203 #region Event Handlers
204  
25 office 205 private void ToastForm_Load(object sender, EventArgs e)
1 office 206 {
24 office 207 if (EnableChime)
16 office 208 {
24 office 209 Task.Factory.StartNew(() =>
9 office 210 {
24 office 211 using (var memoryStream = new MemoryStream(Chime))
16 office 212 {
24 office 213 using (var player = new SoundPlayer(memoryStream))
214 {
215 player.Play();
216 }
16 office 217 }
24 office 218 });
219 }
8 office 220  
16 office 221 try
8 office 222 {
16 office 223 lock (OpenNotificationsLock)
8 office 224 {
35 office 225 // if not image is provided, just collapse the panel for the extra space
226 if (imageBox.Image == null)
227 {
228 splitContainer1.Panel1Collapsed = true;
229 }
230  
34 office 231 Invoke(new MethodInvoker(() =>
232 {
233 // compute notification height from body
234 var maxWidth = tableLayoutPanel4.Width;
235 using (var m_Bitmap = new Bitmap(64, 64))
236 {
237 using (var graphics = Graphics.FromImage(m_Bitmap))
238 {
35 office 239 switch(ContentType)
240 {
241 case "text/markdown":
242 var htmlDocument = new HtmlAgilityPack.HtmlDocument();
243 htmlDocument.LoadHtml(htmlPanel1.Text);
244 foreach (var node in htmlDocument.DocumentNode.SelectNodes("//img"))
245 {
246 node.SetAttributeValue("style", $"max-width: {maxWidth}px");
247 }
248  
249 htmlPanel1.Text = htmlDocument.DocumentNode.WriteTo();
250 break;
251 default:
252 break;
253 }
254  
255 PointF point = new PointF(0, 0);
256 SizeF maxSize = new SizeF(maxWidth, Screen.PrimaryScreen.WorkingArea.Height);
257 var renderSize = HtmlRender.Render(graphics, htmlPanel1.Text, point, maxSize);
34 office 258 // 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 259 var computedOptimalHeight = (int)Math.Round(renderSize.Height) + labelTitle.Height + (int)Math.Round(htmlPanel1.Font.GetHeight());
260 // keep the default height of a notification constant
261 if(computedOptimalHeight > Height)
262 {
263 Height = computedOptimalHeight;
264 }
34 office 265 }
266 }
267 }));
268  
16 office 269 var notifications = OpenNotifications.ToArray();
8 office 270  
16 office 271 foreach (var openForm in notifications)
15 office 272 {
16 office 273 if (openForm == null)
274 {
275 continue;
276 }
7 office 277  
16 office 278 if (openForm.IsDisposed)
279 {
280 OpenNotifications.Remove(openForm);
15 office 281  
16 office 282 continue;
283 }
1 office 284  
16 office 285 if (openForm.IsHandleCreated != true)
286 {
287 continue;
288 }
4 office 289  
16 office 290 // Move each open form upwards to make room for this one
291 var handle = Handle;
292 openForm.BeginInvoke(new MethodInvoker(() =>
293 {
294 try
295 {
296 if (openForm.Handle == handle)
297 {
298 return;
299 }
1 office 300  
16 office 301 openForm.Top -= Height;
302 }
303 catch
304 {
305 Debug.WriteLine("Error while moving forms up.");
306 }
307 }));
308 }
15 office 309  
16 office 310 Invoke(new MethodInvoker(() =>
311 {
34 office 312 // set the location of the toast
16 office 313 Location = new Point(_screenWidth - Width, _screenHeight - Height);
314 }));
315  
316 OpenNotifications.Add(this);
317  
25 office 318 _toastTimer.Enabled = true;
24 office 319 _toastTimer.Start();
16 office 320 }
321 }
322 catch
15 office 323 {
16 office 324 Debug.WriteLine("Error on form load event.");
15 office 325 }
1 office 326 }
327  
25 office 328 private void ToastForm_FormClosed(object sender, FormClosedEventArgs e)
1 office 329 {
16 office 330 try
1 office 331 {
16 office 332 lock (OpenNotificationsLock)
7 office 333 {
16 office 334 OpenNotifications.Remove(this);
7 office 335 }
16 office 336 }
337 catch
338 {
339 Debug.WriteLine("Error in form closed event.");
340 }
341 }
15 office 342  
25 office 343 private void ToastForm_Shown(object sender, EventArgs e)
16 office 344 {
345 // Reverse animation direction for hiding.
346 switch (_formAnimator.Direction)
347 {
348 case FormAnimator.AnimationDirection.Up:
349 _formAnimator.Direction = FormAnimator.AnimationDirection.Down;
350 break;
351 case FormAnimator.AnimationDirection.Down:
352 _formAnimator.Direction = FormAnimator.AnimationDirection.Up;
353 break;
354 case FormAnimator.AnimationDirection.Left:
355 _formAnimator.Direction = FormAnimator.AnimationDirection.Right;
356 break;
357 case FormAnimator.AnimationDirection.Right:
358 _formAnimator.Direction = FormAnimator.AnimationDirection.Left;
359 break;
1 office 360 }
34 office 361  
18 office 362 _showFormTaskCompletionSource.TrySetResult(new { });
1 office 363 }
364  
18 office 365 private async void ToastTimer_Elapsed(object sender, EventArgs e)
1 office 366 {
18 office 367 await _showFormTaskCompletionSource.Task;
368  
16 office 369 if (IsDisposed)
15 office 370 {
371 return;
372 }
1 office 373  
40 office 374 lock(_toastDetachedLock)
375 {
376 if(_toastDetached)
377 {
378 return;
379 }
380 }
381  
16 office 382 try
383 {
384 _toastTimer.Stop();
40 office 385  
16 office 386 BeginInvoke(new MethodInvoker(() =>
387 {
388 lock (OpenNotificationsLock)
389 {
390 Close();
391 }
392 }));
393 }
394 catch
395 {
396 Debug.WriteLine("Error in timer elapsed event.");
397 }
1 office 398 }
399  
40 office 400 private void labelTitle_MouseDown(object sender, MouseEventArgs e)
1 office 401 {
40 office 402 _mouseDown = true;
403 _lastLocation = e.Location;
404 }
405  
406 private void labelTitle_MouseMove(object sender, MouseEventArgs e)
407 {
408 if (!_mouseDown)
409 {
410 return;
411  
412 }
413  
414 Location = new Point((Location.X - _lastLocation.X) + e.X, (Location.Y - _lastLocation.Y) + e.Y);
415  
416 Update();
417  
418 lock (_toastDetachedLock)
419 {
420 if (_toastDetached)
421 {
422 return;
423 }
424  
425 _toastDetached = true;
426  
427 _toastTimer.Elapsed -= ToastTimer_Elapsed;
428 _toastTimer.Stop();
429 _toastTimer.Dispose();
430  
431 BeginInvoke(new MethodInvoker(() =>
432 {
433 lock (OpenNotificationsLock)
434 {
435 if (OpenNotifications.Contains(this))
436 {
437 OpenNotifications.Remove(this);
438 }
439 }
440 }));
441  
442 // remove top-most
443 SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);
444  
445 _formAnimator.Method = AnimationMethodDetached;
446 _formAnimator.Direction = AnimationDirectionDetached;
447 }
448 }
449  
450 private void labelTitle_MouseUp(object sender, MouseEventArgs e)
451 {
452 _mouseDown = false;
453 }
454  
455  
456 #endregion
457  
458 private void panel1_Click(object sender, EventArgs e)
459 {
16 office 460 try
461 {
21 office 462 BeginInvoke(new MethodInvoker(() =>
16 office 463 {
21 office 464 lock (OpenNotificationsLock)
465 {
466 Close();
467 }
468 }));
16 office 469 }
470 catch
471 {
472 Debug.WriteLine("Error in form click event.");
473 }
1 office 474 }
475  
40 office 476 private void Toast_Click(object sender, EventArgs e)
35 office 477 {
40 office 478 lock (_toastDetachedLock)
479 {
480 if(!_toastDetached)
481 {
482 return;
483 }
35 office 484  
40 office 485 BringToFront();
486 }
35 office 487 }
1 office 488 }
489 }