diff --git a/app/brave_settings_strings.grdp b/app/brave_settings_strings.grdp index 7b5c57a19aa..7904572060a 100644 --- a/app/brave_settings_strings.grdp +++ b/app/brave_settings_strings.grdp @@ -180,6 +180,9 @@ Always hide tab close button + + Allow middle button click to close tabs + Auto expand on hover when minimized diff --git a/browser/extensions/api/settings_private/brave_prefs_util.cc b/browser/extensions/api/settings_private/brave_prefs_util.cc index 4f0fadb0561..24551bb0db6 100644 --- a/browser/extensions/api/settings_private/brave_prefs_util.cc +++ b/browser/extensions/api/settings_private/brave_prefs_util.cc @@ -348,6 +348,8 @@ const PrefsUtil::TypedPrefMap& BravePrefsUtil::GetAllowlistedKeys() { settings_api::PrefType::kBoolean; (*s_brave_allowlist)[brave_tabs::kAlwaysHideTabCloseButton] = settings_api::PrefType::kBoolean; + (*s_brave_allowlist)[brave_tabs::kMiddleClickCloseTabEnabled] = + settings_api::PrefType::kBoolean; #endif #if BUILDFLAG(IS_WIN) diff --git a/browser/prefs/brave_pref_service_incognito_allowlist.cc b/browser/prefs/brave_pref_service_incognito_allowlist.cc index 890267d2b26..7eab35f9c67 100644 --- a/browser/prefs/brave_pref_service_incognito_allowlist.cc +++ b/browser/prefs/brave_pref_service_incognito_allowlist.cc @@ -49,6 +49,7 @@ base::span GetBravePersistentPrefNames() { brave_tabs::kSharedPinnedTab, brave_tabs::kTreeTabsEnabled, brave_tabs::kAlwaysHideTabCloseButton, + brave_tabs::kMiddleClickCloseTabEnabled, #endif #if defined(TOOLKIT_VIEWS) sidebar::kSidePanelWidth, diff --git a/browser/resources/settings/brave_appearance_page/tabs.html b/browser/resources/settings/brave_appearance_page/tabs.html index 131904435b1..0883ca702c3 100644 --- a/browser/resources/settings/brave_appearance_page/tabs.html +++ b/browser/resources/settings/brave_appearance_page/tabs.html @@ -106,6 +106,13 @@ label="$i18n{SETTINGS_APPEARANCE_SETTINGS_TABS_ALWAYS_HIDE_TAB_CLOSE_BUTTON}"> + + + +
diff --git a/browser/ui/tabs/brave_tab_prefs.cc b/browser/ui/tabs/brave_tab_prefs.cc index 8f90164d8e9..90dfd50122a 100644 --- a/browser/ui/tabs/brave_tab_prefs.cc +++ b/browser/ui/tabs/brave_tab_prefs.cc @@ -41,6 +41,7 @@ void RegisterBraveProfilePrefs(PrefRegistrySimple* registry) { } registry->RegisterBooleanPref(kAlwaysHideTabCloseButton, false); + registry->RegisterBooleanPref(kMiddleClickCloseTabEnabled, true); } void MigrateBraveProfilePrefs(PrefService* prefs) { diff --git a/browser/ui/tabs/brave_tab_prefs.h b/browser/ui/tabs/brave_tab_prefs.h index 2f443b90a75..e99d4a58e09 100644 --- a/browser/ui/tabs/brave_tab_prefs.h +++ b/browser/ui/tabs/brave_tab_prefs.h @@ -40,6 +40,9 @@ inline constexpr char kSharedPinnedTab[] = "brave.tabs.shared_pinned_tab"; inline constexpr char kAlwaysHideTabCloseButton[] = "brave.tabs.always_hide_tab_close_button"; +inline constexpr char kMiddleClickCloseTabEnabled[] = + "brave.tabs.middle_click_close_tab_enabled"; + void RegisterBraveProfilePrefs(PrefRegistrySimple* registry); void MigrateBraveProfilePrefs(PrefService* prefs); diff --git a/browser/ui/views/tabs/brave_tab_strip.cc b/browser/ui/views/tabs/brave_tab_strip.cc index a68426b0748..16afe095220 100644 --- a/browser/ui/views/tabs/brave_tab_strip.cc +++ b/browser/ui/views/tabs/brave_tab_strip.cc @@ -53,6 +53,8 @@ BraveTabStrip::BraveTabStrip(std::unique_ptr controller) controller_->GetProfile()->GetPrefs(), base::BindRepeating(&BraveTabStrip::OnAlwaysHideCloseButtonPrefChanged, base::Unretained(this))); + middle_click_close_tab_enabled_.Init(brave_tabs::kMiddleClickCloseTabEnabled, + controller_->GetProfile()->GetPrefs()); } BraveTabStrip::~BraveTabStrip() = default; @@ -101,6 +103,10 @@ bool BraveTabStrip::CanPaintThrobberToLayer() const { return false; } +bool BraveTabStrip::CanCloseTabViaMiddleButtonClick() const { + return *middle_click_close_tab_enabled_; +} + bool BraveTabStrip::ShouldDrawStrokes() const { if (ShouldShowVerticalTabs()) { // Prevent root view from drawing lines. For vertical tabs stroke , we diff --git a/browser/ui/views/tabs/brave_tab_strip.h b/browser/ui/views/tabs/brave_tab_strip.h index eecd20e020e..355deca8db5 100644 --- a/browser/ui/views/tabs/brave_tab_strip.h +++ b/browser/ui/views/tabs/brave_tab_strip.h @@ -43,6 +43,7 @@ class BraveTabStrip : public TabStrip { bool ShouldAlwaysHideCloseButton() const override; bool IsVerticalTabsFloating() const override; bool CanPaintThrobberToLayer() const override; + bool CanCloseTabViaMiddleButtonClick() const override; private: FRIEND_TEST_ALL_PREFIXES(VerticalTabStripBrowserTest, ScrollBarVisibility); @@ -63,6 +64,7 @@ class BraveTabStrip : public TabStrip { static constexpr float kBraveMinimumContrastRatioForOutlines = 1.0816f; BooleanPrefMember always_hide_close_button_; + BooleanPrefMember middle_click_close_tab_enabled_; base::WeakPtrFactory weak_factory_{this}; }; diff --git a/browser/ui/views/tabs/brave_tab_unittest.cc b/browser/ui/views/tabs/brave_tab_unittest.cc index 0305bac3402..fcd424125a5 100644 --- a/browser/ui/views/tabs/brave_tab_unittest.cc +++ b/browser/ui/views/tabs/brave_tab_unittest.cc @@ -115,6 +115,7 @@ class BraveTabRenamingUnitTest : public BraveTabTest { SetCustomTitleForTab, (Tab * tab, const std::optional& title), (override)); + MOCK_METHOD(void, CloseTab, (Tab * tab, CloseTabSource source), (override)); }; BraveTabRenamingUnitTest() = default; @@ -287,3 +288,59 @@ TEST_F(BraveTabTest, ShouldAlwaysHideTabCloseButton) { views::test::RunScheduledLayout(&tab); EXPECT_FALSE(tab.close_button_for_test()->GetVisible()); } + +TEST_F(BraveTabTest, CanCloseTabViaMiddleButtonClick) { + testing::NiceMock + tab_slot_controller; + auto tab = std::make_unique(&tab_slot_controller); + tab_slot_controller.set_active_tab(tab.get()); + + // Create a widget to host the tab + auto widget = + CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); + widget->SetContentsView(std::move(tab)); + auto* tab_ptr = static_cast(widget->GetContentsView()); + tab_ptr->SetBoundsRect({0, 0, 100, 50}); + views::test::RunScheduledLayout(tab_ptr); + + // Default should be enabled (true) + EXPECT_TRUE(tab_slot_controller.CanCloseTabViaMiddleButtonClick()); + + // Simulate middle mouse button click - should close the tab + ui::MouseEvent press_event(ui::EventType::kMousePressed, gfx::Point(50, 25), + gfx::Point(), base::TimeTicks(), + ui::EF_MIDDLE_MOUSE_BUTTON, 0); + ui::MouseEvent release_event( + ui::EventType::kMouseReleased, gfx::Point(50, 25), gfx::Point(), + base::TimeTicks(), ui::EF_MIDDLE_MOUSE_BUTTON, 0); + testing::Mock::VerifyAndClearExpectations(&tab_slot_controller); + EXPECT_CALL(tab_slot_controller, + CloseTab(tab_ptr, CloseTabSource::kFromMouse)) + .Times(1); + tab_ptr->OnMousePressed(press_event); + tab_ptr->OnMouseReleased(release_event); + + // Test disabling middle click to close + tab_slot_controller.set_can_close_tab_via_middle_button_click(false); + EXPECT_FALSE(tab_slot_controller.CanCloseTabViaMiddleButtonClick()); + + // Simulate middle mouse button click - should NOT close the tab + testing::Mock::VerifyAndClearExpectations(&tab_slot_controller); + EXPECT_CALL(tab_slot_controller, + CloseTab(tab_ptr, CloseTabSource::kFromMouse)) + .Times(0); + tab_ptr->OnMousePressed(press_event); + tab_ptr->OnMouseReleased(release_event); + + // Test re-enabling middle click to close + tab_slot_controller.set_can_close_tab_via_middle_button_click(true); + EXPECT_TRUE(tab_slot_controller.CanCloseTabViaMiddleButtonClick()); + + // Simulate middle mouse button click - should close the tab again + testing::Mock::VerifyAndClearExpectations(&tab_slot_controller); + EXPECT_CALL(tab_slot_controller, + CloseTab(tab_ptr, CloseTabSource::kFromMouse)) + .Times(1); + tab_ptr->OnMousePressed(press_event); + tab_ptr->OnMouseReleased(release_event); +} diff --git a/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.cc b/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.cc index a2e2748f26f..4295df69a39 100644 --- a/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.cc +++ b/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.cc @@ -13,6 +13,10 @@ bool FakeTabSlotController::ShouldAlwaysHideCloseButton() const { return should_always_hide_close_button_; } +bool FakeTabSlotController::CanCloseTabViaMiddleButtonClick() const { + return can_close_tab_via_middle_button_click_; +} + bool FakeTabSlotController::IsVerticalTabsFloating() const { return false; } diff --git a/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h b/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h index bf3edbcae41..902cd6fc445 100644 --- a/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h +++ b/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h @@ -12,18 +12,23 @@ IsGroupCollapsed(__VA_ARGS__) const override; \ const Browser* GetBrowser() -// Add override for ShouldAlwaysHideTabCloseButton() and setters to control its -// return value in tests. -#define ShouldCompactLeadingEdge() \ - ShouldCompactLeadingEdge() const override; \ - \ - private: \ - bool should_always_hide_close_button_ = false; \ - \ - public: \ - void set_should_always_hide_close_button(bool hide) { \ - should_always_hide_close_button_ = hide; \ - } \ +// Add override for +// ShouldAlwaysHideTabCloseButton()/CanCloseTabViaMiddleButtonClick() and +// setters to control its return value in tests. +#define ShouldCompactLeadingEdge() \ + ShouldCompactLeadingEdge() const override; \ + \ + private: \ + bool should_always_hide_close_button_ = false; \ + bool can_close_tab_via_middle_button_click_ = true; \ + \ + public: \ + void set_should_always_hide_close_button(bool hide) { \ + should_always_hide_close_button_ = hide; \ + } \ + void set_can_close_tab_via_middle_button_click(bool enabled) { \ + can_close_tab_via_middle_button_click_ = enabled; \ + } \ bool ShouldAlwaysHideCloseButton() // Add override for IsVerticalTabsFloating() @@ -31,8 +36,14 @@ EndDrag(__VA_ARGS__) override; \ bool IsVerticalTabsFloating() const +// Add override for CanCloseTabViaMiddleButtonClick() +#define CanPaintThrobberToLayer() \ + CanPaintThrobberToLayer() const override; \ + bool CanCloseTabViaMiddleButtonClick() + #include // IWYU pragma: export +#undef CanPaintThrobberToLayer #undef EndDrag #undef ShouldCompactLeadingEdge #undef IsGroupCollapsed diff --git a/chromium_src/chrome/browser/ui/views/tabs/tab_slot_controller.h b/chromium_src/chrome/browser/ui/views/tabs/tab_slot_controller.h index b728a0b38d1..4443a8668db 100644 --- a/chromium_src/chrome/browser/ui/views/tabs/tab_slot_controller.h +++ b/chromium_src/chrome/browser/ui/views/tabs/tab_slot_controller.h @@ -24,8 +24,15 @@ EndDrag(__VA_ARGS__) = 0; \ virtual bool IsVerticalTabsFloating() const +// Add a method to TabSlotController to determine whether tabs can be closed via +// middle mouse button click. +#define CanPaintThrobberToLayer() \ + CanPaintThrobberToLayer() const = 0; \ + virtual bool CanCloseTabViaMiddleButtonClick() + #include // IWYU pragma: export +#undef CanPaintThrobberToLayer #undef EndDrag #undef ShouldCompactLeadingEdge #undef IsGroupCollapsed diff --git a/chromium_src/chrome/browser/ui/views/tabs/tab_strip.cc b/chromium_src/chrome/browser/ui/views/tabs/tab_strip.cc index d5a9e3a5f83..3286eb335fa 100644 --- a/chromium_src/chrome/browser/ui/views/tabs/tab_strip.cc +++ b/chromium_src/chrome/browser/ui/views/tabs/tab_strip.cc @@ -65,6 +65,10 @@ bool TabStrip::ShouldAlwaysHideCloseButton() const { return false; } +bool TabStrip::CanCloseTabViaMiddleButtonClick() const { + return true; +} + bool TabStrip::IsVerticalTabsFloating() const { return false; } diff --git a/chromium_src/chrome/browser/ui/views/tabs/tab_strip.h b/chromium_src/chrome/browser/ui/views/tabs/tab_strip.h index c0291d3cc0f..22db1e96c1a 100644 --- a/chromium_src/chrome/browser/ui/views/tabs/tab_strip.h +++ b/chromium_src/chrome/browser/ui/views/tabs/tab_strip.h @@ -19,8 +19,10 @@ class BraveTabHoverCardController; friend class BraveVerticalTabStripRegionView; \ void UpdateHoverCard -#define ShouldDrawStrokes \ - UnUsed() { return true; } \ +#define ShouldDrawStrokes \ + UnUsed() { \ + return true; \ + } \ virtual bool ShouldDrawStrokes #define GetDragContext \ Unused_GetDragContext() { \ @@ -30,6 +32,7 @@ class BraveTabHoverCardController; friend class BraveTabDragContext; \ const Browser* GetBrowser() const override; \ bool ShouldAlwaysHideCloseButton() const override; \ + bool CanCloseTabViaMiddleButtonClick() const override; \ bool IsVerticalTabsFloating() const override; \ static constexpr bool IsUsingBraveTabHoverCardController() { \ return std::is_same_v, \ diff --git a/patches/chrome-browser-ui-views-tabs-tab.cc.patch b/patches/chrome-browser-ui-views-tabs-tab.cc.patch index a2ab4c560bc..66ccfd70052 100644 --- a/patches/chrome-browser-ui-views-tabs-tab.cc.patch +++ b/patches/chrome-browser-ui-views-tabs-tab.cc.patch @@ -1,5 +1,5 @@ diff --git a/chrome/browser/ui/views/tabs/tab.cc b/chrome/browser/ui/views/tabs/tab.cc -index 722f2832b44b4fe57fe1738edd7ccfecc7f629ab..011d7c8a7a9efe127c35b6b23123830289948b0a 100644 +index 722f2832b44b4fe57fe1738edd7ccfecc7f629ab..129598d1a22484a9d14987213ce47bf013717a32 100644 --- a/chrome/browser/ui/views/tabs/tab.cc +++ b/chrome/browser/ui/views/tabs/tab.cc @@ -376,6 +376,7 @@ void Tab::Layout(PassKey) { @@ -18,3 +18,12 @@ index 722f2832b44b4fe57fe1738edd7ccfecc7f629ab..011d7c8a7a9efe127c35b6b231238302 } else if (showing_close_button_) { // Allow the title to overlay the close button's empty border padding. title_right = close_x - after_title_padding; +@@ -621,7 +623,7 @@ void Tab::OnMouseReleased(const ui::MouseEvent& event) { + // Close tab on middle click, but only if the button is released over the tab + // (normal windows behavior is to discard presses of a UI element where the + // releases happen off the element). +- if (event.IsOnlyMiddleMouseButton()) { ++ if (event.IsOnlyMiddleMouseButton() && controller_->CanCloseTabViaMiddleButtonClick()) { + if (HitTestPoint(event.location())) { + controller_->CloseTab(this, CloseTabSource::kFromMouse); + } else if (closing_) { diff --git a/rewrite/chrome/browser/ui/views/tabs/tab.cc.toml b/rewrite/chrome/browser/ui/views/tabs/tab.cc.toml new file mode 100644 index 00000000000..e6cc1638be8 --- /dev/null +++ b/rewrite/chrome/browser/ui/views/tabs/tab.cc.toml @@ -0,0 +1,22 @@ +# Copyright (c) 2025 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +[[substitution]] +description = 'Check if tab can be closed via middle mouse button click based on prefs' +pattern = 'if (event.IsOnlyMiddleMouseButton())' +replace = 'if (event.IsOnlyMiddleMouseButton() && controller_->CanCloseTabViaMiddleButtonClick())' +count = 1 + +[[substitution]] +description = 'Add BRAVE_UI_VIEWS_TABS_TAB_LAYOUT_ADJUST_ICON_POSITION' +re_pattern = '(gfx::Rect favicon_bounds\(start, contents_rect\.y\(\), 0, 0\);\s+if \(showing_icon_\) \{\n)' +replace = '\1 BRAVE_UI_VIEWS_TABS_TAB_LAYOUT_ADJUST_ICON_POSITION\n' +count = 1 + +[[substitution]] +description = 'Add BRAVE_UI_VIEWS_TABS_TAB_ALERT_INDICATOR_POSITION' +re_pattern = '(if\s*\(showing_alert_indicator_\)\s*\{\n\s*title_right\s*=\s*alert_indicator_button_->x\(\)\s*-\s*after_title_padding;)' +replace = '\1\n BRAVE_UI_VIEWS_TABS_TAB_ALERT_INDICATOR_POSITION' +