Advanced Tab Switcher (#6732)

![TabSwitchingv2](https://user-images.githubusercontent.com/57155886/88237962-5505d500-cc35-11ea-8384-d91699155067.gif)

## Summary of the Pull Request
This PR adds the Advanced Tab Switcher (ATS) to Terminal. It'll work
similarly to VSCode's tab switcher. Because this implementation rides
off a lot of the Command Palette's XAML code, it'll look just like the
Command Palette, and also have support for tab title search.

## References
#3753 - ATS Spec

Closes #1502
This commit is contained in:
Leon Liang
2020-08-11 07:03:12 -07:00
committed by GitHub
parent c03677b0c9
commit b07c1e49da
23 changed files with 645 additions and 87 deletions

View File

@@ -67,6 +67,7 @@
"wt",
"closeOtherTabs",
"closeTabsAfter",
"tabSwitcher",
"unbound"
],
"type": "string"
@@ -88,6 +89,14 @@
],
"type": "string"
},
"AnchorKey": {
"enum": [
"ctrl",
"alt",
"shift"
],
"type": "string"
},
"NewTerminalArgs": {
"properties": {
"commandline": {
@@ -352,6 +361,22 @@
],
"required": [ "index" ]
},
"TabSwitcherAction": {
"description": "Arguments corresponding to a Tab Switcher Action",
"allOf": [
{ "$ref": "#/definitions/ShortcutAction" },
{
"properties": {
"action": { "type": "string", "pattern": "tabSwitcher" },
"anchorKey": {
"$ref": "#/definitions/AnchorKey",
"default": null,
"description": "If provided, the tab switcher will stay open as long as the anchor key is held down. The anchor key should be part of the keybinding that opens the switcher."
}
}
}
]
},
"Keybinding": {
"additionalProperties": false,
"properties": {
@@ -372,6 +397,7 @@
{ "$ref": "#/definitions/WtAction" },
{ "$ref": "#/definitions/CloseOtherTabsAction" },
{ "$ref": "#/definitions/CloseTabsAfterAction" },
{ "$ref": "#/definitions/TabSwitcherAction" },
{ "type": "null" }
]
},

View File

@@ -44,6 +44,7 @@ static constexpr std::string_view ExecuteCommandlineKey{ "wt" };
static constexpr std::string_view ToggleCommandPaletteKey{ "commandPalette" };
static constexpr std::string_view CloseOtherTabsKey{ "closeOtherTabs" };
static constexpr std::string_view CloseTabsAfterKey{ "closeTabsAfter" };
static constexpr std::string_view ToggleTabSwitcherKey{ "tabSwitcher" };
static constexpr std::string_view ActionKey{ "action" };
@@ -100,6 +101,7 @@ namespace winrt::TerminalApp::implementation
{ ToggleCommandPaletteKey, ShortcutAction::ToggleCommandPalette },
{ CloseOtherTabsKey, ShortcutAction::CloseOtherTabs },
{ CloseTabsAfterKey, ShortcutAction::CloseTabsAfter },
{ ToggleTabSwitcherKey, ShortcutAction::ToggleTabSwitcher },
};
using ParseResult = std::tuple<IActionArgs, std::vector<::TerminalApp::SettingsLoadWarnings>>;
@@ -139,6 +141,8 @@ namespace winrt::TerminalApp::implementation
{ ShortcutAction::CloseTabsAfter, winrt::TerminalApp::implementation::CloseTabsAfterArgs::FromJson },
{ ShortcutAction::ToggleTabSwitcher, winrt::TerminalApp::implementation::ToggleTabSwitcherArgs::FromJson },
{ ShortcutAction::Invalid, nullptr },
};

View File

@@ -19,6 +19,7 @@
#include "SetTabColorArgs.g.cpp"
#include "RenameTabArgs.g.cpp"
#include "ExecuteCommandlineArgs.g.cpp"
#include "ToggleTabSwitcherArgs.g.h"
#include <LibraryResources.h>
@@ -303,4 +304,22 @@ namespace winrt::TerminalApp::implementation
_Index)
};
}
winrt::hstring ToggleTabSwitcherArgs::GenerateName() const
{
// If there's an anchor key set, don't generate a name so that
// it won't show up in the command palette. Only an unanchored
// tab switcher should be able to be toggled from the palette.
// TODO: GH#7179 - once this goes in, make sure to hide the
// anchor mode command that was given a name in settings.
if (_AnchorKey != Windows::System::VirtualKey::None)
{
return L"";
}
else
{
return RS_(L"ToggleTabSwitcherCommandKey");
}
}
}

View File

