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