Toasts – Blame information for rev 41

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