mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 00:48:23 -06:00
Between fmt 7.1.3 and 11.0.2 a lot has happened. `wchar_t` support is now more limited and implicit conversions don't work anymore. Furthermore, even the non-`FMT_COMPILE` API is now compile-time checked and so it fails to work in our UI code which passes `hstring` format strings which aren't implicitly convertible to the expected type. `fmt::runtime` was introduced for this but it also fails to work for `hstring` parameters. To solve this, a new `RS_fmt` macro was added to abstract the added `std::wstring_view` casting away. Finally, some additional changes to reduce `stringstream` usage have been made, whenever `format_to`, etc., is available. This mostly affects `ActionArgs.cpp`. Closes #16000 ## Validation Steps Performed * Compiles ✅ * Settings page opens ✅
1214 lines
49 KiB
C++
1214 lines
49 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "ActionPaletteItem.h"
|
|
#include "CommandLinePaletteItem.h"
|
|
#include "SuggestionsControl.h"
|
|
#include <LibraryResources.h>
|
|
|
|
#include "SuggestionsControl.g.cpp"
|
|
#include "../../types/inc/utils.hpp"
|
|
|
|
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;
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
namespace winrt::TerminalApp::implementation
|
|
{
|
|
SuggestionsControl::SuggestionsControl()
|
|
{
|
|
InitializeComponent();
|
|
|
|
_itemTemplateSelector = Resources().Lookup(winrt::box_value(L"PaletteItemTemplateSelector")).try_as<PaletteItemTemplateSelector>();
|
|
_listItemTemplate = Resources().Lookup(winrt::box_value(L"ListItemTemplate")).try_as<DataTemplate>();
|
|
|
|
_filteredActions = winrt::single_threaded_observable_vector<winrt::TerminalApp::FilteredCommand>();
|
|
_nestedActionStack = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
|
|
_currentNestedCommands = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
|
|
_allCommands = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
|
|
|
|
_switchToMode();
|
|
|
|
// 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();
|
|
UpdateLayout(); // THIS ONE IN PARTICULAR SEEMS LOAD BEARING.
|
|
// Without the UpdateLayout call, our ListView won't have a
|
|
// chance to instantiate ListViewItem's. If we don't have those,
|
|
// then our call to `SelectedItem()` below is going to return
|
|
// null. If it does that, then we won't be able to focus
|
|
// ourselves when we're opened.
|
|
|
|
// Select the correct element in the list, depending on which
|
|
// direction we were opened in.
|
|
//
|
|
// Make sure to use _scrollToIndex, to move the scrollbar too!
|
|
if (_direction == TerminalApp::SuggestionsDirection::TopDown)
|
|
{
|
|
_scrollToIndex(0);
|
|
}
|
|
else // BottomUp
|
|
{
|
|
_scrollToIndex(_filteredActionsView().Items().Size() - 1);
|
|
}
|
|
|
|
if (_mode == SuggestionsMode::Palette)
|
|
{
|
|
// Toss focus into the search box in palette mode
|
|
_searchBox().Visibility(Visibility::Visible);
|
|
_searchBox().Focus(FocusState::Programmatic);
|
|
}
|
|
else if (_mode == SuggestionsMode::Menu)
|
|
{
|
|
// Toss focus onto the selected item in menu mode.
|
|
// Don't just focus the _filteredActionsView, because that will always select the 0th element.
|
|
|
|
_searchBox().Visibility(Visibility::Collapsed);
|
|
|
|
if (const auto& dependencyObj = SelectedItem().try_as<winrt::Windows::UI::Xaml::DependencyObject>())
|
|
{
|
|
Input::FocusManager::TryFocusAsync(dependencyObj, FocusState::Programmatic);
|
|
}
|
|
}
|
|
|
|
TraceLoggingWrite(
|
|
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
|
|
"SuggestionsControlOpened",
|
|
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_ProductAndServiceUsage));
|
|
}
|
|
else
|
|
{
|
|
// Raise an event to return control to the Terminal.
|
|
_dismissPalette();
|
|
}
|
|
});
|
|
|
|
_sizeChangedRevoker = _filteredActionsView().SizeChanged(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) {
|
|
// When we're in BottomUp mode, we need to adjust our own position
|
|
// so that our bottom is aligned with our origin. This will ensure
|
|
// that as the menu changes in size (as we filter results), the menu
|
|
// stays "attached" to the cursor.
|
|
if (Visibility() == Visibility::Visible && _direction == TerminalApp::SuggestionsDirection::BottomUp)
|
|
{
|
|
this->_recalculateTopMargin();
|
|
}
|
|
});
|
|
|
|
_filteredActionsView().SelectionChanged({ this, &SuggestionsControl::_selectedCommandChanged });
|
|
}
|
|
|
|
TerminalApp::SuggestionsMode SuggestionsControl::Mode() const
|
|
{
|
|
return _mode;
|
|
}
|
|
void SuggestionsControl::Mode(TerminalApp::SuggestionsMode mode)
|
|
{
|
|
_mode = mode;
|
|
|
|
if (_mode == SuggestionsMode::Palette)
|
|
{
|
|
_searchBox().Visibility(Visibility::Visible);
|
|
_searchBox().Focus(FocusState::Programmatic);
|
|
}
|
|
else if (_mode == SuggestionsMode::Menu)
|
|
{
|
|
_searchBox().Visibility(Visibility::Collapsed);
|
|
_filteredActionsView().Focus(FocusState::Programmatic);
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
// - <none>
|
|
void SuggestionsControl::SelectNextItem(const bool moveDown)
|
|
{
|
|
auto selected = _filteredActionsView().SelectedIndex();
|
|
const auto numItems = ::base::saturated_cast<int>(_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:
|
|
// - <none>
|
|
void SuggestionsControl::_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<int32_t>(index, 0, numItems - 1);
|
|
_filteredActionsView().SelectedIndex(clampedIndex);
|
|
_filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem());
|
|
}
|
|
|
|
// Method Description:
|
|
// - Computes the number of visible commands
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - the approximate number of items visible in the list (in other words the size of the page)
|
|
uint32_t SuggestionsControl::_getNumVisibleItems()
|
|
{
|
|
if (const auto container = _filteredActionsView().ContainerFromIndex(0))
|
|
{
|
|
if (const auto item = container.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>())
|
|
{
|
|
const auto itemHeight = ::base::saturated_cast<int>(item.ActualHeight());
|
|
const auto listHeight = ::base::saturated_cast<int>(_filteredActionsView().ActualHeight());
|
|
return listHeight / itemHeight;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Scrolls the focus one page up the list of commands.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::ScrollPageUp()
|
|
{
|
|
auto selected = _filteredActionsView().SelectedIndex();
|
|
auto numVisibleItems = _getNumVisibleItems();
|
|
_scrollToIndex(selected - numVisibleItems);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Scrolls the focus one page down the list of commands.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::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:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::ScrollToTop()
|
|
{
|
|
_scrollToIndex(0);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Moves the focus to the bottom item in the list of commands.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::ScrollToBottom()
|
|
{
|
|
_scrollToIndex(_filteredActionsView().Items().Size() - 1);
|
|
}
|
|
|
|
Windows::UI::Xaml::FrameworkElement SuggestionsControl::SelectedItem()
|
|
{
|
|
auto index = _filteredActionsView().SelectedIndex();
|
|
const auto container = _filteredActionsView().ContainerFromIndex(index);
|
|
const auto item = container.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>();
|
|
return item;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Called when the command selection changes. We'll use this to preview the selected action.
|
|
// Arguments:
|
|
// - <unused>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_selectedCommandChanged(const IInspectable& /*sender*/,
|
|
const Windows::UI::Xaml::RoutedEventArgs& /*args*/)
|
|
{
|
|
const auto selectedCommand = _filteredActionsView().SelectedItem();
|
|
const auto filteredCommand{ selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>() };
|
|
|
|
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"SelectedItem" });
|
|
|
|
// Make sure to not send the preview if we're collapsed. This can
|
|
// sometimes fire after we've been closed, which can trigger us to
|
|
// preview the action for the empty text (as we've cleared the search
|
|
// text as a part of closing).
|
|
const bool isVisible{ this->Visibility() == Visibility::Visible };
|
|
|
|
if (filteredCommand != nullptr &&
|
|
isVisible)
|
|
{
|
|
if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
|
|
{
|
|
const auto& cmd = actionPaletteItem.Command();
|
|
PreviewAction.raise(*this, cmd);
|
|
|
|
const auto description{ cmd.Description() };
|
|
|
|
if (const auto& selected{ SelectedItem() })
|
|
{
|
|
selected.SetValue(Automation::AutomationProperties::FullDescriptionProperty(), winrt::box_value(description));
|
|
}
|
|
|
|
if (!description.empty())
|
|
{
|
|
_openTooltip(cmd);
|
|
}
|
|
else
|
|
{
|
|
// If there's no description, then just close the tooltip.
|
|
_descriptionsView().Visibility(Visibility::Collapsed);
|
|
_descriptionsBackdrop().Visibility(Visibility::Collapsed);
|
|
_recalculateTopMargin();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SuggestionsControl::_openTooltip(Command cmd)
|
|
{
|
|
const auto description{ cmd.Description() };
|
|
if (description.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Build the contents of the "tooltip" based on the description
|
|
//
|
|
// First, the title. This is just the name of the command.
|
|
_descriptionTitle().Inlines().Clear();
|
|
Documents::Run titleRun;
|
|
titleRun.Text(cmd.Name());
|
|
_descriptionTitle().Inlines().Append(titleRun);
|
|
|
|
// Now fill up the "subtitle" part of the "tooltip" with the actual
|
|
// description itself.
|
|
const auto& inlines{ _descriptionComment().Inlines() };
|
|
inlines.Clear();
|
|
|
|
// Split the filtered description on '\n`
|
|
const auto lines = ::Microsoft::Console::Utils::SplitString(description, L'\n');
|
|
// build a Run + LineBreak, and add them to the text block
|
|
for (const auto& line : lines)
|
|
{
|
|
// Trim off any `\r`'s in the string. Pwsh completions will
|
|
// frequently have these embedded.
|
|
std::wstring trimmed{ line };
|
|
trimmed.erase(std::remove(trimmed.begin(), trimmed.end(), L'\r'), trimmed.end());
|
|
if (trimmed.empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Documents::Run textRun;
|
|
textRun.Text(trimmed);
|
|
inlines.Append(textRun);
|
|
inlines.Append(Documents::LineBreak{});
|
|
}
|
|
|
|
// Now, make ourselves visible.
|
|
_descriptionsView().Visibility(Visibility::Visible);
|
|
_descriptionsBackdrop().Visibility(Visibility::Visible);
|
|
// and update the padding to account for our new contents.
|
|
_recalculateTopMargin();
|
|
return;
|
|
}
|
|
|
|
void SuggestionsControl::_previewKeyDownHandler(const IInspectable& /*sender*/,
|
|
const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e)
|
|
{
|
|
const auto key = e.OriginalKey();
|
|
const auto coreWindow = CoreWindow::GetForCurrentThread();
|
|
const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
|
|
|
|
if (key == VirtualKey::Home && ctrlDown)
|
|
{
|
|
ScrollToTop();
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::End && ctrlDown)
|
|
{
|
|
ScrollToBottom();
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::Up)
|
|
{
|
|
// Move focus to the next item in the list.
|
|
SelectNextItem(false);
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::Down)
|
|
{
|
|
// Move focus to the previous item in the list.
|
|
SelectNextItem(true);
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::PageUp)
|
|
{
|
|
// Move focus to the first visible item in the list.
|
|
ScrollPageUp();
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::PageDown)
|
|
{
|
|
// Move focus to the last visible item in the list.
|
|
ScrollPageDown();
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::Enter ||
|
|
key == VirtualKey::Tab ||
|
|
key == VirtualKey::Right)
|
|
{
|
|
// If the user pressed enter, tab, or the right arrow key, then
|
|
// we'll want to dispatch the command that's selected as they
|
|
// accepted the suggestion.
|
|
|
|
if (const auto& button = e.OriginalSource().try_as<Button>())
|
|
{
|
|
// Let the button handle the Enter key so an eventually attached click handler will be called
|
|
e.Handled(false);
|
|
return;
|
|
}
|
|
|
|
const auto selectedCommand = _filteredActionsView().SelectedItem();
|
|
const auto filteredCommand = selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>();
|
|
_dispatchCommand(filteredCommand);
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::Escape)
|
|
{
|
|
// Dismiss the palette if the text is empty, otherwise clear the
|
|
// search string.
|
|
if (_searchBox().Text().empty())
|
|
{
|
|
_dismissPalette();
|
|
}
|
|
else
|
|
{
|
|
_searchBox().Text(L"");
|
|
}
|
|
|
|
e.Handled(true);
|
|
}
|
|
|
|
else if (key == VirtualKey::C && ctrlDown)
|
|
{
|
|
_searchBox().CopySelectionToClipboard();
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::V && ctrlDown)
|
|
{
|
|
_searchBox().PasteFromClipboard();
|
|
e.Handled(true);
|
|
}
|
|
|
|
// If the user types a character while the menu (not in palette mode)
|
|
// is open, then dismiss ourselves. That way, when you type a character,
|
|
// we'll instead send it to the TermControl.
|
|
if (_mode == SuggestionsMode::Menu && !e.Handled())
|
|
{
|
|
_dismissPalette();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Implements the Alt handler
|
|
// Return value:
|
|
// - whether the key was handled
|
|
bool SuggestionsControl::OnDirectKeyEvent(const uint32_t /*vkey*/, const uint8_t /*scanCode*/, const bool /*down*/)
|
|
{
|
|
auto handled = false;
|
|
return handled;
|
|
}
|
|
|
|
void SuggestionsControl::_keyUpHandler(const IInspectable& /*sender*/,
|
|
const Windows::UI::Xaml::Input::KeyRoutedEventArgs& /*e*/)
|
|
{
|
|
}
|
|
|
|
// 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:
|
|
// - <unused>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_rootPointerPressed(const Windows::Foundation::IInspectable& /*sender*/,
|
|
const Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*e*/)
|
|
{
|
|
if (Visibility() != Visibility::Collapsed)
|
|
{
|
|
_dismissPalette();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - The purpose of this event handler is to hide the palette if it loses focus.
|
|
// We say we lost focus if our root element and all its descendants lost focus.
|
|
// This handler is invoked when our root element or some descendant loses focus.
|
|
// At this point we need to learn if the newly focused element belongs to this palette.
|
|
// To achieve this:
|
|
// - We start with the newly focused element and traverse its visual ancestors up to the Xaml root.
|
|
// - If one of the ancestors is this SuggestionsControl, then by our definition the focus is not lost
|
|
// - If we reach the Xaml root without meeting this SuggestionsControl,
|
|
// then the focus is not contained in it anymore and it should be dismissed
|
|
// Arguments:
|
|
// - <unused>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_lostFocusHandler(const Windows::Foundation::IInspectable& /*sender*/,
|
|
const Windows::UI::Xaml::RoutedEventArgs& /*args*/)
|
|
{
|
|
const auto flyout = _searchBox().ContextFlyout();
|
|
if (flyout && flyout.IsOpen())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto root = this->XamlRoot();
|
|
if (!root)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto focusedElementOrAncestor = Input::FocusManager::GetFocusedElement(root).try_as<DependencyObject>();
|
|
while (focusedElementOrAncestor)
|
|
{
|
|
if (focusedElementOrAncestor == *this)
|
|
{
|
|
// This palette is the focused element or an ancestor of the focused element. No need to dismiss.
|
|
return;
|
|
}
|
|
|
|
// Go up to the next ancestor
|
|
focusedElementOrAncestor = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::GetParent(focusedElementOrAncestor);
|
|
}
|
|
|
|
// We got to the root (the element with no parent) and didn't meet this palette on the path.
|
|
// It means that it lost the focus and needs to be dismissed.
|
|
_dismissPalette();
|
|
}
|
|
|
|
// 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:
|
|
// - <none>
|
|
void SuggestionsControl::_backdropPointerPressed(const Windows::Foundation::IInspectable& /*sender*/,
|
|
const Windows::UI::Xaml::Input::PointerRoutedEventArgs& 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:
|
|
// - <none>
|
|
void SuggestionsControl::_listItemClicked(const Windows::Foundation::IInspectable& /*sender*/,
|
|
const Windows::UI::Xaml::Controls::ItemClickEventArgs& e)
|
|
{
|
|
const auto selectedCommand = e.ClickedItem();
|
|
if (const auto filteredCommand = selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>())
|
|
{
|
|
_dispatchCommand(filteredCommand);
|
|
}
|
|
}
|
|
|
|
void SuggestionsControl::_listItemSelectionChanged(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e)
|
|
{
|
|
if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(_searchBox()) })
|
|
{
|
|
if (const auto selectedList = e.AddedItems(); selectedList.Size() > 0)
|
|
{
|
|
const auto selectedCommand = selectedList.GetAt(0);
|
|
if (const auto filteredCmd = selectedCommand.try_as<TerminalApp::FilteredCommand>())
|
|
{
|
|
if (const auto paletteItem = filteredCmd.Item().try_as<TerminalApp::PaletteItem>())
|
|
{
|
|
automationPeer.RaiseNotificationEvent(
|
|
Automation::Peers::AutomationNotificationKind::ItemAdded,
|
|
Automation::Peers::AutomationNotificationProcessing::MostRecent,
|
|
paletteItem.Name(),
|
|
L"SuggestionsControlSelectedItemChanged" /* unique name for this notification category */);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// This event is called when the user clicks on a ChevronLeft button right
|
|
// next to the ParentCommandName (e.g. New Tab...) above the subcommands list.
|
|
// It'll go up a single level when the user clicks the button.
|
|
// Arguments:
|
|
// - sender: the button that got clicked
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_moveBackButtonClicked(const Windows::Foundation::IInspectable& /*sender*/,
|
|
const Windows::UI::Xaml::RoutedEventArgs&)
|
|
{
|
|
PreviewAction.raise(*this, nullptr);
|
|
_searchBox().Focus(FocusState::Programmatic);
|
|
|
|
const auto previousAction{ _nestedActionStack.GetAt(_nestedActionStack.Size() - 1) };
|
|
_nestedActionStack.RemoveAtEnd();
|
|
|
|
// Repopulate nested commands when the root has not been reached yet
|
|
if (_nestedActionStack.Size() > 0)
|
|
{
|
|
const auto newPreviousAction{ _nestedActionStack.GetAt(_nestedActionStack.Size() - 1) };
|
|
const auto actionPaletteItem{ newPreviousAction.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() };
|
|
|
|
ParentCommandName(actionPaletteItem.Command().Name());
|
|
_updateCurrentNestedCommands(actionPaletteItem.Command());
|
|
}
|
|
else
|
|
{
|
|
ParentCommandName(L"");
|
|
_currentNestedCommands.Clear();
|
|
}
|
|
_updateFilteredActions();
|
|
|
|
const auto lastSelectedIt = std::find_if(begin(_filteredActions), end(_filteredActions), [&](const auto& filteredCommand) {
|
|
return filteredCommand.Item().Name() == previousAction.Item().Name();
|
|
});
|
|
const auto lastSelectedIndex = static_cast<int32_t>(std::distance(begin(_filteredActions), lastSelectedIt));
|
|
_scrollToIndex(lastSelectedIt != end(_filteredActions) ? lastSelectedIndex : 0);
|
|
}
|
|
|
|
// Method Description:
|
|
// - This is called when the user selects a command with subcommands. It
|
|
// will update our UI to now display the list of subcommands instead, and
|
|
// clear the search text so the user can search from the new list of
|
|
// commands.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_updateUIForStackChange()
|
|
{
|
|
if (_searchBox().Text().empty())
|
|
{
|
|
// Manually call _filterTextChanged, because setting the text to the
|
|
// empty string won't update it for us (as it won't actually change value.)
|
|
_filterTextChanged(nullptr, nullptr);
|
|
}
|
|
|
|
// Changing the value of the search box will trigger _filterTextChanged,
|
|
// which will cause us to refresh the list of filterable commands.
|
|
_searchBox().Text(L"");
|
|
_searchBox().Focus(FocusState::Programmatic);
|
|
|
|
if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(_searchBox()) })
|
|
{
|
|
automationPeer.RaiseNotificationEvent(
|
|
Automation::Peers::AutomationNotificationKind::ActionCompleted,
|
|
Automation::Peers::AutomationNotificationProcessing::CurrentThenMostRecent,
|
|
RS_fmt(L"SuggestionsControl_NestedCommandAnnouncement", ParentCommandName()),
|
|
L"SuggestionsControlNestingLevelChanged" /* unique name for this notification category */);
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
// - <none>
|
|
// Return Value:
|
|
// - A list of Commands to filter.
|
|
Collections::IVector<winrt::TerminalApp::FilteredCommand> SuggestionsControl::_commandsToFilter()
|
|
{
|
|
if (_nestedActionStack.Size() > 0)
|
|
{
|
|
return _currentNestedCommands;
|
|
}
|
|
|
|
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
|
|
// indicating that the user successfully found the action they were
|
|
// looking for.
|
|
// Arguments:
|
|
// - command: the Command to dispatch. This might be null.
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_dispatchCommand(const winrt::TerminalApp::FilteredCommand& filteredCommand)
|
|
{
|
|
if (filteredCommand)
|
|
{
|
|
if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
|
|
{
|
|
if (actionPaletteItem.Command().HasNestedCommands())
|
|
{
|
|
// If this Command had subcommands, then don't dispatch the
|
|
// action. Instead, display a new list of commands for the user
|
|
// to pick from.
|
|
_nestedActionStack.Append(filteredCommand);
|
|
ParentCommandName(actionPaletteItem.Command().Name());
|
|
_updateCurrentNestedCommands(actionPaletteItem.Command());
|
|
|
|
_updateUIForStackChange();
|
|
}
|
|
else
|
|
{
|
|
// First stash the search text length, because _close will clear this.
|
|
const auto searchTextLength = _searchBox().Text().size();
|
|
|
|
// An action from the root command list has depth=0
|
|
const auto nestedCommandDepth = _nestedActionStack.Size();
|
|
|
|
// 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();
|
|
|
|
// A note: the command palette ignores
|
|
// "ToggleCommandPalette" actions. We may want to do the
|
|
// same with "Suggestions" actions in the future, should we
|
|
// ever allow non-sendInput actions.
|
|
DispatchCommandRequested.raise(*this, actionPaletteItem.Command());
|
|
|
|
TraceLoggingWrite(
|
|
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
|
|
"SuggestionsControlDispatchedAction",
|
|
TraceLoggingDescription("Event emitted when the user selects an action in the Command Palette"),
|
|
TraceLoggingUInt32(searchTextLength, "SearchTextLength", "Number of characters in the search string"),
|
|
TraceLoggingUInt32(nestedCommandDepth, "NestedCommandDepth", "the depth in the tree of commands for the dispatched action"),
|
|
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
|
|
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Method Description:
|
|
// - Get all the input text in _searchBox that follows any leading spaces.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - the string of input following any number of leading spaces
|
|
std::wstring SuggestionsControl::_getTrimmedInput()
|
|
{
|
|
const std::wstring input{ _searchBox().Text() };
|
|
if (input.empty())
|
|
{
|
|
return input;
|
|
}
|
|
|
|
// Trim leading whitespace
|
|
const auto firstNonSpace = input.find_first_not_of(L" ");
|
|
if (firstNonSpace == std::wstring::npos)
|
|
{
|
|
// All the following characters are whitespace.
|
|
return L"";
|
|
}
|
|
|
|
return input.substr(firstNonSpace);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper method for closing the command palette, when the user has _not_
|
|
// selected an action. Also fires a tracelogging event indicating that the
|
|
// user closed the palette without running a command.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_dismissPalette()
|
|
{
|
|
_close();
|
|
|
|
TraceLoggingWrite(
|
|
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
|
|
"SuggestionsControlDismissed",
|
|
TraceLoggingDescription("Event emitted when the user dismisses the Command Palette without selecting an action"),
|
|
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
|
|
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
|
|
}
|
|
|
|
// 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:
|
|
// - <unused>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_filterTextChanged(const IInspectable& /*sender*/,
|
|
const Windows::UI::Xaml::RoutedEventArgs& /*args*/)
|
|
{
|
|
// We're setting _lastFilterTextWasEmpty here, because if the user tries
|
|
// to backspace the last character in the input, the Backspace KeyDown
|
|
// event will fire _before_ _filterTextChanged does. Updating the value
|
|
// here will ensure that we can check this case appropriately.
|
|
_lastFilterTextWasEmpty = _searchBox().Text().empty();
|
|
|
|
const auto lastSelectedIndex = _filteredActionsView().SelectedIndex();
|
|
|
|
_updateFilteredActions();
|
|
|
|
// In the command line mode we want the user to explicitly select the command
|
|
_filteredActionsView().SelectedIndex(std::min<int32_t>(lastSelectedIndex, _filteredActionsView().Items().Size() - 1));
|
|
|
|
const auto currentNeedleHasResults{ _filteredActions.Size() > 0 };
|
|
_noMatchesText().Visibility(currentNeedleHasResults ? Visibility::Collapsed : Visibility::Visible);
|
|
if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(_searchBox()) })
|
|
{
|
|
automationPeer.RaiseNotificationEvent(
|
|
Automation::Peers::AutomationNotificationKind::ActionCompleted,
|
|
Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent,
|
|
currentNeedleHasResults ?
|
|
winrt::hstring{ RS_fmt(L"SuggestionsControl_MatchesAvailable", _filteredActions.Size()) } :
|
|
NoMatchesText(), // what to announce if results were found
|
|
L"SuggestionsControlResultAnnouncement" /* unique name for this group of notifications */);
|
|
}
|
|
}
|
|
|
|
Collections::IObservableVector<winrt::TerminalApp::FilteredCommand> SuggestionsControl::FilteredActions()
|
|
{
|
|
return _filteredActions;
|
|
}
|
|
|
|
void SuggestionsControl::SetCommands(const Collections::IVector<Command>& actions)
|
|
{
|
|
_allCommands.Clear();
|
|
for (const auto& action : actions)
|
|
{
|
|
// key chords aren't relevant in the suggestions control, so make the palette item with just the command and no keys
|
|
auto actionPaletteItem{ winrt::make<winrt::TerminalApp::implementation::ActionPaletteItem>(action, winrt::hstring{}) };
|
|
auto filteredCommand{ winrt::make<FilteredCommand>(actionPaletteItem) };
|
|
_allCommands.Append(filteredCommand);
|
|
}
|
|
|
|
if (Visibility() == Visibility::Visible)
|
|
{
|
|
_updateFilteredActions();
|
|
}
|
|
else // THIS BRANCH IS NEW
|
|
{
|
|
auto actions = _collectFilteredActions();
|
|
_filteredActions.Clear();
|
|
for (const auto& action : actions)
|
|
{
|
|
_filteredActions.Append(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SuggestionsControl::_switchToMode()
|
|
{
|
|
ParsedCommandLineText(L"");
|
|
_searchBox().Text(L"");
|
|
_searchBox().Select(_searchBox().Text().size(), 0);
|
|
|
|
_nestedActionStack.Clear();
|
|
ParentCommandName(L"");
|
|
_currentNestedCommands.Clear();
|
|
// Leaving this block of code outside the above if-statement
|
|
// guarantees that the correct text is shown for the mode
|
|
// whenever _switchToMode is called.
|
|
|
|
SearchBoxPlaceholderText(RS_(L"SuggestionsControl_SearchBox/PlaceholderText"));
|
|
NoMatchesText(RS_(L"SuggestionsControl_NoMatchesText/Text"));
|
|
ControlName(RS_(L"SuggestionsControlName"));
|
|
|
|
// 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.
|
|
_filteredActions.Clear();
|
|
_updateFilteredActions();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Produce a list of filtered actions to reflect the current contents of
|
|
// the input box.
|
|
// Arguments:
|
|
// - A collection that will receive the filtered actions
|
|
// Return Value:
|
|
// - <none>
|
|
std::vector<winrt::TerminalApp::FilteredCommand> SuggestionsControl::_collectFilteredActions()
|
|
{
|
|
std::vector<winrt::TerminalApp::FilteredCommand> actions;
|
|
|
|
winrt::hstring searchText{ _getTrimmedInput() };
|
|
|
|
auto commandsToFilter = _commandsToFilter();
|
|
|
|
{
|
|
for (const auto& action : commandsToFilter)
|
|
{
|
|
// Update filter for all commands
|
|
// This will modify the highlighting but will also lead to re-computation of weight (and consequently sorting).
|
|
// Pay attention that it already updates the highlighting in the UI
|
|
action.UpdateFilter(searchText);
|
|
|
|
// if there is active search we skip commands with 0 weight
|
|
if (searchText.empty() || action.Weight() > 0)
|
|
{
|
|
actions.push_back(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
// No sorting in palette mode, so results are still filtered, but in the
|
|
// original order. This feels more right for something like
|
|
// recentCommands.
|
|
//
|
|
// This is in contrast to the Command Palette, which always sorts its
|
|
// actions.
|
|
|
|
// Adjust the order of the results depending on if we're top-down or
|
|
// bottom up. This way, the "first" / "best" match is always closest to
|
|
// the cursor.
|
|
if (_direction == TerminalApp::SuggestionsDirection::BottomUp)
|
|
{
|
|
// Reverse the list
|
|
std::reverse(std::begin(actions), std::end(actions));
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update our list of filtered actions to reflect the current contents of
|
|
// the input box.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_updateFilteredActions()
|
|
{
|
|
auto actions = _collectFilteredActions();
|
|
|
|
// Make _filteredActions look identical to actions, using only Insert and Remove.
|
|
// This allows WinUI to nicely animate the ListView as it changes.
|
|
for (uint32_t i = 0; i < _filteredActions.Size() && i < actions.size(); i++)
|
|
{
|
|
for (auto j = i; j < _filteredActions.Size(); j++)
|
|
{
|
|
if (_filteredActions.GetAt(j).Item() == actions[i].Item())
|
|
{
|
|
for (auto k = i; k < j; k++)
|
|
{
|
|
_filteredActions.RemoveAt(i);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_filteredActions.GetAt(i).Item() != actions[i].Item())
|
|
{
|
|
_filteredActions.InsertAt(i, actions[i]);
|
|
}
|
|
}
|
|
|
|
// Remove any extra trailing items from the destination
|
|
while (_filteredActions.Size() > actions.size())
|
|
{
|
|
_filteredActions.RemoveAtEnd();
|
|
}
|
|
|
|
// Add any extra trailing items from the source
|
|
while (_filteredActions.Size() < actions.size())
|
|
{
|
|
_filteredActions.Append(actions[_filteredActions.Size()]);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update the list of current nested commands to match that of the
|
|
// given parent command.
|
|
// Arguments:
|
|
// - parentCommand: the command with an optional list of nested commands.
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_updateCurrentNestedCommands(const winrt::Microsoft::Terminal::Settings::Model::Command& parentCommand)
|
|
{
|
|
_currentNestedCommands.Clear();
|
|
for (const auto& nameAndCommand : parentCommand.NestedCommands())
|
|
{
|
|
const auto action = nameAndCommand.Value();
|
|
auto nestedActionPaletteItem{ winrt::make<winrt::TerminalApp::implementation::ActionPaletteItem>(action, winrt::hstring{}) };
|
|
auto nestedFilteredCommand{ winrt::make<FilteredCommand>(nestedActionPaletteItem) };
|
|
_currentNestedCommands.Append(nestedFilteredCommand);
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_close()
|
|
{
|
|
Visibility(Visibility::Collapsed);
|
|
|
|
// Clear the text box each time we close the dialog. This is consistent with VsCode.
|
|
_searchBox().Text(L"");
|
|
|
|
_nestedActionStack.Clear();
|
|
|
|
ParentCommandName(L"");
|
|
_currentNestedCommands.Clear();
|
|
|
|
PreviewAction.raise(*this, nullptr);
|
|
}
|
|
|
|
// Method Description:
|
|
// - This event is triggered when filteredActionView is looking for item container (ListViewItem)
|
|
// to use to present the filtered actions.
|
|
// GH#9288: unfortunately the default lookup seems to choose items with wrong data templates,
|
|
// e.g., using DataTemplate rendering actions for tab palette items.
|
|
// We handle this event by manually selecting an item from the cache.
|
|
// If no item is found we allocate a new one.
|
|
// Arguments:
|
|
// - args: the ChoosingItemContainerEventArgs allowing to get container candidate suggested by the
|
|
// system and replace it with another candidate if required.
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_choosingItemContainer(
|
|
const Windows::UI::Xaml::Controls::ListViewBase& /*sender*/,
|
|
const Windows::UI::Xaml::Controls::ChoosingItemContainerEventArgs& args)
|
|
{
|
|
const auto dataTemplate = _itemTemplateSelector.SelectTemplate(args.Item());
|
|
const auto itemContainer = args.ItemContainer();
|
|
if (itemContainer && itemContainer.ContentTemplate() == dataTemplate)
|
|
{
|
|
// If the suggested candidate is OK simply remove it from the cache
|
|
// (so we won't allocate it until it is released) and return
|
|
_listViewItemsCache[dataTemplate].erase(itemContainer);
|
|
}
|
|
else
|
|
{
|
|
// We need another candidate, let's look it up inside the cache
|
|
auto& containersByTemplate = _listViewItemsCache[dataTemplate];
|
|
if (!containersByTemplate.empty())
|
|
{
|
|
// There cache contains available items for required DataTemplate
|
|
// Let's return one of them (and remove it from the cache)
|
|
auto firstItem = containersByTemplate.begin();
|
|
args.ItemContainer(*firstItem);
|
|
containersByTemplate.erase(firstItem);
|
|
}
|
|
else
|
|
{
|
|
ElementFactoryGetArgs factoryArgs{};
|
|
const auto listViewItem = _listItemTemplate.GetElement(factoryArgs).try_as<Controls::ListViewItem>();
|
|
listViewItem.ContentTemplate(dataTemplate);
|
|
|
|
if (dataTemplate == _itemTemplateSelector.NestedItemTemplate())
|
|
{
|
|
const auto helpText = winrt::box_value(RS_(L"SuggestionsControl_MoreOptions/[using:Windows.UI.Xaml.Automation]AutomationProperties/HelpText"));
|
|
listViewItem.SetValue(Automation::AutomationProperties::HelpTextProperty(), helpText);
|
|
}
|
|
|
|
args.ItemContainer(listViewItem);
|
|
}
|
|
}
|
|
args.IsContainerPrepared(true);
|
|
}
|
|
// Method Description:
|
|
// - This event is triggered when the data item associate with filteredActionView list item is changing.
|
|
// We check if the item is being recycled. In this case we return it to the cache
|
|
// Arguments:
|
|
// - args: the ContainerContentChangingEventArgs describing the container change
|
|
// Return Value:
|
|
// - <none>
|
|
void SuggestionsControl::_containerContentChanging(
|
|
const Windows::UI::Xaml::Controls::ListViewBase& /*sender*/,
|
|
const Windows::UI::Xaml::Controls::ContainerContentChangingEventArgs& args)
|
|
{
|
|
const auto itemContainer = args.ItemContainer();
|
|
if (args.InRecycleQueue() && itemContainer && itemContainer.ContentTemplate())
|
|
{
|
|
_listViewItemsCache[itemContainer.ContentTemplate()].insert(itemContainer);
|
|
itemContainer.DataContext(nullptr);
|
|
}
|
|
else
|
|
{
|
|
itemContainer.DataContext(args.Item());
|
|
}
|
|
}
|
|
|
|
void SuggestionsControl::_setDirection(TerminalApp::SuggestionsDirection direction)
|
|
{
|
|
_direction = direction;
|
|
|
|
// We need to move either the list of suggestions, or the tooltip, to
|
|
// the top of the stack panel (depending on the layout).
|
|
Grid controlToMoveToTop = nullptr;
|
|
|
|
if (_direction == TerminalApp::SuggestionsDirection::TopDown)
|
|
{
|
|
Controls::Grid::SetRow(_searchBox(), 0);
|
|
controlToMoveToTop = _backdrop();
|
|
}
|
|
else // BottomUp
|
|
{
|
|
Controls::Grid::SetRow(_searchBox(), 4);
|
|
controlToMoveToTop = _descriptionsBackdrop();
|
|
}
|
|
|
|
assert(controlToMoveToTop);
|
|
const auto& children{ _listAndDescriptionStack().Children() };
|
|
uint32_t index;
|
|
if (children.IndexOf(controlToMoveToTop, index))
|
|
{
|
|
children.Move(index, 0);
|
|
}
|
|
}
|
|
|
|
void SuggestionsControl::_recalculateTopMargin()
|
|
{
|
|
auto currentMargin = Margin();
|
|
// Call Measure() on the descriptions backdrop, so that it gets it's new
|
|
// DesiredSize for this new description text.
|
|
//
|
|
// If you forget this, then we _probably_ weren't laid out since
|
|
// updating that text, and the ActualHeight will be the _last_
|
|
// description's height.
|
|
_descriptionsBackdrop().Measure({
|
|
static_cast<float>(ActualWidth()),
|
|
static_cast<float>(ActualHeight()),
|
|
});
|
|
|
|
// Now, position vertically.
|
|
if (_direction == TerminalApp::SuggestionsDirection::TopDown)
|
|
{
|
|
// The control should open right below the cursor, with the list
|
|
// extending below. This is easy, we can just use the cursor as the
|
|
// origin (more or less)
|
|
currentMargin.Top = (_anchor.Y);
|
|
}
|
|
else
|
|
{
|
|
// Bottom Up.
|
|
|
|
// This is wackier, because we need to calculate the offset upwards
|
|
// from our anchor. So we need to get the size of our elements:
|
|
const auto backdropHeight = _backdrop().ActualHeight();
|
|
const auto descriptionDesiredHeight = _descriptionsBackdrop().Visibility() == Visibility::Visible ?
|
|
_descriptionsBackdrop().DesiredSize().Height :
|
|
0;
|
|
|
|
const auto marginTop = (_anchor.Y - backdropHeight - descriptionDesiredHeight);
|
|
|
|
currentMargin.Top = marginTop;
|
|
}
|
|
Margin(currentMargin);
|
|
}
|
|
|
|
void SuggestionsControl::Open(TerminalApp::SuggestionsMode mode,
|
|
const Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command>& commands,
|
|
winrt::hstring filter,
|
|
Windows::Foundation::Point anchor,
|
|
Windows::Foundation::Size space,
|
|
float characterHeight)
|
|
{
|
|
Mode(mode);
|
|
SetCommands(commands);
|
|
|
|
// LOAD BEARING
|
|
// The control must become visible here, BEFORE we try to get its ActualWidth/Height.
|
|
Visibility(commands.Size() > 0 ? Visibility::Visible : Visibility::Collapsed);
|
|
|
|
_anchor = anchor;
|
|
_space = space;
|
|
|
|
// Is there space in the window below the cursor to open the menu downwards?
|
|
const bool canOpenDownwards = (_anchor.Y + characterHeight + ActualHeight()) < space.Height;
|
|
_setDirection(canOpenDownwards ? TerminalApp::SuggestionsDirection::TopDown :
|
|
TerminalApp::SuggestionsDirection::BottomUp);
|
|
// Set the anchor below by a character height
|
|
_anchor.Y += canOpenDownwards ? characterHeight : 0;
|
|
|
|
// First, position horizontally.
|
|
//
|
|
// We want to align the left edge of the text within the control to the
|
|
// cursor position. We'll need to scoot a little to the left, to align
|
|
// text with cursor
|
|
const auto proposedX = gsl::narrow_cast<int>(_anchor.X - 40);
|
|
// If the control is too wide to fit in the window, clamp it fit inside
|
|
// the window.
|
|
const auto maxX = gsl::narrow_cast<int>(space.Width - ActualWidth());
|
|
const auto clampedX = std::clamp(proposedX, 0, maxX);
|
|
|
|
// Create a thickness for the new margins. This will set the left, then
|
|
// we'll go update the top separately
|
|
Margin(Windows::UI::Xaml::ThicknessHelper::FromLengths(clampedX, 0, 0, 0));
|
|
_recalculateTopMargin();
|
|
|
|
_searchBox().Text(filter);
|
|
|
|
// If we're in bottom-up mode, make sure to re-select the _last_ item in
|
|
// the list, so that it's like we're starting with the most recent one
|
|
// selected.
|
|
if (_direction == TerminalApp::SuggestionsDirection::BottomUp)
|
|
{
|
|
const auto last = _filteredActionsView().Items().Size() - 1;
|
|
_filteredActionsView().SelectedIndex(last);
|
|
}
|
|
// Move the cursor to the very last position, so it starts immediately
|
|
// after the text. This is apparently done by starting a 0-wide
|
|
// selection starting at the end of the string.
|
|
_searchBox().Select(filter.size(), 0);
|
|
}
|
|
}
|