Toasts – Diff between revs 52 and 53

Subversion Repositories:
Rev:
Show entire fileIgnore whitespace
Rev 52 Rev 53
Line 11... Line 11...
11 using System.IO; 11 using System.IO;
12 using System.Linq; 12 using System.Linq;
13 using System.Media; 13 using System.Media;
14 using System.Net.Http; 14 using System.Net.Http;
15 using System.Runtime.InteropServices; 15 using System.Runtime.InteropServices;
-   16 using System.Runtime.Remoting.Messaging;
-   17 using System.Runtime.Serialization;
16 using System.Threading; 18 using System.Threading;
17 using System.Threading.Tasks; 19 using System.Threading.Tasks;
18 using System.Windows.Forms; 20 using System.Windows.Forms;
-   21 using System.Xml.Linq;
19 using TheArtOfDev.HtmlRenderer.WinForms; 22 using TheArtOfDev.HtmlRenderer.WinForms;
20 using Toasts.Properties; 23 using Toasts.Properties;
-   24 using Toasts.Utilities;
21   25  
22 namespace Toasts 26 namespace Toasts
23 { 27 {
24 public partial class ToastForm : Form 28 public partial class ToastForm : Form
25 { 29 {
26 #region Public Fields and Properties 30 #region Public Fields and Properties
27   -  
28 public string UserAgent { get; set; } = string.Empty; 31 public string UserAgent { get; set; } = string.Empty;
Line 29... Line 32...
29   32  
Line 30... Line 33...
30 public bool EnableChime { get; set; } = true; 33 public bool EnableChime { get; set; } = true;
Line 80... Line 83...
80 /// 83 ///
81 /// </summary> 84 /// </summary>
82 /// <remarks>clone the image for safety</remarks> 85 /// <remarks>clone the image for safety</remarks>
83 public Image Image 86 public Image Image
84 { 87 {
-   88 get => _image;
85 get 89 set
86 { 90 {
87 try 91 if (value == _image)
88 { 92 {
89   -  
90 return new Bitmap(imageBox.Image); -  
91 } -  
92 catch -  
93 { -  
94 return null; 93 return;
95 } 94 }
-   95  
-   96 _image = new Bitmap(value);
96 } 97 }
97 set => imageBox.Image = value; -  
98 } 98 }
Line 99... Line 99...
99   99  
Line 100... Line 100...
100 #endregion 100 #endregion
Line 101... Line 101...
101   101  
Line 102... Line 102...
102 #region Private Delegates, Events, Enums, Properties, Indexers and Fields 102 #region Private Delegates, Events, Enums, Properties, Indexers and Fields
-   103  
-   104 private static readonly object OpenNotificationsLock = new object();
Line 103... Line 105...
103   105  
Line 104... Line 106...
104 private static readonly object OpenNotificationsLock = new object(); 106 private static readonly List<ToastForm> OpenNotifications = new List<ToastForm>();
Line -... Line 107...
-   107  
-   108 private TaskCompletionSource<object> _handleCreatedTaskCompletionSource;
105   109  
Line 106... Line 110...
106 private static readonly HashSet<ToastForm> OpenNotifications = new HashSet<ToastForm>(); 110 private bool _toastDetached;
Line 107... Line 111...
107   111  
-   112 private object _toastDetachedLock = new object();
-   113  
Line 108... Line 114...
108 private bool _toastDetached; 114 private Image _image;
Line 109... Line 115...
109   115  
Line 110... Line 116...
110 private object _toastDetachedLock = new object(); 116 private bool _mouseDown;
Line 111... Line 117...
111   117  
Line 112... Line -...
112 private bool _mouseDown; -  
113   -  
114 private Point _lastLocation; 118 private Point _lastLocation;
Line 115... Line 119...
115   119  
Line 116... Line 120...
116 private static HttpClient httpClient = new HttpClient(); 120 private static HttpClient _httpClient = new HttpClient();
Line 155... Line 159...
155 #region Constructors, Destructors and Finalizers 159 #region Constructors, Destructors and Finalizers
Line 156... Line 160...
156   160  
157 private ToastForm() 161 private ToastForm()
158 { 162 {
-   163 InitializeComponent();
159 InitializeComponent(); 164  
-   165 _handleCreatedTaskCompletionSource = new TaskCompletionSource<object>();
160 // round rectangles 166  
Line 161... Line 167...
161 //Region = Region.FromHrgn(NativeMethods.CreateRoundRectRgn(0, 0, Width - 5, Height - 5, 20, 20)); 167 HandleCreated += ToastForm_HandleCreated;
162   168  
163 _screenWidth = Screen.PrimaryScreen.WorkingArea.Width; -  
Line -... Line 169...
-   169 _screenWidth = Screen.PrimaryScreen.WorkingArea.Width;
164 _screenHeight = Screen.PrimaryScreen.WorkingArea.Height; 170 _screenHeight = Screen.PrimaryScreen.WorkingArea.Height;
Line -... Line 171...
-   171  
165 _showFormTaskCompletionSource = new TaskCompletionSource<object>(); 172 _formAnimator = new FormAnimator(this, AnimationMethod, AnimationDirection, AnimationDuration);
166   173 }
167 } 174  
168   175  
169 /// <summary> 176 /// <summary>
Line 183... Line 190...
183 /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> 190 /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
184 protected override void Dispose(bool disposing) 191 protected override void Dispose(bool disposing)
185 { 192 {
186 if (disposing && components != null) 193 if (disposing && components != null)
187 { 194 {
188 components.Dispose(); 195 HandleCreated -= ToastForm_HandleCreated;
189 } -  
Line 190... Line 196...
190   196  
191 if (_toastTimer != null) 197 if (_toastTimer != null)
192 { -  
193 _toastTimer.Stop(); 198 {
194 _toastTimer.Dispose(); 199 _toastTimer.Dispose();
195 _toastTimer = null; 200 _toastTimer = null;
Line 196... Line 201...
196 } 201 }
197   202  
198 if (Image != null) 203 if (_image != null)
199 { 204 {
-   205 _image.Dispose();
-   206 _image = null;
-   207 }
200 Image.Dispose(); 208  
Line 201... Line 209...
201 Image = null; 209 components.Dispose();
202 } 210 }
Line 203... Line 211...
203   211  
Line 204... Line 212...
204 base.Dispose(disposing); 212 base.Dispose(disposing);
Line -... Line 213...
-   213 }
-   214  
-   215 #endregion
-   216  
-   217 #region Event Handlers
-   218  
-   219 private void ToastForm_HandleCreated(object sender, EventArgs e)
-   220 {
-   221 _handleCreatedTaskCompletionSource.TrySetResult(new { });
-   222 }
-   223  
-   224 private void Toast_MouseEnter(object sender, EventArgs e)
-   225 {
-   226 if (_toastTimer == null)
-   227 {
-   228 return;
-   229 }
-   230  
-   231 _toastTimer.Stop();
-   232 }
-   233  
-   234 private void Toast_MouseLeave(object sender, EventArgs e)
-   235 {
-   236 if (_toastTimer == null)
-   237 {
205 } 238 return;
206   239 }
207 #endregion 240  
208   241 _toastTimer.Start();
209 #region Event Handlers 242 }
Line 220... Line 253...
220 } 253 }
221 } 254 }
Line 222... Line 255...
222   255  
223 private void panel2_Click(object sender, EventArgs e) 256 private void panel2_Click(object sender, EventArgs e)
224 { 257 {
225 try 258 lock (OpenNotificationsLock)
226 { 259 {
227 BeginInvoke(new MethodInvoker(() => 260 this.InvokeIfRequired(form =>
228 { -  
229 lock (OpenNotificationsLock) -  
230 { 261 {
231 Close(); -  
232 } 262 form.Close();
233 })); -  
234 } -  
235 catch -  
236 { -  
237 Debug.WriteLine("Error in form click event."); 263 });
238 } 264 }
Line 239... Line 265...
239 } 265 }
240   266  
Line 249... Line 275...
249   275  
250 BringToFront(); 276 BringToFront();
251 } 277 }
Line 252... Line 278...
252 } 278 }
253   279  
254 private void ToastForm_Load(object sender, EventArgs e) -  
255 { -  
256 // set up the timer when the notification should be removed -  
257 _toastTimer = new System.Timers.Timer { Enabled = false, AutoReset = false, Interval = LingerTime }; -  
258 _toastTimer.Elapsed += ToastTimer_Elapsed; 280 private async void ToastForm_Load(object sender, EventArgs e)
259   281 {
260 if (EnableChime) 282 if (EnableChime)
261 { 283 {
262 Task.Factory.StartNew(() => 284 try
263 { 285 {
264 try 286 using (var memoryStream = new MemoryStream(Chime))
265 { 287 {
266 using (var memoryStream = new MemoryStream(Chime)) -  
267 { -  
268 using (var player = new SoundPlayer(memoryStream)) 288 using (var player = new SoundPlayer(memoryStream))
269 { -  
270 player.Play(); 289 {
271 } 290 player.Play();
-   291 }
272 } 292 }
273 } 293 }
274 catch 294 catch
275 { -  
276 Debug.WriteLine("Sound file could not be played."); 295 {
277 } 296 Debug.WriteLine("Sound file could not be played.");
Line 278... Line -...
278 }); -  
279 } -  
280   297 }
281 try -  
282 { -  
283 lock (OpenNotificationsLock) -  
284 { -  
285 // if not image is provided, just collapse the panel for the extra space -  
286 if (imageBox.Image == null) -  
Line -... Line 298...
-   298 }
-   299  
-   300 await _handleCreatedTaskCompletionSource.Task;
-   301  
-   302 // if no image is provided, collapse the panel for the extra space
287 { 303 // otherwise display the image
288 splitContainer1.Panel1Collapsed = true; 304 switch (_image == null)
-   305 {
289 } 306 case true:
290   307 splitContainer1.Panel1Collapsed = true;
Line 291... Line -...
291 Invoke(new MethodInvoker(() => -  
292 { 308 break;
293 // compute notification height from body -  
294 var maxWidth = tableLayoutPanel4.Width; -  
295   -  
296 switch (ContentType) -  
297 { -  
298 case "text/markdown": -  
299 var htmlDocument = new HtmlAgilityPack.HtmlDocument(); -  
300 var panelText = htmlPanel1.Text; -  
301 if (!string.IsNullOrEmpty(panelText)) -  
302 { -  
303 htmlDocument.LoadHtml(panelText); -  
304 if (htmlDocument.DocumentNode != null && htmlDocument.DocumentNode.Descendants().Any()) -  
305 { -  
306 var imgNodes = htmlDocument.DocumentNode.SelectNodes("//img"); -  
307 if (imgNodes != null && imgNodes.Any()) -  
308 { -  
309 foreach (var node in imgNodes) -  
Line 310... Line -...
310 { -  
311 node.SetAttributeValue("style", $"max-width: {maxWidth}px"); -  
312 } 309 default:
313 } 310 imageBox.BackgroundImage = _image;
314 } -  
315   -  
Line -... Line 311...
-   311 break;
-   312  
-   313 }
316 htmlPanel1.Text = htmlDocument.DocumentNode.WriteTo(); 314  
-   315 // compute notification height from body
-   316 var maxWidth = tableLayoutPanel4.Width;
-   317  
-   318 switch (ContentType)
317 } 319 {
318 break; 320 case "text/markdown":
319 default: 321 var htmlDocument = new HtmlAgilityPack.HtmlDocument();
320 break; 322 var panelText = htmlPanel1.Text;
321 } 323 if (!string.IsNullOrEmpty(panelText))
-   324 {
-   325 htmlDocument.LoadHtml(panelText);
-   326 if (htmlDocument.DocumentNode != null && htmlDocument.DocumentNode.Descendants().Any())
322   327 {
323 //var maxSize = new Size(maxWidth, Screen.PrimaryScreen.WorkingArea.Height); 328 var imgNodes = htmlDocument.DocumentNode.SelectNodes("//img");
324 using (var image = HtmlRender.RenderToImage(htmlPanel1.Text, maxWidth, 0, Color.Empty)) 329 if (imgNodes != null && imgNodes.Any())
325 { -  
326 var height = image.Height + labelTitle.Height; -  
327 if(height > Height) -  
328 { -  
329 Height = height; -  
330 } -  
331 } -  
332 })); -  
333   -  
334 if (EnablePin) -  
335 { -  
336 Invoke(new MethodInvoker(() => -  
337 { -  
338 // set the location of the toast -  
339 Location = new Point(PinPoint.X, PinPoint.Y); -  
Line 340... Line 330...
340 })); 330 {
341   331 foreach (var node in imgNodes)
-   332 {
-   333 node.SetAttributeValue("style", $"max-width: {maxWidth}px");
-   334 }
-   335 }
Line -... Line 336...
-   336 }
-   337  
342 // remove top-most 338 htmlPanel1.Text = htmlDocument.DocumentNode.WriteTo();
-   339 }
-   340 break;
-   341 default:
-   342 break;
-   343 }
Line -... Line 344...
-   344  
-   345 using (var image = HtmlRender.RenderToImage(htmlPanel1.Text, maxWidth, 0, Color.Empty))
343 SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002); 346 {
-   347 var height = image.Height + labelTitle.Height;
-   348 if (height > Height)
344   349 {
345 _formAnimator.Method = AnimationMethodDetached; -  
346 _formAnimator.Direction = AnimationDirectionDetached; -  
347   350 Height = height;
348 return; -  
349 } -  
350   351 }
351 var notifications = OpenNotifications.ToArray(); 352 }
352   -  
353 foreach (var openForm in notifications) -  
354 { -  
355 if (openForm == null) 353  
Line 356... Line -...
356 { -  
357 continue; 354 if (EnablePin)
358 } 355 {
359   -  
Line 360... Line -...
360 if (openForm.IsDisposed) -  
361 { 356 lock (OpenNotificationsLock)
362 OpenNotifications.Remove(openForm); 357 {
363   -  
364 continue; -  
365 } -  
366   -  
367 if (openForm.IsHandleCreated != true) -  
368 { -  
369 continue; -  
Line 370... Line 358...
370 } 358 this.InvokeIfRequired(form =>
-   359 {
Line 371... Line 360...
371   360 // set the location of the toast
372 // Move each open form upwards to make room for this one -  
373 var handle = Handle; 361 form.Location = new Point(PinPoint.X, PinPoint.Y);
374 openForm.BeginInvoke(new MethodInvoker(() => 362 });
375 { -  
376 try -  
377 { 363 }
378 if (openForm.Handle == handle) 364  
379 { -  
380 return; -  
381 } -  
382   -  
383 openForm.Top -= Height; 365 // remove top-most
384   366 SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);
385 // do not render offscreen -  
386 if (!IsFormOnScreen(openForm)) 367  
387 { 368 _formAnimator.Method = AnimationMethodDetached;
Line 388... Line 369...
388 OpenNotifications.Remove(openForm); 369 _formAnimator.Direction = AnimationDirectionDetached;
389 openForm.Close(); 370  
-   371 return;
-   372 }
-   373  
-   374 lock (OpenNotificationsLock)
390 if (!openForm.IsDisposed) 375 {
391 { 376 foreach (var openForm in new List<ToastForm>(OpenNotifications))
-   377 {
-   378 if (openForm == null || openForm.IsDisposed)
-   379 {
392 openForm.Dispose(); 380 OpenNotifications.Remove(openForm);
-   381 continue;
Line 393... Line 382...
393 } 382 }
-   383  
Line 394... Line 384...
394 } 384 openForm.InvokeIfRequired(form =>
395 } 385 {
396 catch 386 foreach (Screen screen in Screen.AllScreens)
397 { -  
398 Debug.WriteLine("Error while moving forms up."); -  
399 } -  
400 })); -  
401 } 387 {
-   388 if (!screen.WorkingArea.Contains(new Rectangle(form.Left, form.Top - Height, form.Width, form.Height)))
-   389 {
-   390 form.Close();
-   391 OpenNotifications.Remove(form);
-   392 }
402   393 }
Line 403... Line 394...
403 Invoke(new MethodInvoker(() => 394 form.Top -= Height;
404 { 395 });
405 // set the location of the toast 396 }
406 Location = new Point(_screenWidth - Width, _screenHeight - Height); 397  
-   398 // set the location of the toast
-   399 Location = new Point(_screenWidth - Width, _screenHeight - Height);
-   400  
-   401 OpenNotifications.Add(this);
407 })); 402 // show the form
-   403 //Opacity = 1;
-   404 }
408   405  
Line 409... Line 406...
409 OpenNotifications.Add(this); 406 // set up the timer when the notification should be removed
410   407 _toastTimer = new System.Timers.Timer { Enabled = true, AutoReset = false, Interval = LingerTime };
411 _toastTimer.Enabled = true; 408 _toastTimer.Elapsed += ToastTimer_Elapsed;
412 _toastTimer.Start(); 409 _toastTimer.Start();
413 } 410 }
414 } 411  
415 catch 412 private async void HtmlPanel1_ImageLoad(object sender, TheArtOfDev.HtmlRenderer.Core.Entities.HtmlImageLoadEventArgs e)
Line 444... Line 441...
444 } 441 }
445 } 442 }
Line 446... Line 443...
446   443  
447 private void ToastForm_FormClosed(object sender, FormClosedEventArgs e) 444 private void ToastForm_FormClosed(object sender, FormClosedEventArgs e)
448 { 445 {
449 try 446 lock (OpenNotificationsLock)
450 { -  
451 lock (OpenNotificationsLock) -  
452 { 447 {
453 OpenNotifications.Remove(this); -  
454 } -  
455 } -  
456 catch -  
457 { -  
458 Debug.WriteLine("Error in form closed event."); 448 OpenNotifications.Remove(this);
459 } 449 }
Line 460... Line 450...
460 } 450 }
461   451  
-   452 private async void ToastForm_Shown(object sender, EventArgs e)
462 private void ToastForm_Shown(object sender, EventArgs e) 453 {
Line 463... Line 454...
463 { 454 // wait for window creation
464 _formAnimator = new FormAnimator(this, AnimationMethod, AnimationDirection, AnimationDuration); 455 await _handleCreatedTaskCompletionSource.Task;
465   456  
466 // Reverse animation direction for hiding. 457 // Reverse animation direction for hiding.
Line 478... Line 469...
478 case FormAnimator.AnimationDirection.Right: 469 case FormAnimator.AnimationDirection.Right:
479 _formAnimator.Direction = FormAnimator.AnimationDirection.Left; 470 _formAnimator.Direction = FormAnimator.AnimationDirection.Left;
480 break; 471 break;
481 } 472 }
Line 482... Line -...
482   -  
-   473  
483 _showFormTaskCompletionSource.TrySetResult(new { }); 474  
Line 484... Line 475...
484 } 475 }
485   476  
486 private async void ToastTimer_Elapsed(object sender, EventArgs e) -  
487 { -  
488 await _showFormTaskCompletionSource.Task; 477 private void ToastTimer_Elapsed(object sender, EventArgs e)
489   478 {
490 if (IsDisposed) 479 if (IsDisposed)
491 { 480 {
Line 502... Line 491...
502   491  
503 try 492 try
504 { 493 {
Line 505... Line 494...
505 _toastTimer.Stop(); 494 _toastTimer.Stop();
506   495  
507 BeginInvoke(new MethodInvoker(() => 496 lock (OpenNotificationsLock)
508 { 497 {
509 lock (OpenNotificationsLock) 498 this.InvokeIfRequired(form =>
510 { 499 {
511 Close(); 500 form.Close();
512 } 501 });
513 })); 502 }
514 } 503 }
515 catch 504 catch
516 { 505 {
Line 546... Line 535...
546 _toastDetached = true; 535 _toastDetached = true;
Line 547... Line 536...
547   536  
548 _toastTimer.Elapsed -= ToastTimer_Elapsed; 537 _toastTimer.Elapsed -= ToastTimer_Elapsed;
549 _toastTimer.Stop(); 538 _toastTimer.Stop();
-   539 _toastTimer.Dispose();
Line 550... Line 540...
550 _toastTimer.Dispose(); 540 _toastTimer = null;
551   541  
552 BeginInvoke(new MethodInvoker(() => -  
553 { -  
554 lock (OpenNotificationsLock) -  
555 { -  
556 if (OpenNotifications.Contains(this)) 542 lock (OpenNotificationsLock)
557 { -  
558 OpenNotifications.Remove(this); -  
559 } 543 {
Line 560... Line 544...
560 } 544 OpenNotifications.Remove(this);
561 })); 545 }
Line 562... Line 546...
562   546  
Line 576... Line 560...
576   560  
Line 577... Line 561...
577 #endregion 561 #endregion
Line 578... Line -...
578   -  
579 #region Private Methods -  
580   -  
581 /// <summary> -  
582 /// Checks if a form is contained within any of the screens. -  
583 /// </summary> -  
584 /// <param name="form">the form to check</param> -  
585 /// <remarks>https://stackoverflow.com/questions/987018/determining-if-a-form-is-completely-off-screen</remarks> -  
586 /// <returns>false if the form is entirely or partially offscreen</returns> -  
587 public bool IsFormOnScreen(Form form) -  
588 { -  
589 foreach (Screen screen in Screen.AllScreens) -  
590 { -  
591 Rectangle formRectangle = new Rectangle(form.Left, form.Top, form.Width, form.Height); -  
592   -  
593 if (screen.WorkingArea.Contains(formRectangle)) -  
594 { -  
595 return true; -  
596 } -  
597 } -  
598   -  
599 return false; 562  
600 } -  
601   -  
602 #endregion -  
603   -  
604 private void Toast_MouseEnter(object sender, EventArgs e) -  
605 { -  
606 _toastTimer.Stop(); -  
607 } -  
608   -  
609 private void Toast_MouseLeave(object sender, EventArgs e) -  
610 { 563 #region Private Methods
611 _toastTimer.Start(); 564  
612 } 565 #endregion