New Tab Menu Customization (#13763)

Implements an initial version of #1571 as it has been specified, the
only big thing missing now is the possibility to add actions, which
depends on #6899.

Further upcoming spec tracked in #12584 

Implemented according to [instructions by @zadjii-msft]. Mostly
relatively straightforward, but some notable details:
- In accordance with the spec, the counting/indexing of profiles is
  based on their index in the json (so the index of the profile, not of
  the entry in the menu).
- Resolving a profile name to an actual profile is done in a similar
  fashion as how currently the `DefaultProfile` field is populated: the
  `CascadiaSettings` constructor now has an extra `_resolve` function
  that will iterate over all entries and resolve the names to instances;
  this same function will compute all profile sets (the set of all
  profiles from source "x", and the set of all remaining profiles)
- ~Fun~ fact: I spent two whole afternoons and evenings trying to add 2
  classes (which turned out to be a trivial `.vcxproj` error), and then
  managed to finish the entire rest of it in another afternoon and
  evening...

## Validation Steps Performed
A lot of manual testing; as mentioned above I was not able to run any
tests so I could not add them for now. However, the logic is not too
tricky so it should be relatively safe.

Closes #1571

[instructions by @zadjii-msft]: https://github.com/microsoft/terminal/issues/1571#issuecomment-1184851000
This commit is contained in:
Floris Westerman 2022-12-09 23:40:38 +01:00 committed by GitHub
parent a5f9c85c39
commit 2668273616
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1451 additions and 81 deletions

View File

@ -81,6 +81,7 @@ Thysell
Walisch
WDX
Wellons
Westerman
Wirt
Wojciech
zadjii

View File

@ -251,6 +251,7 @@ conattrs
conbufferout
concfg
conclnt
concretizations
conddkrefs
condrv
conechokey
@ -2112,6 +2113,7 @@ webpage
websites
websockets
wekyb
WEOF
wex
wextest
wextestclass

View File

@ -561,6 +561,160 @@
},
"type": "object"
},
"NewTabMenuEntryType": {
"enum": [
"source",
"profile",
"folder",
"separator",
"remainingProfiles",
"matchProfiles"
]
},
"NewTabMenuEntry": {
"properties": {
"type": {
"description": "The type of menu entry",
"$ref": "#/$defs/NewTabMenuEntryType"
}
},
"required": [
"type"
],
"type": "object"
},
"FolderEntryInlining": {
"enum": [
"never",
"auto"
]
},
"FolderEntry": {
"description": "A folder entry in the new tab dropdown",
"allOf": [
{
"$ref": "#/$defs/NewTabMenuEntry"
},
{
"properties": {
"type": {
"type": "string",
"const": "folder"
},
"name": {
"type": "string",
"default": "",
"description": "The name of the folder to show in the menu"
},
"icon": {
"$ref": "#/$defs/Icon"
},
"entries": {
"type": "array",
"description": "The entries to put inside this folder",
"minItems": 1,
"items": {
"$ref": "#/$defs/NewTabMenuEntry"
}
},
"inline": {
"description": "When set to auto and the folder only has a single entry, the entry will show directly and no folder will be rendered",
"default": "never",
"$ref": "#/$defs/FolderEntryInlining"
},
"allowEmpty": {
"description": "Whether to render a folder without entries, or to hide it",
"default": "false",
"type": "boolean"
}
}
}
]
},
"SeparatorEntry": {
"description": "A separator in the new tab dropdown",
"allOf": [
{
"$ref": "#/$defs/NewTabMenuEntry"
},
{
"properties": {
"type": {
"type": "string",
"const": "separator"
}
}
}
]
},
"ProfileEntry": {
"description": "A profile in the new tab dropdown",
"allOf": [
{
"$ref": "#/$defs/NewTabMenuEntry"
},
{
"properties": {
"type": {
"type": "string",
"const": "profile"
},
"profile": {
"type": "string",
"default": "",
"description": "The name or GUID of the profile to show in this entry"
}
}
}
]
},
"RemainingProfilesEntry": {
"description": "The set of profiles that are not yet explicitly included in another entry, such as the profile or source entries. This entry can be used at most one time!",
"allOf": [
{
"$ref": "#/$defs/NewTabMenuEntry"
},
{
"properties": {
"type": {
"type": "string",
"const": "remainingProfiles"
}
}
}
]
},
"MatchProfilesEntry": {
"description": "A set of profiles all matching the given name, source, or command line, to show in the new tab dropdown",
"allOf": [
{
"$ref": "#/$defs/NewTabMenuEntry"
},
{
"properties": {
"type": {
"type": "string",
"const": "matchProfiles"
},
"name": {
"type": "string",
"default": "",
"description": "The name of the profiles to match"
},
"source": {
"type": "string",
"default": "",
"description": "The source of the profiles to match"
},
"commandline": {
"type": "string",
"default": "",
"description": "The command line of the profiles to match"
}
}
}
]
},
"SwitchToAdjacentTabArgs": {
"oneOf": [
{
@ -1568,6 +1722,29 @@
}
]
},
"NewTabMenu": {
"description": "Defines the order and structure of the 'new tab' menu. It can consist of e.g. profiles, folders, and separators.",
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/$defs/FolderEntry"
},
{
"$ref": "#/$defs/SeparatorEntry"
},
{
"$ref": "#/$defs/ProfileEntry"
},
{
"$ref": "#/$defs/MatchProfilesEntry"
},
{
"$ref": "#/$defs/RemainingProfilesEntry"
}
]
}
},
"Keybinding": {
"additionalProperties": false,
"properties": {
@ -1946,6 +2123,9 @@
},
"type": "array"
},
"newTabMenu": {
"$ref": "#/$defs/NewTabMenu"
},
"language": {
"default": "",
"description": "Sets an override for the app's preferred language, expressed as a BCP-47 language tag like en-US.",

View File

@ -52,6 +52,7 @@ static const std::array settingsLoadWarningsLabels {
USES_RESOURCE(L"FailedToParseStartupActions"),
USES_RESOURCE(L"FailedToParseSubCommands"),
USES_RESOURCE(L"UnknownTheme"),
USES_RESOURCE(L"DuplicateRemainingProfilesEntry"),
};
static const std::array settingsLoadErrorsLabels {
USES_RESOURCE(L"NoProfilesText"),

View File

@ -232,19 +232,19 @@
<value>Warnings were found while parsing your keybindings:</value>
</data>
<data name="TooManyKeysForChord" xml:space="preserve">
<value>&#x2022; Found a keybinding with too many strings for the "keys" array. There should only be one string value in the "keys" array.</value>
<comment>{Locked="\"keys\"","&#x2022;"} This glyph is a bullet, used in a bulleted list.</comment>
<value> Found a keybinding with too many strings for the "keys" array. There should only be one string value in the "keys" array.</value>
<comment>{Locked="\"keys\"",""} This glyph is a bullet, used in a bulleted list.</comment>
</data>
<data name="FailedToParseSubCommands" xml:space="preserve">
<value>&#x2022; Failed to parse all subcommands of nested command.</value>
<value> Failed to parse all subcommands of nested command.</value>
</data>
<data name="MissingRequiredParameter" xml:space="preserve">
<value>&#x2022; Found a keybinding that was missing a required parameter value. This keybinding will be ignored.</value>
<comment>{Locked="&#x2022;"} This glyph is a bullet, used in a bulleted list.</comment>
<value> Found a keybinding that was missing a required parameter value. This keybinding will be ignored.</value>
<comment>{Locked=""} This glyph is a bullet, used in a bulleted list.</comment>
</data>
<data name="UnknownTheme" xml:space="preserve">
<value>&#x2022; The specified "theme" was not found in the list of themes. Temporarily falling back to the default value.</value>
<comment>{Locked="&#x2022;"} This glyph is a bullet, used in a bulleted list.</comment>
<value> The specified "theme" was not found in the list of themes. Temporarily falling back to the default value.</value>
<comment>{Locked=""} This glyph is a bullet, used in a bulleted list.</comment>
</data>
<data name="LegacyGlobalsProperty" xml:space="preserve">
<value>The "globals" property is deprecated - your settings might need updating. </value>
@ -752,6 +752,10 @@
<value>Suggestions found: {0}</value>
<comment>{0} will be replaced with a number.</comment>
</data>
<data name="DuplicateRemainingProfilesEntry" xml:space="preserve">
<value>The "newTabMenu" field contains more than one entry of type "remainingProfiles". Only the first one will be considered.</value>
<comment>{Locked="newTabMenu"} {Locked="remainingProfiles"}</comment>
</data>
<data name="AboutToolTip" xml:space="preserve">
<value>Open a dialog containing product information</value>
</data>
@ -788,4 +792,7 @@
<data name="TabCloseToolTip" xml:space="preserve">
<value>Close this tab</value>
</data>
</root>
<data name="NewTabMenuFolderEmpty" xml:space="preserve">
<value>Empty...</value>
</data>
</root>

View File

@ -824,79 +824,14 @@ namespace winrt::TerminalApp::implementation
auto newTabFlyout = WUX::Controls::MenuFlyout{};
newTabFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedLeft);
auto actionMap = _settings.ActionMap();
const auto defaultProfileGuid = _settings.GlobalSettings().DefaultProfile();
// the number of profiles should not change in the loop for this to work
const auto profileCount = gsl::narrow_cast<int>(_settings.ActiveProfiles().Size());
for (auto profileIndex = 0; profileIndex < profileCount; profileIndex++)
// Create profile entries from the NewTabMenu configuration using a
// recursive helper function. This returns a std::vector of FlyoutItemBases,
// that we then add to our Flyout.
auto entries = _settings.GlobalSettings().NewTabMenu();
auto items = _CreateNewTabFlyoutItems(entries);
for (const auto& item : items)
{
const auto profile = _settings.ActiveProfiles().GetAt(profileIndex);
auto profileMenuItem = WUX::Controls::MenuFlyoutItem{};
// Add the keyboard shortcuts based on the number of profiles defined
// Look for a keychord that is bound to the equivalent
// NewTab(ProfileIndex=N) action
NewTerminalArgs newTerminalArgs{ profileIndex };
NewTabArgs newTabArgs{ newTerminalArgs };
auto profileKeyChord{ actionMap.GetKeyBindingForAction(ShortcutAction::NewTab, newTabArgs) };
// make sure we find one to display
if (profileKeyChord)
{
_SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord);
}
auto profileName = profile.Name();
profileMenuItem.Text(profileName);
// If there's an icon set for this profile, set it as the icon for
// this flyout item.
if (!profile.Icon().empty())
{
auto icon = IconPathConverter::IconWUX(profile.Icon());
Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw);
profileMenuItem.Icon(icon);
}
if (profile.Guid() == defaultProfileGuid)
{
// Contrast the default profile with others in font weight.
profileMenuItem.FontWeight(FontWeights::Bold());
}
auto newTabRun = WUX::Documents::Run();
newTabRun.Text(RS_(L"NewTabRun/Text"));
auto newPaneRun = WUX::Documents::Run();
newPaneRun.Text(RS_(L"NewPaneRun/Text"));
newPaneRun.FontStyle(FontStyle::Italic);
auto newWindowRun = WUX::Documents::Run();
newWindowRun.Text(RS_(L"NewWindowRun/Text"));
newWindowRun.FontStyle(FontStyle::Italic);
auto elevatedRun = WUX::Documents::Run();
elevatedRun.Text(RS_(L"ElevatedRun/Text"));
elevatedRun.FontStyle(FontStyle::Italic);
auto textBlock = WUX::Controls::TextBlock{};
textBlock.Inlines().Append(newTabRun);
textBlock.Inlines().Append(WUX::Documents::LineBreak{});
textBlock.Inlines().Append(newPaneRun);
textBlock.Inlines().Append(WUX::Documents::LineBreak{});
textBlock.Inlines().Append(newWindowRun);
textBlock.Inlines().Append(WUX::Documents::LineBreak{});
textBlock.Inlines().Append(elevatedRun);
auto toolTip = WUX::Controls::ToolTip{};
toolTip.Content(textBlock);
WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip);
profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) {
if (auto page{ weakThis.get() })
{
NewTerminalArgs newTerminalArgs{ profileIndex };
page->_OpenNewTerminalViaDropdown(newTerminalArgs);
}
});
newTabFlyout.Items().Append(profileMenuItem);
newTabFlyout.Items().Append(item);
}
// add menu separator
@ -932,6 +867,7 @@ namespace winrt::TerminalApp::implementation
settingsItem.Click({ this, &TerminalPage::_SettingsButtonOnClick });
newTabFlyout.Items().Append(settingsItem);
auto actionMap = _settings.ActionMap();
const auto settingsKeyChord{ actionMap.GetKeyBindingForAction(ShortcutAction::OpenSettings, OpenSettingsArgs{ SettingsTarget::SettingsUI }) };
if (settingsKeyChord)
{
@ -991,6 +927,215 @@ namespace winrt::TerminalApp::implementation
_newTabButton.Flyout(newTabFlyout);
}
// Method Description:
// - For a given list of tab menu entries, this method will create the corresponding
// list of flyout items. This is a recursive method that calls itself when it comes
// across a folder entry.
std::vector<WUX::Controls::MenuFlyoutItemBase> TerminalPage::_CreateNewTabFlyoutItems(IVector<NewTabMenuEntry> entries)
{
std::vector<WUX::Controls::MenuFlyoutItemBase> items;
if (entries == nullptr || entries.Size() == 0)
{
return items;
}
for (const auto& entry : entries)
{
if (entry == nullptr)
{
continue;
}
switch (entry.Type())
{
case NewTabMenuEntryType::Separator:
{
items.push_back(WUX::Controls::MenuFlyoutSeparator{});
break;
}
// A folder has a custom name and icon, and has a number of entries that require
// us to call this method recursively.
case NewTabMenuEntryType::Folder:
{
const auto folderEntry = entry.as<FolderEntry>();
const auto folderEntries = folderEntry.Entries();
// If the folder is empty, we should skip the entry if AllowEmpty is false, or
// when the folder should inline.
// The IsEmpty check includes semantics for nested (empty) folders
if (folderEntries.Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto))
{
break;
}
// Recursively generate flyout items
auto folderEntryItems = _CreateNewTabFlyoutItems(folderEntries);
// If the folder should auto-inline and there is only one item, do so.
if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntries.Size() == 1)
{
for (auto const& folderEntryItem : folderEntryItems)
{
items.push_back(folderEntryItem);
}
break;
}
// Otherwise, create a flyout
auto folderItem = WUX::Controls::MenuFlyoutSubItem{};
folderItem.Text(folderEntry.Name());
auto icon = _CreateNewTabFlyoutIcon(folderEntry.Icon());
folderItem.Icon(icon);
for (const auto& folderEntryItem : folderEntryItems)
{
folderItem.Items().Append(folderEntryItem);
}
// If the folder is empty, and by now we know we set AllowEmpty to true,
// create a placeholder item here
if (folderEntries.Size() == 0)
{
auto placeholder = WUX::Controls::MenuFlyoutItem{};
placeholder.Text(RS_(L"NewTabMenuFolderEmpty"));
placeholder.IsEnabled(false);
folderItem.Items().Append(placeholder);
}
items.push_back(folderItem);
break;
}
// Any "collection entry" will simply make us add each profile in the collection
// separately. This collection is stored as a map <int, Profile>, so the correct
// profile index is already known.
case NewTabMenuEntryType::RemainingProfiles:
case NewTabMenuEntryType::MatchProfiles:
{
const auto remainingProfilesEntry = entry.as<ProfileCollectionEntry>();
if (remainingProfilesEntry.Profiles() == nullptr)
{
break;
}
for (auto&& [profileIndex, remainingProfile] : remainingProfilesEntry.Profiles())
{
items.push_back(_CreateNewTabFlyoutProfile(remainingProfile, profileIndex));
}
break;
}
// A single profile, the profile index is also given in the entry
case NewTabMenuEntryType::Profile:
{
const auto profileEntry = entry.as<ProfileEntry>();
if (profileEntry.Profile() == nullptr)
{
break;
}
auto profileItem = _CreateNewTabFlyoutProfile(profileEntry.Profile(), profileEntry.ProfileIndex());
items.push_back(profileItem);
break;
}
}
}
return items;
}
// Method Description:
// - This method creates a flyout menu item for a given profile with the given index.
// It makes sure to set the correct icon, keybinding, and click-action.
WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutProfile(const Profile profile, int profileIndex)
{
auto profileMenuItem = WUX::Controls::MenuFlyoutItem{};
// Add the keyboard shortcuts based on the number of profiles defined
// Look for a keychord that is bound to the equivalent
// NewTab(ProfileIndex=N) action
NewTerminalArgs newTerminalArgs{ profileIndex };
NewTabArgs newTabArgs{ newTerminalArgs };
auto profileKeyChord{ _settings.ActionMap().GetKeyBindingForAction(ShortcutAction::NewTab, newTabArgs) };
// make sure we find one to display
if (profileKeyChord)
{
_SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord);
}
auto profileName = profile.Name();
profileMenuItem.Text(profileName);
// If there's an icon set for this profile, set it as the icon for
// this flyout item
if (!profile.Icon().empty())
{
const auto icon = _CreateNewTabFlyoutIcon(profile.Icon());
profileMenuItem.Icon(icon);
}
if (profile.Guid() == _settings.GlobalSettings().DefaultProfile())
{
// Contrast the default profile with others in font weight.
profileMenuItem.FontWeight(FontWeights::Bold());
}
auto newTabRun = WUX::Documents::Run();
newTabRun.Text(RS_(L"NewTabRun/Text"));
auto newPaneRun = WUX::Documents::Run();
newPaneRun.Text(RS_(L"NewPaneRun/Text"));
newPaneRun.FontStyle(FontStyle::Italic);
auto newWindowRun = WUX::Documents::Run();
newWindowRun.Text(RS_(L"NewWindowRun/Text"));
newWindowRun.FontStyle(FontStyle::Italic);
auto elevatedRun = WUX::Documents::Run();
elevatedRun.Text(RS_(L"ElevatedRun/Text"));
elevatedRun.FontStyle(FontStyle::Italic);
auto textBlock = WUX::Controls::TextBlock{};
textBlock.Inlines().Append(newTabRun);
textBlock.Inlines().Append(WUX::Documents::LineBreak{});
textBlock.Inlines().Append(newPaneRun);
textBlock.Inlines().Append(WUX::Documents::LineBreak{});
textBlock.Inlines().Append(newWindowRun);
textBlock.Inlines().Append(WUX::Documents::LineBreak{});
textBlock.Inlines().Append(elevatedRun);
auto toolTip = WUX::Controls::ToolTip{};
toolTip.Content(textBlock);
WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip);
profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) {
if (auto page{ weakThis.get() })
{
NewTerminalArgs newTerminalArgs{ profileIndex };
page->_OpenNewTerminalViaDropdown(newTerminalArgs);
}
});
return profileMenuItem;
}
// Method Description:
// - Helper method to create an IconElement that can be passed to MenuFlyoutItems and
// MenuFlyoutSubItems
IconElement TerminalPage::_CreateNewTabFlyoutIcon(const winrt::hstring& iconSource)
{
if (iconSource.empty())
{
return nullptr;
}
auto icon = IconPathConverter::IconWUX(iconSource);
Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw);
return icon;
}
// Function Description:
// Called when the openNewTabDropdown keybinding is used.
// Shows the dropdown flyout.

