Toasts – Blame information for rev 59

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