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:
@@ -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">
|
||||
|
||||
@@ -41,6 +41,7 @@ void RegisterBraveProfilePrefs(PrefRegistrySimple* registry) {
|
||||
}
|
||||
|
||||
registry->RegisterBooleanPref(kAlwaysHideTabCloseButton, false);
|
||||
registry->RegisterBooleanPref(kMiddleClickCloseTabEnabled, true);
|
||||
}
|
||||
|
||||
void MigrateBraveProfilePrefs(PrefService* prefs) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user