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