Add a snippets pane (#17330)

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
This commit is contained in:
Mike Griese 2024-07-08 13:26:07 -05:00 committed by GitHub
parent ec9289288e
commit 02a7c02548
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 756 additions and 18 deletions

View File

@ -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

View File

@ -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);
@ -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();

View File

@ -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);

View File

@ -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:

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -0,0 +1,154 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "SnippetsPaneContent.g.h"
#include "FilteredTask.g.h"
#include "FilteredCommand.h"
#include "ActionPaletteItem.h"
#include <LibraryResources.h>
namespace winrt::TerminalApp::implementation
{
struct SnippetsPaneContent : SnippetsPaneContentT<SnippetsPaneContent>
{
SnippetsPaneContent();
winrt::Windows::UI::Xaml::FrameworkElement GetRoot();
void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings& settings);
winrt::Windows::Foundation::Size MinimumSize();
void Focus(winrt::Windows::UI::Xaml::FocusState reason = winrt::Windows::UI::Xaml::FocusState::Programmatic);
void Close();
winrt::Microsoft::Terminal::Settings::Model::INewContentArgs GetNewTerminalArgs(BuildStartupKind kind) const;
winrt::hstring Title() { return RS_(L"SnippetPaneTitle/Text"); }
uint64_t TaskbarState() { return 0; }
uint64_t TaskbarProgress() { return 0; }
bool ReadOnly() { return false; }
winrt::hstring Icon() const;
Windows::Foundation::IReference<winrt::Windows::UI::Color> TabColor() const noexcept { return nullptr; }
winrt::Windows::UI::Xaml::Media::Brush BackgroundBrush();
void SetLastActiveControl(const Microsoft::Terminal::Control::TermControl& control);
bool HasSnippets() const;
til::typed_event<> ConnectionStateChanged;
til::typed_event<IPaneContent> CloseRequested;
til::typed_event<IPaneContent, winrt::TerminalApp::BellEventArgs> BellRequested;
til::typed_event<IPaneContent> TitleChanged;
til::typed_event<IPaneContent> TabColorChanged;
til::typed_event<IPaneContent> TaskbarProgressChanged;
til::typed_event<IPaneContent> ReadOnlyChanged;
til::typed_event<IPaneContent> FocusRequested;
til::typed_event<winrt::Windows::Foundation::IInspectable, Microsoft::Terminal::Settings::Model::Command> DispatchCommandRequested;
til::property_changed_event PropertyChanged;
private:
friend struct SnippetsPaneContentT<SnippetsPaneContent>; // for Xaml to bind events
winrt::weak_ref<Microsoft::Terminal::Control::TermControl> _control{ nullptr };
winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr };
winrt::Windows::Foundation::Collections::IObservableVector<TerminalApp::FilteredTask> _allTasks{ nullptr };
void _runCommandButtonClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs&);
void _filterTextChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
void _updateFilteredCommands();
void _treeItemInvokedHandler(const Windows::Foundation::IInspectable& sender, const Microsoft::UI::Xaml::Controls::TreeViewItemInvokedEventArgs& e);
void _treeItemKeyUpHandler(const IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void _runCommand(const Microsoft::Terminal::Settings::Model::Command& command);
};
struct FilteredTask : FilteredTaskT<FilteredTask>
{
FilteredTask() = default;
FilteredTask(const winrt::Microsoft::Terminal::Settings::Model::Command& command)
{
_filteredCommand = winrt::make_self<implementation::FilteredCommand>(winrt::make<winrt::TerminalApp::implementation::ActionPaletteItem>(command, winrt::hstring{}));
_command = command;
// The Children() method must always return a non-null vector
_children = winrt::single_threaded_observable_vector<TerminalApp::FilteredTask>();
if (_command.HasNestedCommands())
{
for (const auto& [_, child] : _command.NestedCommands())
{
auto vm{ winrt::make<FilteredTask>(child) };
_children.Append(vm);
}
}
}
void UpdateFilter(const winrt::hstring& filter)
{
_filteredCommand->UpdateFilter(filter);
for (const auto& c : _children)
{
auto impl = winrt::get_self<implementation::FilteredTask>(c);
impl->UpdateFilter(filter);
}
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"Visibility" });
}
winrt::hstring Input()
{
if (const auto& actionItem{ _filteredCommand->Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
{
if (const auto& command{ actionItem.Command() })
{
if (const auto& sendInput{ command.ActionAndArgs().Args().try_as<winrt::Microsoft::Terminal::Settings::Model::SendInputArgs>() })
{
return winrt::hstring{ til::visualize_nonspace_control_codes(sendInput.Input().c_str()) };
}
}
}
return winrt::hstring{};
};
winrt::Windows::Foundation::Collections::IObservableVector<TerminalApp::FilteredTask> Children() { return _children; }
bool HasChildren() { return _children.Size() > 0; }
winrt::Microsoft::Terminal::Settings::Model::Command Command() { return _command; }
winrt::TerminalApp::FilteredCommand FilteredCommand() { return *_filteredCommand; }
int32_t Row() { return HasChildren() ? 2 : 1; } // See the BODGY comment in the .XAML for explanation
// Used to control if this item is visible in the TreeView. Turns out,
// TreeView is in fact sane enough to remove items entirely if they're
// Collapsed.
winrt::Windows::UI::Xaml::Visibility Visibility()
{
// Is there no filter, or do we match it?
if (_filteredCommand->Filter().empty() || _filteredCommand->Weight() > 0)
{
return winrt::Windows::UI::Xaml::Visibility::Visible;
}
// If we don't match, maybe one of our children does
auto totalWeight = _filteredCommand->Weight();
for (const auto& c : _children)
{
auto impl = winrt::get_self<implementation::FilteredTask>(c);
totalWeight += impl->_filteredCommand->Weight();
}
return totalWeight > 0 ? winrt::Windows::UI::Xaml::Visibility::Visible : winrt::Windows::UI::Xaml::Visibility::Collapsed;
};
til::property_changed_event PropertyChanged;
private:
winrt::Microsoft::Terminal::Settings::Model::Command _command{ nullptr };
winrt::com_ptr<winrt::TerminalApp::implementation::FilteredCommand> _filteredCommand{ nullptr };
winrt::Windows::Foundation::Collections::IObservableVector<TerminalApp::FilteredTask> _children{ nullptr };
};
}
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(SnippetsPaneContent);
}

