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