Toasts – Blame information for rev 52

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