diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt
index 3b54d304979..f48cb4cd12d 100644
--- a/.github/actions/spelling/allow/allow.txt
+++ b/.github/actions/spelling/allow/allow.txt
@@ -54,6 +54,7 @@ Powerline
ptys
pwn
pwshw
+QOL
qof
qps
quickfix
@@ -71,6 +72,7 @@ shcha
similaritytolerance
slnt
stakeholders
+subpage
sustainability
sxn
TLDR
diff --git a/src/cascadia/TerminalSettingsEditor/CommonResources.xaml b/src/cascadia/TerminalSettingsEditor/CommonResources.xaml
index 7f2f3446bf2..1e32c65078b 100644
--- a/src/cascadia/TerminalSettingsEditor/CommonResources.xaml
+++ b/src/cascadia/TerminalSettingsEditor/CommonResources.xaml
@@ -67,6 +67,7 @@
0,24,0,0
250
1000
+ 13,0,13,48
@@ -255,6 +256,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp
new file mode 100644
index 00000000000..62986177cc1
--- /dev/null
+++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp
@@ -0,0 +1,812 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "NewTabMenuViewModel.h"
+#include
+
+#include "NewTabMenuViewModel.g.cpp"
+#include "FolderTreeViewEntry.g.cpp"
+#include "NewTabMenuEntryViewModel.g.cpp"
+#include "ProfileEntryViewModel.g.cpp"
+#include "ActionEntryViewModel.g.cpp"
+#include "SeparatorEntryViewModel.g.cpp"
+#include "FolderEntryViewModel.g.cpp"
+#include "MatchProfilesEntryViewModel.g.cpp"
+#include "RemainingProfilesEntryViewModel.g.cpp"
+
+using namespace winrt::Windows::UI::Xaml::Navigation;
+using namespace winrt::Windows::Foundation;
+using namespace winrt::Windows::Foundation::Collections;
+using namespace winrt::Microsoft::Terminal::Settings::Model;
+using namespace winrt::Windows::UI::Xaml::Data;
+
+namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
+{
+ static IObservableVector _ConvertToViewModelEntries(const IVector& settingsModelEntries, const Model::CascadiaSettings& settings)
+ {
+ std::vector result{};
+ if (!settingsModelEntries)
+ {
+ return single_threaded_observable_vector(std::move(result));
+ }
+
+ for (const auto& entry : settingsModelEntries)
+ {
+ switch (entry.Type())
+ {
+ case NewTabMenuEntryType::Profile:
+ {
+ // If the Profile isn't set, this is an invalid entry. Skip it.
+ if (const auto& profileEntry = entry.as(); profileEntry.Profile())
+ {
+ result.push_back(make(profileEntry));
+ }
+ break;
+ }
+ case NewTabMenuEntryType::Action:
+ {
+ if (const auto& actionEntry = entry.as())
+ {
+ result.push_back(make(actionEntry, settings));
+ }
+ break;
+ }
+ case NewTabMenuEntryType::Separator:
+ {
+ if (const auto& separatorEntry = entry.as())
+ {
+ result.push_back(make(separatorEntry));
+ }
+ break;
+ }
+ case NewTabMenuEntryType::Folder:
+ {
+ if (const auto& folderEntry = entry.as())
+ {
+ // The ctor will convert the children of the folder to view models
+ result.push_back(make(folderEntry, settings));
+ }
+ break;
+ }
+ case NewTabMenuEntryType::MatchProfiles:
+ {
+ if (const auto& matchProfilesEntry = entry.as())
+ {
+ result.push_back(make(matchProfilesEntry));
+ }
+ break;
+ }
+ case NewTabMenuEntryType::RemainingProfiles:
+ {
+ if (const auto& remainingProfilesEntry = entry.as())
+ {
+ result.push_back(make(remainingProfilesEntry));
+ }
+ break;
+ }
+ case NewTabMenuEntryType::Invalid:
+ default:
+ break;
+ }
+ }
+ return single_threaded_observable_vector(std::move(result));
+ }
+
+ bool NewTabMenuViewModel::IsRemainingProfilesEntryMissing() const
+ {
+ return _IsRemainingProfilesEntryMissing(_rootEntries);
+ }
+
+ bool NewTabMenuViewModel::_IsRemainingProfilesEntryMissing(const IVector& entries)
+ {
+ for (const auto& entry : entries)
+ {
+ switch (entry.Type())
+ {
+ case NewTabMenuEntryType::RemainingProfiles:
+ {
+ return false;
+ }
+ case NewTabMenuEntryType::Folder:
+ {
+ if (!_IsRemainingProfilesEntryMissing(entry.as().Entries()))
+ {
+ return false;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return true;
+ }
+
+ bool NewTabMenuViewModel::IsFolderView() const noexcept
+ {
+ return _CurrentFolder != nullptr;
+ }
+
+ NewTabMenuViewModel::NewTabMenuViewModel(Model::CascadiaSettings settings)
+ {
+ UpdateSettings(settings);
+
+ // Add a property changed handler to our own property changed event.
+ // This propagates changes from the settings model to anybody listening to our
+ // unique view model members.
+ PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
+ const auto viewModelProperty{ args.PropertyName() };
+ if (viewModelProperty == L"AvailableProfiles")
+ {
+ _NotifyChanges(L"SelectedProfile");
+ }
+ else if (viewModelProperty == L"CurrentFolder")
+ {
+ if (_CurrentFolder)
+ {
+ CurrentFolderName(_CurrentFolder.Name());
+ _CurrentFolder.PropertyChanged({ this, &NewTabMenuViewModel::_FolderPropertyChanged });
+ }
+ _NotifyChanges(L"IsFolderView", L"CurrentView");
+ }
+ });
+ }
+
+ void NewTabMenuViewModel::_FolderPropertyChanged(const IInspectable& /*sender*/, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args)
+ {
+ const auto viewModelProperty{ args.PropertyName() };
+ if (viewModelProperty == L"Name")
+ {
+ // FolderTree needs to be updated when a folder is renamed
+ _folderTreeCache = nullptr;
+ }
+ }
+
+ hstring NewTabMenuViewModel::CurrentFolderName() const
+ {
+ if (!_CurrentFolder)
+ {
+ return {};
+ }
+ return _CurrentFolder.Name();
+ }
+
+ void NewTabMenuViewModel::CurrentFolderName(const hstring& value)
+ {
+ if (_CurrentFolder && _CurrentFolder.Name() != value)
+ {
+ _CurrentFolder.Name(value);
+ _NotifyChanges(L"CurrentFolderName");
+ }
+ }
+
+ bool NewTabMenuViewModel::CurrentFolderInlining() const
+ {
+ if (!_CurrentFolder)
+ {
+ return {};
+ }
+ return _CurrentFolder.Inlining();
+ }
+
+ void NewTabMenuViewModel::CurrentFolderInlining(bool value)
+ {
+ if (_CurrentFolder && _CurrentFolder.Inlining() != value)
+ {
+ _CurrentFolder.Inlining(value);
+ _NotifyChanges(L"CurrentFolderInlining");
+ }
+ }
+
+ bool NewTabMenuViewModel::CurrentFolderAllowEmpty() const
+ {
+ if (!_CurrentFolder)
+ {
+ return {};
+ }
+ return _CurrentFolder.AllowEmpty();
+ }
+
+ void NewTabMenuViewModel::CurrentFolderAllowEmpty(bool value)
+ {
+ if (_CurrentFolder && _CurrentFolder.AllowEmpty() != value)
+ {
+ _CurrentFolder.AllowEmpty(value);
+ _NotifyChanges(L"CurrentFolderAllowEmpty");
+ }
+ }
+
+ Windows::Foundation::Collections::IObservableVector NewTabMenuViewModel::CurrentView() const
+ {
+ if (!_CurrentFolder)
+ {
+ return _rootEntries;
+ }
+ return _CurrentFolder.Entries();
+ }
+
+ static bool _FindFolderPathByName(const IVector& entries, const hstring& name, std::vector& result)
+ {
+ for (const auto& entry : entries)
+ {
+ if (const auto& folderVM = entry.try_as())
+ {
+ result.push_back(folderVM);
+ if (folderVM.Name() == name)
+ {
+ // Found the folder
+ return true;
+ }
+ else if (_FindFolderPathByName(folderVM.Entries(), name, result))
+ {
+ // Found the folder in the children of this folder
+ return true;
+ }
+ else
+ {
+ // This folder and its descendants are not the folder we're looking for
+ result.pop_back();
+ }
+ }
+ }
+ return false;
+ }
+
+ IVector NewTabMenuViewModel::FindFolderPathByName(const hstring& name)
+ {
+ std::vector entries;
+ _FindFolderPathByName(_rootEntries, name, entries);
+ return single_threaded_vector(std::move(entries));
+ }
+
+ void NewTabMenuViewModel::UpdateSettings(const Model::CascadiaSettings& settings)
+ {
+ _Settings = settings;
+ _NotifyChanges(L"AvailableProfiles");
+
+ SelectedProfile(AvailableProfiles().GetAt(0));
+
+ _rootEntries = _ConvertToViewModelEntries(_Settings.GlobalSettings().NewTabMenu(), _Settings);
+ _rootEntriesChangedRevoker = _rootEntries.VectorChanged(winrt::auto_revoke, [this](auto&&, const IVectorChangedEventArgs& args) {
+ switch (args.CollectionChange())
+ {
+ case CollectionChange::Reset:
+ {
+ // fully replace settings model with view model structure
+ std::vector modelEntries;
+ for (const auto& entry : _rootEntries)
+ {
+ modelEntries.push_back(NewTabMenuEntryViewModel::GetModel(entry));
+ }
+ _Settings.GlobalSettings().NewTabMenu(single_threaded_vector(std::move(modelEntries)));
+ return;
+ }
+ case CollectionChange::ItemInserted:
+ {
+ const auto& insertedEntryVM = _rootEntries.GetAt(args.Index());
+ const auto& insertedEntry = NewTabMenuEntryViewModel::GetModel(insertedEntryVM);
+ _Settings.GlobalSettings().NewTabMenu().InsertAt(args.Index(), insertedEntry);
+ return;
+ }
+ case CollectionChange::ItemRemoved:
+ {
+ _Settings.GlobalSettings().NewTabMenu().RemoveAt(args.Index());
+ return;
+ }
+ case CollectionChange::ItemChanged:
+ {
+ const auto& modifiedEntry = _rootEntries.GetAt(args.Index());
+ _Settings.GlobalSettings().NewTabMenu().SetAt(args.Index(), NewTabMenuEntryViewModel::GetModel(modifiedEntry));
+ return;
+ }
+ }
+ });
+ }
+
+ void NewTabMenuViewModel::RequestReorderEntry(const Editor::NewTabMenuEntryViewModel& vm, bool goingUp)
+ {
+ uint32_t idx;
+ if (CurrentView().IndexOf(vm, idx))
+ {
+ if (goingUp && idx > 0)
+ {
+ CurrentView().RemoveAt(idx);
+ CurrentView().InsertAt(idx - 1, vm);
+ }
+ else if (!goingUp && idx < CurrentView().Size() - 1)
+ {
+ CurrentView().RemoveAt(idx);
+ CurrentView().InsertAt(idx + 1, vm);
+ }
+ }
+ }
+
+ void NewTabMenuViewModel::RequestDeleteEntry(const Editor::NewTabMenuEntryViewModel& vm)
+ {
+ uint32_t idx;
+ if (CurrentView().IndexOf(vm, idx))
+ {
+ CurrentView().RemoveAt(idx);
+
+ if (vm.try_as())
+ {
+ _folderTreeCache = nullptr;
+ }
+ }
+ }
+
+ void NewTabMenuViewModel::RequestMoveEntriesToFolder(const Windows::Foundation::Collections::IVector& entries, const Editor::FolderEntryViewModel& destinationFolder)
+ {
+ auto destination{ destinationFolder == nullptr ? _rootEntries : destinationFolder.Entries() };
+ for (auto&& e : entries)
+ {
+ // Don't move the folder into itself (just skip over it)
+ if (e == destinationFolder)
+ {
+ continue;
+ }
+
+ // Remove entry from the current layer,
+ // and add it to the destination folder
+ RequestDeleteEntry(e);
+ destination.Append(e);
+ }
+ }
+
+ Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddSelectedProfileEntry()
+ {
+ if (_SelectedProfile)
+ {
+ Model::ProfileEntry profileEntry;
+ profileEntry.Profile(_SelectedProfile);
+
+ const auto& entryVM = make(profileEntry);
+ CurrentView().Append(entryVM);
+ _PrintAll();
+ return entryVM;
+ }
+ return nullptr;
+ }
+
+ Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddSeparatorEntry()
+ {
+ Model::SeparatorEntry separatorEntry;
+ const auto& entryVM = make(separatorEntry);
+ CurrentView().Append(entryVM);
+
+ _PrintAll();
+ return entryVM;
+ }
+
+ Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddFolderEntry()
+ {
+ Model::FolderEntry folderEntry;
+ folderEntry.Name(_AddFolderName);
+
+ const auto& entryVM = make(folderEntry, _Settings);
+ CurrentView().Append(entryVM);
+
+ // Reset state after adding the entry
+ AddFolderName({});
+ _folderTreeCache = nullptr;
+
+ _PrintAll();
+ return entryVM;
+ }
+
+ Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddProfileMatcherEntry()
+ {
+ Model::MatchProfilesEntry matchProfilesEntry;
+ matchProfilesEntry.Name(_ProfileMatcherName);
+ matchProfilesEntry.Source(_ProfileMatcherSource);
+ matchProfilesEntry.Commandline(_ProfileMatcherCommandline);
+
+ const auto& entryVM = make(matchProfilesEntry);
+ CurrentView().Append(entryVM);
+
+ // Clear the fields after adding the entry
+ ProfileMatcherName({});
+ ProfileMatcherSource({});
+ ProfileMatcherCommandline({});
+
+ _PrintAll();
+ return entryVM;
+ }
+
+ Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddRemainingProfilesEntry()
+ {
+ Model::RemainingProfilesEntry remainingProfilesEntry;
+ const auto& entryVM = make(remainingProfilesEntry);
+ CurrentView().Append(entryVM);
+
+ _NotifyChanges(L"IsRemainingProfilesEntryMissing");
+
+ _PrintAll();
+ return entryVM;
+ }
+
+ void NewTabMenuViewModel::GenerateFolderTree()
+ {
+ if (!_folderTreeCache)
+ {
+ // Add the root folder
+ auto root = winrt::make(nullptr);
+
+ for (const auto&& entry : _rootEntries)
+ {
+ if (entry.Type() == NewTabMenuEntryType::Folder)
+ {
+ root.Children().Append(winrt::make(entry.as()));
+ }
+ }
+
+ std::vector folderTreeCache;
+ folderTreeCache.emplace_back(std::move(root));
+ _folderTreeCache = single_threaded_observable_vector(std::move(folderTreeCache));
+
+ _NotifyChanges(L"FolderTree");
+ }
+ }
+
+ Collections::IObservableVector NewTabMenuViewModel::FolderTree() const
+ {
+ // We could do this...
+ // if (!_folderTreeCache){ GenerateFolderTree(); }
+ // But FolderTree() gets called when we open the page.
+ // Instead, we generate the tree as needed using GenerateFolderTree()
+ // which caches the tree.
+ return _folderTreeCache;
+ }
+
+ // This recursively constructs the FolderTree
+ FolderTreeViewEntry::FolderTreeViewEntry(Editor::FolderEntryViewModel folderEntry) :
+ _folderEntry{ folderEntry },
+ _Children{ single_threaded_observable_vector() }
+ {
+ if (!_folderEntry)
+ {
+ return;
+ }
+
+ for (const auto&& entry : _folderEntry.Entries())
+ {
+ if (entry.Type() == NewTabMenuEntryType::Folder)
+ {
+ _Children.Append(winrt::make(entry.as()));
+ }
+ }
+ }
+
+ hstring FolderTreeViewEntry::Name() const
+ {
+ if (!_folderEntry)
+ {
+ return RS_(L"NewTabMenu_RootFolderName");
+ }
+ return _folderEntry.Name();
+ }
+
+ hstring FolderTreeViewEntry::Icon() const
+ {
+ if (!_folderEntry)
+ {
+ return {};
+ }
+ return _folderEntry.Icon();
+ }
+
+ void NewTabMenuViewModel::_PrintAll()
+ {
+#ifdef _DEBUG
+ OutputDebugString(L"---Model:---\n");
+ _PrintModel(_Settings.GlobalSettings().NewTabMenu());
+ OutputDebugString(L"\n");
+ OutputDebugString(L"---VM:---\n");
+ _PrintVM(_rootEntries);
+ OutputDebugString(L"\n");
+#endif
+ }
+
+#ifdef _DEBUG
+ void NewTabMenuViewModel::_PrintModel(Windows::Foundation::Collections::IVector list, std::wstring prefix)
+ {
+ if (!list)
+ {
+ return;
+ }
+
+ for (auto&& e : list)
+ {
+ _PrintModel(e, prefix);
+ }
+ }
+
+ void NewTabMenuViewModel::_PrintModel(const Model::NewTabMenuEntry& e, std::wstring prefix)
+ {
+ switch (e.Type())
+ {
+ case NewTabMenuEntryType::Profile:
+ {
+ const auto& pe = e.as();
+ OutputDebugString(fmt::format(L"{}Profile: {}\n", prefix, pe.Profile().Name()).c_str());
+ break;
+ }
+ case NewTabMenuEntryType::Action:
+ {
+ const auto& actionEntry = e.as();
+ OutputDebugString(fmt::format(L"{}Action: {}\n", prefix, actionEntry.ActionId()).c_str());
+ break;
+ }
+ case NewTabMenuEntryType::Separator:
+ {
+ OutputDebugString(fmt::format(L"{}Separator\n", prefix).c_str());
+ break;
+ }
+ case NewTabMenuEntryType::Folder:
+ {
+ const auto& fe = e.as();
+ OutputDebugString(fmt::format(L"{}Folder: {}\n", prefix, fe.Name()).c_str());
+ _PrintModel(fe.RawEntries(), prefix + L" ");
+ break;
+ }
+ case NewTabMenuEntryType::MatchProfiles:
+ {
+ const auto& matchProfilesEntry = e.as();
+ OutputDebugString(fmt::format(L"{}MatchProfiles: {}\n", prefix, matchProfilesEntry.Name()).c_str());
+ break;
+ }
+ case NewTabMenuEntryType::RemainingProfiles:
+ {
+ OutputDebugString(fmt::format(L"{}RemainingProfiles\n", prefix).c_str());
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ void NewTabMenuViewModel::_PrintVM(Windows::Foundation::Collections::IVector list, std::wstring prefix)
+ {
+ if (!list)
+ {
+ return;
+ }
+
+ for (auto&& e : list)
+ {
+ _PrintVM(e, prefix);
+ }
+ }
+
+ void NewTabMenuViewModel::_PrintVM(const Editor::NewTabMenuEntryViewModel& e, std::wstring prefix)
+ {
+ switch (e.Type())
+ {
+ case NewTabMenuEntryType::Profile:
+ {
+ const auto& pe = e.as();
+ OutputDebugString(fmt::format(L"{}Profile: {}\n", prefix, pe.ProfileEntry().Profile().Name()).c_str());
+ break;
+ }
+ case NewTabMenuEntryType::Action:
+ {
+ const auto& actionEntry = e.as();
+ OutputDebugString(fmt::format(L"{}Action: {}\n", prefix, actionEntry.ActionEntry().ActionId()).c_str());
+ break;
+ }
+ case NewTabMenuEntryType::Separator:
+ {
+ OutputDebugString(fmt::format(L"{}Separator\n", prefix).c_str());
+ break;
+ }
+ case NewTabMenuEntryType::Folder:
+ {
+ const auto& fe = e.as();
+ OutputDebugString(fmt::format(L"{}Folder: {}\n", prefix, fe.Name()).c_str());
+ _PrintVM(fe.Entries(), prefix + L" ");
+ break;
+ }
+ case NewTabMenuEntryType::MatchProfiles:
+ {
+ const auto& matchProfilesEntry = e.as();
+ OutputDebugString(fmt::format(L"{}MatchProfiles: {}\n", prefix, matchProfilesEntry.DisplayText()).c_str());
+ break;
+ }
+ case NewTabMenuEntryType::RemainingProfiles:
+ {
+ OutputDebugString(fmt::format(L"{}RemainingProfiles\n", prefix).c_str());
+ break;
+ }
+ default:
+ break;
+ }
+ }
+#endif
+
+ NewTabMenuEntryViewModel::NewTabMenuEntryViewModel(const NewTabMenuEntryType type) noexcept :
+ _Type{ type }
+ {
+ }
+
+ Model::NewTabMenuEntry NewTabMenuEntryViewModel::GetModel(const Editor::NewTabMenuEntryViewModel& viewModel)
+ {
+ switch (viewModel.Type())
+ {
+ case NewTabMenuEntryType::Profile:
+ {
+ const auto& projVM = viewModel.as();
+ return get_self(projVM)->ProfileEntry();
+ }
+ case NewTabMenuEntryType::Action:
+ {
+ const auto& projVM = viewModel.as();
+ return get_self(projVM)->ActionEntry();
+ }
+ case NewTabMenuEntryType::Separator:
+ {
+ const auto& projVM = viewModel.as();
+ return get_self(projVM)->SeparatorEntry();
+ }
+ case NewTabMenuEntryType::Folder:
+ {
+ const auto& projVM = viewModel.as();
+ return get_self(projVM)->FolderEntry();
+ }
+ case NewTabMenuEntryType::MatchProfiles:
+ {
+ const auto& projVM = viewModel.as();
+ return get_self(projVM)->MatchProfilesEntry();
+ }
+ case NewTabMenuEntryType::RemainingProfiles:
+ {
+ const auto& projVM = viewModel.as();
+ return get_self(projVM)->RemainingProfilesEntry();
+ }
+ case NewTabMenuEntryType::Invalid:
+ default:
+ return nullptr;
+ }
+ }
+
+ ProfileEntryViewModel::ProfileEntryViewModel(Model::ProfileEntry profileEntry) :
+ ProfileEntryViewModelT(Model::NewTabMenuEntryType::Profile),
+ _ProfileEntry{ profileEntry }
+ {
+ }
+
+ ActionEntryViewModel::ActionEntryViewModel(Model::ActionEntry actionEntry, Model::CascadiaSettings settings) :
+ ActionEntryViewModelT(Model::NewTabMenuEntryType::Action),
+ _ActionEntry{ actionEntry },
+ _Settings{ settings }
+ {
+ }
+
+ hstring ActionEntryViewModel::DisplayText() const
+ {
+ assert(_Settings);
+
+ const auto actionID = _ActionEntry.ActionId();
+ if (const auto& action = _Settings.ActionMap().GetActionByID(actionID))
+ {
+ return action.Name();
+ }
+ return hstring{ fmt::format(L"{}: {}", RS_(L"NewTabMenu_ActionNotFound"), actionID) };
+ }
+
+ hstring ActionEntryViewModel::Icon() const
+ {
+ assert(_Settings);
+
+ const auto actionID = _ActionEntry.ActionId();
+ if (const auto& action = _Settings.ActionMap().GetActionByID(actionID))
+ {
+ return action.IconPath();
+ }
+ return {};
+ }
+
+ SeparatorEntryViewModel::SeparatorEntryViewModel(Model::SeparatorEntry separatorEntry) :
+ SeparatorEntryViewModelT(Model::NewTabMenuEntryType::Separator),
+ _SeparatorEntry{ separatorEntry }
+ {
+ }
+
+ FolderEntryViewModel::FolderEntryViewModel(Model::FolderEntry folderEntry) :
+ FolderEntryViewModel(folderEntry, nullptr) {}
+
+ FolderEntryViewModel::FolderEntryViewModel(Model::FolderEntry folderEntry, Model::CascadiaSettings settings) :
+ FolderEntryViewModelT(Model::NewTabMenuEntryType::Folder),
+ _FolderEntry{ folderEntry },
+ _Settings{ settings }
+ {
+ _Entries = _ConvertToViewModelEntries(_FolderEntry.RawEntries(), _Settings);
+
+ _entriesChangedRevoker = _Entries.VectorChanged(winrt::auto_revoke, [this](auto&&, const IVectorChangedEventArgs& args) {
+ switch (args.CollectionChange())
+ {
+ case CollectionChange::Reset:
+ {
+ // fully replace settings model with _Entries
+ std::vector modelEntries;
+ for (const auto& entry : _Entries)
+ {
+ modelEntries.push_back(NewTabMenuEntryViewModel::GetModel(entry));
+ }
+ _FolderEntry.RawEntries(single_threaded_vector(std::move(modelEntries)));
+ return;
+ }
+ case CollectionChange::ItemInserted:
+ {
+ const auto& insertedEntryVM = _Entries.GetAt(args.Index());
+ const auto& insertedEntry = NewTabMenuEntryViewModel::GetModel(insertedEntryVM);
+ if (!_FolderEntry.RawEntries())
+ {
+ _FolderEntry.RawEntries(single_threaded_vector());
+ }
+ _FolderEntry.RawEntries().InsertAt(args.Index(), insertedEntry);
+ return;
+ }
+ case CollectionChange::ItemRemoved:
+ {
+ _FolderEntry.RawEntries().RemoveAt(args.Index());
+ return;
+ }
+ case CollectionChange::ItemChanged:
+ {
+ const auto& modifiedEntry = _Entries.GetAt(args.Index());
+ _FolderEntry.RawEntries().SetAt(args.Index(), NewTabMenuEntryViewModel::GetModel(modifiedEntry));
+ return;
+ }
+ }
+ });
+ }
+
+ bool FolderEntryViewModel::Inlining() const
+ {
+ return _FolderEntry.Inlining() == FolderEntryInlining::Auto;
+ }
+
+ void FolderEntryViewModel::Inlining(bool value)
+ {
+ const auto valueAsEnum = value ? FolderEntryInlining::Auto : FolderEntryInlining::Never;
+ if (_FolderEntry.Inlining() != valueAsEnum)
+ {
+ _FolderEntry.Inlining(valueAsEnum);
+ _NotifyChanges(L"Inlining");
+ }
+ };
+
+ MatchProfilesEntryViewModel::MatchProfilesEntryViewModel(Model::MatchProfilesEntry matchProfilesEntry) :
+ MatchProfilesEntryViewModelT(Model::NewTabMenuEntryType::MatchProfiles),
+ _MatchProfilesEntry{ matchProfilesEntry }
+ {
+ }
+
+ hstring MatchProfilesEntryViewModel::DisplayText() const
+ {
+ std::wstring displayText;
+ if (const auto profileName = _MatchProfilesEntry.Name(); !profileName.empty())
+ {
+ fmt::format_to(std::back_inserter(displayText), FMT_COMPILE(L"profile: {}, "), profileName);
+ }
+ if (const auto commandline = _MatchProfilesEntry.Commandline(); !commandline.empty())
+ {
+ fmt::format_to(std::back_inserter(displayText), FMT_COMPILE(L"commandline: {}, "), commandline);
+ }
+ if (const auto source = _MatchProfilesEntry.Source(); !source.empty())
+ {
+ fmt::format_to(std::back_inserter(displayText), FMT_COMPILE(L"source: {}, "), source);
+ }
+
+ // Chop off the last ", "
+ displayText.resize(displayText.size() - 2);
+ return winrt::hstring{ displayText };
+ }
+
+ RemainingProfilesEntryViewModel::RemainingProfilesEntryViewModel(Model::RemainingProfilesEntry remainingProfilesEntry) :
+ RemainingProfilesEntryViewModelT(Model::NewTabMenuEntryType::RemainingProfiles),
+ _RemainingProfilesEntry{ remainingProfilesEntry }
+ {
+ }
+}
diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h
new file mode 100644
index 00000000000..c5486f68b55
--- /dev/null
+++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h
@@ -0,0 +1,183 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "NewTabMenuViewModel.g.h"
+#include "FolderTreeViewEntry.g.h"
+#include "NewTabMenuEntryViewModel.g.h"
+#include "ProfileEntryViewModel.g.h"
+#include "ActionEntryViewModel.g.h"
+#include "SeparatorEntryViewModel.g.h"
+#include "FolderEntryViewModel.g.h"
+#include "MatchProfilesEntryViewModel.g.h"
+#include "RemainingProfilesEntryViewModel.g.h"
+
+#include "ProfileViewModel.h"
+#include "ViewModelHelpers.h"
+#include "Utils.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
+{
+ struct NewTabMenuViewModel : NewTabMenuViewModelT, ViewModelHelper
+ {
+ public:
+ NewTabMenuViewModel(Model::CascadiaSettings settings);
+ void UpdateSettings(const Model::CascadiaSettings& settings);
+ void GenerateFolderTree();
+ Windows::Foundation::Collections::IVector FindFolderPathByName(const hstring& name);
+
+ bool IsRemainingProfilesEntryMissing() const;
+ bool IsFolderView() const noexcept;
+
+ void RequestReorderEntry(const Editor::NewTabMenuEntryViewModel& vm, bool goingUp);
+ void RequestDeleteEntry(const Editor::NewTabMenuEntryViewModel& vm);
+ void RequestMoveEntriesToFolder(const Windows::Foundation::Collections::IVector& entries, const Editor::FolderEntryViewModel& destinationFolder);
+
+ Editor::NewTabMenuEntryViewModel RequestAddSelectedProfileEntry();
+ Editor::NewTabMenuEntryViewModel RequestAddSeparatorEntry();
+ Editor::NewTabMenuEntryViewModel RequestAddFolderEntry();
+ Editor::NewTabMenuEntryViewModel RequestAddProfileMatcherEntry();
+ Editor::NewTabMenuEntryViewModel RequestAddRemainingProfilesEntry();
+
+ hstring CurrentFolderName() const;
+ void CurrentFolderName(const hstring& value);
+ bool CurrentFolderInlining() const;
+ void CurrentFolderInlining(bool value);
+ bool CurrentFolderAllowEmpty() const;
+ void CurrentFolderAllowEmpty(bool value);
+
+ Windows::Foundation::Collections::IObservableVector AvailableProfiles() const { return _Settings.AllProfiles(); }
+ Windows::Foundation::Collections::IObservableVector FolderTree() const;
+ Windows::Foundation::Collections::IObservableVector CurrentView() const;
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::FolderEntryViewModel, CurrentFolder, nullptr);
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::FolderTreeViewEntry, CurrentFolderTreeViewSelectedItem, nullptr);
+
+ // Bound to the UI to create new entries
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Model::Profile, SelectedProfile, nullptr);
+ VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProfileMatcherName);
+ VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProfileMatcherSource);
+ VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProfileMatcherCommandline);
+ VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, AddFolderName);
+
+ private:
+ Model::CascadiaSettings _Settings{ nullptr };
+ Windows::Foundation::Collections::IObservableVector _rootEntries;
+ Windows::Foundation::Collections::IObservableVector _folderTreeCache;
+ Windows::Foundation::Collections::IObservableVector::VectorChanged_revoker _rootEntriesChangedRevoker;
+
+ static bool _IsRemainingProfilesEntryMissing(const Windows::Foundation::Collections::IVector& entries);
+ void _FolderPropertyChanged(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);
+
+ void _PrintAll();
+#ifdef _DEBUG
+ void _PrintModel(Windows::Foundation::Collections::IVector list, std::wstring prefix = L"");
+ void _PrintModel(const Model::NewTabMenuEntry& e, std::wstring prefix = L"");
+ void _PrintVM(Windows::Foundation::Collections::IVector list, std::wstring prefix = L"");
+ void _PrintVM(const Editor::NewTabMenuEntryViewModel& vm, std::wstring prefix = L"");
+#endif
+ };
+
+ struct FolderTreeViewEntry : FolderTreeViewEntryT
+ {
+ public:
+ FolderTreeViewEntry(Editor::FolderEntryViewModel folderEntry);
+
+ hstring Name() const;
+ hstring Icon() const;
+ Editor::FolderEntryViewModel FolderEntryVM() { return _folderEntry; }
+
+ WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, Children);
+
+ private:
+ Editor::FolderEntryViewModel _folderEntry;
+ };
+
+ struct NewTabMenuEntryViewModel : NewTabMenuEntryViewModelT, ViewModelHelper
+ {
+ public:
+ static Model::NewTabMenuEntry GetModel(const Editor::NewTabMenuEntryViewModel& viewModel);
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Model::NewTabMenuEntryType, Type, Model::NewTabMenuEntryType::Invalid);
+
+ protected:
+ explicit NewTabMenuEntryViewModel(const Model::NewTabMenuEntryType type) noexcept;
+ };
+
+ struct ProfileEntryViewModel : ProfileEntryViewModelT
+ {
+ public:
+ ProfileEntryViewModel(Model::ProfileEntry profileEntry);
+
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Model::ProfileEntry, ProfileEntry, nullptr);
+ };
+
+ struct ActionEntryViewModel : ActionEntryViewModelT
+ {
+ public:
+ ActionEntryViewModel(Model::ActionEntry actionEntry, Model::CascadiaSettings settings);
+
+ hstring DisplayText() const;
+ hstring Icon() const;
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Model::ActionEntry, ActionEntry, nullptr);
+
+ private:
+ Model::CascadiaSettings _Settings;
+ };
+
+ struct SeparatorEntryViewModel : SeparatorEntryViewModelT
+ {
+ public:
+ SeparatorEntryViewModel(Model::SeparatorEntry separatorEntry);
+
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Model::SeparatorEntry, SeparatorEntry, nullptr);
+ };
+
+ struct FolderEntryViewModel : FolderEntryViewModelT
+ {
+ public:
+ FolderEntryViewModel(Model::FolderEntry folderEntry, Model::CascadiaSettings settings);
+ explicit FolderEntryViewModel(Model::FolderEntry folderEntry);
+
+ bool Inlining() const;
+ void Inlining(bool value);
+ GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, Name);
+ GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, Icon);
+ GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, AllowEmpty);
+
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector, Entries);
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Model::FolderEntry, FolderEntry, nullptr);
+
+ private:
+ Windows::Foundation::Collections::IObservableVector::VectorChanged_revoker _entriesChangedRevoker;
+ Model::CascadiaSettings _Settings;
+ };
+
+ struct MatchProfilesEntryViewModel : MatchProfilesEntryViewModelT
+ {
+ public:
+ MatchProfilesEntryViewModel(Model::MatchProfilesEntry matchProfilesEntry);
+
+ hstring DisplayText() const;
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Model::MatchProfilesEntry, MatchProfilesEntry, nullptr);
+ };
+
+ struct RemainingProfilesEntryViewModel : RemainingProfilesEntryViewModelT
+ {
+ public:
+ RemainingProfilesEntryViewModel(Model::RemainingProfilesEntry remainingProfilesEntry);
+
+ VIEW_MODEL_OBSERVABLE_PROPERTY(Model::RemainingProfilesEntry, RemainingProfilesEntry, nullptr);
+ };
+};
+
+namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
+{
+ BASIC_FACTORY(NewTabMenuViewModel);
+ BASIC_FACTORY(FolderTreeViewEntry);
+ BASIC_FACTORY(ProfileEntryViewModel);
+ BASIC_FACTORY(ActionEntryViewModel);
+ BASIC_FACTORY(SeparatorEntryViewModel);
+ BASIC_FACTORY(FolderEntryViewModel);
+ BASIC_FACTORY(MatchProfilesEntryViewModel);
+ BASIC_FACTORY(RemainingProfilesEntryViewModel);
+}
diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl
new file mode 100644
index 00000000000..7097f16f92a
--- /dev/null
+++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import "ProfileViewModel.idl";
+
+namespace Microsoft.Terminal.Settings.Editor
+{
+ [default_interface] runtimeclass FolderTreeViewEntry
+ {
+ FolderTreeViewEntry(FolderEntryViewModel folderEntry);
+
+ String Name { get; };
+ String Icon { get; };
+ FolderEntryViewModel FolderEntryVM { get; };
+
+ IObservableVector Children { get; };
+ }
+
+ runtimeclass NewTabMenuViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
+ {
+ NewTabMenuViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
+ void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
+ void GenerateFolderTree();
+ Windows.Foundation.Collections.IVector FindFolderPathByName(String name);
+
+ FolderEntryViewModel CurrentFolder;
+ Boolean IsFolderView { get; };
+ FolderTreeViewEntry CurrentFolderTreeViewSelectedItem;
+ Boolean IsRemainingProfilesEntryMissing { get; };
+
+ IObservableVector CurrentView { get; };
+ IObservableVector AvailableProfiles { get; };
+ IObservableVector FolderTree { get; };
+ Microsoft.Terminal.Settings.Model.Profile SelectedProfile;
+
+ String CurrentFolderName;
+ Boolean CurrentFolderInlining;
+ Boolean CurrentFolderAllowEmpty;
+ String ProfileMatcherName;
+ String ProfileMatcherSource;
+ String ProfileMatcherCommandline;
+ String AddFolderName;
+
+ void RequestReorderEntry(NewTabMenuEntryViewModel vm, Boolean goingUp);
+ void RequestDeleteEntry(NewTabMenuEntryViewModel vm);
+ void RequestMoveEntriesToFolder(IVector entries, FolderEntryViewModel folderEntry);
+
+ NewTabMenuEntryViewModel RequestAddSelectedProfileEntry();
+ NewTabMenuEntryViewModel RequestAddSeparatorEntry();
+ NewTabMenuEntryViewModel RequestAddFolderEntry();
+ NewTabMenuEntryViewModel RequestAddProfileMatcherEntry();
+ NewTabMenuEntryViewModel RequestAddRemainingProfilesEntry();
+ }
+
+ [default_interface] unsealed runtimeclass NewTabMenuEntryViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
+ {
+ Microsoft.Terminal.Settings.Model.NewTabMenuEntryType Type;
+ }
+
+ [default_interface] runtimeclass ProfileEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
+ {
+ ProfileEntryViewModel(Microsoft.Terminal.Settings.Model.ProfileEntry profileEntry);
+
+ Microsoft.Terminal.Settings.Model.ProfileEntry ProfileEntry { get; };
+ }
+
+ [default_interface] runtimeclass ActionEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
+ {
+ ActionEntryViewModel(Microsoft.Terminal.Settings.Model.ActionEntry actionEntry, Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
+
+ Microsoft.Terminal.Settings.Model.ActionEntry ActionEntry { get; };
+ String DisplayText { get; };
+ String Icon { get; };
+ }
+
+ [default_interface] runtimeclass SeparatorEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
+ {
+ SeparatorEntryViewModel(Microsoft.Terminal.Settings.Model.SeparatorEntry separatorEntry);
+ }
+
+ [default_interface] runtimeclass FolderEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
+ {
+ FolderEntryViewModel(Microsoft.Terminal.Settings.Model.FolderEntry folderEntry, Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
+
+ String Name;
+ String Icon;
+ Boolean Inlining;
+ Boolean AllowEmpty;
+ IObservableVector Entries;
+ }
+
+ [default_interface] runtimeclass MatchProfilesEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
+ {
+ MatchProfilesEntryViewModel(Microsoft.Terminal.Settings.Model.MatchProfilesEntry matchProfilesEntry);
+
+ String DisplayText { get; };
+ }
+
+ [default_interface] runtimeclass RemainingProfilesEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
+ {
+ RemainingProfilesEntryViewModel(Microsoft.Terminal.Settings.Model.RemainingProfilesEntry remainingProfilesEntry);
+ }
+}
diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
index 46524ca62d4..11b5f26bc80 100644
--- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
@@ -1929,6 +1929,178 @@
Non-monospace fonts:
This is a label that is followed by a list of proportional fonts.
+
+ New Tab Menu
+ Header for the "new tab menu" menu item. This navigates to a page that lets you see and modify settings related to the app's new tab menu (i.e. profile ordering, nested folders, dividers, etc.)
+
+
+ <Separator>
+ {Locked="<"}, {Locked=">"} Text label for an entry that represents a visual separator in a list.
+
+
+ <Remaining profiles>
+ {Locked="<"}{Locked=">"} Text label for an entry that represents inserting any remaining profiles that have not been inserted.
+
+
+ <Remaining profiles>
+ {Locked="<"}{Locked=">"} Text label for an entry that represents inserting any remaining profiles that have not been inserted. Should match "NewTabMenuEntry_RemainingProfiles.Text".
+
+
+ Profile
+ Header for a control that adds a terminal profile to the new tab menu.
+
+
+ Profile matcher
+ Header for a control that adds a terminal profile matcher to the new tab menu. This entry adds profiles that match the given parameters.
+
+
+ Remaining profiles
+ Header for a control that adds any remaining profiles to the new tab menu.
+
+
+ Add a group of profiles that match at least one of the defined properties
+ Additional information for a control that adds a terminal profile matcher to the new tab menu. Presented near "NewTabMenu_AddMatchProfiles".
+
+
+ There can only be one "remaining profiles" entry
+ Additional information for a control that adds any remaining profiles to the new tab menu. Presented near "NewTabMenu_AddRemainingProfiles".
+
+
+ Separator
+ Header for a control that adds a separator to the new tab menu.
+
+
+ Folder
+ Header for a control that adds a folder to the new tab menu.
+
+
+ Profile name
+ Header for a text box used to define a regex for the names of profiles to add.
+
+
+ Profile source
+ Header for a text box used to define a regex for the sources of profiles to add.
+
+
+ Commandline
+ Header for a text box used to define a regex for the commandlines of profiles to add.
+
+
+ Add profile matcher
+ Label for a button confirming to add the profile matcher to the new tab menu as an entry.
+
+
+ Folder name
+ Placeholder text for a text box control used to set the name of the folder.
+
+
+ Delete selected entries
+ Label for a button that can be used to delete any new tab menu entries that are currently selected
+
+
+ Move selected entries to folder...
+ Label for a button that can be used to move any new tab menu entries that are currently selected into an existing folder
+
+
+ Move to folder
+ Title displayed on a content dialog directing the user to pick a folder to move the selected entries to.
+
+
+ OK
+ Button label for the folder picker content dialog. Used as confirmation to pick the selected folder.
+
+
+ Cancel
+ Text label for the secondary button on the folder picker content dialog. When clicked, the operation of picking a folder is cancelled by the user.
+
+
+ <root>
+ {Locked="<"}{Locked=">"} Text label for the name of the "root" folder. This is used to allow the user to select the root as a destination folder.
+
+
+ Current Folder Properties
+ Header for a group of controls that can be used to modify the current folder entry's properties.
+
+
+ Add Entry
+ Header for a group of controls that can be used to add an entry to the new tab menu
+
+
+ Folder Name
+ Header for a control that allows the user to modify the name of the current folder entry.
+
+
+ Allow inlining
+ Header for a control that allows the nested entries to be presented inline rather than with a folder.
+
+
+ When enabled, if the folder only has a single entry, the entry will show directly and no folder will be rendered.
+ Additional text displayed near "NewTabMenu_CurrentFolderInlining.Header".
+
+
+ Allow empty
+ Header for a control that allows the current folder entry to be empty.
+
+
+ When enabled, if the folder has no entries, it will still be displayed. Otherwise, the folder will not be rendered.
+ Additional text displayed near "NewTabMenu_CurrentFolderAllowEmpty.Header".
+
+
+ Action ID not found
+ Displayed text for an entry who's action identifier wasn't found. The action ID is presented in the format "Action ID not found: <actionID>"
+
+
+ Move up
+ Accessible name for a button that reorders the entry to be moved up when clicked. Should match "NewTabMenuEntry_ReorderUp.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip".
+
+
+ Move up
+ Accessible name for a button that reorders the entry to be moved up when clicked. Should match "NewTabMenuEntry_ReorderUp.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name".
+
+
+ Move down
+ Accessible name for a button that reorders the entry to be moved down when clicked. Should match "NewTabMenuEntry_ReorderDown.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip".
+
+
+ Edit folder
+ Accessible name for a button that begins editing the folder when clicked. Should match "NewTabMenuEntry_EditFolder.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip".
+
+
+ Move down
+ Accessible name for a button that reorders the entry to be moved down when clicked. Should match "NewTabMenuEntry_ReorderDown.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name".
+
+
+ Edit folder
+ Accessible name for a button that begins editing the folder when clicked. Should match "NewTabMenuEntry_EditFolder.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name".
+
+
+ Delete
+ Accessible name for a button that deletes the entry when clicked. Should match "NewTabMenuEntry_Delete.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip"
+
+
+ Delete
+ Accessible name for a button that deletes the entry when clicked. Should match "NewTabMenuEntry_Delete.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name"
+
+
+ Add selected profile
+ Tooltip for a button that adds the selected profile to the new tab menu.
+
+
+ Add separator
+ Tooltip for a button that adds a separator to the new tab menu.
+
+
+ Add folder
+ Tooltip for a button that adds a folder to the new tab menu.
+
+
+ Add remaining profiles
+ Tooltip for a button that adds an entry that represents the remaining profiles to the new tab menu.
+
+
+ <Separator>
+ {Locked="<"}{Locked=">"}Accessible name for an entry that represents a visual separator in a list. Should match "NewTabMenuEntry_Separator.Text".
+
ENQ (Request Terminal Status) response
{Locked=ENQ}{Locked="Request Terminal Status"} Header for a control to determine the response to the ENQ escape sequence. This is represented using a text box.
diff --git a/src/cascadia/TerminalSettingsEditor/SettingContainer.cpp b/src/cascadia/TerminalSettingsEditor/SettingContainer.cpp
index 8a32fbacb31..9bd4e26a8c4 100644
--- a/src/cascadia/TerminalSettingsEditor/SettingContainer.cpp
+++ b/src/cascadia/TerminalSettingsEditor/SettingContainer.cpp
@@ -12,6 +12,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
DependencyProperty SettingContainer::_HeaderProperty{ nullptr };
DependencyProperty SettingContainer::_HelpTextProperty{ nullptr };
+ DependencyProperty SettingContainer::_FontIconGlyphProperty{ nullptr };
DependencyProperty SettingContainer::_CurrentValueProperty{ nullptr };
DependencyProperty SettingContainer::_HasSettingValueProperty{ nullptr };
DependencyProperty SettingContainer::_SettingOverrideSourceProperty{ nullptr };
@@ -45,6 +46,15 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
xaml_typename(),
PropertyMetadata{ box_value(L"") });
}
+ if (!_FontIconGlyphProperty)
+ {
+ _FontIconGlyphProperty =
+ DependencyProperty::Register(
+ L"FontIconGlyph",
+ xaml_typename(),
+ xaml_typename(),
+ PropertyMetadata{ box_value(L"") });
+ }
if (!_CurrentValueProperty)
{
_CurrentValueProperty =
diff --git a/src/cascadia/TerminalSettingsEditor/SettingContainer.h b/src/cascadia/TerminalSettingsEditor/SettingContainer.h
index 9fcb2d24efa..de80a76d2c9 100644
--- a/src/cascadia/TerminalSettingsEditor/SettingContainer.h
+++ b/src/cascadia/TerminalSettingsEditor/SettingContainer.h
@@ -35,6 +35,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
DEPENDENCY_PROPERTY(Windows::Foundation::IInspectable, Header);
DEPENDENCY_PROPERTY(hstring, HelpText);
+ DEPENDENCY_PROPERTY(hstring, FontIconGlyph);
DEPENDENCY_PROPERTY(hstring, CurrentValue);
DEPENDENCY_PROPERTY(bool, HasSettingValue);
DEPENDENCY_PROPERTY(bool, StartExpanded);
diff --git a/src/cascadia/TerminalSettingsEditor/SettingContainer.idl b/src/cascadia/TerminalSettingsEditor/SettingContainer.idl
index 8b5fd0eba95..dcbca302aef 100644
--- a/src/cascadia/TerminalSettingsEditor/SettingContainer.idl
+++ b/src/cascadia/TerminalSettingsEditor/SettingContainer.idl
@@ -15,6 +15,9 @@ namespace Microsoft.Terminal.Settings.Editor
String HelpText;
static Windows.UI.Xaml.DependencyProperty HelpTextProperty { get; };
+ String FontIconGlyph;
+ static Windows.UI.Xaml.DependencyProperty FontIconGlyphProperty { get; };
+
String CurrentValue;
static Windows.UI.Xaml.DependencyProperty CurrentValueProperty { get; };
diff --git a/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml b/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml
index 72b254e4e41..567796de5b4 100644
--- a/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml
+++ b/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml
@@ -189,10 +189,15 @@
+
-
+
+
@@ -206,7 +211,7 @@
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
-
@@ -233,10 +238,15 @@
+
-
+
+
@@ -250,7 +260,7 @@
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
- _propertyChangedHandlers;
};
+#define GETSET_OBSERVABLE_PROJECTED_SETTING(target, name) \
+public: \
+ auto name() const \
+ { \
+ return target.name(); \
+ }; \
+ template \
+ void name(const T& value) \
+ { \
+ if (target.name() != value) \
+ { \
+ target.name(value); \
+ _NotifyChanges(L"Has" #name, L## #name); \
+ } \
+ }
+
#define _BASE_OBSERVABLE_PROJECTED_SETTING(target, name) \
-public: \
- auto name() const \
- { \
- return target.name(); \
- }; \
- template \
- void name(const T& value) \
- { \
- const auto t = target; \
- if (t.name() != value) \
- { \
- t.name(value); \
- _NotifyChanges(L"Has" #name, L## #name); \
- } \
- } \
+ GETSET_OBSERVABLE_PROJECTED_SETTING(target, name) \
bool Has##name() const \
{ \
return target.Has##name(); \
diff --git a/src/cascadia/TerminalSettingsModel/ActionEntry.cpp b/src/cascadia/TerminalSettingsModel/ActionEntry.cpp
index 6f7773e715c..99dfad61885 100644
--- a/src/cascadia/TerminalSettingsModel/ActionEntry.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionEntry.cpp
@@ -8,32 +8,43 @@
#include "ActionEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
-using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::string_view ActionIdKey{ "id" };
static constexpr std::string_view IconKey{ "icon" };
-ActionEntry::ActionEntry() noexcept :
- ActionEntryT(NewTabMenuEntryType::Action)
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
-}
-Json::Value ActionEntry::ToJson() const
-{
- auto json = NewTabMenuEntry::ToJson();
+ ActionEntry::ActionEntry() noexcept :
+ ActionEntryT(NewTabMenuEntryType::Action)
+ {
+ }
- JsonUtils::SetValueForKey(json, ActionIdKey, _ActionId);
- JsonUtils::SetValueForKey(json, IconKey, _Icon);
+ Json::Value ActionEntry::ToJson() const
+ {
+ auto json = NewTabMenuEntry::ToJson();
- return json;
-}
+ JsonUtils::SetValueForKey(json, ActionIdKey, _ActionId);
+ JsonUtils::SetValueForKey(json, IconKey, _Icon);
-winrt::com_ptr ActionEntry::FromJson(const Json::Value& json)
-{
- auto entry = winrt::make_self();
+ return json;
+ }
+
+ winrt::com_ptr ActionEntry::FromJson(const Json::Value& json)
+ {
+ auto entry = winrt::make_self();
+
+ JsonUtils::GetValueForKey(json, ActionIdKey, entry->_ActionId);
+ JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
- JsonUtils::GetValueForKey(json, ActionIdKey, entry->_ActionId);
- JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
+ return entry;
+ }
- return entry;
+ Model::NewTabMenuEntry ActionEntry::Copy() const
+ {
+ auto entry = winrt::make_self();
+ entry->_ActionId = _ActionId;
+ entry->_Icon = _Icon;
+ return *entry;
+ }
}
diff --git a/src/cascadia/TerminalSettingsModel/ActionEntry.h b/src/cascadia/TerminalSettingsModel/ActionEntry.h
index 711cec13bfc..34681820762 100644
--- a/src/cascadia/TerminalSettingsModel/ActionEntry.h
+++ b/src/cascadia/TerminalSettingsModel/ActionEntry.h
@@ -24,6 +24,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
ActionEntry() noexcept;
+ Model::NewTabMenuEntry Copy() const;
+
Json::Value ToJson() const override;
static com_ptr FromJson(const Json::Value& json);
diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.cpp b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp
index 633f529a86a..c0613c116f6 100644
--- a/src/cascadia/TerminalSettingsModel/FolderEntry.cpp
+++ b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp
@@ -9,7 +9,6 @@
#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" };
@@ -18,107 +17,128 @@ 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{} }
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
-}
-
-FolderEntry::FolderEntry(const winrt::hstring& name) noexcept :
- FolderEntryT(NewTabMenuEntryType::Folder),
- _Name{ name }
-{
-}
+ FolderEntry::FolderEntry() noexcept :
+ FolderEntry{ winrt::hstring{} }
+ {
+ }
-Json::Value FolderEntry::ToJson() const
-{
- auto json = NewTabMenuEntry::ToJson();
+ FolderEntry::FolderEntry(const winrt::hstring& name) noexcept :
+ FolderEntryT(NewTabMenuEntryType::Folder),
+ _Name{ name }
+ {
+ }
- 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);
+ Json::Value FolderEntry::ToJson() const
+ {
+ auto json = NewTabMenuEntry::ToJson();
- return json;
-}
+ JsonUtils::SetValueForKey(json, NameKey, _Name);
+ JsonUtils::SetValueForKey(json, IconKey, _Icon);
+ JsonUtils::SetValueForKey(json, EntriesKey, _RawEntries);
+ JsonUtils::SetValueForKey(json, InliningKey, _Inlining);
+ JsonUtils::SetValueForKey(json, AllowEmptyKey, _AllowEmpty);
-winrt::com_ptr FolderEntry::FromJson(const Json::Value& json)
-{
- auto entry = winrt::make_self();
+ return json;
+ }
- 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);
+ winrt::com_ptr FolderEntry::FromJson(const Json::Value& json)
+ {
+ auto entry = winrt::make_self();
- return entry;
-}
+ JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
+ JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
+ JsonUtils::GetValueForKey(json, EntriesKey, entry->_RawEntries);
+ JsonUtils::GetValueForKey(json, InliningKey, entry->_Inlining);
+ JsonUtils::GetValueForKey(json, AllowEmptyKey, entry->_AllowEmpty);
-// 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() };
- if (_Entries == nullptr)
- {
- return result;
+ return entry;
}
- for (const auto& entry : _Entries)
+ // A FolderEntry should only expose the entries to actually render to WinRT,
+ // to keep the logic for collapsing/expanding more centralised.
+ IVector FolderEntry::Entries() const
{
- if (entry == nullptr)
+ // We filter the full list of entries from JSON to just include the
+ // non-empty ones.
+ IVector result{ winrt::single_threaded_vector() };
+ if (_RawEntries == nullptr)
{
- continue;
+ return result;
}
- switch (entry.Type())
+ for (const auto& entry : _RawEntries)
{
- 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)
+ if (entry == 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)
+ 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;
}
- 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))
+ // Any profile collection is filtered out if there are no results
+ case NewTabMenuEntryType::RemainingProfiles:
+ case NewTabMenuEntryType::MatchProfiles:
{
- continue;
+ const auto profileCollectionEntry = entry.as();
+ if (profileCollectionEntry.Profiles().Size() == 0)
+ {
+ continue;
+ }
+ break;
}
- 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);
}
- result.Append(entry);
+ return result;
}
- return result;
+ Model::NewTabMenuEntry FolderEntry::Copy() const
+ {
+ auto entry = winrt::make_self();
+ entry->_Name = _Name;
+ entry->_Icon = _Icon;
+ entry->_Inlining = _Inlining;
+ entry->_AllowEmpty = _AllowEmpty;
+
+ if (_RawEntries)
+ {
+ entry->_RawEntries = winrt::single_threaded_vector();
+ for (const auto& e : _RawEntries)
+ {
+ entry->_RawEntries.Append(get_self(e)->Copy());
+ }
+ }
+ return *entry;
+ }
}
diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.h b/src/cascadia/TerminalSettingsModel/FolderEntry.h
index 619d228fa7b..0334df04d78 100644
--- a/src/cascadia/TerminalSettingsModel/FolderEntry.h
+++ b/src/cascadia/TerminalSettingsModel/FolderEntry.h
@@ -26,6 +26,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
FolderEntry() noexcept;
explicit FolderEntry(const winrt::hstring& name) noexcept;
+ Model::NewTabMenuEntry Copy() const override;
+
Json::Value ToJson() const override;
static com_ptr FromJson(const Json::Value& json);
@@ -34,18 +36,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// 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{};
+ WINRT_PROPERTY(winrt::Windows::Foundation::Collections::IVector, RawEntries);
};
}
diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp
index 7ea21b045da..564e036cce3 100644
--- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp
+++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp
@@ -84,6 +84,14 @@ winrt::com_ptr GlobalAppSettings::Copy() const
globals->_themes.Insert(kv.Key(), *themeImpl->Copy());
}
}
+ if (_NewTabMenu)
+ {
+ globals->_NewTabMenu = winrt::single_threaded_vector();
+ for (const auto& entry : *_NewTabMenu)
+ {
+ globals->_NewTabMenu->Append(get_self(entry)->Copy());
+ }
+ }
for (const auto& parent : _parents)
{
diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp
index 2873679018e..6946eb98e89 100644
--- a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp
+++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp
@@ -8,66 +8,77 @@
#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)
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
-}
-
-Json::Value MatchProfilesEntry::ToJson() const
-{
- auto json = NewTabMenuEntry::ToJson();
+ MatchProfilesEntry::MatchProfilesEntry() noexcept :
+ MatchProfilesEntryT(NewTabMenuEntryType::MatchProfiles)
+ {
+ }
- JsonUtils::SetValueForKey(json, NameKey, _Name);
- JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline);
- JsonUtils::SetValueForKey(json, SourceKey, _Source);
+ Json::Value MatchProfilesEntry::ToJson() const
+ {
+ auto json = NewTabMenuEntry::ToJson();
- return json;
-}
+ JsonUtils::SetValueForKey(json, NameKey, _Name);
+ JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline);
+ JsonUtils::SetValueForKey(json, SourceKey, _Source);
-winrt::com_ptr MatchProfilesEntry::FromJson(const Json::Value& json)
-{
- auto entry = winrt::make_self();
+ return json;
+ }
- JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
- JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline);
- JsonUtils::GetValueForKey(json, SourceKey, entry->_Source);
+ winrt::com_ptr MatchProfilesEntry::FromJson(const Json::Value& json)
+ {
+ auto entry = winrt::make_self();
- return entry;
-}
+ JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
+ JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline);
+ JsonUtils::GetValueForKey(json, SourceKey, entry->_Source);
-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() };
+ return entry;
}
- if (!_Source.empty())
+ bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile)
{
- isMatching = { isMatching.value_or(true) && _Source == profile.Source() };
+ // 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);
}
- if (!_Commandline.empty())
+ Model::NewTabMenuEntry MatchProfilesEntry::Copy() const
{
- isMatching = { isMatching.value_or(true) && _Commandline == profile.Commandline() };
+ auto entry = winrt::make_self();
+ entry->_Name = _Name;
+ entry->_Commandline = _Commandline;
+ entry->_Source = _Source;
+ return *entry;
}
-
- return isMatching.value_or(false);
}
diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h
index 464a6780c12..815d1be3e85 100644
--- a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h
+++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h
@@ -25,6 +25,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
MatchProfilesEntry() noexcept;
+ Model::NewTabMenuEntry Copy() const override;
+
Json::Value ToJson() const override;
static com_ptr FromJson(const Json::Value& json);
diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp
index c4762067ed7..292774a21b7 100644
--- a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp
+++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp
@@ -15,47 +15,49 @@
#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 }
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
-}
-
-// 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 };
+ NewTabMenuEntry::NewTabMenuEntry(const NewTabMenuEntryType type) noexcept :
+ _Type{ type }
+ {
+ }
- JsonUtils::SetValueForKey(json, TypeKey, _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 };
- return json;
-}
+ JsonUtils::SetValueForKey(json, TypeKey, _Type);
-// 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);
+ return json;
+ }
- switch (type)
+ // 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)
{
- 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);
- case NewTabMenuEntryType::Action:
- return ActionEntry::FromJson(json);
- default:
- return nullptr;
+ 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);
+ case NewTabMenuEntryType::Action:
+ return ActionEntry::FromJson(json);
+ default:
+ return nullptr;
+ }
}
}
diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h
index 57f6bc1a000..62e892fd144 100644
--- a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h
+++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h
@@ -25,6 +25,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
static com_ptr FromJson(const Json::Value& json);
virtual Json::Value ToJson() const;
+ virtual Model::NewTabMenuEntry Copy() const = 0;
WINRT_PROPERTY(NewTabMenuEntryType, Type, NewTabMenuEntryType::Invalid);
diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl
index acf1f33f6f4..c76de6e8487 100644
--- a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl
+++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl
@@ -61,6 +61,7 @@ namespace Microsoft.Terminal.Settings.Model
Boolean AllowEmpty;
IVector Entries();
+ IVector RawEntries;
}
[default_interface] unsealed runtimeclass ProfileCollectionEntry : NewTabMenuEntry
diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp
index 5f01a293beb..251b7cdc6a5 100644
--- a/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp
+++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp
@@ -8,56 +8,67 @@
#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" };
static constexpr std::string_view IconKey{ "icon" };
-ProfileEntry::ProfileEntry() noexcept :
- ProfileEntry{ winrt::hstring{} }
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
-}
-
-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)
+ ProfileEntry::ProfileEntry() noexcept :
+ ProfileEntry{ winrt::hstring{} }
{
- JsonUtils::SetValueForKey(json, ProfileKey, _ProfileName);
}
- else
+
+ ProfileEntry::ProfileEntry(const winrt::hstring& profile) noexcept :
+ ProfileEntryT(NewTabMenuEntryType::Profile),
+ _ProfileName{ profile }
{
- JsonUtils::SetValueForKey(json, ProfileKey, _Profile.Guid());
}
- JsonUtils::SetValueForKey(json, IconKey, _Icon);
+ Json::Value ProfileEntry::ToJson() const
+ {
+ auto json = NewTabMenuEntry::ToJson();
- return json;
-}
+ // 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());
+ }
+ JsonUtils::SetValueForKey(json, IconKey, _Icon);
-winrt::com_ptr ProfileEntry::FromJson(const Json::Value& json)
-{
- auto entry = winrt::make_self();
+ return json;
+ }
+
+ winrt::com_ptr ProfileEntry::FromJson(const Json::Value& json)
+ {
+ auto entry = winrt::make_self();
- JsonUtils::GetValueForKey(json, ProfileKey, entry->_ProfileName);
- JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
+ JsonUtils::GetValueForKey(json, ProfileKey, entry->_ProfileName);
+ JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
- return entry;
+ return entry;
+ }
+
+ Model::NewTabMenuEntry ProfileEntry::Copy() const
+ {
+ auto entry{ winrt::make_self() };
+ entry->_Profile = _Profile;
+ entry->_ProfileIndex = _ProfileIndex;
+ entry->_ProfileName = _ProfileName;
+ entry->_Icon = _Icon;
+ return *entry;
+ }
}
diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.h b/src/cascadia/TerminalSettingsModel/ProfileEntry.h
index 9ea78497f1f..e9274e28f49 100644
--- a/src/cascadia/TerminalSettingsModel/ProfileEntry.h
+++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.h
@@ -28,6 +28,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
ProfileEntry() noexcept;
explicit ProfileEntry(const winrt::hstring& profile) noexcept;
+ Model::NewTabMenuEntry Copy() const override;
+
Json::Value ToJson() const override;
static com_ptr FromJson(const Json::Value& json);
diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp
index 1d2539da2d8..6f2a4b531f5 100644
--- a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp
+++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp
@@ -9,14 +9,21 @@
#include "RemainingProfilesEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
-using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
-RemainingProfilesEntry::RemainingProfilesEntry() noexcept :
- RemainingProfilesEntryT(NewTabMenuEntryType::RemainingProfiles)
+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();
+ winrt::com_ptr RemainingProfilesEntry::FromJson(const Json::Value&)
+ {
+ return winrt::make_self();
+ }
+
+ Model::NewTabMenuEntry RemainingProfilesEntry::Copy() const
+ {
+ return winrt::make();
+ }
}
diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h
index 153669e4f7b..6d0e21ed85f 100644
--- a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h
+++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h
@@ -25,6 +25,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
RemainingProfilesEntry() noexcept;
+ Model::NewTabMenuEntry Copy() const override;
+
static com_ptr FromJson(const Json::Value& json);
};
}
diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp
index d6cee21090c..b5dd4ff0aea 100644
--- a/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp
+++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp
@@ -8,14 +8,21 @@
#include "SeparatorEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
-using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
-SeparatorEntry::SeparatorEntry() noexcept :
- SeparatorEntryT(NewTabMenuEntryType::Separator)
+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();
+ winrt::com_ptr SeparatorEntry::FromJson(const Json::Value&)
+ {
+ return winrt::make_self();
+ }
+
+ Model::NewTabMenuEntry SeparatorEntry::Copy() const
+ {
+ return winrt::make();
+ }
}
diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.h b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h
index e074ce251c7..93a38121a92 100644
--- a/src/cascadia/TerminalSettingsModel/SeparatorEntry.h
+++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h
@@ -24,6 +24,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
SeparatorEntry() noexcept;
+ Model::NewTabMenuEntry Copy() const override;
+
static com_ptr FromJson(const Json::Value& json);
};
}