start working on nested actions

This commit is contained in:
Mike Griese
2020-07-08 14:13:31 -05:00
parent 7321639de2
commit d953d8bedc
7 changed files with 145 additions and 27 deletions

View File

@@ -85,6 +85,10 @@ namespace TerminalAppLocalTests
TEST_METHOD(TestIterateCommands);
TEST_METHOD(TestIterateAutogenNamedCommands);
TEST_METHOD(TestIterateOnBadJson);
TEST_METHOD(TestNestedCommands);
TEST_METHOD(TestNestedInIterableCommand);
TEST_METHOD(TestIterableInNestedCommand);
TEST_METHOD(TestNestedCommandWithoutName);
TEST_CLASS_SETUP(ClassSetup)
{
@@ -2927,4 +2931,50 @@ namespace TerminalAppLocalTests
}
}
void SettingsTests::TestNestedCommands()
{
// This test
Log::Result(WEX::Logging::TestResults::Skipped);
}
void SettingsTests::TestNestedInIterableCommand()
{
// This test checks a iterable command that includes a nested command.
// The commands should look like:
//
// <Command Palette>
// └─ New Pane...
// ├─ Profile 1...
// | ├─ Split Automatically
// | ├─ Split Vertically
// | └─ Split Horizontally
// ├─ Profile 2...
// | ├─ Split Automatically
// | ├─ Split Vertically
// | └─ Split Horizontally
// └─ Profile 3...
// ├─ Split Automatically
// ├─ Split Vertically
// └─ Split Horizontally
Log::Result(WEX::Logging::TestResults::Skipped);
}
void SettingsTests::TestIterableInNestedCommand()
{
// This test checks a nested command that includes an iterable command.
// The commands should look like:
//
// <Command Palette>
// └─ New Tab With Profile...
// ├─ Profile 1
// ├─ Profile 2
// └─ Profile 3
Log::Result(WEX::Logging::TestResults::Skipped);
}
void SettingsTests::TestNestedCommandWithoutName()
{
// This test tests a nested command without a name specified. This type
// of command should just be ignored, since we can't auto-generate names
// for nested commands, they _must_ have names specified.
Log::Result(WEX::Logging::TestResults::Skipped);
}
}

View File

@@ -11,13 +11,14 @@
using namespace winrt::Microsoft::Terminal::Settings;
using namespace winrt::TerminalApp;
using namespace winrt::Windows::Foundation;
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view IconPathKey{ "iconPath" };
static constexpr std::string_view ActionKey{ "command" };
static constexpr std::string_view ArgsKey{ "args" };
static constexpr std::string_view IterateOnKey{ "iterateOn" };
static constexpr std::string_view CommandsKey{ "commands" };
static constexpr std::string_view IterateOnProfilesValue{ "profiles" };
@@ -25,6 +26,16 @@ static constexpr std::wstring_view ProfileName{ L"${profile.name}" };
namespace winrt::TerminalApp::implementation
{
Command::Command()
{
_nestedCommandsView = winrt::single_threaded_observable_vector<winrt::TerminalApp::Command>();
}
Collections::IObservableVector<winrt::TerminalApp::Command> Command::NestedCommands()
{
return _nestedCommandsView;
}
// Function Description:
// - attempt to get the name of this command from the provided json object.
// * If the "name" property is a string, return that value.
@@ -108,51 +119,95 @@ namespace winrt::TerminalApp::implementation
// Return Value:
// - the newly constructed Command object.
winrt::com_ptr<Command> Command::FromJson(const Json::Value& json,
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings)
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings,
const bool postExpansion)
{
auto result = winrt::make_self<Command>();
if (const auto iterateOnJson{ json[JsonKey(IterateOnKey)] })
bool iterable = false;
bool nested = false;
if (!postExpansion)
{
auto s = iterateOnJson.asString();
if (s == IterateOnProfilesValue)
if (const auto iterateOnJson{ json[JsonKey(IterateOnKey)] })
{
result->_IterateOn = ExpandCommandType::Profiles;
auto s = iterateOnJson.asString();
if (s == IterateOnProfilesValue)
{
result->_IterateOn = ExpandCommandType::Profiles;
iterable = true;
}
}
}
// For iterable commands, we'll make another pass at parsing them once
// the json is patched. So ignore parsing sub-commands for now. Commands
// will only be marked iterable on the first pass.
if (!iterable)
{
if (const auto nestedCommandsJson{ json[JsonKey(CommandsKey)] })
{
auto nestedWarnings = Command::LayerJson(result->_subcommands, nestedCommandsJson);
// It's possible that the nested commands have some warnings
warnings.insert(warnings.end(), nestedWarnings.begin(), nestedWarnings.end());
// Add all the commands we've parsed to the observable vector we
// have, so we can access them in XAML.
for (auto& nameAndCommand : result->_subcommands)
{
auto command = nameAndCommand.second;
result->_nestedCommandsView.Append(command);
}
nested = true;
}
// TODO: else if (hasKey(CommandKey) )
// {
// // { name: "foo", commands: null } will land in this case, which
// // should also be used for unbinding.
// return nullptr;
// }
}
// TODO GH#6644: iconPath not implemented quite yet. Can't seem to get
// the binding quite right. Additionally, do we want it to be an image,
// or a FontIcon? I've had difficulty binding either/or.
if (const auto actionJson{ json[JsonKey(ActionKey)] })
// If we're a nested command, we can ignore the current command.
if (!nested)
{
auto actionAndArgs = ActionAndArgs::FromJson(actionJson, warnings);
if (actionAndArgs)
if (const auto actionJson{ json[JsonKey(ActionKey)] })
{
result->_setAction(*actionAndArgs);
auto actionAndArgs = ActionAndArgs::FromJson(actionJson, warnings);
if (actionAndArgs)
{
result->_setAction(*actionAndArgs);
}
else
{
// Something like
// { name: "foo", action: "unbound" }
// will _remove_ the "foo" command, by returning null here.
return nullptr;
}
// If an iterable command doesn't have a name set, we'll still just
// try and generate a fake name for the command give the string we
// currently have. It'll probably generate something like "New tab,
// profile: ${profile.name}". This string will only be temporarily
// used internally, so there's no problem.
result->_setName(_nameFromJsonOrAction(json, actionAndArgs));
}
else
{
// Something like
// { name: "foo", action: "unbound" }
// will _remove_ the "foo" command, by returning null here.
// { name: "foo", action: null } will land in this case, which
// should also be used for unbinding.
return nullptr;
}
// If an iterable command doesn't have a name set, we'll still just
// try and generate a fake name for the command give the string we
// currently have. It'll probably generate something like "New tab,
// profile: ${profile.name}". This string will only be temporarily
// used internally, so there's no problem.
result->_setName(_nameFromJsonOrAction(json, actionAndArgs));
}
else
{
// { name: "foo", action: null } will land in this case, which
// should also be used for unbinding.
return nullptr;
result->_setName(_nameFromJson(json));
}
// Stash the original json value in this object. If the command is
@@ -332,7 +387,7 @@ namespace winrt::TerminalApp::implementation
}
// Pass the new json back though FromJson, to get the new expanded value.
if (auto newCmd{ Command::FromJson(newJsonValue, warnings) })
if (auto newCmd{ Command::FromJson(newJsonValue, warnings, true) })
{
newCommands.push_back(*newCmd);
}

