Toasts – Blame information for rev 45

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