Preliminary work to add Swap Panes functionality (GH Issues 1000, 4922) (#10638)

<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? -->
## Summary of the Pull Request
Add functionality to swap a pane with an adjacent (Up/Down/Left/Right) neighbor.

<!-- Other than the issue solved, is this relevant to any other issues/existing PRs? --> 
## References
This work potentially touches on: #1000 #2398 and #4922
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
* [x] Closes a component of #1000 (partially, comment), #4922 (partially, `SwapPanes` function is added but not hooked up, no detach functionality)
* [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA
* [x] Tests added/passed
* [ ] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx
* [x] Schema updated.
* [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx

<!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

Its been a while since I've written C++ code, and it is my first time working on a Windows application. I hope that I have not made too many mistakes.

Work currently done:
- Add boilerplate/infrastructure for argument parsing, hotkeys, event handling
- Adds the `MovePane` function that finds the focused pane, and then tries to find
  a pane that is visually adjacent to according to direction.
- First pass at the `SwapPanes` function that swaps the tree location of two panes
- First working version of helpers `_FindFocusAndNeighbor` and `_FindNeighborFromFocus`
  that search the tree for the currently focused pane, and then climbs back up the tree
  to try to find a sibling pane that is adjacent to it. 
- An `_IsAdjacent' function that tests whether two panes, given their relative offsets, are adjacent to each other according to the direction.

Next steps:
- Once working these functions (`_FindFocusAndNeighbor`, etc) could be utilized to also solve #2398 by updating the `NavigateFocus` function.
- Do we want default hotkeys for the new actions?

<!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
At this point, compilation and manual testing of functionality (with hotkeys) by creating panes, adding distinguishers to each pane, and then swapping them around to confirm they went to the right location.
This commit is contained in:
Schuyler Rosefield 2021-07-22 08:53:03 -04:00 committed by GitHub
parent 41ade2c57e
commit cf97a9f772
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1037 additions and 2 deletions

View File

@ -238,6 +238,7 @@
"identifyWindow",
"identifyWindows",
"moveFocus",
"movePane",
"moveTab",
"newTab",
"newWindow",
@ -514,6 +515,23 @@
],
"required": [ "direction" ]
},
"MovePaneAction": {
"description": "Arguments corresponding to a Move Pane Action",
"allOf": [
{ "$ref": "#/definitions/ShortcutAction" },
{
"properties": {
"action": { "type": "string", "pattern": "movePane" },
"direction": {
"$ref": "#/definitions/FocusDirection",
"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."
}
}
}
],
"required": [ "direction" ]
},
"ResizePaneAction": {
"description": "Arguments corresponding to a Resize Pane Action",
"allOf": [
@ -952,6 +970,7 @@
{ "$ref": "#/definitions/NewTabAction" },
{ "$ref": "#/definitions/SwitchToTabAction" },
{ "$ref": "#/definitions/MoveFocusAction" },
{ "$ref": "#/definitions/MovePaneAction" },
{ "$ref": "#/definitions/ResizePaneAction" },
{ "$ref": "#/definitions/SendInputAction" },
{ "$ref": "#/definitions/SplitPaneAction" },

View File

@ -56,6 +56,7 @@ namespace TerminalAppLocalTests
TEST_METHOD(ParseComboCommandlineIntoArgs);
TEST_METHOD(ParseFocusTabArgs);
TEST_METHOD(ParseMoveFocusArgs);
TEST_METHOD(ParseMovePaneArgs);
TEST_METHOD(ParseArgumentsWithParsingTerminators);
TEST_METHOD(ParseFocusPaneArgs);
@ -1207,6 +1208,124 @@ namespace TerminalAppLocalTests
}
}
void CommandlineTest::ParseMovePaneArgs()
{
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";
{
AppCommandlineArgs appArgs{};
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand };
Log::Comment(NoThrowString().Format(
L"Just the subcommand, without a direction, should fail."));
_buildCommandlinesExpectFailureHelper(appArgs, 1u, rawCommands);
}
{
AppCommandlineArgs appArgs{};
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"left" };
_buildCommandlinesHelper(appArgs, 1u, rawCommands);
VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size());
// The first action is going to always be a new-tab action
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction());
}
{
AppCommandlineArgs appArgs{};
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"right" };
_buildCommandlinesHelper(appArgs, 1u, rawCommands);
VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size());
// The first action is going to always be a new-tab action
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction());
}
{
AppCommandlineArgs appArgs{};
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"up" };
_buildCommandlinesHelper(appArgs, 1u, rawCommands);
VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size());
// The first action is going to always be a new-tab action
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Up, myArgs.Direction());
}
{
AppCommandlineArgs appArgs{};
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"down" };
_buildCommandlinesHelper(appArgs, 1u, rawCommands);
VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size());
// The first action is going to always be a new-tab action
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Down, myArgs.Direction());
}
{
AppCommandlineArgs appArgs{};
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"badDirection" };
Log::Comment(NoThrowString().Format(
L"move-pane with an invalid direction should fail."));
_buildCommandlinesExpectFailureHelper(appArgs, 1u, rawCommands);
}
{
AppCommandlineArgs appArgs{};
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"left", L";", subcommand, L"right" };
_buildCommandlinesHelper(appArgs, 2u, rawCommands);
VERIFY_ARE_EQUAL(3u, appArgs._startupActions.size());
// The first action is going to always be a new-tab action
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action());
auto actionAndArgs = appArgs._startupActions.at(1);
VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
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_IS_NOT_NULL(actionAndArgs.Args());
myArgs = actionAndArgs.Args().try_as<MovePaneArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction());
}
}
void CommandlineTest::ParseFocusPaneArgs()
{
BEGIN_TEST_METHOD_PROPERTIES()

View File

@ -82,6 +82,8 @@ namespace TerminalAppLocalTests
TEST_METHOD(MoveFocusFromZoomedPane);
TEST_METHOD(CloseZoomedPane);
TEST_METHOD(MovePanes);
TEST_METHOD(NextMRUTab);
TEST_METHOD(VerifyCommandPaletteTabSwitcherOrder);
@ -817,6 +819,212 @@ namespace TerminalAppLocalTests
VERIFY_SUCCEEDED(result);
}
void TabTests::MovePanes()
{
auto page = _commonSetup();
Log::Comment(L"Setup 4 panes.");
// Create the following layout
// -------------------
// | 1 | 2 |
// | | |
// -------------------
// | 3 | 4 |
// | | |
// -------------------
uint32_t firstId = 0, secondId = 0, thirdId = 0, fourthId = 0;
TestOnUIThread([&]() {
VERIFY_ARE_EQUAL(1u, page->_tabs.Size());
auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
firstId = tab->_activePane->Id().value();
// We start with 1 tab, split vertically to get
// -------------------
// | 1 | 2 |
// | | |
// -------------------
page->_SplitPane(SplitState::Vertical, SplitType::Duplicate, 0.5f, nullptr);
secondId = tab->_activePane->Id().value();
});
Sleep(250);
TestOnUIThread([&]() {
// After this the `2` pane is focused, go back to `1` being focused
page->_MoveFocus(FocusDirection::Left);
});
Sleep(250);
TestOnUIThread([&]() {
// Split again to make the 3rd tab
// -------------------
// | 1 | |
// | | |
// ---------| 2 |
// | 3 | |
// | | |
// -------------------
page->_SplitPane(SplitState::Horizontal, SplitType::Duplicate, 0.5f, nullptr);
auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
// Split again to make the 3rd tab
thirdId = tab->_activePane->Id().value();
});
Sleep(250);
TestOnUIThread([&]() {
// After this the `3` pane is focused, go back to `2` being focused
page->_MoveFocus(FocusDirection::Right);
});
Sleep(250);
TestOnUIThread([&]() {
// Split to create the final pane
// -------------------
// | 1 | 2 |
// | | |
// -------------------
// | 3 | 4 |
// | | |
// -------------------
page->_SplitPane(SplitState::Horizontal, SplitType::Duplicate, 0.5f, nullptr);
auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
fourthId = tab->_activePane->Id().value();
});
Sleep(250);
TestOnUIThread([&]() {
auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount());
// just to be complete, make sure we actually have 4 different ids
VERIFY_ARE_NOT_EQUAL(firstId, fourthId);
VERIFY_ARE_NOT_EQUAL(secondId, fourthId);
VERIFY_ARE_NOT_EQUAL(thirdId, fourthId);
VERIFY_ARE_NOT_EQUAL(firstId, thirdId);
VERIFY_ARE_NOT_EQUAL(secondId, thirdId);
VERIFY_ARE_NOT_EQUAL(firstId, secondId);
});
// Gratuitous use of sleep to make sure that the UI has updated properly
// after each operation.
Sleep(250);
// Now try to move the pane through the tree
Log::Comment(L"Move pane to the left. This should swap panes 3 and 4");
// -------------------
// | 1 | 2 |
// | | |
// -------------------
// | 4 | 3 |
// | | |
// -------------------
TestOnUIThread([&]() {
// Set up action
MovePaneArgs args{ FocusDirection::Left };
ActionEventArgs eventArgs{ args };
page->_HandleMovePane(nullptr, eventArgs);
});
Sleep(250);
TestOnUIThread([&]() {
auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount());
// Our currently focused pane should be `4`
VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value());
// Inspect the tree to make sure we swapped
VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_firstChild->_secondChild->Id().value());
VERIFY_ARE_EQUAL(thirdId, tab->_rootPane->_secondChild->_secondChild->Id().value());
});
Sleep(250);
Log::Comment(L"Move pane to up. This should swap panes 1 and 4");
// -------------------
// | 4 | 2 |
// | | |
// -------------------
// | 1 | 3 |
// | | |
// -------------------
TestOnUIThread([&]() {
// Set up action
MovePaneArgs args{ FocusDirection::Up };
ActionEventArgs eventArgs{ args };
page->_HandleMovePane(nullptr, eventArgs);
});
Sleep(250);
TestOnUIThread([&]() {
auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount());
// Our currently focused pane should be `4`
VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value());
// Inspect the tree to make sure we swapped
VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_firstChild->_firstChild->Id().value());
VERIFY_ARE_EQUAL(firstId, tab->_rootPane->_firstChild->_secondChild->Id().value());
});
Sleep(250);
Log::Comment(L"Move pane to the right. This should swap panes 2 and 4");
// -------------------
// | 2 | 4 |
// | | |
// -------------------
// | 1 | 3 |
// | | |
// -------------------
TestOnUIThread([&]() {
// Set up action
MovePaneArgs args{ FocusDirection::Right };
ActionEventArgs eventArgs{ args };
page->_HandleMovePane(nullptr, eventArgs);
});
Sleep(250);
TestOnUIThread([&]() {
auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount());
// Our currently focused pane should be `4`
VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value());
// Inspect the tree to make sure we swapped
VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_secondChild->_firstChild->Id().value());
VERIFY_ARE_EQUAL(secondId, tab->_rootPane->_firstChild->_firstChild->Id().value());
});
Sleep(250);
Log::Comment(L"Move pane down. This should swap panes 3 and 4");
// -------------------
// | 2 | 3 |
// | | |
// -------------------
// | 1 | 4 |
// | | |
// -------------------
TestOnUIThread([&]() {
// Set up action
MovePaneArgs args{ FocusDirection::Down };
ActionEventArgs eventArgs{ args };
page->_HandleMovePane(nullptr, eventArgs);
});
Sleep(250);
TestOnUIThread([&]() {
auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0));
VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount());
// Our currently focused pane should be `4`
VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value());
// Inspect the tree to make sure we swapped
VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_secondChild->_secondChild->Id().value());
VERIFY_ARE_EQUAL(thirdId, tab->_rootPane->_secondChild->_firstChild->Id().value());
});
}
void TabTests::NextMRUTab()
{
// This is a test for GH#8025 - we want to make sure that we can do both

View File

@ -313,6 +313,24 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::_HandleMovePane(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if (const auto& realArgs = args.ActionArgs().try_as<MovePaneArgs>())
{
if (realArgs.Direction() == FocusDirection::None)
{
// Do nothing
args.Handled(false);
}
else
{
_MovePane(realArgs.Direction());
args.Handled(true);
}
}
}
void TerminalPage::_HandleCopyText(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{

View File

@ -192,6 +192,7 @@ void AppCommandlineArgs::_buildParser()
_buildSplitPaneParser();
_buildFocusTabParser();
_buildMoveFocusParser();
_buildMovePaneParser();
_buildFocusPaneParser();
}
@ -398,6 +399,54 @@ void AppCommandlineArgs::_buildMoveFocusParser()
setupSubcommand(_moveFocusShort);
}
// 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:
// - <none>
// Return Value:
// - <none>
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) {
std::map<std::string, FocusDirection> map = {
{ "left", FocusDirection::Left },
{ "right", FocusDirection::Right },
{ "up", FocusDirection::Up },
{ "down", FocusDirection::Down }
};
auto* directionOpt = subcommand->add_option("direction",
_movePaneDirection,
RS_A(L"CmdMovePaneDirectionArgDesc"));
directionOpt->transform(CLI::CheckedTransformer(map, CLI::ignore_case));
directionOpt->required();
// 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]() {
if (_movePaneDirection != FocusDirection::None)
{
MovePaneArgs args{ _movePaneDirection };
ActionAndArgs actionAndArgs{};
actionAndArgs.Action(ShortcutAction::MovePane);
actionAndArgs.Args(args);
_startupActions.push_back(std::move(actionAndArgs));
}
});
};
setupSubcommand(_movePaneCommand);
setupSubcommand(_movePaneShort);
}
// Method Description:
// - Adds the `focus-pane` subcommand and related options to the commandline parser.
// - Additionally adds the `fp` subcommand, which is just a shortened version of `focus-pane`
@ -574,6 +623,8 @@ bool AppCommandlineArgs::_noCommandsProvided()
*_focusTabShort ||
*_moveFocusCommand ||
*_moveFocusShort ||
*_movePaneCommand ||
*_movePaneShort ||
*_focusPaneCommand ||
*_focusPaneShort ||
*_newPaneShort.subcommand ||
@ -607,6 +658,7 @@ void AppCommandlineArgs::_resetStateToDefault()
_focusPrevTab = false;
_moveFocusDirection = FocusDirection::None;
_movePaneDirection = FocusDirection::None;
_focusPaneTarget = -1;

View File

@ -82,6 +82,8 @@ private:
CLI::App* _focusTabShort;
CLI::App* _moveFocusCommand;
CLI::App* _moveFocusShort;
CLI::App* _movePaneCommand;
CLI::App* _movePaneShort;
CLI::App* _focusPaneCommand;
CLI::App* _focusPaneShort;
@ -95,6 +97,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 };
// _commandline will contain the command line with which we'll be spawning a new terminal
std::vector<std::string> _commandline;
@ -128,6 +131,7 @@ private:
void _buildSplitPaneParser();
void _buildFocusTabParser();
void _buildMoveFocusParser();
void _buildMovePaneParser();
void _buildFocusPaneParser();
bool _noCommandsProvided();
void _resetStateToDefault();

View File

@ -302,6 +302,382 @@ bool Pane::NavigateFocus(const FocusDirection& direction)
return false;
}
// Method Description:
// - Attempts to find the parent pane of the provided pane.
// Arguments:
// - pane: The pane to search for.
// Return Value:
// - the parent of `pane` if pane is in this tree.
std::shared_ptr<Pane> Pane::_FindParentOfPane(const std::shared_ptr<Pane> pane)
{
if (_IsLeaf())
{
return nullptr;
}
if (_firstChild == pane || _secondChild == pane)
{
return shared_from_this();
}
if (auto p = _firstChild->_FindParentOfPane(pane))
{
return p;
}
return _secondChild->_FindParentOfPane(pane);
}
// Method Description:
// - Attempts to swap the location of the two given panes in the tree.
// Searches the tree starting at this pane to find the parent pane for each of
// the arguments, and if both parents are found, replaces the appropriate
// child in each.
// Arguments:
// - first: A pointer to the first pane to switch.
// - second: A pointer to the second pane to switch.
// Return Value:
// - true if a swap was performed.
bool Pane::SwapPanes(std::shared_ptr<Pane> first, std::shared_ptr<Pane> second)
{
// If there is nothing to swap, just return.
if (first == second || _IsLeaf())
{
return false;
}
std::unique_lock lock{ _createCloseLock };
// Recurse through the tree to find the parent panes of each pane that is
// being swapped.
std::shared_ptr<Pane> firstParent = _FindParentOfPane(first);
std::shared_ptr<Pane> secondParent = _FindParentOfPane(second);
// We should have found either no elements, or both elements.
// If we only found one parent then the pane SwapPane was called on did not
// contain both panes as leaves, as could happen if the tree was modified
// after the pointers were found but before we reached this function.
if (firstParent && secondParent)
{
// Swap size/display information of the two panes.
std::swap(first->_borders, second->_borders);
// Replace the old child with new one, and revoke appropriate event
// handlers.
auto replaceChild = [](auto& parent, auto oldChild, auto newChild) {
// Revoke the old handlers
if (parent->_firstChild == oldChild)
{
parent->_firstChild->Closed(parent->_firstClosedToken);
parent->_firstChild = newChild;
}
else if (parent->_secondChild == oldChild)
{
parent->_secondChild->Closed(parent->_secondClosedToken);
parent->_secondChild = newChild;
}
// Clear now to ensure that we can add the child's grid to us later
parent->_root.Children().Clear();
};
// Make sure that the right event handlers are set, and the children
// are placed in the appropriate locations in the grid.
auto updateParent = [](auto& parent) {
parent->_SetupChildCloseHandlers();
parent->_root.Children().Clear();
parent->_root.Children().Append(parent->_firstChild->GetRootElement());
parent->_root.Children().Append(parent->_secondChild->GetRootElement());
// Make sure they have the correct borders, and also that they are
// placed in the right location in the grid.
// This mildly reproduces ApplySplitDefinitions, but is different in
// that it does not want to utilize the parent's border to set child
// borders.
if (parent->_splitState == SplitState::Vertical)
{
Controls::Grid::SetColumn(parent->_firstChild->GetRootElement(), 0);
Controls::Grid::SetColumn(parent->_secondChild->GetRootElement(), 1);
}
else if (parent->_splitState == SplitState::Horizontal)
{
Controls::Grid::SetRow(parent->_firstChild->GetRootElement(), 0);
Controls::Grid::SetRow(parent->_secondChild->GetRootElement(), 1);
}
parent->_firstChild->_UpdateBorders();
parent->_secondChild->_UpdateBorders();
};
// If the firstParent and secondParent are the same, then we are just
// swapping the first child and second child of that parent.
if (firstParent == secondParent)
{
firstParent->_firstChild->Closed(firstParent->_firstClosedToken);
firstParent->_secondChild->Closed(firstParent->_secondClosedToken);
std::swap(firstParent->_firstChild, firstParent->_secondChild);
updateParent(firstParent);
}
else
{
// Replace both children before updating display to ensure
// that the grid elements are not attached to multiple panes
replaceChild(firstParent, first, second);
replaceChild(secondParent, second, first);
updateParent(firstParent);
updateParent(secondParent);
}
return true;
}
return false;
}
// Method Description:
// - Given two panes, test whether the `direction` side of first is adjacent to second.
// Arguments:
// - first: The reference pane.
// - second: the pane to test adjacency with.
// - direction: The direction to search in from the reference pane.
// Return Value:
// - true if the two panes are adjacent.
bool Pane::_IsAdjacent(const std::shared_ptr<Pane> first,
const Pane::PanePoint firstOffset,
const std::shared_ptr<Pane> second,
const Pane::PanePoint secondOffset,
const FocusDirection& direction) const
{
// Since float equality is tricky (arithmetic is non-associative, commutative),
// test if the two numbers are within an epsilon distance of each other.
auto floatEqual = [](float left, float right) {
return abs(left - right) < 1e-4F;
};
// When checking containment in a range, the range is half-closed, i.e. [x, x+w).
// If the direction is left test that the left side of the first element is
// next to the right side of the second element, and that the top left
// corner of the first element is within the second element's height
if (direction == FocusDirection::Left)
{
auto sharesBorders = floatEqual(firstOffset.x, secondOffset.x + gsl::narrow_cast<float>(second->GetRootElement().ActualWidth()));
auto withinHeight = (firstOffset.y >= secondOffset.y) && (firstOffset.y < secondOffset.y + gsl::narrow_cast<float>(second->GetRootElement().ActualHeight()));
return sharesBorders && withinHeight;
}
// If the direction is right test that the right side of the first element is
// next to the left side of the second element, and that the top left
// corner of the first element is within the second element's height
else if (direction == FocusDirection::Right)
{
auto sharesBorders = floatEqual(firstOffset.x + gsl::narrow_cast<float>(first->GetRootElement().ActualWidth()), secondOffset.x);
auto withinHeight = (firstOffset.y >= secondOffset.y) && (firstOffset.y < secondOffset.y + gsl::narrow_cast<float>(second->GetRootElement().ActualHeight()));
return sharesBorders && withinHeight;
}
// If the direction is up test that the top side of the first element is
// next to the bottom side of the second element, and that the top left
// corner of the first element is within the second element's width
else if (direction == FocusDirection::Up)
{
auto sharesBorders = floatEqual(firstOffset.y, secondOffset.y + gsl::narrow_cast<float>(second->GetRootElement().ActualHeight()));
auto withinWidth = (firstOffset.x >= secondOffset.x) && (firstOffset.x < secondOffset.x + gsl::narrow_cast<float>(second->GetRootElement().ActualWidth()));
return sharesBorders && withinWidth;
}
// If the direction is down test that the bottom side of the first element is
// next to the top side of the second element, and that the top left
// corner of the first element is within the second element's width
else if (direction == FocusDirection::Down)
{
auto sharesBorders = floatEqual(firstOffset.y + gsl::narrow_cast<float>(first->GetRootElement().ActualHeight()), secondOffset.y);
auto withinWidth = (firstOffset.x >= secondOffset.x) && (firstOffset.x < secondOffset.x + gsl::narrow_cast<float>(second->GetRootElement().ActualWidth()));
return sharesBorders && withinWidth;
}
return false;
}
// Method Description:
// - Given the focused pane, and its relative position in the tree, attempt to
// find its visual neighbor within the current pane's tree.
// The neighbor, if it exists, will be a leaf pane.
// Arguments:
// - direction: The direction to search in from the focused pane.
// - focus: the focused pane
// - focusIsSecondSide: If the focused pane is on the "second" side (down/right of split)
// relative to the branch being searched
// - offset: the offset of the current pane
// Return Value:
// - A tuple of Panes, the first being the focused pane if found, and the second
// being the adjacent pane if it exists, and a bool that represents if the move
// goes out of bounds.
Pane::FocusNeighborSearch Pane::_FindNeighborForPane(const FocusDirection& direction,
FocusNeighborSearch searchResult,
const bool focusIsSecondSide,
const Pane::PanePoint offset)
{
// Test if the move will go out of boundaries. E.g. if the focus is already
// on the second child of some pane and it attempts to move right, there
// can't possibly be a neighbor to be found in the first child.
if ((focusIsSecondSide && (direction == FocusDirection::Right || direction == FocusDirection::Down)) ||
(!focusIsSecondSide && (direction == FocusDirection::Left || direction == FocusDirection::Up)))
{
return searchResult;
}
// If we are a leaf node test if we adjacent to the focus node
if (_IsLeaf())
{
if (_IsAdjacent(searchResult.focus, searchResult.focusOffset, shared_from_this(), offset, direction))
{
searchResult.neighbor = shared_from_this();
}
return searchResult;
}
auto firstOffset = offset;
auto secondOffset = offset;
// The second child has an offset depending on the split
if (_splitState == SplitState::Horizontal)
{
secondOffset.y += gsl::narrow_cast<float>(_firstChild->GetRootElement().ActualHeight());
}
else
{
secondOffset.x += gsl::narrow_cast<float>(_firstChild->GetRootElement().ActualWidth());
}
auto focusNeighborSearch = _firstChild->_FindNeighborForPane(direction, searchResult, focusIsSecondSide, firstOffset);
if (focusNeighborSearch.neighbor)
{
return focusNeighborSearch;
}
return _secondChild->_FindNeighborForPane(direction, searchResult, focusIsSecondSide, secondOffset);
}
// Method Description:
// - Searches the tree to find the currently focused pane, and if it exists, the
// visually adjacent pane by direction.
// Arguments:
// - direction: The direction to search in from the focused pane.
// - offset: The offset, with the top-left corner being (0,0), that the current pane is relative to the root.
// Return Value:
// - The (partial) search result. If the search was successful, the focus and its neighbor will be returned.
// Otherwise, the neighbor will be null and the focus will be null/non-null if it was found.
Pane::FocusNeighborSearch Pane::_FindFocusAndNeighbor(const FocusDirection& direction, const Pane::PanePoint offset)
{
// If we are the currently focused pane, return ourselves
if (_IsLeaf())
{
return { _lastActive ? shared_from_this() : nullptr, nullptr, offset };
}
// Search the first child, which has no offset from the parent pane
auto firstOffset = offset;
auto secondOffset = offset;
// The second child has an offset depending on the split
if (_splitState == SplitState::Horizontal)
{
secondOffset.y += gsl::narrow_cast<float>(_firstChild->GetRootElement().ActualHeight());
}
else
{
secondOffset.x += gsl::narrow_cast<float>(_firstChild->GetRootElement().ActualWidth());
}
auto focusNeighborSearch = _firstChild->_FindFocusAndNeighbor(direction, firstOffset);
// If we have both the focus element and its neighbor, we are done
if (focusNeighborSearch.focus && focusNeighborSearch.neighbor)
{
return focusNeighborSearch;
}
// if we only found the focus, then we search the second branch for the
// neighbor.
if (focusNeighborSearch.focus)
{
// If we can possibly have both sides of a direction, check if the sibling has the neighbor
if (DirectionMatchesSplit(direction, _splitState))
{
return _secondChild->_FindNeighborForPane(direction, focusNeighborSearch, false, secondOffset);
}
return focusNeighborSearch;
}
// If we didn't find the focus at all, we need to search the second branch
// for the focus (and possibly its neighbor).
focusNeighborSearch = _secondChild->_FindFocusAndNeighbor(direction, secondOffset);
// We found both so we are done.
if (focusNeighborSearch.focus && focusNeighborSearch.neighbor)
{
return focusNeighborSearch;
}
// We only found the focus, which means that its neighbor might be in the
// first branch.
if (focusNeighborSearch.focus)
{
// If we can possibly have both sides of a direction, check if the sibling has the neighbor
if (DirectionMatchesSplit(direction, _splitState))
{
return _firstChild->_FindNeighborForPane(direction, focusNeighborSearch, true, firstOffset);
}
return focusNeighborSearch;
}
return { nullptr, nullptr, offset };
}
// Method Description:
// - Attempts to swap places of the focused pane with one of our children. This
// will swap with the visually adjacent leaf pane if one exists in the
// direction requested, maintaining the existing tree structure.
// This breaks down into a few possible cases
// - If the move direction would encounter the edge of the pane, no move occurs
// - If the focused pane has a single neighbor according to the direction,
// then it will swap with it.
// - If the focused pane has multiple neighbors, it will swap with the
// first-most leaf of the neighboring panes.
// Arguments:
// - 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)
{
// If we're a leaf, do nothing. We can't possibly swap anything.
if (_IsLeaf())
{
return false;
}
// If we get a request to move to the previous pane return false because
// that needs to be handled at the tab level.
if (direction == FocusDirection::Previous)
{
return false;
}
// If the move direction does not match the split direction, the focused pane
// and its neighbor must necessarily be contained within the same child.
if (!DirectionMatchesSplit(direction, _splitState))
{
return _firstChild->MovePane(direction) || _secondChild->MovePane(direction);
}
// Since the direction is the same as our split, it is possible that we must
// swap a pane from one child to the other child.
// We now must keep track of state while we recurse.
auto focusNeighborPair = _FindFocusAndNeighbor(direction, { 0, 0 });
// Once we have found the focused pane and its neighbor, wherever they may
// be, we can swap them.
if (focusNeighborPair.focus && focusNeighborPair.neighbor)
{
auto swapped = SwapPanes(focusNeighborPair.focus, focusNeighborPair.neighbor);
focusNeighborPair.focus->_FocusFirstChild();
return swapped;
}
return false;
}
// Method Description:
// - Called when our attached control is closed. Triggers listeners to our close
// event, if we're a leaf pane.
@ -1625,6 +2001,36 @@ bool Pane::FocusPane(const uint32_t id)
return false;
}
// Method Description:
// - Recursive function that finds a pane with the given ID
// Arguments:
// - The ID of the pane we want to find
// Return Value:
// - A pointer to the pane with the given ID, if found.
std::shared_ptr<Pane> Pane::FindPane(const uint32_t id)
{
if (_IsLeaf())
{
if (id == _id)
{
return shared_from_this();
}
}
else
{
if (auto pane = _firstChild->FindPane(id))
{
return pane;
}
if (auto pane = _secondChild->FindPane(id))
{
return pane;
}
}
return nullptr;
}
// Method Description:
// - Gets the size in pixels of each of our children, given the full size they
// should fill. Since these children own their own separators (borders), this

View File

@ -22,6 +22,12 @@
#include "../../cascadia/inc/cppwinrt_utils.h"
// fwdecl unittest classes
namespace TerminalAppLocalTests
{
class TabTests;
};
enum class Borders : int
{
None = 0x0,
@ -56,6 +62,8 @@ 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 SwapPanes(std::shared_ptr<Pane> first, std::shared_ptr<Pane> second);
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType,
const float splitSize,
@ -79,6 +87,7 @@ public:
std::optional<uint32_t> Id() noexcept;
void Id(uint32_t id) noexcept;
bool FocusPane(const uint32_t id);
std::shared_ptr<Pane> FindPane(const uint32_t id);
bool ContainsReadOnly() const;
@ -88,6 +97,8 @@ public:
DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler<bool>);
private:
struct PanePoint;
struct FocusNeighborSearch;
struct SnapSizeResult;
struct SnapChildrenSizeResult;
struct LayoutSizeNode;
@ -139,6 +150,15 @@ private:
bool _Resize(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction);
bool _NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction);
std::shared_ptr<Pane> _FindParentOfPane(const std::shared_ptr<Pane> pane);
bool _IsAdjacent(const std::shared_ptr<Pane> first, const PanePoint firstOffset, const std::shared_ptr<Pane> second, const PanePoint secondOffset, const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction) const;
FocusNeighborSearch _FindNeighborForPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction,
FocusNeighborSearch searchResult,
const bool focusIsSecondSide,
const PanePoint offset);
FocusNeighborSearch _FindFocusAndNeighbor(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction,
const PanePoint offset);
void _CloseChild(const bool closeFirst);
winrt::fire_and_forget _CloseChildRoutine(const bool closeFirst);
@ -200,6 +220,19 @@ private:
static void _SetupResources();
struct PanePoint
{
float x;
float y;
};
struct FocusNeighborSearch
{
std::shared_ptr<Pane> focus;
std::shared_ptr<Pane> neighbor;
PanePoint focusOffset;
};
struct SnapSizeResult
{
float lower;
@ -236,4 +269,6 @@ private:
private:
void _AssignChildNode(std::unique_ptr<LayoutSizeNode>& nodeField, const LayoutSizeNode* const newNode);
};
friend class ::TerminalAppLocalTests::TabTests;
};

View File

@ -359,6 +359,16 @@
<data name="CmdMoveFocusDirectionArgDesc" xml:space="preserve">
<value>The direction to move focus in</value>
</data>
<data name="CmdMovePaneDesc" xml:space="preserve">
<value>Swap the focused pane with the adjacent pane in the specified direction</value>
</data>
<data name="CmdMPDesc" xml:space="preserve">
<value>An alias for the "move-pane" subcommand.</value>
<comment>{Locked="\"move-pane\""}</comment>
</data>
<data name="CmdMovePaneDirectionArgDesc" xml:space="preserve">
<value>The direction to move the focused pane in</value>
</data>
<data name="CmdFocusDesc" xml:space="preserve">
<value>Launch the window in focus mode</value>
</data>

View File

@ -1133,6 +1133,22 @@ namespace winrt::TerminalApp::implementation
}
}
// Method Description:
// - Attempt to swap the positions of the focused pane with another pane.
// See Pane::MovePane for details.
// Arguments:
// - direction: The direction to move the focused pane in.
// Return Value:
// - <none>
void TerminalPage::_MovePane(const FocusDirection& direction)
{
if (const auto terminalTab{ _GetFocusedTabImpl() })
{
_UnZoomIfNeeded();
terminalTab->MovePane(direction);
}
}
TermControl TerminalPage::_GetActiveControl()
{
if (const auto terminalTab{ _GetFocusedTabImpl() })

View File

@ -235,6 +235,7 @@ namespace winrt::TerminalApp::implementation
void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference<Microsoft::Terminal::Settings::Model::TabSwitcherMode>& customTabSwitcherMode);
bool _SelectTab(const uint32_t tabIndex);
void _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
void _MovePane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl();
std::optional<uint32_t> _GetFocusedTabIndex() const noexcept;

View File

@ -486,6 +486,10 @@ namespace winrt::TerminalApp::implementation
{
if (direction == FocusDirection::Previous)
{
if (_mruPanes.size() < 2)
{
return;
}
// To get to the previous pane, get the id of the previous pane and focus to that
_rootPane->FocusPane(_mruPanes.at(1));
}
@ -497,6 +501,35 @@ namespace winrt::TerminalApp::implementation
}
}
// Method Description:
// - Attempts to swap the location of the focused pane with another pane
// according to direction. When there are multiple adjacent panes it will
// select the first one (top-left-most).
// Arguments:
// - direction: The direction to move the pane in.
// Return Value:
// - <none>
void TerminalTab::MovePane(const FocusDirection& direction)
{
if (direction == FocusDirection::Previous)
{
if (_mruPanes.size() < 2)
{
return;
}
if (auto lastPane = _rootPane->FindPane(_mruPanes.at(1)))
{
_rootPane->SwapPanes(_activePane, lastPane);
}
}
else
{
// NOTE: This _must_ be called on the root pane, so that it can propagate
// throughout the entire tree.
_rootPane->MovePane(direction);
}
}
bool TerminalTab::FocusPane(const uint32_t id)
{
return _rootPane->FocusPane(id);

View File

@ -53,6 +53,7 @@ namespace winrt::TerminalApp::implementation
void ResizeContent(const winrt::Windows::Foundation::Size& newSize);
void ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction);
void NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction);
void MovePane(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);

View File

@ -20,6 +20,7 @@ static constexpr std::string_view DuplicateTabKey{ "duplicateTab" };
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 NewTabKey{ "newTab" };
static constexpr std::string_view NextTabKey{ "nextTab" };
static constexpr std::string_view OpenNewTabDropdownKey{ "openNewTabDropdown" };
@ -319,6 +320,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ ShortcutAction::Find, RS_(L"FindCommandKey") },
{ ShortcutAction::Invalid, L"" },
{ ShortcutAction::MoveFocus, RS_(L"MoveFocusCommandKey") },
{ ShortcutAction::MovePane, RS_(L"MovePaneCommandKey") },
{ ShortcutAction::NewTab, RS_(L"NewTabCommandKey") },
{ ShortcutAction::NextTab, RS_(L"NextTabCommandKey") },
{ ShortcutAction::OpenNewTabDropdown, RS_(L"OpenNewTabDropdownCommandKey") },

View File

@ -12,6 +12,7 @@
#include "SwitchToTabArgs.g.cpp"
#include "ResizePaneArgs.g.cpp"
#include "MoveFocusArgs.g.cpp"
#include "MovePaneArgs.g.cpp"
#include "AdjustFontSizeArgs.g.cpp"
#include "SendInputArgs.g.cpp"
#include "SplitPaneArgs.g.cpp"
@ -282,6 +283,32 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
};
}
winrt::hstring MovePaneArgs::GenerateName() const
{
winrt::hstring directionString;
switch (Direction())
{
case FocusDirection::Left:
directionString = RS_(L"DirectionLeft");
break;
case FocusDirection::Right:
directionString = RS_(L"DirectionRight");
break;
case FocusDirection::Up:
directionString = RS_(L"DirectionUp");
break;
case FocusDirection::Down:
directionString = RS_(L"DirectionDown");
break;
case FocusDirection::Previous:
return RS_(L"MovePaneToLastUsedPane");
}
return winrt::hstring{
fmt::format(std::wstring_view(RS_(L"MovePaneWithArgCommandKey")),
directionString)
};
}
winrt::hstring AdjustFontSizeArgs::GenerateName() const
{
// If the amount is just 1 (or -1), we'll just return "Increase font

View File

@ -12,6 +12,7 @@
#include "SwitchToTabArgs.g.h"
#include "ResizePaneArgs.g.h"
#include "MoveFocusArgs.g.h"
#include "MovePaneArgs.g.h"
#include "AdjustFontSizeArgs.g.h"
#include "SendInputArgs.g.h"
#include "SplitPaneArgs.g.h"
@ -451,6 +452,65 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
}
};
struct MovePaneArgs : public MovePaneArgsT<MovePaneArgs>
{
MovePaneArgs() = default;
MovePaneArgs(Model::FocusDirection direction) :
_Direction{ direction } {};
ACTION_ARG(Model::FocusDirection, Direction, FocusDirection::None);
static constexpr std::string_view DirectionKey{ "direction" };
public:
hstring GenerateName() const;
bool Equals(const IActionArgs& other)
{
auto otherAsUs = other.try_as<MovePaneArgs>();
if (otherAsUs)
{
return otherAsUs->_Direction == _Direction;
}
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<MovePaneArgs>();
JsonUtils::GetValueForKey(json, DirectionKey, args->_Direction);
if (args->Direction() == FocusDirection::None)
{
return { nullptr, { SettingsLoadWarnings::MissingRequiredParameter } };
}
else
{
return { *args, {} };
}
}
static Json::Value ToJson(const IActionArgs& val)
{
if (!val)
{
return {};
}
Json::Value json{ Json::ValueType::objectValue };
const auto args{ get_self<MovePaneArgs>(val) };
JsonUtils::SetValueForKey(json, DirectionKey, args->_Direction);
return json;
}
IActionArgs Copy() const
{
auto copy{ winrt::make_self<MovePaneArgs>() };
copy->_Direction = _Direction;
return *copy;
}
size_t Hash() const
{
return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(Direction());
}
};
struct AdjustFontSizeArgs : public AdjustFontSizeArgsT<AdjustFontSizeArgs>
{
AdjustFontSizeArgs() = default;
@ -1647,6 +1707,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
BASIC_FACTORY(NewTerminalArgs);
BASIC_FACTORY(NewTabArgs);
BASIC_FACTORY(MoveFocusArgs);
BASIC_FACTORY(MovePaneArgs);
BASIC_FACTORY(SplitPaneArgs);
BASIC_FACTORY(SetColorSchemeArgs);
BASIC_FACTORY(ExecuteCommandlineArgs);

View File

@ -158,6 +158,12 @@ namespace Microsoft.Terminal.Settings.Model
FocusDirection FocusDirection { get; };
};
[default_interface] runtimeclass MovePaneArgs : IActionArgs
{
MovePaneArgs(FocusDirection direction);
FocusDirection Direction { get; };
};
[default_interface] runtimeclass AdjustFontSizeArgs : IActionArgs
{
Int32 Delta { get; };

View File

@ -48,6 +48,7 @@
ON_ALL_ACTIONS(ScrollToBottom) \
ON_ALL_ACTIONS(ResizePane) \
ON_ALL_ACTIONS(MoveFocus) \
ON_ALL_ACTIONS(MovePane) \
ON_ALL_ACTIONS(Find) \
ON_ALL_ACTIONS(ToggleShaderEffects) \
ON_ALL_ACTIONS(ToggleFocusMode) \
@ -87,6 +88,7 @@
ON_ALL_ACTIONS_WITH_ARGS(FindMatch) \
ON_ALL_ACTIONS_WITH_ARGS(GlobalSummon) \
ON_ALL_ACTIONS_WITH_ARGS(MoveFocus) \
ON_ALL_ACTIONS_WITH_ARGS(MovePane) \
ON_ALL_ACTIONS_WITH_ARGS(MoveTab) \
ON_ALL_ACTIONS_WITH_ARGS(NewTab) \
ON_ALL_ACTIONS_WITH_ARGS(NewWindow) \

View File

@ -246,6 +246,16 @@
<data name="MoveFocusToLastUsedPane" xml:space="preserve">
<value>Move focus to the last used pane</value>
</data>
<data name="MovePaneCommandKey" xml:space="preserve">
<value>Move pane</value>
</data>
<data name="MovePaneWithArgCommandKey" xml:space="preserve">
<value>Move pane {0}</value>
<comment>{0} will be replaced with one of the four directions "DirectionLeft", "DirectionRight", "DirectionUp", "DirectionDown"</comment>
</data>
<data name="MovePaneToLastUsedPane" xml:space="preserve">
<value>Move pane to the last used pane</value>
</data>
<data name="NewTabCommandKey" xml:space="preserve">
<value>New tab</value>
</data>
@ -417,4 +427,4 @@
<value>Windows Console Host</value>
<comment>Name describing the usage of the classic windows console as the terminal UI. (`conhost.exe`)</comment>
</data>
</root>
</root>

View File

@ -343,7 +343,12 @@
{ "command": { "action": "moveFocus", "direction": "left" }, "keys": "alt+left" },
{ "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": "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": "togglePaneZoom" },
{ "command": "toggleReadOnlyMode" },