View File

@ -0,0 +1,261 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
-->
<UserControl x:Class="TerminalApp.SnippetsPaneContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:SettingsModel="using:Microsoft.Terminal.Settings.Model"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:Microsoft.Terminal.Settings.Model"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
AutomationProperties.LocalizedControlType="pane"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<Style x:Key="PlayButtonTemplate"
TargetType="Button">
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="4" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="ButtonBaseElement"
Padding="{TemplateBinding Padding}"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Viewbox Width="14"
Height="14">
<Grid>
<FontIcon x:Name="ButtonBackgroundIcon"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Foreground="{ThemeResource PlayButtonHoveredColor}"
Glyph="&#xF5b0;"
Visibility="Collapsed" />
<FontIcon x:Name="ButtonOutlineIcon"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Foreground="{ThemeResource PlayButtonNormalColor}"
Glyph="&#xE768;" />
</Grid>
</Viewbox>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Target="ButtonBackgroundIcon.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="ButtonBackgroundIcon.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled" />
</VisualStateGroup>
<VisualStateGroup x:Name="PlayButtonStates">
<VisualState x:Name="Ready">
<VisualState.Setters>
<Setter Target="ButtonBackgroundIcon.Glyph" Value="&#xF5b0;" />
<Setter Target="ButtonOutlineIcon.Glyph" Value="&#xE768;" />
<Setter Target="StatusProgress.IsActive" Value="False" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="TaskItemTemplate"
x:DataType="local:FilteredTask">
<mux:TreeViewItem x:Name="rootItem"
AutomationProperties.HelpText="{x:Bind Input, Mode=OneWay}"
AutomationProperties.Name="{x:Bind FilteredCommand.Item.Name, Mode=OneWay}"
ItemsSource="{x:Bind Children, Mode=OneWay}"
KeyUp="_treeItemKeyUpHandler"
Visibility="{x:Bind Visibility, Mode=OneWay}">
<Grid AutomationProperties.AccessibilityView="Raw">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<!-- Name -->
<RowDefinition Height="*" />
<!-- Input -->
<RowDefinition Height="0" />
<!-- Input gets put here when we're a nested command, to hide it -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button x:Uid="SnippetPlayButton"
Grid.Row="0"
Grid.RowSpan="1"
Grid.Column="0"
Margin="-28,0,0,0"
Padding="3"
VerticalAlignment="Bottom"
Background="Transparent"
BorderBrush="Transparent"
Click="_runCommandButtonClicked"
Style="{StaticResource PlayButtonTemplate}"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(HasChildren), Mode=OneWay}">
<Button.Content>
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Glyph="&#xE768;" />
<!--
xE768 is Play, which is just an outline.
xF5B0 is PlaySolid, also a good option. Seemed
better to have a lightweight outline though
-->
</Button.Content>
<Button.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonForegroundPointerOver"
Color="{StaticResource SystemAccentColor}" />
<SolidColorBrush x:Key="ButtonForegroundPressed"
Color="{StaticResource SystemAccentColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonForegroundPointerOver"
Color="{StaticResource SystemAccentColor}" />
<SolidColorBrush x:Key="ButtonForegroundPressed"
Color="{StaticResource SystemAccentColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="ButtonBackground"
Color="{ThemeResource SystemColorButtonFaceColor}" />
<SolidColorBrush x:Key="ButtonBackgroundPointerOver"
Color="{ThemeResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="ButtonBackgroundPressed"
Color="{ThemeResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="ButtonForeground"
Color="{ThemeResource SystemColorButtonTextColor}" />
<SolidColorBrush x:Key="ButtonForegroundPointerOver"
Color="{ThemeResource SystemColorHighlightTextColor}" />
<SolidColorBrush x:Key="ButtonForegroundPressed"
Color="{ThemeResource SystemColorHighlightTextColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
</Button>
<local:HighlightedTextControl Grid.Column="1"
HorizontalAlignment="Left"
IsTabStop="False"
Text="{x:Bind FilteredCommand.HighlightedName, Mode=OneWay}" />
<!--
BODGY: I can't choose different templates if this item is nested or not.
(See https://github.com/microsoft/microsoft-ui-xaml/issues/2121)
But what I can do, is toss the text into a 0-height row to hide it.
-->
<TextBlock Grid.Row="{x:Bind Row, Mode=OneWay}"
Grid.Column="1"
Margin="12,0,12,6"
FontFamily="Cascadia Mono, Consolas"
IsTextSelectionEnabled="False"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind Input}"
Visibility="{Binding ElementName=rootItem, Path=IsSelected}" />
</Grid>
</mux:TreeViewItem>
</DataTemplate>
<ResourceDictionary.ThemeDictionaries>
<!-- same as in MainPage, this is SolidBackgroundFillColorTertiary -->
<ResourceDictionary x:Key="Dark">
<Color x:Key="PageBackground">#282828</Color>
<Color x:Key="PlayButtonHoveredColor">#90ef90</Color>
<Color x:Key="PlayButtonNormalColor">#8888</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<Color x:Key="PageBackground">#F9F9F9</Color>
<Color x:Key="PlayButtonHoveredColor">#257f01</Color>
<Color x:Key="PlayButtonNormalColor">#88222222</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<!-- Define resources for HighContrast mode here -->
<StaticResource x:Key="PageBackground"
ResourceKey="SystemColorWindowColorBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<!-- Don't allow the user to focus the root pane, but do allow individual controls to be focused -->
<!-- TabFocusNavigation="Cycle" is load-bearing: otherwise, tab focus disappears to nowhere off the play button -->
<Grid AllowFocusOnInteraction="False"
AutomationProperties.AccessibilityView="Raw"
Background="{ThemeResource PageBackground}"
TabFocusNavigation="Cycle">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock x:Name="_title"
x:Uid="SnippetPaneTitle"
Grid.Row="0"
Margin="4"
FontSize="24" />
<TextBlock Grid.Row="1"
Margin="8,16,8,8"
TextWrapping="WrapWholeWords"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(HasSnippets), Mode=OneWay}">
<Run x:Uid="SnippetsPaneNoneFoundHeader" />
<LineBreak />
<Run x:Uid="SnippetsPaneNoneFoundDetails" />
</TextBlock>
<TextBox x:Name="_filterBox"
x:Uid="SnippetsFilterBox"
Grid.Row="1"
Margin="8,0,8,8"
AllowFocusOnInteraction="True"
TextChanged="_filterTextChanged"
Visibility="{x:Bind HasSnippets, Mode=OneWay}" />
<mux:TreeView x:Name="_treeView"
Grid.Row="2"
AllowFocusOnInteraction="True"
CanDragItems="False"
CanReorderItems="False"
ItemInvoked="_treeItemInvokedHandler"
ItemTemplate="{StaticResource TaskItemTemplate}"
Visibility="{x:Bind HasSnippets, Mode=OneWay}" />
</Grid>
</UserControl>

