From e3b3db58cc2b6edbf94afc5dfbef26f6c4702c87 Mon Sep 17 00:00:00 2001 From: Ryan Lieu Date: Tue, 7 Jul 2026 11:35:33 +0800 Subject: [PATCH] feat: activate tabs on hover --- README.md | 1 + README.zh-CN.md | 1 + src/chrome++.ini | Bin 15274 -> 15958 bytes src/config.cc | 17 +++++++ src/config.h | 7 +++ src/inputhook.cc | 2 +- src/tabbookmark.cc | 119 +++++++++++++++++++++++++++++++++++++++++++++ src/uia.cc | 37 +++++++++++++- src/uia.h | 2 + 9 files changed, 184 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60141a8e..33e4db5a 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Chrome++ Next is a `version.dll` injection project for Google Chrome. It is load - Keep the last tab from closing the browser window. - Switch tabs with the mouse wheel over the tab strip. - Switch tabs with the mouse wheel while holding the right mouse button. +- Activate a tab after hovering over it. - Open omnibox input or bookmarks in a new tab. - Control new-tab detection through `new_tab_disable` and `new_tab_disable_name`. diff --git a/README.zh-CN.md b/README.zh-CN.md index 45e40d90..ebd91511 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -32,6 +32,7 @@ Chrome++ Next 是一个 `version.dll` 注入项目,会与 `chrome.exe` 一同 - 保留最后一个标签页,避免关闭整个浏览器窗口。 - 鼠标悬停标签栏时滚轮切换标签页。 - 按住右键时滚轮切换标签页。 +- 鼠标悬停标签页后自动激活该标签页。 - 将地址栏输入内容或书签在新标签页中打开。 - 通过 `new_tab_disable` 和 `new_tab_disable_name` 控制新标签页判定。 diff --git a/src/chrome++.ini b/src/chrome++.ini index 5333ad635aabb1ebbeb81ec01f5bd69e2546f7b4..b45830db54537f386d16d4effda82c75ef039e58 100644 GIT binary patch delta 617 zcmZ2geywK16HVz1hJ1!HhE#?khIocVhGd2khD;zo5lE*_K4>U8`GP8sx+AJMg91=q z0jMI8AqmV*1F2>xnryEjUXL)Z7$^rbTLEZl2?H+!7lSoW6rl#gPM|9w=EgIm0L{z+ z+EEGA0fV{>x;Tx1=2Y{ zn8{EK)R_!4ArELh$O9S-20$xKfg})azNzWXPE3f^E6iulSd$PFz@AZ8T{?p=Ad?~Hwv>H~yWh_v`9Cz+*Z+0)rAm(fNbYCg{6G1QrpM+4 f!!+h(Vj{s7=xdNuiBXFjVYUoL;P5(V@{$bzV}6Wm delta 26 icmcasv#NZ<6V1&Y+BWQyJ+vJ*FELJI-dtq kMaxDelayMs) { + return kDefaultDelayMs; + } + return static_cast(delay); +} + const Config& config = Config::Instance(); diff --git a/src/config.h b/src/config.h index 3c61c93f..2919ae1f 100644 --- a/src/config.h +++ b/src/config.h @@ -37,6 +37,10 @@ class Config { bool IsWheelTabWhenPressRightButton() const { return wheel_tab_when_press_rbutton_; } + bool IsHoverActivateTab() const { return hover_activate_tab_; } + unsigned int GetHoverActivateTabDelay() const { + return hover_activate_tab_delay_; + } int GetOpenUrlNewTabMode() const { return open_url_new_tab_; } int GetBookmarkNewTabMode() const { return bookmark_new_tab_; } bool IsNewTabDisable() const { return new_tab_disable_; } @@ -61,6 +65,7 @@ class Config { std::optional LoadDirPath(const std::wstring& dir_type); int LoadOpenUrlNewTabMode(); int LoadBookmarkNewTabMode(); + unsigned int LoadHoverActivateTabDelay(); private: // general @@ -82,6 +87,8 @@ class Config { bool right_click_close_; bool wheel_tab_; bool wheel_tab_when_press_rbutton_; + bool hover_activate_tab_; + unsigned int hover_activate_tab_delay_; int open_url_new_tab_; int bookmark_new_tab_; bool new_tab_disable_; diff --git a/src/inputhook.cc b/src/inputhook.cc index 4ba550b2..e70bfcc0 100644 --- a/src/inputhook.cc +++ b/src/inputhook.cc @@ -37,7 +37,7 @@ LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) { return CallNextHookEx(mouse_hook, nCode, wParam, lParam); } - if (wParam == WM_MOUSEMOVE || wParam == WM_NCMOUSEMOVE) { + if (wParam == WM_NCMOUSEMOVE) { return CallNextHookEx(mouse_hook, nCode, wParam, lParam); } diff --git a/src/tabbookmark.cc b/src/tabbookmark.cc index 1af37636..38555332 100644 --- a/src/tabbookmark.cc +++ b/src/tabbookmark.cc @@ -10,8 +10,16 @@ namespace { constexpr UINT kDefaultDpi = 96; +constexpr UINT_PTR kHoverActivateTabTimerId = 0x1603ABDA; POINT lbutton_down_point = {-1, -1}; +struct HoverActivateTabState { + HWND hwnd = nullptr; + RECT tab_rect = {}; +}; + +HoverActivateTabState hover_activate_tab_state; + enum class KeepTabTrigger { kRightClick = 0, kMiddleClick, @@ -42,6 +50,111 @@ bool IsNeedKeep(int tab_count, KeepTabTrigger trigger) { return keep_tab; } +bool IsAnyMouseButtonPressed() { + return IsKeyPressed(VK_LBUTTON) || IsKeyPressed(VK_RBUTTON) || + IsKeyPressed(VK_MBUTTON); +} + +HWND GetChromeRootAtPoint(POINT pt) { + const HWND hwnd = WindowFromPoint(pt); + const HWND root = hwnd ? GetAncestor(hwnd, GA_ROOT) : nullptr; + if (!root || !IsChromeWindow(root)) { + return nullptr; + } + return root; +} + +bool IsSameHoverTarget(HWND hwnd, const RECT& tab_rect) { + return hover_activate_tab_state.hwnd == hwnd && + EqualRect(&hover_activate_tab_state.tab_rect, &tab_rect); +} + +void CancelHoverActivateTabTimer() { + if (hover_activate_tab_state.hwnd) { + KillTimer(hover_activate_tab_state.hwnd, kHoverActivateTabTimerId); + } + hover_activate_tab_state = {}; +} + +void ActivateHoveredTabIfStable(HWND hwnd) { + if (!hover_activate_tab_state.hwnd || + hover_activate_tab_state.hwnd != hwnd) { + CancelHoverActivateTabTimer(); + return; + } + + POINT pt = {}; + if (IsAnyMouseButtonPressed() || !GetCursorPos(&pt) || + GetChromeRootAtPoint(pt) != hwnd || + !PtInRect(&hover_activate_tab_state.tab_rect, pt)) { + CancelHoverActivateTabTimer(); + return; + } + + const auto hit = FindTabHitResult(pt, false, true); + if (!hit || hit->on_close_button || + !EqualRect(&hit->tab_rect, &hover_activate_tab_state.tab_rect)) { + CancelHoverActivateTabTimer(); + return; + } + + SelectTab(*hit); + CancelHoverActivateTabTimer(); +} + +void CALLBACK HoverActivateTabTimerProc(HWND hwnd, + UINT, + UINT_PTR event_id, + DWORD) { + if (event_id != kHoverActivateTabTimerId) { + return; + } + ActivateHoveredTabIfStable(hwnd); +} + +void ArmHoverActivateTabTimer(HWND hwnd, const RECT& tab_rect) { + if (IsSameHoverTarget(hwnd, tab_rect)) { + return; + } + + CancelHoverActivateTabTimer(); + hover_activate_tab_state.hwnd = hwnd; + hover_activate_tab_state.tab_rect = tab_rect; + + const UINT delay = config.GetHoverActivateTabDelay(); + if (delay == 0) { + ActivateHoveredTabIfStable(hwnd); + return; + } + + if (!SetTimer(hwnd, kHoverActivateTabTimerId, delay, + HoverActivateTabTimerProc)) { + hover_activate_tab_state = {}; + } +} + +void HandleHoverActivateTab(const MOUSEHOOKSTRUCT* pmouse) { + if (!config.IsHoverActivateTab() || IsAnyMouseButtonPressed()) { + CancelHoverActivateTabTimer(); + return; + } + + const POINT pt = pmouse->pt; + const HWND root = GetChromeRootAtPoint(pt); + if (!root) { + CancelHoverActivateTabTimer(); + return; + } + + const auto hit = FindTabHitResult(pt, false, true); + if (!hit || hit->on_close_button) { + CancelHoverActivateTabTimer(); + return; + } + + ArmHoverActivateTabTimer(root, hit->tab_rect); +} + // Use the mouse wheel to switch tabs bool HandleMouseWheel(LPARAM lParam, const MOUSEHOOKSTRUCT* pmouse) { if (!config.IsWheelTab() && !config.IsWheelTabWhenPressRightButton()) { @@ -240,15 +353,21 @@ bool TabBookmarkMouseHandler(WPARAM wParam, LPARAM lParam) { static bool closing_tab_by_right = false; switch (wParam) { + case WM_MOUSEMOVE: + HandleHoverActivateTab(pmouse); + return false; case WM_LBUTTONDOWN: + CancelHoverActivateTabTimer(); // Simply record the position of `LBUTTONDOWN` for drag detection closing_tab_by_dblclk = false; lbutton_down_point = pmouse->pt; return false; case WM_MBUTTONDOWN: + CancelHoverActivateTabTimer(); closing_tab_by_middle = false; return false; case WM_RBUTTONDOWN: + CancelHoverActivateTabTimer(); closing_tab_by_right = false; return false; case WM_LBUTTONUP: diff --git a/src/uia.cc b/src/uia.cc index 3de491e8..db7d46b5 100644 --- a/src/uia.cc +++ b/src/uia.cc @@ -616,6 +616,7 @@ ComPtr FindTabElements( ComPtr FindTabElementAtPoint( const ComPtr& tab_elements, + RECT* tab_rect, POINT pt) { if (!tab_elements) { return nullptr; @@ -639,6 +640,9 @@ ComPtr FindTabElementAtPoint( continue; } if (PtInRect(&rect, pt)) { + if (tab_rect) { + *tab_rect = rect; + } return tab_element; } } @@ -710,7 +714,8 @@ std::optional BuildTabHitResult(const UiaSession& session, return std::nullopt; } - const auto tab_element = FindTabElementAtPoint(tab_elements, pt); + RECT tab_rect = {}; + const auto tab_element = FindTabElementAtPoint(tab_elements, &tab_rect, pt); if (!tab_element) { return std::nullopt; } @@ -728,6 +733,7 @@ std::optional BuildTabHitResult(const UiaSession& session, TabHitResult hit_result; hit_result.tab = tab_element; + hit_result.tab_rect = tab_rect; hit_result.tab_count = need_count ? tab_count : 0; hit_result.on_close_button = need_close_button && IsOnTabCloseButton(session, tab_element, pt); @@ -867,6 +873,35 @@ std::optional FindTabHitResult(POINT pt, need_close_button); } +bool SelectTab(const TabHitResult& hit_result) { + if (!hit_result.tab) { + return false; + } + + ComPtr pattern; + HRESULT hr = hit_result.tab->GetCurrentPattern( + UIA_SelectionItemPatternId, pattern.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !pattern) { + DebugLog(L"UIA: tab selection item pattern unavailable"); + return false; + } + + ComPtr selection_item; + hr = pattern->QueryInterface( + IID_PPV_ARGS(selection_item.ReleaseAndGetAddressOf())); + if (FAILED(hr) || !selection_item) { + DebugLog(L"UIA: tab selection item QueryInterface failed"); + return false; + } + + hr = selection_item->Select(); + if (FAILED(hr)) { + DebugLog(L"UIA: tab Select failed"); + return false; + } + return true; +} + std::optional FindTabCount(HWND hwnd) { const UiaSession* session = GetUiaSession(); if (!session) { diff --git a/src/uia.h b/src/uia.h index c417edf7..3d7dc605 100644 --- a/src/uia.h +++ b/src/uia.h @@ -10,12 +10,14 @@ struct TabHitResult { Microsoft::WRL::ComPtr tab; + RECT tab_rect = {}; int tab_count = 0; bool on_close_button = false; }; [[nodiscard]] std::optional FindTabHitResult(POINT pt, bool need_count, bool need_close_button); +[[nodiscard]] bool SelectTab(const TabHitResult& hit_result); [[nodiscard]] std::optional FindTabCount(HWND hwnd); [[nodiscard]] bool IsOnTabBar(POINT pt); [[nodiscard]] bool IsOnBookmark(POINT pt);