@@ -21,6 +21,7 @@
#include "ExecuteCommandlineArgs.g.h"
#include "CloseOtherTabsArgs.g.h"
#include "CloseTabsAfterArgs.g.h"
#include "ToggleTabSwitcherArgs.g.h"
#include "../../cascadia/inc/cppwinrt_utils.h"
#include "Utils.h"
@@ -512,6 +513,33 @@ namespace winrt::TerminalApp::implementation
}
};
struct ToggleTabSwitcherArgs : public ToggleTabSwitcherArgsT<ToggleTabSwitcherArgs>
{
ToggleTabSwitcherArgs() = default;
GETSET_PROPERTY(Windows::System::VirtualKey, AnchorKey, Windows::System::VirtualKey::None);
static constexpr std::string_view AnchorJsonKey{ "anchorKey" };
public:
hstring GenerateName() const;
bool Equals(const IActionArgs& other)
{
auto otherAsUs = other.try_as<ToggleTabSwitcherArgs>();
if (otherAsUs)
{
return otherAsUs->_AnchorKey == _AnchorKey;
}
return false;
};
static FromJsonResult FromJson(const Json::Value& json)
{
// LOAD BEARING: Not using make_self here _will_ break you in the future!
auto args = winrt::make_self<ToggleTabSwitcherArgs>();
JsonUtils::GetValueForKey(json, AnchorJsonKey, args->_AnchorKey);
return { *args, {} };
}
};
}
namespace winrt::TerminalApp::factory_implementation

View File

@@ -135,4 +135,9 @@ namespace TerminalApp
{
UInt32 Index { get; };
};
[default_interface] runtimeclass ToggleTabSwitcherArgs : IActionArgs
{
Windows.System.VirtualKey AnchorKey { get; };
};
}

View File

@@ -285,6 +285,7 @@ namespace winrt::TerminalApp::implementation
{
// TODO GH#6677: When we add support for commandline mode, first set the
// mode that the command palette should be in, before making it visible.
CommandPalette().EnableCommandPaletteMode();
CommandPalette().Visibility(CommandPalette().Visibility() == Visibility::Visible ?
Visibility::Collapsed :
Visibility::Visible);
@@ -414,6 +415,7 @@ namespace winrt::TerminalApp::implementation
actionArgs.Handled(true);
}
}
void TerminalPage::_HandleCloseTabsAfter(const IInspectable& /*sender*/,
const TerminalApp::ActionEventArgs& actionArgs)
{
@@ -436,4 +438,28 @@ namespace winrt::TerminalApp::implementation
actionArgs.Handled(true);
}
}
void TerminalPage::_HandleToggleTabSwitcher(const IInspectable& /*sender*/,
const TerminalApp::ActionEventArgs& args)
{
if (const auto& realArgs = args.ActionArgs().try_as<TerminalApp::ToggleTabSwitcherArgs>())
{
auto anchorKey = realArgs.AnchorKey();
auto opt = _GetFocusedTabIndex();
uint32_t startIdx = opt ? *opt : 0;
if (anchorKey != VirtualKey::None)
{
// TODO: GH#7178 - delta should also have the option of being -1, in the case when
// a user decides to open the tab switcher going to the prev tab.
int delta = 1;
startIdx = (startIdx + _tabs.Size() + delta) % _tabs.Size();
}
CommandPalette().EnableTabSwitcherMode(anchorKey, startIdx);
CommandPalette().Visibility(Visibility::Visible);
}
args.Handled(true);
}
}

View File

@@ -32,10 +32,13 @@ namespace winrt::TerminalApp::implementation
static std::vector<::TerminalApp::SettingsLoadWarnings> LayerJson(std::unordered_map<winrt::hstring, winrt::TerminalApp::Command>& commands,
const Json::Value& json);
winrt::Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker propertyChangedRevoker;
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Name, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::TerminalApp::ActionAndArgs, Action, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, KeyChordText, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::Windows::UI::Xaml::Controls::IconSource, IconSource, _PropertyChangedHandlers, nullptr);
};
}

View File

@@ -12,5 +12,7 @@ namespace TerminalApp
String Name;
ActionAndArgs Action;
String KeyChordText;
Windows.UI.Xaml.Controls.IconSource IconSource;
}
}

View File

