Toasts – Blame information for rev 42

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 }
117 }
118  
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 ToastForm_Load(object sender, EventArgs e)
210 {
211 if (EnableChime)
212 {
213 Task.Factory.StartNew(() =>
214 {
215 using (var memoryStream = new MemoryStream(Chime))
216 {
217 using (var player = new SoundPlayer(memoryStream))
218 {
219 player.Play();
220 }
221 }
222 });
223 }
224  
225 try
226 {
227 lock (OpenNotificationsLock)
228 {
229 // if not image is provided, just collapse the panel for the extra space
230 if (imageBox.Image == null)
231 {
232 splitContainer1.Panel1Collapsed = true;
233 }
234  
235 Invoke(new MethodInvoker(() =>
236 {
237 // compute notification height from body
238 var maxWidth = tableLayoutPanel4.Width;
239 using (var m_Bitmap = new Bitmap(64, 64))
240 {
241 using (var graphics = Graphics.FromImage(m_Bitmap))
242 {
243 switch(ContentType)
244 {
245 case "text/markdown":
246 var htmlDocument = new HtmlAgilityPack.HtmlDocument();
247 htmlDocument.LoadHtml(htmlPanel1.Text);
248 foreach (var node in htmlDocument.DocumentNode.SelectNodes("//img"))
249 {
250 node.SetAttributeValue("style", $"max-width: {maxWidth}px");
251 }
252  
253 htmlPanel1.Text = htmlDocument.DocumentNode.WriteTo();
254 break;
255 default:
256 break;
257 }
258  
259 PointF point = new PointF(0, 0);
260 SizeF maxSize = new SizeF(maxWidth, Screen.PrimaryScreen.WorkingArea.Height);
261 var renderSize = HtmlRender.Render(graphics, htmlPanel1.Text, point, maxSize);
262 // total height = height of text fitting in rectangle + the height of the title on top + one extra line for the last line wrap
263 var computedOptimalHeight = (int)Math.Round(renderSize.Height) + labelTitle.Height + (int)Math.Round(htmlPanel1.Font.GetHeight());
264 // keep the default height of a notification constant
265 if(computedOptimalHeight > Height)
266 {
267 Height = computedOptimalHeight;
268 }
269 }
270 }
271 }));
272  
273 var notifications = OpenNotifications.ToArray();
274  
275 foreach (var openForm in notifications)
276 {
277 if (openForm == null)
278 {
279 continue;
280 }
281  
282 if (openForm.IsDisposed)
283 {
284 OpenNotifications.Remove(openForm);
285  
286 continue;
287 }
288  
289 if (openForm.IsHandleCreated != true)
290 {
291 continue;
292 }
293  
294 // Move each open form upwards to make room for this one
295 var handle = Handle;
296 openForm.BeginInvoke(new MethodInvoker(() =>
297 {
298 try
299 {
300 if (openForm.Handle == handle)
301 {
302 return;
303 }
304  
305 openForm.Top -= Height;
306 }
307 catch
308 {
309 Debug.WriteLine("Error while moving forms up.");
310 }
311 }));
312 }
313  
314 Invoke(new MethodInvoker(() =>
315 {
316 // set the location of the toast
317 Location = new Point(_screenWidth - Width, _screenHeight - Height);
318 }));
319  
320 OpenNotifications.Add(this);
321  
322 _toastTimer.Enabled = true;
323 _toastTimer.Start();
324 }
325 }
326 catch
327 {
328 Debug.WriteLine("Error on form load event.");
329 }
330 }
331  
332 private void ToastForm_FormClosed(object sender, FormClosedEventArgs e)
333 {
334 try
335 {
336 lock (OpenNotificationsLock)
337 {
338 OpenNotifications.Remove(this);
339 }
340 }
341 catch
342 {
343 Debug.WriteLine("Error in form closed event.");
344 }
345 }
346  
347 private void ToastForm_Shown(object sender, EventArgs e)
348 {
349 // Reverse animation direction for hiding.
350 switch (_formAnimator.Direction)
351 {
352 case FormAnimator.AnimationDirection.Up:
353 _formAnimator.Direction = FormAnimator.AnimationDirection.Down;
354 break;
355 case FormAnimator.AnimationDirection.Down:
356 _formAnimator.Direction = FormAnimator.AnimationDirection.Up;
357 break;
358 case FormAnimator.AnimationDirection.Left:
359 _formAnimator.Direction = FormAnimator.AnimationDirection.Right;
360 break;
361 case FormAnimator.AnimationDirection.Right:
362 _formAnimator.Direction = FormAnimator.AnimationDirection.Left;
363 break;
364 }
365  
366 _showFormTaskCompletionSource.TrySetResult(new { });
367 }
368  
369 private async void ToastTimer_Elapsed(object sender, EventArgs e)
370 {
371 await _showFormTaskCompletionSource.Task;
372  
373 if (IsDisposed)
374 {
375 return;
376 }
377  
378 lock(_toastDetachedLock)
379 {
380 if(_toastDetached)
381 {
382 return;
383 }
384 }
385  
386 try
387 {
388 _toastTimer.Stop();
389  
390 BeginInvoke(new MethodInvoker(() =>
391 {
392 lock (OpenNotificationsLock)
393 {
394 Close();
395 }
396 }));
397 }
398 catch
399 {
400 Debug.WriteLine("Error in timer elapsed event.");
401 }
402 }
403  
404 private void labelTitle_MouseDown(object sender, MouseEventArgs e)
405 {
406 _mouseDown = true;
407 _lastLocation = e.Location;
408 }
409  
410 private void labelTitle_MouseMove(object sender, MouseEventArgs e)
411 {
412 if (!_mouseDown)
413 {
414 return;
415  
416 }
417  
418 Location = new Point((Location.X - _lastLocation.X) + e.X, (Location.Y - _lastLocation.Y) + e.Y);
419  
420 Update();
421  
422 lock (_toastDetachedLock)
423 {
424 if (_toastDetached)
425 {
426 return;
427 }
428  
429 _toastDetached = true;
430  
431 _toastTimer.Elapsed -= ToastTimer_Elapsed;
432 _toastTimer.Stop();
433 _toastTimer.Dispose();
434  
435 BeginInvoke(new MethodInvoker(() =>
436 {
437 lock (OpenNotificationsLock)
438 {
439 if (OpenNotifications.Contains(this))
440 {
441 OpenNotifications.Remove(this);
442 }
443 }
444 }));
445  
446 // remove top-most
447 SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);
448  
449 _formAnimator.Method = AnimationMethodDetached;
450 _formAnimator.Direction = AnimationDirectionDetached;
451 }
452 }
453  
454 private void labelTitle_MouseUp(object sender, MouseEventArgs e)
455 {
456 _mouseDown = false;
457 }
458  
459  
460 #endregion
461  
462 private void panel1_Click(object sender, EventArgs e)
463 {
464 try
465 {
466 BeginInvoke(new MethodInvoker(() =>
467 {
468 lock (OpenNotificationsLock)
469 {
470 Close();
471 }
472 }));
473 }
474 catch
475 {
476 Debug.WriteLine("Error in form click event.");
477 }
478 }
479  
480 private void Toast_Click(object sender, EventArgs e)
481 {
482 lock (_toastDetachedLock)
483 {
484 if(!_toastDetached)
485 {
486 return;
487 }
488  
489 BringToFront();
490 }
491 }
492 }
1 office 493 }