View File

@ -240,6 +240,10 @@ namespace winrt::TerminalApp::implementation
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowLargePasteWarningDialog();
void _CreateNewTabFlyout();
std::vector<winrt::Windows::UI::Xaml::Controls::MenuFlyoutItemBase> _CreateNewTabFlyoutItems(winrt::Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::NewTabMenuEntry> entries);
winrt::Windows::UI::Xaml::Controls::IconElement _CreateNewTabFlyoutIcon(const winrt::hstring& icon);
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutProfile(const Microsoft::Terminal::Settings::Model::Profile profile, int profileIndex);
void _OpenNewTabDropdown();
HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
void _CreateNewTabFromPane(std::shared_ptr<Pane> pane);

View File

@ -153,6 +153,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _refreshDefaultTerminals();
void _resolveDefaultProfile() const;
void _resolveNewTabMenuProfiles() const;
void _resolveNewTabMenuProfilesSet(const winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> entries, winrt::Windows::Foundation::Collections::IMap<int, Model::Profile>& remainingProfiles, Model::RemainingProfilesEntry& remainingProfilesEntry) const;
void _validateSettings();
void _validateAllSchemesExist();

View File

@ -31,6 +31,10 @@
#include "DefaultTerminal.h"
#include "FileUtils.h"
#include "ProfileEntry.h"
#include "FolderEntry.h"
#include "MatchProfilesEntry.h"
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::ApplicationModel::AppExtensions;
using namespace winrt::Microsoft::Terminal::Settings;
@ -1104,6 +1108,7 @@ CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) :
_warnings = winrt::single_threaded_vector(std::move(warnings));
_resolveDefaultProfile();
_resolveNewTabMenuProfiles();
_validateSettings();
}
@ -1282,3 +1287,162 @@ void CascadiaSettings::_resolveDefaultProfile() const
// Use the first profile as the new default.
GlobalSettings().DefaultProfile(_allProfiles.GetAt(0).Guid());
}
// Method Description:
// - Iterates through the "newTabMenu" entries and for ProfileEntries resolves the "profile"
// fields, which can be a profile name, to a GUID and stores it back.
// - It finds any "source" entries and finds all profiles generated by that source
// - Lastly, it finds any "remainingProfiles" entries and stores which profiles they
// represent (those that were not resolved before). It adds a warning when
// multiple of these entries are found.
void CascadiaSettings::_resolveNewTabMenuProfiles() const
{
Model::RemainingProfilesEntry remainingProfilesEntry = nullptr;
// The TerminalPage needs to know which profile has which profile ID. To prevent
// continuous lookups in the _activeProfiles vector, we create a map <int, Profile>
// to store these indices in-flight.
auto remainingProfilesMap = std::map<int, Model::Profile>{};
auto activeProfileCount = gsl::narrow_cast<int>(_activeProfiles.Size());
for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
{
remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex));
}
// We keep track of the "remaining profiles" - those that have not yet been resolved
// in either a "profile" or "source" entry. They will possibly be assigned to a
// "remainingProfiles" entry
auto remainingProfiles = single_threaded_map(std::move(remainingProfilesMap));
// We call a recursive helper function to process the entries
auto entries = _globals->NewTabMenu();
_resolveNewTabMenuProfilesSet(entries, remainingProfiles, remainingProfilesEntry);
// If a "remainingProfiles" entry has been found, assign to it the remaining profiles
if (remainingProfilesEntry != nullptr)
{
remainingProfilesEntry.Profiles(remainingProfiles);
}
// If the configuration does not have a "newTabMenu" field, GlobalAppSettings
// will return a default value containing just a "remainingProfiles" entry. However,
// this value is regenerated on every "get" operation, so the effect of setting
// the remaining profiles above will be undone. So only in the case that no custom
// value is present in GlobalAppSettings, we will store the modified default value.
if (!_globals->HasNewTabMenu())
{
_globals->NewTabMenu(entries);
}
}
// Method Description:
// - Helper function that processes a set of tab menu entries and resolves any profile names
// or source fields as necessary - see function above for a more detailed explanation.
void CascadiaSettings::_resolveNewTabMenuProfilesSet(const IVector<Model::NewTabMenuEntry> entries, IMap<int, Model::Profile>& remainingProfilesMap, Model::RemainingProfilesEntry& remainingProfilesEntry) const
{
if (entries == nullptr || entries.Size() == 0)
{
return;
}
for (const auto& entry : entries)
{
if (entry == nullptr)
{
continue;
}
switch (entry.Type())
{
// For a simple profile entry, the "profile" field can either be a name or a GUID. We
// use the GetProfileByName function to resolve this name to a profile instance, then
// find the index of that profile, and store this information in the entry.
case NewTabMenuEntryType::Profile:
{
// We need to access the unresolved profile name, a field that is not exposed
// in the projected class. So, we need to first obtain our implementation struct
// instance, to access this field.
const auto profileEntry{ winrt::get_self<implementation::ProfileEntry>(entry.as<Model::ProfileEntry>()) };
// Find the profile by name
const auto profile = GetProfileByName(profileEntry->ProfileName());
// If not found, or if the profile is hidden, skip it
if (profile == nullptr || profile.Hidden())
{
profileEntry->Profile(nullptr); // override "default" profile
break;
}
// Find the index of the resulting profile and store the result in the entry
uint32_t profileIndex;
_activeProfiles.IndexOf(profile, profileIndex);
profileEntry->Profile(profile);
profileEntry->ProfileIndex(profileIndex);
// Remove from remaining profiles list (map)
remainingProfilesMap.TryRemove(profileIndex);
break;
}
// For a remainingProfiles entry, we store it in the variable that is passed back to our caller,
// except when that one has already been set (so we found a second/third/...) instance, which will
// trigger a warning. We then ignore this entry.
case NewTabMenuEntryType::RemainingProfiles:
{
if (remainingProfilesEntry != nullptr)
{
_warnings.Append(SettingsLoadWarnings::DuplicateRemainingProfilesEntry);
}
else
{
remainingProfilesEntry = entry.as<Model::RemainingProfilesEntry>();
}
break;
}
// For a folder, we simply call this method recursively
case NewTabMenuEntryType::Folder:
{
// We need to access the unfiltered entry list, a field that is not exposed
// in the projected class. So, we need to first obtain our implementation struct
// instance, to access this field.
const auto folderEntry{ winrt::get_self<implementation::FolderEntry>(entry.as<Model::FolderEntry>()) };
auto folderEntries = folderEntry->RawEntries();
_resolveNewTabMenuProfilesSet(folderEntries, remainingProfilesMap, remainingProfilesEntry);
break;
}
// For a "matchProfiles" entry, we iterate through the list of all profiles and
// find all those matching: generated by the same source, having the same name, or
// having the same commandline. This can be expanded with regex support in the future.
// We make sure that none of the matches are included in the "remaining profiles" section.
case NewTabMenuEntryType::MatchProfiles:
{
// We need to access the matching function, which is not exposed in the projected class.
// So, we need to first obtain our implementation struct instance, to access this field.
const auto matchEntry{ winrt::get_self<implementation::MatchProfilesEntry>(entry.as<Model::MatchProfilesEntry>()) };
matchEntry->Profiles(single_threaded_map<int, Model::Profile>());
auto activeProfileCount = gsl::narrow_cast<int>(_activeProfiles.Size());
for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
{
const auto profile = _activeProfiles.GetAt(profileIndex);
// On a match, we store it in the entry and remove it from the remaining list
if (matchEntry->MatchesProfile(profile))
{
matchEntry->Profiles().Insert(profileIndex, profile);
remainingProfilesMap.TryRemove(profileIndex);
}
}
break;
}
}
}
}