@@ -3,6 +3,11 @@
#include "pch.h"
#include "CommandPalette.h"
#include "ActionAndArgs.h"
#include "ActionArgs.h"
#include "Command.h"
#include <LibraryResources.h>
#include "CommandPalette.g.cpp"
@@ -12,15 +17,18 @@ using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::System;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
namespace winrt::TerminalApp::implementation
{
CommandPalette::CommandPalette()
CommandPalette::CommandPalette() :
_switcherStartIdx{ 0 }
{
InitializeComponent();
_filteredActions = winrt::single_threaded_observable_vector<winrt::TerminalApp::Command>();
_allActions = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
_allCommands = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
_allTabActions = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
if (CommandPaletteShadow())
{
@@ -38,8 +46,26 @@ namespace winrt::TerminalApp::implementation
RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) {
if (Visibility() == Visibility::Visible)
{
_searchBox().Focus(FocusState::Programmatic);
_filteredActionsView().SelectedIndex(0);
if (_currentMode == CommandPaletteMode::TabSwitcherMode)
{
if (_anchorKey != VirtualKey::None)
{
_searchBox().Visibility(Visibility::Collapsed);
_filteredActionsView().Focus(FocusState::Keyboard);
}
else
{
_searchBox().Focus(FocusState::Programmatic);
}
_filteredActionsView().SelectedIndex(_switcherStartIdx);
_filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem());
}
else
{
_searchBox().Focus(FocusState::Programmatic);
_filteredActionsView().SelectedIndex(0);
}
TraceLoggingWrite(
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
@@ -55,6 +81,19 @@ namespace winrt::TerminalApp::implementation
_dismissPalette();
}
});
// Focusing the ListView when the Command Palette control is set to Visible
// for the first time fails because the ListView hasn't finished loading by
// the time Focus is called. Luckily, We can listen to SizeChanged to know
// when the ListView has been measured out and is ready, and we'll immediately
// revoke the handler because we only needed to handle it once on initialization.
_sizeChangedRevoker = _filteredActionsView().SizeChanged(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) {
if (_currentMode == CommandPaletteMode::TabSwitcherMode && _anchorKey != VirtualKey::None)
{
_filteredActionsView().Focus(FocusState::Keyboard);
}
_sizeChangedRevoker.revoke();
});
}
// Method Description:
@@ -77,6 +116,36 @@ namespace winrt::TerminalApp::implementation
_filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem());
}
void CommandPalette::_previewKeyDownHandler(IInspectable const& /*sender*/,
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e)
{
auto key = e.OriginalKey();
// Some keypresses such as Tab, Return, Esc, and Arrow Keys are ignored by controls because
// they're not considered input key presses. While they don't raise KeyDown events,
// they do raise PreviewKeyDown events.
//
// Only give anchored tab switcher the ability to cycle through tabs with the tab button.
// For unanchored mode, accessibility becomes an issue when we try to hijack tab since it's
// a really widely used keyboard navigation key.
if (_currentMode == CommandPaletteMode::TabSwitcherMode &&
key == VirtualKey::Tab &&
_anchorKey != VirtualKey::None)
{
auto const state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift);
if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down))
{
_selectNextItem(false);
e.Handled(true);
}
else
{
_selectNextItem(true);
e.Handled(true);
}
}
}
// Method Description:
// - Process keystrokes in the input box. This is used for moving focus up
// and down the list of commands in Action mode, and for executing
@@ -108,7 +177,7 @@ namespace winrt::TerminalApp::implementation
if (const auto selectedItem = _filteredActionsView().SelectedItem())
{
_dispatchCommand(selectedItem.try_as<Command>());
_dispatchCommand(selectedItem.try_as<TerminalApp::Command>());
}
e.Handled(true);
@@ -129,6 +198,34 @@ namespace winrt::TerminalApp::implementation
}
}
void CommandPalette::_keyUpHandler(IInspectable const& /*sender*/,
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e)
{
auto key = e.OriginalKey();
if (_currentMode == CommandPaletteMode::TabSwitcherMode)
{
if (_anchorKey && key == _anchorKey.value())
{
// Once the user lifts the anchor key, we'll switch to the currently selected tab
// then close the tab switcher.
if (const auto selectedItem = _filteredActionsView().SelectedItem())
{
if (const auto data = selectedItem.try_as<TerminalApp::Command>())
{
const auto actionAndArgs = data.Action();
_dispatch.DoAction(actionAndArgs);
_updateFilteredActions();
_dismissPalette();
}
}
e.Handled(true);
}
}
}
// 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,
@@ -171,6 +268,28 @@ namespace winrt::TerminalApp::implementation
_dispatchCommand(e.ClickedItem().try_as<TerminalApp::Command>());
}
// 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<TerminalApp::Command> CommandPalette::_commandsToFilter()
{
switch (_currentMode)
{
case CommandPaletteMode::ActionMode:
return _allCommands;
case CommandPaletteMode::TabSwitcherMode:
return _allTabActions;
default:
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
@@ -184,6 +303,10 @@ namespace winrt::TerminalApp::implementation
{
if (command)
{
// 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();
const auto actionAndArgs = command.Action();
_dispatch.DoAction(actionAndArgs);
@@ -194,8 +317,6 @@ namespace winrt::TerminalApp::implementation
TraceLoggingUInt32(_searchBox().Text().size(), "SearchTextLength", "Number of characters in the search string"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
_close();
}
}
@@ -235,17 +356,59 @@ namespace winrt::TerminalApp::implementation
_noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible);
}
Collections::IObservableVector<Command> CommandPalette::FilteredActions()
Collections::IObservableVector<TerminalApp::Command> CommandPalette::FilteredActions()
{
return _filteredActions;
}
void CommandPalette::SetActions(Collections::IVector<TerminalApp::Command> const& actions)
void CommandPalette::SetCommands(Collections::IVector<TerminalApp::Command> const& actions)
{
_allActions = actions;
_allCommands = actions;
_updateFilteredActions();
}
void CommandPalette::EnableCommandPaletteMode()
{
_switchToMode(CommandPaletteMode::ActionMode);
_updateFilteredActions();
}
void CommandPalette::_switchToMode(CommandPaletteMode mode)
{
// 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.
if (mode != _currentMode)
{
_currentMode = mode;
_filteredActions.Clear();
auto commandsToFilter = _commandsToFilter();
for (auto action : commandsToFilter)
{
_filteredActions.Append(action);
}
switch (_currentMode)
{
case CommandPaletteMode::TabSwitcherMode:
{
SearchBoxText(RS_(L"TabSwitcher_SearchBoxText"));
NoMatchesText(RS_(L"TabSwitcher_NoMatchesText"));
ControlName(RS_(L"TabSwitcherControlName"));
break;
}
case CommandPaletteMode::ActionMode:
default:
SearchBoxText(RS_(L"CommandPalette_SearchBox/PlaceholderText"));
NoMatchesText(RS_(L"CommandPalette_NoMatchesText/Text"));
ControlName(RS_(L"CommandPaletteControlName"));
break;
}
}
}
// This is a helper to aid in sorting commands by their `Name`s, alphabetically.
static bool _compareCommandNames(const TerminalApp::Command& lhs, const TerminalApp::Command& rhs)
{
@@ -259,13 +422,23 @@ namespace winrt::TerminalApp::implementation
{
TerminalApp::Command command;
int weight;
int inOrderCounter;
bool operator<(const WeightedCommand& other) const
{
// If two commands have the same weight, then we'll sort them alphabetically.
if (weight == other.weight)
{
return !_compareCommandNames(command, other.command);
// If two commands have the same weight, then we'll sort them alphabetically.
// If they both have the same name, fall back to the order in which they were
// pushed into the heap.
if (command.Name() == other.command.Name())
{
return inOrderCounter > other.inOrderCounter;
}
else
{
return !_compareCommandNames(command, other.command);
}
}
return weight < other.weight;
}
@@ -286,16 +459,30 @@ namespace winrt::TerminalApp::implementation
auto searchText = _searchBox().Text();
const bool addAll = searchText.empty();
auto commandsToFilter = _commandsToFilter();
// If there's no filter text, then just add all the commands in order to the list.
// - TODO GH#6647:Possibly add the MRU commands first in order, followed
// by the rest of the commands.
if (addAll)
{
// If TabSwitcherMode, just add all as is. We don't want
// them to be sorted alphabetically.
if (_currentMode == CommandPaletteMode::TabSwitcherMode)
{
for (auto action : commandsToFilter)
{
actions.push_back(action);
}
return actions;
}
// Add all the commands, but make sure they're sorted alphabetically.
std::vector<TerminalApp::Command> sortedCommands;
sortedCommands.reserve(_allActions.Size());
sortedCommands.reserve(commandsToFilter.Size());
for (auto action : _allActions)
for (auto action : commandsToFilter)
{
sortedCommands.push_back(action);
}
@@ -325,7 +512,12 @@ namespace winrt::TerminalApp::implementation
// appear first in the list. The ordering will be determined by the
// match weight produced by _getWeight.
std::priority_queue<WeightedCommand> heap;
for (auto action : _allActions)
// TODO GH#7205: Find a better way to ensure that WCs of the same
// weight and name stay in the order in which they were pushed onto
// the PQ.
uint32_t counter = 0;
for (auto action : commandsToFilter)
{
const auto weight = CommandPalette::_getWeight(searchText, action.Name());
if (weight > 0)
@@ -333,6 +525,8 @@ namespace winrt::TerminalApp::implementation
WeightedCommand wc;
wc.command = action;
wc.weight = weight;
wc.inOrderCounter = counter++;
heap.push(wc);
}
}
@@ -502,8 +696,150 @@ namespace winrt::TerminalApp::implementation
{
Visibility(Visibility::Collapsed);
// Reset visibility in case anchor mode tab switcher just finished.
_searchBox().Visibility(Visibility::Visible);
// Clear the text box each time we close the dialog. This is consistent with VsCode.
_searchBox().Text(L"");
}
// Method Description:
// - Listens for changes to TerminalPage's _tabs vector. Updates our vector of
// tab switching commands accordingly.
// Arguments:
// - s: The vector being listened to.
// - e: The vector changed args that tells us whether a change, insert, or removal was performed
// on the listened-to vector.
// Return Value:
// - <none>
void CommandPalette::OnTabsChanged(const IInspectable& s, const IVectorChangedEventArgs& e)
{
if (auto tabList = s.try_as<IObservableVector<TerminalApp::Tab>>())
{
auto idx = e.Index();
auto changedEvent = e.CollectionChange();
switch (changedEvent)
{
case CollectionChange::ItemChanged:
{
winrt::com_ptr<Command> item;
item.copy_from(winrt::get_self<Command>(_allTabActions.GetAt(idx)));
item->propertyChangedRevoker.revoke();
auto tab = tabList.GetAt(idx);
GenerateCommandForTab(idx, false, tab);
UpdateTabIndices(idx);
break;
}
case CollectionChange::ItemInserted:
{
auto tab = tabList.GetAt(idx);
GenerateCommandForTab(idx, true, tab);
UpdateTabIndices(idx);
break;
}
case CollectionChange::ItemRemoved:
{
winrt::com_ptr<Command> item;
item.copy_from(winrt::get_self<Command>(_allTabActions.GetAt(idx)));
item->propertyChangedRevoker.revoke();
_allTabActions.RemoveAt(idx);
UpdateTabIndices(idx);
break;
}
}
_updateFilteredActions();
}
}
// Method Description:
// - In the case where a tab is removed or reordered, the given indices of
// the tab switch commands following the removed/reordered tab will get out of sync by 1
// (e.g. if tab 1 is removed, tabs 2,3,4,... need to become tabs 1,2,3,...)
// This function just loops through the tabs following startIdx and adjusts their given indices.
// Arguments:
// - startIdx: The index to start the update loop at.
// Return Value:
// - <none>
void CommandPalette::UpdateTabIndices(const uint32_t startIdx)
{
if (startIdx != _allTabActions.Size() - 1)
{
for (auto i = startIdx; i < _allTabActions.Size(); ++i)
{
auto command = _allTabActions.GetAt(i);
command.Action().Args().as<implementation::SwitchToTabArgs>()->TabIndex(i);
}
}
}
// Method Description:
// - Create a tab switching command based on the given tab object and insert/update the command
// at the given index. The command will call a SwitchToTab action on the given idx.
// Arguments:
// - idx: The index to insert or update the tab switch command.
// - tab: The tab object to refer to when creating the tab switch command.
// Return Value:
// - <none>
void CommandPalette::GenerateCommandForTab(const uint32_t idx, bool inserted, TerminalApp::Tab& tab)
{
auto focusTabAction = winrt::make_self<implementation::ActionAndArgs>();
auto args = winrt::make_self<implementation::SwitchToTabArgs>();
args->TabIndex(idx);
focusTabAction->Action(ShortcutAction::SwitchToTab);
focusTabAction->Args(*args);
auto command = winrt::make_self<implementation::Command>();
command->Action(*focusTabAction);
command->Name(tab.Title());
command->IconSource(tab.IconSource());
// Listen for changes to the Tab so we can update this Command's attributes accordingly.
auto weakThis{ get_weak() };
auto weakCommand{ command->get_weak() };
command->propertyChangedRevoker = tab.PropertyChanged(winrt::auto_revoke, [weakThis, weakCommand, tab](auto&&, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args) {
auto palette{ weakThis.get() };
auto command{ weakCommand.get() };
if (palette && command)
{
if (args.PropertyName() == L"Title")
{
if (command->Name() != tab.Title())
{
command->Name(tab.Title());
}
}
if (args.PropertyName() == L"IconSource")
{
if (command->IconSource() != tab.IconSource())
{
command->IconSource(tab.IconSource());
}
}
}
});
if (inserted)
{
_allTabActions.InsertAt(idx, *command);
}
else
{
_allTabActions.SetAt(idx, *command);
}
}
void CommandPalette::EnableTabSwitcherMode(const VirtualKey& anchorKey, const uint32_t startIdx)
{
_switcherStartIdx = startIdx;
_anchorKey = anchorKey;
_switchToMode(CommandPaletteMode::TabSwitcherMode);
_updateFilteredActions();
}
}

View File

@@ -8,26 +8,50 @@
namespace winrt::TerminalApp::implementation
{
enum class CommandPaletteMode
{
ActionMode = 0,
TabSwitcherMode
};
struct CommandPalette : CommandPaletteT<CommandPalette>
{
CommandPalette();
Windows::Foundation::Collections::IObservableVector<TerminalApp::Command> FilteredActions();
void SetActions(Windows::Foundation::Collections::IVector<TerminalApp::Command> const& actions);
void SetCommands(Windows::Foundation::Collections::IVector<TerminalApp::Command> const& actions);
void EnableCommandPaletteMode();
void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch);
// Tab Switcher
void EnableTabSwitcherMode(const Windows::System::VirtualKey& anchorKey, const uint32_t startIdx);
void OnTabsChanged(const Windows::Foundation::IInspectable& s, const Windows::Foundation::Collections::IVectorChangedEventArgs& e);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, NoMatchesText, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, SearchBoxText, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ControlName, _PropertyChangedHandlers);
private:
friend struct CommandPaletteT<CommandPalette>; // for Xaml to bind events
Windows::Foundation::Collections::IObservableVector<TerminalApp::Command> _filteredActions{ nullptr };
Windows::Foundation::Collections::IVector<TerminalApp::Command> _allActions{ nullptr };
Windows::Foundation::Collections::IVector<TerminalApp::Command> _allCommands{ nullptr };
winrt::TerminalApp::ShortcutActionDispatch _dispatch;
Windows::Foundation::Collections::IVector<TerminalApp::Command> _commandsToFilter();
void _filterTextChanged(Windows::Foundation::IInspectable const& sender,
Windows::UI::Xaml::RoutedEventArgs const& args);
void _previewKeyDownHandler(Windows::Foundation::IInspectable const& sender,
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e);
void _keyDownHandler(Windows::Foundation::IInspectable const& sender,
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e);
void _keyUpHandler(Windows::Foundation::IInspectable const& sender,
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e);
void _rootPointerPressed(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
void _backdropPointerPressed(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
@@ -41,6 +65,18 @@ namespace winrt::TerminalApp::implementation
static int _getWeight(const winrt::hstring& searchText, const winrt::hstring& name);
void _close();
CommandPaletteMode _currentMode;
void _switchToMode(CommandPaletteMode mode);
// Tab Switcher
std::optional<winrt::Windows::System::VirtualKey> _anchorKey;
void GenerateCommandForTab(const uint32_t idx, bool inserted, winrt::TerminalApp::Tab& tab);
void UpdateTabIndices(const uint32_t startIdx);
Windows::Foundation::Collections::IVector<TerminalApp::Command> _allTabActions{ nullptr };
uint32_t _switcherStartIdx;
winrt::Windows::UI::Xaml::Controls::ListView::SizeChanged_revoker _sizeChangedRevoker;
void _dispatchCommand(const TerminalApp::Command& command);
void _dismissPalette();

View File

@@ -5,14 +5,22 @@ import "../Command.idl";
namespace TerminalApp
{
[default_interface] runtimeclass CommandPalette : Windows.UI.Xaml.Controls.Grid
[default_interface] runtimeclass CommandPalette : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged
{
CommandPalette();
String NoMatchesText { get; };
String SearchBoxText { get; };
String ControlName { get; };
Windows.Foundation.Collections.IObservableVector<Command> FilteredActions { get; };
void SetActions(Windows.Foundation.Collections.IVector<Command> actions);
void SetCommands(Windows.Foundation.Collections.IVector<Command> actions);
void EnableCommandPaletteMode();
void SetDispatch(ShortcutActionDispatch dispatch);
void EnableTabSwitcherMode(Windows.System.VirtualKey anchorKey, UInt32 startIdx);
void OnTabsChanged(IInspectable s, Windows.Foundation.Collections.IVectorChangedEventArgs e);
}
}

View File

@@ -1,6 +1,6 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information. -->
<Grid
<UserControl
x:Class="TerminalApp.CommandPalette"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -9,10 +9,17 @@ the MIT License. See LICENSE in the project root for license information. -->
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Windows10version1903="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 8)"
TabNavigation="Cycle"
IsTabStop="True"
AllowFocusOnInteraction="True"
PointerPressed="_rootPointerPressed"
mc:Ignorable="d">
PreviewKeyDown="_previewKeyDownHandler"
KeyDown="_keyDownHandler"
PreviewKeyUp="_keyUpHandler"
mc:Ignorable="d"
AutomationProperties.Name="{x:Bind ControlName, Mode=OneWay}">
<Grid.Resources>
<UserControl.Resources>
<ResourceDictionary>
<!-- ThemeShadow is only on 18362. This "Windows10version1903" bit
@@ -100,29 +107,30 @@ the MIT License. See LICENSE in the project root for license information. -->
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</UserControl.Resources>
</Grid.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="6*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="6*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
<RowDefinition Height="2*"/>
</Grid.RowDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
<RowDefinition Height="2*"/>
</Grid.RowDefinitions>
<!-- Setting the row/col span of this shadow backdrop is a bit of a hack. In
order to receive pointer events, an element needs to be _not_ transparent.
However, we want to be able to eat all the clicks outside the immediate
bounds of the command palette, and we don't want a semi-transparent overlay
over all of the UI. Fortunately, if we make this _shadowBackdrop the size of
the entire page, then it can be mostly transparent, and cause the root grid
to receive clicks _anywhere_ in its bounds. -->
<!-- Setting the row/col span of this shadow backdrop is a bit of a hack. In
order to receive pointer events, an element needs to be _not_ transparent.
However, we want to be able to eat all the clicks outside the immediate
bounds of the command palette, and we don't want a semi-transparent overlay
over all of the UI. Fortunately, if we make this _shadowBackdrop the size of
the entire page, then it can be mostly transparent, and cause the root grid
to receive clicks _anywhere_ in its bounds. -->
<Grid
<Grid
x:Name="_shadowBackdrop"
Background="Transparent"
Grid.Column="0"
@@ -133,7 +141,7 @@ the MIT License. See LICENSE in the project root for license information. -->
VerticalAlignment="Stretch">
</Grid>
<Grid
<Grid
x:Name="_backdrop"
Style="{ThemeResource CommandPaletteBackground}"
CornerRadius="{ThemeResource ControlCornerRadius}"
@@ -145,33 +153,32 @@ the MIT License. See LICENSE in the project root for license information. -->
HorizontalAlignment="Stretch"
VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox
<TextBox
Grid.Row="0"
x:Uid="CommandPalette_SearchBox"
x:Name="_searchBox"
Margin="8"
IsSpellCheckEnabled="False"
TextChanged="_filterTextChanged"
KeyDown="_keyDownHandler"
PlaceholderText="{x:Bind SearchBoxText, Mode=OneWay}"
Text="">
</TextBox>
</TextBox>
<TextBlock
<TextBlock
Padding="16"
x:Name="_noMatchesText"
x:Uid="CommandPalette_NoMatchesText"
FontStyle="Italic"
Visibility="Collapsed"
Grid.Row="1">
</TextBlock>
Grid.Row="1"
Text="{x:Bind NoMatchesText, Mode=OneWay}">
</TextBlock>
<ListView
<ListView
Grid.Row="2"
x:Name="_filteredActionsView"
HorizontalAlignment="Stretch"
@@ -181,36 +188,39 @@ the MIT License. See LICENSE in the project root for license information. -->
AllowDrop="False"
IsItemClickEnabled="True"
ItemClick="_listItemClicked"
PreviewKeyDown="_keyDownHandler"
ItemsSource="{x:Bind FilteredActions}">
<ItemsControl.ItemTemplate >
<DataTemplate x:DataType="local:Command">
<ItemsControl.ItemTemplate >
<DataTemplate x:DataType="local:Command">
<!-- This HorizontalContentAlignment="Stretch" is important
<!-- This HorizontalContentAlignment="Stretch" is important
to make sure it takes the entire width of the line -->
<ListViewItem HorizontalContentAlignment="Stretch"
<ListViewItem HorizontalContentAlignment="Stretch"
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
AutomationProperties.AcceleratorKey="{x:Bind KeyChordText, Mode=OneWay}">
<Grid HorizontalAlignment="Stretch" >
<Grid HorizontalAlignment="Stretch" ColumnSpacing="8" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/> <!-- icon -->
<ColumnDefinition Width="Auto"/> <!-- command label -->
<ColumnDefinition Width="*"/> <!-- key chord -->
<ColumnDefinition Width="16"/> <!-- gutter for scrollbar -->
</Grid.ColumnDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/> <!-- icon -->
<ColumnDefinition Width="Auto"/> <!-- command label -->
<ColumnDefinition Width="*"/> <!-- key chord -->
<ColumnDefinition Width="16"/> <!-- gutter for scrollbar -->
</Grid.ColumnDefinitions>
<!-- TODO GH#6644: Add Icon to command palette entries, in column 0 -->
<IconSourceElement
Grid.Column="0"
IconSource="{x:Bind IconSource, Mode=OneWay}"/>
<TextBlock Grid.Column="1"
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
Text="{x:Bind Name, Mode=OneWay}" />
<!-- The block for the key chord is only visible
<!-- The block for the key chord is only visible
when there's actual text set as the label. See
CommandKeyChordVisibilityConverter for details. -->
<Border
<Border
Grid.Column="2"
Visibility="{x:Bind KeyChordText,
Mode=OneWay,
@@ -220,19 +230,20 @@ the MIT License. See LICENSE in the project root for license information. -->
HorizontalAlignment="Right"
VerticalAlignment="Center">
<TextBlock
<TextBlock
Style="{ThemeResource KeyChordTextBlockStyle}"
FontSize="12"
Text="{x:Bind KeyChordText, Mode=OneWay}" />
</Border>
</Border>
</Grid>
</ListViewItem>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListView>
</Grid>
</Grid>
</ListViewItem>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListView>
</Grid>
</Grid>
</UserControl>

View File

@@ -529,6 +529,9 @@
<data name="ToggleCommandPaletteCommandKey" xml:space="preserve">
<value>Toggle command palette</value>
</data>
<data name="CommandPaletteControlName" xml:space="preserve">
<value>Command Palette</value>
</data>
<data name="SetColorSchemeCommandKey" xml:space="preserve">
<value>Set color scheme to {0}</value>
<comment>{0} will be replaced with the name of a color scheme as defined by the user.</comment>
@@ -562,6 +565,18 @@
<value>Close tabs after index {0}</value>
<comment>{0} will be replaced with a number</comment>
</data>
<data name="TabSwitcherControlName" xml:space="preserve">
<value>Tab Switcher</value>
</data>
<data name="ToggleTabSwitcherCommandKey" xml:space="preserve">
<value>Toggle tab switcher</value>
</data>
<data name="TabSwitcher_SearchBoxText" xml:space="preserve">
<value>Type a tab name...</value>
</data>
<data name="TabSwitcher_NoMatchesText" xml:space="preserve">
<value>No matching tab name</value>
</data>
<data name="CrimsonColorButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Crimson</value>
</data>

View File

@@ -220,6 +220,11 @@ namespace winrt::TerminalApp::implementation
_CloseTabsAfterHandlers(*this, *eventArgs);
break;
}
case ShortcutAction::ToggleTabSwitcher:
{
_ToggleTabSwitcherHandlers(*this, *eventArgs);
break;
}
default:
return false;
}

View File

@@ -59,6 +59,7 @@ namespace winrt::TerminalApp::implementation
TYPED_EVENT(ExecuteCommandline, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs);
TYPED_EVENT(CloseOtherTabs, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs);
TYPED_EVENT(CloseTabsAfter, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs);
TYPED_EVENT(ToggleTabSwitcher, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs);
// clang-format on
private:

View File

@@ -44,7 +44,8 @@ namespace TerminalApp
ExecuteCommandline,
ToggleCommandPalette,
CloseOtherTabs,
CloseTabsAfter
CloseTabsAfter,
ToggleTabSwitcher
};
[default_interface] runtimeclass ActionAndArgs {
@@ -94,5 +95,6 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, ActionEventArgs> ExecuteCommandline;
event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, ActionEventArgs> CloseOtherTabs;
event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, ActionEventArgs> CloseTabsAfter;
event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, ActionEventArgs> ToggleTabSwitcher;
}
}

