diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 38bdd5352ab..a2fd54ec1ab 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -67,6 +67,7 @@ "wt", "closeOtherTabs", "closeTabsAfter", + "tabSwitcher", "unbound" ], "type": "string" @@ -88,6 +89,14 @@ ], "type": "string" }, + "AnchorKey": { + "enum": [ + "ctrl", + "alt", + "shift" + ], + "type": "string" + }, "NewTerminalArgs": { "properties": { "commandline": { @@ -352,6 +361,22 @@ ], "required": [ "index" ] }, + "TabSwitcherAction": { + "description": "Arguments corresponding to a Tab Switcher Action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "tabSwitcher" }, + "anchorKey": { + "$ref": "#/definitions/AnchorKey", + "default": null, + "description": "If provided, the tab switcher will stay open as long as the anchor key is held down. The anchor key should be part of the keybinding that opens the switcher." + } + } + } + ] + }, "Keybinding": { "additionalProperties": false, "properties": { @@ -372,6 +397,7 @@ { "$ref": "#/definitions/WtAction" }, { "$ref": "#/definitions/CloseOtherTabsAction" }, { "$ref": "#/definitions/CloseTabsAfterAction" }, + { "$ref": "#/definitions/TabSwitcherAction" }, { "type": "null" } ] }, diff --git a/src/cascadia/TerminalApp/ActionAndArgs.cpp b/src/cascadia/TerminalApp/ActionAndArgs.cpp index f4511a00d26..fadadac3b24 100644 --- a/src/cascadia/TerminalApp/ActionAndArgs.cpp +++ b/src/cascadia/TerminalApp/ActionAndArgs.cpp @@ -44,6 +44,7 @@ static constexpr std::string_view ExecuteCommandlineKey{ "wt" }; static constexpr std::string_view ToggleCommandPaletteKey{ "commandPalette" }; static constexpr std::string_view CloseOtherTabsKey{ "closeOtherTabs" }; static constexpr std::string_view CloseTabsAfterKey{ "closeTabsAfter" }; +static constexpr std::string_view ToggleTabSwitcherKey{ "tabSwitcher" }; static constexpr std::string_view ActionKey{ "action" }; @@ -100,6 +101,7 @@ namespace winrt::TerminalApp::implementation { ToggleCommandPaletteKey, ShortcutAction::ToggleCommandPalette }, { CloseOtherTabsKey, ShortcutAction::CloseOtherTabs }, { CloseTabsAfterKey, ShortcutAction::CloseTabsAfter }, + { ToggleTabSwitcherKey, ShortcutAction::ToggleTabSwitcher }, }; using ParseResult = std::tuple>; @@ -139,6 +141,8 @@ namespace winrt::TerminalApp::implementation { ShortcutAction::CloseTabsAfter, winrt::TerminalApp::implementation::CloseTabsAfterArgs::FromJson }, + { ShortcutAction::ToggleTabSwitcher, winrt::TerminalApp::implementation::ToggleTabSwitcherArgs::FromJson }, + { ShortcutAction::Invalid, nullptr }, }; diff --git a/src/cascadia/TerminalApp/ActionArgs.cpp b/src/cascadia/TerminalApp/ActionArgs.cpp index eedf3338d9a..d6e7219fd13 100644 --- a/src/cascadia/TerminalApp/ActionArgs.cpp +++ b/src/cascadia/TerminalApp/ActionArgs.cpp @@ -19,6 +19,7 @@ #include "SetTabColorArgs.g.cpp" #include "RenameTabArgs.g.cpp" #include "ExecuteCommandlineArgs.g.cpp" +#include "ToggleTabSwitcherArgs.g.h" #include @@ -303,4 +304,22 @@ namespace winrt::TerminalApp::implementation _Index) }; } + + winrt::hstring ToggleTabSwitcherArgs::GenerateName() const + { + // If there's an anchor key set, don't generate a name so that + // it won't show up in the command palette. Only an unanchored + // tab switcher should be able to be toggled from the palette. + // TODO: GH#7179 - once this goes in, make sure to hide the + // anchor mode command that was given a name in settings. + if (_AnchorKey != Windows::System::VirtualKey::None) + { + return L""; + } + else + { + return RS_(L"ToggleTabSwitcherCommandKey"); + } + } + } diff --git a/src/cascadia/TerminalApp/ActionArgs.h b/src/cascadia/TerminalApp/ActionArgs.h index ac84dde5a5c..7c768d58054 100644 --- a/src/cascadia/TerminalApp/ActionArgs.h +++ b/src/cascadia/TerminalApp/ActionArgs.h @@ -21,6 +21,7 @@ #include "ExecuteCommandlineArgs.g.h" #include "CloseOtherTabsArgs.g.h" #include "CloseTabsAfterArgs.g.h" +#include "ToggleTabSwitcherArgs.g.h" #include "../../cascadia/inc/cppwinrt_utils.h" #include "Utils.h" @@ -512,6 +513,33 @@ namespace winrt::TerminalApp::implementation } }; + struct ToggleTabSwitcherArgs : public ToggleTabSwitcherArgsT + { + ToggleTabSwitcherArgs() = default; + GETSET_PROPERTY(Windows::System::VirtualKey, AnchorKey, Windows::System::VirtualKey::None); + + static constexpr std::string_view AnchorJsonKey{ "anchorKey" }; + + public: + hstring GenerateName() const; + + bool Equals(const IActionArgs& other) + { + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_AnchorKey == _AnchorKey; + } + return false; + }; + static FromJsonResult FromJson(const Json::Value& json) + { + // LOAD BEARING: Not using make_self here _will_ break you in the future! + auto args = winrt::make_self(); + JsonUtils::GetValueForKey(json, AnchorJsonKey, args->_AnchorKey); + return { *args, {} }; + } + }; } namespace winrt::TerminalApp::factory_implementation diff --git a/src/cascadia/TerminalApp/ActionArgs.idl b/src/cascadia/TerminalApp/ActionArgs.idl index e57c5243903..a460d712a92 100644 --- a/src/cascadia/TerminalApp/ActionArgs.idl +++ b/src/cascadia/TerminalApp/ActionArgs.idl @@ -135,4 +135,9 @@ namespace TerminalApp { UInt32 Index { get; }; }; + + [default_interface] runtimeclass ToggleTabSwitcherArgs : IActionArgs + { + Windows.System.VirtualKey AnchorKey { get; }; + }; } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 1f062241e04..19a8c44f2ad 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -285,6 +285,7 @@ namespace winrt::TerminalApp::implementation { // 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().EnableCommandPaletteMode(); CommandPalette().Visibility(CommandPalette().Visibility() == Visibility::Visible ? Visibility::Collapsed : Visibility::Visible); @@ -414,6 +415,7 @@ namespace winrt::TerminalApp::implementation actionArgs.Handled(true); } } + void TerminalPage::_HandleCloseTabsAfter(const IInspectable& /*sender*/, const TerminalApp::ActionEventArgs& actionArgs) { @@ -436,4 +438,28 @@ namespace winrt::TerminalApp::implementation actionArgs.Handled(true); } } + + void TerminalPage::_HandleToggleTabSwitcher(const IInspectable& /*sender*/, + const TerminalApp::ActionEventArgs& args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + auto anchorKey = realArgs.AnchorKey(); + + auto opt = _GetFocusedTabIndex(); + uint32_t startIdx = opt ? *opt : 0; + + if (anchorKey != VirtualKey::None) + { + // TODO: GH#7178 - delta should also have the option of being -1, in the case when + // a user decides to open the tab switcher going to the prev tab. + int delta = 1; + startIdx = (startIdx + _tabs.Size() + delta) % _tabs.Size(); + } + + CommandPalette().EnableTabSwitcherMode(anchorKey, startIdx); + CommandPalette().Visibility(Visibility::Visible); + } + args.Handled(true); + } } diff --git a/src/cascadia/TerminalApp/Command.h b/src/cascadia/TerminalApp/Command.h index 2882fc8244a..b037df3beb7 100644 --- a/src/cascadia/TerminalApp/Command.h +++ b/src/cascadia/TerminalApp/Command.h @@ -32,10 +32,13 @@ namespace winrt::TerminalApp::implementation static std::vector<::TerminalApp::SettingsLoadWarnings> LayerJson(std::unordered_map& commands, const Json::Value& json); + winrt::Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker propertyChangedRevoker; + 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); + OBSERVABLE_GETSET_PROPERTY(winrt::Windows::UI::Xaml::Controls::IconSource, IconSource, _PropertyChangedHandlers, nullptr); }; } diff --git a/src/cascadia/TerminalApp/Command.idl b/src/cascadia/TerminalApp/Command.idl index 80e1a2b476b..f54de393f9e 100644 --- a/src/cascadia/TerminalApp/Command.idl +++ b/src/cascadia/TerminalApp/Command.idl @@ -12,5 +12,7 @@ namespace TerminalApp String Name; ActionAndArgs Action; String KeyChordText; + + Windows.UI.Xaml.Controls.IconSource IconSource; } } diff --git a/src/cascadia/TerminalApp/CommandPalette.cpp b/src/cascadia/TerminalApp/CommandPalette.cpp index f6fdfa9567b..b743d8c1e29 100644 --- a/src/cascadia/TerminalApp/CommandPalette.cpp +++ b/src/cascadia/TerminalApp/CommandPalette.cpp @@ -3,6 +3,11 @@ #include "pch.h" #include "CommandPalette.h" +#include "ActionAndArgs.h" +#include "ActionArgs.h" +#include "Command.h" + +#include #include "CommandPalette.g.cpp" @@ -12,15 +17,18 @@ using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::System; using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Foundation::Collections; namespace winrt::TerminalApp::implementation { - CommandPalette::CommandPalette() + CommandPalette::CommandPalette() : + _switcherStartIdx{ 0 } { InitializeComponent(); _filteredActions = winrt::single_threaded_observable_vector(); - _allActions = winrt::single_threaded_vector(); + _allCommands = winrt::single_threaded_vector(); + _allTabActions = winrt::single_threaded_vector(); if (CommandPaletteShadow()) { @@ -38,8 +46,26 @@ namespace winrt::TerminalApp::implementation RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { if (Visibility() == Visibility::Visible) { - _searchBox().Focus(FocusState::Programmatic); - _filteredActionsView().SelectedIndex(0); + if (_currentMode == CommandPaletteMode::TabSwitcherMode) + { + if (_anchorKey != VirtualKey::None) + { + _searchBox().Visibility(Visibility::Collapsed); + _filteredActionsView().Focus(FocusState::Keyboard); + } + else + { + _searchBox().Focus(FocusState::Programmatic); + } + + _filteredActionsView().SelectedIndex(_switcherStartIdx); + _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); + } + else + { + _searchBox().Focus(FocusState::Programmatic); + _filteredActionsView().SelectedIndex(0); + } TraceLoggingWrite( g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider @@ -55,6 +81,19 @@ namespace winrt::TerminalApp::implementation _dismissPalette(); } }); + + // Focusing the ListView when the Command Palette control is set to Visible + // for the first time fails because the ListView hasn't finished loading by + // the time Focus is called. Luckily, We can listen to SizeChanged to know + // when the ListView has been measured out and is ready, and we'll immediately + // revoke the handler because we only needed to handle it once on initialization. + _sizeChangedRevoker = _filteredActionsView().SizeChanged(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { + if (_currentMode == CommandPaletteMode::TabSwitcherMode && _anchorKey != VirtualKey::None) + { + _filteredActionsView().Focus(FocusState::Keyboard); + } + _sizeChangedRevoker.revoke(); + }); } // Method Description: @@ -77,6 +116,36 @@ namespace winrt::TerminalApp::implementation _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); } + void CommandPalette::_previewKeyDownHandler(IInspectable const& /*sender*/, + Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) + { + auto key = e.OriginalKey(); + + // Some keypresses such as Tab, Return, Esc, and Arrow Keys are ignored by controls because + // they're not considered input key presses. While they don't raise KeyDown events, + // they do raise PreviewKeyDown events. + // + // Only give anchored tab switcher the ability to cycle through tabs with the tab button. + // For unanchored mode, accessibility becomes an issue when we try to hijack tab since it's + // a really widely used keyboard navigation key. + if (_currentMode == CommandPaletteMode::TabSwitcherMode && + key == VirtualKey::Tab && + _anchorKey != VirtualKey::None) + { + auto const state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift); + if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down)) + { + _selectNextItem(false); + e.Handled(true); + } + else + { + _selectNextItem(true); + e.Handled(true); + } + } + } + // 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 @@ -108,7 +177,7 @@ namespace winrt::TerminalApp::implementation if (const auto selectedItem = _filteredActionsView().SelectedItem()) { - _dispatchCommand(selectedItem.try_as()); + _dispatchCommand(selectedItem.try_as()); } e.Handled(true); @@ -129,6 +198,34 @@ namespace winrt::TerminalApp::implementation } } + void CommandPalette::_keyUpHandler(IInspectable const& /*sender*/, + Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) + { + auto key = e.OriginalKey(); + + if (_currentMode == CommandPaletteMode::TabSwitcherMode) + { + if (_anchorKey && key == _anchorKey.value()) + { + // Once the user lifts the anchor key, we'll switch to the currently selected tab + // then close the tab switcher. + + if (const auto selectedItem = _filteredActionsView().SelectedItem()) + { + if (const auto data = selectedItem.try_as()) + { + const auto actionAndArgs = data.Action(); + _dispatch.DoAction(actionAndArgs); + _updateFilteredActions(); + _dismissPalette(); + } + } + + 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, @@ -171,6 +268,28 @@ namespace winrt::TerminalApp::implementation _dispatchCommand(e.ClickedItem().try_as()); } + // Method Description: + // - Retrieve the list of commands that we should currently be filtering. + // * If the user has command with subcommands, this will return that command's subcommands. + // * If we're in Tab Switcher mode, return the tab actions. + // * Otherwise, just return the list of all the top-level commands. + // Arguments: + // - + // Return Value: + // - A list of Commands to filter. + Collections::IVector CommandPalette::_commandsToFilter() + { + switch (_currentMode) + { + case CommandPaletteMode::ActionMode: + return _allCommands; + case CommandPaletteMode::TabSwitcherMode: + return _allTabActions; + default: + return _allCommands; + } + } + // Method Description: // - Helper method for retrieving the action from a command the user // selected, and dispatching that command. Also fires a tracelogging event @@ -184,6 +303,10 @@ namespace winrt::TerminalApp::implementation { if (command) { + // Close before we dispatch so that actions that open the command + // palette like the Tab Switcher will be able to have the last laugh. + _close(); + const auto actionAndArgs = command.Action(); _dispatch.DoAction(actionAndArgs); @@ -194,8 +317,6 @@ namespace winrt::TerminalApp::implementation TraceLoggingUInt32(_searchBox().Text().size(), "SearchTextLength", "Number of characters in the search string"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); - - _close(); } } @@ -235,17 +356,59 @@ namespace winrt::TerminalApp::implementation _noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible); } - Collections::IObservableVector CommandPalette::FilteredActions() + Collections::IObservableVector CommandPalette::FilteredActions() { return _filteredActions; } - void CommandPalette::SetActions(Collections::IVector const& actions) + void CommandPalette::SetCommands(Collections::IVector const& actions) { - _allActions = actions; + _allCommands = actions; _updateFilteredActions(); } + void CommandPalette::EnableCommandPaletteMode() + { + _switchToMode(CommandPaletteMode::ActionMode); + _updateFilteredActions(); + } + + void CommandPalette::_switchToMode(CommandPaletteMode mode) + { + // The smooth remove/add animations that happen during + // UpdateFilteredActions don't work very well when switching between + // modes because of the sheer amount of remove/adds. So, let's just + // clear + append when switching between modes. + if (mode != _currentMode) + { + _currentMode = mode; + _filteredActions.Clear(); + auto commandsToFilter = _commandsToFilter(); + + for (auto action : commandsToFilter) + { + _filteredActions.Append(action); + } + + switch (_currentMode) + { + case CommandPaletteMode::TabSwitcherMode: + { + SearchBoxText(RS_(L"TabSwitcher_SearchBoxText")); + NoMatchesText(RS_(L"TabSwitcher_NoMatchesText")); + ControlName(RS_(L"TabSwitcherControlName")); + break; + } + case CommandPaletteMode::ActionMode: + default: + SearchBoxText(RS_(L"CommandPalette_SearchBox/PlaceholderText")); + NoMatchesText(RS_(L"CommandPalette_NoMatchesText/Text")); + ControlName(RS_(L"CommandPaletteControlName")); + break; + } + } + } + // 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) { @@ -259,13 +422,23 @@ namespace winrt::TerminalApp::implementation { TerminalApp::Command command; int weight; + int inOrderCounter; 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); + // If two commands have the same weight, then we'll sort them alphabetically. + // If they both have the same name, fall back to the order in which they were + // pushed into the heap. + if (command.Name() == other.command.Name()) + { + return inOrderCounter > other.inOrderCounter; + } + else + { + return !_compareCommandNames(command, other.command); + } } return weight < other.weight; } @@ -286,16 +459,30 @@ namespace winrt::TerminalApp::implementation auto searchText = _searchBox().Text(); const bool addAll = searchText.empty(); + auto commandsToFilter = _commandsToFilter(); + // 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) { + // If TabSwitcherMode, just add all as is. We don't want + // them to be sorted alphabetically. + if (_currentMode == CommandPaletteMode::TabSwitcherMode) + { + for (auto action : commandsToFilter) + { + actions.push_back(action); + } + + return actions; + } + // Add all the commands, but make sure they're sorted alphabetically. std::vector sortedCommands; - sortedCommands.reserve(_allActions.Size()); + sortedCommands.reserve(commandsToFilter.Size()); - for (auto action : _allActions) + for (auto action : commandsToFilter) { sortedCommands.push_back(action); } @@ -325,7 +512,12 @@ namespace winrt::TerminalApp::implementation // 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) + + // TODO GH#7205: Find a better way to ensure that WCs of the same + // weight and name stay in the order in which they were pushed onto + // the PQ. + uint32_t counter = 0; + for (auto action : commandsToFilter) { const auto weight = CommandPalette::_getWeight(searchText, action.Name()); if (weight > 0) @@ -333,6 +525,8 @@ namespace winrt::TerminalApp::implementation WeightedCommand wc; wc.command = action; wc.weight = weight; + wc.inOrderCounter = counter++; + heap.push(wc); } } @@ -502,8 +696,150 @@ namespace winrt::TerminalApp::implementation { Visibility(Visibility::Collapsed); + // Reset visibility in case anchor mode tab switcher just finished. + _searchBox().Visibility(Visibility::Visible); + // Clear the text box each time we close the dialog. This is consistent with VsCode. _searchBox().Text(L""); } + // Method Description: + // - Listens for changes to TerminalPage's _tabs vector. Updates our vector of + // tab switching commands accordingly. + // Arguments: + // - s: The vector being listened to. + // - e: The vector changed args that tells us whether a change, insert, or removal was performed + // on the listened-to vector. + // Return Value: + // - + void CommandPalette::OnTabsChanged(const IInspectable& s, const IVectorChangedEventArgs& e) + { + if (auto tabList = s.try_as>()) + { + auto idx = e.Index(); + auto changedEvent = e.CollectionChange(); + + switch (changedEvent) + { + case CollectionChange::ItemChanged: + { + winrt::com_ptr item; + item.copy_from(winrt::get_self(_allTabActions.GetAt(idx))); + item->propertyChangedRevoker.revoke(); + + auto tab = tabList.GetAt(idx); + GenerateCommandForTab(idx, false, tab); + UpdateTabIndices(idx); + break; + } + case CollectionChange::ItemInserted: + { + auto tab = tabList.GetAt(idx); + GenerateCommandForTab(idx, true, tab); + UpdateTabIndices(idx); + break; + } + case CollectionChange::ItemRemoved: + { + winrt::com_ptr item; + item.copy_from(winrt::get_self(_allTabActions.GetAt(idx))); + item->propertyChangedRevoker.revoke(); + + _allTabActions.RemoveAt(idx); + UpdateTabIndices(idx); + break; + } + } + + _updateFilteredActions(); + } + } + + // Method Description: + // - In the case where a tab is removed or reordered, the given indices of + // the tab switch commands following the removed/reordered tab will get out of sync by 1 + // (e.g. if tab 1 is removed, tabs 2,3,4,... need to become tabs 1,2,3,...) + // This function just loops through the tabs following startIdx and adjusts their given indices. + // Arguments: + // - startIdx: The index to start the update loop at. + // Return Value: + // - + void CommandPalette::UpdateTabIndices(const uint32_t startIdx) + { + if (startIdx != _allTabActions.Size() - 1) + { + for (auto i = startIdx; i < _allTabActions.Size(); ++i) + { + auto command = _allTabActions.GetAt(i); + + command.Action().Args().as()->TabIndex(i); + } + } + } + + // Method Description: + // - Create a tab switching command based on the given tab object and insert/update the command + // at the given index. The command will call a SwitchToTab action on the given idx. + // Arguments: + // - idx: The index to insert or update the tab switch command. + // - tab: The tab object to refer to when creating the tab switch command. + // Return Value: + // - + void CommandPalette::GenerateCommandForTab(const uint32_t idx, bool inserted, TerminalApp::Tab& tab) + { + auto focusTabAction = winrt::make_self(); + auto args = winrt::make_self(); + args->TabIndex(idx); + + focusTabAction->Action(ShortcutAction::SwitchToTab); + focusTabAction->Args(*args); + + auto command = winrt::make_self(); + command->Action(*focusTabAction); + command->Name(tab.Title()); + command->IconSource(tab.IconSource()); + + // Listen for changes to the Tab so we can update this Command's attributes accordingly. + auto weakThis{ get_weak() }; + auto weakCommand{ command->get_weak() }; + command->propertyChangedRevoker = tab.PropertyChanged(winrt::auto_revoke, [weakThis, weakCommand, tab](auto&&, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args) { + auto palette{ weakThis.get() }; + auto command{ weakCommand.get() }; + + if (palette && command) + { + if (args.PropertyName() == L"Title") + { + if (command->Name() != tab.Title()) + { + command->Name(tab.Title()); + } + } + if (args.PropertyName() == L"IconSource") + { + if (command->IconSource() != tab.IconSource()) + { + command->IconSource(tab.IconSource()); + } + } + } + }); + + if (inserted) + { + _allTabActions.InsertAt(idx, *command); + } + else + { + _allTabActions.SetAt(idx, *command); + } + } + + void CommandPalette::EnableTabSwitcherMode(const VirtualKey& anchorKey, const uint32_t startIdx) + { + _switcherStartIdx = startIdx; + _anchorKey = anchorKey; + _switchToMode(CommandPaletteMode::TabSwitcherMode); + _updateFilteredActions(); + } } diff --git a/src/cascadia/TerminalApp/CommandPalette.h b/src/cascadia/TerminalApp/CommandPalette.h index a14466f4f36..b062b42003c 100644 --- a/src/cascadia/TerminalApp/CommandPalette.h +++ b/src/cascadia/TerminalApp/CommandPalette.h @@ -8,26 +8,50 @@ namespace winrt::TerminalApp::implementation { + enum class CommandPaletteMode + { + ActionMode = 0, + TabSwitcherMode + }; + struct CommandPalette : CommandPaletteT { CommandPalette(); Windows::Foundation::Collections::IObservableVector FilteredActions(); - void SetActions(Windows::Foundation::Collections::IVector const& actions); + + void SetCommands(Windows::Foundation::Collections::IVector const& actions); + void EnableCommandPaletteMode(); void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch); + // Tab Switcher + void EnableTabSwitcherMode(const Windows::System::VirtualKey& anchorKey, const uint32_t startIdx); + void OnTabsChanged(const Windows::Foundation::IInspectable& s, const Windows::Foundation::Collections::IVectorChangedEventArgs& e); + + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, NoMatchesText, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, SearchBoxText, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ControlName, _PropertyChangedHandlers); + private: friend struct CommandPaletteT; // for Xaml to bind events Windows::Foundation::Collections::IObservableVector _filteredActions{ nullptr }; - Windows::Foundation::Collections::IVector _allActions{ nullptr }; + + Windows::Foundation::Collections::IVector _allCommands{ nullptr }; winrt::TerminalApp::ShortcutActionDispatch _dispatch; + Windows::Foundation::Collections::IVector _commandsToFilter(); + void _filterTextChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& args); + void _previewKeyDownHandler(Windows::Foundation::IInspectable const& sender, + Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); void _keyDownHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); + void _keyUpHandler(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); @@ -41,6 +65,18 @@ namespace winrt::TerminalApp::implementation static int _getWeight(const winrt::hstring& searchText, const winrt::hstring& name); void _close(); + CommandPaletteMode _currentMode; + void _switchToMode(CommandPaletteMode mode); + + // Tab Switcher + std::optional _anchorKey; + void GenerateCommandForTab(const uint32_t idx, bool inserted, winrt::TerminalApp::Tab& tab); + void UpdateTabIndices(const uint32_t startIdx); + Windows::Foundation::Collections::IVector _allTabActions{ nullptr }; + uint32_t _switcherStartIdx; + + winrt::Windows::UI::Xaml::Controls::ListView::SizeChanged_revoker _sizeChangedRevoker; + void _dispatchCommand(const TerminalApp::Command& command); void _dismissPalette(); diff --git a/src/cascadia/TerminalApp/CommandPalette.idl b/src/cascadia/TerminalApp/CommandPalette.idl index f0e7c9bb539..8286077a4e4 100644 --- a/src/cascadia/TerminalApp/CommandPalette.idl +++ b/src/cascadia/TerminalApp/CommandPalette.idl @@ -5,14 +5,22 @@ import "../Command.idl"; namespace TerminalApp { - [default_interface] runtimeclass CommandPalette : Windows.UI.Xaml.Controls.Grid + [default_interface] runtimeclass CommandPalette : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged { CommandPalette(); + String NoMatchesText { get; }; + String SearchBoxText { get; }; + String ControlName { get; }; + Windows.Foundation.Collections.IObservableVector FilteredActions { get; }; - void SetActions(Windows.Foundation.Collections.IVector actions); + void SetCommands(Windows.Foundation.Collections.IVector actions); + void EnableCommandPaletteMode(); void SetDispatch(ShortcutActionDispatch dispatch); + + void EnableTabSwitcherMode(Windows.System.VirtualKey anchorKey, UInt32 startIdx); + void OnTabsChanged(IInspectable s, Windows.Foundation.Collections.IVectorChangedEventArgs e); } } diff --git a/src/cascadia/TerminalApp/CommandPalette.xaml b/src/cascadia/TerminalApp/CommandPalette.xaml index 39b0e1a25be..50b35e8bbb6 100644 --- a/src/cascadia/TerminalApp/CommandPalette.xaml +++ b/src/cascadia/TerminalApp/CommandPalette.xaml @@ -1,6 +1,6 @@ - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Windows10version1903="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 8)" + TabNavigation="Cycle" + IsTabStop="True" + AllowFocusOnInteraction="True" PointerPressed="_rootPointerPressed" - mc:Ignorable="d"> + PreviewKeyDown="_previewKeyDownHandler" + KeyDown="_keyDownHandler" + PreviewKeyUp="_keyUpHandler" + mc:Ignorable="d" + AutomationProperties.Name="{x:Bind ControlName, Mode=OneWay}"> - + + - + - - - - - + + + + + - - - - + + + + - + - VerticalAlignment="Stretch"> - HorizontalAlignment="Stretch" VerticalAlignment="Top"> - - - - - + + + + + - - + - - + Grid.Row="1" + Text="{x:Bind NoMatchesText, Mode=OneWay}"> + - AllowDrop="False" IsItemClickEnabled="True" ItemClick="_listItemClicked" + PreviewKeyDown="_keyDownHandler" ItemsSource="{x:Bind FilteredActions}"> - - + + - - - + - - - - - - + + + + + + - + - - - - - + - - - - - + + + + + - + - + + diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index aaded2a06b4..c891d53df6a 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -529,6 +529,9 @@ Toggle command palette + + Command Palette + Set color scheme to {0} {0} will be replaced with the name of a color scheme as defined by the user. @@ -562,6 +565,18 @@ Close tabs after index {0} {0} will be replaced with a number + + Tab Switcher + + + Toggle tab switcher + + + Type a tab name... + + + No matching tab name + Crimson diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp index 862eb1a9de6..e8b9eb778c4 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp @@ -220,6 +220,11 @@ namespace winrt::TerminalApp::implementation _CloseTabsAfterHandlers(*this, *eventArgs); break; } + case ShortcutAction::ToggleTabSwitcher: + { + _ToggleTabSwitcherHandlers(*this, *eventArgs); + break; + } default: return false; } diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.h b/src/cascadia/TerminalApp/ShortcutActionDispatch.h index 1005129b531..fa139848e63 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.h +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.h @@ -59,6 +59,7 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(ExecuteCommandline, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); TYPED_EVENT(CloseOtherTabs, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); TYPED_EVENT(CloseTabsAfter, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ToggleTabSwitcher, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); // clang-format on private: diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl index 83681760755..b0341d4bf81 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl @@ -44,7 +44,8 @@ namespace TerminalApp ExecuteCommandline, ToggleCommandPalette, CloseOtherTabs, - CloseTabsAfter + CloseTabsAfter, + ToggleTabSwitcher }; [default_interface] runtimeclass ActionAndArgs { @@ -94,5 +95,6 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler ExecuteCommandline; event Windows.Foundation.TypedEventHandler CloseOtherTabs; event Windows.Foundation.TypedEventHandler CloseTabsAfter; + event Windows.Foundation.TypedEventHandler ToggleTabSwitcher; } } diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index eb396275303..ead28ce1f02 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -18,6 +18,7 @@ using namespace winrt::Windows::System; namespace winrt { namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; } namespace winrt::TerminalApp::implementation @@ -225,7 +226,8 @@ namespace winrt::TerminalApp::implementation if (auto tab{ weakThis.get() }) { - IconPath(_lastIconPath); + // The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX... + IconSource(GetColoredIcon(_lastIconPath)); _tabViewItem.IconSource(GetColoredIcon(_lastIconPath)); } } diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index 6e3e5cc5b95..02055b05767 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -73,7 +73,7 @@ namespace winrt::TerminalApp::implementation DECLARE_EVENT(ColorCleared, _colorCleared, winrt::delegate<>); OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Title, _PropertyChangedHandlers); - OBSERVABLE_GETSET_PROPERTY(winrt::hstring, IconPath, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(winrt::Windows::UI::Xaml::Controls::IconSource, IconSource, _PropertyChangedHandlers, nullptr); private: std::shared_ptr _rootPane{ nullptr }; diff --git a/src/cascadia/TerminalApp/Tab.idl b/src/cascadia/TerminalApp/Tab.idl index 955c243845a..5aba64605fc 100644 --- a/src/cascadia/TerminalApp/Tab.idl +++ b/src/cascadia/TerminalApp/Tab.idl @@ -6,6 +6,6 @@ namespace TerminalApp [default_interface] runtimeclass Tab : Windows.UI.Xaml.Data.INotifyPropertyChanged { String Title { get; }; - String IconPath { get; }; + Windows.UI.Xaml.Controls.IconSource IconSource { get; }; } } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 3b245440c03..3838e4ccf50 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -80,9 +80,20 @@ namespace winrt::TerminalApp::implementation { command.KeyChordText(KeyChordSerialization::ToString(keyChord)); } + + // Set the default IconSource to a BitmapIconSource with a null source + // (instead of just nullptr) because there's a really weird crash when swapping + // data bound IconSourceElements in a ListViewTemplate (i.e. CommandPalette). + // Swapping between nullptr IconSources and non-null IconSources causes a crash + // to occur, but swapping between IconSources with a null source and non-null IconSources + // work perfectly fine :shrug:. + winrt::Windows::UI::Xaml::Controls::BitmapIconSource icon; + icon.UriSource(nullptr); + command.IconSource(icon); + commandsCollection.Append(command); } - CommandPalette().SetActions(commandsCollection); + CommandPalette().SetCommands(commandsCollection); } } @@ -211,6 +222,13 @@ namespace winrt::TerminalApp::implementation } }); + _tabs.VectorChanged([weakThis{ get_weak() }](auto&& s, auto&& e) { + if (auto page{ weakThis.get() }) + { + page->CommandPalette().OnTabsChanged(s, e); + } + }); + // 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. @@ -897,6 +915,7 @@ namespace winrt::TerminalApp::implementation _actionDispatch->ExecuteCommandline({ this, &TerminalPage::_HandleExecuteCommandline }); _actionDispatch->CloseOtherTabs({ this, &TerminalPage::_HandleCloseOtherTabs }); _actionDispatch->CloseTabsAfter({ this, &TerminalPage::_HandleCloseTabsAfter }); + _actionDispatch->ToggleTabSwitcher({ this, &TerminalPage::_HandleToggleTabSwitcher }); } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 30ca3bced58..984f1064245 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -230,6 +230,7 @@ namespace winrt::TerminalApp::implementation void _HandleToggleCommandPalette(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); void _HandleCloseOtherTabs(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); void _HandleCloseTabsAfter(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); + void _HandleToggleTabSwitcher(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); // Make sure to hook new actions up in _RegisterActionCallbacks! #pragma endregion diff --git a/src/cascadia/TerminalApp/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalApp/TerminalSettingsSerializationHelpers.h index f7058a49a74..80459845a09 100644 --- a/src/cascadia/TerminalApp/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalApp/TerminalSettingsSerializationHelpers.h @@ -270,3 +270,12 @@ JSON_ENUM_MAPPER(::winrt::TerminalApp::SettingsTarget) pair_type{ "allFiles", ValueType::AllFiles }, }; }; + +JSON_ENUM_MAPPER(::winrt::Windows::System::VirtualKey) +{ + JSON_MAPPINGS(3) = { + pair_type{ "ctrl", ValueType::Control }, + pair_type{ "alt", ValueType::Menu }, + pair_type{ "shift", ValueType::Shift }, + }; +}; diff --git a/src/cascadia/inc/cppwinrt_utils.h b/src/cascadia/inc/cppwinrt_utils.h index 3d9599bbeef..dcc53ef23ce 100644 --- a/src/cascadia/inc/cppwinrt_utils.h +++ b/src/cascadia/inc/cppwinrt_utils.h @@ -114,7 +114,7 @@ private: \ // 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) \ +#define OBSERVABLE_GETSET_PROPERTY(type, name, event, ...) \ public: \ type name() { return _##name; }; \ void name(const type& value) \ @@ -127,7 +127,7 @@ public: }; \ \ private: \ - const type _##name; \ + const type _##name{ __VA_ARGS__ }; \ void _set##name(const type& value) \ { \ const_cast(_##name) = value; \