Swap the command palette modes for the prefix > (#7935)

VsCode uses `>` as its "prefix" for the equivalent of their "action
mode". This PR aligns the Terminal with their logic here. 

We have to be tricky - if we use the `>` in the actual input as the
indicator for action mode, we can't display any placeholder text in the
input to tell users to type a command. This wasn't an issue for the
commandline mode previously, because we'd stick the "prompt" in the "no
matches text" space. However, we can't do that for action mode. Instead,
we'll stick a floating text block over the input box, and when the
user's in action mode, we'll manually place a `>` into that space. When
the user backspaces the `>`, we'll remove it from that block, and switch
into commandline mode.

## Validation Steps Performed
Played with the cmdpal in lots of different modes, this finally feels
good

Closes #7736
This commit is contained in:
Mike Griese 2020-10-15 17:58:35 -05:00 committed by GitHub
parent 30e363e7ac
commit bd7cd5512d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 114 additions and 96 deletions

View File

@ -213,41 +213,27 @@ namespace winrt::TerminalApp::implementation
} }
else if (key == VirtualKey::Escape) else if (key == VirtualKey::Escape)
{ {
// Action, TabSearch, TabSwitch Mode: Dismiss the palette if the // Dismiss the palette if the text is empty, otherwise clear the
// text is empty, otherwise clear the search string. // search string.
if (_currentMode != CommandPaletteMode::CommandlineMode) if (_searchBox().Text().empty())
{ {
if (_searchBox().Text().empty()) _dismissPalette();
{
_dismissPalette();
}
else
{
_searchBox().Text(L"");
}
} }
else if (_currentMode == CommandPaletteMode::CommandlineMode) else
{ {
const auto currentInput = _getPostPrefixInput(); _searchBox().Text(L"");
if (currentInput.empty()) }
{
// The user's only input "> " so far. We should just dismiss
// the palette. This is like dismissing the Action mode with
// empty input.
_dismissPalette();
}
else
{
// Clear out the current input. We'll leave a ">" in the
// input (to stay in commandline mode), and a leading space
// (if they currently had one).
const bool hasLeadingSpace = (_searchBox().Text().size()) - (currentInput.size()) > 1;
_searchBox().Text(hasLeadingSpace ? L"> " : L">");
// This will conveniently move the cursor to the end of the e.Handled(true);
// text input for us. }
_searchBox().Select(_searchBox().Text().size(), 0); else if (key == VirtualKey::Back)
} {
// If the last filter text was empty, and we're backspacing from
// that state, then the user "backspaced" the virtual '>' we're
// using as the action mode indicator. Switch into commandline mode.
if (_searchBox().Text().empty() && _lastFilterTextWasEmpty && _currentMode == CommandPaletteMode::ActionMode)
{
_switchToMode(CommandPaletteMode::CommandlineMode);
} }
e.Handled(true); e.Handled(true);
@ -480,18 +466,13 @@ namespace winrt::TerminalApp::implementation
} }
} }
} }
// Method Description: // Method Description:
// - Get all the input text in _searchBox that follows the prefix character // - Get all the input text in _searchBox that follows any leading spaces.
// and any whitespace following that prefix character. This can be used in
// commandline mode to get all the useful input that the user input after
// the leading ">" prefix.
// - Note that this will behave unexpectedly in Action Mode.
// Arguments: // Arguments:
// - <none> // - <none>
// Return Value: // Return Value:
// - the string of input following the prefix character. // - the string of input following any number of leading spaces
std::wstring CommandPalette::_getPostPrefixInput() std::wstring CommandPalette::_getTrimmedInput()
{ {
const std::wstring input{ _searchBox().Text() }; const std::wstring input{ _searchBox().Text() };
if (input.empty()) if (input.empty())
@ -499,17 +480,15 @@ namespace winrt::TerminalApp::implementation
return input; return input;
} }
const auto rawCmdline{ input.substr(1) };
// Trim leading whitespace // Trim leading whitespace
const auto firstNonSpace = rawCmdline.find_first_not_of(L" "); const auto firstNonSpace = input.find_first_not_of(L" ");
if (firstNonSpace == std::wstring::npos) if (firstNonSpace == std::wstring::npos)
{ {
// All the following characters are whitespace. // All the following characters are whitespace.
return L""; return L"";
} }
return rawCmdline.substr(firstNonSpace); return input.substr(firstNonSpace);
} }
// Method Description: // Method Description:
@ -520,12 +499,11 @@ namespace winrt::TerminalApp::implementation
// - <none> // - <none>
void CommandPalette::_dispatchCommandline() void CommandPalette::_dispatchCommandline()
{ {
const auto input = _getPostPrefixInput(); auto cmdline{ _getTrimmedInput() };
if (input.empty()) if (cmdline.empty())
{ {
return; return;
} }
winrt::hstring cmdline{ input };
// Build the ExecuteCommandline action from the values we've parsed on the commandline. // Build the ExecuteCommandline action from the values we've parsed on the commandline.
ExecuteCommandlineArgs args{ cmdline }; ExecuteCommandlineArgs args{ cmdline };
@ -574,32 +552,49 @@ namespace winrt::TerminalApp::implementation
void CommandPalette::_filterTextChanged(IInspectable const& /*sender*/, void CommandPalette::_filterTextChanged(IInspectable const& /*sender*/,
Windows::UI::Xaml::RoutedEventArgs const& /*args*/) Windows::UI::Xaml::RoutedEventArgs const& /*args*/)
{ {
if (_currentMode == CommandPaletteMode::CommandlineMode || _currentMode == CommandPaletteMode::ActionMode) if (_currentMode == CommandPaletteMode::CommandlineMode)
{ {
_evaluatePrefix(); _evaluatePrefix();
} }
// 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();
_updateFilteredActions(); _updateFilteredActions();
_filteredActionsView().SelectedIndex(0); _filteredActionsView().SelectedIndex(0);
_noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible); if (_currentMode == CommandPaletteMode::TabSearchMode || _currentMode == CommandPaletteMode::ActionMode)
{
_noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible);
}
else
{
_noMatchesText().Visibility(Visibility::Collapsed);
}
} }
void CommandPalette::_evaluatePrefix() void CommandPalette::_evaluatePrefix()
{ {
auto newMode = CommandPaletteMode::ActionMode; // This will take you from commandline mode, into action mode. The
// backspace handler in _keyDownHandler will handle taking us from
// action mode to commandline mode.
auto newMode = CommandPaletteMode::CommandlineMode;
auto inputText = _searchBox().Text(); auto inputText = _getTrimmedInput();
if (inputText.size() > 0) if (inputText.size() > 0)
{ {
if (inputText[0] == L'>') if (inputText[0] == L'>')
{ {
newMode = CommandPaletteMode::CommandlineMode; newMode = CommandPaletteMode::ActionMode;
} }
} }
if (newMode != _currentMode) if (newMode != _currentMode)
{ {
//_switchToMode will remove the '>' character from the input.
_switchToMode(newMode); _switchToMode(newMode);
} }
} }
@ -644,6 +639,8 @@ namespace winrt::TerminalApp::implementation
} }
} }
_searchBox().Text(L"");
_searchBox().Select(_searchBox().Text().size(), 0);
// Leaving this block of code outside the above if-statement // Leaving this block of code outside the above if-statement
// guarantees that the correct text is shown for the mode // guarantees that the correct text is shown for the mode
// whenever _switchToMode is called. // whenever _switchToMode is called.
@ -652,20 +649,24 @@ namespace winrt::TerminalApp::implementation
case CommandPaletteMode::TabSearchMode: case CommandPaletteMode::TabSearchMode:
case CommandPaletteMode::TabSwitchMode: case CommandPaletteMode::TabSwitchMode:
{ {
SearchBoxText(RS_(L"TabSwitcher_SearchBoxText")); SearchBoxPlaceholderText(RS_(L"TabSwitcher_SearchBoxText"));
NoMatchesText(RS_(L"TabSwitcher_NoMatchesText")); NoMatchesText(RS_(L"TabSwitcher_NoMatchesText"));
ControlName(RS_(L"TabSwitcherControlName")); ControlName(RS_(L"TabSwitcherControlName"));
PrefixCharacter(L"");
break; break;
} }
case CommandPaletteMode::CommandlineMode: case CommandPaletteMode::CommandlineMode:
NoMatchesText(RS_(L"CmdPalCommandlinePrompt")); SearchBoxPlaceholderText(RS_(L"CmdPalCommandlinePrompt"));
NoMatchesText(L"");
ControlName(RS_(L"CommandPaletteControlName")); ControlName(RS_(L"CommandPaletteControlName"));
PrefixCharacter(L"");
break; break;
case CommandPaletteMode::ActionMode: case CommandPaletteMode::ActionMode:
default: default:
SearchBoxText(RS_(L"CommandPalette_SearchBox/PlaceholderText")); SearchBoxPlaceholderText(RS_(L"CommandPalette_SearchBox/PlaceholderText"));
NoMatchesText(RS_(L"CommandPalette_NoMatchesText/Text")); NoMatchesText(RS_(L"CommandPalette_NoMatchesText/Text"));
ControlName(RS_(L"CommandPaletteControlName")); ControlName(RS_(L"CommandPaletteControlName"));
PrefixCharacter(L">");
break; break;
} }
} }
@ -717,7 +718,7 @@ namespace winrt::TerminalApp::implementation
{ {
std::vector<Command> actions; std::vector<Command> actions;
auto searchText = _searchBox().Text(); winrt::hstring searchText{ _getTrimmedInput() };
const bool addAll = searchText.empty(); const bool addAll = searchText.empty();
auto commandsToFilter = _commandsToFilter(); auto commandsToFilter = _commandsToFilter();

View File

@ -39,7 +39,8 @@ namespace winrt::TerminalApp::implementation
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, NoMatchesText, _PropertyChangedHandlers); OBSERVABLE_GETSET_PROPERTY(winrt::hstring, NoMatchesText, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, SearchBoxText, _PropertyChangedHandlers); OBSERVABLE_GETSET_PROPERTY(winrt::hstring, SearchBoxPlaceholderText, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, PrefixCharacter, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ControlName, _PropertyChangedHandlers); OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ControlName, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ParentCommandName, _PropertyChangedHandlers); OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ParentCommandName, _PropertyChangedHandlers);
@ -55,6 +56,8 @@ namespace winrt::TerminalApp::implementation
Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> _commandsToFilter(); Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> _commandsToFilter();
bool _lastFilterTextWasEmpty{ true };
void _filterTextChanged(Windows::Foundation::IInspectable const& sender, void _filterTextChanged(Windows::Foundation::IInspectable const& sender,
Windows::UI::Xaml::RoutedEventArgs const& args); Windows::UI::Xaml::RoutedEventArgs const& args);
void _previewKeyDownHandler(Windows::Foundation::IInspectable const& sender, void _previewKeyDownHandler(Windows::Foundation::IInspectable const& sender,
@ -84,8 +87,8 @@ namespace winrt::TerminalApp::implementation
CommandPaletteMode _currentMode; CommandPaletteMode _currentMode;
void _switchToMode(CommandPaletteMode mode); void _switchToMode(CommandPaletteMode mode);
std::wstring _getTrimmedInput();
void _evaluatePrefix(); void _evaluatePrefix();
std::wstring _getPostPrefixInput();
Microsoft::Terminal::TerminalControl::IKeyBindings _bindings; Microsoft::Terminal::TerminalControl::IKeyBindings _bindings;

View File

@ -11,7 +11,8 @@ namespace TerminalApp
CommandPalette(); CommandPalette();
String NoMatchesText { get; }; String NoMatchesText { get; };
String SearchBoxText { get; }; String SearchBoxPlaceholderText { get; };
String PrefixCharacter { get; };
String ControlName { get; }; String ControlName { get; };
String ParentCommandName { get; }; String ParentCommandName { get; };

View File

@ -8,8 +8,8 @@ the MIT License. See LICENSE in the project root for license information. -->
xmlns:mux="using:Microsoft.UI.Xaml.Controls" xmlns:mux="using:Microsoft.UI.Xaml.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Windows10version1903="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 8)" xmlns:Windows10version1903="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 8)"
xmlns:SettingsModel="using:Microsoft.Terminal.Settings.Model" xmlns:SettingsModel="using:Microsoft.Terminal.Settings.Model"
TabNavigation="Cycle" TabNavigation="Cycle"
IsTabStop="True" IsTabStop="True"
AllowFocusOnInteraction="True" AllowFocusOnInteraction="True"
@ -135,27 +135,27 @@ the MIT License. See LICENSE in the project root for license information. -->
to receive clicks _anywhere_ in its bounds. --> to receive clicks _anywhere_ in its bounds. -->
<Grid <Grid
x:Name="_shadowBackdrop" x:Name="_shadowBackdrop"
Background="Transparent" Background="Transparent"
Grid.Column="0" Grid.Column="0"
Grid.Row="0" Grid.Row="0"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Grid.RowSpan="2" Grid.RowSpan="2"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
</Grid> </Grid>
<Grid <Grid
x:Name="_backdrop" x:Name="_backdrop"
Style="{ThemeResource CommandPaletteBackground}" Style="{ThemeResource CommandPaletteBackground}"
CornerRadius="{ThemeResource ControlCornerRadius}" CornerRadius="{ThemeResource ControlCornerRadius}"
PointerPressed="_backdropPointerPressed" PointerPressed="_backdropPointerPressed"
Margin="8" Margin="8"
Grid.Column="1" Grid.Column="1"
Grid.Row="0" Grid.Row="0"
Windows10version1903:Shadow="{StaticResource CommandPaletteShadow}" Windows10version1903:Shadow="{StaticResource CommandPaletteShadow}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Top"> VerticalAlignment="Top">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
@ -164,15 +164,28 @@ the MIT License. See LICENSE in the project root for license information. -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBox <TextBox
Grid.Row="0" Grid.Row="0"
x:Name="_searchBox" x:Name="_searchBox"
Margin="8" Margin="8"
IsSpellCheckEnabled="False" Padding="18,8,8,8"
TextChanged="_filterTextChanged" IsSpellCheckEnabled="False"
PlaceholderText="{x:Bind SearchBoxText, Mode=OneWay}" TextChanged="_filterTextChanged"
Text=""> PlaceholderText="{x:Bind SearchBoxPlaceholderText, Mode=OneWay}"
Text="">
</TextBox> </TextBox>
<TextBlock
Grid.Row="0"
x:Name="_prefixCharacter"
Margin="16,16,0,-8"
FontSize="14"
Visibility="{x:Bind PrefixCharacter,
Mode=OneWay,
Converter={StaticResource ParentCommandVisibilityConverter}}"
Text="{x:Bind PrefixCharacter, Mode=OneWay}"
>
</TextBlock>
<TextBlock <TextBlock
Padding="16, 0, 16, 4" Padding="16, 0, 16, 4"
x:Name="_parentCommandText" x:Name="_parentCommandText"
@ -194,17 +207,17 @@ the MIT License. See LICENSE in the project root for license information. -->
</TextBlock> </TextBlock>
<ListView <ListView
Grid.Row="2" Grid.Row="2"
x:Name="_filteredActionsView" x:Name="_filteredActionsView"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
SelectionMode="Single" SelectionMode="Single"
CanReorderItems="False" CanReorderItems="False"
AllowDrop="False" AllowDrop="False"
IsItemClickEnabled="True" IsItemClickEnabled="True"
ItemClick="_listItemClicked" ItemClick="_listItemClicked"
PreviewKeyDown="_keyDownHandler" PreviewKeyDown="_keyDownHandler"
ItemsSource="{x:Bind FilteredActions}"> ItemsSource="{x:Bind FilteredActions}">
<ItemsControl.ItemTemplate > <ItemsControl.ItemTemplate >
<DataTemplate x:DataType="SettingsModel:Command"> <DataTemplate x:DataType="SettingsModel:Command">