// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "ActionPaletteItem.h" #include "TabPaletteItem.h" #include "CommandLinePaletteItem.h" #include "CommandPalette.h" #include #include "CommandPalette.g.cpp" using namespace winrt; using namespace winrt::TerminalApp; using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Controls; using namespace winrt::Windows::System; using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Microsoft::Terminal::Settings::Model; namespace winrt::TerminalApp::implementation { CommandPalette::CommandPalette() : _switcherStartIdx{ 0 } { InitializeComponent(); _itemTemplateSelector = Resources().Lookup(winrt::box_value(L"PaletteItemTemplateSelector")).try_as(); _listItemTemplate = Resources().Lookup(winrt::box_value(L"ListItemTemplate")).try_as(); _filteredActions = winrt::single_threaded_observable_vector(); _nestedActionStack = winrt::single_threaded_vector(); _currentNestedCommands = winrt::single_threaded_vector(); _allCommands = winrt::single_threaded_vector(); _tabActions = winrt::single_threaded_vector(); _mruTabActions = winrt::single_threaded_vector(); _commandLineHistory = winrt::single_threaded_vector(); _switchToMode(CommandPaletteMode::ActionMode); 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) { // Force immediate binding update so we can select an item Bindings->Update(); if (_currentMode == CommandPaletteMode::TabSwitchMode) { _searchBox().Visibility(Visibility::Collapsed); _filteredActionsView().SelectedIndex(_switcherStartIdx); _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); _filteredActionsView().Focus(FocusState::Keyboard); // Do this right after becoming visible so we can quickly catch scenarios where // modifiers aren't held down (e.g. command palette invocation). _anchorKeyUpHandler(); } else { _filteredActionsView().SelectedIndex(0); _searchBox().Focus(FocusState::Programmatic); } TraceLoggingWrite( g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider "CommandPaletteOpened", TraceLoggingDescription("Event emitted when the Command Palette is opened"), TraceLoggingWideString(L"Action", "Mode", "which mode the palette was opened in"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); } else { // Raise an event to return control to the Terminal. _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::TabSwitchMode) { _filteredActionsView().Focus(FocusState::Keyboard); } _sizeChangedRevoker.revoke(); }); _filteredActionsView().SelectionChanged({ this, &CommandPalette::_selectedCommandChanged }); _appArgs.DisableHelpInExitMessage(); } // 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) { auto selected = _filteredActionsView().SelectedIndex(); const int numItems = ::base::saturated_cast(_filteredActionsView().Items().Size()); // Do not try to select an item if // - the list is empty // - if no item is selected and "up" is pressed if (numItems != 0 && (selected != -1 || moveDown)) { // 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: // - Scroll the command palette to the specified index // Arguments: // - index within a list view of commands // Return Value: // - void CommandPalette::_scrollToIndex(uint32_t index) { auto numItems = _filteredActionsView().Items().Size(); if (numItems == 0) { // if the list is empty no need to scroll return; } auto clampedIndex = std::clamp(index, 0, numItems - 1); _filteredActionsView().SelectedIndex(clampedIndex); _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); } // Method Description: // - Computes the number of visible commands // Arguments: // - // Return Value: // - the approximate number of items visible in the list (in other words the size of the page) uint32_t CommandPalette::_getNumVisibleItems() { const auto container = _filteredActionsView().ContainerFromIndex(0); const auto item = container.try_as(); const auto itemHeight = ::base::saturated_cast(item.ActualHeight()); const auto listHeight = ::base::saturated_cast(_filteredActionsView().ActualHeight()); return listHeight / itemHeight; } // Method Description: // - Scrolls the focus one page up the list of commands. // Arguments: // - // Return Value: // - void CommandPalette::ScrollPageUp() { auto selected = _filteredActionsView().SelectedIndex(); auto numVisibleItems = _getNumVisibleItems(); _scrollToIndex(selected - numVisibleItems); } // Method Description: // - Scrolls the focus one page down the list of commands. // Arguments: // - // Return Value: // - void CommandPalette::ScrollPageDown() { auto selected = _filteredActionsView().SelectedIndex(); auto numVisibleItems = _getNumVisibleItems(); _scrollToIndex(selected + numVisibleItems); } // Method Description: // - Moves the focus to the top item in the list of commands. // Arguments: // - // Return Value: // - void CommandPalette::ScrollToTop() { _scrollToIndex(0); } // Method Description: // - Moves the focus to the bottom item in the list of commands. // Arguments: // - // Return Value: // - void CommandPalette::ScrollToBottom() { _scrollToIndex(_filteredActionsView().Items().Size() - 1); } // Method Description: // - Called when the command selection changes. We'll use this in the tab // switcher to "preview" tabs as the user navigates the list of tabs. To // do that, we'll dispatch the switch to tab command for this tab, but not // dismiss the switcher. // Arguments: // - // Return Value: // - void CommandPalette::_selectedCommandChanged(const IInspectable& /*sender*/, const Windows::UI::Xaml::RoutedEventArgs& /*args*/) { const auto selectedCommand = _filteredActionsView().SelectedItem(); const auto filteredCommand{ selectedCommand.try_as() }; if (_currentMode == CommandPaletteMode::TabSwitchMode) { _switchToTab(filteredCommand); } else if (_currentMode == CommandPaletteMode::ActionMode && filteredCommand != nullptr) { if (const auto actionPaletteItem{ filteredCommand.Item().try_as() }) { _PreviewActionHandlers(*this, actionPaletteItem.Command()); } } } void CommandPalette::_previewKeyDownHandler(IInspectable const& /*sender*/, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) { const auto key = e.OriginalKey(); const auto scanCode = e.KeyStatus().ScanCode; const auto coreWindow = CoreWindow::GetForCurrentThread(); const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); // 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::TabSwitchMode && _actionMap) { winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, false, static_cast(key), static_cast(scanCode) }; if (const auto cmd{ _actionMap.GetActionByKeyChord(kc) }) { if (cmd.ActionAndArgs().Action() == ShortcutAction::PrevTab) { SelectNextItem(false); e.Handled(true); return; } else if (cmd.ActionAndArgs().Action() == ShortcutAction::NextTab) { SelectNextItem(true); e.Handled(true); return; } } } if (key == VirtualKey::Home && ctrlDown) { ScrollToTop(); e.Handled(true); } else if (key == VirtualKey::End && ctrlDown) { ScrollToBottom(); e.Handled(true); } else 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::PageUp) { // Action Mode: Move focus to the first visible item in the list. ScrollPageUp(); e.Handled(true); } else if (key == VirtualKey::PageDown) { // Action Mode: Move focus to the last visible item in the list. ScrollPageDown(); e.Handled(true); } else if (key == VirtualKey::Enter) { if (const auto& button = e.OriginalSource().try_as