View File

@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "FolderEntry.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
#include "FolderEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
using namespace winrt::Windows::Foundation::Collections;
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view IconKey{ "icon" };
static constexpr std::string_view EntriesKey{ "entries" };
static constexpr std::string_view InliningKey{ "inline" };
static constexpr std::string_view AllowEmptyKey{ "allowEmpty" };
FolderEntry::FolderEntry() noexcept :
FolderEntry{ winrt::hstring{} }
{
}
FolderEntry::FolderEntry(const winrt::hstring& name) noexcept :
FolderEntryT<FolderEntry, NewTabMenuEntry>(NewTabMenuEntryType::Folder),
_Name{ name }
{
}
Json::Value FolderEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
JsonUtils::SetValueForKey(json, NameKey, _Name);
JsonUtils::SetValueForKey(json, IconKey, _Icon);
JsonUtils::SetValueForKey(json, EntriesKey, _Entries);
JsonUtils::SetValueForKey(json, InliningKey, _Inlining);
JsonUtils::SetValueForKey(json, AllowEmptyKey, _AllowEmpty);
return json;
}
winrt::com_ptr<NewTabMenuEntry> FolderEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<FolderEntry>();
JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
JsonUtils::GetValueForKey(json, EntriesKey, entry->_Entries);
JsonUtils::GetValueForKey(json, InliningKey, entry->_Inlining);
JsonUtils::GetValueForKey(json, AllowEmptyKey, entry->_AllowEmpty);
return entry;
}
// A FolderEntry should only expose the entries to actually render to WinRT,
// to keep the logic for collapsing/expanding more centralised.
using NewTabMenuEntryModel = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntry;
IVector<NewTabMenuEntryModel> FolderEntry::Entries() const
{
// We filter the full list of entries from JSON to just include the
// non-empty ones.
IVector<NewTabMenuEntryModel> result{ winrt::single_threaded_vector<NewTabMenuEntryModel>() };
for (const auto& entry : _Entries)
{
if (entry == nullptr)
{
continue;
}
switch (entry.Type())
{
case NewTabMenuEntryType::Invalid:
continue;
// A profile is filtered out if it is not valid, so if it was not resolved
case NewTabMenuEntryType::Profile:
{
const auto profileEntry = entry.as<ProfileEntry>();
if (profileEntry.Profile() == nullptr)
{
continue;
}
break;
}
// Any profile collection is filtered out if there are no results
case NewTabMenuEntryType::RemainingProfiles:
case NewTabMenuEntryType::MatchProfiles:
{
const auto profileCollectionEntry = entry.as<ProfileCollectionEntry>();
if (profileCollectionEntry.Profiles().Size() == 0)
{
continue;
}
break;
}
// A folder is filtered out if it has an effective size of 0 (calling
// this filtering method recursively), and if it is not allowed to be
// empty, or if it should auto-inline.
case NewTabMenuEntryType::Folder:
{
const auto folderEntry = entry.as<Model::FolderEntry>();
if (folderEntry.Entries().Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto))
{
continue;
}
break;
}
}
result.Append(entry);
}
return result;
}

