Toasts – Blame information for rev 56
?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 | { |
||
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 | } |