View File

@@ -18,6 +18,7 @@ using namespace winrt::Windows::System;
namespace winrt
{
namespace MUX = Microsoft::UI::Xaml;
namespace WUX = Windows::UI::Xaml;
}
namespace winrt::TerminalApp::implementation
@@ -225,7 +226,8 @@ namespace winrt::TerminalApp::implementation
if (auto tab{ weakThis.get() })
{
IconPath(_lastIconPath);
// The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX...
IconSource(GetColoredIcon<winrt::WUX::Controls::IconSource>(_lastIconPath));
_tabViewItem.IconSource(GetColoredIcon<winrt::MUX::Controls::IconSource>(_lastIconPath));
}
}

View File

@@ -73,7 +73,7 @@ namespace winrt::TerminalApp::implementation
DECLARE_EVENT(ColorCleared, _colorCleared, winrt::delegate<>);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Title, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, IconPath, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::Windows::UI::Xaml::Controls::IconSource, IconSource, _PropertyChangedHandlers, nullptr);
private:
std::shared_ptr<Pane> _rootPane{ nullptr };

View File

@@ -6,6 +6,6 @@ namespace TerminalApp
[default_interface] runtimeclass Tab : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
String Title { get; };
String IconPath { get; };
Windows.UI.Xaml.Controls.IconSource IconSource { get; };
}
}