View File

@ -0,0 +1,55 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- FolderEntry.h
Abstract:
- A folder entry in the "new tab" dropdown menu,
Author(s):
- Floris Westerman - August 2022
--*/
#pragma once
#include "pch.h"
#include "NewTabMenuEntry.h"
#include "FolderEntry.g.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
struct FolderEntry : FolderEntryT<FolderEntry, NewTabMenuEntry>
{
public:
FolderEntry() noexcept;
explicit FolderEntry(const winrt::hstring& name) noexcept;
Json::Value ToJson() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
// In JSON, we can set arbitrarily many profiles or nested profiles, that might not all
// be rendered; for example, when a profile entry is invalid, or when a folder is empty.
// Therefore, we will store the JSON entries list internally, and then expose only the
// entries to be rendered to WinRT.
winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> Entries() const;
winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> RawEntries() const
{
return _Entries;
};
WINRT_PROPERTY(winrt::hstring, Name);
WINRT_PROPERTY(winrt::hstring, Icon);
WINRT_PROPERTY(FolderEntryInlining, Inlining, FolderEntryInlining::Never);
WINRT_PROPERTY(bool, AllowEmpty, false);
private:
winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> _Entries{};
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
{
BASIC_FACTORY(FolderEntry);
}

View File

@ -23,6 +23,8 @@ Author(s):
#include "Command.h"
#include "ColorScheme.h"
#include "Theme.h"
#include "NewTabMenuEntry.h"
#include "RemainingProfilesEntry.h"
// fwdecl unittest classes
namespace SettingsModelLocalTests

View File

@ -6,6 +6,7 @@
import "Theme.idl";
import "ColorScheme.idl";
import "ActionMap.idl";
import "NewTabMenuEntry.idl";
namespace Microsoft.Terminal.Settings.Model
{
@ -94,6 +95,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_SETTING(Boolean, AlwaysShowNotificationIcon);
INHERITABLE_SETTING(IVector<String>, DisabledProfileSources);
INHERITABLE_SETTING(Boolean, ShowAdminShield);
INHERITABLE_SETTING(IVector<NewTabMenuEntry>, NewTabMenu);
INHERITABLE_SETTING(Boolean, EnableColorSelection);
Windows.Foundation.Collections.IMapView<String, ColorScheme> ColorSchemes();

View File

@ -61,7 +61,8 @@ Author(s):
X(winrt::Windows::Foundation::Collections::IVector<winrt::hstring>, DisabledProfileSources, "disabledProfileSources", nullptr) \
X(bool, ShowAdminShield, "showAdminShield", true) \
X(bool, TrimPaste, "trimPaste", true) \
X(bool, EnableColorSelection, "experimental.enableColorSelection", false)
X(bool, EnableColorSelection, "experimental.enableColorSelection", false) \
X(winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>, NewTabMenu, "newTabMenu", winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} }))
#define MTSM_PROFILE_SETTINGS(X) \
X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE) \

