Toasts – Rev 41
?pathlinks?
// =====COPYRIGHT=====
// Code originally retrieved from http://www.vbforums.com/showthread.php?t=547778 - no license information supplied
// =====COPYRIGHT=====
using Markdig.Extensions.Tables;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Media;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using TheArtOfDev.HtmlRenderer.WinForms;
using Toasts.Properties;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
using static TheArtOfDev.HtmlRenderer.Adapters.RGraphicsPath;
using static Toasts.FormAnimator;
namespace Toasts
{
public partial class ToastForm : Form
{
#region Public Fields and Properties
public bool EnableChime { get; set; } = true;
public int LingerTime { get; set; } = 5000;
public FormAnimator.AnimationMethod AnimationMethodDetached { get; set; } = FormAnimator.AnimationMethod.Fade;
public FormAnimator.AnimationDirection AnimationDirectionDetached { get; set; } = FormAnimator.AnimationDirection.None;
public FormAnimator.AnimationMethod AnimationMethod { get; set; } = FormAnimator.AnimationMethod.Slide;
public FormAnimator.AnimationDirection AnimationDirection { get; set; } = FormAnimator.AnimationDirection.Up;
public int AnimationDuration { get; set; } = 500;
public string ContentType { get; internal set; } = "text/plain";
public byte[] Chime
{
get => _chime;
set
{
if (value is null)
{
return;
}
_chime = value;
}
}
/// <summary>
///
/// </summary>
/// <remarks>clone the image for safety</remarks>
public Image Image
{
get
{
if (imageBox.Image == null)
{
return null;
}
return new Bitmap(imageBox.Image);
}
set => imageBox.Image = value;
}
#endregion
#region Static Fields and Constants
private static readonly object OpenNotificationsLock = new object();
private static readonly HashSet<ToastForm> OpenNotifications = new HashSet<ToastForm>();
#endregion
#region Private Fields and Properties
private bool _toastDetached;
private object _toastDetachedLock = new object();
private bool _mouseDown;
private Point _lastLocation;
#endregion
#region Private Overrides
protected override bool ShowWithoutActivation => true;
protected override CreateParams CreateParams
{
get
{
var baseParams = base.CreateParams;
//const int WS_EX_NOACTIVATE = 0x08000000;
const int WS_EX_TOOLWINDOW = 0x00000080;
const int WS_EX_TOPMOST = 0x00000008;
baseParams.ExStyle |= WS_EX_TOOLWINDOW | WS_EX_TOPMOST;
return baseParams;
}
}
[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
public static extern IntPtr SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int Y, int cx, int cy, int wFlags);
#endregion
#region Constructors, Destructors and Finalizers
private ToastForm()
{
InitializeComponent();
// round rectangles
//Region = Region.FromHrgn(NativeMethods.CreateRoundRectRgn(0, 0, Width - 5, Height - 5, 20, 20));
_screenWidth = Screen.PrimaryScreen.WorkingArea.Width;
_screenHeight = Screen.PrimaryScreen.WorkingArea.Height;
_formAnimator = new FormAnimator(this, AnimationMethod, AnimationDirection, AnimationDuration);
using (var memoryStream = new MemoryStream())
{
Resources.normal.CopyTo(memoryStream);
memoryStream.Position = 0L;
Chime = memoryStream.ToArray();
}
_showFormTaskCompletionSource = new TaskCompletionSource<object>();
_toastTimer = new System.Timers.Timer { Enabled = false, AutoReset = false, Interval = LingerTime };
_toastTimer.Elapsed += ToastTimer_Elapsed;
}
/// <summary>
/// Display a toast with a title, body and a logo indefinitely on screen.
/// </summary>
/// <param name="title">the toast title</param>
/// <param name="body">the toast body</param>
public ToastForm(string title, string body) : this()
{
labelTitle.Text = title;
htmlPanel1.Text = body;
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && components != null)
{
components.Dispose();
}
if (_toastTimer != null)
{
_toastTimer.Dispose();
_toastTimer = null;
}
if (Image != null)
{
Image.Dispose();
Image = null;
}
base.Dispose(disposing);
}
#endregion
#region Private Delegates, Events, Enums, Properties, Indexers and Fields
private readonly FormAnimator _formAnimator;
private System.Timers.Timer _toastTimer;
private readonly int _screenWidth;
private readonly int _screenHeight;
private readonly TaskCompletionSource<object> _showFormTaskCompletionSource;
private byte[] _chime;
#endregion
#region Event Handlers
private void ToastForm_Load(object sender, EventArgs e)
{
if (EnableChime)
{
Task.Factory.StartNew(() =>
{
using (var memoryStream = new MemoryStream(Chime))
{
using (var player = new SoundPlayer(memoryStream))
{
player.Play();
}
}
});
}
try
{
lock (OpenNotificationsLock)
{
// if not image is provided, just collapse the panel for the extra space
if (imageBox.Image == null)
{
splitContainer1.Panel1Collapsed = true;
}
Invoke(new MethodInvoker(() =>
{
// compute notification height from body
var maxWidth = tableLayoutPanel4.Width;
using (var m_Bitmap = new Bitmap(64, 64))
{
using (var graphics = Graphics.FromImage(m_Bitmap))
{
switch(ContentType)
{
case "text/markdown":
var htmlDocument = new HtmlAgilityPack.HtmlDocument();
htmlDocument.LoadHtml(htmlPanel1.Text);
foreach (var node in htmlDocument.DocumentNode.SelectNodes("//img"))
{
node.SetAttributeValue("style", $"max-width: {maxWidth}px");
}
htmlPanel1.Text = htmlDocument.DocumentNode.WriteTo();
break;
default:
break;
}
PointF point = new PointF(0, 0);
SizeF maxSize = new SizeF(maxWidth, Screen.PrimaryScreen.WorkingArea.Height);
var renderSize = HtmlRender.Render(graphics, htmlPanel1.Text, point, maxSize);
// total height = height of text fitting in rectangle + the height of the title on top + one extra line for the last line wrap
var computedOptimalHeight = (int)Math.Round(renderSize.Height) + labelTitle.Height + (int)Math.Round(htmlPanel1.Font.GetHeight());
// keep the default height of a notification constant
if(computedOptimalHeight > Height)
{
Height = computedOptimalHeight;
}
}
}
}));
var notifications = OpenNotifications.ToArray();
foreach (var openForm in notifications)
{
if (openForm == null)
{
continue;
}
if (openForm.IsDisposed)
{
OpenNotifications.Remove(openForm);
continue;
}
if (openForm.IsHandleCreated != true)
{
continue;
}
// Move each open form upwards to make room for this one
var handle = Handle;
openForm.BeginInvoke(new MethodInvoker(() =>
{
try
{
if (openForm.Handle == handle)
{
return;
}
openForm.Top -= Height;
}
catch
{
Debug.WriteLine("Error while moving forms up.");
}
}));
}
Invoke(new MethodInvoker(() =>
{
// set the location of the toast
Location = new Point(_screenWidth - Width, _screenHeight - Height);
}));
OpenNotifications.Add(this);
_toastTimer.Enabled = true;
_toastTimer.Start();
}
}
catch
{
Debug.WriteLine("Error on form load event.");
}
}
private void ToastForm_FormClosed(object sender, FormClosedEventArgs e)
{
try
{
lock (OpenNotificationsLock)
{
OpenNotifications.Remove(this);
}
}
catch
{
Debug.WriteLine("Error in form closed event.");
}
}
private void ToastForm_Shown(object sender, EventArgs e)
{
// Reverse animation direction for hiding.
switch (_formAnimator.Direction)
{
case FormAnimator.AnimationDirection.Up:
_formAnimator.Direction = FormAnimator.AnimationDirection.Down;
break;
case FormAnimator.AnimationDirection.Down:
_formAnimator.Direction = FormAnimator.AnimationDirection.Up;
break;
case FormAnimator.AnimationDirection.Left:
_formAnimator.Direction = FormAnimator.AnimationDirection.Right;
break;
case FormAnimator.AnimationDirection.Right:
_formAnimator.Direction = FormAnimator.AnimationDirection.Left;
break;
}
_showFormTaskCompletionSource.TrySetResult(new { });
}
private async void ToastTimer_Elapsed(object sender, EventArgs e)
{
await _showFormTaskCompletionSource.Task;
if (IsDisposed)
{
return;
}
lock(_toastDetachedLock)
{
if(_toastDetached)
{
return;
}
}
try
{
_toastTimer.Stop();
BeginInvoke(new MethodInvoker(() =>
{
lock (OpenNotificationsLock)
{
Close();
}
}));
}
catch
{
Debug.WriteLine("Error in timer elapsed event.");
}
}
private void labelTitle_MouseDown(object sender, MouseEventArgs e)
{
_mouseDown = true;
_lastLocation = e.Location;
}
private void labelTitle_MouseMove(object sender, MouseEventArgs e)
{
if (!_mouseDown)
{
return;
}
Location = new Point((Location.X - _lastLocation.X) + e.X, (Location.Y - _lastLocation.Y) + e.Y);
Update();
lock (_toastDetachedLock)
{
if (_toastDetached)
{
return;
}
_toastDetached = true;
_toastTimer.Elapsed -= ToastTimer_Elapsed;
_toastTimer.Stop();
_toastTimer.Dispose();
BeginInvoke(new MethodInvoker(() =>
{
lock (OpenNotificationsLock)
{
if (OpenNotifications.Contains(this))
{
OpenNotifications.Remove(this);
}
}
}));
// remove top-most
SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);
_formAnimator.Method = AnimationMethodDetached;
_formAnimator.Direction = AnimationDirectionDetached;
}
}
private void labelTitle_MouseUp(object sender, MouseEventArgs e)
{
_mouseDown = false;
}
#endregion
private void panel1_Click(object sender, EventArgs e)
{
try
{
BeginInvoke(new MethodInvoker(() =>
{
lock (OpenNotificationsLock)
{
Close();
}
}));
}
catch
{
Debug.WriteLine("Error in form click event.");
}
}
private void Toast_Click(object sender, EventArgs e)
{
lock (_toastDetachedLock)
{
if(!_toastDetached)
{
return;
}
BringToFront();
}
}
}
}
Generated by GNU Enscript 1.6.5.90.