Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a move-focus subcommand #8546

Merged
15 commits merged into from
Jan 11, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ namespace TerminalAppLocalTests
TEST_METHOD(ParseSplitPaneIntoArgs);
TEST_METHOD(ParseComboCommandlineIntoArgs);
TEST_METHOD(ParseFocusTabArgs);
TEST_METHOD(ParseMoveFocusArgs);
TEST_METHOD(ParseArgumentsWithParsingTerminators);

TEST_METHOD(ParseNoCommandIsNewTab);
Expand Down Expand Up @@ -76,6 +77,23 @@ namespace TerminalAppLocalTests
appArgs.ValidateStartupCommands();
}

void _buildCommandlinesExpectFailureHelper(AppCommandlineArgs& appArgs,
const size_t expectedSubcommands,
std::vector<const wchar_t*>& rawCommands)
{
auto commandlines = AppCommandlineArgs::BuildCommands(rawCommands);
VERIFY_ARE_EQUAL(expectedSubcommands, commandlines.size());
for (auto& cmdBlob : commandlines)
{
const auto result = appArgs.ParseCommand(cmdBlob);
VERIFY_ARE_NOT_EQUAL(0, result);
VERIFY_ARE_NOT_EQUAL("", appArgs._exitMessage);
Log::Comment(NoThrowString().Format(
L"Exit Message:\n%hs",
appArgs._exitMessage.c_str()));
}
}

void _logCommandline(std::vector<const wchar_t*>& rawCommands)
{
std::wstring buffer;
Expand Down Expand Up @@ -995,6 +1013,124 @@ namespace TerminalAppLocalTests
}
}

void CommandlineTest::ParseMoveFocusArgs()
{
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 `mf` instead of `move-focus`");
const wchar_t* subcommand = useShortForm ? L"mf" : L"move-focus";

{
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::MoveFocus, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MoveFocusArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(winrt::TerminalApp::Direction::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::MoveFocus, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MoveFocusArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(winrt::TerminalApp::Direction::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::MoveFocus, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MoveFocusArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(winrt::TerminalApp::Direction::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::MoveFocus, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MoveFocusArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(winrt::TerminalApp::Direction::Down, myArgs.Direction());
}
{
AppCommandlineArgs appArgs{};
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"badDirection" };
Log::Comment(NoThrowString().Format(
L"move-focus 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::MoveFocus, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
auto myArgs = actionAndArgs.Args().try_as<MoveFocusArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(winrt::TerminalApp::Direction::Left, myArgs.Direction());

actionAndArgs = appArgs._startupActions.at(2);
VERIFY_ARE_EQUAL(ShortcutAction::MoveFocus, actionAndArgs.Action());
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
myArgs = actionAndArgs.Args().try_as<MoveFocusArgs>();
VERIFY_IS_NOT_NULL(myArgs);
VERIFY_ARE_EQUAL(winrt::TerminalApp::Direction::Right, myArgs.Direction());
}
}

void CommandlineTest::ValidateFirstCommandIsNewTab()
{
AppCommandlineArgs appArgs{};
Expand Down
59 changes: 59 additions & 0 deletions src/cascadia/TerminalApp/AppCommandlineArgs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ void AppCommandlineArgs::_buildParser()
_buildNewTabParser();
_buildSplitPaneParser();
_buildFocusTabParser();
_buildMoveFocusParser();
}

// Method Description:
Expand Down Expand Up @@ -337,6 +338,61 @@ void AppCommandlineArgs::_buildFocusTabParser()
setupSubcommand(_focusTabShort);
}

// Method Description:
// - Adds the `focus-tab` subcommand and related options to the commandline parser.
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
// - Additionally adds the `ft` subcommand, which is just a shortened version of `focus-tab`
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildMoveFocusParser()
{
_moveFocusCommand = _app.add_subcommand("move-focus", RS_A(L"CmdMoveFocusDesc"));
_moveFocusShort = _app.add_subcommand("mf", RS_A(L"CmdMFDesc"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zadjii-msft - dumb question.. let's say we introduce "m" for minimized, and then I write -mf. What will the command line do: minimized focus or move focus? 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably, wt -mf sp ; mf left will still work, because the --minimizeFocus arg will be an option, not a subcommand. I'd think that CLI11 should be able to differentiate between the -mf and mf args. If not, that would definitely be an upstream bug 😄


auto setupSubcommand = [this](auto* subcommand) {
std::map<std::string, Direction> map = {
{ "left", Direction::Left },
{ "right", Direction::Right },
{ "up", Direction::Up },
{ "down", Direction::Down }
};

auto* directionOpt = subcommand->add_option("direction",
_moveFocusDirection,
RS_A(L"CmdMoveFocusDirectionArgDesc"));

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 (_moveFocusDirection != Direction::None)
{
// auto moveFocusAction = winrt::make_self<implementation::ActionAndArgs>();
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved

// moveFocusAction->Action(ShortcutAction::MoveFocus);
// auto args = winrt::make_self<implementation::MoveFocusArgs>();
MoveFocusArgs args{ _moveFocusDirection };
// ActionAndArgs actionAndArgs {args};

ActionAndArgs actionAndArgs{};
actionAndArgs.Action(ShortcutAction::MoveFocus);
actionAndArgs.Args(args);

// args->Direction(_moveFocusDirection);
// moveFocusAction->Args(*args);
_startupActions.push_back(std::move(actionAndArgs));
}
});
};

setupSubcommand(_moveFocusCommand);
setupSubcommand(_moveFocusShort);
}

// Method Description:
// - Add the `NewTerminalArgs` parameters to the given subcommand. This enables
// that subcommand to support all the properties in a NewTerminalArgs.
Expand Down Expand Up @@ -444,6 +500,8 @@ bool AppCommandlineArgs::_noCommandsProvided()
*_newTabShort.subcommand ||
*_focusTabCommand ||
*_focusTabShort ||
*_moveFocusCommand ||
*_moveFocusShort ||
*_newPaneShort.subcommand ||
*_newPaneCommand.subcommand);
}
Expand Down Expand Up @@ -471,6 +529,7 @@ void AppCommandlineArgs::_resetStateToDefault()
_focusNextTab = false;
_focusPrevTab = false;

_moveFocusDirection = Direction::None;
// DON'T clear _launchMode here! This will get called once for every
// subcommand, so we don't want `wt -F new-tab ; split-pane` clearing out
// the "global" fullscreen flag (-F).
Expand Down
6 changes: 6 additions & 0 deletions src/cascadia/TerminalApp/AppCommandlineArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,18 @@ class TerminalApp::AppCommandlineArgs final
NewPaneSubcommand _newPaneShort;
CLI::App* _focusTabCommand;
CLI::App* _focusTabShort;
CLI::App* _moveFocusCommand;
CLI::App* _moveFocusShort;

// Are you adding a new sub-command? Make sure to update _noCommandsProvided!
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved

std::string _profileName;
std::string _startingDirectory;
std::string _startingTitle;
std::string _startingTabColor;

winrt::Microsoft::Terminal::Settings::Model::Direction _moveFocusDirection{ winrt::Microsoft::Terminal::Settings::Model::Direction::None };

// _commandline will contain the command line with which we'll be spawning a new terminal
std::vector<std::string> _commandline;

Expand All @@ -102,6 +107,7 @@ class TerminalApp::AppCommandlineArgs final
void _buildNewTabParser();
void _buildSplitPaneParser();
void _buildFocusTabParser();
void _buildMoveFocusParser();
bool _noCommandsProvided();
void _resetStateToDefault();
int _handleExit(const CLI::App& command, const CLI::Error& e);
Expand Down
13 changes: 13 additions & 0 deletions src/cascadia/TerminalApp/Pane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,19 @@ void Pane::_FocusFirstChild()
{
if (_IsLeaf())
{
if (_root.ActualWidth() == 0 && _root.ActualHeight() == 0)
{
// When these sizes are 0, then the pane might still be in startup,
// and doesn't yet have a real size. In that case, the control.Focus
// event won't be handled until _after_ the startup events are all
// processed. THis will lead to the Tab not being notified that the
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
// focus moved to a different Pane.
//
// In that scenario, trigger the event manually here, to correctly
// inform the Tab that we're now focused.
_GotFocusHandlers(shared_from_this());
}

_control.Focus(FocusState::Programmatic);
}
else
Expand Down
66 changes: 38 additions & 28 deletions src/cascadia/TerminalApp/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema

Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
Expand All @@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple

There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Expand Down Expand Up @@ -311,6 +311,16 @@
<data name="CmdFullscreenDesc" xml:space="preserve">
<value>Launch the window in fullscreen mode</value>
</data>
<data name="CmdMoveFocusDesc" xml:space="preserve">
<value>Move focus to the adjacent pane in the specified direction</value>
</data>
<data name="CmdMFDesc" xml:space="preserve">
<value>An alias for the "move-focus" subcommand.</value>
<comment>{Locked="\"move-focus\""}</comment>
</data>
<data name="CmdMoveFocusDirectionArgDesc" xml:space="preserve">
<value>The direction to move focus in</value>
</data>
<data name="CmdFocusDesc" xml:space="preserve">
<value>Launch the window in focus mode</value>
</data>
Expand Down Expand Up @@ -513,4 +523,4 @@
<data name="NoticeWarning" xml:space="preserve">
<value>Warning</value>
</data>
</root>
</root>