View File

@ -71,6 +71,9 @@
<Page Include="SuggestionsControl.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="SnippetsPaneContent.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<!-- ========================= Headers ======================== -->
<ItemGroup>
@ -161,6 +164,9 @@
<ClInclude Include="ScratchpadContent.h">
<DependentUpon>TerminalPaneContent.idl</DependentUpon>
</ClInclude>
<ClInclude Include="SnippetsPaneContent.h">
<DependentUpon>SnippetsPaneContent.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="SettingsPaneContent.h">
<DependentUpon>TerminalPaneContent.idl</DependentUpon>
</ClInclude>
@ -274,6 +280,9 @@
<ClCompile Include="ScratchpadContent.cpp">
<DependentUpon>TerminalPaneContent.idl</DependentUpon>
</ClCompile>
<ClCompile Include="SnippetsPaneContent.cpp">
<DependentUpon>SnippetsPaneContent.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="SettingsPaneContent.cpp">
<DependentUpon>TerminalPaneContent.idl</DependentUpon>
</ClCompile>
@ -352,7 +361,10 @@
</Midl>
<Midl Include="FilteredCommand.idl" />
<Midl Include="IPaneContent.idl" />
<Midl Include="TerminalPaneContent.idl" />
<Midl Include="TerminalPaneContent.idl" >
<DependentUpon>TaskPaneContent.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="TerminalSettingsCache.idl" />
</ItemGroup>
<!-- ========================= Misc Files ======================== -->

