Skip to content

Commit ce0f8d6

Browse files
authored
Fix two panes being closed when just one is (#17358)
#17333 introduced a regression: While it fixes a recursion *into* `Pane::Close()` that doesn't fix the recursion outside of it. In this case, `Close()` raises the `Closed` event which results in another tab being closed because it's bound to `_RemoveTab`. The recursion is now obvious, because I made the entire process synchronous. Previously, it would (hopefully) just be scheduled after the pane and its content are already gone. The issue can be fixed by moving the recursion check from `Pane::Close()` to `TerminalTab::Shutdown()` but I felt like it would better to fix the issue a bit more thoroughly. `IPaneContent` can raise a `CloseRequested` event to indicate it wants to be closed. However, that also contained recursion, because the content would call its own `Close()` to raise the event, which the tab catches, calls `Close()` on the `Pane` which calls `Close()` on the content which raises the event again and so on. That's what was fixed in #17333 among others. We can do this better by not raising the event from `IPaneContent::Close()`. Instead, that method will now be exclusively called by `Pane`. The `CloseRequested` event will now truly be just a request and nothing more. Furthermore, the ownership of the event handling was moved from the `TerminalTab` to the `Pane`. To make all of this a bit simpler and more robust, two new methods were added to `Pane`: `_takePaneContent` and `_setPaneContent`. These methods ensure that `Close()` is called on the content, that the event handlers are always added and revoked and that the ownership transfers cleanly between panes. ## Validation Steps Performed * Open 3 tabs, close the middle one ✅ * Open 3 vertical panes, close the middle one ✅ * Drag tabs with multiple panes between windows ✅
1 parent ece0c04 commit ce0f8d6

File tree

8 files changed

+61
-66
lines changed

8 files changed

+61
-66
lines changed

src/cascadia/TerminalApp/Pane.cpp

+39-17
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ static const int CombinedPaneBorderSize = 2 * PaneBorderSize;
2727
static const int AnimationDurationInMilliseconds = 200;
2828
static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(AnimationDurationInMilliseconds)));
2929

30-
Pane::Pane(const IPaneContent& content, const bool lastFocused) :
31-
_content{ content },
30+
Pane::Pane(IPaneContent content, const bool lastFocused) :
3231
_lastActive{ lastFocused }
3332
{
33+
_setPaneContent(std::move(content));
3434
_root.Children().Append(_borderFirst);
3535

3636
const auto& control{ _content.GetRoot() };
@@ -985,17 +985,7 @@ void Pane::_ContentLostFocusHandler(const winrt::Windows::Foundation::IInspectab
985985
// - <none>
986986
void Pane::Close()
987987
{
988-
// Pane has two events, CloseRequested and Closed. CloseRequested is raised by the content asking to be closed,
989-
// but also by the window who owns the tab when it's closing. The event is then caught by the TerminalTab which
990-
// calls Close() which then raises the Closed event. Now, if this is the last pane in the window, this will result
991-
// in the window raising CloseRequested again which leads to infinite recursion, so we need to guard against that.
992-
// Ideally we would have just a single event in the future.
993-
if (_closed)
994-
{
995-
return;
996-
}
997-
998-
_closed = true;
988+
_setPaneContent(nullptr);
999989
// Fire our Closed event to tell our parent that we should be removed.
1000990
Closed.raise(nullptr, nullptr);
1001991
}
@@ -1007,7 +997,7 @@ void Pane::Shutdown()
1007997
{
1008998
if (_IsLeaf())
1009999
{
1010-
_content.Close();
1000+
_setPaneContent(nullptr);
10111001
}
10121002
else
10131003
{
@@ -1411,7 +1401,7 @@ void Pane::_CloseChild(const bool closeFirst)
14111401
_borders = _GetCommonBorders();
14121402

14131403
// take the control, profile, id and isDefTermSession of the pane that _wasn't_ closed.
1414-
_content = remainingChild->_content;
1404+
_setPaneContent(remainingChild->_takePaneContent());
14151405
_id = remainingChild->Id();
14161406

14171407
// Revoke the old event handlers. Remove both the handlers for the panes
@@ -1716,6 +1706,34 @@ void Pane::_SetupChildCloseHandlers()
17161706
});
17171707
}
17181708

1709+
// With this method you take ownership of the control from this Pane.
1710+
// Assign it to another Pane with _setPaneContent() or Close() it.
1711+
IPaneContent Pane::_takePaneContent()
1712+
{
1713+
_closeRequestedRevoker.revoke();
1714+
return std::move(_content);
1715+
}
1716+
1717+
// This method safely sets the content of the Pane. It'll ensure to revoke and
1718+
// assign event handlers, and to Close() the existing content if there's any.
1719+
// The new content can be nullptr to remove any content.
1720+
void Pane::_setPaneContent(IPaneContent content)
1721+
{
1722+
// The IPaneContent::Close() implementation may be buggy and raise the CloseRequested event again.
1723+
// _takePaneContent() avoids this as it revokes the event handler.
1724+
if (const auto c = _takePaneContent())
1725+
{
1726+
c.Close();
1727+
}
1728+
1729+
_content = std::move(content);
1730+
1731+
if (_content)
1732+
{
1733+
_closeRequestedRevoker = _content.CloseRequested(winrt::auto_revoke, [this](auto&&, auto&&) { Close(); });
1734+
}
1735+
}
1736+
17191737
// Method Description:
17201738
// - Sets up row/column definitions for this pane. There are three total
17211739
// row/cols. The middle one is for the separator. The first and third are for
@@ -2266,8 +2284,7 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitDirect
22662284
else
22672285
{
22682286
// Move our control, guid, isDefTermSession into the first one.
2269-
_firstChild = std::make_shared<Pane>(_content);
2270-
_content = nullptr;
2287+
_firstChild = std::make_shared<Pane>(_takePaneContent());
22712288
_firstChild->_broadcastEnabled = _broadcastEnabled;
22722289
}
22732290

@@ -2462,6 +2479,11 @@ bool Pane::_HasChild(const std::shared_ptr<Pane> child)
24622479
});
24632480
}
24642481

