Add setting to control middle-click-to-close tab behavior (#32926)

This commit adds a new user preference that allows users to disable the
middle-click-to-close tab functionality, addressing user feedback that
accidental middle-clicks can cause unwanted tab closures.

Changes include:

**Browser Preferences:**
- Add kTabsCloseOnMiddleClick pref (default: true) in brave_tab_prefs.h
- Register preference in brave_prefs_util.cc for settings UI exposure

**UI Implementation:**
- Extend TabSlotController interface with CanCloseTabViaMiddleButtonClick()
- Implement pref checking in BraveTabStrip via base class override
- Override TabSlotController methods in tab_strip.h/cc to respect preference
- Add chromium_src overrides for FakeTabSlotController and FakeBaseTabStripController
  to support testing infrastructure

**Tab Click Handling:**
- Patch tab.cc to check controller preference before handling middle clicks
- Use plaster.toml rewrite for cleaner code transformation
- Maintain existing behavior when preference is enabled (default)

**Settings UI:**
- Add "Close tabs on middle click" toggle to Appearance > Tabs settings
- Include localized strings in brave_settings_strings.grdp
This commit is contained in:
Sangwoo Ko
2026-01-08 12:52:45 +09:00
committed by GitHub
parent 2c5e2fabee
commit 2e2024b511
16 changed files with 157 additions and 15 deletions
+3
View File
@@ -180,6 +180,9 @@
<message name="IDS_SETTINGS_APPEARANCE_SETTINGS_TABS_ALWAYS_HIDE_TAB_CLOSE_BUTTON" desc="The label for always hiding tab close button" formatter_data="webui=BraveSettings">
Always hide tab close button
</message>
<message name="IDS_SETTINGS_APPEARANCE_SETTINGS_TABS_MIDDLE_CLICK_CLOSE_TAB" desc="The label for allowing middle button click to close tabs" formatter_data="webui=BraveSettings">
Allow middle button click to close tabs
</message>
<message name="IDS_SETTINGS_APPEARANCE_SETTINGS_TABS_USE_FLOATING_VERTICAL_TABS" desc="The label for enabling floating mode for vertical tab strip">
Auto expand on hover when minimized
</message>
@@ -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)
@@ -49,6 +49,7 @@ base::span<const base::cstring_view> GetBravePersistentPrefNames() {
brave_tabs::kSharedPinnedTab,
brave_tabs::kTreeTabsEnabled,
brave_tabs::kAlwaysHideTabCloseButton,
brave_tabs::kMiddleClickCloseTabEnabled,
#endif
#if defined(TOOLKIT_VIEWS)
sidebar::kSidePanelWidth,
@@ -106,6 +106,13 @@
label="$i18n{SETTINGS_APPEARANCE_SETTINGS_TABS_ALWAYS_HIDE_TAB_CLOSE_BUTTON}">
</settings-toggle-button>
<!-- Middle click close tab -->
<settings-toggle-button
pref="{{prefs.brave.tabs.middle_click_close_tab_enabled}}"
class="cr-row"
label="$i18n{SETTINGS_APPEARANCE_SETTINGS_TABS_MIDDLE_CLICK_CLOSE_TAB}">
</settings-toggle-button>
<!-- Tab hover mode -->
<div class="cr-row settings-box">
<div class="flex">
+1
View File
@@ -41,6 +41,7 @@ void RegisterBraveProfilePrefs(PrefRegistrySimple* registry) {
}
registry->RegisterBooleanPref(kAlwaysHideTabCloseButton, false);
registry->RegisterBooleanPref(kMiddleClickCloseTabEnabled, true);
}
void MigrateBraveProfilePrefs(PrefService* prefs) {
+3
View File
@@ -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);
+6
View File
@@ -53,6 +53,8 @@ BraveTabStrip::BraveTabStrip(std::unique_ptr<TabStripController> 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
+2
View File
@@ -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<BraveTabStrip> weak_factory_{this};
};
@@ -115,6 +115,7 @@ class BraveTabRenamingUnitTest : public BraveTabTest {
SetCustomTitleForTab,
(Tab * tab, const std::optional<std::u16string>& 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<BraveTabRenamingUnitTest::MockTabSlotController>
tab_slot_controller;
auto tab = std::make_unique<BraveTab>(&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<BraveTab*>(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);
}
@@ -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;
}
@@ -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 <chrome/browser/ui/views/tabs/fake_tab_slot_controller.h> // IWYU pragma: export
#undef CanPaintThrobberToLayer
#undef EndDrag
#undef ShouldCompactLeadingEdge
#undef IsGroupCollapsed
@@ -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 <chrome/browser/ui/views/tabs/tab_slot_controller.h> // IWYU pragma: export
#undef CanPaintThrobberToLayer
#undef EndDrag
#undef ShouldCompactLeadingEdge
#undef IsGroupCollapsed
@@ -65,6 +65,10 @@ bool TabStrip::ShouldAlwaysHideCloseButton() const {
return false;
}
bool TabStrip::CanCloseTabViaMiddleButtonClick() const {
return true;
}
bool TabStrip::IsVerticalTabsFloating() const {
return false;
}
@@ -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<std::unique_ptr<BraveTabHoverCardController>, \
@@ -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_) {
@@ -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'