Toasts – Blame information for rev 53

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