Toasts – Blame information for rev 49

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  
38 public FormAnimator.AnimationMethod AnimationMethod { get; set; } = FormAnimator.AnimationMethod.Slide;
39  
40 public FormAnimator.AnimationDirection AnimationDirection { get; set; } = FormAnimator.AnimationDirection.Up;
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 {
193 _toastTimer.Dispose();
194 _toastTimer = null;
195 }
196  
197 if (Image != null)
198 {
199 Image.Dispose();
200 Image = null;
201 }
202  
203 base.Dispose(disposing);
204 }
205  
206 #endregion
207  
208 #region Event Handlers
209  
210 private void panel1_Click(object sender, EventArgs e)
211 {
212 try
213 {
45 office 214 Clipboard.SetText(htmlPanel1.Text);
215 }
216 catch
217 {
218 Debug.WriteLine("Could not copy to clipboard.");
219 }
220 }
221  
222 private void panel2_Click(object sender, EventArgs e)
223 {
224 try
225 {
44 office 226 BeginInvoke(new MethodInvoker(() =>
227 {
228 lock (OpenNotificationsLock)
229 {
230 Close();
231 }
232 }));
233 }
234 catch
235 {
236 Debug.WriteLine("Error in form click event.");
237 }
238 }
239  
240 private void Toast_Click(object sender, EventArgs e)
241 {
242 lock (_toastDetachedLock)
243 {
244 if (!_toastDetached)
245 {
246 return;
247 }
248  
249 BringToFront();
250 }
251 }
252  
253 private void ToastForm_Load(object sender, EventArgs e)
254 {
49 office 255 _toastTimer = new System.Timers.Timer { Enabled = false, AutoReset = false, Interval = LingerTime };
256 _toastTimer.Elapsed += ToastTimer_Elapsed;
257  
44 office 258 if (EnableChime)
259 {
260 Task.Factory.StartNew(() =>
261 {
43 office 262 try
263 {
264 using (var memoryStream = new MemoryStream(Chime))
265 {
266 using (var player = new SoundPlayer(memoryStream))
267 {
268 player.Play();
269 }
44 office 270 }
271 }
43 office 272 catch
273 {
274 Debug.WriteLine("Sound file could not be played.");
44 office 275 }
276 });
277 }
278  
279 try
280 {
281 lock (OpenNotificationsLock)
282 {
283 // if not image is provided, just collapse the panel for the extra space
284 if (imageBox.Image == null)
285 {
286 splitContainer1.Panel1Collapsed = true;
287 }
288  
289 Invoke(new MethodInvoker(() =>
290 {
291 // compute notification height from body
292 var maxWidth = tableLayoutPanel4.Width;
293 using (var m_Bitmap = new Bitmap(64, 64))
294 {
295 using (var graphics = Graphics.FromImage(m_Bitmap))
296 {
297 switch (ContentType)
298 {
299 case "text/markdown":
300 var htmlDocument = new HtmlAgilityPack.HtmlDocument();
301 var panelText = htmlPanel1.Text;
302 if (!string.IsNullOrEmpty(panelText))
303 {
304 htmlDocument.LoadHtml(panelText);
305 if (htmlDocument.DocumentNode != null && htmlDocument.DocumentNode.Descendants().Any())
306 {
307 var imgNodes = htmlDocument.DocumentNode.SelectNodes("//img");
308 if (imgNodes != null && imgNodes.Any())
309 {
310 foreach (var node in imgNodes)
311 {
312 node.SetAttributeValue("style", $"max-width: {maxWidth}px");
313 }
314 }
315 }
316  
317 htmlPanel1.Text = htmlDocument.DocumentNode.WriteTo();
318 }
319 break;
320 default:
321 break;
322 }
323  
324 PointF point = new PointF(0, 0);
325 SizeF maxSize = new SizeF(maxWidth, Screen.PrimaryScreen.WorkingArea.Height);
326 var renderSize = HtmlRender.Render(graphics, htmlPanel1.Text, point, maxSize);
327 // total height = height of text fitting in rectangle + the height of the title on top + one extra line for the last line wrap
328 var computedOptimalHeight = (int)Math.Round(renderSize.Height) + labelTitle.Height + (int)Math.Round(htmlPanel1.Font.GetHeight());
329 // keep the default height of a notification constant
330 if (computedOptimalHeight > Height)
331 {
332 Height = computedOptimalHeight;
333 }
334 }
335 }
336 }));
337  
43 office 338 if (EnablePin)
339 {
340 Invoke(new MethodInvoker(() =>
341 {
342 // set the location of the toast
343 Location = new Point(PinPoint.X, PinPoint.Y);
344 }));
345  
346 // remove top-most
347 SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);
348  
349 _formAnimator.Method = AnimationMethodDetached;
350 _formAnimator.Direction = AnimationDirectionDetached;
351  
352 return;
44 office 353 }
354  
355 var notifications = OpenNotifications.ToArray();
356  
357 foreach (var openForm in notifications)
358 {
359 if (openForm == null)
360 {
361 continue;
362 }
363  
364 if (openForm.IsDisposed)
365 {
366 OpenNotifications.Remove(openForm);
367  
368 continue;
369 }
370  
371 if (openForm.IsHandleCreated != true)
372 {
373 continue;
374 }
375  
376 // Move each open form upwards to make room for this one
377 var handle = Handle;
378 openForm.BeginInvoke(new MethodInvoker(() =>
379 {
380 try
381 {
382 if (openForm.Handle == handle)
383 {
384 return;
385 }
386  
387 openForm.Top -= Height;
388  
389 // do not render offscreen
390 if (!IsFormOnScreen(openForm))
391 {
392 OpenNotifications.Remove(openForm);
393 openForm.Close();
394 if (!openForm.IsDisposed)
395 {
396 openForm.Dispose();
397 }
398 }
399 }
400 catch
401 {
402 Debug.WriteLine("Error while moving forms up.");
403 }
404 }));
405 }
406  
407 Invoke(new MethodInvoker(() =>
408 {
409 // set the location of the toast
410 Location = new Point(_screenWidth - Width, _screenHeight - Height);
411 }));
412  
413 OpenNotifications.Add(this);
414  
415 _toastTimer.Enabled = true;
416 _toastTimer.Start();
417 }
418 }
419 catch
420 {
421 Debug.WriteLine("Error on form load event.");
422 }
423 }
424  
49 office 425 private async void HtmlPanel1_ImageLoad(object sender, TheArtOfDev.HtmlRenderer.Core.Entities.HtmlImageLoadEventArgs e)
426 {
427 if(!string.IsNullOrEmpty(UserAgent))
428 {
429 httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
430 }
431  
432 try
433 {
434 using (var response = await httpClient.GetAsync(e.Src))
435 {
436 using (var responseStream = await response.Content.ReadAsStreamAsync())
437 {
438 var image = Image.FromStream(responseStream);
439  
440 e.Callback(image);
441 e.Handled = true;
442 }
443 }
444 }
445 catch
446 {
447 Debug.WriteLine("Error downloading image.");
448 }
449 }
450  
44 office 451 private void ToastForm_FormClosed(object sender, FormClosedEventArgs e)
452 {
453 try
454 {
455 lock (OpenNotificationsLock)
456 {
457 OpenNotifications.Remove(this);
458 }
459 }
460 catch
461 {
462 Debug.WriteLine("Error in form closed event.");
463 }
464 }
465  
466 private void ToastForm_Shown(object sender, EventArgs e)
467 {
49 office 468 _formAnimator = new FormAnimator(this, AnimationMethod, AnimationDirection, AnimationDuration);
469  
44 office 470 // Reverse animation direction for hiding.
471 switch (_formAnimator.Direction)
472 {
473 case FormAnimator.AnimationDirection.Up:
474 _formAnimator.Direction = FormAnimator.AnimationDirection.Down;
475 break;
476 case FormAnimator.AnimationDirection.Down:
477 _formAnimator.Direction = FormAnimator.AnimationDirection.Up;
478 break;
479 case FormAnimator.AnimationDirection.Left:
480 _formAnimator.Direction = FormAnimator.AnimationDirection.Right;
481 break;
482 case FormAnimator.AnimationDirection.Right:
483 _formAnimator.Direction = FormAnimator.AnimationDirection.Left;
484 break;
485 }
486  
487 _showFormTaskCompletionSource.TrySetResult(new { });
488 }
489  
490 private async void ToastTimer_Elapsed(object sender, EventArgs e)
491 {
492 await _showFormTaskCompletionSource.Task;
493  
494 if (IsDisposed)
495 {
496 return;
497 }
498  
499 lock (_toastDetachedLock)
500 {
501 if (_toastDetached)
502 {
503 return;
504 }
505 }
506  
507 try
508 {
509 _toastTimer.Stop();
510  
511 BeginInvoke(new MethodInvoker(() =>
512 {
513 lock (OpenNotificationsLock)
514 {
515 Close();
516 }
517 }));
518 }
519 catch
520 {
521 Debug.WriteLine("Error in timer elapsed event.");
522 }
523 }
524  
525 private void labelTitle_MouseDown(object sender, MouseEventArgs e)
526 {
527 _mouseDown = true;
528 _lastLocation = e.Location;
529 }
530  
531 private void labelTitle_MouseMove(object sender, MouseEventArgs e)
532 {
533 if (!_mouseDown)
534 {
535 return;
536  
537 }
538  
539 Location = new Point((Location.X - _lastLocation.X) + e.X, (Location.Y - _lastLocation.Y) + e.Y);
540  
541 Update();
542  
543 lock (_toastDetachedLock)
544 {
545 if (_toastDetached)
546 {
547 return;
548 }
549  
550 _toastDetached = true;
551  
552 _toastTimer.Elapsed -= ToastTimer_Elapsed;
553 _toastTimer.Stop();
554 _toastTimer.Dispose();
555  
556 BeginInvoke(new MethodInvoker(() =>
557 {
558 lock (OpenNotificationsLock)
559 {
560 if (OpenNotifications.Contains(this))
561 {
562 OpenNotifications.Remove(this);
563 }
564 }
565 }));
566  
567 // remove top-most
568 SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);
569  
570 _formAnimator.Method = AnimationMethodDetached;
571 _formAnimator.Direction = AnimationDirectionDetached;
572 }
573 }
574  
575 private void labelTitle_MouseUp(object sender, MouseEventArgs e)
576 {
577 _mouseDown = false;
578 }
579  
580  
581 #endregion
582  
583 #region Private Methods
584  
585 /// <summary>
586 /// Checks if a form is contained within any of the screens.
587 /// </summary>
588 /// <param name="form">the form to check</param>
589 /// <remarks>https://stackoverflow.com/questions/987018/determining-if-a-form-is-completely-off-screen</remarks>
590 /// <returns>false if the form is entirely or partially offscreen</returns>
591 public bool IsFormOnScreen(Form form)
592 {
593 foreach (Screen screen in Screen.AllScreens)
594 {
595 Rectangle formRectangle = new Rectangle(form.Left, form.Top, form.Width, form.Height);
596  
597 if (screen.WorkingArea.Contains(formRectangle))
598 {
599 return true;
600 }
601 }
602  
603 return false;
604 }
605  
606 #endregion
41 office 607 }
1 office 608 }