Toasts – Blame information for rev 58

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