diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt index 56ae1aa03a..adac99c60a 100644 --- a/.github/actions/spelling/allow/names.txt +++ b/.github/actions/spelling/allow/names.txt @@ -81,6 +81,7 @@ Thysell Walisch WDX Wellons +Westerman Wirt Wojciech zadjii diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index c9e694e0b4..6bee01dfa5 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -251,6 +251,7 @@ conattrs conbufferout concfg conclnt +concretizations conddkrefs condrv conechokey @@ -2112,6 +2113,7 @@ webpage websites websockets wekyb +WEOF wex wextest wextestclass diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index e20f854568..a92ed350d1 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -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.", diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 040c39d29d..4381e24e0b 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -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"), diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index d6a6772b69..fa5194db7a 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -232,19 +232,19 @@ Warnings were found while parsing your keybindings: - • Found a keybinding with too many strings for the "keys" array. There should only be one string value in the "keys" array. - {Locked="\"keys\"","•"} This glyph is a bullet, used in a bulleted list. + • Found a keybinding with too many strings for the "keys" array. There should only be one string value in the "keys" array. + {Locked="\"keys\"","•"} This glyph is a bullet, used in a bulleted list. - • Failed to parse all subcommands of nested command. + • Failed to parse all subcommands of nested command. - • Found a keybinding that was missing a required parameter value. This keybinding will be ignored. - {Locked="•"} This glyph is a bullet, used in a bulleted list. + • Found a keybinding that was missing a required parameter value. This keybinding will be ignored. + {Locked="•"} This glyph is a bullet, used in a bulleted list. - • The specified "theme" was not found in the list of themes. Temporarily falling back to the default value. - {Locked="•"} This glyph is a bullet, used in a bulleted list. + • The specified "theme" was not found in the list of themes. Temporarily falling back to the default value. + {Locked="•"} This glyph is a bullet, used in a bulleted list. The "globals" property is deprecated - your settings might need updating. @@ -752,6 +752,10 @@ Suggestions found: {0} {0} will be replaced with a number. + + The "newTabMenu" field contains more than one entry of type "remainingProfiles". Only the first one will be considered. + {Locked="newTabMenu"} {Locked="remainingProfiles"} + Open a dialog containing product information @@ -788,4 +792,7 @@ Close this tab - + + Empty... + + \ No newline at end of file diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 564f99379d..f03c6101a1 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -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(_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 TerminalPage::_CreateNewTabFlyoutItems(IVector entries) + { + std::vector 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(); + 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 , so the correct + // profile index is already known. + case NewTabMenuEntryType::RemainingProfiles: + case NewTabMenuEntryType::MatchProfiles: + { + const auto remainingProfilesEntry = entry.as(); + 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(); + 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. diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 519d8786ea..5a0ad22f18 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -240,6 +240,10 @@ namespace winrt::TerminalApp::implementation winrt::Windows::Foundation::IAsyncOperation _ShowLargePasteWarningDialog(); void _CreateNewTabFlyout(); + std::vector _CreateNewTabFlyoutItems(winrt::Windows::Foundation::Collections::IVector 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); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index d8543e74b5..ac9049b4b1 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -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 entries, winrt::Windows::Foundation::Collections::IMap& remainingProfiles, Model::RemainingProfilesEntry& remainingProfilesEntry) const; void _validateSettings(); void _validateAllSchemesExist(); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 39124e2f0d..bc59792677 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -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 + // to store these indices in-flight. + auto remainingProfilesMap = std::map{}; + auto activeProfileCount = gsl::narrow_cast(_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 entries, IMap& 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(entry.as()) }; + + // 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(); + } + 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(entry.as()) }; + + 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(entry.as()) }; + + matchEntry->Profiles(single_threaded_map()); + + auto activeProfileCount = gsl::narrow_cast(_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; + } + } + } +} diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.cpp b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp new file mode 100644 index 0000000000..54af1d8c77 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp @@ -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(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 FolderEntry::FromJson(const Json::Value& json) +{ + auto entry = winrt::make_self(); + + 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 FolderEntry::Entries() const +{ + // We filter the full list of entries from JSON to just include the + // non-empty ones. + IVector result{ winrt::single_threaded_vector() }; + + 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(); + 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(); + 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(); + if (folderEntry.Entries().Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto)) + { + continue; + } + break; + } + } + + result.Append(entry); + } + + return result; +} diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.h b/src/cascadia/TerminalSettingsModel/FolderEntry.h new file mode 100644 index 0000000000..619d228fa7 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/FolderEntry.h @@ -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 + { + public: + FolderEntry() noexcept; + explicit FolderEntry(const winrt::hstring& name) noexcept; + + Json::Value ToJson() const override; + static com_ptr 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 Entries() const; + winrt::Windows::Foundation::Collections::IVector 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 _Entries{}; + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(FolderEntry); +} diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index e90253a28c..4a594948eb 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -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 diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index a6ff9c188c..06931641e4 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -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, DisabledProfileSources); INHERITABLE_SETTING(Boolean, ShowAdminShield); + INHERITABLE_SETTING(IVector, NewTabMenu); INHERITABLE_SETTING(Boolean, EnableColorSelection); Windows.Foundation.Collections.IMapView ColorSchemes(); diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 335fc7ad86..37029ec71a 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -61,7 +61,8 @@ Author(s): X(winrt::Windows::Foundation::Collections::IVector, 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, NewTabMenu, "newTabMenu", winrt::single_threaded_vector({ Model::RemainingProfilesEntry{} })) #define MTSM_PROFILE_SETTINGS(X) \ X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE) \ diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp new file mode 100644 index 0000000000..2873679018 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp @@ -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(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 MatchProfilesEntry::FromJson(const Json::Value& json) +{ + auto entry = winrt::make_self(); + + 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{}; + + 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); +} diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h new file mode 100644 index 0000000000..464a6780c1 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h @@ -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 + { + public: + MatchProfilesEntry() noexcept; + + Json::Value ToJson() const override; + static com_ptr 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); +} diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj index 6231c3664a..06d89dcf6a 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj @@ -21,6 +21,27 @@ + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + DefaultTerminal.idl @@ -163,6 +184,27 @@ EnumMappings.idl + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + @@ -182,6 +224,7 @@ + diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters index 3a4aab8232..cd38077aee 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters @@ -100,6 +100,7 @@ profileGeneration + @@ -119,6 +120,7 @@ + diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp new file mode 100644 index 0000000000..e346d9711c --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp @@ -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::FromJson(const Json::Value& json) +{ + const auto type = JsonUtils::GetValueForKey(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; + } +} diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h new file mode 100644 index 0000000000..57f6bc1a00 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h @@ -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 + { + public: + static com_ptr 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 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(val)->ToJson(); + } + + std::string TypeDescription() const + { + return "NewTabMenuEntry"; + } + }; +} diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl new file mode 100644 index 0000000000..64a3b1fdd9 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl @@ -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 Entries(); + } + + [default_interface] unsealed runtimeclass ProfileCollectionEntry : NewTabMenuEntry + { + IMap Profiles; + } + + [default_interface] runtimeclass RemainingProfilesEntry : ProfileCollectionEntry + { + RemainingProfilesEntry(); + } + + [default_interface] runtimeclass MatchProfilesEntry : ProfileCollectionEntry + { + MatchProfilesEntry(); + + String Name; + String Commandline; + String Source; + } +} diff --git a/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp new file mode 100644 index 0000000000..430f415301 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp @@ -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(type) +{ +} diff --git a/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h new file mode 100644 index 0000000000..86cfa72344 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h @@ -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 + { + 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, 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; + }; +} diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp new file mode 100644 index 0000000000..c857eea867 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp @@ -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(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 ProfileEntry::FromJson(const Json::Value& json) +{ + auto entry = winrt::make_self(); + + JsonUtils::GetValueForKey(json, ProfileKey, entry->_ProfileName); + + return entry; +} diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.h b/src/cascadia/TerminalSettingsModel/ProfileEntry.h new file mode 100644 index 0000000000..fe0f4573d2 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.h @@ -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 + { + public: + ProfileEntry() noexcept; + explicit ProfileEntry(const winrt::hstring& profile) noexcept; + + Json::Value ToJson() const override; + static com_ptr 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); +} diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp new file mode 100644 index 0000000000..1d2539da2d --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp @@ -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(NewTabMenuEntryType::RemainingProfiles) +{ +} + +winrt::com_ptr RemainingProfilesEntry::FromJson(const Json::Value&) +{ + return winrt::make_self(); +} diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h new file mode 100644 index 0000000000..153669e4f7 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h @@ -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 + { + public: + RemainingProfilesEntry() noexcept; + + static com_ptr FromJson(const Json::Value& json); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(RemainingProfilesEntry); +} diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp new file mode 100644 index 0000000000..d6cee21090 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp @@ -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(NewTabMenuEntryType::Separator) +{ +} + +winrt::com_ptr SeparatorEntry::FromJson(const Json::Value&) +{ + return winrt::make_self(); +} diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.h b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h new file mode 100644 index 0000000000..e074ce251c --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h @@ -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 + { + public: + SeparatorEntry() noexcept; + + static com_ptr FromJson(const Json::Value& json); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(SeparatorEntry); +} diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 95e5a03cf8..fd5738da4a 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -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> { diff --git a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl index 43dee4e5a6..5d3f676609 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl @@ -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. }; diff --git a/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj b/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj index 88132dc940..14a3532fdf 100644 --- a/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj +++ b/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj @@ -38,6 +38,25 @@ + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h +