View File

@@ -33,9 +33,11 @@ namespace winrt::TerminalApp::implementation
struct Command : CommandT<Command>
{
Command() = default;
Command();
static winrt::com_ptr<Command> FromJson(const Json::Value& json, std::vector<::TerminalApp::SettingsLoadWarnings>& warnings);
static winrt::com_ptr<Command> FromJson(const Json::Value& json,
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings,
const bool postExpansion = false);
static std::vector<winrt::TerminalApp::Command> ExpandCommand(winrt::com_ptr<Command> expandable,
const std::vector<::TerminalApp::Profile>& profiles,
@@ -44,6 +46,8 @@ namespace winrt::TerminalApp::implementation
static std::vector<::TerminalApp::SettingsLoadWarnings> LayerJson(std::unordered_map<winrt::hstring, winrt::TerminalApp::Command>& commands,
const Json::Value& json);
Windows::Foundation::Collections::IObservableVector<TerminalApp::Command> NestedCommands();
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Name, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::TerminalApp::ActionAndArgs, Action, _PropertyChangedHandlers);
@@ -52,6 +56,8 @@ namespace winrt::TerminalApp::implementation
private:
Json::Value _originalJson;
std::unordered_map<winrt::hstring, winrt::TerminalApp::Command> _subcommands;
Windows::Foundation::Collections::IObservableVector<TerminalApp::Command> _nestedCommandsView{ nullptr };
};
}

View File

@@ -12,5 +12,7 @@ namespace TerminalApp
String Name;
ActionAndArgs Action;
String KeyChordText;
Windows.Foundation.Collections.IObservableVector<Command> NestedCommands { get; };
}
}

View File

@@ -22,6 +22,7 @@ namespace winrt::TerminalApp::implementation
_filteredActions = winrt::single_threaded_observable_vector<winrt::TerminalApp::Command>();
_allActions = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
_nestedActionStack = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
if (CommandPaletteShadow())
{

View File

@@ -22,6 +22,7 @@ namespace winrt::TerminalApp::implementation
Windows::Foundation::Collections::IObservableVector<TerminalApp::Command> _filteredActions{ nullptr };
Windows::Foundation::Collections::IVector<TerminalApp::Command> _allActions{ nullptr };
Windows::Foundation::Collections::IVector<TerminalApp::Command> _nestedActionStack{ nullptr };
winrt::TerminalApp::ShortcutActionDispatch _dispatch;
void _filterTextChanged(Windows::Foundation::IInspectable const& sender,

View File

@@ -105,6 +105,9 @@ namespace winrt::TerminalApp::implementation
// unset by default, so we don't need to worry about clearing it
// if there isn't a key associated with it.
auto keyChord{ _settings->GetKeybindings().GetKeyBindingForActionWithArgs(command.Action()) };
// TODO: Make this recursive, because commands might have
// subcommands that also have keybindings
if (keyChord)
{
command.KeyChordText(KeyChordSerialization::ToString(keyChord));