View File

@@ -80,9 +80,20 @@ namespace winrt::TerminalApp::implementation
{
command.KeyChordText(KeyChordSerialization::ToString(keyChord));
}
// Set the default IconSource to a BitmapIconSource with a null source
// (instead of just nullptr) because there's a really weird crash when swapping
// data bound IconSourceElements in a ListViewTemplate (i.e. CommandPalette).
// Swapping between nullptr IconSources and non-null IconSources causes a crash
// to occur, but swapping between IconSources with a null source and non-null IconSources
// work perfectly fine :shrug:.
winrt::Windows::UI::Xaml::Controls::BitmapIconSource icon;
icon.UriSource(nullptr);
command.IconSource(icon);
commandsCollection.Append(command);
}
CommandPalette().SetActions(commandsCollection);
CommandPalette().SetCommands(commandsCollection);
}
}
@@ -211,6 +222,13 @@ namespace winrt::TerminalApp::implementation
}
});
_tabs.VectorChanged([weakThis{ get_weak() }](auto&& s, auto&& e) {
if (auto page{ weakThis.get() })
{
page->CommandPalette().OnTabsChanged(s, e);
}
});
// Once the page is actually laid out on the screen, trigger all our
// startup actions. Things like Panes need to know at least how big the
// window will be, so they can subdivide that space.
@@ -897,6 +915,7 @@ namespace winrt::TerminalApp::implementation
_actionDispatch->ExecuteCommandline({ this, &TerminalPage::_HandleExecuteCommandline });
_actionDispatch->CloseOtherTabs({ this, &TerminalPage::_HandleCloseOtherTabs });
_actionDispatch->CloseTabsAfter({ this, &TerminalPage::_HandleCloseTabsAfter });
_actionDispatch->ToggleTabSwitcher({ this, &TerminalPage::_HandleToggleTabSwitcher });
}
// Method Description:

View File

@@ -230,6 +230,7 @@ namespace winrt::TerminalApp::implementation
void _HandleToggleCommandPalette(const IInspectable& sender, const TerminalApp::ActionEventArgs& args);
void _HandleCloseOtherTabs(const IInspectable& sender, const TerminalApp::ActionEventArgs& args);
void _HandleCloseTabsAfter(const IInspectable& sender, const TerminalApp::ActionEventArgs& args);
void _HandleToggleTabSwitcher(const IInspectable& sender, const TerminalApp::ActionEventArgs& args);
// Make sure to hook new actions up in _RegisterActionCallbacks!
#pragma endregion

View File

@@ -270,3 +270,12 @@ JSON_ENUM_MAPPER(::winrt::TerminalApp::SettingsTarget)
pair_type{ "allFiles", ValueType::AllFiles },
};
};
JSON_ENUM_MAPPER(::winrt::Windows::System::VirtualKey)
{
JSON_MAPPINGS(3) = {
pair_type{ "ctrl", ValueType::Control },
pair_type{ "alt", ValueType::Menu },
pair_type{ "shift", ValueType::Shift },
};
};

View File

@@ -114,7 +114,7 @@ private: \
// private _setName() method, that the class can internally use to change the
// value when it _knows_ it doesn't need to raise the PropertyChanged event
// (like when the class is being initialized).
#define OBSERVABLE_GETSET_PROPERTY(type, name, event) \
#define OBSERVABLE_GETSET_PROPERTY(type, name, event, ...) \
public: \
type name() { return _##name; }; \
void name(const type& value) \
@@ -127,7 +127,7 @@ public:
}; \
\
private: \
const type _##name; \
const type _##name{ __VA_ARGS__ }; \
void _set##name(const type& value) \
{ \
const_cast<type&>(_##name) = value; \