Fix right click on tabs closing them (#19273)

I do not like this.

## Validation Steps Performed
* Enable close buttons on tabs
* Open a tab
* Close the tab with middle click
* Open a tab
* Right click the tab
* Tab doesn't close, Menu opens 
This commit is contained in:
Leonard Hecker 2025-09-09 01:16:20 +02:00 committed by GitHub
parent 54aaa4a98a
commit 4a34a76504
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 83 deletions

View File

@ -158,12 +158,7 @@ namespace winrt::TerminalApp::implementation
// Set this tab's icon to the icon from the content
_UpdateTabIcon(*newTabImpl);
// This is necessary, because WinUI does not have support for middle clicks.
// Its Tapped event doesn't provide the information what button was used either.
tabViewItem.PointerPressed({ this, &TerminalPage::_OnTabPointerPressed });
tabViewItem.PointerReleased({ this, &TerminalPage::_OnTabPointerReleased });
tabViewItem.PointerExited({ this, &TerminalPage::_OnTabPointerExited });
tabViewItem.PointerEntered({ this, &TerminalPage::_OnTabPointerEntered });
// When the tab requests close, try to close it (prompt for approval, if required)
newTabImpl->CloseRequested([weakTab, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) {
@ -665,7 +660,7 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - returns a tab corresponding to a view item. This might return null,
// so make sure to check the result!
winrt::TerminalApp::Tab TerminalPage::_GetTabByTabViewItem(const Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem) const noexcept
winrt::TerminalApp::Tab TerminalPage::_GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept
{
uint32_t tabIndexFromControl{};
const auto items{ _tabView.TabItems() };
@ -877,60 +872,64 @@ namespace winrt::TerminalApp::implementation
_UpdateTabView();
}
// Method Description:
// - Additional responses to clicking on a TabView's item. Currently, just remove tab with middle click
// Arguments:
// - sender: the control that originated this event (TabViewItem)
// - eventArgs: the event's constituent arguments
void TerminalPage::_OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs)
void TerminalPage::_OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e)
{
if (eventArgs.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
if (!_tabItemMiddleClickHookEnabled || !e.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
if (const auto tabViewItem{ sender.try_as<MUX::Controls::TabViewItem>() })
{
_tabPointerMiddleButtonPressed = tabViewItem.CapturePointer(eventArgs.Pointer());
_tabPointerMiddleButtonExited = false;
}
eventArgs.Handled(true);
return;
}
const auto tabViewItem = sender.try_as<MUX::Controls::TabViewItem>();
if (!tabViewItem || !tabViewItem.CapturePointer(e.Pointer()))
{
return;
}
_tabItemMiddleClickExited = false;
_tabItemMiddleClickPointerEntered = tabViewItem.PointerEntered(winrt::auto_revoke, [this](auto&&, auto&& e) {
_tabItemMiddleClickExited = false;
e.Handled(true);
});
_tabItemMiddleClickPointerExited = tabViewItem.PointerExited(winrt::auto_revoke, [this](auto&&, auto&& e) {
_tabItemMiddleClickExited = true;
e.Handled(true);
});
_tabItemMiddleClickPointerCaptureLost = tabViewItem.PointerCaptureLost(winrt::auto_revoke, [this](auto&& sender, auto&& e) {
// The WinUI TabView calls CapturePointer() internally and it's not reference counted,
// so when it calls ReleasePointerCapture() in its PointerReleased handler,
// we get a PointerCaptureLost before we receive the PointerReleased event.
// This makes typical handling of PointerReleased events on our side difficult.
// Well, whatever, now we just hook PointerCaptureLost because we know WinUI will trigger it.
_tabItemMiddleClickPointerEntered.revoke();
_tabItemMiddleClickPointerExited.revoke();
_tabItemMiddleClickPointerCaptureLost.revoke();
if (!_tabItemMiddleClickExited && !e.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
_OnTabPointerReleasedCloseTab(std::move(sender));
}
e.Handled(true);
});
e.Handled(true);
}
// Method Description:
// - Tracking pointer state for tab remove
// Arguments:
// - sender: the control that originated this event (TabViewItem)
// - eventArgs: the event's constituent arguments
void TerminalPage::_OnTabPointerReleased(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs)
safe_void_coroutine TerminalPage::_OnTabPointerReleasedCloseTab(IInspectable sender)
{
if (_tabPointerMiddleButtonPressed && !eventArgs.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
_tabPointerMiddleButtonPressed = false;
if (auto tabViewItem{ sender.try_as<MUX::Controls::TabViewItem>() })
{
tabViewItem.ReleasePointerCapture(eventArgs.Pointer());
if (!_tabPointerMiddleButtonExited)
{
_OnTabPointerReleasedCloseTab(std::move(tabViewItem));
}
}
eventArgs.Handled(true);
}
}
safe_void_coroutine TerminalPage::_OnTabPointerReleasedCloseTab(winrt::Microsoft::UI::Xaml::Controls::TabViewItem sender)
{
const auto tab = _GetTabByTabViewItem(sender);
if (!tab)
{
co_return;
}
// WinUI asynchronously updates its tab view items, so it may happen that we're given a
// `TabViewItem` that still contains a `Tab` which has actually already been removed.
// First we must yield once, to flush out whatever TabView is currently doing.
const auto strong = get_strong();
co_await wil::resume_foreground(Dispatcher());
const auto tab = _GetTabByTabViewItem(sender);
if (!tab)
{
co_return;
}
// `tab.Shutdown()` in `_RemoveTab()` sets the content to null = This checks if the tab is closed.
if (tab.Content())
{
@ -938,34 +937,6 @@ namespace winrt::TerminalApp::implementation
}
}
// Method Description:
// - Tracking pointer state for tab remove
// Arguments:
// - sender: the control that originated this event (TabViewItem)
// - eventArgs: the event's constituent arguments
void TerminalPage::_OnTabPointerEntered(const IInspectable& /*sender*/, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs)
{
if (eventArgs.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
_tabPointerMiddleButtonExited = false;
eventArgs.Handled(true);
}
}
// Method Description:
// - Tracking pointer state for tab remove
// Arguments:
// - sender: the control that originated this event (TabViewItem)
// - eventArgs: the event's constituent arguments
void TerminalPage::_OnTabPointerExited(const IInspectable& /*sender*/, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs)
{
if (eventArgs.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
_tabPointerMiddleButtonExited = true;
eventArgs.Handled(true);
}
}
void TerminalPage::_UpdatedSelectedTab(const winrt::TerminalApp::Tab& tab)
{
// Unfocus all the tabs.

View File

@ -374,6 +374,8 @@ namespace winrt::TerminalApp::implementation
{
const auto visibility = theme.Tab() ? theme.Tab().ShowCloseButton() : Settings::Model::TabCloseButtonVisibility::Always;
_tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never;
switch (visibility)
{
case Settings::Model::TabCloseButtonVisibility::Never:
@ -3848,6 +3850,8 @@ namespace winrt::TerminalApp::implementation
theme.Tab().ShowCloseButton() :
Settings::Model::TabCloseButtonVisibility::Always;
_tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never;
for (const auto& tab : _tabs)
{
tab.CloseButtonVisibility(visibility);

View File

@ -396,7 +396,7 @@ namespace winrt::TerminalApp::implementation
std::optional<uint32_t> _GetTabIndex(const TerminalApp::Tab& tab) const noexcept;
TerminalApp::Tab _GetFocusedTab() const noexcept;
winrt::com_ptr<Tab> _GetFocusedTabImpl() const noexcept;
TerminalApp::Tab _GetTabByTabViewItem(const Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem) const noexcept;
TerminalApp::Tab _GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept;
void _HandleClosePaneRequested(std::shared_ptr<Pane> pane);
safe_void_coroutine _SetFocusedTab(const winrt::TerminalApp::Tab tab);
@ -440,13 +440,18 @@ namespace winrt::TerminalApp::implementation
void _TabDragStarted(const IInspectable& sender, const IInspectable& eventArgs);
void _TabDragCompleted(const IInspectable& sender, const IInspectable& eventArgs);
bool _tabPointerMiddleButtonPressed{ false };
bool _tabPointerMiddleButtonExited{ false };
// BODGY: WinUI's TabView has a broken close event handler:
// If the close button is disabled, middle-clicking the tab raises no close
// event. Because that's dumb, we implement our own middle-click handling.
// `_tabItemMiddleClickHookEnabled` is true whenever the close button is hidden,
// and that enables all of the rest of this machinery (and this workaround).
bool _tabItemMiddleClickHookEnabled = false;
bool _tabItemMiddleClickExited = false;
PointerEntered_revoker _tabItemMiddleClickPointerEntered;
PointerExited_revoker _tabItemMiddleClickPointerExited;
PointerCaptureLost_revoker _tabItemMiddleClickPointerCaptureLost;
void _OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs);
void _OnTabPointerReleased(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs);
safe_void_coroutine _OnTabPointerReleasedCloseTab(winrt::Microsoft::UI::Xaml::Controls::TabViewItem sender);
void _OnTabPointerEntered(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs);
void _OnTabPointerExited(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs);
safe_void_coroutine _OnTabPointerReleasedCloseTab(IInspectable sender);
void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs);
void _OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs);