Toasts – Blame information for rev 44

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