Skip to content

Commit

Permalink
Add a snippets pane (#17330)
Browse files Browse the repository at this point in the history
This adds a snippets pane, which can be a static pane with all your
snippets (`sendInput` actions) in it. (See #17329)

This pane has a treeview with these actions in it, that we can filter
with a textbox at the top.

Play buttons next to entries make it quick to run the command you found.

Bound in the default actions with

```json
        { "command": { "action": "splitPane", "type": "snippets" }, "id": "Terminal.OpenSnippetsPane", "name": { "key": "SnippetsPaneCommandName" } },
```

re: #1595

----

TODO, from 06-04 bug bash

* [x] Snippets pane doesn't display some "no snippets found" text if
there aren't any yet
* [x] open snippets pane; find a "send input"; click the play button on
it; input is sent to active pane; begin typing
* [x] I can open an infinite amount of suggestions panes
* ~I'm closing this as by-design for now at least. Nothing stopping
anyone from opening infinite of any kind of pane.~
* ~This would require kind of a lot of refactoring in this PR to mark a
kind of pane as being a singleton or singleton-per-tab~
  * Okay everyone hates infinite suggestions panes, so I got rid of that
* [x] Ctrl+Shift+W should still work in the snippets pane even if focus
isn't in textbox
* [ ] open snippets pane; click on text box; press TAB key;
  * [ ] If you press TAB again, I have no idea where focus went
* [x] some previews don't work. Like `^c` (`"input": "\u0003"`)
* [x] nested items just give you a bit of extra space for no reason and
it looks a little awkward
* [x] UI Suggestion: add padding on the right side
* [ ] [Accessibility] Narrator says "Clear buffer; Suggestions found
132" when you open the snippets pane
- Note: this is probably Narrator reading out the command palette (since
that's where I opened it from)
- We should probably expect something like "Snippets", then (assuming
focus is thrown into text box) "Type to filter snippets" or something
like that
  • Loading branch information
zadjii-msft authored Jul 8, 2024
1 parent ec92892 commit 02a7c02
Show file tree
Hide file tree
Showing 15 changed files with 756 additions and 18 deletions.
19 changes: 15 additions & 4 deletions src/cascadia/TerminalApp/FilteredCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,22 @@ namespace winrt::TerminalApp::implementation
{
// This class is a wrapper of PaletteItem, that is used as an item of a filterable list in CommandPalette.
// It manages a highlighted text that is computed by matching search filter characters to item name
FilteredCommand::FilteredCommand(const winrt::TerminalApp::PaletteItem& item) :
_Item(item),
_Filter(L""),
_Weight(0)
FilteredCommand::FilteredCommand(const winrt::TerminalApp::PaletteItem& item)
{
// Actually implement the ctor in _constructFilteredCommand
_constructFilteredCommand(item);
}

// We need to actually implement the ctor in a separate helper. This is
// because we have a FilteredTask class which derives from FilteredCommand.
// HOWEVER, for cppwinrt ~ r e a s o n s ~, it doesn't actually derive from
// FilteredCommand directly, so we can't just use the FilteredCommand ctor
// directly in the base class.
void FilteredCommand::_constructFilteredCommand(const winrt::TerminalApp::PaletteItem& item)
{
_Item = item;
_Filter = L"";
_Weight = 0;
_HighlightedName = _computeHighlightedName();

// Recompute the highlighted name if the item name changes
Expand Down
5 changes: 4 additions & 1 deletion src/cascadia/TerminalApp/FilteredCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace winrt::TerminalApp::implementation
FilteredCommand() = default;
FilteredCommand(const winrt::TerminalApp::PaletteItem& item);

void UpdateFilter(const winrt::hstring& filter);
virtual void UpdateFilter(const winrt::hstring& filter);

static int Compare(const winrt::TerminalApp::FilteredCommand& first, const winrt::TerminalApp::FilteredCommand& second);

Expand All @@ -29,6 +29,9 @@ namespace winrt::TerminalApp::implementation
WINRT_OBSERVABLE_PROPERTY(winrt::TerminalApp::HighlightedText, HighlightedName, PropertyChanged.raise);
WINRT_OBSERVABLE_PROPERTY(int, Weight, PropertyChanged.raise);

protected:
void _constructFilteredCommand(const winrt::TerminalApp::PaletteItem& item);

private:
winrt::TerminalApp::HighlightedText _computeHighlightedName();
int _computeWeight();
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalApp/FilteredCommand.idl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "HighlightedTextControl.idl";

namespace TerminalApp
{
[default_interface] runtimeclass FilteredCommand : Windows.UI.Xaml.Data.INotifyPropertyChanged
[default_interface] unsealed runtimeclass FilteredCommand : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
FilteredCommand();
FilteredCommand(PaletteItem item);
Expand Down
3 changes: 2 additions & 1 deletion src/cascadia/TerminalApp/Pane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2944,7 +2944,8 @@ void Pane::FinalizeConfigurationGivenDefault()
// - Returns true if the pane or one of its descendants is read-only
bool Pane::ContainsReadOnly() const
{
return _IsLeaf() ? _content.ReadOnly() : (_firstChild->ContainsReadOnly() || _secondChild->ContainsReadOnly());
return _IsLeaf() ? (_content == nullptr ? false : _content.ReadOnly()) :
(_firstChild->ContainsReadOnly() || _secondChild->ContainsReadOnly());
}

// Method Description:
Expand Down
22 changes: 22 additions & 0 deletions src/cascadia/TerminalApp/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,28 @@
<data name="RestartConnectionToolTip" xml:space="preserve">
<value>Restart the active pane connection</value>
</data>
<data name="SnippetPaneTitle.Text" xml:space="preserve">
<value>Snippets</value>
<comment>Header for a page that includes small "snippets" of text for the user to enter</comment>
</data>
<data name="SnippetsFilterBox.PlaceholderText" xml:space="preserve">
<value>Filter snippets...</value>
<comment>Placeholder text for a text box to filter snippets (on the same page as the 'SnippetPaneTitle')</comment>
</data>
<data name="SnippetPlayButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Input this command</value>
</data>
<data name="SnippetPlayButton.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Input this command</value>
</data>
<data name="SnippetsPaneNoneFoundHeader.Text" xml:space="preserve">
<value>No snippets found in your settings.</value>
<comment>Text shown to user when no snippets are found in their settings</comment>
</data>
<data name="SnippetsPaneNoneFoundDetails.Text" xml:space="preserve">
<value>Add some "Send input" actions in your settings to have them show up here.</value>
<comment>Additional information presented to the user to let them know they can add a "Send input" action in their setting to populate the snippets pane</comment>
</data>
<data name="ActionSavedToast.Title" xml:space="preserve">
<value>Action saved</value>
</data>
Expand Down
199 changes: 199 additions & 0 deletions src/cascadia/TerminalApp/SnippetsPaneContent.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "pch.h"
#include "SnippetsPaneContent.h"
#include "SnippetsPaneContent.g.cpp"
#include "FilteredTask.g.cpp"
#include "Utils.h"

using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::System;
using namespace winrt::Microsoft::Terminal::Settings;
using namespace winrt::Microsoft::Terminal::Settings::Model;

namespace winrt
{
namespace WUX = Windows::UI::Xaml;
namespace MUX = Microsoft::UI::Xaml;
using IInspectable = Windows::Foundation::IInspectable;
}

namespace winrt::TerminalApp::implementation
{
SnippetsPaneContent::SnippetsPaneContent()
{
InitializeComponent();

WUX::Automation::AutomationProperties::SetName(*this, RS_(L"SnippetPaneTitle/Text"));
}

void SnippetsPaneContent::_updateFilteredCommands()
{
const auto& queryString = _filterBox().Text();

// DON'T replace the itemSource here. If you do, it'll un-expand all the
// nested items the user has expanded. Instead, just update the filter.
// That'll also trigger a PropertyChanged for the Visibility property.
for (const auto& t : _allTasks)
{
auto impl = winrt::get_self<implementation::FilteredTask>(t);
impl->UpdateFilter(queryString);
}
}

void SnippetsPaneContent::UpdateSettings(const CascadiaSettings& settings)
{
_settings = settings;

// You'd think that `FilterToSendInput(queryString)` would work. It
// doesn't! That uses the queryString as the current command the user
// has typed, then relies on the suggestions UI to _also_ filter with that
// string.

const auto tasks = _settings.GlobalSettings().ActionMap().FilterToSendInput(winrt::hstring{}); // IVector<Model::Command>
_allTasks = winrt::single_threaded_observable_vector<TerminalApp::FilteredTask>();
for (const auto& t : tasks)
{
const auto& filtered{ winrt::make<FilteredTask>(t) };
_allTasks.Append(filtered);
}
_treeView().ItemsSource(_allTasks);

_updateFilteredCommands();

PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"HasSnippets" });
}

bool SnippetsPaneContent::HasSnippets() const
{
return _allTasks.Size() != 0;
}

void SnippetsPaneContent::_filterTextChanged(const IInspectable& /*sender*/,
const Windows::UI::Xaml::RoutedEventArgs& /*args*/)
{
_updateFilteredCommands();
}

winrt::Windows::UI::Xaml::FrameworkElement SnippetsPaneContent::GetRoot()
{
return *this;
}
winrt::Windows::Foundation::Size SnippetsPaneContent::MinimumSize()
{
return { 200, 200 };
}
void SnippetsPaneContent::Focus(winrt::Windows::UI::Xaml::FocusState reason)
{
_filterBox().Focus(reason);
}
void SnippetsPaneContent::Close()
{
CloseRequested.raise(*this, nullptr);
}

INewContentArgs SnippetsPaneContent::GetNewTerminalArgs(BuildStartupKind /*kind*/) const
{
return BaseContentArgs(L"snippets");
}

winrt::hstring SnippetsPaneContent::Icon() const
{
static constexpr std::wstring_view glyph{ L"\xe70b" }; // QuickNote
return winrt::hstring{ glyph };
}

winrt::WUX::Media::Brush SnippetsPaneContent::BackgroundBrush()
{
static const auto key = winrt::box_value(L"UnfocusedBorderBrush");
return ThemeLookup(WUX::Application::Current().Resources(),
_settings.GlobalSettings().CurrentTheme().RequestedTheme(),
key)
.try_as<winrt::WUX::Media::Brush>();
}

void SnippetsPaneContent::SetLastActiveControl(const Microsoft::Terminal::Control::TermControl& control)
{
_control = control;
}

void SnippetsPaneContent::_runCommand(const Microsoft::Terminal::Settings::Model::Command& command)
{
if (const auto& strongControl{ _control.get() })
{
// By using the last active control as the sender here, the
// action dispatch will send this to the active control,
// thinking that it is the control that requested this event.
strongControl.Focus(winrt::WUX::FocusState::Programmatic);
DispatchCommandRequested.raise(strongControl, command);
}
}

void SnippetsPaneContent::_runCommandButtonClicked(const Windows::Foundation::IInspectable& sender,
const Windows::UI::Xaml::RoutedEventArgs&)
{
if (const auto& taskVM{ sender.try_as<WUX::Controls::Button>().DataContext().try_as<FilteredTask>() })
{
_runCommand(taskVM->Command());
}
}

// Called when one of the items in the list is tapped, or enter/space is
// pressed on it while focused. Notably, this isn't the Tapped event - it
// isn't called when the user clicks the dropdown arrow (that does usually
// also trigger a Tapped).
//
// We'll use this to toggle the expanded state of nested items, since the
// tree view arrow is so little
void SnippetsPaneContent::_treeItemInvokedHandler(const IInspectable& /*sender*/,
const MUX::Controls::TreeViewItemInvokedEventArgs& e)
{
// The InvokedItem here is the item in the data collection that was
// bound itself.
if (const auto& taskVM{ e.InvokedItem().try_as<FilteredTask>() })
{
if (taskVM->HasChildren())
{
// We then need to find the actual TreeViewItem for that
// FilteredTask.
if (const auto& item{ _treeView().ContainerFromItem(*taskVM).try_as<MUX::Controls::TreeViewItem>() })
{
item.IsExpanded(!item.IsExpanded());
}
}
}
}

// Raised on individual TreeViewItems. We'll use this event to send the
// input on an Enter/Space keypress, when a leaf item is selected.
void SnippetsPaneContent::_treeItemKeyUpHandler(const IInspectable& sender,
const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e)
{
const auto& item{ sender.try_as<MUX::Controls::TreeViewItem>() };
if (!item)
{
return;
}
const auto& taskVM{ item.DataContext().try_as<FilteredTask>() };
if (!taskVM || taskVM->HasChildren())
{
return;
}

const auto& key = e.OriginalKey();
if (key == VirtualKey::Enter || key == VirtualKey::Space)
{
if (const auto& button = e.OriginalSource().try_as<WUX::Controls::Button>())
{
// Let the button handle the Enter key so an eventually attached click handler will be called
e.Handled(false);
return;
}

_runCommand(taskVM->Command());
e.Handled(true);
}
}

}
Loading

0 comments on commit 02a7c02

Please sign in to comment.