diff --git a/Source/Installer/Product.wxs b/Source/Installer/Product.wxs index 547fc966..a7ccc8bb 100644 --- a/Source/Installer/Product.wxs +++ b/Source/Installer/Product.wxs @@ -1,6 +1,6 @@  - - /// Gets the current point of cursor. + /// Gets the current location of cursor. /// - /// The point of cursor - public static Point GetCursorPoint() + /// Location of cursor + public static Point GetCursorLocation() { - return TryGetCursorPoint(out POINT point) - ? point + return TryGetCursorLocation(out POINT location) + ? location : default(Point); // (0, 0) } - internal static bool TryGetCursorPoint(out POINT point) + internal static bool TryGetCursorLocation(out POINT location) { - return GetCursorPos(out point); + return GetCursorPos(out location); } } \ No newline at end of file diff --git a/Source/ScreenFrame/Helper/Throttle.cs b/Source/ScreenFrame/Helper/Throttle.cs index 00b980d4..2e595967 100644 --- a/Source/ScreenFrame/Helper/Throttle.cs +++ b/Source/ScreenFrame/Helper/Throttle.cs @@ -8,14 +8,21 @@ namespace ScreenFrame.Helper; /// internal class Throttle { - private static readonly TimeSpan _dueTime = TimeSpan.FromSeconds(0.2); - private readonly Action _action; + protected readonly TimeSpan _dueTime; + protected readonly Action _action; - public Throttle(Action action) => this._action = action; + public Throttle(TimeSpan dueTime, Action action) + { + if (dueTime <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(dueTime), dueTime, "The time must be positive."); + + this._dueTime = dueTime; + this._action = action; + } - private Task _lastWaitTask; + protected Task _lastWaitTask; - public async Task PushAsync() + public virtual async Task PushAsync() { var currentWaitTask = Task.Delay(_dueTime); _lastWaitTask = currentWaitTask; @@ -29,14 +36,21 @@ public async Task PushAsync() internal class Throttle { - private static readonly TimeSpan _dueTime = TimeSpan.FromSeconds(0.2); - private readonly Action _action; + protected readonly TimeSpan _dueTime; + protected readonly Action _action; + + public Throttle(TimeSpan dueTime, Action action) + { + if (dueTime <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(dueTime), dueTime, "The time must be positive."); - public Throttle(Action action) => this._action = action; + this._dueTime = dueTime; + this._action = action; + } - private Task _lastWaitTask; + protected Task _lastWaitTask; - public async Task PushAsync(T value) + public virtual async Task PushAsync(T value) { var currentWaitTask = Task.Delay(_dueTime); _lastWaitTask = currentWaitTask; @@ -46,4 +60,24 @@ public async Task PushAsync(T value) _action?.Invoke(value); } } +} + +/// +/// Rx Sample like operator +/// +internal class Sample : Throttle +{ + public Sample(TimeSpan dueTime, Action action) : base(dueTime, action) + { } + + public override async Task PushAsync() + { + if (_lastWaitTask is not null) + return; + + _lastWaitTask = Task.Delay(_dueTime); + await _lastWaitTask; + _action?.Invoke(); + _lastWaitTask = null; + } } \ No newline at end of file diff --git a/Source/ScreenFrame/Movers/FloatWindowMover.cs b/Source/ScreenFrame/Movers/FloatWindowMover.cs index d6201348..68a55aa3 100644 --- a/Source/ScreenFrame/Movers/FloatWindowMover.cs +++ b/Source/ScreenFrame/Movers/FloatWindowMover.cs @@ -51,7 +51,7 @@ protected override bool TryGetAdjacentLocation(double windowWidth, double window /// True if successfully gets protected bool TryGetAdjacentLocationToPivot(double windowWidth, double windowHeight, out Rect location) { - if (!WindowHelper.TryGetTaskbar(out _, out TaskbarAlignment taskbarAlignment, out _)) + if (!WindowHelper.TryGetTaskbar(out _, out TaskbarAlignment taskbarAlignment)) { location = default; return false; diff --git a/Source/ScreenFrame/Movers/StickWindowMover.cs b/Source/ScreenFrame/Movers/StickWindowMover.cs index 2d7efeaa..6922a435 100644 --- a/Source/ScreenFrame/Movers/StickWindowMover.cs +++ b/Source/ScreenFrame/Movers/StickWindowMover.cs @@ -90,13 +90,6 @@ protected bool TryGetAdjacentLocationToTaskbar(double windowWidth, double window if (isShown) { - if (OsVersion.Is11Build22621OrGreater && - (WindowHelper.TryGetStartButtonRect(out Rect buttonRect) || - WindowHelper.TryGetSystemPrimaryTaskbar(out buttonRect, out _))) - { - taskbarRect = new Rect(taskbarRect.Left, buttonRect.Top, taskbarRect.Width, buttonRect.Height); - } - if (NotifyIconHelper.TryGetNotifyIconRect(_notifyIcon, out iconRect)) { if (taskbarRect.Contains( diff --git a/Source/ScreenFrame/NotifyIconContainer.cs b/Source/ScreenFrame/NotifyIconContainer.cs index a671b76d..25787ae1 100644 --- a/Source/ScreenFrame/NotifyIconContainer.cs +++ b/Source/ScreenFrame/NotifyIconContainer.cs @@ -3,6 +3,8 @@ using System.Windows; using System.Windows.Forms; +using ScreenFrame.Helper; + namespace ScreenFrame; /// @@ -247,8 +249,8 @@ private void OnMouseClick(object sender, MouseEventArgs e) if (e.Button == MouseButtons.Right) { - if (NotifyIconHelper.TryGetNotifyIconClickedPoint(NotifyIcon, out Point point)) - MouseRightButtonClick?.Invoke(this, point); + if (NotifyIconHelper.TryGetNotifyIconCursorLocation(NotifyIcon, out Point location, isSubstitutable: true)) + MouseRightButtonClick?.Invoke(this, location); } else { @@ -267,6 +269,103 @@ private void OnMouseDoubleClick(object sender, MouseEventArgs e) #endregion + #region Hover + + private readonly object _lock = new(); + + /// + /// Occurs when mouse pointer entered the rectangle of NotifyIcon. + /// + public event EventHandler MouseHover + { + add + { + lock (_lock) + { + RegisterMouseMove(); + _mouseHover += value; + } + } + remove + { + lock (_lock) + { + _mouseHover -= value; + UnregisterMouseMove(); + } + } + } + private event EventHandler _mouseHover; + + /// + /// Occurs when mouse pointer left the rectangle of NotifyIcon. + /// + public event EventHandler MouseUnhover + { + add + { + lock (_lock) + { + RegisterMouseMove(); + _mouseUnhover += value; + } + } + remove + { + lock (_lock) + { + _mouseUnhover -= value; + UnregisterMouseMove(); + } + } + } + private event EventHandler _mouseUnhover; + + private void RegisterMouseMove() + { + if ((_mouseHover is null) && + (_mouseUnhover is null)) + { + NotifyIcon.MouseMove += OnMouseMove; + } + } + + private void UnregisterMouseMove() + { + if ((_mouseHover is null) && + (_mouseUnhover is null)) + { + NotifyIcon.MouseMove -= OnMouseMove; + } + } + + private Sample _reactMouseHover; + private bool _isHover; + + private async void OnMouseMove(object sender, MouseEventArgs e) + { + _reactMouseHover ??= new Sample( + TimeSpan.FromSeconds(0.1), + () => + { + if (_isHover != NotifyIconHelper.TryGetNotifyIconCursorLocation(NotifyIcon, out _, isSubstitutable: false)) + { + _isHover = !_isHover; + if (_isHover) + { + _mouseHover?.Invoke(this, EventArgs.Empty); + } + else + { + _mouseUnhover?.Invoke(this, EventArgs.Empty); + } + } + }); + await _reactMouseHover.PushAsync(); + } + + #endregion + #region IDisposable private bool _isDisposed = false; diff --git a/Source/ScreenFrame/NotifyIconHelper.cs b/Source/ScreenFrame/NotifyIconHelper.cs index a0643d6f..c4a82aa1 100644 --- a/Source/ScreenFrame/NotifyIconHelper.cs +++ b/Source/ScreenFrame/NotifyIconHelper.cs @@ -42,26 +42,30 @@ public static bool SetNotifyIconWindowForeground(NotifyIcon notifyIcon) } /// - /// Attempts to get the point where a specified NotifyIcon is clicked. + /// Attempts to get the location of cursor when cursor is over a specified NotifyIcon. /// /// NotifyIcon - /// Clicked point + /// Location of cursor + /// Whether to substitute the NotifyIcon for cursor when cursor is not over the NotifyIcon /// True if successfully gets /// MouseEventArgs.Location property of MouseClick event does not contain data. - public static bool TryGetNotifyIconClickedPoint(NotifyIcon notifyIcon, out Point point) + public static bool TryGetNotifyIconCursorLocation(NotifyIcon notifyIcon, out Point location, bool isSubstitutable) { if (TryGetNotifyIconRect(notifyIcon, out Rect iconRect)) { - if (CursorHelper.TryGetCursorPoint(out POINT source)) + if (CursorHelper.TryGetCursorLocation(out POINT source)) { - point = source; - if (iconRect.Contains(point)) + location = source; + if (iconRect.Contains(location)) return true; } - point = iconRect.Location; // Fallback - return true; + if (isSubstitutable) + { + location = iconRect.Location; + return true; + } } - point = default; + location = default; return false; } diff --git a/Source/ScreenFrame/Painter/WindowPainter.cs b/Source/ScreenFrame/Painter/WindowPainter.cs index f7426301..9ab5dafb 100644 --- a/Source/ScreenFrame/Painter/WindowPainter.cs +++ b/Source/ScreenFrame/Painter/WindowPainter.cs @@ -204,25 +204,29 @@ private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref b private async void OnThemeChanged() { - _applyChangedTheme ??= new Throttle(() => - { - if (ApplyChangedTheme()) + _applyChangedTheme ??= new Throttle( + TimeSpan.FromSeconds(0.2), + () => { - ThemeChanged?.Invoke(null, EventArgs.Empty); - } - }); + if (ApplyChangedTheme()) + { + ThemeChanged?.Invoke(null, EventArgs.Empty); + } + }); await _applyChangedTheme.PushAsync(); } private async void OnAccentColorChanged(Color color) { - _applyChangedAccentColor ??= new Throttle(c => - { - if (ApplyChangedAccentColor(c)) + _applyChangedAccentColor ??= new Throttle( + TimeSpan.FromSeconds(0.2), + c => { - AccentColorChanged?.Invoke(null, EventArgs.Empty); - } - }); + if (ApplyChangedAccentColor(c)) + { + AccentColorChanged?.Invoke(null, EventArgs.Empty); + } + }); await _applyChangedAccentColor.PushAsync(color); } diff --git a/Source/ScreenFrame/Properties/AssemblyInfo.cs b/Source/ScreenFrame/Properties/AssemblyInfo.cs index c8070639..772a4f7c 100644 --- a/Source/ScreenFrame/Properties/AssemblyInfo.cs +++ b/Source/ScreenFrame/Properties/AssemblyInfo.cs @@ -33,6 +33,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("4.6.12.0")] -[assembly: AssemblyFileVersion("4.6.12.0")] +[assembly: AssemblyVersion("4.6.15.0")] +[assembly: AssemblyFileVersion("4.6.15.0")] [assembly: NeutralResourcesLanguage("en-US")] diff --git a/Source/ScreenFrame/WindowHelper.cs b/Source/ScreenFrame/WindowHelper.cs index 686908d8..60e94798 100644 --- a/Source/ScreenFrame/WindowHelper.cs +++ b/Source/ScreenFrame/WindowHelper.cs @@ -594,9 +594,8 @@ internal static bool IsTaskbarAutoHide() /// /// Primary taskbar rectange /// Primary taskbar alignment - /// Whether primary taskbar is shown or hidden /// True if successfully gets - internal static bool TryGetTaskbar(out Rect taskbarRect, out TaskbarAlignment taskbarAlignment, out bool isShown) + internal static bool TryGetTaskbar(out Rect taskbarRect, out TaskbarAlignment taskbarAlignment) { var data = new APPBARDATA { cbSize = (uint)Marshal.SizeOf() }; @@ -607,20 +606,10 @@ internal static bool TryGetTaskbar(out Rect taskbarRect, out TaskbarAlignment ta { taskbarRect = data.rc; taskbarAlignment = ConvertToTaskbarAlignment(data.uEdge); - - if (TryGetWindow(PrimaryTaskbarWindowClassName, out _, out Rect rect)) - { - // SHAppBarMessage function returns primary taskbar rectangle as if the taskbar - // is fully shown even when it is actually hidden. In contrast, GetWindowRect - // function returns actual, current primary taskbar rectangle. Thus, if those - // rectangles do not match, the taskbar is hidden in full or part. - isShown = (taskbarRect == rect); - return true; - } + return true; } taskbarRect = Rect.Empty; taskbarAlignment = default; - isShown = default; return false; static TaskbarAlignment ConvertToTaskbarAlignment(ABE value) @@ -636,6 +625,52 @@ static TaskbarAlignment ConvertToTaskbarAlignment(ABE value) } } + /// + /// Attempts to get the information on primary taskbar. + /// + /// Primary taskbar rectange + /// Primary taskbar alignment + /// Whether primary taskbar is shown or hidden + /// True if successfully gets + internal static bool TryGetTaskbar(out Rect taskbarRect, out TaskbarAlignment taskbarAlignment, out bool isShown) + { + if (TryGetTaskbar(out taskbarRect, out taskbarAlignment) + && TryGetWindow(PrimaryTaskbarWindowClassName, out _, out Rect rect) + && TryGetMonitorRect(taskbarRect, out Rect monitorRect, out Rect workRect)) + { + // SHAppBarMessage function returns primary taskbar rectangle as if the taskbar + // is fully shown even when it is actually hidden. In contrast, GetWindowRect + // function returns actual, current primary taskbar rectangle. Thus, if those + // rectangles match, the taskbar is fully shown. + isShown = (taskbarRect == rect) + // As of Windows 11 10.0.22621.xxx, current primary taskbar rectangle obtained + // specifying the traditional window of primary taskbar (Shell_TrayWnd) + // no longer indicates actual height of primary taskbar. Even so, if current + // primary taskbar rectangle is contained in monitor rectangle to which primary + // taskbar rectangle belongs, the taskbar is fully shown. + || monitorRect.Contains(rect); + + if (isShown) + { + // As a result of the change explained above, primary taskbar rectangle may not + // match the rectangle calculated by monitor rectangle and working area rectangle. + // In such case, the calculated rectangle seems reliable. + var height = (monitorRect.Height - workRect.Height); + if ((height > 0) && (taskbarRect.Height != height)) + { + taskbarRect = new Rect( + taskbarRect.Left, + ((monitorRect.Top != workRect.Top) ? monitorRect.Top : workRect.Bottom), + taskbarRect.Width, + height); + } + } + return true; + } + isShown = default; + return false; + } + // Primary taskbar does not necessarily locate in primary monitor. private const string PrimaryTaskbarWindowClassName = "Shell_TrayWnd"; private const string SecondaryTaskbarWindowClassName = "Shell_SecondaryTrayWnd"; @@ -808,32 +843,6 @@ private static TaskbarAlignment GetTaskbarAlignment(Rect monitorRect, Rect taskb }; } - /// - /// Attempts to get the rectangle of Start button or ToolBar. - /// - /// Rectangle of Start button or ToolBar - /// True if successfully gets - internal static bool TryGetStartButtonRect(out Rect buttonRect) - { - // As of Windows 11 10.0.22621.xxx, the traditional window of primary taskbar - // (Shell_TrayWnd) no longer indicates the actual height of primary taskbar. Instead, - // other windows (Start, ReBarWindow32) can be used to get the actual height. - if (TryGetChildWindow(PrimaryTaskbarWindowClassName, "Start", out _, out Rect windowRect) - && IsValid(windowRect)) - { - buttonRect = windowRect; - return true; - } - if (TryGetChildWindow(PrimaryTaskbarWindowClassName, "ReBarWindow32", out _, out windowRect) - && IsValid(windowRect)) - { - buttonRect = windowRect; - return true; - } - buttonRect = default; - return false; - } - /// /// Attempts to get the rectangle of overflow area. ///