using System; using System.Drawing; using System.Windows.Forms; using CefSharp; using TweetDuck.Browser.Handling; using TweetDuck.Controls; using TweetDuck.Utils; using TweetLib.Core.Features.Notifications; namespace TweetDuck.Browser.Notification { abstract partial class FormNotificationMain : FormNotificationBase, CustomKeyboardHandler.IBrowserKeyHandler { protected sealed class NotificationInterfaceImpl : INotificationInterface { public bool FreezeTimer { get => notification.FreezeTimer; set => notification.FreezeTimer = value; } public bool IsHovered => notification.IsCursorOverBrowser; private readonly FormNotificationBase notification; public NotificationInterfaceImpl(FormNotificationBase notification) { this.notification = notification; } public void DisplayTooltip(string text) { notification.InvokeAsyncSafe(() => notification.DisplayTooltip(text)); } public void FinishCurrentNotification() { notification.InvokeAsyncSafe(notification.FinishCurrentNotification); } public void ShowTweetDetail() { notification.InvokeAsyncSafe(notification.ShowTweetDetail); } } private static int FontSizeLevel { get => NotificationBrowser.FontSize switch { "largest" => 4, "large" => 3, "small" => 1, "smallest" => 0, _ => 2 }; } private readonly int timerBarHeight; protected int timeLeft, totalTime; protected bool pausedDuringNotification; private readonly NativeMethods.HookProc mouseHookDelegate; private IntPtr mouseHook; private bool blockXButtonUp; private int currentOpacity; private bool? prevDisplayTimer; private int? prevFontSize; public virtual bool RequiresResize { get { return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Config.DisplayNotificationTimer || prevFontSize != FontSizeLevel; } set { if (value) { prevDisplayTimer = null; prevFontSize = null; } else { prevDisplayTimer = Config.DisplayNotificationTimer; prevFontSize = FontSizeLevel; } } } private int BaseClientWidth { get => Config.NotificationSize switch { DesktopNotification.Size.Custom => Config.CustomNotificationSize.Width, _ => BrowserUtils.Scale(284, SizeScale * (1.0 + 0.05 * FontSizeLevel)) }; } private int BaseClientHeight { get => Config.NotificationSize switch { DesktopNotification.Size.Custom => Config.CustomNotificationSize.Height, _ => BrowserUtils.Scale(122, SizeScale * (1.0 + 0.08 * FontSizeLevel)) }; } public Size BrowserSize => Config.DisplayNotificationTimer ? new Size(ClientSize.Width, ClientSize.Height - timerBarHeight) : ClientSize; protected FormNotificationMain(FormBrowser owner, CreateBrowserImplFunc createBrowserImpl) : base(owner, createBrowserImpl) { InitializeComponent(); this.timerBarHeight = BrowserUtils.Scale(4, DpiScale); browser.KeyboardHandler = new CustomKeyboardHandler(this); browser.LoadingStateChanged += Browser_LoadingStateChanged; mouseHookDelegate = MouseHookProc; Disposed += (sender, args) => StopMouseHook(true); } private void SetOpacity(int opacity) { if (currentOpacity != opacity) { currentOpacity = opacity; Opacity = opacity / 100.0; } } // mouse wheel hook private void StartMouseHook() { if (mouseHook == IntPtr.Zero) { mouseHook = NativeMethods.SetWindowsHookEx(NativeMethods.WM_MOUSE_LL, mouseHookDelegate, IntPtr.Zero, 0); } } private void StopMouseHook(bool force) { if (mouseHook != IntPtr.Zero && (force || !blockXButtonUp)) { NativeMethods.UnhookWindowsHookEx(mouseHook); mouseHook = IntPtr.Zero; blockXButtonUp = false; } } private IntPtr MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode == 0) { int eventType = wParam.ToInt32(); if (eventType == NativeMethods.WM_MOUSEWHEEL && IsCursorOverBrowser) { int delta = BrowserUtils.Scale(NativeMethods.GetMouseHookData(lParam), Config.NotificationScrollSpeed * 0.01); if (Config.EnableSmoothScrolling) { browser.BrowserCore.ExecuteScriptAsync("window.TDGF_scrollSmoothly", (int) Math.Round(-delta / 0.6)); } else { browser.SendMouseWheelEvent(0, 0, 0, delta, CefEventFlags.None); } return NativeMethods.HOOK_HANDLED; } else if (eventType == NativeMethods.WM_XBUTTONDOWN && DesktopBounds.Contains(Cursor.Position)) { int extraButton = NativeMethods.GetMouseHookData(lParam); if (extraButton == 2) { // forward button this.InvokeAsyncSafe(FinishCurrentNotification); } else if (extraButton == 1) { // back button this.InvokeAsyncSafe(Close); } blockXButtonUp = true; return NativeMethods.HOOK_HANDLED; } else if (eventType == NativeMethods.WM_XBUTTONUP && blockXButtonUp) { blockXButtonUp = false; if (!Visible) { StopMouseHook(false); } return NativeMethods.HOOK_HANDLED; } } return NativeMethods.CallNextHookEx(mouseHook, nCode, wParam, lParam); } // event handlers private void FormNotification_FormClosing(object sender, FormClosingEventArgs e) { if (e.CloseReason == CloseReason.UserClosing) { HideNotification(); e.Cancel = true; } } private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) { if (!e.IsLoading && browser.Address != NotificationBrowser.BlankURL) { this.InvokeSafe(() => { Visible = true; // ensures repaint before moving the window to a visible location timerDisplayDelay.Start(); }); } } private void timerDisplayDelay_Tick(object sender, EventArgs e) { OnNotificationReady(); timerDisplayDelay.Stop(); } private void timerHideProgress_Tick(object sender, EventArgs e) { bool isCursorInside = Bounds.Contains(Cursor.Position); if (isCursorInside) { StartMouseHook(); SetOpacity(100); } else { StopMouseHook(false); SetOpacity(Config.NotificationWindowOpacity); } if (isCursorInside || FreezeTimer || ContextMenuOpen) { return; } timeLeft -= timerProgress.Interval; int value = BrowserUtils.Scale(progressBarTimer.Maximum + 25, (totalTime - timeLeft) / (double) totalTime); progressBarTimer.SetValueInstant(Config.NotificationTimerCountDown ? progressBarTimer.Maximum - value : value); if (timeLeft <= 0) { FinishCurrentNotification(); } } // notification methods public virtual void ShowNotification(DesktopNotification notification) { LoadTweet(notification); } public override void HideNotification() { base.HideNotification(); progressBarTimer.Value = Config.NotificationTimerCountDown ? progressBarTimer.Maximum : progressBarTimer.Minimum; timerProgress.Stop(); totalTime = 0; StopMouseHook(false); } public override void FinishCurrentNotification() { timerProgress.Stop(); } public override void PauseNotification() { if (!IsPaused) { pausedDuringNotification = IsNotificationVisible; timerProgress.Stop(); StopMouseHook(true); } base.PauseNotification(); } public override void ResumeNotification() { bool wasPaused = IsPaused; base.ResumeNotification(); if (wasPaused && !IsPaused && pausedDuringNotification) { OnNotificationReady(); } } protected override void LoadTweet(DesktopNotification tweet) { timerProgress.Stop(); totalTime = timeLeft = tweet.GetDisplayDuration(Config.NotificationDurationValue); progressBarTimer.Value = Config.NotificationTimerCountDown ? progressBarTimer.Maximum : progressBarTimer.Minimum; base.LoadTweet(tweet); } protected override void SetNotificationSize(int width, int height) { if (Config.DisplayNotificationTimer) { ClientSize = new Size(width, height + timerBarHeight); progressBarTimer.Visible = true; } else { ClientSize = new Size(width, height); progressBarTimer.Visible = false; } browser.ClientSize = new Size(width, height); } protected void PrepareAndDisplayWindow() { if (RequiresResize) { RequiresResize = false; SetNotificationSize(BaseClientWidth, BaseClientHeight); } SetOpacity(IsCursorOverBrowser ? 100 : Config.NotificationWindowOpacity); MoveToVisibleLocation(); } protected virtual void OnNotificationReady() { PrepareAndDisplayWindow(); timerProgress.Start(); } bool CustomKeyboardHandler.IBrowserKeyHandler.HandleBrowserKey(Keys key) { switch (key) { case Keys.Enter: this.InvokeAsyncSafe(FinishCurrentNotification); return true; case Keys.Escape: this.InvokeAsyncSafe(HideNotification); return true; case Keys.Space: this.InvokeAsyncSafe(() => FreezeTimer = !FreezeTimer); return true; default: return false; } } } }