Add the ability to interact with subtrees of panes (#11153)

This commit adds the ability to interact with subtrees of panes. 

Have you ever thought that you don't have enough regression testing to
do? Boy do I have the PR for you! This breaks all kinds of assumptions
about what is or is not focused, largely complicated by the fact that a
pane is not a proper control. I did my best to cover as many cases as I
could, but I wouldn't be surprised if there are some things broken that
I am unaware of.

Done:
- Add `parent` and `child` movement directions to move up and down the
  tree respectively
- When a parent pane is selected it will have borders all around it in
  addition to any borders the children have.
- Fix focus, swap, split, zoom, toggle orientation, resize, and move to
  all handle interacting with more than one pane.
- Similarly the actions for font size changing, closing, read-only, clearing
   buffer, and changing color scheme will distribute to all children.
- This technically leaves control focus on the original control in the
  focused subtree because panes aren't proper controls themselves. This
  is also used to make sure we go back down the same path with the
  `child` movement.
- You can zoom a parent pane, and click between different zoomed
  sub-panes and it won't unzoom you until you use moveFocus or another
  action. This wasn't explicitly programmed behavior so it is probably
  buggy (I've quashed a couple at least). It is a natural consequence of
  showing multiple terminals and allowing you to focus a terminal and a
  parent separately, since changing the active pane directly does not
  unzoom. This also means there can be a disconnect between what pane is
  zoomed and what pane is active.

## Validation Steps Performed
Tested focus movement, swapping, moving panes, and zooming.

Closes #10733
This commit is contained in:
Schuyler Rosefield 2021-09-28 15:16:05 -04:00 committed by GitHub
parent 75e2b5fae7
commit 43297315ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 797 additions and 364 deletions

View file

@ -319,7 +319,9 @@
"previous", "previous",
"nextInOrder", "nextInOrder",
"previousInOrder", "previousInOrder",
"first" "first",
"parent",
"child"
], ],
"type": "string" "type": "string"
}, },
@ -562,7 +564,7 @@
"direction": { "direction": {
"$ref": "#/definitions/FocusDirection", "$ref": "#/definitions/FocusDirection",
"default": "left", "default": "left",
"description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane." "description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane, 'nextInOrder' or 'previousInOrder' to move to the next or previous pane, 'first' to focus the first pane, or 'parent' or 'child' to move up and down the tree."
} }
} }
} }
@ -579,7 +581,7 @@
"direction": { "direction": {
"$ref": "#/definitions/FocusDirection", "$ref": "#/definitions/FocusDirection",
"default": "left", "default": "left",
"description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane." "description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane, 'nextInOrder' or 'previousInOrder' to move to the next or previous pane, or 'first' to swap with the first pane."
} }
} }
} }

View file

@ -751,7 +751,7 @@ namespace TerminalAppLocalTests
}); });
VERIFY_SUCCEEDED(result); VERIFY_SUCCEEDED(result);
Log::Comment(L"Move focus. This will cause us to un-zoom."); Log::Comment(L"Move focus. We should still be zoomed.");
result = RunOnUIThread([&page]() { result = RunOnUIThread([&page]() {
// Set up action // Set up action
MoveFocusArgs args{ FocusDirection::Left }; MoveFocusArgs args{ FocusDirection::Left };
@ -761,7 +761,7 @@ namespace TerminalAppLocalTests
auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount());
VERIFY_IS_FALSE(firstTab->IsZoomed()); VERIFY_IS_TRUE(firstTab->IsZoomed());
}); });
VERIFY_SUCCEEDED(result); VERIFY_SUCCEEDED(result);
} }
@ -1357,7 +1357,8 @@ namespace TerminalAppLocalTests
Log::Comment(L"Color should be changed to the preview"); Log::Comment(L"Color should be changed to the preview");
VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground());
VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings); // And we should have stored a function to revert the change.
VERIFY_ARE_EQUAL(1u, page->_restorePreviewFuncs.size());
}); });
TestOnUIThread([&page]() { TestOnUIThread([&page]() {
@ -1383,7 +1384,8 @@ namespace TerminalAppLocalTests
Log::Comment(L"Color should be changed"); Log::Comment(L"Color should be changed");
VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground());
VERIFY_ARE_EQUAL(nullptr, page->_originalSettings); // After preview there should be no more restore functions to execute.
VERIFY_ARE_EQUAL(0u, page->_restorePreviewFuncs.size());
}); });
} }
@ -1428,7 +1430,6 @@ namespace TerminalAppLocalTests
Log::Comment(L"Color should be changed to the preview"); Log::Comment(L"Color should be changed to the preview");
VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground());
VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings);
}); });
TestOnUIThread([&page]() { TestOnUIThread([&page]() {
@ -1451,7 +1452,6 @@ namespace TerminalAppLocalTests
Log::Comment(L"Color should be the same as it originally was"); Log::Comment(L"Color should be the same as it originally was");
VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, controlSettings.DefaultBackground()); VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, controlSettings.DefaultBackground());
VERIFY_ARE_EQUAL(nullptr, page->_originalSettings);
}); });
} }
@ -1498,7 +1498,6 @@ namespace TerminalAppLocalTests
Log::Comment(L"Color should be changed to the preview"); Log::Comment(L"Color should be changed to the preview");
VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground());
VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings);
}); });
TestOnUIThread([&page]() { TestOnUIThread([&page]() {
@ -1522,7 +1521,6 @@ namespace TerminalAppLocalTests
Log::Comment(L"Color should be changed to the preview"); Log::Comment(L"Color should be changed to the preview");
VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground()); VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground());
VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings);
}); });
TestOnUIThread([&page]() { TestOnUIThread([&page]() {
@ -1548,7 +1546,6 @@ namespace TerminalAppLocalTests
Log::Comment(L"Color should be changed"); Log::Comment(L"Color should be changed");
VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground()); VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground());
VERIFY_ARE_EQUAL(nullptr, page->_originalSettings);
}); });
} }

View file

@ -67,41 +67,17 @@ namespace winrt::TerminalApp::implementation
// - <none> // - <none>
void TerminalPage::_EndPreviewColorScheme() void TerminalPage::_EndPreviewColorScheme()
{ {
// Get the focused control for (const auto& f : _restorePreviewFuncs)
if (const auto& activeControl{ _GetActiveControl() })
{ {
// Get the runtime settings of the focused control f();
const auto& controlSettings{ activeControl.Settings().as<TerminalSettings>() };
// Get the control's root settings, the ones that we actually
// assigned to it.
auto parentSettings{ controlSettings.GetParent() };
while (parentSettings.GetParent() != nullptr)
{
parentSettings = parentSettings.GetParent();
}
// If the root settings are the same as the ones we stashed,
// then reset the parent of the runtime settings to the stashed
// settings. This condition might be false if the settings
// hot-reloaded while the palette was open. In that case, we
// don't want to reset the settings to what they were _before_
// the hot-reload.
if (_originalSettings == parentSettings)
{
// Set the original settings as the parent of the control's settings
activeControl.Settings().as<TerminalSettings>().SetParent(_originalSettings);
}
activeControl.UpdateSettings();
} }
_originalSettings = nullptr; _restorePreviewFuncs.clear();
} }
// Method Description: // Method Description:
// - Preview handler for the SetColorScheme action. // - Preview handler for the SetColorScheme action.
// - This method will stash the settings of the current control in // - This method will stash functions to reset the settings of the selected controls in
// _originalSettings. Then it will create a new TerminalSettings object // _restorePreviewFuncs. Then it will create a new TerminalSettings object
// with only the properties from the ColorScheme set. It'll _insert_ a // with only the properties from the ColorScheme set. It'll _insert_ a
// TerminalSettings between the control's root settings (built from // TerminalSettings between the control's root settings (built from
// CascadiaSettings) and the control's runtime settings. That'll cause the // CascadiaSettings) and the control's runtime settings. That'll cause the
@ -112,33 +88,63 @@ namespace winrt::TerminalApp::implementation
// - <none> // - <none>
void TerminalPage::_PreviewColorScheme(const Settings::Model::SetColorSchemeArgs& args) void TerminalPage::_PreviewColorScheme(const Settings::Model::SetColorSchemeArgs& args)
{ {
// Get the focused control if (const auto& scheme{ _settings.GlobalSettings().ColorSchemes().TryLookup(args.SchemeName()) })
if (const auto& activeControl{ _GetActiveControl() })
{ {
if (const auto& scheme{ _settings.GlobalSettings().ColorSchemes().TryLookup(args.SchemeName()) }) // Clear the saved preview funcs because we don't need to add a restore each time
{ // the preview color changes, we only need to be able to restore the last one.
_restorePreviewFuncs.clear();
_ApplyToActiveControls([&](const auto& control) {
// Get the settings of the focused control and stash them // Get the settings of the focused control and stash them
const auto& controlSettings = activeControl.Settings().as<TerminalSettings>(); const auto& controlSettings = control.Settings().as<TerminalSettings>();
// Make sure to recurse up to the root - if you're doing // Make sure to recurse up to the root - if you're doing
// this while you're currently previewing a SetColorScheme // this while you're currently previewing a SetColorScheme
// action, then the parent of the control's settings is _the // action, then the parent of the control's settings is _the
// last preview TerminalSettings we inserted! We don't want // last preview TerminalSettings we inserted! We don't want
// to save that one! // to save that one!
_originalSettings = controlSettings.GetParent(); auto originalSettings = controlSettings.GetParent();
while (_originalSettings.GetParent() != nullptr) while (originalSettings.GetParent() != nullptr)
{ {
_originalSettings = _originalSettings.GetParent(); originalSettings = originalSettings.GetParent();
} }
// Create a new child for those settings // Create a new child for those settings
TerminalSettingsCreateResult fake{ _originalSettings }; TerminalSettingsCreateResult fake{ originalSettings };
const auto& childStruct = TerminalSettings::CreateWithParent(fake); const auto& childStruct = TerminalSettings::CreateWithParent(fake);
// Modify the child to have the applied color scheme // Modify the child to have the applied color scheme
childStruct.DefaultSettings().ApplyColorScheme(scheme); childStruct.DefaultSettings().ApplyColorScheme(scheme);
// Insert that new child as the parent of the control's settings // Insert that new child as the parent of the control's settings
controlSettings.SetParent(childStruct.DefaultSettings()); controlSettings.SetParent(childStruct.DefaultSettings());
activeControl.UpdateSettings(); control.UpdateSettings();
}
// Take a copy of the inputs, since they are pointers anyways.
_restorePreviewFuncs.emplace_back([=]() {
// Get the runtime settings of the focused control
const auto& controlSettings{ control.Settings().as<TerminalSettings>() };
// Get the control's root settings, the ones that we actually
// assigned to it.
auto parentSettings{ controlSettings.GetParent() };
while (parentSettings.GetParent() != nullptr)
{
parentSettings = parentSettings.GetParent();
}
// If the root settings are the same as the ones we stashed,
// then reset the parent of the runtime settings to the stashed
// settings. This condition might be false if the settings
// hot-reloaded while the palette was open. In that case, we
// don't want to reset the settings to what they were _before_
// the hot-reload.
if (originalSettings == parentSettings)
{
// Set the original settings as the parent of the control's settings
control.Settings().as<TerminalSettings>().SetParent(originalSettings);
}
control.UpdateSettings();
});
});
} }
} }

