Toasts – Rev 41

Subversion Repositories:
Rev:
// =====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.