diff --git a/README.md b/README.md index 60141a8..33e4db5 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 45e40d9..ebd9151 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 5333ad6..b45830d 100644 Binary files a/src/chrome++.ini and b/src/chrome++.ini differ diff --git a/src/config.cc b/src/config.cc index 8740602..da1a2b3 100644 --- a/src/config.cc +++ b/src/config.cc @@ -50,6 +50,10 @@ void Config::LoadConfig() { wheel_tab_when_press_rbutton_ = ::GetPrivateProfileIntW(L"tabs", L"wheel_tab_when_press_rbutton", 1, GetIniPath().c_str()) != 0; + hover_activate_tab_ = + ::GetPrivateProfileIntW(L"tabs", L"hover_activate_tab", 0, + GetIniPath().c_str()) != 0; + hover_activate_tab_delay_ = LoadHoverActivateTabDelay(); open_url_new_tab_ = LoadOpenUrlNewTabMode(); bookmark_new_tab_ = LoadBookmarkNewTabMode(); new_tab_disable_ = ::GetPrivateProfileIntW(L"tabs", L"new_tab_disable", 1, @@ -125,4 +129,17 @@ int Config::LoadBookmarkNewTabMode() { GetIniPath().c_str()); } +unsigned int Config::LoadHoverActivateTabDelay() { + constexpr int kDefaultDelayMs = 200; + constexpr int kMaxDelayMs = 5000; + const int delay = ::GetPrivateProfileIntW(L"tabs", + L"hover_activate_tab_delay", + kDefaultDelayMs, + GetIniPath().c_str()); + if (delay < 0 || delay > kMaxDelayMs) { + return kDefaultDelayMs; + } + return static_cast(delay); +} + const Config& config = Config::Instance(); diff --git a/src/config.h b/src/config.h index 3c61c93..2919ae1 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 4ba550b..e70bfcc 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 1af3763..3855533 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 3de491e..db7d46b 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 c417edf..3d7dc60 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);