View File

@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "MatchProfilesEntry.h"
#include "JsonUtils.h"
#include "MatchProfilesEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view CommandlineKey{ "commandline" };
static constexpr std::string_view SourceKey{ "source" };
MatchProfilesEntry::MatchProfilesEntry() noexcept :
MatchProfilesEntryT<MatchProfilesEntry, ProfileCollectionEntry>(NewTabMenuEntryType::MatchProfiles)
{
}
Json::Value MatchProfilesEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
JsonUtils::SetValueForKey(json, NameKey, _Name);
JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline);
JsonUtils::SetValueForKey(json, SourceKey, _Source);
return json;
}
winrt::com_ptr<NewTabMenuEntry> MatchProfilesEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<MatchProfilesEntry>();
JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline);
JsonUtils::GetValueForKey(json, SourceKey, entry->_Source);
return entry;
}
bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile)
{
// We use an optional here instead of a simple bool directly, since there is no
// sensible default value for the desired semantics: the first property we want
// to match on should always be applied (so one would set "true" as a default),
// but if none of the properties are set, the default return value should be false
// since this entry type is expected to behave like a positive match/whitelist.
//
// The easiest way to deal with this neatly is to use an optional, then for any
// property to match we consider a null value to be "true", and for the return
// value of the function we consider the null value to be "false".
auto isMatching = std::optional<bool>{};
if (!_Name.empty())
{
isMatching = { isMatching.value_or(true) && _Name == profile.Name() };
}
if (!_Source.empty())
{
isMatching = { isMatching.value_or(true) && _Source == profile.Source() };
}
if (!_Commandline.empty())
{
isMatching = { isMatching.value_or(true) && _Commandline == profile.Commandline() };
}
return isMatching.value_or(false);
}

