Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a snippets pane #17330

Merged
merged 30 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0ec5aef
Squashed commit of the following:
zadjii-msft May 2, 2024
43d46db
hey it runs again
zadjii-msft May 2, 2024
7b8b60b
resources to localize
zadjii-msft May 3, 2024
32234ca
nits for polish
zadjii-msft May 24, 2024
2dc16e4
the cool thing with the hovering
zadjii-msft May 24, 2024
57930ad
Merge remote-tracking branch 'origin/main' into dev/migrie/f/snippets…
zadjii-msft May 28, 2024
3ac5414
last nits before review
zadjii-msft May 28, 2024
e923513
spel
zadjii-msft May 28, 2024
5fd1100
Rename this, so the code doesn't reflect the old name
zadjii-msft May 28, 2024
eed0b02
Merge remote-tracking branch 'origin/main' into dev/migrie/f/snippets…
zadjii-msft Jun 6, 2024
bef9a2e
add padding
zadjii-msft Jun 6, 2024
be590b4
focus the control
zadjii-msft Jun 7, 2024
776164a
Don't let focus get lost into the pane
zadjii-msft Jun 10, 2024
825971c
visualize escape characters
zadjii-msft Jun 10, 2024
8d8590d
add warning when empty
zadjii-msft Jun 10, 2024
def4190
Try to do the DataTemplate thing, and fail.
zadjii-msft Jun 10, 2024
2a9e02e
Revert "Try to do the DataTemplate thing, and fail."
zadjii-msft Jun 10, 2024
31747e4
oh its so bodgy
zadjii-msft Jun 10, 2024
a9293b5
fix the brush
zadjii-msft Jun 10, 2024
d50b05e
Merge remote-tracking branch 'origin/main' into dev/migrie/f/snippets…
zadjii-msft Jun 11, 2024
b109582
the most minor of nits
zadjii-msft Jun 11, 2024
6e3fd24
a11y issues
zadjii-msft Jun 11, 2024
3e9b596
oh yea nice tapped events and keypresses
zadjii-msft Jun 11, 2024
e54c1dd
spel
zadjii-msft Jun 11, 2024
df58d96
only one snippets pane
zadjii-msft Jun 11, 2024
25aff99
Merge remote-tracking branch 'origin/main' into dev/migrie/f/snippets…
zadjii-msft Jun 12, 2024
1a08ad4
last nits
zadjii-msft Jun 12, 2024
313535e
Merge remote-tracking branch 'origin/main' into dev/migrie/f/snippets…
zadjii-msft Jul 1, 2024
5ec555a
Composing's probably actually easier than... whatever that abominatio…
zadjii-msft Jul 1, 2024
a50ffbf
Merge remote-tracking branch 'origin/main' into dev/migrie/f/snippets…
zadjii-msft Jul 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
{
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 @@ -2943,7 +2943,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
26 changes: 26 additions & 0 deletions src/cascadia/TerminalApp/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -898,4 +898,30 @@
<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="SnippetPaneAnnouncement" xml:space="preserve">
<value>Snippets pane</value>
<comment>Announced to screen readers when the user focuses the snippets pane</comment>
</data>
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
<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>
</root>
205 changes: 205 additions & 0 deletions src/cascadia/TerminalApp/SnippetsPaneContent.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// 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();
}

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)
{
t.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(L""); // IVector<Model::Command>
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
_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 { 1, 1 };
}
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
void SnippetsPaneContent::Focus(winrt::Windows::UI::Xaml::FocusState reason)
{
_filterBox().Focus(reason);

if (auto automationPeer{ WUX::Automation::Peers::FrameworkElementAutomationPeer::FromElement(_filterBox()) })
{
automationPeer.RaiseNotificationEvent(
WUX::Automation::Peers::AutomationNotificationKind::ActionCompleted,
WUX::Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, // CurrentThenMostRecent,
RS_(L"SnippetPaneAnnouncement"),
L"SnippetPaneFocused" /* unique ID for this notification */);
}
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading