From a9f07bfc4b3528976634ef8f2660dc193297cc38 Mon Sep 17 00:00:00 2001 From: emoacht Date: Sun, 30 Jun 2024 20:12:37 +0900 Subject: [PATCH 1/5] Add sample like operator --- Source/ScreenFrame/Helper/Throttle.cs | 54 +++++++++++++++++---- Source/ScreenFrame/Painter/WindowPainter.cs | 28 ++++++----- 2 files changed, 60 insertions(+), 22 deletions(-) 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/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); } From a2a668b6382b023c56e27825a969a7accc1a38df Mon Sep 17 00:00:00 2001 From: emoacht Date: Fri, 19 Jul 2024 06:25:08 +0900 Subject: [PATCH 2/5] Rename methods to get cursor location --- Source/ScreenFrame/CursorHelper.cs | 14 +++++++------- Source/ScreenFrame/NotifyIconContainer.cs | 4 ++-- Source/ScreenFrame/NotifyIconHelper.cs | 22 +++++++++++++--------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Source/ScreenFrame/CursorHelper.cs b/Source/ScreenFrame/CursorHelper.cs index 1d962379..4a1410a7 100644 --- a/Source/ScreenFrame/CursorHelper.cs +++ b/Source/ScreenFrame/CursorHelper.cs @@ -19,18 +19,18 @@ public static class CursorHelper #endregion /// - /// 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/NotifyIconContainer.cs b/Source/ScreenFrame/NotifyIconContainer.cs index a671b76d..0a5f21a8 100644 --- a/Source/ScreenFrame/NotifyIconContainer.cs +++ b/Source/ScreenFrame/NotifyIconContainer.cs @@ -247,8 +247,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 { 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; } From 2f17f7284e6f2a7ceb1cf3c727fbf1f10d3c4a57 Mon Sep 17 00:00:00 2001 From: emoacht Date: Fri, 19 Jul 2024 07:01:47 +0900 Subject: [PATCH 3/5] Add mouse hover event to NotifyIcon --- Source/ScreenFrame/NotifyIconContainer.cs | 99 +++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/Source/ScreenFrame/NotifyIconContainer.cs b/Source/ScreenFrame/NotifyIconContainer.cs index 0a5f21a8..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; /// @@ -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; From 31c6240fc3e912bec44a431b006c242d1032923d Mon Sep 17 00:00:00 2001 From: emoacht Date: Fri, 19 Jul 2024 07:06:35 +0900 Subject: [PATCH 4/5] Improve way to get taskbar rectangle --- Source/ScreenFrame/Movers/FloatWindowMover.cs | 2 +- Source/ScreenFrame/Movers/StickWindowMover.cs | 7 -- Source/ScreenFrame/WindowHelper.cs | 87 ++++++++++--------- 3 files changed, 49 insertions(+), 47 deletions(-) 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/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. /// From e8e03e9a311077d13e4d713d87747b6c6524c57e Mon Sep 17 00:00:00 2001 From: emoacht Date: Fri, 19 Jul 2024 07:13:35 +0900 Subject: [PATCH 5/5] Increment version to 4.6.15 --- Source/Installer/Product.wxs | 2 +- Source/Monitorian.Core/Properties/AssemblyInfo.cs | 4 ++-- Source/Monitorian/Properties/AssemblyInfo.cs | 4 ++-- Source/ScreenFrame/Properties/AssemblyInfo.cs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) 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 @@  -