diff --git a/src/cascadia/LocalTests_TerminalApp/CommandTests.cpp b/src/cascadia/LocalTests_TerminalApp/CommandTests.cpp new file mode 100644 index 00000000000..129e2469182 --- /dev/null +++ b/src/cascadia/LocalTests_TerminalApp/CommandTests.cpp @@ -0,0 +1,349 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalApp/CascadiaSettings.h" +#include "JsonTestClass.h" +#include "TestUtils.h" + +using namespace Microsoft::Console; +using namespace TerminalApp; +using namespace winrt::TerminalApp; +using namespace winrt::Microsoft::Terminal::Settings; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; + +namespace TerminalAppLocalTests +{ + // TODO:microsoft/terminal#3838: + // Unfortunately, these tests _WILL NOT_ work in our CI. We're waiting for + // an updated TAEF that will let us install framework packages when the test + // package is deployed. Until then, these tests won't deploy in CI. + + class CommandTests : public JsonTestClass + { + // Use a custom AppxManifest to ensure that we can activate winrt types + // from our test. This property will tell taef to manually use this as + // the AppxManifest for this test class. + // This does not yet work for anything XAML-y. See TabTests.cpp for more + // details on that. + BEGIN_TEST_CLASS(CommandTests) + TEST_CLASS_PROPERTY(L"RunAs", L"UAP") + TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") + END_TEST_CLASS() + + TEST_METHOD(ManyCommandsSameAction); + TEST_METHOD(LayerCommand); + TEST_METHOD(TestSplitPaneArgs); + TEST_METHOD(TestResourceKeyName); + TEST_METHOD(TestAutogeneratedName); + TEST_METHOD(TestLayerOnAutogeneratedName); + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + return true; + } + }; + + void CommandTests::ManyCommandsSameAction() + { + const std::string commands0String{ R"([ { "name":"action0", "command": "copy" } ])" }; + const std::string commands1String{ R"([ { "name":"action1", "command": { "action": "copy", "singleLine": false } } ])" }; + const std::string commands2String{ R"([ + { "name":"action2", "command": "paste" }, + { "name":"action3", "command": "paste" } + ])" }; + + const auto commands0Json = VerifyParseSucceeded(commands0String); + const auto commands1Json = VerifyParseSucceeded(commands1String); + const auto commands2Json = VerifyParseSucceeded(commands2String); + + std::unordered_map commands; + VERIFY_ARE_EQUAL(0u, commands.size()); + { + auto warnings = implementation::Command::LayerJson(commands, commands0Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + } + VERIFY_ARE_EQUAL(1u, commands.size()); + + { + auto warnings = implementation::Command::LayerJson(commands, commands1Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + } + VERIFY_ARE_EQUAL(2u, commands.size()); + + { + auto warnings = implementation::Command::LayerJson(commands, commands2Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + } + VERIFY_ARE_EQUAL(4u, commands.size()); + } + + void CommandTests::LayerCommand() + { + // Each one of the commands in this test should layer upon the previous, overriding the action. + const std::string commands0String{ R"([ { "name":"action0", "command": "copy" } ])" }; + const std::string commands1String{ R"([ { "name":"action0", "command": "paste" } ])" }; + const std::string commands2String{ R"([ { "name":"action0", "command": "newTab" } ])" }; + const std::string commands3String{ R"([ { "name":"action0", "command": null } ])" }; + + const auto commands0Json = VerifyParseSucceeded(commands0String); + const auto commands1Json = VerifyParseSucceeded(commands1String); + const auto commands2Json = VerifyParseSucceeded(commands2String); + const auto commands3Json = VerifyParseSucceeded(commands3String); + + std::unordered_map commands; + VERIFY_ARE_EQUAL(0u, commands.size()); + { + auto warnings = implementation::Command::LayerJson(commands, commands0Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + VERIFY_ARE_EQUAL(1u, commands.size()); + auto command = commands.at(L"action0"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::CopyText, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + } + { + auto warnings = implementation::Command::LayerJson(commands, commands1Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + VERIFY_ARE_EQUAL(1u, commands.size()); + auto command = commands.at(L"action0"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::PasteText, command.Action().Action()); + VERIFY_IS_NULL(command.Action().Args()); + } + { + auto warnings = implementation::Command::LayerJson(commands, commands2Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + VERIFY_ARE_EQUAL(1u, commands.size()); + auto command = commands.at(L"action0"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + } + { + // This last command should "unbind" the action. + auto warnings = implementation::Command::LayerJson(commands, commands3Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + VERIFY_ARE_EQUAL(0u, commands.size()); + } + } + + void CommandTests::TestSplitPaneArgs() + { + // This is the same as KeyBindingsTests::TestSplitPaneArgs, but with + // looking up the action and its args from a map of commands, instead + // of from keybindings. + + const std::string commands0String{ R"([ + { "name": "command0", "command": { "action": "splitPane", "split": null } }, + { "name": "command1", "command": { "action": "splitPane", "split": "vertical" } }, + { "name": "command2", "command": { "action": "splitPane", "split": "horizontal" } }, + { "name": "command3", "command": { "action": "splitPane", "split": "none" } }, + { "name": "command4", "command": { "action": "splitPane" } }, + { "name": "command5", "command": { "action": "splitPane", "split": "auto" } }, + { "name": "command6", "command": { "action": "splitPane", "split": "foo" } } + ])" }; + + const auto commands0Json = VerifyParseSucceeded(commands0String); + + std::unordered_map commands; + VERIFY_ARE_EQUAL(0u, commands.size()); + auto warnings = implementation::Command::LayerJson(commands, commands0Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + VERIFY_ARE_EQUAL(7u, commands.size()); + + { + auto command = commands.at(L"command0"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + } + { + auto command = commands.at(L"command1"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + } + { + auto command = commands.at(L"command2"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + } + { + auto command = commands.at(L"command3"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + } + { + auto command = commands.at(L"command4"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + } + { + auto command = commands.at(L"command5"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + } + { + auto command = commands.at(L"command6"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + } + } + void CommandTests::TestResourceKeyName() + { + // This test checks looking up a name from a resource key. + + const std::string commands0String{ R"([ { "name": { "key": "DuplicateTabCommandKey"}, "command": "copy" } ])" }; + const auto commands0Json = VerifyParseSucceeded(commands0String); + + std::unordered_map commands; + VERIFY_ARE_EQUAL(0u, commands.size()); + { + auto warnings = implementation::Command::LayerJson(commands, commands0Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + VERIFY_ARE_EQUAL(1u, commands.size()); + + // NOTE: We're relying on DuplicateTabCommandKey being defined as + // "Duplicate Tab" here. If that string changes in our resources, + // this test will break. + auto command = commands.at(L"Duplicate tab"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::CopyText, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + } + } + + void CommandTests::TestAutogeneratedName() + { + // This test ensures that we'll correctly create commands for actions + // that don't have given names, pursuant to the spec in GH#6532. + + // NOTE: The keys used to look up these commands are partially generated + // from strings in our Resources.resw. If those string values should + // change, it's likely that this test will break. + + const std::string commands0String{ R"([ + { "command": { "action": "splitPane", "split": null } }, + { "command": { "action": "splitPane", "split": "vertical" } }, + { "command": { "action": "splitPane", "split": "horizontal" } }, + { "command": { "action": "splitPane", "split": "none" } }, + { "command": { "action": "splitPane" } }, + { "command": { "action": "splitPane", "split": "auto" } }, + { "command": { "action": "splitPane", "split": "foo" } } + ])" }; + + const auto commands0Json = VerifyParseSucceeded(commands0String); + + std::unordered_map commands; + VERIFY_ARE_EQUAL(0u, commands.size()); + auto warnings = implementation::Command::LayerJson(commands, commands0Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + + // There are only 3 commands here: all of the `"none"`, `"auto"`, + // `"foo"`, `null`, and bindings all generate the same action, + // which will generate just a single name for all of them. + VERIFY_ARE_EQUAL(3u, commands.size()); + + { + auto command = commands.at(L"Split pane"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + } + { + auto command = commands.at(L"Split pane, direction: vertical"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + } + { + auto command = commands.at(L"Split pane, direction: horizontal"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + } + } + void CommandTests::TestLayerOnAutogeneratedName() + { + const std::string commands0String{ R"([ + { "command": { "action": "splitPane" } }, + { "name":"Split pane", "command": { "action": "splitPane", "split": "vertical" } }, + ])" }; + + const auto commands0Json = VerifyParseSucceeded(commands0String); + + std::unordered_map commands; + VERIFY_ARE_EQUAL(0u, commands.size()); + auto warnings = implementation::Command::LayerJson(commands, commands0Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + VERIFY_ARE_EQUAL(1u, commands.size()); + + { + auto command = commands.at(L"Split pane"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + } + } +} diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index 4ef477929a1..938ebd291df 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -80,6 +80,8 @@ namespace TerminalAppLocalTests TEST_METHOD(TestTrailingCommas); + TEST_METHOD(TestCommandsAndKeybindings); + TEST_CLASS_SETUP(ClassSetup) { InitializeJsonReader(); @@ -2331,4 +2333,204 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(false, L"This call to LayerJson should succeed, even with the trailing comma"); } } + + void SettingsTests::TestCommandsAndKeybindings() + { + const std::string settingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" + } + ], + "bindings": [ + { "keys": "ctrl+a", "command": { "action": "splitPane", "split": "vertical" } }, + { "name": "ctrl+b", "command": { "action": "splitPane", "split": "vertical" } }, + { "keys": "ctrl+c", "name": "ctrl+c", "command": { "action": "splitPane", "split": "vertical" } }, + { "keys": "ctrl+d", "command": { "action": "splitPane", "split": "vertical" } }, + { "keys": "ctrl+e", "command": { "action": "splitPane", "split": "horizontal" } }, + { "keys": "ctrl+f", "name":null, "command": { "action": "splitPane", "split": "horizontal" } } + ] + })" }; + + const auto guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"); + const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + + VerifyParseSucceeded(settingsJson); + CascadiaSettings settings{}; + settings._ParseJsonString(settingsJson, false); + settings.LayerJson(settings._userSettings); + settings._ValidateSettings(); + + VERIFY_ARE_EQUAL(3u, settings.GetProfiles().size()); + + const auto profile2Guid = settings._profiles.at(2).GetGuid(); + VERIFY_ARE_NOT_EQUAL(GUID{ 0 }, profile2Guid); + + auto appKeyBindings = settings._globals._keybindings; + VERIFY_ARE_EQUAL(5u, appKeyBindings->_keyShortcuts.size()); + + // A/D, B, C, E will be in the list of commands, for 4 total. + // * A and D share the same name, so they'll only generate a single action. + // * F's name is set manually to `null` + auto commands = settings._globals.GetCommands(); + VERIFY_ARE_EQUAL(4u, commands.size()); + + { + KeyChord kc{ true, false, false, static_cast('A') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + + Log::Comment(L"Note that we're skipping ctrl+B, since that doesn't have `keys` set."); + + { + KeyChord kc{ true, false, false, static_cast('C') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + KeyChord kc{ true, false, false, static_cast('D') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + KeyChord kc{ true, false, false, static_cast('E') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + KeyChord kc{ true, false, false, static_cast('F') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + + Log::Comment(L"Now verify the commands"); + + { + auto command = commands.at(L"Split pane, direction: Vertical"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + auto command = commands.at(L"ctrl+b"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + auto command = commands.at(L"ctrl+c"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + auto command = commands.at(L"Split pane, direction: Horizontal"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + } + } diff --git a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj index ee5d3972a57..7fe855df01e 100644 --- a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj @@ -60,6 +60,7 @@ + Create diff --git a/src/cascadia/TerminalApp/ActionAndArgs.cpp b/src/cascadia/TerminalApp/ActionAndArgs.cpp index b69dd978694..9ba804b384d 100644 --- a/src/cascadia/TerminalApp/ActionAndArgs.cpp +++ b/src/cascadia/TerminalApp/ActionAndArgs.cpp @@ -2,11 +2,7 @@ #include "ActionArgs.h" #include "ActionAndArgs.h" #include "ActionAndArgs.g.cpp" - -static constexpr std::string_view ActionKey{ "action" }; - -// This key is reserved to remove a keybinding, instead of mapping it to an action. -static constexpr std::string_view UnboundKey{ "unbound" }; +#include static constexpr std::string_view CopyTextKey{ "copy" }; static constexpr std::string_view PasteTextKey{ "paste" }; @@ -36,6 +32,12 @@ static constexpr std::string_view ToggleFullscreenKey{ "toggleFullscreen" }; static constexpr std::string_view SetTabColorKey{ "setTabColor" }; static constexpr std::string_view OpenTabColorPickerKey{ "openTabColorPicker" }; static constexpr std::string_view RenameTabKey{ "renameTab" }; +static constexpr std::string_view ToggleCommandPaletteKey{ "commandPalette" }; + +static constexpr std::string_view ActionKey{ "action" }; + +// This key is reserved to remove a keybinding, instead of mapping it to an action. +static constexpr std::string_view UnboundKey{ "unbound" }; namespace winrt::TerminalApp::implementation { @@ -47,7 +49,7 @@ namespace winrt::TerminalApp::implementation // the actual strings being pointed to. However, since both these strings and // the map are all const for the lifetime of the app, we have nothing to worry // about here. - const std::map> ActionAndArgs::ActionNamesMap{ + const std::map> ActionAndArgs::ActionKeyNamesMap{ { CopyTextKey, ShortcutAction::CopyText }, { PasteTextKey, ShortcutAction::PasteText }, { OpenNewTabDropdownKey, ShortcutAction::OpenNewTabDropdown }, @@ -75,7 +77,8 @@ namespace winrt::TerminalApp::implementation { OpenTabColorPickerKey, ShortcutAction::OpenTabColorPicker }, { UnboundKey, ShortcutAction::Invalid }, { FindKey, ShortcutAction::Find }, - { RenameTabKey, ShortcutAction::RenameTab } + { RenameTabKey, ShortcutAction::RenameTab }, + { ToggleCommandPaletteKey, ShortcutAction::ToggleCommandPalette }, }; using ParseResult = std::tuple>; @@ -121,8 +124,8 @@ namespace winrt::TerminalApp::implementation { // Try matching the command to one we have. If we can't find the // action name in our list of names, let's just unbind that key. - const auto found = ActionAndArgs::ActionNamesMap.find(actionString); - return found != ActionAndArgs::ActionNamesMap.end() ? found->second : ShortcutAction::Invalid; + const auto found = ActionAndArgs::ActionKeyNamesMap.find(actionString); + return found != ActionAndArgs::ActionKeyNamesMap.end() ? found->second : ShortcutAction::Invalid; } // Method Description: @@ -219,4 +222,55 @@ namespace winrt::TerminalApp::implementation } } + winrt::hstring ActionAndArgs::GenerateName() const + { + // Use a magic static to initialize this map, because we won't be able + // to load the resources at _init_, only at runtime. + static const auto GeneratedActionNames = []() { + return std::unordered_map{ + { ShortcutAction::CopyText, RS_(L"CopyTextCommandKey") }, + { ShortcutAction::PasteText, RS_(L"PasteTextCommandKey") }, + { ShortcutAction::OpenNewTabDropdown, RS_(L"OpenNewTabDropdownCommandKey") }, + { ShortcutAction::DuplicateTab, RS_(L"DuplicateTabCommandKey") }, + { ShortcutAction::NewTab, RS_(L"NewTabCommandKey") }, + { ShortcutAction::NewWindow, RS_(L"NewWindowCommandKey") }, + { ShortcutAction::CloseWindow, RS_(L"CloseWindowCommandKey") }, + { ShortcutAction::CloseTab, RS_(L"CloseTabCommandKey") }, + { ShortcutAction::ClosePane, RS_(L"ClosePaneCommandKey") }, + { ShortcutAction::NextTab, RS_(L"NextTabCommandKey") }, + { ShortcutAction::PrevTab, RS_(L"PrevTabCommandKey") }, + { ShortcutAction::AdjustFontSize, RS_(L"AdjustFontSizeCommandKey") }, + { ShortcutAction::ResetFontSize, RS_(L"ResetFontSizeCommandKey") }, + { ShortcutAction::ScrollUp, RS_(L"ScrollUpCommandKey") }, + { ShortcutAction::ScrollDown, RS_(L"ScrollDownCommandKey") }, + { ShortcutAction::ScrollUpPage, RS_(L"ScrollUpPageCommandKey") }, + { ShortcutAction::ScrollDownPage, RS_(L"ScrollDownPageCommandKey") }, + { ShortcutAction::SwitchToTab, RS_(L"SwitchToTabCommandKey") }, + { ShortcutAction::ResizePane, RS_(L"ResizePaneCommandKey") }, + { ShortcutAction::MoveFocus, RS_(L"MoveFocusCommandKey") }, + { ShortcutAction::OpenSettings, RS_(L"OpenSettingsCommandKey") }, + { ShortcutAction::ToggleFullscreen, RS_(L"ToggleFullscreenCommandKey") }, + { ShortcutAction::SplitPane, RS_(L"SplitPaneCommandKey") }, + { ShortcutAction::Invalid, L"" }, + { ShortcutAction::Find, RS_(L"FindCommandKey") }, + { ShortcutAction::SetTabColor, RS_(L"ResetTabColorCommandKey") }, + { ShortcutAction::OpenTabColorPicker, RS_(L"OpenTabColorPickerCommandKey") }, + { ShortcutAction::RenameTab, RS_(L"ResetTabNameCommandKey") }, + { ShortcutAction::ToggleCommandPalette, RS_(L"ToggleCommandPaletteCommandKey") }, + }; + }(); + + if (_Args) + { + auto nameFromArgs = _Args.GenerateName(); + if (!nameFromArgs.empty()) + { + return nameFromArgs; + } + } + + const auto found = GeneratedActionNames.find(_Action); + return found != GeneratedActionNames.end() ? found->second : L""; + } + } diff --git a/src/cascadia/TerminalApp/ActionAndArgs.h b/src/cascadia/TerminalApp/ActionAndArgs.h index a1d16cbae16..bc202216379 100644 --- a/src/cascadia/TerminalApp/ActionAndArgs.h +++ b/src/cascadia/TerminalApp/ActionAndArgs.h @@ -7,11 +7,13 @@ namespace winrt::TerminalApp::implementation { struct ActionAndArgs : public ActionAndArgsT { - static const std::map> ActionNamesMap; + static const std::map> ActionKeyNamesMap; static winrt::com_ptr FromJson(const Json::Value& json, std::vector<::TerminalApp::SettingsLoadWarnings>& warnings); ActionAndArgs() = default; + hstring GenerateName() const; + GETSET_PROPERTY(TerminalApp::ShortcutAction, Action, TerminalApp::ShortcutAction::Invalid); GETSET_PROPERTY(IActionArgs, Args, nullptr); }; diff --git a/src/cascadia/TerminalApp/ActionArgs.cpp b/src/cascadia/TerminalApp/ActionArgs.cpp index 1c7eae2abab..012eacd7f22 100644 --- a/src/cascadia/TerminalApp/ActionArgs.cpp +++ b/src/cascadia/TerminalApp/ActionArgs.cpp @@ -17,3 +17,245 @@ #include "OpenSettingsArgs.g.cpp" #include "SetTabColorArgs.g.cpp" #include "RenameTabArgs.g.cpp" + +#include + +namespace winrt::TerminalApp::implementation +{ + winrt::hstring NewTerminalArgs::GenerateName() const + { + std::wstringstream ss; + + if (!_Profile.empty()) + { + ss << fmt::format(L"profile: {}, ", _Profile); + } + else if (_ProfileIndex) + { + ss << fmt::format(L"profile index: {}, ", _ProfileIndex.Value()); + } + + if (!_Commandline.empty()) + { + ss << fmt::format(L"commandline: {}, ", _Commandline); + } + + if (!_StartingDirectory.empty()) + { + ss << fmt::format(L"directory: {}, ", _StartingDirectory); + } + + if (!_TabTitle.empty()) + { + ss << fmt::format(L"title: {}, ", _TabTitle); + } + auto s = ss.str(); + if (s.empty()) + { + return L""; + } + + // Chop off the last ", " + return winrt::hstring{ s.substr(0, s.size() - 2) }; + } + + winrt::hstring CopyTextArgs::GenerateName() const + { + if (_SingleLine) + { + return RS_(L"CopyTextAsSingleLineCommandKey"); + } + return RS_(L"CopyTextCommandKey"); + } + + winrt::hstring NewTabArgs::GenerateName() const + { + winrt::hstring newTerminalArgsStr; + if (_TerminalArgs) + { + newTerminalArgsStr = _TerminalArgs.GenerateName(); + } + + if (newTerminalArgsStr.empty()) + { + return RS_(L"NewTabCommandKey"); + } + return winrt::hstring{ + fmt::format(L"{}, {}", RS_(L"NewTabCommandKey"), newTerminalArgsStr) + }; + } + + winrt::hstring SwitchToTabArgs::GenerateName() const + { + return winrt::hstring{ + fmt::format(L"{}, index:{}", RS_(L"SwitchToTabCommandKey"), _TabIndex) + }; + } + + winrt::hstring ResizePaneArgs::GenerateName() const + { + winrt::hstring directionString; + switch (_Direction) + { + case Direction::Left: + directionString = RS_(L"DirectionLeft"); + break; + case Direction::Right: + directionString = RS_(L"DirectionRight"); + break; + case Direction::Up: + directionString = RS_(L"DirectionUp"); + break; + case Direction::Down: + directionString = RS_(L"DirectionDown"); + break; + } + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"ResizePaneWithArgCommandKey")), + directionString) + }; + } + + winrt::hstring MoveFocusArgs::GenerateName() const + { + winrt::hstring directionString; + switch (_Direction) + { + case Direction::Left: + directionString = RS_(L"DirectionLeft"); + break; + case Direction::Right: + directionString = RS_(L"DirectionRight"); + break; + case Direction::Up: + directionString = RS_(L"DirectionUp"); + break; + case Direction::Down: + directionString = RS_(L"DirectionDown"); + break; + } + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"MoveFocusWithArgCommandKey")), + directionString) + }; + } + + winrt::hstring AdjustFontSizeArgs::GenerateName() const + { + // If the amount is just 1 (or -1), we'll just return "Increase font + // size" (or "Decrease font size"). If the amount delta has a greater + // absolute value, we'll include it like" + // * Decrease font size, amount: {delta}" + if (_Delta < 0) + { + return _Delta == -1 ? RS_(L"DecreaseFontSizeCommandKey") : + winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"DecreaseFontSizeWithAmountCommandKey")), + -_Delta) + }; + } + else + { + return _Delta == 1 ? RS_(L"IncreaseFontSizeCommandKey") : + winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"IncreaseFontSizeWithAmountCommandKey")), + _Delta) + }; + } + } + + winrt::hstring SplitPaneArgs::GenerateName() const + { + // The string will be similar to the following: + // * "Duplicate pane[, split: ][, new terminal arguments...]" + // * "Split pane[, split: ][, new terminal arguments...]" + // + // Direction will only be added to the string if the split direction is + // not "auto". + // If this is a "duplicate pane" action, then the new terminal arguments + // will be omitted (as they're unused) + + std::wstringstream ss; + if (_SplitMode == SplitType::Duplicate) + { + ss << std::wstring_view(RS_(L"DuplicatePaneCommandKey")); + } + else + { + ss << std::wstring_view(RS_(L"SplitPaneCommandKey")); + } + ss << L", "; + + // This text is intentionally _not_ localized, to attempt to mirror the + // exact syntax that the property would have in JSON. + switch (_SplitStyle) + { + case SplitState::Vertical: + ss << L"split: vertical, "; + break; + case SplitState::Horizontal: + ss << L"split: horizontal, "; + break; + } + + winrt::hstring newTerminalArgsStr; + if (_TerminalArgs) + { + newTerminalArgsStr = _TerminalArgs.GenerateName(); + } + + if (_SplitMode != SplitType::Duplicate && !newTerminalArgsStr.empty()) + { + ss << newTerminalArgsStr.c_str(); + ss << L", "; + } + + // Chop off the last ", " + auto s = ss.str(); + return winrt::hstring{ s.substr(0, s.size() - 2) }; + } + + winrt::hstring OpenSettingsArgs::GenerateName() const + { + switch (_Target) + { + case SettingsTarget::DefaultsFile: + return RS_(L"OpenDefaultSettingsCommandKey"); + case SettingsTarget::AllFiles: + return RS_(L"OpenBothSettingsFilesCommandKey"); + default: + return RS_(L"OpenSettingsCommandKey"); + } + } + + winrt::hstring SetTabColorArgs::GenerateName() const + { + // "Set tab color to #RRGGBB" + // "Reset tab color" + if (_TabColor) + { + til::color c{ _TabColor.Value() }; + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"SetTabColorCommandKey")), + c.ToHexString(true)) + }; + } + + return RS_(L"ResetTabColorCommandKey"); + } + + winrt::hstring RenameTabArgs::GenerateName() const + { + // "Rename tab to \"{_Title}\"" + // "Reset tab title" + if (!_Title.empty()) + { + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"RenameTabCommandKey")), + _Title.c_str()) + }; + } + return RS_(L"ResetTabNameCommandKey"); + } + +} diff --git a/src/cascadia/TerminalApp/ActionArgs.h b/src/cascadia/TerminalApp/ActionArgs.h index 76426637b07..eb233b5967a 100644 --- a/src/cascadia/TerminalApp/ActionArgs.h +++ b/src/cascadia/TerminalApp/ActionArgs.h @@ -59,6 +59,8 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view ProfileKey{ "profile" }; public: + hstring GenerateName() const; + bool Equals(const winrt::TerminalApp::NewTerminalArgs& other) { return other.Commandline() == _Commandline && @@ -103,6 +105,8 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view SingleLineKey{ "singleLine" }; public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); @@ -130,6 +134,8 @@ namespace winrt::TerminalApp::implementation GETSET_PROPERTY(winrt::TerminalApp::NewTerminalArgs, TerminalArgs, nullptr); public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); @@ -156,6 +162,8 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view TabIndexKey{ "index" }; public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); @@ -220,6 +228,8 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view DirectionKey{ "direction" }; public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); @@ -256,6 +266,8 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view DirectionKey{ "direction" }; public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); @@ -292,6 +304,8 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view AdjustFontSizeDelta{ "delta" }; public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); @@ -358,13 +372,16 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view SplitModeKey{ "splitMode" }; public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); if (otherAsUs) { return otherAsUs->_SplitStyle == _SplitStyle && - otherAsUs->_TerminalArgs == _TerminalArgs && + (otherAsUs->_TerminalArgs ? otherAsUs->_TerminalArgs.Equals(_TerminalArgs) : + otherAsUs->_TerminalArgs == _TerminalArgs) && otherAsUs->_SplitMode == _SplitMode; } return false; @@ -424,6 +441,8 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view TargetKey{ "target" }; public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); @@ -453,6 +472,8 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view ColorKey{ "color" }; public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); @@ -488,6 +509,8 @@ namespace winrt::TerminalApp::implementation static constexpr std::string_view TitleKey{ "title" }; public: + hstring GenerateName() const; + bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as(); diff --git a/src/cascadia/TerminalApp/ActionArgs.idl b/src/cascadia/TerminalApp/ActionArgs.idl index 771d9387647..36d5025bb4a 100644 --- a/src/cascadia/TerminalApp/ActionArgs.idl +++ b/src/cascadia/TerminalApp/ActionArgs.idl @@ -6,6 +6,7 @@ namespace TerminalApp interface IActionArgs { Boolean Equals(IActionArgs other); + String GenerateName(); }; interface IActionEventArgs @@ -53,7 +54,9 @@ namespace TerminalApp // ProfileIndex can be null (for "use the default"), so this needs to be // a IReference, so it's nullable Windows.Foundation.IReference ProfileIndex { get; }; + Boolean Equals(NewTerminalArgs other); + String GenerateName(); }; [default_interface] runtimeclass ActionEventArgs : IActionEventArgs diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 1f1ab072416..e972002a8a3 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -238,6 +238,17 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } + void TerminalPage::_HandleToggleCommandPalette(const IInspectable& /*sender*/, + const TerminalApp::ActionEventArgs& args) + { + // TODO GH#6677: When we add support for commandline mode, first set the + // mode that the command palette should be in, before making it visible. + CommandPalette().Visibility(CommandPalette().Visibility() == Visibility::Visible ? + Visibility::Collapsed : + Visibility::Visible); + args.Handled(true); + } + void TerminalPage::_HandleSetTabColor(const IInspectable& /*sender*/, const TerminalApp::ActionEventArgs& args) { diff --git a/src/cascadia/TerminalApp/AppKeyBindings.cpp b/src/cascadia/TerminalApp/AppKeyBindings.cpp index 9fade7d93c3..58821ac5a6a 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindings.cpp @@ -55,8 +55,11 @@ namespace winrt::TerminalApp::implementation { for (auto& kv : _keyShortcuts) { - if (kv.second.Action() == actionAndArgs.Action() && - kv.second.Args().Equals(actionAndArgs.Args())) + const auto action = kv.second.Action(); + const auto args = kv.second.Args(); + const auto actionMatched = action == actionAndArgs.Action(); + const auto argsMatched = args ? args.Equals(actionAndArgs.Args()) : args == actionAndArgs.Args(); + if (actionMatched && argsMatched) { return kv.first; } diff --git a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp b/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp index f4856477e3e..6be18ca683e 100644 --- a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp @@ -64,7 +64,7 @@ Json::Value winrt::TerminalApp::implementation::AppKeyBindings::ToJson() // Iterate over all the possible actions in the names list, and see if // it has a binding. - for (auto& actionName : ActionAndArgs::ActionNamesMap) + for (auto& actionName : ActionAndArgs::ActionKeyNamesMap) { const auto searchedForName = actionName.first; const auto searchedForAction = actionName.second; @@ -85,7 +85,7 @@ Json::Value winrt::TerminalApp::implementation::AppKeyBindings::ToJson() // - Deserialize an AppKeyBindings from the key mappings that are in the array // `json`. The json array should contain an array of objects with both a // `command` string and a `keys` array, where `command` is one of the names -// listed in `ActionAndArgs::ActionNamesMap`, and `keys` is an array of +// listed in `ActionAndArgs::ActionKeyNamesMap`, and `keys` is an array of // keypresses. Currently, the array should contain a single string, which can // be deserialized into a KeyChord. // - Applies the deserialized keybindings to the provided `bindings` object. If diff --git a/src/cascadia/TerminalApp/Command.cpp b/src/cascadia/TerminalApp/Command.cpp new file mode 100644 index 00000000000..a8e72ec6a84 --- /dev/null +++ b/src/cascadia/TerminalApp/Command.cpp @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Command.h" +#include "Command.g.cpp" + +#include "Utils.h" +#include "ActionAndArgs.h" +#include + +using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::TerminalApp; + +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" }; + +namespace winrt::TerminalApp::implementation +{ + // 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. + // * If the "name" property is an object, attempt to lookup the string + // resource specified by the "key" property, to support localizable + // command names. + // Arguments: + // - json: The Json::Value representing the command object we should get the name for. + // Return Value: + // - the empty string if we couldn't find a name, otherwise the command's name. + static winrt::hstring _nameFromJson(const Json::Value& json) + { + if (const auto name{ json[JsonKey(NameKey)] }) + { + if (name.isObject()) + { + try + { + if (const auto keyJson{ name[JsonKey("key")] }) + { + // Make sure the key is present before we try + // loading it. Otherwise we'll crash + const auto resourceKey = GetWstringFromJson(keyJson); + if (HasLibraryResourceWithName(resourceKey)) + { + return GetLibraryResourceString(resourceKey); + } + } + } + CATCH_LOG(); + } + else if (name.isString()) + { + auto nameStr = name.asString(); + return winrt::to_hstring(nameStr); + } + } + + return L""; + } + + // Method Description: + // - Get the name for the command specified in `json`. If there is no "name" + // property in the provided json object, then instead generate a name for + // the provided ActionAndArgs. + // Arguments: + // - json: json for the command to generate a name for. + // - actionAndArgs: An ActionAndArgs object to use to generate a name for, + // if the json object doesn't contain a "name". + // Return Value: + // - The "name" from the json, or the generated name from ActionAndArgs::GenerateName + static winrt::hstring _nameFromJsonOrAction(const Json::Value& json, + winrt::com_ptr actionAndArgs) + { + auto manualName = _nameFromJson(json); + if (!manualName.empty()) + { + return manualName; + } + if (!actionAndArgs) + { + return L""; + } + + return actionAndArgs->GenerateName(); + } + + // Method Description: + // - Deserialize a Command from the `json` object. The json object should + // contain a "name" and "action", and optionally an "icon". + // * "name": string|object - the name of the command to display in the + // command palette. If this is an object, look for the "key" property, + // and try to load the string from our resources instead. + // * "action": string|object - A ShortcutAction, either as a name or as an + // ActionAndArgs serialization. See ActionAndArgs::FromJson for details. + // If this is null, we'll remove this command from the list of commands. + // Arguments: + // - json: the Json::Value to deserialize into a Command + // - warnings: If there were any warnings during parsing, they'll be + // appended to this vector. + // Return Value: + // - the newly constructed Command object. + winrt::com_ptr Command::FromJson(const Json::Value& json, + std::vector<::TerminalApp::SettingsLoadWarnings>& warnings) + { + auto result = winrt::make_self(); + + // 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)] }) + { + 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; + } + + result->_setName(_nameFromJsonOrAction(json, actionAndArgs)); + } + else + { + // { name: "foo", action: null } will land in this case, which + // should also be used for unbinding. + return nullptr; + } + + if (result->_Name.empty()) + { + return nullptr; + } + + return result; + } + + // Function Description: + // - Attempt to parse all the json objects in `json` into new Command + // objects, and add them to the map of commands. + // - If any parsed command has + // the same Name as an existing command in commands, the new one will + // layer on top of the existing one. + // Arguments: + // - commands: a map of Name->Command which new commands should be layered upon. + // - json: A Json::Value containing an array of serialized commands + // Return Value: + // - A vector containing any warnings detected while parsing + std::vector<::TerminalApp::SettingsLoadWarnings> Command::LayerJson(std::unordered_map& commands, + const Json::Value& json) + { + std::vector<::TerminalApp::SettingsLoadWarnings> warnings; + + for (const auto& value : json) + { + if (value.isObject()) + { + try + { + auto result = Command::FromJson(value, warnings); + if (result) + { + // Override commands with the same name + commands.insert_or_assign(result->Name(), *result); + } + else + { + // If there wasn't a parsed command, then try to get the + // name from the json blob. If that name currently + // exists in our list of commands, we should remove it. + const auto name = _nameFromJson(value); + if (!name.empty()) + { + commands.erase(name); + } + } + } + CATCH_LOG(); + } + } + return warnings; + } +} diff --git a/src/cascadia/TerminalApp/Command.h b/src/cascadia/TerminalApp/Command.h new file mode 100644 index 00000000000..2882fc8244a --- /dev/null +++ b/src/cascadia/TerminalApp/Command.h @@ -0,0 +1,45 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Command.h + +Abstract: +- A command represents a single entry in the Command Palette. This is an object + that has a user facing "name" to display to the user, and an associated action + which can be dispatched. + +- For more information, see GH#2046, #5400, #5674, and #6635 + +Author(s): +- Mike Griese - June 2020 + +--*/ +#pragma once + +#include "Command.g.h" +#include "TerminalWarnings.h" +#include "..\inc\cppwinrt_utils.h" + +namespace winrt::TerminalApp::implementation +{ + struct Command : CommandT + { + Command() = default; + + static winrt::com_ptr FromJson(const Json::Value& json, std::vector<::TerminalApp::SettingsLoadWarnings>& warnings); + static std::vector<::TerminalApp::SettingsLoadWarnings> LayerJson(std::unordered_map& commands, + const Json::Value& json); + + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Name, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(winrt::TerminalApp::ActionAndArgs, Action, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, KeyChordText, _PropertyChangedHandlers); + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(Command); +} diff --git a/src/cascadia/TerminalApp/Command.idl b/src/cascadia/TerminalApp/Command.idl new file mode 100644 index 00000000000..80e1a2b476b --- /dev/null +++ b/src/cascadia/TerminalApp/Command.idl @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "../ShortcutActionDispatch.idl"; + +namespace TerminalApp +{ + [default_interface] runtimeclass Command : Windows.UI.Xaml.Data.INotifyPropertyChanged + { + Command(); + + String Name; + ActionAndArgs Action; + String KeyChordText; + } +} diff --git a/src/cascadia/TerminalApp/CommandPalette.cpp b/src/cascadia/TerminalApp/CommandPalette.cpp new file mode 100644 index 00000000000..7a1470ff75e --- /dev/null +++ b/src/cascadia/TerminalApp/CommandPalette.cpp @@ -0,0 +1,414 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "CommandPalette.h" + +#include "CommandPalette.g.cpp" +#include + +using namespace winrt; +using namespace winrt::TerminalApp; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::System; +using namespace winrt::Windows::Foundation; + +namespace winrt::TerminalApp::implementation +{ + CommandPalette::CommandPalette() + { + InitializeComponent(); + + _filteredActions = winrt::single_threaded_observable_vector(); + _allActions = winrt::single_threaded_vector(); + + if (CommandPaletteShadow()) + { + // Hook up the shadow on the command palette to the backdrop that + // will actually show it. This needs to be done at runtime, and only + // if the shadow actually exists. ThemeShadow isn't supported below + // version 18362. + CommandPaletteShadow().Receivers().Append(_shadowBackdrop()); + // "raise" the command palette up by 16 units, so it will cast a shadow. + _backdrop().Translation({ 0, 0, 16 }); + } + + // Whatever is hosting us will enable us by setting our visibility to + // "Visible". When that happens, set focus to our search box. + RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (Visibility() == Visibility::Visible) + { + _searchBox().Focus(FocusState::Programmatic); + _filteredActionsView().SelectedIndex(0); + } + else + { + // Raise an event to return control to the Terminal. + _close(); + } + }); + } + + // Method Description: + // - Moves the focus up or down the list of commands. If we're at the top, + // we'll loop around to the bottom, and vice-versa. + // Arguments: + // - moveDown: if true, we're attempting to move to the next item in the + // list. Otherwise, we're attempting to move to the previous. + // Return Value: + // - + void CommandPalette::_selectNextItem(const bool moveDown) + { + const auto selected = _filteredActionsView().SelectedIndex(); + const int numItems = ::base::saturated_cast(_filteredActionsView().Items().Size()); + // Wraparound math. By adding numItems and then calculating modulo numItems, + // we clamp the values to the range [0, numItems) while still supporting moving + // upward from 0 to numItems - 1. + const auto newIndex = ((numItems + selected + (moveDown ? 1 : -1)) % numItems); + _filteredActionsView().SelectedIndex(newIndex); + _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); + } + + // Method Description: + // - Process keystrokes in the input box. This is used for moving focus up + // and down the list of commands in Action mode, and for executing + // commands in both Action mode and Commandline mode. + // Arguments: + // - e: the KeyRoutedEventArgs containing info about the keystroke. + // Return Value: + // - + void CommandPalette::_keyDownHandler(IInspectable const& /*sender*/, + Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) + { + auto key = e.OriginalKey(); + + if (key == VirtualKey::Up) + { + // Action Mode: Move focus to the next item in the list. + _selectNextItem(false); + e.Handled(true); + } + else if (key == VirtualKey::Down) + { + // Action Mode: Move focus to the previous item in the list. + _selectNextItem(true); + e.Handled(true); + } + else if (key == VirtualKey::Enter) + { + // Action Mode: Dispatch the action of the selected command. + + if (const auto selectedItem = _filteredActionsView().SelectedItem()) + { + if (const auto data = selectedItem.try_as()) + { + const auto actionAndArgs = data.Action(); + _dispatch.DoAction(actionAndArgs); + _close(); + } + } + + e.Handled(true); + } + else if (key == VirtualKey::Escape) + { + // Action Mode: Dismiss the palette if the text is empty, otherwise clear the search string. + if (_searchBox().Text().empty()) + { + _close(); + } + else + { + _searchBox().Text(L""); + } + + e.Handled(true); + } + } + + // Method Description: + // - This event is triggered when someone clicks anywhere in the bounds of + // the window that's _not_ the command palette UI. When that happens, + // we'll want to dismiss the palette. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::_rootPointerPressed(Windows::Foundation::IInspectable const& /*sender*/, + Windows::UI::Xaml::Input::PointerRoutedEventArgs const& /*e*/) + { + _close(); + } + + // Method Description: + // - This event is only triggered when someone clicks in the space right + // next to the text box in the command palette. We _don't_ want that click + // to light dismiss the palette, so we'll mark it handled here. + // Arguments: + // - e: the PointerRoutedEventArgs that we want to mark as handled + // Return Value: + // - + void CommandPalette::_backdropPointerPressed(Windows::Foundation::IInspectable const& /*sender*/, + Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e) + { + e.Handled(true); + } + + // Method Description: + // - This event is called when the user clicks on an individual item from + // the list. We'll get the item that was clicked and dispatch the command + // that the user clicked on. + // Arguments: + // - e: an ItemClickEventArgs who's ClickedItem() will be the command that was clicked on. + // Return Value: + // - + void CommandPalette::_listItemClicked(Windows::Foundation::IInspectable const& /*sender*/, + Windows::UI::Xaml::Controls::ItemClickEventArgs const& e) + { + if (auto command{ e.ClickedItem().try_as() }) + { + const auto actionAndArgs = command.Action(); + _dispatch.DoAction(actionAndArgs); + _close(); + } + } + + // Method Description: + // - Event handler for when the text in the input box changes. In Action + // Mode, we'll update the list of displayed commands, and select the first one. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::_filterTextChanged(IInspectable const& /*sender*/, + Windows::UI::Xaml::RoutedEventArgs const& /*args*/) + { + _updateFilteredActions(); + _filteredActionsView().SelectedIndex(0); + + _noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible); + } + + Collections::IObservableVector CommandPalette::FilteredActions() + { + return _filteredActions; + } + + void CommandPalette::SetActions(Collections::IVector const& actions) + { + _allActions = actions; + _updateFilteredActions(); + } + + // This is a helper to aid in sorting commands by their `Name`s, alphabetically. + static bool _compareCommandNames(const TerminalApp::Command& lhs, const TerminalApp::Command& rhs) + { + std::wstring_view leftName{ lhs.Name() }; + std::wstring_view rightName{ rhs.Name() }; + return leftName.compare(rightName) < 0; + } + + // This is a helper struct to aid in sorting Commands by a given weighting. + struct WeightedCommand + { + TerminalApp::Command command; + int weight; + + bool operator<(const WeightedCommand& other) const + { + // If two commands have the same weight, then we'll sort them alphabetically. + if (weight == other.weight) + { + return !_compareCommandNames(command, other.command); + } + return weight < other.weight; + } + }; + + // Method Description: + // - Update our list of filtered actions to reflect the current contents of + // the input box. For more details on which commands will be displayed, + // see `_getWeight`. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::_updateFilteredActions() + { + _filteredActions.Clear(); + auto searchText = _searchBox().Text(); + const bool addAll = searchText.empty(); + + // If there's no filter text, then just add all the commands in order to the list. + // - TODO GH#6647:Possibly add the MRU commands first in order, followed + // by the rest of the commands. + if (addAll) + { + // Add all the commands, but make sure they're sorted alphabetically. + std::vector sortedCommands; + sortedCommands.reserve(_allActions.Size()); + + for (auto action : _allActions) + { + sortedCommands.push_back(action); + } + std::sort(sortedCommands.begin(), + sortedCommands.end(), + _compareCommandNames); + + for (auto action : sortedCommands) + { + _filteredActions.Append(action); + } + + return; + } + + // Here, there was some filter text. + // Show these actions in a weighted order. + // - Matching the first character of a word, then the first char of a + // subsequent word seems better than just "the order they appear in + // the list". + // - TODO GH#6647:"Recently used commands" ordering also seems valuable. + // * This could be done by weighting the recently used commands + // higher the more recently they were used, then weighting all + // the unused commands as 1 + + // Use a priority queue to order commands so that "better" matches + // appear first in the list. The ordering will be determined by the + // match weight produced by _getWeight. + std::priority_queue heap; + for (auto action : _allActions) + { + const auto weight = CommandPalette::_getWeight(searchText, action.Name()); + if (weight > 0) + { + WeightedCommand wc; + wc.command = action; + wc.weight = weight; + heap.push(wc); + } + } + + // At this point, all the commands in heap are matches. We've also + // sorted commands with the same weight alphabetically. + // Remove everything in-order from the queue, and add to the list of + // filtered actions. + while (!heap.empty()) + { + auto top = heap.top(); + heap.pop(); + _filteredActions.Append(top.command); + } + } + + // Function Description: + // - Calculates a "weighting" by which should be used to order a command + // name relative to other names, given a specific search string. + // Currently, this is based off of two factors: + // * The weight is incremented once for each matched character of the + // search text. + // * If a matching character from the search text was found at the start + // of a word in the name, then we increment the weight again. + // * For example, for a search string "sp", we want "Split Pane" to + // appear in the list before "Close Pane" + // * Consecutive matches will be weighted higher than matches with + // characters in between the search characters. + // - This will return 0 if the command should not be shown. If all the + // characters of search text appear in order in `name`, then this function + // will return a positive number. There can be any number of characters + // separating consecutive characters in searchText. + // * For example: + // "name": "New Tab" + // "name": "Close Tab" + // "name": "Close Pane" + // "name": "[-] Split Horizontal" + // "name": "[ | ] Split Vertical" + // "name": "Next Tab" + // "name": "Prev Tab" + // "name": "Open Settings" + // "name": "Open Media Controls" + // * "open" should return both "**Open** Settings" and "**Open** Media Controls". + // * "Tab" would return "New **Tab**", "Close **Tab**", "Next **Tab**" and "Prev + // **Tab**". + // * "P" would return "Close **P**ane", "[-] S**p**lit Horizontal", "[ | ] + // S**p**lit Vertical", "**P**rev Tab", "O**p**en Settings" and "O**p**en Media + // Controls". + // * "sv" would return "[ | ] Split Vertical" (by matching the **S** in + // "Split", then the **V** in "Vertical"). + // Arguments: + // - searchText: the string of text to search for in `name` + // - name: the name to check + // Return Value: + // - the relative weight of this match + int CommandPalette::_getWeight(const winrt::hstring& searchText, + const winrt::hstring& name) + { + int totalWeight = 0; + bool lastWasSpace = true; + + auto it = name.cbegin(); + + for (auto searchChar : searchText) + { + searchChar = std::towlower(searchChar); + // Advance the iterator to the next character that we're looking + // for. + + bool lastWasMatch = true; + while (true) + { + // If we are at the end of the name string, we haven't found + // it. + if (it == name.cend()) + { + return false; + } + + // found it + if (std::towlower(*it) == searchChar) + { + break; + } + + lastWasSpace = *it == L' '; + ++it; + lastWasMatch = false; + } + + // Advance the iterator by one character so that we don't + // end up on the same character in the next iteration. + ++it; + + totalWeight += 1; + totalWeight += lastWasSpace ? 1 : 0; + totalWeight += (lastWasMatch) ? 1 : 0; + } + + return totalWeight; + } + + void CommandPalette::SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch) + { + _dispatch = dispatch; + } + + // Method Description: + // - Dismiss the command palette. This will: + // * select all the current text in the input box + // * set our visibility to Hidden + // * raise our Closed event, so the page can return focus to the active Terminal + // Arguments: + // - + // Return Value: + // - + void CommandPalette::_close() + { + Visibility(Visibility::Collapsed); + + // Clear the text box each time we close the dialog. This is consistent with VsCode. + _searchBox().Text(L""); + } + +} diff --git a/src/cascadia/TerminalApp/CommandPalette.h b/src/cascadia/TerminalApp/CommandPalette.h new file mode 100644 index 00000000000..94e3ac495dd --- /dev/null +++ b/src/cascadia/TerminalApp/CommandPalette.h @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "CommandPalette.g.h" +#include "../../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::TerminalApp::implementation +{ + struct CommandPalette : CommandPaletteT + { + CommandPalette(); + + Windows::Foundation::Collections::IObservableVector FilteredActions(); + void SetActions(Windows::Foundation::Collections::IVector const& actions); + + void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch); + + private: + friend struct CommandPaletteT; // for Xaml to bind events + + Windows::Foundation::Collections::IObservableVector _filteredActions{ nullptr }; + Windows::Foundation::Collections::IVector _allActions{ nullptr }; + winrt::TerminalApp::ShortcutActionDispatch _dispatch; + + void _filterTextChanged(Windows::Foundation::IInspectable const& sender, + Windows::UI::Xaml::RoutedEventArgs const& args); + void _keyDownHandler(Windows::Foundation::IInspectable const& sender, + Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); + + void _rootPointerPressed(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + void _backdropPointerPressed(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + + void _listItemClicked(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Controls::ItemClickEventArgs const& e); + + void _selectNextItem(const bool moveDown); + + void _updateFilteredActions(); + static int _getWeight(const winrt::hstring& searchText, const winrt::hstring& name); + void _close(); + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(CommandPalette); +} diff --git a/src/cascadia/TerminalApp/CommandPalette.idl b/src/cascadia/TerminalApp/CommandPalette.idl new file mode 100644 index 00000000000..f0e7c9bb539 --- /dev/null +++ b/src/cascadia/TerminalApp/CommandPalette.idl @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "../Command.idl"; + +namespace TerminalApp +{ + [default_interface] runtimeclass CommandPalette : Windows.UI.Xaml.Controls.Grid + { + CommandPalette(); + + Windows.Foundation.Collections.IObservableVector FilteredActions { get; }; + + void SetActions(Windows.Foundation.Collections.IVector actions); + + void SetDispatch(ShortcutActionDispatch dispatch); + } +} diff --git a/src/cascadia/TerminalApp/CommandPalette.xaml b/src/cascadia/TerminalApp/CommandPalette.xaml new file mode 100644 index 00000000000..86cd6e2edce --- /dev/null +++ b/src/cascadia/TerminalApp/CommandPalette.xaml @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.cpp b/src/cascadia/TerminalApp/GlobalAppSettings.cpp index d16ba285024..0a8d23976b3 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalApp/GlobalAppSettings.cpp @@ -7,7 +7,6 @@ #include "../../inc/DefaultSettings.h" #include "Utils.h" #include "JsonUtils.h" -#include using namespace TerminalApp; using namespace winrt::Microsoft::Terminal::Settings; @@ -17,7 +16,8 @@ using namespace winrt::Windows::UI::Xaml; using namespace ::Microsoft::Console; using namespace winrt::Microsoft::UI::Xaml::Controls; -static constexpr std::string_view KeybindingsKey{ "keybindings" }; +static constexpr std::string_view LegacyKeybindingsKey{ "keybindings" }; +static constexpr std::string_view BindingsKey{ "bindings" }; static constexpr std::string_view DefaultProfileKey{ "defaultProfile" }; static constexpr std::string_view AlwaysShowTabsKey{ "alwaysShowTabs" }; static constexpr std::string_view InitialRowsKey{ "initialRows" }; @@ -205,18 +205,6 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) _TabWidthMode = _ParseTabWidthMode(GetWstringFromJson(tabWidthMode)); } - if (auto keybindings{ json[JsonKey(KeybindingsKey)] }) - { - auto warnings = _keybindings->LayerJson(keybindings); - // It's possible that the user provided keybindings have some warnings - // in them - problems that we should alert the user to, but we can - // recover from. Most of these warnings cannot be detected later in the - // Validate settings phase, so we'll collect them now. If there were any - // warnings generated from parsing these keybindings, add them to our - // list of warnings. - _keybindingsWarnings.insert(_keybindingsWarnings.end(), warnings.begin(), warnings.end()); - } - JsonUtils::GetBool(json, SnapToGridOnResizeKey, _SnapToGridOnResize); JsonUtils::GetBool(json, ForceFullRepaintRenderingKey, _ForceFullRepaintRendering); @@ -228,6 +216,31 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetBool(json, DebugFeaturesKey, _DebugFeaturesEnabled); JsonUtils::GetBool(json, EnableStartupTaskKey, _StartOnUserLogin); + + // This is a helper lambda to get the keybindings and commands out of both + // and array of objects. We'll use this twice, once on the legacy + // `keybindings` key, and again on the newer `bindings` key. + auto parseBindings = [this, &json](auto jsonKey) { + if (auto bindings{ json[JsonKey(jsonKey)] }) + { + auto warnings = _keybindings->LayerJson(bindings); + // It's possible that the user provided keybindings have some warnings + // in them - problems that we should alert the user to, but we can + // recover from. Most of these warnings cannot be detected later in the + // Validate settings phase, so we'll collect them now. If there were any + // warnings generated from parsing these keybindings, add them to our + // list of warnings. + _keybindingsWarnings.insert(_keybindingsWarnings.end(), warnings.begin(), warnings.end()); + + // Now parse the array again, but this time as a list of commands. + warnings = winrt::TerminalApp::implementation::Command::LayerJson(_commands, bindings); + // It's possible that the user provided commands have some warnings + // in them, similar to the keybindings. + _keybindingsWarnings.insert(_keybindingsWarnings.end(), warnings.begin(), warnings.end()); + } + }; + parseBindings(LegacyKeybindingsKey); + parseBindings(BindingsKey); } // Method Description: @@ -365,3 +378,8 @@ std::vector GlobalAppSettings::GetKeybindings { return _keybindingsWarnings; } + +const std::unordered_map& GlobalAppSettings::GetCommands() const noexcept +{ + return _commands; +} diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.h b/src/cascadia/TerminalApp/GlobalAppSettings.h index bec6376d4de..677e7f446c9 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.h +++ b/src/cascadia/TerminalApp/GlobalAppSettings.h @@ -16,6 +16,7 @@ Author(s): #pragma once #include "AppKeyBindings.h" #include "ColorScheme.h" +#include "Command.h" // fwdecl unittest classes namespace TerminalAppLocalTests @@ -54,6 +55,8 @@ class TerminalApp::GlobalAppSettings final std::vector GetKeybindingsWarnings() const; + const std::unordered_map& GetCommands() const noexcept; + // These are implemented manually to handle the string/GUID exchange // by higher layers in the app. void DefaultProfile(const GUID defaultProfile) noexcept; @@ -79,7 +82,6 @@ class TerminalApp::GlobalAppSettings final GETSET_PROPERTY(bool, SoftwareRendering, false); GETSET_PROPERTY(bool, ForceVTInput, false); GETSET_PROPERTY(bool, DebugFeaturesEnabled); // default value set in constructor - GETSET_PROPERTY(bool, StartOnUserLogin, false); private: @@ -90,6 +92,7 @@ class TerminalApp::GlobalAppSettings final std::vector<::TerminalApp::SettingsLoadWarnings> _keybindingsWarnings; std::unordered_map _colorSchemes; + std::unordered_map _commands; static winrt::Windows::UI::Xaml::ElementTheme _ParseTheme(const std::wstring& themeString) noexcept; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index b6fee2b14b1..92266027677 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -347,4 +347,154 @@ Command Prompt This is the name of "Command Prompt", as localized in Windows. The localization here should match the one in the Windows product for "Command Prompt" + + Type a command name... + + + No matching commands + + + Close window + + + Toggle fullscreen + + + Open new tab dropdown + + + Open settings file + + + Open default settings file + + + Open both settings and default settings files + + + Find + + + Resize pane + + + Move focus + + + Move focus {0} + {0} will be replaced with one of the four directions "DirectionLeft", "DirectionRight", "DirectionUp", or "DirectionDown" + + + Resize pane {0} + {0} will be replaced with one of the four directions "DirectionLeft", "DirectionRight", "DirectionUp", or "DirectionDown" + + + left + + + right + + + up + + + down + + + Switch to tab + + + New tab + + + Split pane + + + New window + + + Duplicate tab + + + Duplicate pane + + + Next tab + + + Previous tab + + + Close pane + + + Close tab + + + Split pane horizontally + + + Split pane vertically + + + Copy text + + + Copy text as a single line + + + Paste + + + Scroll down one line + + + Scroll down one page + + + Scroll up one line + + + Scroll up one page + + + Adjust font size + + + Increase font size + + + Decrease font size + + + Increase font size, amount: {0} + {0} will be replaced with a positive number + + + Decrease font size, amount: {0} + {0} will be replaced with a positive number + + + Reset font size + + + Toggle command palette + + + Set tab color to {0} + {0} will be replaced with a color, displayed in hexadecimal (#RRGGBB) notation. + + + Reset tab color + + + Set the tab color... + + + Rename tab to "{0}" + {0} will be replaced with user-defined string + + + Reset tab title + diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp index 682b2a4594a..f36654dd0ce 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp @@ -159,6 +159,11 @@ namespace winrt::TerminalApp::implementation _ToggleFullscreenHandlers(*this, *eventArgs); break; } + case ShortcutAction::ToggleCommandPalette: + { + _ToggleCommandPaletteHandlers(*this, *eventArgs); + break; + } case ShortcutAction::SetTabColor: { _SetTabColorHandlers(*this, *eventArgs); diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.h b/src/cascadia/TerminalApp/ShortcutActionDispatch.h index 422858a0e17..5a70f2abd10 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.h +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.h @@ -23,33 +23,34 @@ namespace winrt::TerminalApp::implementation bool DoAction(const ActionAndArgs& actionAndArgs); // clang-format off - TYPED_EVENT(CopyText, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(PasteText, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(OpenNewTabDropdown,TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(DuplicateTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(NewTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(NewWindow, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(CloseWindow, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(CloseTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(ClosePane, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(SwitchToTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(NextTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(PrevTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(SplitPane, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(AdjustFontSize, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(ResetFontSize, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(ScrollUp, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(ScrollDown, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(ScrollUpPage, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(ScrollDownPage, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(OpenSettings, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(ResizePane, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(Find, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(MoveFocus, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(ToggleFullscreen, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(SetTabColor, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(OpenTabColorPicker,TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); - TYPED_EVENT(RenameTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(CopyText, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(PasteText, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(OpenNewTabDropdown, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(DuplicateTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(NewTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(NewWindow, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(CloseWindow, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(CloseTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ClosePane, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(SwitchToTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(NextTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(PrevTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(SplitPane, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(AdjustFontSize, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ResetFontSize, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ScrollUp, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ScrollDown, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ScrollUpPage, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ScrollDownPage, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(OpenSettings, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ResizePane, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(Find, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(MoveFocus, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ToggleFullscreen, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ToggleCommandPalette, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(SetTabColor, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(OpenTabColorPicker, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(RenameTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); // clang-format on private: diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl index a09390ebaee..89d4aabbfe9 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl @@ -35,7 +35,8 @@ namespace TerminalApp SetTabColor, OpenTabColorPicker, OpenSettings, - RenameTab + RenameTab, + ToggleCommandPalette }; [default_interface] runtimeclass ActionAndArgs { @@ -73,6 +74,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler Find; event Windows.Foundation.TypedEventHandler MoveFocus; event Windows.Foundation.TypedEventHandler ToggleFullscreen; + event Windows.Foundation.TypedEventHandler ToggleCommandPalette; event Windows.Foundation.TypedEventHandler SetTabColor; event Windows.Foundation.TypedEventHandler OpenTabColorPicker; event Windows.Foundation.TypedEventHandler RenameTab; diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index ccb7cbcd124..47bc5100e6b 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -14,6 +14,7 @@ #include #include +#include "KeyChordSerialization.h" #include "AzureCloudShellGenerator.h" // For AzureConnectionType #include "TelnetGenerator.h" // For TelnetConnectionType #include "TabRowControl.h" @@ -49,13 +50,39 @@ namespace winrt::TerminalApp::implementation InitializeComponent(); } - void TerminalPage::SetSettings(std::shared_ptr<::TerminalApp::CascadiaSettings> settings, bool needRefreshUI) + winrt::fire_and_forget TerminalPage::SetSettings(std::shared_ptr<::TerminalApp::CascadiaSettings> settings, + bool needRefreshUI) { _settings = settings; if (needRefreshUI) { _RefreshUIForSettingsReload(); } + + auto weakThis{ get_weak() }; + co_await winrt::resume_foreground(Dispatcher()); + if (auto page{ weakThis.get() }) + { + // Update the command palette when settings reload + auto commandsCollection = winrt::single_threaded_vector(); + for (auto& nameAndCommand : _settings->GlobalSettings().GetCommands()) + { + auto command = nameAndCommand.second; + + // If there's a keybinding that's bound to exactly this command, + // then get the string for that keychord and display it as a + // part of the command in the UI. Each Command's KeyChordText is + // 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()) }; + if (keyChord) + { + command.KeyChordText(KeyChordSerialization::ToString(keyChord)); + } + commandsCollection.Append(command); + } + CommandPalette().SetActions(commandsCollection); + } } void TerminalPage::Create() @@ -151,6 +178,17 @@ namespace winrt::TerminalApp::implementation _tabContent.SizeChanged({ this, &TerminalPage::_OnContentSizeChanged }); + CommandPalette().SetDispatch(*_actionDispatch); + // When the visibility of the command palette changes to "collapsed", + // the palette has been closed. Toss focus back to the currently active + // control. + CommandPalette().RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (CommandPalette().Visibility() == Visibility::Collapsed) + { + _CommandPaletteClosed(nullptr, nullptr); + } + }); + // Once the page is actually laid out on the screen, trigger all our // startup actions. Things like Panes need to know at least how big the // window will be, so they can subdivide that space. @@ -765,6 +803,7 @@ namespace winrt::TerminalApp::implementation _actionDispatch->Find({ this, &TerminalPage::_HandleFind }); _actionDispatch->ResetFontSize({ this, &TerminalPage::_HandleResetFontSize }); _actionDispatch->ToggleFullscreen({ this, &TerminalPage::_HandleToggleFullscreen }); + _actionDispatch->ToggleCommandPalette({ this, &TerminalPage::_HandleToggleCommandPalette }); _actionDispatch->SetTabColor({ this, &TerminalPage::_HandleSetTabColor }); _actionDispatch->OpenTabColorPicker({ this, &TerminalPage::_HandleOpenTabColorPicker }); _actionDispatch->RenameTab({ this, &TerminalPage::_HandleRenameTab }); @@ -2041,6 +2080,16 @@ namespace winrt::TerminalApp::implementation // TODO GH#3327: Look at what to do with the NC area when we have XAML theming } + void TerminalPage::_CommandPaletteClosed(const IInspectable& /*sender*/, + const RoutedEventArgs& /*eventArgs*/) + { + // Return focus to the active control + if (auto index{ _GetFocusedTabIndex() }) + { + _GetStrongTabImpl(index.value())->SetFocused(true); + } + } + // -------------------------------- WinRT Events --------------------------------- // Winrt events need a method for adding a callback to the event and removing the callback. // These macros will define them both for you. diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index e6eb55d63d3..ec692558dd1 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -32,7 +32,7 @@ namespace winrt::TerminalApp::implementation public: TerminalPage(); - void SetSettings(std::shared_ptr<::TerminalApp::CascadiaSettings> settings, bool needRefreshUI); + winrt::fire_and_forget SetSettings(std::shared_ptr<::TerminalApp::CascadiaSettings> settings, bool needRefreshUI); void Create(); @@ -173,6 +173,8 @@ namespace winrt::TerminalApp::implementation void _CompleteInitialization(); + void _CommandPaletteClosed(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + #pragma region ActionHandlers // These are all defined in AppActionHandlers.cpp void _HandleOpenNewTabDropdown(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); @@ -201,6 +203,8 @@ namespace winrt::TerminalApp::implementation void _HandleSetTabColor(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); void _HandleOpenTabColorPicker(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); void _HandleRenameTab(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); + void _HandleToggleCommandPalette(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); + // Make sure to hook new actions up in _RegisterActionCallbacks! #pragma endregion friend class TerminalAppLocalTests::TabTests; diff --git a/src/cascadia/TerminalApp/TerminalPage.xaml b/src/cascadia/TerminalApp/TerminalPage.xaml index 7e813884368..c91b7ea33eb 100644 --- a/src/cascadia/TerminalApp/TerminalPage.xaml +++ b/src/cascadia/TerminalApp/TerminalPage.xaml @@ -53,5 +53,11 @@ the MIT License. See LICENSE in the project root for license information. --> DefaultButton="Primary" PrimaryButtonClick="_CloseWarningPrimaryButtonOnClick"> + + diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj index c3469c108e7..4e2db8c0bf3 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj @@ -63,6 +63,9 @@ Designer + + Designer + @@ -85,6 +88,12 @@ ../ColorPickupFlyout.xaml + + ../CommandPalette.xaml + + + ../Command.idl + ../Tab.idl @@ -103,7 +112,7 @@ - + ../ShortcutActionDispatch.idl @@ -146,6 +155,12 @@ ../ColorPickupFlyout.xaml + + ../CommandPalette.xaml + + + ../Command.idl + ../Tab.idl @@ -229,7 +244,12 @@ ../ColorPickupFlyout.xaml Code - + + ../CommandPalette.xaml + Code + + + diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters index e697e7c4c96..5f856a6d4a8 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters @@ -4,7 +4,7 @@ - + @@ -59,7 +59,13 @@ pane - + + + + + + settings + @@ -107,8 +113,16 @@ tab - - + + + + + + profileGeneration + + + settings + @@ -126,6 +140,10 @@ tab + + + commandPalette + @@ -146,6 +164,9 @@ controls + + commandPalette + @@ -169,6 +190,9 @@ {6d40e12f-b83f-462e-8f93-fa421f87b27e} + + {2ad498e1-d8ea-4381-9464-a74c141bd7dd} + diff --git a/src/cascadia/TerminalApp/lib/pch.h b/src/cascadia/TerminalApp/lib/pch.h index 291021bd4bc..52a51a3bd53 100644 --- a/src/cascadia/TerminalApp/lib/pch.h +++ b/src/cascadia/TerminalApp/lib/pch.h @@ -28,6 +28,7 @@ #include #include +#include #include #include #include diff --git a/src/cascadia/WinRTUtils/LibraryResources.cpp b/src/cascadia/WinRTUtils/LibraryResources.cpp index e562fafa670..82c4acbc9f5 100644 --- a/src/cascadia/WinRTUtils/LibraryResources.cpp +++ b/src/cascadia/WinRTUtils/LibraryResources.cpp @@ -94,3 +94,11 @@ try return loader.GetLocalizedString(key); } CATCH_FAIL_FAST() + +bool HasLibraryResourceWithName(const std::wstring_view key) +try +{ + static auto loader{ GetLibraryResourceLoader() }; + return loader.HasResourceWithName(key); +} +CATCH_FAIL_FAST() diff --git a/src/cascadia/WinRTUtils/inc/LibraryResources.h b/src/cascadia/WinRTUtils/inc/LibraryResources.h index 02040079f89..c593cd15644 100644 --- a/src/cascadia/WinRTUtils/inc/LibraryResources.h +++ b/src/cascadia/WinRTUtils/inc/LibraryResources.h @@ -68,3 +68,4 @@ namespace Microsoft::Console::Utils __declspec(selectany) extern const wchar_t* g_WinRTUtilsLibraryResourceScope{ (x) }; winrt::hstring GetLibraryResourceString(const std::wstring_view key); +bool HasLibraryResourceWithName(const std::wstring_view key); diff --git a/src/cascadia/inc/cppwinrt_utils.h b/src/cascadia/inc/cppwinrt_utils.h index 81f5ae51133..3d9599bbeef 100644 --- a/src/cascadia/inc/cppwinrt_utils.h +++ b/src/cascadia/inc/cppwinrt_utils.h @@ -107,15 +107,16 @@ public: \ private: \ type _##name{ __VA_ARGS__ }; -// Use this macro to quickly implement both the getter and setter for an observable property. -// This is similar to the GETSET_PROPERTY macro above, except this will also raise a -// PropertyChanged event with the name of the property that has changed inside of the setter. +// Use this macro to quickly implement both the getter and setter for an +// observable property. This is similar to the GETSET_PROPERTY macro above, +// except this will also raise a PropertyChanged event with the name of the +// property that has changed inside of the setter. This also implements a +// private _setName() method, that the class can internally use to change the +// value when it _knows_ it doesn't need to raise the PropertyChanged event +// (like when the class is being initialized). #define OBSERVABLE_GETSET_PROPERTY(type, name, event) \ public: \ type name() { return _##name; }; \ - \ -private: \ - const type _##name; \ void name(const type& value) \ { \ if (_##name != value) \ @@ -123,6 +124,13 @@ private: const_cast(_##name) = value; \ event(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L#name }); \ } \ + }; \ + \ +private: \ + const type _##name; \ + void _set##name(const type& value) \ + { \ + const_cast(_##name) = value; \ }; // Use this macro for quickly defining the factory_implementation part of a diff --git a/src/inc/til/color.h b/src/inc/til/color.h index 31ba570309d..122aaafb6ba 100644 --- a/src/inc/til/color.h +++ b/src/inc/til/color.h @@ -148,10 +148,19 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" std::wstring to_string() const { std::wstringstream wss; - wss << L"Color #" << std::uppercase << std::setfill(L'0') << std::hex; + wss << L"Color " << ToHexString(false); + return wss.str(); + } + std::wstring ToHexString(const bool omitAlpha = false) const + { + std::wstringstream wss; + wss << L"#" << std::uppercase << std::setfill(L'0') << std::hex; // Force the compiler to promote from byte to int. Without it, the // stringstream will try to write the components as chars - wss << std::setw(2) << static_cast(a); + if (!omitAlpha) + { + wss << std::setw(2) << static_cast(a); + } wss << std::setw(2) << static_cast(r); wss << std::setw(2) << static_cast(g); wss << std::setw(2) << static_cast(b);