View File

@ -0,0 +1,42 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- MatchProfilesEntry.h
Abstract:
- An entry in the "new tab" dropdown menu that represents a collection
of profiles that match a specified name, source, or command line.
Author(s):
- Floris Westerman - November 2022
--*/
#pragma once
#include "ProfileCollectionEntry.h"
#include "MatchProfilesEntry.g.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
struct MatchProfilesEntry : MatchProfilesEntryT<MatchProfilesEntry, ProfileCollectionEntry>
{
public:
MatchProfilesEntry() noexcept;
Json::Value ToJson() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
bool MatchesProfile(const Model::Profile& profile);
WINRT_PROPERTY(winrt::hstring, Name);
WINRT_PROPERTY(winrt::hstring, Commandline);
WINRT_PROPERTY(winrt::hstring, Source);
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
{
BASIC_FACTORY(MatchProfilesEntry);
}

View File

@ -21,6 +21,27 @@
<!-- ========================= Headers ======================== -->
<ItemGroup>
<ClInclude Include="ActionArgsMagic.h" />
<ClInclude Include="NewTabMenuEntry.h">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClInclude>
<ClInclude Include="SeparatorEntry.h">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClInclude>
<ClInclude Include="FolderEntry.h">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClInclude>
<ClInclude Include="ProfileEntry.h">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClInclude>
<ClInclude Include="RemainingProfilesEntry.h">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClInclude>
<ClInclude Include="ProfileCollectionEntry.h">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClInclude>
<ClInclude Include="MatchProfilesEntry.h">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClInclude>
<ClInclude Include="VisualStudioGenerator.h" />
<ClInclude Include="DefaultTerminal.h">
<DependentUpon>DefaultTerminal.idl</DependentUpon>
@ -163,6 +184,27 @@
<ClCompile Include="EnumMappings.cpp">
<DependentUpon>EnumMappings.idl</DependentUpon>
</ClCompile>
<ClCompile Include="NewTabMenuEntry.cpp">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClCompile>
<ClCompile Include="SeparatorEntry.cpp">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClCompile>
<ClCompile Include="FolderEntry.cpp">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClCompile>
<ClCompile Include="ProfileEntry.cpp">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClCompile>
<ClCompile Include="ProfileCollectionEntry.cpp">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClCompile>
<ClCompile Include="RemainingProfilesEntry.cpp">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClCompile>
<ClCompile Include="MatchProfilesEntry.cpp">
<DependentUpon>NewTabMenuEntry.idl</DependentUpon>
</ClCompile>
<ClCompile Include="VsDevCmdGenerator.cpp" />
<ClCompile Include="VsDevShellGenerator.cpp" />
<ClCompile Include="VsSetupConfiguration.cpp" />
@ -182,6 +224,7 @@
<Midl Include="ApplicationState.idl" />
<Midl Include="CascadiaSettings.idl" />
<Midl Include="ColorScheme.idl" />
<Midl Include="NewTabMenuEntry.idl" />
<Midl Include="Theme.idl" />
<Midl Include="Command.idl" />
<Midl Include="DefaultTerminal.idl" />

View File

@ -100,6 +100,7 @@
<Filter>profileGeneration</Filter>
</ClInclude>
<ClInclude Include="ActionArgsMagic.h" />
<ClInclude Include="NewTabMenuEntry.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="ActionArgs.idl" />
@ -119,6 +120,7 @@
<Midl Include="FontConfig.idl" />
<Midl Include="DefaultTerminal.idl" />
<Midl Include="ApplicationState.idl" />
<Midl Include="NewTabMenuEntry.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "NewTabMenuEntry.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
#include "SeparatorEntry.h"
#include "FolderEntry.h"
#include "ProfileEntry.h"
#include "RemainingProfilesEntry.h"
#include "MatchProfilesEntry.h"
#include "NewTabMenuEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
using NewTabMenuEntryType = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntryType;
static constexpr std::string_view TypeKey{ "type" };
NewTabMenuEntry::NewTabMenuEntry(const NewTabMenuEntryType type) noexcept :
_Type{ type }
{
}
// This method will be overridden by the subclasses, which will then call this
// parent implementation for a "base" json object.
Json::Value NewTabMenuEntry::ToJson() const
{
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, TypeKey, _Type);
return json;
}
// Deserialize the JSON object based on the given type. We use the map from above for that.
winrt::com_ptr<NewTabMenuEntry> NewTabMenuEntry::FromJson(const Json::Value& json)
{
const auto type = JsonUtils::GetValueForKey<NewTabMenuEntryType>(json, TypeKey);
switch (type)
{
case NewTabMenuEntryType::Separator:
return SeparatorEntry::FromJson(json);
case NewTabMenuEntryType::Folder:
return FolderEntry::FromJson(json);
case NewTabMenuEntryType::Profile:
return ProfileEntry::FromJson(json);
case NewTabMenuEntryType::RemainingProfiles:
return RemainingProfilesEntry::FromJson(json);
case NewTabMenuEntryType::MatchProfiles:
return MatchProfilesEntry::FromJson(json);
default:
return nullptr;
}
}

View File

@ -0,0 +1,72 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- NewTabMenuEntry.h
Abstract:
- An entry in the "new tab" dropdown menu. These entries exist in a few varieties,
such as separators, folders, or profile links.
Author(s):
- Floris Westerman - August 2022
--*/
#pragma once
#include "NewTabMenuEntry.g.h"
#include "JsonUtils.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
struct NewTabMenuEntry : NewTabMenuEntryT<NewTabMenuEntry>
{
public:
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
virtual Json::Value ToJson() const;
WINRT_PROPERTY(NewTabMenuEntryType, Type, NewTabMenuEntryType::Invalid);
// We have a protected/hidden constructor so consumers cannot instantiate
// this base class directly and need to go through either FromJson
// or one of the subclasses.
protected:
explicit NewTabMenuEntry(const NewTabMenuEntryType type) noexcept;
};
}
namespace Microsoft::Terminal::Settings::Model::JsonUtils
{
using namespace winrt::Microsoft::Terminal::Settings::Model;
template<>
struct ConversionTrait<NewTabMenuEntry>
{
NewTabMenuEntry FromJson(const Json::Value& json)
{
const auto entry = implementation::NewTabMenuEntry::FromJson(json);
if (entry == nullptr)
{
return nullptr;
}
return *entry;
}
bool CanConvert(const Json::Value& json) const
{
return json.isObject();
}
Json::Value ToJson(const NewTabMenuEntry& val)
{
return winrt::get_self<implementation::NewTabMenuEntry>(val)->ToJson();
}
std::string TypeDescription() const
{
return "NewTabMenuEntry";
}
};
}

View File

@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "Profile.idl";
namespace Microsoft.Terminal.Settings.Model
{
enum NewTabMenuEntryType
{
Invalid = 0,
Profile,
Separator,
Folder,
RemainingProfiles,
MatchProfiles
};
[default_interface] unsealed runtimeclass NewTabMenuEntry
{
NewTabMenuEntryType Type;
}
[default_interface] runtimeclass SeparatorEntry : NewTabMenuEntry
{
SeparatorEntry();
}
[default_interface] runtimeclass ProfileEntry : NewTabMenuEntry
{
ProfileEntry();
ProfileEntry(String profile);
Profile Profile;
Int32 ProfileIndex;
}
enum FolderEntryInlining
{
Never = 0,
Auto
};
[default_interface] runtimeclass FolderEntry : NewTabMenuEntry
{
FolderEntry();
FolderEntry(String name);
String Name;
String Icon;
FolderEntryInlining Inlining;
Boolean AllowEmpty;
IVector<NewTabMenuEntry> Entries();
}
[default_interface] unsealed runtimeclass ProfileCollectionEntry : NewTabMenuEntry
{
IMap<Int32, Profile> Profiles;
}
[default_interface] runtimeclass RemainingProfilesEntry : ProfileCollectionEntry
{
RemainingProfilesEntry();
}
[default_interface] runtimeclass MatchProfilesEntry : ProfileCollectionEntry
{
MatchProfilesEntry();
String Name;
String Commandline;
String Source;
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ProfileCollectionEntry.h"
#include "JsonUtils.h"
#include "ProfileCollectionEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
ProfileCollectionEntry::ProfileCollectionEntry(const NewTabMenuEntryType type) noexcept :
ProfileCollectionEntryT<ProfileCollectionEntry, NewTabMenuEntry>(type)
{
}

View File

@ -0,0 +1,39 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- SeparatorEntry.h
Abstract:
- An entry in the "new tab" dropdown menu that represents some collection
of profiles. This is an abstract class that has concretizations like
"all profiles from a source" or "all remaining profiles"
Author(s):
- Floris Westerman - August 2022
--*/
#pragma once
#include "NewTabMenuEntry.h"
#include "ProfileCollectionEntry.g.h"
#include "Profile.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
struct ProfileCollectionEntry : ProfileCollectionEntryT<ProfileCollectionEntry, NewTabMenuEntry>
{
public:
// Since a comma does not work very nicely in a macro and we need one
// for our map definition, we use a macro te define a comma.
#define COMMA ,
WINRT_PROPERTY(winrt::Windows::Foundation::Collections::IMap<int COMMA Model::Profile>, Profiles);
#undef COMMA
// We have a protected/hidden constructor so consumers cannot instantiate
// this class directly and need to go through one of the subclasses.
protected:
explicit ProfileCollectionEntry(const NewTabMenuEntryType type) noexcept;
};
}

View File

@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ProfileEntry.h"
#include "JsonUtils.h"
#include "ProfileEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::string_view ProfileKey{ "profile" };
ProfileEntry::ProfileEntry() noexcept :
ProfileEntry{ winrt::hstring{} }
{
}
ProfileEntry::ProfileEntry(const winrt::hstring& profile) noexcept :
ProfileEntryT<ProfileEntry, NewTabMenuEntry>(NewTabMenuEntryType::Profile),
_ProfileName{ profile }
{
}
Json::Value ProfileEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
// We will now return a profile reference to the JSON representation. Logic is
// as follows:
// - When Profile is null, this is typically because an existing profile menu entry
// in the JSON config is invalid (nonexistent or hidden profile). Then, we store
// the original profile string value as read from JSON, to improve portability
// of the settings file and limit modifications to the JSON.
// - Otherwise, we always store the GUID of the profile. This will effectively convert
// all name-matched profiles from the settings file to GUIDs. This might be unexpected
// to some users, but is less error-prone and will continue to work when profile
// names are changed.
if (_Profile == nullptr)
{
JsonUtils::SetValueForKey(json, ProfileKey, _ProfileName);
}
else
{
JsonUtils::SetValueForKey(json, ProfileKey, _Profile.Guid());
}
return json;
}
winrt::com_ptr<NewTabMenuEntry> ProfileEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<ProfileEntry>();
JsonUtils::GetValueForKey(json, ProfileKey, entry->_ProfileName);
return entry;
}

