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