View File

@ -16,6 +16,7 @@
#include "DebugTapConnection.h"
#include "SettingsPaneContent.h"
#include "ScratchpadContent.h"
#include "SnippetsPaneContent.h"
#include "TabRowControl.h"
#include "TerminalPage.g.cpp"
@ -451,10 +452,10 @@ namespace winrt::TerminalApp::implementation
// - command - command to dispatch
// Return Value:
// - <none>
void TerminalPage::_OnDispatchCommandRequested(const IInspectable& /*sender*/, const Microsoft::Terminal::Settings::Model::Command& command)
void TerminalPage::_OnDispatchCommandRequested(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& command)
{
const auto& actionAndArgs = command.ActionAndArgs();
_actionDispatch->DoAction(actionAndArgs);
_actionDispatch->DoAction(sender, actionAndArgs);
}
// Method Description:
@ -620,9 +621,12 @@ namespace winrt::TerminalApp::implementation
// GH#6586: now that we're done processing all startup commands,
// focus the active control. This will work as expected for both
// commandline invocations and for `wt` action invocations.
if (const auto control = _GetActiveControl())
if (const auto& terminalTab{ _GetFocusedTabImpl() })
{
control.Focus(FocusState::Programmatic);
if (const auto& content{ terminalTab->GetActiveContent() })
{
content.Focus(FocusState::Programmatic);
}
}
}
if (initial)
@ -2466,16 +2470,16 @@ namespace winrt::TerminalApp::implementation
}
_UnZoomIfNeeded();
auto [original, _] = activeTab->SplitPane(*realSplitType, splitSize, newPane);
auto [original, newGuy] = activeTab->SplitPane(*realSplitType, splitSize, newPane);
// After GH#6586, the control will no longer focus itself
// automatically when it's finished being laid out. Manually focus
// the control here instead.
if (_startupState == StartupState::Initialized)
{
if (const auto control = _GetActiveControl())
if (const auto& content{ newGuy->GetContent() })
{
control.Focus(FocusState::Programmatic);
content.Focus(FocusState::Programmatic);
}
}
}
@ -3270,6 +3274,8 @@ namespace winrt::TerminalApp::implementation
return resultPane;
}
// NOTE: callers of _MakePane should be able to accept nullptr as a return
// value gracefully.
std::shared_ptr<Pane> TerminalPage::_MakePane(const INewContentArgs& contentArgs,
const winrt::TerminalApp::TabBase& sourceTab,
TerminalConnection::ITerminalConnection existingConnection)
@ -3300,6 +3306,36 @@ namespace winrt::TerminalApp::implementation
{
content = _makeSettingsContent();
}
else if (paneType == L"snippets")
{
// Prevent the user from opening a bunch of snippets panes.
//
// Look at the focused tab, and if it already has one, then just focus it.
const bool found = _GetFocusedTab().try_as<TerminalTab>()->GetRootPane()->WalkTree([](const auto& p) -> bool {
if (const auto& snippets{ p->GetContent().try_as<SnippetsPaneContent>() })
{
snippets->Focus(FocusState::Programmatic);
return true;
}
return false;
});
// Bail out if we already found one.
if (found)
{
return nullptr;
}
const auto& tasksContent{ winrt::make_self<SnippetsPaneContent>() };
tasksContent->UpdateSettings(_settings);
tasksContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler });
tasksContent->DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested });
if (const auto& termControl{ _GetActiveControl() })
{
tasksContent->SetLastActiveControl(termControl);
}
content = *tasksContent;
}
assert(content);