View file

@ -377,11 +377,10 @@ namespace winrt::TerminalApp::implementation
{ {
if (const auto& realArgs = args.ActionArgs().try_as<AdjustFontSizeArgs>()) if (const auto& realArgs = args.ActionArgs().try_as<AdjustFontSizeArgs>())
{ {
if (const auto& termControl{ _GetActiveControl() }) const auto res = _ApplyToActiveControls([&](auto& control) {
{ control.AdjustFontSize(realArgs.Delta());
termControl.AdjustFontSize(realArgs.Delta()); });
args.Handled(true); args.Handled(res);
}
} }
} }
@ -395,21 +394,19 @@ namespace winrt::TerminalApp::implementation
void TerminalPage::_HandleResetFontSize(const IInspectable& /*sender*/, void TerminalPage::_HandleResetFontSize(const IInspectable& /*sender*/,
const ActionEventArgs& args) const ActionEventArgs& args)
{ {
if (const auto& termControl{ _GetActiveControl() }) const auto res = _ApplyToActiveControls([](auto& control) {
{ control.ResetFontSize();
termControl.ResetFontSize(); });
args.Handled(true); args.Handled(res);
}
} }
void TerminalPage::_HandleToggleShaderEffects(const IInspectable& /*sender*/, void TerminalPage::_HandleToggleShaderEffects(const IInspectable& /*sender*/,
const ActionEventArgs& args) const ActionEventArgs& args)
{ {
if (const auto& termControl{ _GetActiveControl() }) const auto res = _ApplyToActiveControls([](auto& control) {
{ control.ToggleShaderEffects();
termControl.ToggleShaderEffects(); });
args.Handled(true); args.Handled(res);
}
} }
void TerminalPage::_HandleToggleFocusMode(const IInspectable& /*sender*/, void TerminalPage::_HandleToggleFocusMode(const IInspectable& /*sender*/,
@ -452,37 +449,33 @@ namespace winrt::TerminalApp::implementation
args.Handled(false); args.Handled(false);
if (const auto& realArgs = args.ActionArgs().try_as<SetColorSchemeArgs>()) if (const auto& realArgs = args.ActionArgs().try_as<SetColorSchemeArgs>())
{ {
if (const auto activeTab{ _GetFocusedTabImpl() }) if (const auto scheme = _settings.GlobalSettings().ColorSchemes().TryLookup(realArgs.SchemeName()))
{ {
if (auto activeControl = activeTab->GetActiveTerminalControl()) const auto res = _ApplyToActiveControls([&](auto& control) {
{ // Start by getting the current settings of the control
if (const auto scheme = _settings.GlobalSettings().ColorSchemes().TryLookup(realArgs.SchemeName())) auto controlSettings = control.Settings().as<TerminalSettings>();
auto parentSettings = controlSettings;
// Those are the _runtime_ settings however. What we
// need to do is:
//
// 1. Blow away any colors set in the runtime settings.
// 2. Apply the color scheme to the parent settings.
//
// 1 is important to make sure that the effects of
// something like `colortool` are cleared when setting
// the scheme.
if (controlSettings.GetParent() != nullptr)
{ {
// Start by getting the current settings of the control parentSettings = controlSettings.GetParent();
auto controlSettings = activeControl.Settings().as<TerminalSettings>();
auto parentSettings = controlSettings;
// Those are the _runtime_ settings however. What we
// need to do is:
//
// 1. Blow away any colors set in the runtime settings.
// 2. Apply the color scheme to the parent settings.
//
// 1 is important to make sure that the effects of
// something like `colortool` are cleared when setting
// the scheme.
if (controlSettings.GetParent() != nullptr)
{
parentSettings = controlSettings.GetParent();
}
// ApplyColorScheme(nullptr) will clear the old color scheme.
controlSettings.ApplyColorScheme(nullptr);
parentSettings.ApplyColorScheme(scheme);
activeControl.UpdateSettings();
args.Handled(true);
} }
}
// ApplyColorScheme(nullptr) will clear the old color scheme.
controlSettings.ApplyColorScheme(nullptr);
parentSettings.ApplyColorScheme(scheme);
control.UpdateSettings();
});
args.Handled(res);
} }
} }
} }
@ -896,11 +889,10 @@ namespace winrt::TerminalApp::implementation
{ {
if (const auto& realArgs = args.ActionArgs().try_as<ClearBufferArgs>()) if (const auto& realArgs = args.ActionArgs().try_as<ClearBufferArgs>())
{ {
if (const auto termControl{ _GetActiveControl() }) const auto res = _ApplyToActiveControls([&](auto& control) {
{ control.ClearBuffer(realArgs.Clear());
termControl.ClearBuffer(realArgs.Clear()); });
args.Handled(true); args.Handled(res);
}
} }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -40,7 +40,8 @@ enum class Borders : int
Top = 0x1, Top = 0x1,
Bottom = 0x2, Bottom = 0x2,
Left = 0x4, Left = 0x4,
Right = 0x8 Right = 0x8,
All = 0xF
}; };
DEFINE_ENUM_FLAG_OPERATORS(Borders); DEFINE_ENUM_FLAG_OPERATORS(Borders);
@ -58,7 +59,14 @@ public:
const winrt::Microsoft::Terminal::Control::TermControl& control, const winrt::Microsoft::Terminal::Control::TermControl& control,
const bool lastFocused = false); const bool lastFocused = false);
Pane(std::shared_ptr<Pane> first,
std::shared_ptr<Pane> second,
const SplitState splitType,
const float splitPosition,
const bool lastFocused = false);
std::shared_ptr<Pane> GetActivePane(); std::shared_ptr<Pane> GetActivePane();
winrt::Microsoft::Terminal::Control::TermControl GetLastFocusedTerminalControl();
winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl(); winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl();
winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile(); winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile();
@ -142,25 +150,43 @@ public:
// - true if the predicate returned true on any pane. // - true if the predicate returned true on any pane.
template<typename F> template<typename F>
//requires std::predicate<F, std::shared_ptr<Pane>> //requires std::predicate<F, std::shared_ptr<Pane>>
bool WalkTree(F f) auto WalkTree(F f) -> decltype(f(shared_from_this()))
{ {
if (f(shared_from_this())) using R = std::invoke_result_t<F, std::shared_ptr<Pane>>;
{ static constexpr auto IsVoid = std::is_void_v<R>;
return true;
}
if (!_IsLeaf()) if constexpr (IsVoid)
{ {
return _firstChild->WalkTree(f) || _secondChild->WalkTree(f); f(shared_from_this());
if (!_IsLeaf())
{
_firstChild->WalkTree(f);
_secondChild->WalkTree(f);
}
} }
else
{
if (f(shared_from_this()))
{
return true;
}
return false; if (!_IsLeaf())
{
return _firstChild->WalkTree(f) || _secondChild->WalkTree(f);
}
return false;
}
} }
void CollectTaskbarStates(std::vector<winrt::TerminalApp::TaskbarState>& states); void CollectTaskbarStates(std::vector<winrt::TerminalApp::TaskbarState>& states);
WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable>); WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable>);
DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
using gotFocusArgs = winrt::delegate<std::shared_ptr<Pane>, winrt::Windows::UI::Xaml::FocusState>;
DECLARE_EVENT(GotFocus, _GotFocusHandlers, gotFocusArgs);
DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>); DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler<bool>); DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler<bool>);
DECLARE_EVENT(Detached, _PaneDetachedHandlers, winrt::delegate<std::shared_ptr<Pane>>); DECLARE_EVENT(Detached, _PaneDetachedHandlers, winrt::delegate<std::shared_ptr<Pane>>);
@ -173,7 +199,8 @@ private:
struct LayoutSizeNode; struct LayoutSizeNode;
winrt::Windows::UI::Xaml::Controls::Grid _root{}; winrt::Windows::UI::Xaml::Controls::Grid _root{};
winrt::Windows::UI::Xaml::Controls::Border _border{}; winrt::Windows::UI::Xaml::Controls::Border _borderFirst{};
winrt::Windows::UI::Xaml::Controls::Border _borderSecond{};
winrt::Microsoft::Terminal::Control::TermControl _control{ nullptr }; winrt::Microsoft::Terminal::Control::TermControl _control{ nullptr };
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState _connectionState{ winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::NotConnected }; winrt::Microsoft::Terminal::TerminalConnection::ConnectionState _connectionState{ winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::NotConnected };
static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_focusedBorderBrush; static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_focusedBorderBrush;
@ -185,6 +212,7 @@ private:
float _desiredSplitPosition; float _desiredSplitPosition;
std::optional<uint32_t> _id; std::optional<uint32_t> _id;
std::weak_ptr<Pane> _parentChildPath{};
bool _lastActive{ false }; bool _lastActive{ false };
winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr }; winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr };
@ -205,6 +233,7 @@ private:
bool _IsLeaf() const noexcept; bool _IsLeaf() const noexcept;
bool _HasFocusedChild() const noexcept; bool _HasFocusedChild() const noexcept;
void _SetupChildCloseHandlers(); void _SetupChildCloseHandlers();
bool _HasChild(const std::shared_ptr<Pane> child);
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType, std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType,
const float splitSize, const float splitSize,
@ -232,6 +261,7 @@ private:
void _CloseChild(const bool closeFirst, const bool isDetaching); void _CloseChild(const bool closeFirst, const bool isDetaching);
winrt::fire_and_forget _CloseChildRoutine(const bool closeFirst); winrt::fire_and_forget _CloseChildRoutine(const bool closeFirst);
void _Focus();
void _FocusFirstChild(); void _FocusFirstChild();
void _ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/); void _ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
void _ControlWarningBellHandler(winrt::Windows::Foundation::IInspectable const& sender, void _ControlWarningBellHandler(winrt::Windows::Foundation::IInspectable const& sender,

View file

@ -190,6 +190,9 @@
<data name="CloseWindowWarningTitle" xml:space="preserve"> <data name="CloseWindowWarningTitle" xml:space="preserve">
<value>Do you want to close all tabs?</value> <value>Do you want to close all tabs?</value>
</data> </data>
<data name="MultiplePanes" xml:space="preserve">
<value>Multiple panes</value>
</data>
<data name="TabCloseSubMenu" xml:space="preserve"> <data name="TabCloseSubMenu" xml:space="preserve">
<value>Close...</value> <value>Close...</value>
</data> </data>

View file

@ -736,31 +736,32 @@ namespace winrt::TerminalApp::implementation
{ {
_UnZoomIfNeeded(); _UnZoomIfNeeded();
auto pane = terminalTab->GetActivePane();
if (const auto pane{ terminalTab->GetActivePane() }) if (const auto pane{ terminalTab->GetActivePane() })
{ {
if (const auto control{ pane->GetTerminalControl() }) if (pane->ContainsReadOnly())
{ {
if (control.ReadOnly()) ContentDialogResult warningResult = co_await _ShowCloseReadOnlyDialog();
// If the user didn't explicitly click on close tab - leave
if (warningResult != ContentDialogResult::Primary)
{ {
ContentDialogResult warningResult = co_await _ShowCloseReadOnlyDialog(); co_return;
// If the user didn't explicitly click on close tab - leave
if (warningResult != ContentDialogResult::Primary)
{
co_return;
}
// Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab
if (control.ReadOnly())
{
control.ToggleReadOnly();
}
} }
pane->Close(); // Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab
pane->WalkTree([](auto p) {
if (const auto control{ p->GetTerminalControl() })
{
if (control.ReadOnly())
{
control.ToggleReadOnly();
}
}
return false;
});
} }
pane->Close();
} }
} }
else if (auto index{ _GetFocusedTabIndex() }) else if (auto index{ _GetFocusedTabIndex() })

View file

@ -262,6 +262,26 @@ namespace winrt::TerminalApp::implementation
bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool _MovePane(const uint32_t tabIdx); bool _MovePane(const uint32_t tabIdx);
template<typename F>
bool _ApplyToActiveControls(F f)
{
if (const auto tab{ _GetFocusedTabImpl() })
{
if (const auto activePane = tab->GetActivePane())
{
activePane->WalkTree([&](auto p) {
if (const auto& control{ p->GetTerminalControl() })
{
f(control);
}
});
return true;
}
}
return false;
}
winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl();
std::optional<uint32_t> _GetFocusedTabIndex() const noexcept; std::optional<uint32_t> _GetFocusedTabIndex() const noexcept;
TerminalApp::TabBase _GetFocusedTab() const noexcept; TerminalApp::TabBase _GetFocusedTab() const noexcept;
@ -365,7 +385,7 @@ namespace winrt::TerminalApp::implementation
void _EndPreviewColorScheme(); void _EndPreviewColorScheme();
void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args); void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args);
winrt::Microsoft::Terminal::Settings::Model::Command _lastPreviewedCommand{ nullptr }; winrt::Microsoft::Terminal::Settings::Model::Command _lastPreviewedCommand{ nullptr };
winrt::Microsoft::Terminal::Settings::Model::TerminalSettings _originalSettings{ nullptr }; std::vector<std::function<void()>> _restorePreviewFuncs{};
HRESULT _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); HRESULT _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection);
void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);

