diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 85121e065..18023d517 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -79,6 +79,7 @@ localtime lround LSHIFT memicmp +mptt mov msappx MULTIPLEUSE diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 2b078d00d..2c736dd2f 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -239,6 +239,7 @@ "identifyWindows", "moveFocus", "movePane", + "swapPane", "moveTab", "newTab", "newWindow", @@ -493,6 +494,23 @@ "type": "integer", "default": 0, "description": "Which tab to switch to, with the first being 0" + } + } + } + ], + "required": [ "index" ] + }, + "MovePaneAction": { + "description": "Arguments corresponding to a Move Pane Action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "movePane" }, + "index": { + "type": "integer", + "default": 0, + "description": "Which tab to move the pane to, with the first being 0" } } } @@ -516,13 +534,13 @@ ], "required": [ "direction" ] }, - "MovePaneAction": { - "description": "Arguments corresponding to a Move Pane Action", + "SwapPaneAction": { + "description": "Arguments corresponding to a Swap Pane Action", "allOf": [ { "$ref": "#/definitions/ShortcutAction" }, { "properties": { - "action": { "type": "string", "pattern": "movePane" }, + "action": { "type": "string", "pattern": "swapPane" }, "direction": { "$ref": "#/definitions/FocusDirection", "default": "left", @@ -972,6 +990,7 @@ { "$ref": "#/definitions/SwitchToTabAction" }, { "$ref": "#/definitions/MoveFocusAction" }, { "$ref": "#/definitions/MovePaneAction" }, + { "$ref": "#/definitions/SwapPaneAction" }, { "$ref": "#/definitions/ResizePaneAction" }, { "$ref": "#/definitions/SendInputAction" }, { "$ref": "#/definitions/SplitPaneAction" }, diff --git a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp index c5d853d32..8384690c4 100644 --- a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp +++ b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp @@ -56,7 +56,7 @@ namespace TerminalAppLocalTests TEST_METHOD(ParseComboCommandlineIntoArgs); TEST_METHOD(ParseFocusTabArgs); TEST_METHOD(ParseMoveFocusArgs); - TEST_METHOD(ParseMovePaneArgs); + TEST_METHOD(ParseSwapPaneArgs); TEST_METHOD(ParseArgumentsWithParsingTerminators); TEST_METHOD(ParseFocusPaneArgs); @@ -1208,14 +1208,9 @@ namespace TerminalAppLocalTests } } - void CommandlineTest::ParseMovePaneArgs() + void CommandlineTest::ParseSwapPaneArgs() { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:useShortForm", L"{false, true}") - END_TEST_METHOD_PROPERTIES() - - INIT_TEST_PROPERTY(bool, useShortForm, L"If true, use `mp` instead of `move-pane`"); - const wchar_t* subcommand = useShortForm ? L"mp" : L"move-pane"; + const wchar_t* subcommand = L"swap-pane"; { AppCommandlineArgs appArgs{}; @@ -1236,9 +1231,9 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction()); } @@ -1253,9 +1248,9 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction()); } @@ -1270,9 +1265,9 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Up, myArgs.Direction()); } @@ -1287,9 +1282,9 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Down, myArgs.Direction()); } @@ -1311,16 +1306,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction()); actionAndArgs = appArgs._startupActions.at(2); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - myArgs = actionAndArgs.Args().try_as(); + myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction()); } diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 9d7653dc8..7ae93e093 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -82,7 +82,7 @@ namespace TerminalAppLocalTests TEST_METHOD(MoveFocusFromZoomedPane); TEST_METHOD(CloseZoomedPane); - TEST_METHOD(MovePanes); + TEST_METHOD(SwapPanes); TEST_METHOD(NextMRUTab); TEST_METHOD(VerifyCommandPaletteTabSwitcherOrder); @@ -821,7 +821,7 @@ namespace TerminalAppLocalTests VERIFY_SUCCEEDED(result); } - void TabTests::MovePanes() + void TabTests::SwapPanes() { auto page = _commonSetup(); @@ -914,10 +914,10 @@ namespace TerminalAppLocalTests // ------------------- TestOnUIThread([&]() { // Set up action - MovePaneArgs args{ FocusDirection::Left }; + SwapPaneArgs args{ FocusDirection::Left }; ActionEventArgs eventArgs{ args }; - page->_HandleMovePane(nullptr, eventArgs); + page->_HandleSwapPane(nullptr, eventArgs); }); Sleep(250); @@ -945,10 +945,10 @@ namespace TerminalAppLocalTests // ------------------- TestOnUIThread([&]() { // Set up action - MovePaneArgs args{ FocusDirection::Up }; + SwapPaneArgs args{ FocusDirection::Up }; ActionEventArgs eventArgs{ args }; - page->_HandleMovePane(nullptr, eventArgs); + page->_HandleSwapPane(nullptr, eventArgs); }); Sleep(250); @@ -976,10 +976,10 @@ namespace TerminalAppLocalTests // ------------------- TestOnUIThread([&]() { // Set up action - MovePaneArgs args{ FocusDirection::Right }; + SwapPaneArgs args{ FocusDirection::Right }; ActionEventArgs eventArgs{ args }; - page->_HandleMovePane(nullptr, eventArgs); + page->_HandleSwapPane(nullptr, eventArgs); }); Sleep(250); @@ -1007,10 +1007,10 @@ namespace TerminalAppLocalTests // ------------------- TestOnUIThread([&]() { // Set up action - MovePaneArgs args{ FocusDirection::Down }; + SwapPaneArgs args{ FocusDirection::Down }; ActionEventArgs eventArgs{ args }; - page->_HandleMovePane(nullptr, eventArgs); + page->_HandleSwapPane(nullptr, eventArgs); }); Sleep(250); diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index f88017e1e..3960b6c38 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -143,6 +143,20 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleMovePane(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (args == nullptr) + { + args.Handled(false); + } + else if (const auto& realArgs = args.ActionArgs().try_as()) + { + auto moved = _MovePane(realArgs.TabIndex()); + args.Handled(moved); + } + } + void TerminalPage::_HandleSplitPane(const IInspectable& /*sender*/, const ActionEventArgs& args) { @@ -323,10 +337,10 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleMovePane(const IInspectable& /*sender*/, + void TerminalPage::_HandleSwapPane(const IInspectable& /*sender*/, const ActionEventArgs& args) { - if (const auto& realArgs = args.ActionArgs().try_as()) + if (const auto& realArgs = args.ActionArgs().try_as()) { if (realArgs.Direction() == FocusDirection::None) { @@ -335,7 +349,7 @@ namespace winrt::TerminalApp::implementation } else { - _MovePane(realArgs.Direction()); + _SwapPane(realArgs.Direction()); args.Handled(true); } } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index c820fa09b..5e7581d8f 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -193,6 +193,7 @@ void AppCommandlineArgs::_buildParser() _buildFocusTabParser(); _buildMoveFocusParser(); _buildMovePaneParser(); + _buildSwapPaneParser(); _buildFocusPaneParser(); } @@ -297,6 +298,43 @@ void AppCommandlineArgs::_buildSplitPaneParser() setupSubcommand(_newPaneCommand); setupSubcommand(_newPaneShort); } +// Method Description: +// - Adds the `move-pane` subcommand and related options to the commandline parser. +// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane` +// Arguments: +// - +// Return Value: +// - +void AppCommandlineArgs::_buildMovePaneParser() +{ + _movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc")); + _movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc")); + + auto setupSubcommand = [this](auto* subcommand) { + subcommand->add_option("-t,--tab", + _movePaneTabIndex, + RS_A(L"CmdMovePaneTabArgDesc")); + + // When ParseCommand is called, if this subcommand was provided, this + // callback function will be triggered on the same thread. We can be sure + // that `this` will still be safe - this function just lets us know this + // command was parsed. + subcommand->callback([&, this]() { + // Build the action from the values we've parsed on the commandline. + ActionAndArgs movePaneAction{}; + + if (_movePaneTabIndex >= 0) + { + movePaneAction.Action(ShortcutAction::MovePane); + MovePaneArgs args{ static_cast(_movePaneTabIndex) }; + movePaneAction.Args(args); + _startupActions.push_back(movePaneAction); + } + }); + }; + setupSubcommand(_movePaneCommand); + setupSubcommand(_movePaneShort); +} // Method Description: // - Adds the `focus-tab` subcommand and related options to the commandline parser. @@ -400,16 +438,14 @@ void AppCommandlineArgs::_buildMoveFocusParser() } // Method Description: -// - Adds the `move-pane` subcommand and related options to the commandline parser. -// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane` +// - Adds the `swap-pane` subcommand and related options to the commandline parser. // Arguments: // - // Return Value: // - -void AppCommandlineArgs::_buildMovePaneParser() +void AppCommandlineArgs::_buildSwapPaneParser() { - _movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc")); - _movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc")); + _swapPaneCommand = _app.add_subcommand("swap-pane", RS_A(L"CmdSwapPaneDesc")); auto setupSubcommand = [this](auto* subcommand) { std::map map = { @@ -420,8 +456,8 @@ void AppCommandlineArgs::_buildMovePaneParser() }; auto* directionOpt = subcommand->add_option("direction", - _movePaneDirection, - RS_A(L"CmdMovePaneDirectionArgDesc")); + _swapPaneDirection, + RS_A(L"CmdSwapPaneDirectionArgDesc")); directionOpt->transform(CLI::CheckedTransformer(map, CLI::ignore_case)); directionOpt->required(); @@ -430,12 +466,12 @@ void AppCommandlineArgs::_buildMovePaneParser() // that `this` will still be safe - this function just lets us know this // command was parsed. subcommand->callback([&, this]() { - if (_movePaneDirection != FocusDirection::None) + if (_swapPaneDirection != FocusDirection::None) { - MovePaneArgs args{ _movePaneDirection }; + SwapPaneArgs args{ _swapPaneDirection }; ActionAndArgs actionAndArgs{}; - actionAndArgs.Action(ShortcutAction::MovePane); + actionAndArgs.Action(ShortcutAction::SwapPane); actionAndArgs.Args(args); _startupActions.push_back(std::move(actionAndArgs)); @@ -443,8 +479,7 @@ void AppCommandlineArgs::_buildMovePaneParser() }); }; - setupSubcommand(_movePaneCommand); - setupSubcommand(_movePaneShort); + setupSubcommand(_swapPaneCommand); } // Method Description: @@ -625,6 +660,7 @@ bool AppCommandlineArgs::_noCommandsProvided() *_moveFocusShort || *_movePaneCommand || *_movePaneShort || + *_swapPaneCommand || *_focusPaneCommand || *_focusPaneShort || *_newPaneShort.subcommand || @@ -653,12 +689,13 @@ void AppCommandlineArgs::_resetStateToDefault() _splitPaneSize = 0.5f; _splitDuplicate = false; + _movePaneTabIndex = -1; _focusTabIndex = -1; _focusNextTab = false; _focusPrevTab = false; _moveFocusDirection = FocusDirection::None; - _movePaneDirection = FocusDirection::None; + _swapPaneDirection = FocusDirection::None; _focusPaneTarget = -1; diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 81dc564ba..598c3b8ed 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -84,6 +84,7 @@ private: CLI::App* _moveFocusShort; CLI::App* _movePaneCommand; CLI::App* _movePaneShort; + CLI::App* _swapPaneCommand; CLI::App* _focusPaneCommand; CLI::App* _focusPaneShort; @@ -97,7 +98,7 @@ private: bool _suppressApplicationTitle{ false }; winrt::Microsoft::Terminal::Settings::Model::FocusDirection _moveFocusDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; - winrt::Microsoft::Terminal::Settings::Model::FocusDirection _movePaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; + winrt::Microsoft::Terminal::Settings::Model::FocusDirection _swapPaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; // _commandline will contain the command line with which we'll be spawning a new terminal std::vector _commandline; @@ -107,6 +108,7 @@ private: bool _splitDuplicate{ false }; float _splitPaneSize{ 0.5f }; + int _movePaneTabIndex{ -1 }; int _focusTabIndex{ -1 }; bool _focusNextTab{ false }; bool _focusPrevTab{ false }; @@ -132,6 +134,7 @@ private: void _buildFocusTabParser(); void _buildMoveFocusParser(); void _buildMovePaneParser(); + void _buildSwapPaneParser(); void _buildFocusPaneParser(); bool _noCommandsProvided(); void _resetStateToDefault(); diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 45db24870..439f41313 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -220,7 +220,7 @@ bool Pane::ResizePane(const ResizeDirection& direction) // direction, we'll return false. This will indicate to our parent that they // should try and move the focus themselves. In this way, the focus can move // up and down the tree to the correct pane. -// - This method is _very_ similar to MovePane. Both are trying to find the +// - This method is _very_ similar to SwapPane. Both are trying to find the // right pane to move (focus) in a direction. // Arguments: // - direction: The direction to move the focus in. @@ -595,7 +595,7 @@ Pane::FocusNeighborSearch Pane::_FindFocusAndNeighbor(const FocusDirection& dire // - direction: The direction to move the focused pane in. // Return Value: // - true if we or a child handled this pane move request. -bool Pane::MovePane(const FocusDirection& direction) +bool Pane::SwapPane(const FocusDirection& direction) { // If we're a leaf, do nothing. We can't possibly swap anything. if (_IsLeaf()) @@ -614,7 +614,7 @@ bool Pane::MovePane(const FocusDirection& direction) // and its neighbor must necessarily be contained within the same child. if (!DirectionMatchesSplit(direction, _splitState)) { - return _firstChild->MovePane(direction) || _secondChild->MovePane(direction); + return _firstChild->SwapPane(direction) || _secondChild->SwapPane(direction); } // Since the direction is the same as our split, it is possible that we must @@ -1011,6 +1011,85 @@ void Pane::UpdateSettings(const TerminalSettingsCreateResult& settings, const GU } } +// Method Description: +// - Attempts to add the provided pane as a split of the current pane. +// Arguments: +// - pane: the new pane to add +// - splitType: How the pane should be attached +// Return Value: +// - the new reference to the child created from the current pane. +std::shared_ptr Pane::AttachPane(std::shared_ptr pane, SplitState splitType) +{ + // Splice the new pane into the tree + const auto [first, _] = _Split(splitType, .5, pane); + + // If the new pane has a child that was the focus, re-focus it + // to steal focus from the currently focused pane. + if (pane->_HasFocusedChild()) + { + pane->WalkTree([](auto p) { + if (p->_lastActive) + { + p->_FocusFirstChild(); + return true; + } + return false; + }); + } + + return first; +} + +// Method Description: +// - Attempts to find the parent of the target pane, +// if found remove the pane from the tree and return it. +// - If the removed pane was (or contained the focus) the first sibling will +// gain focus. +// Arguments: +// - pane: the pane to detach +// Return Value: +// - The removed pane, if found. +std::shared_ptr Pane::DetachPane(std::shared_ptr pane) +{ + // We can't remove a pane if we only have a reference to a leaf, even if we + // are the pane. + if (_IsLeaf()) + { + return nullptr; + } + + // Check if either of our children matches the search + const auto isFirstChild = _firstChild == pane; + const auto isSecondChild = _secondChild == pane; + + if (isFirstChild || isSecondChild) + { + // Keep a reference to the child we are removing + auto detached = isFirstChild ? _firstChild : _secondChild; + // Remove the child from the tree, replace the current node with the + // other child. + _CloseChild(isFirstChild); + + detached->_borders = Borders::None; + detached->_UpdateBorders(); + + // Trigger the detached event on each child + detached->WalkTree([](auto pane) { + pane->_PaneDetachedHandlers(pane); + return false; + }); + + return detached; + } + + if (const auto detached = _firstChild->DetachPane(pane)) + { + return detached; + } + + return _secondChild->DetachPane(pane); +} + // Method Description: // - Closes one of our children. In doing so, takes the control from the other // child, and makes this pane a leaf node again. @@ -1073,12 +1152,10 @@ void Pane::_CloseChild(const bool closeFirst) // them. _lastActive = _firstChild->_lastActive || _secondChild->_lastActive; - // Remove all the ui elements of our children. This'll make sure we can - // re-attach the TermControl to our Grid. - _firstChild->_root.Children().Clear(); - _secondChild->_root.Children().Clear(); - _firstChild->_border.Child(nullptr); - _secondChild->_border.Child(nullptr); + // Remove all the ui elements of the remaining child. This'll make sure + // we can re-attach the TermControl to our Grid. + remainingChild->_root.Children().Clear(); + remainingChild->_border.Child(nullptr); // Reset our UI: _root.Children().Clear(); @@ -1125,17 +1202,8 @@ void Pane::_CloseChild(const bool closeFirst) } else { - // Determine which border flag we gave to the child when we first split - // it, so that we can take just that flag away from them. - Borders clearBorderFlag = Borders::None; - if (_splitState == SplitState::Horizontal) - { - clearBorderFlag = closeFirst ? Borders::Top : Borders::Bottom; - } - else if (_splitState == SplitState::Vertical) - { - clearBorderFlag = closeFirst ? Borders::Left : Borders::Right; - } + // Find what borders need to persist after we close the child + auto remainingBorders = _GetCommonBorders(); // First stash away references to the old panes and their tokens const auto oldFirstToken = _firstClosedToken; @@ -1192,13 +1260,9 @@ void Pane::_CloseChild(const bool closeFirst) _root.Children().Append(_firstChild->GetRootElement()); _root.Children().Append(_secondChild->GetRootElement()); - // Take the flag away from the children that they inherited from their - // parent, and update their borders to visually match - WI_ClearAllFlags(_firstChild->_borders, clearBorderFlag); - WI_ClearAllFlags(_secondChild->_borders, clearBorderFlag); - _UpdateBorders(); - _firstChild->_UpdateBorders(); - _secondChild->_UpdateBorders(); + // Propagate the new borders down to the children. + _borders = remainingBorders; + _ApplySplitDefinitions(); // If the closed child was focused, transfer the focus to it's first sibling. if (closedChild->_lastActive) @@ -1757,7 +1821,8 @@ std::pair, std::shared_ptr> Pane::Split(SplitState s return { nullptr, nullptr }; } - return _Split(splitType, splitSize, profile, control); + auto newPane = std::make_shared(profile, control); + return _Split(splitType, splitSize, newPane); } // Method Description: @@ -1827,14 +1892,13 @@ SplitState Pane::_convertAutomaticSplitState(const SplitState& splitType) const // creates a new Pane to host the control, registers event handlers. // Arguments: // - splitType: what type of split we should create. -// - profile: The profile GUID to associate with the newly created pane. -// - control: A TermControl to use in the new pane. +// - splitSize: what fraction of the pane the new pane should get +// - newPane: the pane to add as a child // Return Value: // - The two newly created Panes std::pair, std::shared_ptr> Pane::_Split(SplitState splitType, const float splitSize, - const GUID& profile, - const TermControl& control) + std::shared_ptr newPane) { if (splitType == SplitState::None) { @@ -1874,7 +1938,7 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState _firstChild->_connectionState = std::exchange(_connectionState, ConnectionState::NotConnected); _profile = std::nullopt; _control = { nullptr }; - _secondChild = std::make_shared(profile, control); + _secondChild = newPane; _CreateRowColDefinitions(); @@ -2574,3 +2638,4 @@ void Pane::CollectTaskbarStates(std::vector& s DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate>); DEFINE_EVENT(Pane, LostFocus, _LostFocusHandlers, winrt::delegate>); DEFINE_EVENT(Pane, PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); +DEFINE_EVENT(Pane, Detached, _PaneDetachedHandlers, winrt::delegate>); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 34d696340..be51dfcad 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -29,6 +29,11 @@ namespace TerminalAppLocalTests class TabTests; }; +namespace winrt::TerminalApp::implementation +{ + struct TerminalTab; +} + enum class Borders : int { None = 0x0, @@ -63,7 +68,7 @@ public: void Relayout(); bool ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); - bool MovePane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool SwapPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool SwapPanes(std::shared_ptr first, std::shared_ptr second); std::pair, std::shared_ptr> Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, @@ -81,6 +86,10 @@ public: void Shutdown(); void Close(); + std::shared_ptr AttachPane(std::shared_ptr pane, + winrt::Microsoft::Terminal::Settings::Model::SplitState splitType); + std::shared_ptr DetachPane(std::shared_ptr pane); + int GetLeafPaneCount() const noexcept; void Maximize(std::shared_ptr zoomedPane); @@ -93,12 +102,38 @@ public: bool ContainsReadOnly() const; + // Method Description: + // - A helper method for ad-hoc recursion on a pane tree. Walks the pane + // tree, calling a predicate on each pane in a depth-first pattern. + // - If the predicate returns true, recursion is stopped early. + // Arguments: + // - f: The function to be applied to each pane. + // Return Value: + // - true if the predicate returned true on any pane. + template + //requires std::predicate> + bool WalkTree(F f) + { + if (f(shared_from_this())) + { + return true; + } + + if (!_IsLeaf()) + { + return _firstChild->WalkTree(f) || _secondChild->WalkTree(f); + } + + return false; + } + void CollectTaskbarStates(std::vector& states); WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler); DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate>); DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate>); DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); + DECLARE_EVENT(Detached, _PaneDetachedHandlers, winrt::delegate>); private: struct PanePoint; @@ -143,8 +178,7 @@ private: std::pair, std::shared_ptr> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, const float splitSize, - const GUID& profile, - const winrt::Microsoft::Terminal::Control::TermControl& control); + std::shared_ptr newPane); void _CreateRowColDefinitions(); void _ApplySplitDefinitions(); @@ -274,5 +308,6 @@ private: void _AssignChildNode(std::unique_ptr& nodeField, const LayoutSizeNode* const newNode); }; + friend struct winrt::TerminalApp::implementation::TerminalTab; friend class ::TerminalAppLocalTests::TabTests; }; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 5f325b1a6..a64a239df 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -282,6 +282,16 @@ Move focus the tab at the given index + + Move focused pane to the tab at the given index + + + Move focused pane to another tab + + + An alias for the "move-pane" subcommand. + {Locked="\"move-pane\""} + Specify the size as a percentage of the parent pane. Valid values are between (0,1), exclusive. @@ -359,14 +369,10 @@ The direction to move focus in - + Swap the focused pane with the adjacent pane in the specified direction - - An alias for the "move-pane" subcommand. - {Locked="\"move-pane\""} - - + The direction to move the focused pane in diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index dd9e9dc96..62d67d260 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -95,45 +95,12 @@ namespace winrt::TerminalApp::implementation CATCH_RETURN(); // Method Description: - // - Creates a new tab with the given settings. If the tab bar is not being - // currently displayed, it will be shown. + // - Sets up state, event handlers, etc on a tab object that was just made. // Arguments: - // - profileGuid: ID to use to lookup profile settings for this connection - // - settings: the TerminalSettings object to use to create the TerminalControl with. - // - existingConnection: optionally receives a connection from the outside world instead of attempting to create one - void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, const TerminalSettingsCreateResult& settings, TerminalConnection::ITerminalConnection existingConnection) + // - newTabImpl: the uninitialized tab. + void TerminalPage::_InitializeTab(winrt::com_ptr newTabImpl) { - // Initialize the new tab - // Create a connection based on the values in our settings object if we weren't given one. - auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profileGuid, settings.DefaultSettings()); - - // If we had an `existingConnection`, then this is an inbound handoff from somewhere else. - // We need to tell it about our size information so it can match the dimensions of what - // we are about to present. - if (existingConnection) - { - connection.Resize(settings.DefaultSettings().InitialRows(), settings.DefaultSettings().InitialCols()); - } - - TerminalConnection::ITerminalConnection debugConnection{ nullptr }; - if (_settings.GlobalSettings().DebugFeaturesEnabled()) - { - const CoreWindow window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - if (bothAltsPressed) - { - std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); - } - } - - // Give term control a child of the settings so that any overrides go in the child - // This way, when we do a settings reload we just update the parent and the overrides remain - auto term = _InitControl(settings, connection); - - auto newTabImpl = winrt::make_self(profileGuid, term); + newTabImpl->Initialize(); // Add the new tab to the list of our tabs. _tabs.Append(*newTabImpl); @@ -146,7 +113,7 @@ namespace winrt::TerminalApp::implementation _UpdateTabIndices(); // Hookup our event handlers to the new terminal - _RegisterTerminalEvents(term, *newTabImpl); + _RegisterTabEvents(*newTabImpl); // Don't capture a strong ref to the tab. If the tab is removed as this // is called, we don't really care anymore about handling the event. @@ -208,10 +175,13 @@ namespace winrt::TerminalApp::implementation _tabView.TabItems().Append(tabViewItem); // Set this tab's icon to the icon from the user's profile - const auto profile = _settings.FindProfile(profileGuid); - if (profile != nullptr && !profile.Icon().empty()) + if (const auto profileGuid = newTabImpl->GetFocusedProfile()) { - newTabImpl->UpdateIcon(profile.Icon()); + const auto profile = _settings.FindProfile(profileGuid.value()); + if (profile != nullptr && !profile.Icon().empty()) + { + newTabImpl->UpdateIcon(profile.Icon()); + } } tabViewItem.PointerReleased({ this, &TerminalPage::_OnTabClick }); @@ -244,19 +214,73 @@ namespace winrt::TerminalApp::implementation } }); - if (debugConnection) // this will only be set if global debugging is on and tap is active - { - auto newControl = _InitControl(settings, debugConnection); - _RegisterTerminalEvents(newControl, *newTabImpl); - // Split (auto) with the debug tap. - newTabImpl->SplitPane(SplitState::Automatic, 0.5f, profileGuid, newControl); - } - // This kicks off TabView::SelectionChanged, in response to which // we'll attach the terminal's Xaml control to the Xaml root. _tabView.SelectedItem(tabViewItem); } + // Method Description: + // - Create a new tab using a specified pane as the root. + // Arguments: + // - pane: The pane to use as the root. + void TerminalPage::_CreateNewTabFromPane(std::shared_ptr pane) + { + auto newTabImpl = winrt::make_self(pane); + _InitializeTab(newTabImpl); + } + + // Method Description: + // - Creates a new tab with the given settings. If the tab bar is not being + // currently displayed, it will be shown. + // Arguments: + // - profileGuid: ID to use to lookup profile settings for this connection + // - settings: the TerminalSettings object to use to create the TerminalControl with. + // - existingConnection: optionally receives a connection from the outside world instead of attempting to create one + void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, const TerminalSettingsCreateResult& settings, TerminalConnection::ITerminalConnection existingConnection) + { + // Initialize the new tab + // Create a connection based on the values in our settings object if we weren't given one. + auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profileGuid, settings.DefaultSettings()); + + // If we had an `existingConnection`, then this is an inbound handoff from somewhere else. + // We need to tell it about our size information so it can match the dimensions of what + // we are about to present. + if (existingConnection) + { + connection.Resize(settings.DefaultSettings().InitialRows(), settings.DefaultSettings().InitialCols()); + } + + TerminalConnection::ITerminalConnection debugConnection{ nullptr }; + if (_settings.GlobalSettings().DebugFeaturesEnabled()) + { + const CoreWindow window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + if (bothAltsPressed) + { + std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); + } + } + + // Give term control a child of the settings so that any overrides go in the child + // This way, when we do a settings reload we just update the parent and the overrides remain + auto term = _InitControl(settings, connection); + + auto newTabImpl = winrt::make_self(profileGuid, term); + _RegisterTerminalEvents(term); + _InitializeTab(newTabImpl); + + if (debugConnection) // this will only be set if global debugging is on and tap is active + { + auto newControl = _InitControl(settings, debugConnection); + _RegisterTerminalEvents(newControl); + // Split (auto) with the debug tap. + newTabImpl->SplitPane(SplitState::Automatic, 0.5f, profileGuid, newControl); + } + } + // Method Description: // - Get the icon of the currently focused terminal control, and set its // tab's icon to that icon. diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 4d49779ad..67f30992c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -980,11 +980,9 @@ namespace winrt::TerminalApp::implementation // handle. This includes: // * the Copy and Paste events, for setting and retrieving clipboard data // on the right thread - // * the TitleChanged event, for changing the text of the tab // Arguments: // - term: The newly created TermControl to connect the events for - // - hostingTab: The Tab that's hosting this TermControl instance - void TerminalPage::_RegisterTerminalEvents(TermControl term, TerminalTab& hostingTab) + void TerminalPage::_RegisterTerminalEvents(TermControl term) { term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler }); @@ -999,10 +997,20 @@ namespace winrt::TerminalApp::implementation term.HidePointerCursor({ get_weak(), &TerminalPage::_HidePointerCursorHandler }); term.RestorePointerCursor({ get_weak(), &TerminalPage::_RestorePointerCursorHandler }); + // Add an event handler for when the terminal or tab wants to set a + // progress indicator on the taskbar + term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); + } - // Bind Tab events to the TermControl and the Tab's Pane - hostingTab.Initialize(term); - + // Method Description: + // - Connects event handlers to the TerminalTab for events that we want to + // handle. This includes: + // * the TitleChanged event, for changing the text of the tab + // * the Color{Selected,Cleared} events to change the color of a tab. + // Arguments: + // - hostingTab: The Tab that's hosting this TermControl instance + void TerminalPage::_RegisterTabEvents(TerminalTab& hostingTab) + { auto weakTab{ hostingTab.get_weak() }; auto weakThis{ get_weak() }; // PropertyChanged is the generic mechanism by which the Tab @@ -1054,7 +1062,6 @@ namespace winrt::TerminalApp::implementation // Add an event handler for when the terminal or tab wants to set a // progress indicator on the taskbar hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); - term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); // TODO GH#3327: Once we support colorizing the NewTab button based on // the color of the tab, we'll want to make sure to call @@ -1115,17 +1122,17 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Attempt to swap the positions of the focused pane with another pane. - // See Pane::MovePane for details. + // See Pane::SwapPane for details. // Arguments: // - direction: The direction to move the focused pane in. // Return Value: // - - void TerminalPage::_MovePane(const FocusDirection& direction) + void TerminalPage::_SwapPane(const FocusDirection& direction) { if (const auto terminalTab{ _GetFocusedTabImpl() }) { _UnZoomIfNeeded(); - terminalTab->MovePane(direction); + terminalTab->SwapPane(direction); } } @@ -1188,6 +1195,52 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - Moves the currently active pane on the currently active tab to the + // specified tab. If the tab index is greater than the number of + // tabs, then a new tab will be created for the pane. Similarly, if a pane + // is the last remaining pane on a tab, that tab will be closed upon moving. + // Arguments: + // - tabIdx: The target tab index. + bool TerminalPage::_MovePane(const uint32_t tabIdx) + { + auto focusedTab{ _GetFocusedTabImpl() }; + + if (!focusedTab) + { + return false; + } + + // If we are trying to move from the current tab to the current tab do nothing. + if (_GetFocusedTabIndex() == tabIdx) + { + return false; + } + + // Moving the pane from the current tab might close it, so get the next + // tab before its index changes. + if (_tabs.Size() > tabIdx) + { + auto targetTab = _GetTerminalTabImpl(_tabs.GetAt(tabIdx)); + // if the selected tab is not a host of terminals (e.g. settings) + // don't attempt to add a pane to it. + if (!targetTab) + { + return false; + } + auto pane = focusedTab->DetachPane(); + targetTab->AttachPane(pane); + _SetFocusedTab(*targetTab); + } + else + { + auto pane = focusedTab->DetachPane(); + _CreateNewTabFromPane(pane); + } + + return true; + } + // Method Description: // - Split the focused pane either horizontally or vertically, and place the // given TermControl into the newly created pane. @@ -1306,7 +1359,7 @@ namespace winrt::TerminalApp::implementation auto newControl = _InitControl(controlSettings, controlConnection); // Hookup our event handlers to the new terminal - _RegisterTerminalEvents(newControl, tab); + _RegisterTerminalEvents(newControl); _UnZoomIfNeeded(); diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 6ec7bd7a0..c7996bb1b 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -188,6 +188,7 @@ namespace winrt::TerminalApp::implementation void _CreateNewTabFlyout(); void _OpenNewTabDropdown(); HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + void _CreateNewTabFromPane(std::shared_ptr pane); void _CreateNewTabFromSettings(GUID profileGuid, const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(GUID profileGuid, Microsoft::Terminal::Settings::Model::TerminalSettings settings); @@ -225,7 +226,9 @@ namespace winrt::TerminalApp::implementation void _RemoveTab(const winrt::TerminalApp::TabBase& tab); winrt::fire_and_forget _RemoveTabs(const std::vector tabs); - void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term, TerminalTab& hostingTab); + void _InitializeTab(winrt::com_ptr newTabImpl); + void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); + void _RegisterTabEvents(TerminalTab& hostingTab); void _DismissTabContextMenus(); void _FocusCurrentTab(const bool focusAlways); @@ -236,7 +239,8 @@ namespace winrt::TerminalApp::implementation void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); bool _SelectTab(uint32_t tabIndex); bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); - void _MovePane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + void _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _MovePane(const uint32_t tabIdx); winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); std::optional _GetFocusedTabIndex() const noexcept; diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 48cf05d33..7896cd851 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -30,14 +30,61 @@ namespace winrt::TerminalApp::implementation _rootPane = std::make_shared(profile, control, true); _rootPane->Id(_nextPaneId); + _activePane = _rootPane; _mruPanes.insert(_mruPanes.begin(), _nextPaneId); ++_nextPaneId; - _rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) { + _Setup(); + } + + TerminalTab::TerminalTab(std::shared_ptr rootPane) + { + _rootPane = rootPane; + _activePane = nullptr; + + auto firstId = _nextPaneId; + + _rootPane->WalkTree([&](std::shared_ptr pane) { + // update the IDs on each pane + if (pane->_IsLeaf()) + { + pane->Id(_nextPaneId); + _nextPaneId++; + } + // Try to find the pane marked active (if it exists) + if (pane->_lastActive) + { + _activePane = pane; + } + + return false; + }); + + // In case none of the panes were already marked as the focus, just + // focus the first one. + if (_activePane == nullptr) + { + _rootPane->FocusPane(firstId); + _activePane = _rootPane->GetActivePane(); + } + // Set the active control + _mruPanes.insert(_mruPanes.begin(), _activePane->Id().value()); + + _Setup(); + } + + // Method Description: + // - Shared setup for the constructors. Assumed that _rootPane has been set. + // Arguments: + // - + // Return Value: + // - + void TerminalTab::_Setup() + { + _rootClosedToken = _rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) { _ClosedHandlers(nullptr, nullptr); }); - _activePane = _rootPane; Content(_rootPane->GetRootElement()); _MakeTabViewItem(); @@ -144,19 +191,31 @@ namespace winrt::TerminalApp::implementation // that was last focused. TermControl TerminalTab::GetActiveTerminalControl() const { - return _activePane->GetTerminalControl(); + if (_activePane) + { + return _activePane->GetTerminalControl(); + } + return nullptr; } // Method Description: // - Called after construction of a Tab object to bind event handlers to its - // associated Pane and TermControl object + // associated Pane and TermControl objects // Arguments: - // - control: reference to the TermControl object to bind event to + // - // Return Value: // - - void TerminalTab::Initialize(const TermControl& control) + void TerminalTab::Initialize() { - _BindEventHandlers(control); + _rootPane->WalkTree([&](std::shared_ptr pane) { + // Attach event handlers to each new pane + _AttachEventHandlersToPane(pane); + if (auto control = pane->GetTerminalControl()) + { + _AttachEventHandlersToControl(pane->Id().value(), control); + } + return false; + }); } // Method Description: @@ -203,19 +262,6 @@ namespace winrt::TerminalApp::implementation return _activePane->GetFocusedProfile(); } - // Method Description: - // - Called after construction of a Tab object to bind event handlers to its - // associated Pane and TermControl object - // Arguments: - // - control: reference to the TermControl object to bind event to - // Return Value: - // - - void TerminalTab::_BindEventHandlers(const TermControl& control) noexcept - { - _AttachEventHandlersToPane(_rootPane); - _AttachEventHandlersToControl(control); - } - // Method Description: // - Attempts to update the settings of this tab's tree of panes. // Arguments: @@ -425,10 +471,10 @@ namespace winrt::TerminalApp::implementation ++_nextPaneId; } _activePane = first; - _AttachEventHandlersToControl(control); // Add a event handlers to the new panes' GotFocus event. When the pane // gains focus, we'll mark it as the new active pane. + _AttachEventHandlersToControl(second->Id().value(), control); _AttachEventHandlersToPane(first); _AttachEventHandlersToPane(second); @@ -439,11 +485,121 @@ namespace winrt::TerminalApp::implementation _UpdateActivePane(second); } + // Method Description: + // - Removes the currently active pane from this tab. If that was the only + // remaining pane, then the entire tab is closed as well. + // Arguments: + // - + // Return Value: + // - The removed pane. + std::shared_ptr TerminalTab::DetachPane() + { + // if we only have one pane, remove it entirely + // and close this tab + if (_rootPane == _activePane) + { + return DetachRoot(); + } + + // Attempt to remove the active pane from the tree + if (const auto pane = _rootPane->DetachPane(_activePane)) + { + // Just make sure that the remaining pane is marked active + _UpdateActivePane(_rootPane->GetActivePane()); + + return pane; + } + + return nullptr; + } + + // Method Description: + // - Closes this tab and returns the root pane to be used elsewhere. + // Arguments: + // - + // Return Value: + // - The root pane. + std::shared_ptr TerminalTab::DetachRoot() + { + // remove the closed event handler since we are closing the tab + // manually. + _rootPane->Closed(_rootClosedToken); + auto p = _rootPane; + p->WalkTree([](auto pane) { + pane->_PaneDetachedHandlers(pane); + return false; + }); + + // Clean up references and close the tab + _rootPane = nullptr; + _activePane = nullptr; + Content(nullptr); + _ClosedHandlers(nullptr, nullptr); + + return p; + } + + // Method Description: + // - Add an arbitrary pane to this tab. This will be added as a split on the + // currently active pane. + // Arguments: + // - pane: The pane to add. + // Return Value: + // - + void TerminalTab::AttachPane(std::shared_ptr pane) + { + // Add the new event handlers to the new pane(s) + // and update their ids. + pane->WalkTree([&](auto p) { + _AttachEventHandlersToPane(p); + if (p->_IsLeaf()) + { + p->Id(_nextPaneId); + _nextPaneId++; + } + if (auto control = p->GetTerminalControl()) + { + _AttachEventHandlersToControl(p->Id().value(), control); + } + return false; + }); + + // pass the old id to the new child + const auto previousId = _activePane->Id(); + + // Add the new pane as an automatic split on the active pane. + auto first = _activePane->AttachPane(pane, SplitState::Automatic); + + // under current assumptions this condition should always be true. + if (previousId) + { + first->Id(previousId.value()); + } + else + { + first->Id(_nextPaneId); + ++_nextPaneId; + } + + // Update with event handlers on the new child. + _activePane = first; + _AttachEventHandlersToPane(first); + + // Make sure that we have the right pane set as the active pane + pane->WalkTree([&](auto p) { + if (p->_lastActive) + { + _UpdateActivePane(p); + return true; + } + return false; + }); + } + // Method Description: // - Find the currently active pane, and then switch the split direction of // its parent. E.g. switch from Horizontal to Vertical. // Return Value: - // - void TerminalTab::ToggleSplitOrientation() { @@ -520,7 +676,7 @@ namespace winrt::TerminalApp::implementation // - direction: The direction to move the pane in. // Return Value: // - - void TerminalTab::MovePane(const FocusDirection& direction) + void TerminalTab::SwapPane(const FocusDirection& direction) { if (direction == FocusDirection::Previous) { @@ -537,7 +693,7 @@ namespace winrt::TerminalApp::implementation { // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. - _rootPane->MovePane(direction); + _rootPane->SwapPane(direction); } } @@ -550,7 +706,10 @@ namespace winrt::TerminalApp::implementation // - Prepares this tab for being removed from the UI hierarchy by shutting down all active connections. void TerminalTab::Shutdown() { - _rootPane->Shutdown(); + if (_rootPane) + { + _rootPane->Shutdown(); + } } // Method Description: @@ -595,6 +754,34 @@ namespace winrt::TerminalApp::implementation _headerControl.BeginRename(); } + // Method Description: + // - Removes any event handlers set by the tab on the given pane's control. + // The pane's ID is the most stable identifier for a given control, because + // the control itself doesn't have a particular ID and its pointer is + // unstable since it is moved when panes split. + // Arguments: + // - paneId: The ID of the pane that contains the given control. + // - control: the control to remove events from. + // Return Value: + // - + void TerminalTab::_DetachEventHandlersFromControl(const uint32_t paneId, const TermControl& control) + { + auto it = _controlEvents.find(paneId); + if (it != _controlEvents.end()) + { + auto& events = it->second; + + control.TitleChanged(events.titleToken); + control.FontSizeChanged(events.fontToken); + control.TabColorChanged(events.colorToken); + control.SetTaskbarProgress(events.taskbarToken); + control.ReadOnlyChanged(events.readOnlyToken); + control.FocusFollowMouseRequested(events.focusToken); + + _controlEvents.erase(paneId); + } + } + // Method Description: // - Register any event handlers that we may need with the given TermControl. // This should be called on each and every TermControl that we add to the tree @@ -602,15 +789,17 @@ namespace winrt::TerminalApp::implementation // * notify us when the control's title changed, so we can update our own // title (if necessary) // Arguments: + // - paneId: the ID of the pane that this control belongs to. // - control: the TermControl to add events to. // Return Value: // - - void TerminalTab::_AttachEventHandlersToControl(const TermControl& control) + void TerminalTab::_AttachEventHandlersToControl(const uint32_t paneId, const TermControl& control) { auto weakThis{ get_weak() }; auto dispatcher = TabViewItem().Dispatcher(); + ControlEventTokens events{}; - control.TitleChanged([weakThis](auto&&, auto&&) { + events.titleToken = control.TitleChanged([weakThis](auto&&, auto&&) { // Check if Tab's lifetime has expired if (auto tab{ weakThis.get() }) { @@ -625,16 +814,16 @@ namespace winrt::TerminalApp::implementation // On the latter event, we tell the root pane to resize itself so that its descendants // (including ourself) can properly snap to character grids. In future, we may also // want to do that on regular font changes. - control.FontSizeChanged([this](const int /* fontWidth */, - const int /* fontHeight */, - const bool isInitialChange) { + events.fontToken = control.FontSizeChanged([this](const int /* fontWidth */, + const int /* fontHeight */, + const bool isInitialChange) { if (isInitialChange) { _rootPane->Relayout(); } }); - control.TabColorChanged([weakThis](auto&&, auto&&) { + events.colorToken = control.TabColorChanged([weakThis](auto&&, auto&&) { if (auto tab{ weakThis.get() }) { // The control's tabColor changed, but it is not necessarily the @@ -644,7 +833,7 @@ namespace winrt::TerminalApp::implementation } }); - control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto &&) -> winrt::fire_and_forget { + events.taskbarToken = control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto &&) -> winrt::fire_and_forget { co_await winrt::resume_foreground(dispatcher); // Check if Tab's lifetime has expired if (auto tab{ weakThis.get() }) @@ -653,14 +842,14 @@ namespace winrt::TerminalApp::implementation } }); - control.ReadOnlyChanged([weakThis](auto&&, auto&&) { + events.readOnlyToken = control.ReadOnlyChanged([weakThis](auto&&, auto&&) { if (auto tab{ weakThis.get() }) { tab->_RecalculateAndApplyReadOnly(); } }); - control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) { + events.focusToken = control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) { if (const auto tab{ weakThis.get() }) { if (tab->_focusState != FocusState::Unfocused) @@ -672,6 +861,8 @@ namespace winrt::TerminalApp::implementation } } }); + + _controlEvents[paneId] = events; } // Method Description: @@ -689,7 +880,10 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::TaskbarState TerminalTab::GetCombinedTaskbarState() const { std::vector states; - _rootPane->CollectTaskbarStates(states); + if (_rootPane) + { + _rootPane->CollectTaskbarStates(states); + } return states.empty() ? winrt::make() : *std::min_element(states.begin(), states.end(), TerminalApp::implementation::TaskbarState::ComparePriority); } @@ -798,7 +992,7 @@ namespace winrt::TerminalApp::implementation auto weakThis{ get_weak() }; std::weak_ptr weakPane{ pane }; - pane->GotFocus([weakThis](std::shared_ptr sender) { + auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr sender) { // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; @@ -818,7 +1012,7 @@ namespace winrt::TerminalApp::implementation } }); - pane->LostFocus([weakThis](std::shared_ptr /*sender*/) { + auto lostFocusToken = pane->LostFocus([weakThis](std::shared_ptr /*sender*/) { // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; @@ -832,7 +1026,7 @@ namespace winrt::TerminalApp::implementation // Add a Closed event handler to the Pane. If the pane closes out from // underneath us, and it's zoomed, we want to be able to make sure to // update our state accordingly to un-zoom that pane. See GH#7252. - pane->Closed([weakThis, weakPane](auto&& /*s*/, auto && /*e*/) -> winrt::fire_and_forget { + auto closedToken = pane->Closed([weakThis, weakPane](auto&& /*s*/, auto && /*e*/) -> winrt::fire_and_forget { if (auto tab{ weakThis.get() }) { if (tab->_zoomedPane) @@ -857,7 +1051,7 @@ namespace winrt::TerminalApp::implementation }); // Add a PaneRaiseBell event handler to the Pane - pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) { + auto bellToken = pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) { if (auto tab{ weakThis.get() }) { if (visual) @@ -879,6 +1073,40 @@ namespace winrt::TerminalApp::implementation } } }); + + // box the event token so that we can give a reference to it in the + // event handler. + auto detachedToken = std::make_shared(); + // Add a Detached event handler to the Pane to clean up tab state + // and other event handlers when a pane is removed from this tab. + *detachedToken = pane->Detached([weakThis, weakPane, gotFocusToken, lostFocusToken, closedToken, bellToken, detachedToken](std::shared_ptr /*sender*/) { + // Make sure we do this at most once + if (auto pane{ weakPane.lock() }) + { + pane->Detached(*detachedToken); + pane->GotFocus(gotFocusToken); + pane->LostFocus(lostFocusToken); + pane->Closed(closedToken); + pane->PaneRaiseBell(bellToken); + + if (auto tab{ weakThis.get() }) + { + if (auto control = pane->GetTerminalControl()) + { + tab->_DetachEventHandlersFromControl(pane->Id().value(), control); + } + + for (auto i = tab->_mruPanes.begin(); i != tab->_mruPanes.end(); ++i) + { + if (*i == pane->Id()) + { + tab->_mruPanes.erase(i); + break; + } + } + } + } + }); } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 6a9c7808c..bafa93951 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -22,9 +22,10 @@ namespace winrt::TerminalApp::implementation { public: TerminalTab(const GUID& profile, const winrt::Microsoft::Terminal::Control::TermControl& control); + TerminalTab(std::shared_ptr rootPane); // Called after construction to perform the necessary setup, which relies on weak_ptr - void Initialize(const winrt::Microsoft::Terminal::Control::TermControl& control); + void Initialize(); winrt::Microsoft::Terminal::Control::TermControl GetActiveTerminalControl() const; std::optional GetFocusedProfile() const noexcept; @@ -33,6 +34,10 @@ namespace winrt::TerminalApp::implementation winrt::fire_and_forget Scroll(const int delta); + std::shared_ptr DetachRoot(); + std::shared_ptr DetachPane(); + void AttachPane(std::shared_ptr pane); + void SplitPane(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, const float splitSize, const GUID& profile, @@ -54,7 +59,7 @@ namespace winrt::TerminalApp::implementation void ResizeContent(const winrt::Windows::Foundation::Size& newSize); void ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); - void MovePane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + void SwapPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool FocusPane(const uint32_t id); void UpdateSettings(const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, const GUID& profile); @@ -102,6 +107,7 @@ namespace winrt::TerminalApp::implementation std::shared_ptr _rootPane{ nullptr }; std::shared_ptr _activePane{ nullptr }; std::shared_ptr _zoomedPane{ nullptr }; + winrt::hstring _lastIconPath{}; winrt::TerminalApp::ColorPickupFlyout _tabColorPickup{}; std::optional _themeTabColor{}; @@ -109,6 +115,19 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::TabHeaderControl _headerControl{}; winrt::TerminalApp::TerminalTabStatus _tabStatus{}; + struct ControlEventTokens + { + winrt::event_token titleToken; + winrt::event_token fontToken; + winrt::event_token colorToken; + winrt::event_token taskbarToken; + winrt::event_token readOnlyToken; + winrt::event_token focusToken; + }; + std::unordered_map _controlEvents; + + winrt::event_token _rootClosedToken{}; + std::vector _mruPanes; uint32_t _nextPaneId{ 0 }; @@ -121,6 +140,8 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::ShortcutActionDispatch _dispatch; + void _Setup(); + std::optional _bellIndicatorTimer; void _BellIndicatorTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); @@ -133,9 +154,8 @@ namespace winrt::TerminalApp::implementation void _RefreshVisualState(); - void _BindEventHandlers(const winrt::Microsoft::Terminal::Control::TermControl& control) noexcept; - - void _AttachEventHandlersToControl(const winrt::Microsoft::Terminal::Control::TermControl& control); + void _DetachEventHandlersFromControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control); + void _AttachEventHandlersToControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control); void _AttachEventHandlersToPane(std::shared_ptr pane); void _UpdateActivePane(std::shared_ptr pane); diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 0bd4b174a..acef483be 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -21,6 +21,7 @@ static constexpr std::string_view ExecuteCommandlineKey{ "wt" }; static constexpr std::string_view FindKey{ "find" }; static constexpr std::string_view MoveFocusKey{ "moveFocus" }; static constexpr std::string_view MovePaneKey{ "movePane" }; +static constexpr std::string_view SwapPaneKey{ "swapPane" }; static constexpr std::string_view NewTabKey{ "newTab" }; static constexpr std::string_view NextTabKey{ "nextTab" }; static constexpr std::string_view OpenNewTabDropdownKey{ "openNewTabDropdown" }; @@ -322,6 +323,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::Invalid, L"" }, { ShortcutAction::MoveFocus, RS_(L"MoveFocusCommandKey") }, { ShortcutAction::MovePane, RS_(L"MovePaneCommandKey") }, + { ShortcutAction::SwapPane, RS_(L"SwapPaneCommandKey") }, { ShortcutAction::NewTab, RS_(L"NewTabCommandKey") }, { ShortcutAction::NextTab, RS_(L"NextTabCommandKey") }, { ShortcutAction::OpenNewTabDropdown, RS_(L"OpenNewTabDropdownCommandKey") }, diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 9baaaf240..d422bf2e2 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -13,6 +13,7 @@ #include "ResizePaneArgs.g.cpp" #include "MoveFocusArgs.g.cpp" #include "MovePaneArgs.g.cpp" +#include "SwapPaneArgs.g.cpp" #include "AdjustFontSizeArgs.g.cpp" #include "SendInputArgs.g.cpp" #include "SplitPaneArgs.g.cpp" @@ -229,6 +230,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation }; } + winrt::hstring MovePaneArgs::GenerateName() const + { + return winrt::hstring{ + fmt::format(L"{}, tab index:{}", RS_(L"MovePaneCommandKey"), TabIndex()) + }; + } + winrt::hstring SwitchToTabArgs::GenerateName() const { if (TabIndex() == UINT32_MAX) @@ -291,7 +299,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation }; } - winrt::hstring MovePaneArgs::GenerateName() const + winrt::hstring SwapPaneArgs::GenerateName() const { winrt::hstring directionString; switch (Direction()) @@ -309,10 +317,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation directionString = RS_(L"DirectionDown"); break; case FocusDirection::Previous: - return RS_(L"MovePaneToLastUsedPane"); + return RS_(L"SwapPaneToLastUsedPane"); } return winrt::hstring{ - fmt::format(std::wstring_view(RS_(L"MovePaneWithArgCommandKey")), + fmt::format(std::wstring_view(RS_(L"SwapPaneWithArgCommandKey")), directionString) }; } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 4861ea805..1d67ef809 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -13,6 +13,7 @@ #include "ResizePaneArgs.g.h" #include "MoveFocusArgs.g.h" #include "MovePaneArgs.g.h" +#include "SwapPaneArgs.g.h" #include "AdjustFontSizeArgs.g.h" #include "SendInputArgs.g.h" #include "SplitPaneArgs.g.h" @@ -286,6 +287,57 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } }; + struct MovePaneArgs : public MovePaneArgsT + { + MovePaneArgs() = default; + MovePaneArgs(uint32_t& tabIndex) : + _TabIndex{ tabIndex } {}; + ACTION_ARG(uint32_t, TabIndex, 0); + + static constexpr std::string_view TabIndexKey{ "index" }; + + public: + hstring GenerateName() const; + + bool Equals(const IActionArgs& other) + { + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_TabIndex == _TabIndex; + } + return false; + }; + static FromJsonResult FromJson(const Json::Value& json) + { + // LOAD BEARING: Not using make_self here _will_ break you in the future! + auto args = winrt::make_self(); + JsonUtils::GetValueForKey(json, TabIndexKey, args->_TabIndex); + return { *args, {} }; + } + static Json::Value ToJson(const IActionArgs& val) + { + if (!val) + { + return {}; + } + Json::Value json{ Json::ValueType::objectValue }; + const auto args{ get_self(val) }; + JsonUtils::SetValueForKey(json, TabIndexKey, args->_TabIndex); + return json; + } + IActionArgs Copy() const + { + auto copy{ winrt::make_self() }; + copy->_TabIndex = _TabIndex; + return *copy; + } + size_t Hash() const + { + return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(TabIndex()); + } + }; + struct SwitchToTabArgs : public SwitchToTabArgsT { SwitchToTabArgs() = default; @@ -452,10 +504,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } }; - struct MovePaneArgs : public MovePaneArgsT + struct SwapPaneArgs : public SwapPaneArgsT { - MovePaneArgs() = default; - MovePaneArgs(Model::FocusDirection direction) : + SwapPaneArgs() = default; + SwapPaneArgs(Model::FocusDirection direction) : _Direction{ direction } {}; ACTION_ARG(Model::FocusDirection, Direction, FocusDirection::None); @@ -467,7 +519,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation bool Equals(const IActionArgs& other) { - auto otherAsUs = other.try_as(); + auto otherAsUs = other.try_as(); if (otherAsUs) { return otherAsUs->_Direction == _Direction; @@ -477,7 +529,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static FromJsonResult FromJson(const Json::Value& json) { // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); + auto args = winrt::make_self(); JsonUtils::GetValueForKey(json, DirectionKey, args->_Direction); if (args->Direction() == FocusDirection::None) { @@ -495,13 +547,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return {}; } Json::Value json{ Json::ValueType::objectValue }; - const auto args{ get_self(val) }; + const auto args{ get_self(val) }; JsonUtils::SetValueForKey(json, DirectionKey, args->_Direction); return json; } IActionArgs Copy() const { - auto copy{ winrt::make_self() }; + auto copy{ winrt::make_self() }; copy->_Direction = _Direction; return *copy; } @@ -1708,6 +1760,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(NewTabArgs); BASIC_FACTORY(MoveFocusArgs); BASIC_FACTORY(MovePaneArgs); + BASIC_FACTORY(SwapPaneArgs); BASIC_FACTORY(SplitPaneArgs); BASIC_FACTORY(SetColorSchemeArgs); BASIC_FACTORY(ExecuteCommandlineArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 9e418a335..25e765f64 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -141,6 +141,12 @@ namespace Microsoft.Terminal.Settings.Model NewTerminalArgs TerminalArgs { get; }; }; + [default_interface] runtimeclass MovePaneArgs : IActionArgs + { + MovePaneArgs(UInt32 tabIndex); + UInt32 TabIndex; + }; + [default_interface] runtimeclass SwitchToTabArgs : IActionArgs { SwitchToTabArgs(UInt32 tabIndex); @@ -158,9 +164,9 @@ namespace Microsoft.Terminal.Settings.Model FocusDirection FocusDirection { get; }; }; - [default_interface] runtimeclass MovePaneArgs : IActionArgs + [default_interface] runtimeclass SwapPaneArgs : IActionArgs { - MovePaneArgs(FocusDirection direction); + SwapPaneArgs(FocusDirection direction); FocusDirection Direction { get; }; }; diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index 71c74fccb..7d0d0a728 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -50,6 +50,7 @@ ON_ALL_ACTIONS(ResizePane) \ ON_ALL_ACTIONS(MoveFocus) \ ON_ALL_ACTIONS(MovePane) \ + ON_ALL_ACTIONS(SwapPane) \ ON_ALL_ACTIONS(Find) \ ON_ALL_ACTIONS(ToggleShaderEffects) \ ON_ALL_ACTIONS(ToggleFocusMode) \ @@ -90,6 +91,7 @@ ON_ALL_ACTIONS_WITH_ARGS(GlobalSummon) \ ON_ALL_ACTIONS_WITH_ARGS(MoveFocus) \ ON_ALL_ACTIONS_WITH_ARGS(MovePane) \ + ON_ALL_ACTIONS_WITH_ARGS(SwapPane) \ ON_ALL_ACTIONS_WITH_ARGS(MoveTab) \ ON_ALL_ACTIONS_WITH_ARGS(NewTab) \ ON_ALL_ACTIONS_WITH_ARGS(NewWindow) \ diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index ad8762ed3..4ea614c25 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -246,15 +246,15 @@ Move focus to the last used pane - - Move pane + + Swap pane - - Move pane {0} + + Swap pane {0} {0} will be replaced with one of the four directions "DirectionLeft", "DirectionRight", "DirectionUp", "DirectionDown" - - Move pane to the last used pane + + Swap panes with the last used pane New tab @@ -356,6 +356,9 @@ Split pane horizontally + + Move pane + Split pane diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 6d8cdceb7..8cfb25cd3 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -344,14 +344,23 @@ { "command": { "action": "moveFocus", "direction": "right" }, "keys": "alt+right" }, { "command": { "action": "moveFocus", "direction": "up" }, "keys": "alt+up" }, { "command": { "action": "moveFocus", "direction": "previous" }, "keys": "ctrl+alt+left"}, - { "command": { "action": "movePane", "direction": "down" } }, - { "command": { "action": "movePane", "direction": "left" } }, - { "command": { "action": "movePane", "direction": "right" } }, - { "command": { "action": "movePane", "direction": "up" } }, - { "command": { "action": "movePane", "direction": "previous"} }, + { "command": { "action": "swapPane", "direction": "down" } }, + { "command": { "action": "swapPane", "direction": "left" } }, + { "command": { "action": "swapPane", "direction": "right" } }, + { "command": { "action": "swapPane", "direction": "up" } }, + { "command": { "action": "swapPane", "direction": "previous"} }, { "command": "togglePaneZoom" }, { "command": "toggleSplitOrientation" }, { "command": "toggleReadOnlyMode" }, + { "command": { "action": "movePane", "index": 0 } }, + { "command": { "action": "movePane", "index": 1 } }, + { "command": { "action": "movePane", "index": 2 } }, + { "command": { "action": "movePane", "index": 3 } }, + { "command": { "action": "movePane", "index": 4 } }, + { "command": { "action": "movePane", "index": 5 } }, + { "command": { "action": "movePane", "index": 6 } }, + { "command": { "action": "movePane", "index": 7 } }, + { "command": { "action": "movePane", "index": 8 } }, // Clipboard Integration { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+shift+c" },