View File

@ -3,6 +3,7 @@
import "IPaneContent.idl";
import "TerminalSettingsCache.idl";
import "FilteredCommand.idl";
namespace TerminalApp
{
@ -16,4 +17,22 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<TerminalPaneContent, Object> RestartTerminalRequested;
}
[default_interface] runtimeclass FilteredTask : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
String Input{ get; };
Windows.Foundation.Collections.IObservableVector<FilteredTask> Children { get; };
Boolean HasChildren { get; };
Int32 Row { get; };
Windows.UI.Xaml.Visibility Visibility { get; };
TerminalApp.FilteredCommand FilteredCommand{ get; };
}
[default_interface] runtimeclass SnippetsPaneContent : Windows.UI.Xaml.Controls.UserControl, IPaneContent, Windows.UI.Xaml.Data.INotifyPropertyChanged
{
SnippetsPaneContent();
void SetLastActiveControl(Microsoft.Terminal.Control.TermControl control);
Boolean HasSnippets { get; };
event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Settings.Model.Command> DispatchCommandRequested;
}
}

View File

@ -525,7 +525,7 @@ namespace winrt::TerminalApp::implementation
// - pane: The new pane to add to the tree of panes; note that this pane
// could itself be a parent pane/the root node of a tree of panes
// Return Value:
// - <none>
// - a pair of (the Pane that now holds the original content, the new Pane in the tree)
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> TerminalTab::SplitPane(SplitDirection splitType,
const float splitSize,
std::shared_ptr<Pane> pane)
@ -1223,6 +1223,20 @@ namespace winrt::TerminalApp::implementation
// Raise our own ActivePaneChanged event.
ActivePaneChanged.raise(*this, nullptr);
// If the new active pane is a terminal, tell other interested panes
// what the new active pane is.
const auto content{ pane->GetContent() };
if (const auto termContent{ content.try_as<winrt::TerminalApp::TerminalPaneContent>() })
{
const auto& termControl{ termContent.GetTermControl() };
_rootPane->WalkTree([termControl](const auto& p) {
if (const auto& taskPane{ p->GetContent().try_as<SnippetsPaneContent>() })
{
taskPane.SetLastActiveControl(termControl);
}
});
}
}
// Method Description:

View File

@ -126,6 +126,10 @@
<data name="SplitPaneParentCommandName" xml:space="preserve">
<value>Split pane</value>
</data>
<data name="SnippetsPaneCommandName" xml:space="preserve">
<value>Open snippets pane</value>
<comment>This will open a pane with a list of the users's saved commands ("snippets") in it</comment>
</data>
<data name="ApplicationDisplayNamePortable" xml:space="preserve">
<value>Terminal (Portable)</value>
<comment>This display name is used when the Terminal application is running in a "portable" mode, where settings are not stored in a shared location.</comment>

View File

@ -446,6 +446,7 @@
{ "command": "quit", "id": "Terminal.Quit" },
{ "command": "restoreLastClosed", "id": "Terminal.RestoreLastClosed" },
{ "command": "openAbout", "id": "Terminal.OpenAboutDialog" },
{ "command": "experimental.openTasks", "id": "Terminal.OpenTasks" },
// Tab Management
// "command": "closeTab" is unbound by default.
@ -528,6 +529,7 @@
{ "command": { "action": "movePane", "index": 8 }, "id": "Terminal.MovePaneToTab8" },
{ "command": { "action": "movePane", "window": "new" }, "id": "Terminal.MovePaneToNewWindow" },
{ "command": "restartConnection", "id": "Terminal.RestartConnection" },
{ "command": { "action": "splitPane", "type": "snippets" }, "id": "Terminal.OpenSnippetsPane", "name": { "key": "SnippetsPaneCommandName" } },
// Clipboard Integration
{ "command": { "action": "copy", "singleLine": false }, "id": "Terminal.CopyToClipboard" },

View File

@ -133,7 +133,7 @@ public: \
_##name = value; \
} \
\
private: \
protected: \
type _##name{ __VA_ARGS__ };
// Use this macro to quickly implement both the getter and setter for an
@ -158,7 +158,7 @@ public:
} \
}; \
\
private: \
protected: \
type _##name{ __VA_ARGS__ }; \
void _set##name(const type& value) \
{ \