View File

@ -0,0 +1,53 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- FolderEntry.h
Abstract:
- A profile entry in the "new tab" dropdown menu, referring to
a single profile.
Author(s):
- Floris Westerman - August 2022
--*/
#pragma once
#include "NewTabMenuEntry.h"
#include "ProfileEntry.g.h"
#include "Profile.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
struct ProfileEntry : ProfileEntryT<ProfileEntry, NewTabMenuEntry>
{
public:
ProfileEntry() noexcept;
explicit ProfileEntry(const winrt::hstring& profile) noexcept;
Json::Value ToJson() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
// In JSON, only a profile name (guid or string) can be set;
// but the consumers of this class would like to have direct access
// to the appropriate Model::Profile. Therefore, we have a read-only
// property ProfileName that corresponds to the JSON value, and
// then CascadiaSettings::_resolveNewTabMenuProfiles() will populate
// the Profile and ProfileIndex properties appropriately
winrt::hstring ProfileName() const noexcept { return _ProfileName; };
WINRT_PROPERTY(Model::Profile, Profile);
WINRT_PROPERTY(int, ProfileIndex);
private:
winrt::hstring _ProfileName;
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
{
BASIC_FACTORY(ProfileEntry);
}

View File

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "RemainingProfilesEntry.h"
#include "NewTabMenuEntry.h"
#include "JsonUtils.h"
#include "RemainingProfilesEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
RemainingProfilesEntry::RemainingProfilesEntry() noexcept :
RemainingProfilesEntryT<RemainingProfilesEntry, ProfileCollectionEntry>(NewTabMenuEntryType::RemainingProfiles)
{
}
winrt::com_ptr<NewTabMenuEntry> RemainingProfilesEntry::FromJson(const Json::Value&)
{
return winrt::make_self<RemainingProfilesEntry>();
}

View File

@ -0,0 +1,35 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- SeparatorEntry.h
Abstract:
- An entry in the "new tab" dropdown menu that represents all profiles
that were not included explicitly elsewhere
Author(s):
- Floris Westerman - August 2022
--*/
#pragma once
#include "ProfileCollectionEntry.h"
#include "RemainingProfilesEntry.g.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
struct RemainingProfilesEntry : RemainingProfilesEntryT<RemainingProfilesEntry, ProfileCollectionEntry>
{
public:
RemainingProfilesEntry() noexcept;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
{
BASIC_FACTORY(RemainingProfilesEntry);
}

View File

@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "SeparatorEntry.h"
#include "JsonUtils.h"
#include "SeparatorEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
SeparatorEntry::SeparatorEntry() noexcept :
SeparatorEntryT<SeparatorEntry, NewTabMenuEntry>(NewTabMenuEntryType::Separator)
{
}
winrt::com_ptr<NewTabMenuEntry> SeparatorEntry::FromJson(const Json::Value&)
{
return winrt::make_self<SeparatorEntry>();
}

View File

@ -0,0 +1,34 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- SeparatorEntry.h
Abstract:
- A separator entry in the "new tab" dropdown menu
Author(s):
- Floris Westerman - August 2022
--*/
#pragma once
#include "NewTabMenuEntry.h"
#include "SeparatorEntry.g.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
struct SeparatorEntry : SeparatorEntryT<SeparatorEntry, NewTabMenuEntry>
{
public:
SeparatorEntry() noexcept;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
{
BASIC_FACTORY(SeparatorEntry);
}

View File

@ -651,6 +651,27 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollToMarkDirection)
};
};
// Possible NewTabMenuEntryType values
JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntryType)
{
JSON_MAPPINGS(5) = {
pair_type{ "profile", ValueType::Profile },
pair_type{ "separator", ValueType::Separator },
pair_type{ "folder", ValueType::Folder },
pair_type{ "remainingProfiles", ValueType::RemainingProfiles },
pair_type{ "matchProfiles", ValueType::MatchProfiles },
};
};
// Possible FolderEntryInlining values
JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FolderEntryInlining)
{
JSON_MAPPINGS(2) = {
pair_type{ "never", ValueType::Never },
pair_type{ "auto", ValueType::Auto },
};
};
template<>
struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<::winrt::Microsoft::Terminal::Control::SelectionColor>
{

View File

@ -22,6 +22,7 @@ namespace Microsoft.Terminal.Settings.Model
FailedToParseStartupActions,
FailedToParseSubCommands,
UnknownTheme,
DuplicateRemainingProfilesEntry,
WARNINGS_SIZE // IMPORTANT: This MUST be the last value in this enum. It's an unused placeholder.
};

View File

@ -38,6 +38,25 @@
<ClInclude Include="../Profile.h" />
<ClInclude Include="../TerminalWarnings.h" />
<ClInclude Include="../IconPathConverter.h" />
<ClInclude Include="../NewTabMenuEntry.h" />
<ClInclude Include="../SeparatorEntry.h">
<DependentUpon>../NewTabMenuEntry.h</DependentUpon>
</ClInclude>
<ClInclude Include="../FolderEntry.h">
<DependentUpon>../NewTabMenuEntry.h</DependentUpon>
</ClInclude>
<ClInclude Include="../ProfileEntry.h">
<DependentUpon>../NewTabMenuEntry.h</DependentUpon>
</ClInclude>
<ClInclude Include="../RemainingProfilesEntry.h">
<DependentUpon>../NewTabMenuEntry.h</DependentUpon>
</ClInclude>
<ClInclude Include="../ProfileCollectionEntry.h">
<DependentUpon>../NewTabMenuEntry.h</DependentUpon>
</ClInclude>
<ClInclude Include="../MatchProfilesEntry.h">
<DependentUpon>../NewTabMenuEntry.h</DependentUpon>
</ClInclude>
</ItemGroup>
<!-- ========================= Cpp Files ======================== -->
<!-- Don't put source files in here - put them in the lib's vcxproj instead! -->