View file

@ -67,8 +67,11 @@ namespace winrt::TerminalApp::implementation
_rootPane->FocusPane(firstId); _rootPane->FocusPane(firstId);
_activePane = _rootPane->GetActivePane(); _activePane = _rootPane->GetActivePane();
} }
// Set the active control // If the focused pane is a leaf, add it to the MRU panes
_mruPanes.insert(_mruPanes.begin(), _activePane->Id().value()); if (const auto id = _activePane->Id())
{
_mruPanes.insert(_mruPanes.begin(), id.value());
}
_Setup(); _Setup();
} }
@ -180,8 +183,8 @@ namespace winrt::TerminalApp::implementation
// Method Description: // Method Description:
// - Returns nullptr if no children of this tab were the last control to be // - Returns nullptr if no children of this tab were the last control to be
// focused, or the TermControl that _was_ the last control to be focused (if // focused, the active control of the current pane, or the last active child control
// there was one). // of the active pane if it is a parent.
// - This control might not currently be focused, if the tab itself is not // - This control might not currently be focused, if the tab itself is not
// currently focused. // currently focused.
// Arguments: // Arguments:
@ -193,7 +196,7 @@ namespace winrt::TerminalApp::implementation
{ {
if (_activePane) if (_activePane)
{ {
return _activePane->GetTerminalControl(); return _activePane->GetLastFocusedTerminalControl();
} }
return nullptr; return nullptr;
} }
@ -390,6 +393,10 @@ namespace winrt::TerminalApp::implementation
{ {
return _runtimeTabText; return _runtimeTabText;
} }
if (!_activePane->_IsLeaf())
{
return RS_(L"MultiplePanes");
}
const auto lastFocusedControl = GetActiveTerminalControl(); const auto lastFocusedControl = GetActiveTerminalControl();
return lastFocusedControl ? lastFocusedControl.Title() : L""; return lastFocusedControl ? lastFocusedControl.Title() : L"";
} }
@ -514,19 +521,14 @@ namespace winrt::TerminalApp::implementation
// either the first or second child, but this will always return the // either the first or second child, but this will always return the
// original pane first. // original pane first.
auto [original, newPane] = _activePane->Split(splitType, splitSize, profile, control); auto [original, newPane] = _activePane->Split(splitType, splitSize, profile, control);
// The active pane has an id if it is a leaf
if (activePaneId) if (activePaneId)
{ {
original->Id(activePaneId.value()); original->Id(activePaneId.value());
newPane->Id(_nextPaneId);
++_nextPaneId;
}
else
{
original->Id(_nextPaneId);
++_nextPaneId;
newPane->Id(_nextPaneId);
++_nextPaneId;
} }
newPane->Id(_nextPaneId);
++_nextPaneId;
_activePane = original; _activePane = original;
// Add a event handlers to the new panes' GotFocus event. When the pane // Add a event handlers to the new panes' GotFocus event. When the pane
@ -551,8 +553,8 @@ namespace winrt::TerminalApp::implementation
// - The removed pane, if the remove succeeded. // - The removed pane, if the remove succeeded.
std::shared_ptr<Pane> TerminalTab::DetachPane() std::shared_ptr<Pane> TerminalTab::DetachPane()
{ {
// if we only have one pane, remove it entirely // if we only have one pane, or the focused pane is the root, remove it
// and close this tab // entirely and close this tab
if (_rootPane == _activePane) if (_rootPane == _activePane)
{ {
return DetachRoot(); return DetachRoot();
@ -627,16 +629,12 @@ namespace winrt::TerminalApp::implementation
// Add the new pane as an automatic split on the active pane. // Add the new pane as an automatic split on the active pane.
auto first = _activePane->AttachPane(pane, SplitDirection::Automatic); auto first = _activePane->AttachPane(pane, SplitDirection::Automatic);
// under current assumptions this condition should always be true. // This will be true if the original _activePane is a leaf pane.
// If it is a parent pane then we don't want to set an ID on it.
if (previousId) if (previousId)
{ {
first->Id(previousId.value()); first->Id(previousId.value());
} }
else
{
first->Id(_nextPaneId);
++_nextPaneId;
}
// Update with event handlers on the new child. // Update with event handlers on the new child.
_activePane = first; _activePane = first;
@ -712,7 +710,10 @@ namespace winrt::TerminalApp::implementation
// throughout the entire tree. // throughout the entire tree.
if (const auto newFocus = _rootPane->NavigateDirection(_activePane, direction, _mruPanes)) if (const auto newFocus = _rootPane->NavigateDirection(_activePane, direction, _mruPanes))
{ {
// Mark that we want the active pane to changed
_changingActivePane = true;
const auto res = _rootPane->FocusPane(newFocus); const auto res = _rootPane->FocusPane(newFocus);
_changingActivePane = false;
if (_zoomedPane) if (_zoomedPane)
{ {
@ -735,11 +736,22 @@ namespace winrt::TerminalApp::implementation
// - true if two panes were swapped. // - true if two panes were swapped.
bool TerminalTab::SwapPane(const FocusDirection& direction) bool TerminalTab::SwapPane(const FocusDirection& direction)
{ {
// You cannot swap panes with the parent/child pane because of the
// circular reference.
if (direction == FocusDirection::Parent || direction == FocusDirection::Child)
{
return false;
}
// NOTE: This _must_ be called on the root pane, so that it can propagate // NOTE: This _must_ be called on the root pane, so that it can propagate
// throughout the entire tree. // throughout the entire tree.
if (auto neighbor = _rootPane->NavigateDirection(_activePane, direction, _mruPanes)) if (auto neighbor = _rootPane->NavigateDirection(_activePane, direction, _mruPanes))
{ {
return _rootPane->SwapPanes(_activePane, neighbor); // SwapPanes will refocus the terminal to make sure that it has focus
// even after moving.
_changingActivePane = true;
const auto res = _rootPane->SwapPanes(_activePane, neighbor);
_changingActivePane = false;
return res;
} }
return false; return false;
@ -747,7 +759,10 @@ namespace winrt::TerminalApp::implementation
bool TerminalTab::FocusPane(const uint32_t id) bool TerminalTab::FocusPane(const uint32_t id)
{ {
return _rootPane->FocusPane(id); _changingActivePane = true;
const auto res = _rootPane->FocusPane(id);
_changingActivePane = false;
return res;
} }
// Method Description: // Method Description:
@ -1040,7 +1055,7 @@ namespace winrt::TerminalApp::implementation
auto weakThis{ get_weak() }; auto weakThis{ get_weak() };
std::weak_ptr<Pane> weakPane{ pane }; std::weak_ptr<Pane> weakPane{ pane };
auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr<Pane> sender) { auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr<Pane> sender, WUX::FocusState focus) {
// Do nothing if the Tab's lifetime is expired or pane isn't new. // Do nothing if the Tab's lifetime is expired or pane isn't new.
auto tab{ weakThis.get() }; auto tab{ weakThis.get() };
@ -1048,8 +1063,20 @@ namespace winrt::TerminalApp::implementation
{ {
if (sender != tab->_activePane) if (sender != tab->_activePane)
{ {
tab->_UpdateActivePane(sender); auto senderIsChild = tab->_activePane->_HasChild(sender);
tab->_RecalculateAndApplyTabColor();
// Only move focus if we the program moved focus, or the
// user moved with their mouse. This is a problem because a
// pane isn't a control itself, and if we have the parent
// focused we are fine if the terminal control is focused,
// but we don't want to update the active pane.
if (!senderIsChild ||
(focus == WUX::FocusState::Programmatic && tab->_changingActivePane) ||
focus == WUX::FocusState::Pointer)
{
tab->_UpdateActivePane(sender);
tab->_RecalculateAndApplyTabColor();
}
} }
tab->_focusState = WUX::FocusState::Programmatic; tab->_focusState = WUX::FocusState::Programmatic;
// This tab has gained focus, remove the bell indicator if it is active // This tab has gained focus, remove the bell indicator if it is active
@ -1084,8 +1111,19 @@ namespace winrt::TerminalApp::implementation
tab->Content(tab->_rootPane->GetRootElement()); tab->Content(tab->_rootPane->GetRootElement());
tab->ExitZoom(); tab->ExitZoom();
} }
if (auto pane = weakPane.lock()) if (auto pane = weakPane.lock())
{ {
// When a parent pane is selected, but one of its children
// close out under it we still need to update title/focus information
// but the GotFocus handler will rightly see that the _activePane
// did not actually change. Triggering
if (pane != tab->_activePane && !tab->_activePane->_IsLeaf())
{
co_await winrt::resume_foreground(tab->Content().Dispatcher());
tab->_UpdateActivePane(tab->_activePane);
}
for (auto i = tab->_mruPanes.begin(); i != tab->_mruPanes.end(); ++i) for (auto i = tab->_mruPanes.begin(); i != tab->_mruPanes.end(); ++i)
{ {
if (*i == pane->Id()) if (*i == pane->Id())
@ -1303,11 +1341,13 @@ namespace winrt::TerminalApp::implementation
// - The tab's color, if any // - The tab's color, if any
std::optional<winrt::Windows::UI::Color> TerminalTab::GetTabColor() std::optional<winrt::Windows::UI::Color> TerminalTab::GetTabColor()
{ {
const auto currControlColor{ GetActiveTerminalControl().TabColor() };
std::optional<winrt::Windows::UI::Color> controlTabColor; std::optional<winrt::Windows::UI::Color> controlTabColor;
if (currControlColor != nullptr) if (const auto& control = GetActiveTerminalControl())
{ {
controlTabColor = currControlColor.Value(); if (const auto color = control.TabColor())
{
controlTabColor = color.Value();
}
} }
// A Tab's color will be the result of layering a variety of sources, // A Tab's color will be the result of layering a variety of sources,
@ -1598,6 +1638,11 @@ namespace winrt::TerminalApp::implementation
void TerminalTab::EnterZoom() void TerminalTab::EnterZoom()
{ {
// Clear the content first, because with parent focusing it is possible
// to zoom the root pane, but setting the content will not trigger the
// property changed event since it is the same and you would end up with
// an empty tab.
Content(nullptr);
_zoomedPane = _activePane; _zoomedPane = _activePane;
_rootPane->Maximize(_zoomedPane); _rootPane->Maximize(_zoomedPane);
// Update the tab header to show the magnifying glass // Update the tab header to show the magnifying glass
@ -1606,6 +1651,7 @@ namespace winrt::TerminalApp::implementation
} }
void TerminalTab::ExitZoom() void TerminalTab::ExitZoom()
{ {
Content(nullptr);
_rootPane->Restore(_zoomedPane); _rootPane->Restore(_zoomedPane);
_zoomedPane = nullptr; _zoomedPane = nullptr;
// Update the tab header to hide the magnifying glass // Update the tab header to hide the magnifying glass
@ -1620,13 +1666,34 @@ namespace winrt::TerminalApp::implementation
// Method Description: // Method Description:
// - Toggle read-only mode on the active pane // - Toggle read-only mode on the active pane
// - If a parent pane is selected, this will ensure that all children have
// the same read-only status.
void TerminalTab::TogglePaneReadOnly() void TerminalTab::TogglePaneReadOnly()
{ {
auto control = GetActiveTerminalControl(); auto hasReadOnly = false;
if (control) auto allReadOnly = true;
{ _activePane->WalkTree([&](auto p) {
control.ToggleReadOnly(); if (const auto& control{ p->GetTerminalControl() })
} {
hasReadOnly |= control.ReadOnly();
allReadOnly &= control.ReadOnly();
}
});
_activePane->WalkTree([&](auto p) {
if (const auto& control{ p->GetTerminalControl() })
{
// If all controls have the same read only state then just toggle
if (allReadOnly || !hasReadOnly)
{
control.ToggleReadOnly();
}
// otherwise set to all read only.
else if (!control.ReadOnly())
{
control.ToggleReadOnly();
}
}
});
} }
// Method Description: // Method Description:

View file

@ -139,6 +139,7 @@ namespace winrt::TerminalApp::implementation
bool _receivedKeyDown{ false }; bool _receivedKeyDown{ false };
bool _iconHidden{ false }; bool _iconHidden{ false };
bool _changingActivePane{ false };
winrt::hstring _runtimeTabText{}; winrt::hstring _runtimeTabText{};
bool _inRename{ false }; bool _inRename{ false };

View file

@ -469,6 +469,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{ {
_renderEngine->ToggleShaderEffects(); _renderEngine->ToggleShaderEffects();
} }
// Always redraw after toggling effects. This way even if the control
// does not have focus it will update immediately.
_renderer->TriggerRedrawAll();
} }
// Method Description: // Method Description:

View file

@ -298,6 +298,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return RS_(L"MoveFocusPreviousInOrder"); return RS_(L"MoveFocusPreviousInOrder");
case FocusDirection::First: case FocusDirection::First:
return RS_(L"MoveFocusFirstPane"); return RS_(L"MoveFocusFirstPane");
case FocusDirection::Parent:
return RS_(L"MoveFocusParentPane");
case FocusDirection::Child:
return RS_(L"MoveFocusChildPane");
} }
return winrt::hstring{ return winrt::hstring{

View file

@ -38,7 +38,9 @@ namespace Microsoft.Terminal.Settings.Model
Previous, Previous,
PreviousInOrder, PreviousInOrder,
NextInOrder, NextInOrder,
First First,
Parent,
Child
}; };
enum SplitDirection enum SplitDirection

View file

@ -258,6 +258,12 @@
<data name="MoveFocusFirstPane" xml:space="preserve"> <data name="MoveFocusFirstPane" xml:space="preserve">
<value>Move focus to the first pane</value> <value>Move focus to the first pane</value>
</data> </data>
<data name="MoveFocusParentPane" xml:space="preserve">
<value>Move focus to the parent pane</value>
</data>
<data name="MoveFocusChildPane" xml:space="preserve">
<value>Move focus to the child pane</value>
</data>
<data name="SwapPaneCommandKey" xml:space="preserve"> <data name="SwapPaneCommandKey" xml:space="preserve">
<value>Swap pane</value> <value>Swap pane</value>
</data> </data>

View file

@ -373,7 +373,7 @@ struct IntAsFloatPercentConversionTrait : ::Microsoft::Terminal::Settings::Model
// Possible FocusDirection values // Possible FocusDirection values
JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FocusDirection) JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FocusDirection)
{ {
JSON_MAPPINGS(8) = { JSON_MAPPINGS(10) = {
pair_type{ "left", ValueType::Left }, pair_type{ "left", ValueType::Left },
pair_type{ "right", ValueType::Right }, pair_type{ "right", ValueType::Right },
pair_type{ "up", ValueType::Up }, pair_type{ "up", ValueType::Up },
@ -382,6 +382,8 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FocusDirection)
pair_type{ "previousInOrder", ValueType::PreviousInOrder }, pair_type{ "previousInOrder", ValueType::PreviousInOrder },
pair_type{ "nextInOrder", ValueType::NextInOrder }, pair_type{ "nextInOrder", ValueType::NextInOrder },
pair_type{ "first", ValueType::First }, pair_type{ "first", ValueType::First },
pair_type{ "parent", ValueType::Parent },
pair_type{ "child", ValueType::Child },
}; };
}; };

View file

@ -354,6 +354,8 @@
{ "command": { "action": "moveFocus", "direction": "previousInOrder" } }, { "command": { "action": "moveFocus", "direction": "previousInOrder" } },
{ "command": { "action": "moveFocus", "direction": "nextInOrder" } }, { "command": { "action": "moveFocus", "direction": "nextInOrder" } },
{ "command": { "action": "moveFocus", "direction": "first" } }, { "command": { "action": "moveFocus", "direction": "first" } },
{ "command": { "action": "moveFocus", "direction": "parent" } },
{ "command": { "action": "moveFocus", "direction": "child" } },
{ "command": { "action": "swapPane", "direction": "down" } }, { "command": { "action": "swapPane", "direction": "down" } },
{ "command": { "action": "swapPane", "direction": "left" } }, { "command": { "action": "swapPane", "direction": "left" } },
{ "command": { "action": "swapPane", "direction": "right" } }, { "command": { "action": "swapPane", "direction": "right" } },

View file

@ -247,6 +247,7 @@ bool DxEngine::_HasTerminalEffects() const noexcept
void DxEngine::ToggleShaderEffects() void DxEngine::ToggleShaderEffects()
{ {
_terminalEffectsEnabled = !_terminalEffectsEnabled; _terminalEffectsEnabled = !_terminalEffectsEnabled;
_recreateDeviceRequested = true;
LOG_IF_FAILED(InvalidateAll()); LOG_IF_FAILED(InvalidateAll());
} }