Toasts – Rev 57

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.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Linq;
using TheArtOfDev.HtmlRenderer.WinForms;
using Toasts.Properties;
using Toasts.Utilities;

namespace Toasts
{
    public partial class ToastForm : Form
    {
        #region Public Fields and Properties
        public string UserAgent { get; set; } = string.Empty;

        public bool EnableChime { get; set; } = true;

        public int LingerTime { get; set; } = 5000;

        public int AnimationDuration { get; set; } = 500;

        public string ContentType { get; internal set; } = "text/plain";

        public byte[] Chime
        {
            get
            {
                // offer the default built-in chime
                if(_chime == null)
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        Resources.normal.CopyTo(memoryStream);

                        memoryStream.Position = 0L;

                        return memoryStream.ToArray();
                    }
                }

                return _chime;
            }
            set
            {
                if (value is null)
                {
                    return;
                }

                _chime = value;
            }
        }

        public Proxy Proxy { get; set; }

        public bool EnablePin { get; set; }

        public Point PinPoint { get; set; }

        /// <summary>
        /// 
        /// </summary>
        /// <remarks>clone the image for safety</remarks>
        public Image Image
        {
            get 
            {
                if(_image == null)
                {
                    return null;
                }

                var binaryFormatter = new BinaryFormatter();
                using (var memoryStream = new MemoryStream())
                {
                    binaryFormatter.Serialize(memoryStream, _image);
                    memoryStream.Position = 0L;
                    return  (Bitmap)binaryFormatter.Deserialize(memoryStream);
                }
            }
            set
            {
                if (value == _image)
                {
                    return;
                }

                var binaryFormatter = new BinaryFormatter();
                using (var memoryStream = new MemoryStream())
                {
                    binaryFormatter.Serialize(memoryStream, value);
                    memoryStream.Position = 0L;
                    _image = (Bitmap)binaryFormatter.Deserialize(memoryStream);
                }
            }
        }

        #endregion

        #region Private Delegates, Events, Enums, Properties, Indexers and Fields

        private static readonly object OpenNotificationsLock = new object();

        private static readonly List<ToastForm> OpenNotifications = new List<ToastForm>();

        private TaskCompletionSource<object> _handleCreatedTaskCompletionSource;

        private bool _toastDetached;

        private object _toastDetachedLock = new object();

        private Image _image;

        private bool _mouseDown;

        private Point _lastLocation;

        private static HttpClient _httpClient;

        private static object _httpClientDefaultRequestHeadersLock = new object();

        private System.Timers.Timer _toastTimer;

        private readonly int _screenWidth;

        private readonly int _screenHeight;

        private byte[] _chime;
        
        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;
            }
        }

        #endregion

        #region Natives
        [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();

            _handleCreatedTaskCompletionSource = new TaskCompletionSource<object>();

            HandleCreated += ToastForm_HandleCreated;

            _screenWidth = Screen.PrimaryScreen.WorkingArea.Width;
            _screenHeight = Screen.PrimaryScreen.WorkingArea.Height;
        }


        /// <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;

            var httpClientHandler = new HttpClientHandler
            {
                // mono does not implement this
                //SslProtocols = SslProtocols.Tls12
            };

            if (Proxy != null && Proxy.Enable)
            {
                httpClientHandler.Proxy = new WebProxy(Proxy.Url, false, new string[] { },
                    new NetworkCredential(Proxy.Username, Proxy.Password));
            }

            _httpClient = new HttpClient(httpClientHandler);
        }

        /// <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)
            {
                HandleCreated -= ToastForm_HandleCreated;

                if (_toastTimer != null)
                {
                    _toastTimer.Dispose();
                    _toastTimer = null;
                }

                if (_image != null)
                {
                    _image.Dispose();
                    _image = null;
                }

                components.Dispose();
            }

            base.Dispose(disposing);
        }

        #endregion

        #region Event Handlers

        private void ToastForm_HandleCreated(object sender, EventArgs e)
        {
            _handleCreatedTaskCompletionSource.TrySetResult(new { });
        }

        private void Toast_MouseEnter(object sender, EventArgs e)
        {
            if (_toastTimer == null)
            {
                return;
            }

            _toastTimer.Stop();
        }

        private void Toast_MouseLeave(object sender, EventArgs e)
        {
            if (_toastTimer == null)
            {
                return;
            }

            _toastTimer.Start();
        }

        private void panel1_Click(object sender, EventArgs e)
        {
            try
            {
                Clipboard.SetText(htmlPanel1.Text);
            }
            catch
            {
                Debug.WriteLine("Could not copy to clipboard.");
            }
        }

        private void panel2_Click(object sender, EventArgs e)
        {
            lock (OpenNotificationsLock)
            {
                this.InvokeIfRequired(form =>
                {
                    form.Close();
                });
            }
        }

        private void Toast_Click(object sender, EventArgs e)
        {
            lock (_toastDetachedLock)
            {
                if (!_toastDetached)
                {
                    return;
                }

                BringToFront();
            }
        }

        private async void ToastForm_Load(object sender, EventArgs e)
        {
            if (EnableChime)
            {
                try
                {
                    using (var memoryStream = new MemoryStream(Chime))
                    {
                        using (var player = new SoundPlayer(memoryStream))
                        {
                            player.Play();
                        }
                    }
                }
                catch
                {
                    Debug.WriteLine("Sound file could not be played.");
                }
            }

            await _handleCreatedTaskCompletionSource.Task;

            // if no image is provided, collapse the panel for the extra space
            // otherwise display the image
            switch (_image == null)
            {
                case true:
                    splitContainer1.Panel1Collapsed = true;
                    break;
                default:
                    imageBox.BackgroundImage = _image;
                    break;

            }

            // compute notification height from body
            var maxWidth = tableLayoutPanel4.Width;

            switch (ContentType)
            {
                case "text/markdown":
                    var htmlDocument = new HtmlAgilityPack.HtmlDocument();
                    var panelText = htmlPanel1.Text;
                    if (!string.IsNullOrEmpty(panelText))
                    {
                        htmlDocument.LoadHtml(panelText);
                        if (htmlDocument.DocumentNode != null && htmlDocument.DocumentNode.Descendants().Any())
                        {
                            var imgNodes = htmlDocument.DocumentNode.SelectNodes("//img");
                            if (imgNodes != null && imgNodes.Any())
                            {
                                foreach (var node in imgNodes)
                                {
                                    node.SetAttributeValue("style", $"max-width: {maxWidth}px");
                                }
                            }
                        }

                        htmlPanel1.Text = htmlDocument.DocumentNode.WriteTo();
                    }
                    break;
                default:
                    break;
            }

            using (var image = HtmlRender.RenderToImage(htmlPanel1.Text, maxWidth, 0, Color.Empty))
            {
                var height = image.Height + labelTitle.Height;
                if (height > Height)
                {
                    Height = height;
                }
            }

            if (EnablePin)
            {
                lock (OpenNotificationsLock)
                {
                    this.InvokeIfRequired(form =>
                    {
                        // set the location of the toast
                        form.Location = new Point(PinPoint.X, PinPoint.Y);
                    });
                }

                // remove top-most
                SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);

                return;
            }

            lock (OpenNotificationsLock)
            {
                foreach (var openForm in new List<ToastForm>(OpenNotifications))
                {
                    if (openForm == null || openForm.IsDisposed)
                    {
                        OpenNotifications.Remove(openForm);
                        continue;
                    }

                    openForm.InvokeIfRequired(form =>
                    {
                        // if the form will end up off-screen, then don't even bother moving it
                        foreach (Screen screen in Screen.AllScreens)
                        {
                            if (!screen.WorkingArea.Contains(new Rectangle(form.Left, form.Top - Height, form.Width, form.Height)))
                            {
                                form.Close();
                                OpenNotifications.Remove(form);

                                return;
                            }
                        }

                        form.Top -= Height;

                    });
                }

                // set the location of the toast
                Location = new Point(_screenWidth - Width, _screenHeight - Height);

                OpenNotifications.Add(this);
                // show the form
                //Opacity = 1;
            }

            // set up the timer when the notification should be removed
            _toastTimer = new System.Timers.Timer { Enabled = true, AutoReset = false, Interval = LingerTime };
            _toastTimer.Elapsed += ToastTimer_Elapsed;
            _toastTimer.Start();
        }

        private async void HtmlPanel1_ImageLoad(object sender, TheArtOfDev.HtmlRenderer.Core.Entities.HtmlImageLoadEventArgs e)
        {
            if (!string.IsNullOrEmpty(UserAgent))
            {
                lock (_httpClientDefaultRequestHeadersLock)
                {
                    if (!_httpClient.DefaultRequestHeaders.Contains("User-Agent"))
                    {
                        _httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
                    }
                }
            }

            try
            {
                using (var response = await _httpClient.GetAsync(e.Src))
                {
                    using (var responseStream = await response.Content.ReadAsStreamAsync())
                    {
                        var image = Image.FromStream(responseStream);

                        e.Callback(image);
                        e.Handled = true;
                    }
                }
            }
            catch
            {
                Debug.WriteLine("Error downloading image.");
            }
        }

        private void ToastForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            lock (OpenNotificationsLock)
            {
                OpenNotifications.Remove(this);
            }
        }

        private void ToastTimer_Elapsed(object sender, EventArgs e)
        {
            if (IsDisposed)
            {
                return;
            }

            lock (_toastDetachedLock)
            {
                if (_toastDetached)
                {
                    return;
                }
            }

            try
            {
                _toastTimer.Stop();

                lock (OpenNotificationsLock)
                {
                    this.InvokeIfRequired(form =>
                    {
                        form.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();
                _toastTimer = null;

                lock (OpenNotificationsLock)
                {
                    OpenNotifications.Remove(this);
                }

                // remove top-most
                SetWindowPos(Handle, -2, 0, 0, 0, 0, 0x0001 | 0x0002);
            }
        }

        private void labelTitle_MouseUp(object sender, MouseEventArgs e)
        {
            _mouseDown = false;
        }


        #endregion

        #region Private Methods

        #endregion
    }
}

Generated by GNU Enscript 1.6.5.90.