2482+
winrt::TerminalApp::TerminalPaneContent Pane::_getTerminalContent() const
2483+
{
2484+
return _IsLeaf() ? _content.try_as<winrt::TerminalApp::TerminalPaneContent>() : nullptr;
2485+
}
2486+
24652487
// Method Description:
24662488
// - Recursive function that finds a pane with the given ID
24672489
// Arguments:

src/cascadia/TerminalApp/Pane.h

+5-6
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ struct PaneResources
6262
class Pane : public std::enable_shared_from_this<Pane>
6363
{
6464
public:
65-
Pane(const winrt::TerminalApp::IPaneContent& content,
65+
Pane(winrt::TerminalApp::IPaneContent content,
6666
const bool lastFocused = false);
6767

6868
Pane(std::shared_ptr<Pane> first,
@@ -248,13 +248,13 @@ class Pane : public std::enable_shared_from_this<Pane>
248248

249249
std::optional<uint32_t> _id;
250250
std::weak_ptr<Pane> _parentChildPath{};
251-
bool _closed{ false };
252251
bool _lastActive{ false };
253252
winrt::event_token _firstClosedToken{ 0 };
254253
winrt::event_token _secondClosedToken{ 0 };
255254

256255
winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker;
257256
winrt::Windows::UI::Xaml::UIElement::LostFocus_revoker _lostFocusRevoker;
257+
winrt::TerminalApp::IPaneContent::CloseRequested_revoker _closeRequestedRevoker;
258258

259259
Borders _borders{ Borders::None };
260260

@@ -264,11 +264,10 @@ class Pane : public std::enable_shared_from_this<Pane>
264264
bool _IsLeaf() const noexcept;
265265
bool _HasFocusedChild() const noexcept;
266266
void _SetupChildCloseHandlers();
267+
winrt::TerminalApp::IPaneContent _takePaneContent();
268+
void _setPaneContent(winrt::TerminalApp::IPaneContent content);
267269
bool _HasChild(const std::shared_ptr<Pane> child);
268-
winrt::TerminalApp::TerminalPaneContent _getTerminalContent() const
269-
{
270-
return _IsLeaf() ? _content.try_as<winrt::TerminalApp::TerminalPaneContent>() : nullptr;
271-
}
270+
winrt::TerminalApp::TerminalPaneContent _getTerminalContent() const;
272271

273272
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType,
274273
const float splitSize,

src/cascadia/TerminalApp/ScratchpadContent.cpp

-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ namespace winrt::TerminalApp::implementation
4545
}
4646
void ScratchpadContent::Close()
4747
{
48-
CloseRequested.raise(*this, nullptr);
4948
}
5049

5150
INewContentArgs ScratchpadContent::GetNewTerminalArgs(const BuildStartupKind /* kind */) const

src/cascadia/TerminalApp/SettingsPaneContent.cpp

-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ namespace winrt::TerminalApp::implementation
4747
}
4848
void SettingsPaneContent::Close()
4949
{
50-
CloseRequested.raise(*this, nullptr);
5150
}
5251

5352
INewContentArgs SettingsPaneContent::GetNewTerminalArgs(const BuildStartupKind /*kind*/) const

src/cascadia/TerminalApp/TerminalPaneContent.cpp

+13-14
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ namespace winrt::TerminalApp::implementation
7878
_bellPlayer = nullptr;
7979
_bellPlayerCreated = false;
8080
}
81-
82-
CloseRequested.raise(*this, nullptr);
8381
}
8482

8583
winrt::hstring TerminalPaneContent::Icon() const
@@ -239,19 +237,20 @@ namespace winrt::TerminalApp::implementation
239237

