Toasts – Blame information for rev 43

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