Toasts – Blame information for rev 56

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