Toasts – Blame information for rev 50

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)
255 {
49 office 256 _toastTimer = new System.Timers.Timer { Enabled = false, AutoReset = false, Interval = LingerTime };
257 _toastTimer.Elapsed += ToastTimer_Elapsed;
258  
44 office 259 if (EnableChime)
260 {
261 Task.Factory.StartNew(() =>
262 {
43 office 263 try
264 {
265 using (var memoryStream = new MemoryStream(Chime))
266 {
267 using (var player = new SoundPlayer(memoryStream))
268 {
269 player.Play();
270 }
44 office 271 }
272 }
43 office 273 catch
274 {
275 Debug.WriteLine("Sound file could not be played.");
44 office 276 }
277 });
278 }
279  
280 try
281 {
282 lock (OpenNotificationsLock)
283 {
284 // if not image is provided, just collapse the panel for the extra space
285 if (imageBox.Image == null)
286 {
287 splitContainer1.Panel1Collapsed = true;
288 }
289  
290 Invoke(new MethodInvoker(() =>
291 {
292 // compute notification height from body
293 var maxWidth = tableLayoutPanel4.Width;
50 office 294  
295 switch (ContentType)
44 office 296 {
50 office 297 case "text/markdown":
298 var htmlDocument = new HtmlAgilityPack.HtmlDocument();
299 var panelText = htmlPanel1.Text;
300 if (!string.IsNullOrEmpty(panelText))
44 office 301 {
50 office 302 htmlDocument.LoadHtml(panelText);
303 if (htmlDocument.DocumentNode != null && htmlDocument.DocumentNode.Descendants().Any())
304 {
305 var imgNodes = htmlDocument.DocumentNode.SelectNodes("//img");
306 if (imgNodes != null && imgNodes.Any())
44 office 307 {
50 office 308 foreach (var node in imgNodes)
44 office 309 {
50 office 310 node.SetAttributeValue("style", $"max-width: {maxWidth}px");
44 office 311 }
50 office 312 }
313 }
44 office 314  
50 office 315 htmlPanel1.Text = htmlDocument.DocumentNode.WriteTo();
44 office 316 }
50 office 317 break;
318 default:
319 break;
320 }
44 office 321  
50 office 322 //var maxSize = new Size(maxWidth, Screen.PrimaryScreen.WorkingArea.Height);
323 using (var image = HtmlRender.RenderToImage(htmlPanel1.Text, maxWidth, 0, Color.Empty))
324 {
325 var height = image.Height + labelTitle.Height;
326 if(height > Height)
327 {
328 Height = height;
44 office 329 }
330 }
331 }));
332  
43 office 333 if (EnablePin)
334 {
335 Invoke(new MethodInvoker(() =>
336 {
337 // set the location of the toast
338 Location = new Point(PinPoint.X, PinPoint.Y);
339 }));
340  
341 // remove top-most
342 SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);
343  
344 _formAnimator.Method = AnimationMethodDetached;
345 _formAnimator.Direction = AnimationDirectionDetached;
346  
347 return;
44 office 348 }
349  
350 var notifications = OpenNotifications.ToArray();
351  
352 foreach (var openForm in notifications)
353 {
354 if (openForm == null)
355 {
356 continue;
357 }
358  
359 if (openForm.IsDisposed)
360 {
361 OpenNotifications.Remove(openForm);
362  
363 continue;
364 }
365  
366 if (openForm.IsHandleCreated != true)
367 {
368 continue;
369 }
370  
371 // Move each open form upwards to make room for this one
372 var handle = Handle;
373 openForm.BeginInvoke(new MethodInvoker(() =>
374 {
375 try
376 {
377 if (openForm.Handle == handle)
378 {
379 return;
380 }
381  
382 openForm.Top -= Height;
383  
384 // do not render offscreen
385 if (!IsFormOnScreen(openForm))
386 {
387 OpenNotifications.Remove(openForm);
388 openForm.Close();
389 if (!openForm.IsDisposed)
390 {
391 openForm.Dispose();
392 }
393 }
394 }
395 catch
396 {
397 Debug.WriteLine("Error while moving forms up.");
398 }
399 }));
400 }
401  
402 Invoke(new MethodInvoker(() =>
403 {
404 // set the location of the toast
405 Location = new Point(_screenWidth - Width, _screenHeight - Height);
406 }));
407  
408 OpenNotifications.Add(this);
409  
410 _toastTimer.Enabled = true;
411 _toastTimer.Start();
412 }
413 }
414 catch
415 {
416 Debug.WriteLine("Error on form load event.");
417 }
418 }
419  
49 office 420 private async void HtmlPanel1_ImageLoad(object sender, TheArtOfDev.HtmlRenderer.Core.Entities.HtmlImageLoadEventArgs e)
421 {
422 if(!string.IsNullOrEmpty(UserAgent))
423 {
424 httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
425 }
426  
427 try
428 {
429 using (var response = await httpClient.GetAsync(e.Src))
430 {
431 using (var responseStream = await response.Content.ReadAsStreamAsync())
432 {
433 var image = Image.FromStream(responseStream);
434  
435 e.Callback(image);
436 e.Handled = true;
437 }
438 }
439 }
440 catch
441 {
442 Debug.WriteLine("Error downloading image.");
443 }
444 }
445  
44 office 446 private void ToastForm_FormClosed(object sender, FormClosedEventArgs e)
447 {
448 try
449 {
450 lock (OpenNotificationsLock)
451 {
452 OpenNotifications.Remove(this);
453 }
454 }
455 catch
456 {
457 Debug.WriteLine("Error in form closed event.");
458 }
459 }
460  
461 private void ToastForm_Shown(object sender, EventArgs e)
462 {
49 office 463 _formAnimator = new FormAnimator(this, AnimationMethod, AnimationDirection, AnimationDuration);
464  
44 office 465 // Reverse animation direction for hiding.
466 switch (_formAnimator.Direction)
467 {
468 case FormAnimator.AnimationDirection.Up:
469 _formAnimator.Direction = FormAnimator.AnimationDirection.Down;
470 break;
471 case FormAnimator.AnimationDirection.Down:
472 _formAnimator.Direction = FormAnimator.AnimationDirection.Up;
473 break;
474 case FormAnimator.AnimationDirection.Left:
475 _formAnimator.Direction = FormAnimator.AnimationDirection.Right;
476 break;
477 case FormAnimator.AnimationDirection.Right:
478 _formAnimator.Direction = FormAnimator.AnimationDirection.Left;
479 break;
480 }
481  
482 _showFormTaskCompletionSource.TrySetResult(new { });
483 }
484  
485 private async void ToastTimer_Elapsed(object sender, EventArgs e)
486 {
487 await _showFormTaskCompletionSource.Task;
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  
506 BeginInvoke(new MethodInvoker(() =>
507 {
508 lock (OpenNotificationsLock)
509 {
510 Close();
511 }
512 }));
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();
550  
551 BeginInvoke(new MethodInvoker(() =>
552 {
553 lock (OpenNotificationsLock)
554 {
555 if (OpenNotifications.Contains(this))
556 {
557 OpenNotifications.Remove(this);
558 }
559 }
560 }));
561  
562 // remove top-most
563 SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);
564  
565 _formAnimator.Method = AnimationMethodDetached;
566 _formAnimator.Direction = AnimationDirectionDetached;
567 }
568 }
569  
570 private void labelTitle_MouseUp(object sender, MouseEventArgs e)
571 {
572 _mouseDown = false;
573 }
574  
575  
576 #endregion
577  
578 #region Private Methods
579  
580 /// <summary>
581 /// Checks if a form is contained within any of the screens.
582 /// </summary>
583 /// <param name="form">the form to check</param>
584 /// <remarks>https://stackoverflow.com/questions/987018/determining-if-a-form-is-completely-off-screen</remarks>
585 /// <returns>false if the form is entirely or partially offscreen</returns>
586 public bool IsFormOnScreen(Form form)
587 {
588 foreach (Screen screen in Screen.AllScreens)
589 {
590 Rectangle formRectangle = new Rectangle(form.Left, form.Top, form.Width, form.Height);
591  
592 if (screen.WorkingArea.Contains(formRectangle))
593 {
594 return true;
595 }
596 }
597  
598 return false;
599 }
600  
601 #endregion
41 office 602 }
1 office 603 }