240238
if (_profile)
241239
{
242-
if (_isDefTermSession && _profile.CloseOnExit() == CloseOnExitMode::Automatic)
243-
{
244-
// For 'automatic', we only care about the connection state if we were launched by Terminal
245-
// Since we were launched via defterm, ignore the connection state (i.e. we treat the
246-
// close on exit mode as 'always', see GH #13325 for discussion)
247-
Close();
248-
}
249-
250240
const auto mode = _profile.CloseOnExit();
251-
if ((mode == CloseOnExitMode::Always) ||
252-
((mode == CloseOnExitMode::Graceful || mode == CloseOnExitMode::Automatic) && newConnectionState == ConnectionState::Closed))
241+
242+
if (
243+
// This one is obvious: If the user asked for "always" we do just that.
244+
(mode == CloseOnExitMode::Always) ||
245+
// Otherwise, and unless the user asked for the opposite of "always",
246+
// close the pane when the connection closed gracefully (not failed).
247+
(mode != CloseOnExitMode::Never && newConnectionState == ConnectionState::Closed) ||
248+
// However, defterm handoff can result in Windows Terminal randomly opening which may be annoying,
249+
// so by default we should at least always close the pane, even if the command failed.
250+
// See GH #13325 for discussion.
251+
(mode == CloseOnExitMode::Automatic && _isDefTermSession))
253252
{
254-
Close();
253+
CloseRequested.raise(nullptr, nullptr);
255254
}
256255
}
257256
}
@@ -331,7 +330,7 @@ namespace winrt::TerminalApp::implementation
331330
void TerminalPaneContent::_closeTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
332331
const winrt::Windows::Foundation::IInspectable& /*args*/)
333332
{
334-
Close();
333+
CloseRequested.raise(nullptr, nullptr);
335334
}
336335

337336
void TerminalPaneContent::_restartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,

src/cascadia/TerminalApp/TerminalTab.cpp

-20
Original file line numberDiff line numberDiff line change
@@ -947,26 +947,6 @@ namespace winrt::TerminalApp::implementation
947947
auto dispatcher = TabViewItem().Dispatcher();
948948
ContentEventTokens events{};
949949

950-
events.CloseRequested = content.CloseRequested(
951-
winrt::auto_revoke,
952-
[this](auto&& sender, auto&&) {
953-
if (const auto content{ sender.try_as<TerminalApp::IPaneContent>() })
954-
{
955-
// Calling Close() while walking the tree is not safe, because Close() mutates the tree.
956-
const auto pane = _rootPane->_FindPane([&](const auto& p) -> std::shared_ptr<Pane> {
957-
if (p->GetContent() == content)
958-
{
959-
return p;
960-
}
961-
return {};
962-
});
963-
if (pane)
964-
{
965-
pane->Close();
966-
}
967-
}
968-
});
969-
970950
events.TitleChanged = content.TitleChanged(
971951
winrt::auto_revoke,
972952
[dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {

src/cascadia/TerminalApp/TerminalTab.h

-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ namespace winrt::TerminalApp::implementation
134134
winrt::TerminalApp::IPaneContent::ConnectionStateChanged_revoker ConnectionStateChanged;
135135
winrt::TerminalApp::IPaneContent::ReadOnlyChanged_revoker ReadOnlyChanged;
136136
winrt::TerminalApp::IPaneContent::FocusRequested_revoker FocusRequested;
137-
winrt::TerminalApp::IPaneContent::CloseRequested_revoker CloseRequested;
138137

139138
// These events literally only apply if the content is a TermControl.
140139
winrt::Microsoft::Terminal::Control::TermControl::KeySent_revoker KeySent;

src/cascadia/TerminalConnection/ConptyConnection.cpp

+4-6
Original file line numberDiff line numberDiff line change
@@ -431,12 +431,10 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
431431
try
432432
{
433433
// GH#11556 - make sure to format the error code to this string as an UNSIGNED int
434-
winrt::hstring exitText{ fmt::format(std::wstring_view{ RS_(L"ProcessExited") }, fmt::format(_errorFormat, status)) };
435-
TerminalOutput.raise(L"\r\n");
436-
TerminalOutput.raise(exitText);
437-
TerminalOutput.raise(L"\r\n");
438-
TerminalOutput.raise(RS_(L"CtrlDToClose"));
439-
TerminalOutput.raise(L"\r\n");
434+
const auto msg1 = fmt::format(std::wstring_view{ RS_(L"ProcessExited") }, fmt::format(_errorFormat, status));
435+
const auto msg2 = RS_(L"CtrlDToClose");
436+
const auto msg = fmt::format(FMT_COMPILE(L"\r\n{}\r\n{}\r\n"), msg1, msg2);
437+
TerminalOutput.raise(msg);
440438
}
441439
CATCH_LOG();
442440
}

0 commit comments

Comments
 (0)