From 0e304d819786f8d1f661562c87b03189ca769e1e Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Thu, 14 Mar 2024 15:19:29 -0700 Subject: [PATCH 01/22] Add New Tab Menu Customization to Settings UI --- .../TerminalSettingsEditor/MainPage.cpp | 10 + .../TerminalSettingsEditor/MainPage.h | 1 + .../TerminalSettingsEditor/MainPage.xaml | 7 + ...Microsoft.Terminal.Settings.Editor.vcxproj | 22 ++ ...t.Terminal.Settings.Editor.vcxproj.filters | 2 + .../TerminalSettingsEditor/NewTabMenu.cpp | 78 +++++ .../TerminalSettingsEditor/NewTabMenu.h | 47 +++ .../TerminalSettingsEditor/NewTabMenu.idl | 24 ++ .../TerminalSettingsEditor/NewTabMenu.xaml | 258 ++++++++++++++ .../NewTabMenuViewModel.cpp | 321 ++++++++++++++++++ .../NewTabMenuViewModel.h | 113 ++++++ .../NewTabMenuViewModel.idl | 68 ++++ .../Resources/en-US/Resources.resw | 68 ++++ .../SettingContainer.cpp | 10 + .../TerminalSettingsEditor/SettingContainer.h | 1 + .../SettingContainer.idl | 3 + .../SettingContainerStyle.xaml | 18 +- .../TerminalSettingsEditor/ViewModelHelpers.h | 33 +- .../TerminalSettingsModel/ActionEntry.cpp | 7 + .../TerminalSettingsModel/ActionEntry.h | 2 + .../TerminalSettingsModel/FolderEntry.cpp | 53 ++- .../TerminalSettingsModel/FolderEntry.h | 2 + .../GlobalAppSettings.cpp | 39 +++ .../MatchProfilesEntry.cpp | 9 + .../MatchProfilesEntry.h | 2 + .../TerminalSettingsModel/ProfileEntry.cpp | 9 + .../TerminalSettingsModel/ProfileEntry.h | 2 + .../RemainingProfilesEntry.cpp | 6 + .../RemainingProfilesEntry.h | 2 + .../TerminalSettingsModel/SeparatorEntry.cpp | 5 + .../TerminalSettingsModel/SeparatorEntry.h | 2 + 31 files changed, 1203 insertions(+), 21 deletions(-) create mode 100644 src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp create mode 100644 src/cascadia/TerminalSettingsEditor/NewTabMenu.h create mode 100644 src/cascadia/TerminalSettingsEditor/NewTabMenu.idl create mode 100644 src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml create mode 100644 src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp create mode 100644 src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h create mode 100644 src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 76bbeacb248..160f0869e83 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -16,6 +16,7 @@ #include "AddProfile.h" #include "InteractionViewModel.h" #include "LaunchViewModel.h" +#include "NewTabMenuViewModel.h" #include "..\types\inc\utils.hpp" #include <..\WinRTUtils\inc\Utils.h> @@ -40,6 +41,7 @@ static const std::wstring_view launchTag{ L"Launch_Nav" }; static const std::wstring_view interactionTag{ L"Interaction_Nav" }; static const std::wstring_view renderingTag{ L"Rendering_Nav" }; static const std::wstring_view actionsTag{ L"Actions_Nav" }; +static const std::wstring_view newTabMenuTag{ L"NewTabMenu_Nav" }; static const std::wstring_view globalProfileTag{ L"GlobalProfile_Nav" }; static const std::wstring_view addProfileTag{ L"AddProfile" }; static const std::wstring_view colorSchemesTag{ L"ColorSchemes_Nav" }; @@ -59,6 +61,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation InitializeComponent(); _UpdateBackgroundForMica(); + _newTabMenuPageVM = winrt::make(_settingsClone); _colorSchemesPageVM = winrt::make(_settingsClone); _colorSchemesPageViewModelChangedRevoker = _colorSchemesPageVM.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) { const auto settingName{ args.PropertyName() }; @@ -134,6 +137,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _InitializeProfilesList(); // Update the Nav State with the new version of the settings _colorSchemesPageVM.UpdateSettings(_settingsClone); + get_self(_newTabMenuPageVM)->UpdateSettings(_settingsClone); // We'll update the profile in the _profilesNavState whenever we actually navigate to one @@ -378,6 +382,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Actions/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } + else if (clickedItemTag == newTabMenuTag) + { + contentFrame().Navigate(xaml_typename(), _newTabMenuPageVM); + const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None); + _breadcrumbs.Append(crumb); + } else if (clickedItemTag == globalProfileTag) { auto profileVM{ _viewModelForProfile(_settingsClone.ProfileDefaults(), _settingsClone) }; diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.h b/src/cascadia/TerminalSettingsEditor/MainPage.h index 1535c85a33c..d08424e5610 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.h +++ b/src/cascadia/TerminalSettingsEditor/MainPage.h @@ -74,6 +74,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void _MoveXamlParsedNavItemsIntoItemSource(); winrt::Microsoft::Terminal::Settings::Editor::ColorSchemesPageViewModel _colorSchemesPageVM{ nullptr }; + winrt::Microsoft::Terminal::Settings::Editor::NewTabMenuViewModel _newTabMenuPageVM{ nullptr }; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _profileViewModelChangedRevoker; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _colorSchemesPageViewModelChangedRevoker; diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.xaml b/src/cascadia/TerminalSettingsEditor/MainPage.xaml index d63d634d2e0..fbe8e5bbcbf 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.xaml +++ b/src/cascadia/TerminalSettingsEditor/MainPage.xaml @@ -142,6 +142,13 @@ + + + + + + Launch.xaml + + NewTabMenu.xaml + MainPage.xaml @@ -106,6 +109,10 @@ LaunchViewModel.idl Code + + NewTabMenuViewModel.idl + Code + Profiles_Base.xaml Code @@ -160,6 +167,9 @@ Designer + + Designer + Designer @@ -210,6 +220,9 @@ Launch.xaml + + NewTabMenu.xaml + Create @@ -249,6 +262,10 @@ LaunchViewModel.idl Code + + NewTabMenuViewModel.idl + Code + Profiles_Base.xaml Code @@ -307,6 +324,10 @@ Launch.xaml Code + + NewTabMenu.xaml + Code + Interaction.xaml Code @@ -326,6 +347,7 @@ + Profiles_Base.xaml Code diff --git a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters index 6e8ad60b9e8..7f579df69f8 100644 --- a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters +++ b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters @@ -27,6 +27,7 @@ + @@ -49,5 +50,6 @@ + diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp new file mode 100644 index 00000000000..b7bc57a0f27 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "NewTabMenu.h" +#include "NewTabMenu.g.cpp" +#include "NewTabMenuEntryTemplateSelector.g.cpp" +#include "EnumEntry.h" + +#include "NewTabMenuViewModel.h" + +#include + +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Navigation; +using namespace winrt::Windows::Foundation; +using namespace winrt::Microsoft::Terminal::Settings::Model; + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + NewTabMenu::NewTabMenu() + { + InitializeComponent(); + + _entryTemplateSelector = Resources().Lookup(box_value(L"NewTabMenuEntryTemplateSelector")).as(); + + // TODO CARLOS: set auto props, if necessary + //Automation::AutomationProperties::SetName(NewTabMenuModeComboBox(), RS_(L"Globals_NewTabMenuModeSetting/Text")); + //Automation::AutomationProperties::SetHelpText(NewTabMenuModeComboBox(), RS_(L"Globals_NewTabMenuModeSetting/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); + //Automation::AutomationProperties::SetHelpText(PosXBox(), RS_(L"Globals_InitialPosXBox/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); + //Automation::AutomationProperties::SetHelpText(PosYBox(), RS_(L"Globals_InitialPosYBox/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); + //Automation::AutomationProperties::SetHelpText(UseDefaultNewTabMenuPositionCheckbox(), RS_(L"Globals_DefaultNewTabMenuPositionCheckbox/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); + //Automation::AutomationProperties::SetName(CenterOnNewTabMenuToggle(), RS_(L"Globals_CenterOnNewTabMenu/Text")); + //Automation::AutomationProperties::SetHelpText(CenterOnNewTabMenuToggle(), RS_(L"Globals_CenterOnNewTabMenu/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); + } + + void NewTabMenu::OnNavigatedTo(const NavigationEventArgs& e) + { + _ViewModel = e.Parameter().as(); + } + + void NewTabMenu::FolderNameTextBox_TextChanged(const IInspectable& sender, const Controls::TextChangedEventArgs& /*e*/) + { + const auto isTextEmpty = sender.as().Text().empty(); + AddFolderButton().IsEnabled(!isTextEmpty); + } + + DataTemplate NewTabMenuEntryTemplateSelector::SelectTemplateCore(const IInspectable& item, const DependencyObject& /*container*/) + { + return SelectTemplateCore(item); + } + + DataTemplate NewTabMenuEntryTemplateSelector::SelectTemplateCore(const IInspectable& item) + { + if (const auto entryVM = item.try_as()) + { + switch (entryVM.Type()) + { + case Model::NewTabMenuEntryType::Profile: + return ProfileEntryTemplate(); + case Model::NewTabMenuEntryType::Separator: + return SeparatorEntryTemplate(); + case Model::NewTabMenuEntryType::Folder: + return FolderEntryTemplate(); + case Model::NewTabMenuEntryType::MatchProfiles: + return MatchProfilesEntryTemplate(); + case Model::NewTabMenuEntryType::RemainingProfiles: + return RemainingProfilesEntryTemplate(); + case Model::NewTabMenuEntryType::Invalid: + default: + assert(false); + return nullptr; + } + } + assert(false); + return nullptr; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.h b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h new file mode 100644 index 00000000000..1c083b77961 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "NewTabMenu.g.h" +#include "NewTabMenuEntryTemplateSelector.g.h" +#include "Utils.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct NewTabMenu : public HasScrollViewer, NewTabMenuT + { + public: + NewTabMenu(); + + void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); + void FolderNameTextBox_TextChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::TextChangedEventArgs& e); + + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + WINRT_OBSERVABLE_PROPERTY(Editor::NewTabMenuViewModel, ViewModel, _PropertyChangedHandlers, nullptr); + + private: + Editor::NewTabMenuEntryTemplateSelector _entryTemplateSelector{ nullptr }; + }; + + struct NewTabMenuEntryTemplateSelector : public NewTabMenuEntryTemplateSelectorT + { + public: + NewTabMenuEntryTemplateSelector() = default; + + Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item, const Windows::UI::Xaml::DependencyObject& container); + Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item); + + WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, ProfileEntryTemplate, nullptr); + WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, SeparatorEntryTemplate, nullptr); + WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, FolderEntryTemplate, nullptr); + WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, MatchProfilesEntryTemplate, nullptr); + WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, RemainingProfilesEntryTemplate, nullptr); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + BASIC_FACTORY(NewTabMenu); + BASIC_FACTORY(NewTabMenuEntryTemplateSelector); +} diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.idl b/src/cascadia/TerminalSettingsEditor/NewTabMenu.idl new file mode 100644 index 00000000000..28a08342fd5 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.idl @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "NewTabMenuViewModel.idl"; + +namespace Microsoft.Terminal.Settings.Editor +{ + [default_interface] runtimeclass NewTabMenu : Windows.UI.Xaml.Controls.Page + { + NewTabMenu(); + NewTabMenuViewModel ViewModel { get; }; + } + + [default_interface] runtimeclass NewTabMenuEntryTemplateSelector : Windows.UI.Xaml.Controls.DataTemplateSelector + { + NewTabMenuEntryTemplateSelector(); + + Windows.UI.Xaml.DataTemplate ProfileEntryTemplate; + Windows.UI.Xaml.DataTemplate SeparatorEntryTemplate; + Windows.UI.Xaml.DataTemplate FolderEntryTemplate; + Windows.UI.Xaml.DataTemplate MatchProfilesEntryTemplate; + Windows.UI.Xaml.DataTemplate RemainingProfilesEntryTemplate; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml new file mode 100644 index 00000000000..b7aee89a8ff --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp new file mode 100644 index 00000000000..54ca02e5c2f --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp @@ -0,0 +1,321 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "NewTabMenuViewModel.h" +#include + +#include "NewTabMenuViewModel.g.cpp" +#include "NewTabMenuEntryViewModel.g.cpp" +#include "ProfileEntryViewModel.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(IVector settingsModelEntries) + { + auto result = single_threaded_observable_vector(); + 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()) + { + const auto profileEntryVM = make(profileEntry); + result.Append(profileEntryVM); + } + break; + } + case NewTabMenuEntryType::Separator: + { + if (const auto& separatorEntry = entry.as()) + { + const auto separatorEntryVM = make(separatorEntry); + result.Append(separatorEntryVM); + } + break; + } + case NewTabMenuEntryType::Folder: + { + if (const auto& folderEntry = entry.as()) + { + const auto folderEntryVM = make(folderEntry); + result.Append(folderEntryVM); + } + break; + } + case NewTabMenuEntryType::MatchProfiles: + { + if (const auto& matchProfilesEntry = entry.as()) + { + const auto matchProfilesEntryVM = make(matchProfilesEntry); + result.Append(matchProfilesEntryVM); + } + break; + } + case NewTabMenuEntryType::RemainingProfiles: + { + if (const auto& remainingProfilesEntry = entry.as()) + { + const auto remainingProfilesEntryVM = make(remainingProfilesEntry); + result.Append(remainingProfilesEntryVM); + } + break; + } + case NewTabMenuEntryType::Invalid: + default: + break; + } + } + return result; + } + + bool NewTabMenuViewModel::IsRemainingProfilesEntryMissing(const IObservableVector& entries) + { + for (const auto& entry : entries) + { + switch (entry.Type()) + { + case NewTabMenuEntryType::RemainingProfiles: + { + return false; + } + case NewTabMenuEntryType::Folder: + { + if (!IsRemainingProfilesEntryMissing(entry.as().Entries())) + { + return false; + } + } + default: + break; + } + } + return true; + }; + + 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"); + } + }); + } + + void NewTabMenuViewModel::UpdateSettings(Model::CascadiaSettings settings) + { + _Settings = settings; + _NotifyChanges(L"AvailableProfiles"); + + const auto& newTabMenuEntries = _Settings.GlobalSettings().NewTabMenu(); + Entries(_ConvertToViewModelEntries(newTabMenuEntries)); + + SelectedProfile(AvailableProfiles().GetAt(0)); + + _Entries.VectorChanged([this](auto&&, const IVectorChangedEventArgs& args) { + switch (args.CollectionChange()) + { + case CollectionChange::Reset: + { + // fully replace settings model with _Entries + for (const auto& entry : _Entries) + { + auto modelEntries = single_threaded_vector(); + modelEntries.Append(NewTabMenuEntryViewModel::GetModel(entry)); + + _Settings.GlobalSettings().NewTabMenu(modelEntries); + } + return; + } + case CollectionChange::ItemInserted: + { + const auto& insertedEntry = _Entries.GetAt(args.Index()); + auto newTabMenu = _Settings.GlobalSettings().NewTabMenu(); + newTabMenu.InsertAt(args.Index(), NewTabMenuEntryViewModel::GetModel(insertedEntry)); + return; + } + case CollectionChange::ItemRemoved: + { + auto newTabMenu = _Settings.GlobalSettings().NewTabMenu(); + newTabMenu.RemoveAt(args.Index()); + return; + } + case CollectionChange::ItemChanged: + { + auto newTabMenu = _Settings.GlobalSettings().NewTabMenu(); + const auto modifiedEntry = _Entries.GetAt(args.Index()); + newTabMenu.SetAt(args.Index(), NewTabMenuEntryViewModel::GetModel(modifiedEntry)); + return; + } + } + }); + } + + void NewTabMenuViewModel::RequestAddSelectedProfileEntry() + { + if (_SelectedProfile) + { + Model::ProfileEntry profileEntry; + profileEntry.Profile(_SelectedProfile); + _Settings.GlobalSettings().NewTabMenu().Append(profileEntry); + + _Entries.Append(make(profileEntry)); + } + } + + void NewTabMenuViewModel::RequestAddSeparatorEntry() + { + Model::SeparatorEntry separatorEntry; + _Settings.GlobalSettings().NewTabMenu().Append(separatorEntry); + + _Entries.Append(make(separatorEntry)); + } + + void NewTabMenuViewModel::RequestAddFolderEntry() + { + Model::FolderEntry folderEntry; + folderEntry.Name(_FolderName); + _Settings.GlobalSettings().NewTabMenu().Append(folderEntry); + + _Entries.Append(make(folderEntry)); + + // Clear the field after adding the entry + FolderName({}); + } + + void NewTabMenuViewModel::RequestAddProfileMatcherEntry() + { + Model::MatchProfilesEntry matchProfilesEntry; + matchProfilesEntry.Name(_ProfileMatcherName); + matchProfilesEntry.Source(_ProfileMatcherSource); + matchProfilesEntry.Commandline(_ProfileMatcherCommandline); + _Settings.GlobalSettings().NewTabMenu().Append(matchProfilesEntry); + + _Entries.Append(make(matchProfilesEntry)); + + // Clear the fields after adding the entry + ProfileMatcherName({}); + ProfileMatcherSource({}); + ProfileMatcherCommandline({}); + } + + void NewTabMenuViewModel::RequestAddRemainingProfilesEntry() + { + Model::RemainingProfilesEntry remainingProfilesEntry; + _Settings.GlobalSettings().NewTabMenu().Append(remainingProfilesEntry); + + _Entries.Append(make(remainingProfilesEntry)); + } + + 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::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 } + { + } + + SeparatorEntryViewModel::SeparatorEntryViewModel(Model::SeparatorEntry separatorEntry) : + SeparatorEntryViewModelT(Model::NewTabMenuEntryType::Separator), + _SeparatorEntry{ separatorEntry } + { + } + + FolderEntryViewModel::FolderEntryViewModel(Model::FolderEntry folderEntry) : + FolderEntryViewModelT(Model::NewTabMenuEntryType::Folder), + _FolderEntry{ folderEntry } + { + _Entries = _ConvertToViewModelEntries(_FolderEntry.Entries()); + } + + MatchProfilesEntryViewModel::MatchProfilesEntryViewModel(Model::MatchProfilesEntry matchProfilesEntry) : + MatchProfilesEntryViewModelT(Model::NewTabMenuEntryType::MatchProfiles), + _MatchProfilesEntry{ matchProfilesEntry } + { + } + + hstring MatchProfilesEntryViewModel::DisplayText() const + { + std::wstringstream ss; + if (const auto profileName = _MatchProfilesEntry.Name(); !profileName.empty()) + { + ss << fmt::format(L"profile: {}, ", profileName); + } + if (const auto commandline = _MatchProfilesEntry.Commandline(); !commandline.empty()) + { + ss << fmt::format(L"profile: {}, ", commandline); + } + if (const auto source = _MatchProfilesEntry.Source(); !source.empty()) + { + ss << fmt::format(L"profile: {}, ", source); + } + + // Chop off the last ", " + auto s = ss.str(); + return winrt::hstring{ s.substr(0, s.size() - 2) }; + } + + 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..3be5bb0c035 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "NewTabMenuViewModel.g.h" +#include "NewTabMenuEntryViewModel.g.h" +#include "ProfileEntryViewModel.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); + + static bool IsRemainingProfilesEntryMissing(const Windows::Foundation::Collections::IObservableVector& entries); + + void UpdateSettings(Model::CascadiaSettings settings); + + void RequestAddSelectedProfileEntry(); + void RequestAddSeparatorEntry(); + void RequestAddFolderEntry(); + void RequestAddProfileMatcherEntry(); + void RequestAddRemainingProfilesEntry(); + + Windows::Foundation::Collections::IObservableVector AvailableProfiles() { return _Settings.AllProfiles(); } + VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector, 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, FolderName); + + private: + Model::CascadiaSettings _Settings{ nullptr }; + }; + + 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 SeparatorEntryViewModel : SeparatorEntryViewModelT + { + public: + SeparatorEntryViewModel(Model::SeparatorEntry separatorEntry); + + VIEW_MODEL_OBSERVABLE_PROPERTY(Model::SeparatorEntry, SeparatorEntry, nullptr); + }; + + struct FolderEntryViewModel : FolderEntryViewModelT + { + public: + FolderEntryViewModel(Model::FolderEntry folderEntry); + + GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, Name); + GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, Icon); + + VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector, Entries); + VIEW_MODEL_OBSERVABLE_PROPERTY(Model::FolderEntry, FolderEntry, nullptr); + }; + + 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 remainingProfielsEntry); + + VIEW_MODEL_OBSERVABLE_PROPERTY(Model::RemainingProfilesEntry, RemainingProfilesEntry, nullptr); + }; +}; + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + BASIC_FACTORY(NewTabMenuViewModel); + BASIC_FACTORY(ProfileEntryViewModel); + 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..9a77c1f3263 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ProfileViewModel.idl"; + +namespace Microsoft.Terminal.Settings.Editor +{ + runtimeclass NewTabMenuViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged + { + NewTabMenuViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings); + + IObservableVector Entries; + IObservableVector AvailableProfiles { get; }; + Microsoft.Terminal.Settings.Model.Profile SelectedProfile; + + String ProfileMatcherName; + String ProfileMatcherSource; + String ProfileMatcherCommandline; + + String FolderName; + + void RequestAddSelectedProfileEntry(); + void RequestAddSeparatorEntry(); + void RequestAddFolderEntry(); + void RequestAddProfileMatcherEntry(); + void RequestAddRemainingProfilesEntry(); + + static Boolean IsRemainingProfilesEntryMissing(IObservableVector entries); + } + + [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 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); + + String Name; + String Icon; + 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 cffae127f5a..bd2390cada9 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -1839,4 +1839,72 @@ Non-monospace fonts: This is a label that is followed by a list of proportional fonts. + + Warning: + Title for the warning info bar used when a non monospace font face is chosen to indicate that there may be visual artifacts + + + Choosing a non-monospaced font will likely result in visual artifacts. Use at your own discretion. + Warning info bar used when a non monospace font face is chosen to indicate that there may be visual artifacts + + + 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=">"} + + + <Remaining profiles> + {Locked="<"}{Locked=">"} + + + 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. + \ No newline at end of file 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 0e12e7d3de7..066fce89d1a 100644 --- a/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml +++ b/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml @@ -180,10 +180,15 @@ + - + + @@ -197,7 +202,7 @@ Style="{StaticResource SettingsPageItemDescriptionStyle}" Text="{TemplateBinding HelpText}" /> - @@ -224,10 +229,15 @@ + - + + @@ -241,7 +251,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) \ + { \ + const auto t = target; \ + if (t.name() != value) \ + { \ + t.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 b48207e74e9..5d3279a0973 100644 --- a/src/cascadia/TerminalSettingsModel/ActionEntry.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionEntry.cpp @@ -34,3 +34,10 @@ winrt::com_ptr ActionEntry::FromJson(const Json::Value& json) return entry; } + +winrt::com_ptr ActionEntry::Copy() const +{ + auto entry = winrt::make_self(); + entry->_ActionId = _ActionId; + return entry; +} diff --git a/src/cascadia/TerminalSettingsModel/ActionEntry.h b/src/cascadia/TerminalSettingsModel/ActionEntry.h index 9c89077828c..de2551064da 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; + winrt::com_ptr 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..f2ad75e290a 100644 --- a/src/cascadia/TerminalSettingsModel/FolderEntry.cpp +++ b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp @@ -3,6 +3,11 @@ #include "pch.h" #include "FolderEntry.h" +#include "ProfileEntry.h" +#include "SeparatorEntry.h" +#include "RemainingProfilesEntry.h" +#include "MatchProfilesEntry.h" +#include "ActionEntry.h" #include "JsonUtils.h" #include "TerminalSettingsSerializationHelpers.h" @@ -83,7 +88,7 @@ IVector FolderEntry::Entries() const // A profile is filtered out if it is not valid, so if it was not resolved case NewTabMenuEntryType::Profile: { - const auto profileEntry = entry.as(); + const auto profileEntry = entry.as(); if (profileEntry.Profile() == nullptr) { continue; @@ -95,7 +100,7 @@ IVector FolderEntry::Entries() const case NewTabMenuEntryType::RemainingProfiles: case NewTabMenuEntryType::MatchProfiles: { - const auto profileCollectionEntry = entry.as(); + const auto profileCollectionEntry = entry.as(); if (profileCollectionEntry.Profiles().Size() == 0) { continue; @@ -122,3 +127,47 @@ IVector FolderEntry::Entries() const return result; } + +winrt::com_ptr FolderEntry::Copy() const +{ + auto entry = winrt::make_self(); + entry->_Name = _Name; + entry->_Icon = _Icon; + entry->_Inlining = _Inlining; + entry->_AllowEmpty = _AllowEmpty; + + if (_Entries) + { + entry->_Entries = winrt::single_threaded_vector(); + for (const auto& e : _Entries) + { + switch (e.Type()) + { + case NewTabMenuEntryType::Profile: + entry->_Entries.Append(*winrt::get_self(e.as())->Copy()); + break; + case NewTabMenuEntryType::Separator: + entry->_Entries.Append(*winrt::get_self(e.as())->Copy()); + break; + case NewTabMenuEntryType::Folder: + entry->_Entries.Append(*winrt::get_self(e.as())->Copy()); + break; + case NewTabMenuEntryType::RemainingProfiles: + entry->_Entries.Append(*winrt::get_self(e.as())->Copy()); + break; + case NewTabMenuEntryType::MatchProfiles: + entry->_Entries.Append(*winrt::get_self(e.as())->Copy()); + break; + case NewTabMenuEntryType::Action: + { + entry->_Entries.Append(*winrt::get_self(e.as())->Copy()); + break; + } + case NewTabMenuEntryType::Invalid: + // ignore invalid + break; + } + } + } + return entry; +} diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.h b/src/cascadia/TerminalSettingsModel/FolderEntry.h index 619d228fa7b..83fb40f4a1d 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; + winrt::com_ptr Copy() const; + Json::Value ToJson() const override; static com_ptr FromJson(const Json::Value& json); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index e7f930c175a..d419314aa27 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -6,6 +6,12 @@ #include "../../types/inc/Utils.hpp" #include "JsonUtils.h" #include "KeyChordSerialization.h" +#include "FolderEntry.h" +#include "ProfileEntry.h" +#include "SeparatorEntry.h" +#include "RemainingProfilesEntry.h" +#include "MatchProfilesEntry.h" +#include "ActionEntry.h" #include "GlobalAppSettings.g.cpp" @@ -79,6 +85,39 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_themes.Insert(kv.Key(), *themeImpl->Copy()); } } + if (_NewTabMenu) + { + globals->_NewTabMenu = winrt::single_threaded_vector(); + for (auto entry : *_NewTabMenu) + { + switch (entry.Type()) + { + case NewTabMenuEntryType::Profile: + globals->_NewTabMenu->Append(*winrt::get_self(entry.as())->Copy()); + break; + case NewTabMenuEntryType::Separator: + globals->_NewTabMenu->Append(*winrt::get_self(entry.as())->Copy()); + break; + case NewTabMenuEntryType::Folder: + globals->_NewTabMenu->Append(*winrt::get_self(entry.as())->Copy()); + break; + case NewTabMenuEntryType::RemainingProfiles: + globals->_NewTabMenu->Append(*winrt::get_self(entry.as())->Copy()); + break; + case NewTabMenuEntryType::MatchProfiles: + globals->_NewTabMenu->Append(*winrt::get_self(entry.as())->Copy()); + break; + case NewTabMenuEntryType::Action: + { + globals->_NewTabMenu->Append(*winrt::get_self(entry.as())->Copy()); + break; + } + case NewTabMenuEntryType::Invalid: + // ignore invalid + break; + } + } + } for (const auto& parent : _parents) { diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp index 2873679018e..154b29480af 100644 --- a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp +++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp @@ -71,3 +71,12 @@ bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile) return isMatching.value_or(false); } + +winrt::com_ptr MatchProfilesEntry::Copy() const +{ + auto entry = winrt::make_self(); + entry->_Name = _Name; + entry->_Commandline = _Commandline; + entry->_Source = _Source; + return entry; +} diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h index 464a6780c12..4825d94c8c4 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; + winrt::com_ptr Copy() const; + Json::Value ToJson() const override; static com_ptr FromJson(const Json::Value& json); diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp index c857eea8678..599237a04bf 100644 --- a/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp +++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp @@ -57,3 +57,12 @@ winrt::com_ptr ProfileEntry::FromJson(const Json::Value& json) return entry; } + +winrt::com_ptr ProfileEntry::Copy() const +{ + auto entry{ winrt::make_self() }; + entry->_Profile = _Profile; + entry->_ProfileIndex = _ProfileIndex; + entry->_ProfileName = _ProfileName; + return entry; +} diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.h b/src/cascadia/TerminalSettingsModel/ProfileEntry.h index fe0f4573d29..6b7e4104640 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; + winrt::com_ptr Copy() const; + 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..eb77e295877 100644 --- a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp +++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp @@ -20,3 +20,9 @@ winrt::com_ptr RemainingProfilesEntry::FromJson(const Json::Val { return winrt::make_self(); } + +winrt::com_ptr RemainingProfilesEntry::Copy() const +{ + auto entry = winrt::make_self(); + return entry; +} diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h index 153669e4f7b..4cf32a57c46 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; + winrt::com_ptr Copy() const; + static com_ptr FromJson(const Json::Value& json); }; } diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp index d6cee21090c..5afd704c9a7 100644 --- a/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp +++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp @@ -19,3 +19,8 @@ winrt::com_ptr SeparatorEntry::FromJson(const Json::Value&) { return winrt::make_self(); } + +winrt::com_ptr SeparatorEntry::Copy() const +{ + return winrt::make_self(); +} diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.h b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h index e074ce251c7..dd75c26e6e8 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; + winrt::com_ptr Copy() const; + static com_ptr FromJson(const Json::Value& json); }; } From 2cef0c1dc60a7446ee74af92a3a6417b76b2cac6 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Fri, 20 Sep 2024 11:10:27 -0700 Subject: [PATCH 02/22] [broken] drag and drop don't work --- .../TerminalSettingsEditor/NewTabMenu.cpp | 55 ++++++++++++++++++- .../TerminalSettingsEditor/NewTabMenu.h | 10 ++++ .../TerminalSettingsEditor/NewTabMenu.xaml | 24 ++++++-- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp index b7bc57a0f27..370bbbd447f 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp @@ -39,11 +39,60 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _ViewModel = e.Parameter().as(); } - void NewTabMenu::FolderNameTextBox_TextChanged(const IInspectable& sender, const Controls::TextChangedEventArgs& /*e*/) - { + void NewTabMenu::FolderNameTextBox_TextChanged(const IInspectable& sender, const Controls::TextChangedEventArgs& /*e*/) + { const auto isTextEmpty = sender.as().Text().empty(); AddFolderButton().IsEnabled(!isTextEmpty); - } + } + + void NewTabMenu::TreeView_DragItemsStarting(const winrt::Microsoft::UI::Xaml::Controls::TreeView& /*sender*/, const winrt::Microsoft::UI::Xaml::Controls::TreeViewDragItemsStartingEventArgs& e) + { + _draggedEntry = e.Items().GetAt(0).as(); + } + + void NewTabMenu::TreeView_DragOver(const IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::DragEventArgs& e) + { + e.AcceptedOperation(Windows::ApplicationModel::DataTransfer::DataPackageOperation::Move); + OutputDebugString(L"TreeView_DragOver\n"); + } + + void NewTabMenu::TreeView_Drop(const IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::DragEventArgs& /*e*/) + { + OutputDebugString(L"TreeView_Drop\n"); + } + + void NewTabMenu::TreeView_DragItemsCompleted(const winrt::Microsoft::UI::Xaml::Controls::TreeView& /*sender*/, const winrt::Microsoft::UI::Xaml::Controls::TreeViewDragItemsCompletedEventArgs& /*e*/) + { + _draggedEntry = nullptr; + } + + void NewTabMenu::TreeViewItem_DragOver(const IInspectable& /*sender*/, const DragEventArgs& e) + { + e.AcceptedOperation(Windows::ApplicationModel::DataTransfer::DataPackageOperation::Move); + } + + void NewTabMenu::TreeViewItem_Drop(const IInspectable& sender, const DragEventArgs& /*e*/) + { + auto element = sender.as(); + auto entry = element.DataContext().as(); + if (entry.Type() == NewTabMenuEntryType::Folder) + { + // add to the current folder + auto folderEntry = entry.as(); + folderEntry.Entries().Append(_draggedEntry); + } + else + { + // create a parent folder and add both entries to it + // TODO CARLOS: localize + auto folderEntry = winrt::make(FolderEntry{ L"New Folder" }); + folderEntry.Entries().Append(entry); + folderEntry.Entries().Append(_draggedEntry); + + // TODO CARLOS: this is wrong, we should be placing the folder in the same place as before, but we're testing this stuff out + _ViewModel.Entries().Append(folderEntry); + } + } DataTemplate NewTabMenuEntryTemplateSelector::SelectTemplateCore(const IInspectable& item, const DependencyObject& /*container*/) { diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.h b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h index 1c083b77961..64fddd224fb 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.h +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h @@ -17,11 +17,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); void FolderNameTextBox_TextChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::TextChangedEventArgs& e); + void TreeView_DragItemsStarting(const winrt::Microsoft::UI::Xaml::Controls::TreeView& sender, const winrt::Microsoft::UI::Xaml::Controls::TreeViewDragItemsStartingEventArgs& e); + void TreeView_DragOver(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e); + void TreeView_Drop(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e); + void TreeView_DragItemsCompleted(const winrt::Microsoft::UI::Xaml::Controls::TreeView& sender, const winrt::Microsoft::UI::Xaml::Controls::TreeViewDragItemsCompletedEventArgs& e); + + void TreeViewItem_DragOver(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e); + void TreeViewItem_Drop(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e); + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); WINRT_OBSERVABLE_PROPERTY(Editor::NewTabMenuViewModel, ViewModel, _PropertyChangedHandlers, nullptr); private: Editor::NewTabMenuEntryTemplateSelector _entryTemplateSelector{ nullptr }; + + Editor::NewTabMenuEntryViewModel _draggedEntry{ nullptr }; }; struct NewTabMenuEntryTemplateSelector : public NewTabMenuEntryTemplateSelectorT diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml index b7aee89a8ff..d5b09c61097 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml @@ -36,7 +36,9 @@ - + @@ -52,7 +54,9 @@ + FontStyle="Italic" + DragOver="TreeViewItem_DragOver" + Drop="TreeViewItem_Drop"/> + + - - - - - - - - + + + + + + - + + + - - - - - + + + + + + + - - - - + Height="16" + VerticalAlignment="Center" />--> + + + + + + + + + - + + + - + + + + ProfileEntryTemplate="{StaticResource ProfileEntryTemplate}" + RemainingProfilesEntryTemplate="{StaticResource RemainingProfilesEntryTemplate}" + SeparatorEntryTemplate="{StaticResource SeparatorEntryTemplate}" /> - + - - + + - + - + CornerRadius="{ThemeResource ControlCornerRadius}"> + + HorizontalAlignment="Stretch" + Style="{StaticResource SettingsStackStyle}"> + + + + + + + + + + + + + + + - - + + + SelectedItem="{x:Bind ViewModel.SelectedProfile, Mode=TwoWay}"> - + - - + TextChanged="FolderNameTextBox_TextChanged" /> - - + + + + + + - - - - - - - + + + - - + + - - - - - - - + + + + - - - - - - + + + + - + + + + + - - - - - + + + - - - + + + + + + + + + - - - - - - - - - - - - - + + - - - - - - - - - + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp index 76c3a2f3a9c..97ed2059de3 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp @@ -6,6 +6,7 @@ #include #include "NewTabMenuViewModel.g.cpp" +#include "FolderTreeViewEntry.g.cpp" #include "NewTabMenuEntryViewModel.g.cpp" #include "ProfileEntryViewModel.g.cpp" #include "SeparatorEntryViewModel.g.cpp" @@ -24,6 +25,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation static IObservableVector _ConvertToViewModelEntries(IVector settingsModelEntries) { auto result = single_threaded_observable_vector(); + if (!settingsModelEntries) + { + return result; + } + for (const auto& entry : settingsModelEntries) { switch (entry.Type()) @@ -83,7 +89,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation return result; } - bool NewTabMenuViewModel::IsRemainingProfilesEntryMissing(const IObservableVector& entries) + bool NewTabMenuViewModel::IsRemainingProfilesEntryMissing() const + { + return _IsRemainingProfilesEntryMissing(_rootEntries); + } + + bool NewTabMenuViewModel::_IsRemainingProfilesEntryMissing(const IVector& entries) { for (const auto& entry : entries) { @@ -95,7 +106,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } case NewTabMenuEntryType::Folder: { - if (!IsRemainingProfilesEntryMissing(entry.as().Entries())) + if (!_IsRemainingProfilesEntryMissing(entry.as().Entries())) { return false; } @@ -105,19 +116,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } } return true; - }; + } bool NewTabMenuViewModel::IsFolderView() const noexcept { - return _CurrentFolderEntry != nullptr; + return CurrentView() != _rootEntries; } NewTabMenuViewModel::NewTabMenuViewModel(Model::CascadiaSettings settings) { UpdateSettings(settings); - _UpdateEntries(); - // 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. @@ -127,135 +136,160 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { _NotifyChanges(L"SelectedProfile"); } - else if (viewModelProperty == L"CurrentFolderEntry") + else if (viewModelProperty == L"CurrentFolder") { - _NotifyChanges(L"IsFolderView"); - _UpdateEntries(); + if (_CurrentFolder) + { + CurrentFolderName(_CurrentFolder.Name()); + } + _NotifyChanges(L"IsFolderView", L"CurrentView"); } }); } - void NewTabMenuViewModel::UpdateSettings(Model::CascadiaSettings settings) + hstring NewTabMenuViewModel::CurrentFolderName() const { - _Settings = settings; - _NotifyChanges(L"AvailableProfiles"); + if (!_CurrentFolder) + { + return {}; + } + return _CurrentFolder.Name(); + } - SelectedProfile(AvailableProfiles().GetAt(0)); + void NewTabMenuViewModel::CurrentFolderName(const hstring& value) + { + if (_CurrentFolder && _CurrentFolder.Name() != value) + { + _CurrentFolder.Name(value); + } } - void NewTabMenuViewModel::_UpdateEntries() + bool NewTabMenuViewModel::CurrentFolderInlining() const { - // TODO CARLOS: shorthand - //Entries(_CurrentFolderEntry ? _CurrentFolderEntry.Entries() : _ConvertToViewModelEntries(_Settings.GlobalSettings().NewTabMenu())); - _entriesChangedRevoker.revoke(); - if (_CurrentFolderEntry) + if (!_CurrentFolder) { - Entries(_CurrentFolderEntry.Entries()); + return {}; } - else + return _CurrentFolder.Inlining(); + } + + void NewTabMenuViewModel::CurrentFolderInlining(bool value) + { + if (_CurrentFolder && _CurrentFolder.Inlining() != value) { - Entries(_ConvertToViewModelEntries(_Settings.GlobalSettings().NewTabMenu())); + _CurrentFolder.Inlining(value); } + } - // TODO CARLOS: I have the revoker here, not sure when/how to use it though, but I suspect we need it - _entriesChangedRevoker = _Entries.VectorChanged(winrt::auto_revoke, [this](auto&&, const IVectorChangedEventArgs& args) { + bool NewTabMenuViewModel::CurrentFolderAllowEmpty() const + { + if (!_CurrentFolder) + { + return {}; + } + return _CurrentFolder.Inlining(); + } + + void NewTabMenuViewModel::CurrentFolderAllowEmpty(bool value) + { + if (_CurrentFolder && _CurrentFolder.Inlining() != value) + { + _CurrentFolder.Inlining(value); + } + } + + Windows::Foundation::Collections::IObservableVector NewTabMenuViewModel::CurrentView() const + { + if (!_CurrentFolder) + { + return _rootEntries; + } + return _CurrentFolder.Entries(); + } + + void NewTabMenuViewModel::UpdateSettings(const Model::CascadiaSettings& settings) + { + _Settings = settings; + _NotifyChanges(L"AvailableProfiles"); + + SelectedProfile(AvailableProfiles().GetAt(0)); + + _rootEntries = _ConvertToViewModelEntries(_Settings.GlobalSettings().NewTabMenu()); + + _rootEntriesChangedRevoker = _rootEntries.VectorChanged(winrt::auto_revoke, [this](auto&&, const IVectorChangedEventArgs& args) { switch (args.CollectionChange()) { case CollectionChange::Reset: { - // fully replace settings model with _Entries - for (const auto& entry : _Entries) + // fully replace settings model with view model structure + auto modelEntries = single_threaded_vector(); + for (const auto& entry : _rootEntries) { - auto modelEntries = single_threaded_vector(); modelEntries.Append(NewTabMenuEntryViewModel::GetModel(entry)); - - if (_CurrentFolderEntry) - { - FolderEntry modelCurrentFolder = NewTabMenuEntryViewModel::GetModel(_CurrentFolderEntry).as(); - modelCurrentFolder.RawEntries(modelEntries); - } - else - { - _Settings.GlobalSettings().NewTabMenu(modelEntries); - } } + _Settings.GlobalSettings().NewTabMenu(modelEntries); return; } case CollectionChange::ItemInserted: { - const auto& insertedEntryVM = _Entries.GetAt(args.Index()); + const auto& insertedEntryVM = _rootEntries.GetAt(args.Index()); const auto& insertedEntry = NewTabMenuEntryViewModel::GetModel(insertedEntryVM); - - if (_CurrentFolderEntry) - { - FolderEntry modelCurrentFolder = NewTabMenuEntryViewModel::GetModel(_CurrentFolderEntry).as(); - modelCurrentFolder.RawEntries().InsertAt(args.Index(), insertedEntry); - } - else - { - auto newTabMenu = _Settings.GlobalSettings().NewTabMenu(); - newTabMenu.InsertAt(args.Index(), insertedEntry); - } + _Settings.GlobalSettings().NewTabMenu().InsertAt(args.Index(), insertedEntry); return; } case CollectionChange::ItemRemoved: { - if (_CurrentFolderEntry) - { - FolderEntry modelCurrentFolder = NewTabMenuEntryViewModel::GetModel(_CurrentFolderEntry).as(); - modelCurrentFolder.RawEntries().RemoveAt(args.Index()); - } - else - { - auto newTabMenu = _Settings.GlobalSettings().NewTabMenu(); - newTabMenu.RemoveAt(args.Index()); - } + _Settings.GlobalSettings().NewTabMenu().RemoveAt(args.Index()); return; } case CollectionChange::ItemChanged: { - if (_CurrentFolderEntry) - { - FolderEntry modelCurrentFolder = NewTabMenuEntryViewModel::GetModel(_CurrentFolderEntry).as(); - const auto modifiedEntry = _Entries.GetAt(args.Index()); - modelCurrentFolder.Entries().SetAt(args.Index(), NewTabMenuEntryViewModel::GetModel(modifiedEntry)); - } - else - { - auto newTabMenu = _Settings.GlobalSettings().NewTabMenu(); - const auto modifiedEntry = _Entries.GetAt(args.Index()); - newTabMenu.SetAt(args.Index(), NewTabMenuEntryViewModel::GetModel(modifiedEntry)); - } + const auto& modifiedEntry = _rootEntries.GetAt(args.Index()); + _Settings.GlobalSettings().NewTabMenu().SetAt(args.Index(), NewTabMenuEntryViewModel::GetModel(modifiedEntry)); return; } } }); + + // Clear CurrentFolder to reset the view + _CurrentFolder = nullptr; } - void NewTabMenuViewModel::RequestReorderEntry(Editor::NewTabMenuEntryViewModel vm, bool goingUp) + void NewTabMenuViewModel::RequestReorderEntry(const Editor::NewTabMenuEntryViewModel& vm, bool goingUp) { uint32_t idx; - if (_Entries.IndexOf(vm, idx)) + if (CurrentView().IndexOf(vm, idx)) { if (goingUp && idx > 0) { - _Entries.RemoveAt(idx); - _Entries.InsertAt(idx - 1, vm); + CurrentView().RemoveAt(idx); + CurrentView().InsertAt(idx - 1, vm); } - else if (!goingUp && idx < _Entries.Size() - 1) + else if (!goingUp && idx < CurrentView().Size() - 1) { - _Entries.RemoveAt(idx); - _Entries.InsertAt(idx + 1, vm); + CurrentView().RemoveAt(idx); + CurrentView().InsertAt(idx + 1, vm); } } } - void NewTabMenuViewModel::RequestDeleteEntry(Editor::NewTabMenuEntryViewModel vm) + void NewTabMenuViewModel::RequestDeleteEntry(const Editor::NewTabMenuEntryViewModel& vm) { uint32_t idx; - if (_Entries.IndexOf(vm, idx)) + if (CurrentView().IndexOf(vm, idx)) { - _Entries.RemoveAt(idx); + CurrentView().RemoveAt(idx); + } + } + + void NewTabMenuViewModel::RequestMoveEntriesToFolder(const Windows::Foundation::Collections::IVector& entries, const Editor::FolderEntryViewModel& destinationFolder) + { + for (auto&& e : entries) + { + // Remove entry from the current layer + RequestDeleteEntry(e); + + destinationFolder.Entries().Append(e); } } @@ -266,7 +300,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Model::ProfileEntry profileEntry; profileEntry.Profile(_SelectedProfile); - _Entries.Append(make(profileEntry)); + CurrentView().Append(make(profileEntry)); } _PrintAll(); } @@ -275,7 +309,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { Model::SeparatorEntry separatorEntry; - _Entries.Append(make(separatorEntry)); + CurrentView().Append(make(separatorEntry)); _PrintAll(); } @@ -283,12 +317,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void NewTabMenuViewModel::RequestAddFolderEntry() { Model::FolderEntry folderEntry; - folderEntry.Name(_FolderName); + folderEntry.Name(_AddFolderName); - _Entries.Append(make(folderEntry)); + CurrentView().Append(make(folderEntry)); // Clear the field after adding the entry - FolderName({}); + AddFolderName({}); _PrintAll(); } @@ -300,7 +334,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation matchProfilesEntry.Source(_ProfileMatcherSource); matchProfilesEntry.Commandline(_ProfileMatcherCommandline); - _Entries.Append(make(matchProfilesEntry)); + CurrentView().Append(make(matchProfilesEntry)); // Clear the fields after adding the entry ProfileMatcherName({}); @@ -313,19 +347,82 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void NewTabMenuViewModel::RequestAddRemainingProfilesEntry() { Model::RemainingProfilesEntry remainingProfilesEntry; - _Entries.Append(make(remainingProfilesEntry)); + CurrentView().Append(make(remainingProfilesEntry)); + + _NotifyChanges(L"IsRemainingProfilesEntryMissing"); _PrintAll(); } + // TODO CARLOS: + // - validate + // - reset cache when needed + Collections::IObservableVector NewTabMenuViewModel::FolderTree() + { + if (!_folderTreeCache) + { + _folderTreeCache = single_threaded_observable_vector(); + + // Add the root folder + auto root = winrt::make(nullptr); + _folderTreeCache.Append(root); + + for (const auto&& entry : _rootEntries) + { + if (entry.Type() == NewTabMenuEntryType::Folder) + { + root.Children().Append(winrt::make(entry.as())); + } + } + } + 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() + { + if (!_folderEntry) + { + return RS_(L"NewTabMenu_RootFolderName"); + } + return _folderEntry.Name(); + } + + hstring FolderTreeViewEntry::Icon() + { + 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 (Current Layer):---\n"); - _PrintVM(Entries()); + OutputDebugString(L"---VM:---\n"); + _PrintVM(_rootEntries); OutputDebugString(L"\n"); #endif } @@ -333,6 +430,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation #ifdef _DEBUG void NewTabMenuViewModel::_PrintModel(Windows::Foundation::Collections::IVector list, std::wstring prefix) { + if (!list) + { + return; + } + for (auto&& e : list) { _PrintModel(e, prefix); @@ -379,9 +481,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void NewTabMenuViewModel::_PrintVM(Windows::Foundation::Collections::IVector list, std::wstring prefix) { + if (!list) + { + return; + } + for (auto&& e : list) { - _PrintVM(e); + _PrintVM(e, prefix); } } @@ -481,8 +588,63 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _FolderEntry{ folderEntry } { _Entries = _ConvertToViewModelEntries(_FolderEntry.RawEntries()); + + // TODO CARLOS: we need to use this revoker somewhere + _entriesChangedRevoker = _Entries.VectorChanged(winrt::auto_revoke, [this](auto&&, const IVectorChangedEventArgs& args) { + switch (args.CollectionChange()) + { + case CollectionChange::Reset: + { + // fully replace settings model with _Entries + auto modelEntries = single_threaded_vector(); + for (const auto& entry : _Entries) + { + modelEntries.Append(NewTabMenuEntryViewModel::GetModel(entry)); + } + _FolderEntry.RawEntries(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 } diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h index dcb4b1c751e..01654bb4ef1 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h @@ -4,6 +4,7 @@ #pragma once #include "NewTabMenuViewModel.g.h" +#include "FolderTreeViewEntry.g.h" #include "NewTabMenuEntryViewModel.g.h" #include "ProfileEntryViewModel.g.h" #include "SeparatorEntryViewModel.g.h" @@ -21,14 +22,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { public: NewTabMenuViewModel(Model::CascadiaSettings settings); + void UpdateSettings(const Model::CascadiaSettings& settings); - static bool IsRemainingProfilesEntryMissing(const Windows::Foundation::Collections::IObservableVector& entries); + bool IsRemainingProfilesEntryMissing() const; bool IsFolderView() const noexcept; - void UpdateSettings(Model::CascadiaSettings settings); - - void RequestReorderEntry(Editor::NewTabMenuEntryViewModel vm, bool goingUp); - void RequestDeleteEntry(Editor::NewTabMenuEntryViewModel vm); + 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); void RequestAddSelectedProfileEntry(); void RequestAddSeparatorEntry(); @@ -36,24 +37,35 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void RequestAddProfileMatcherEntry(); void 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() { return _Settings.AllProfiles(); } - VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector, Entries); - VIEW_MODEL_OBSERVABLE_PROPERTY(Model::Profile, SelectedProfile, nullptr); + Windows::Foundation::Collections::IObservableVector FolderTree(); + 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, FolderName); - - VIEW_MODEL_OBSERVABLE_PROPERTY(NTMSubPage, CurrentPage); - VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::FolderEntryViewModel, CurrentFolderEntry, nullptr); + VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, AddFolderName); private: Model::CascadiaSettings _Settings{ nullptr }; - Windows::Foundation::Collections::IObservableVector::VectorChanged_revoker _entriesChangedRevoker; + Windows::Foundation::Collections::IObservableVector _rootEntries; + Windows::Foundation::Collections::IObservableVector::VectorChanged_revoker _rootEntriesChangedRevoker; - void _UpdateEntries(); + Windows::Foundation::Collections::IObservableVector _folderTreeCache; + Windows::Foundation::Collections::IObservableVector::VectorChanged_revoker _CurrentViewChangedRevoker; + + static bool _IsRemainingProfilesEntryMissing(const Windows::Foundation::Collections::IVector& entries); void _PrintAll(); #ifdef _DEBUG @@ -64,6 +76,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation #endif }; + struct FolderTreeViewEntry : FolderTreeViewEntryT + { + public: + FolderTreeViewEntry(Editor::FolderEntryViewModel folderEntry); + + hstring Name(); + hstring Icon(); + Editor::FolderEntryViewModel FolderEntryVM() { return _folderEntry; } + + WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, Children); + + private: + Editor::FolderEntryViewModel _folderEntry; + }; + struct NewTabMenuEntryViewModel : NewTabMenuEntryViewModelT, ViewModelHelper { public: @@ -95,11 +122,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation public: 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; }; struct MatchProfilesEntryViewModel : MatchProfilesEntryViewModelT @@ -123,6 +156,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation { BASIC_FACTORY(NewTabMenuViewModel); + BASIC_FACTORY(FolderTreeViewEntry); BASIC_FACTORY(ProfileEntryViewModel); BASIC_FACTORY(SeparatorEntryViewModel); BASIC_FACTORY(FolderEntryViewModel); diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl index af05db90721..2da949fdab6 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl @@ -5,40 +5,49 @@ import "ProfileViewModel.idl"; namespace Microsoft.Terminal.Settings.Editor { - enum NTMSubPage + [default_interface] runtimeclass FolderTreeViewEntry { - Base = 0, - Folder = 1 - }; + 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); - NTMSubPage CurrentPage; - FolderEntryViewModel CurrentFolderEntry; + FolderEntryViewModel CurrentFolder; Boolean IsFolderView { get; }; + FolderTreeViewEntry CurrentFolderTreeViewSelectedItem; + Boolean IsRemainingProfilesEntryMissing { get; }; - IObservableVector Entries; + 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 FolderName; + String AddFolderName; void RequestReorderEntry(NewTabMenuEntryViewModel vm, Boolean goingUp); void RequestDeleteEntry(NewTabMenuEntryViewModel vm); + void RequestMoveEntriesToFolder(IVector entries, FolderEntryViewModel folderEntry); void RequestAddSelectedProfileEntry(); void RequestAddSeparatorEntry(); void RequestAddFolderEntry(); void RequestAddProfileMatcherEntry(); void RequestAddRemainingProfilesEntry(); - - static Boolean IsRemainingProfilesEntryMissing(IObservableVector entries); } [default_interface] unsealed runtimeclass NewTabMenuEntryViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged @@ -64,6 +73,8 @@ namespace Microsoft.Terminal.Settings.Editor String Name; String Icon; + Boolean Inlining; + Boolean AllowEmpty; IObservableVector Entries; } diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 9c916ecb316..88ed29719c8 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -1907,4 +1907,56 @@ 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". + \ No newline at end of file From 4d5616ce07454f8f3c9307b44a3a850519ef90a5 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Tue, 8 Oct 2024 15:21:56 -0700 Subject: [PATCH 05/22] last bit of polish --- .../TerminalSettingsEditor/MainPage.cpp | 5 +- .../TerminalSettingsEditor/NewTabMenu.cpp | 1 + .../TerminalSettingsEditor/NewTabMenu.h | 1 + .../TerminalSettingsEditor/NewTabMenu.xaml | 2 +- .../NewTabMenuViewModel.cpp | 48 ++++++++++++++----- .../NewTabMenuViewModel.h | 10 ++-- .../NewTabMenuViewModel.idl | 1 + 7 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 16b30155579..f2d66d19cee 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -150,7 +150,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // Early exit if the last breadcrumb was a FolderEntry in the NewTabMenu if (const auto& breadcrumbFolderEntry{ crumb->Tag().try_as() }) { - // TODO CARLOS: It's _a lot_ of extra work to figure out where this folder is and recreate the breadcrumbs + // It's _a lot_ of extra work to figure out where this folder is and recreate the breadcrumbs // (and that assumes that the folder even exists!) so for now we'll just navigate to the base page _newTabMenuPageVM.CurrentFolder(nullptr); _Navigate(breadcrumbFolderEntry, BreadcrumbSubPage::None); @@ -369,7 +369,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void MainPage::_SetupNTMEventHandling() { - // TODO CARLOS: validate _ntmViewModelChangedRevoker = _newTabMenuPageVM.PropertyChanged(winrt::auto_revoke, [this](auto&&, const PropertyChangedEventArgs& args) { const auto settingName{ args.PropertyName() }; if (settingName == L"CurrentFolder") @@ -514,7 +513,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void MainPage::_Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage) { - // TODO CARLOS: validate _PreNavigateHelper(); _SetupNTMEventHandling(); @@ -577,7 +575,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } else if (const auto ntmEntryViewModel = tag.try_as()) { - // TODO CARLOS: validate _Navigate(*ntmEntryViewModel, subPage); } else diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp index e35acfe3c72..2cab73f4da5 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp @@ -33,6 +33,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void NewTabMenu::FolderPickerDialog_Opened(const IInspectable& /*sender*/, const Controls::ContentDialogOpenedEventArgs& /*e*/) { _ViewModel.CurrentFolderTreeViewSelectedItem(nullptr); + _ViewModel.GenerateFolderTree(); } void NewTabMenu::FolderPickerDialog_PrimaryButtonClick(const IInspectable& /*sender*/, const Controls::ContentDialogButtonClickEventArgs& /*e*/) diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.h b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h index f15b4a668a4..990f5a518b2 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.h +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h @@ -18,6 +18,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // FolderPickerDialog handlers void FolderPickerDialog_Opened(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::ContentDialogOpenedEventArgs& e); + void FolderPickerDialog_Closed(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::ContentDialogClosedEventArgs& e); void FolderPickerDialog_PrimaryButtonClick(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs& e); // NTM Entry handlers diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml index 3e92b06a8da..5cbe56c1497 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml @@ -277,7 +277,7 @@ - + diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp index 97ed2059de3..18026d994cb 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.cpp @@ -141,12 +141,23 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation 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) @@ -161,6 +172,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation if (_CurrentFolder && _CurrentFolder.Name() != value) { _CurrentFolder.Name(value); + _NotifyChanges(L"CurrentFolderName"); } } @@ -178,6 +190,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation if (_CurrentFolder && _CurrentFolder.Inlining() != value) { _CurrentFolder.Inlining(value); + _NotifyChanges(L"CurrentFolderInlining"); } } @@ -187,14 +200,15 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { return {}; } - return _CurrentFolder.Inlining(); + return _CurrentFolder.AllowEmpty(); } void NewTabMenuViewModel::CurrentFolderAllowEmpty(bool value) { - if (_CurrentFolder && _CurrentFolder.Inlining() != value) + if (_CurrentFolder && _CurrentFolder.AllowEmpty() != value) { - _CurrentFolder.Inlining(value); + _CurrentFolder.AllowEmpty(value); + _NotifyChanges(L"CurrentFolderAllowEmpty"); } } @@ -279,6 +293,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation if (CurrentView().IndexOf(vm, idx)) { CurrentView().RemoveAt(idx); + + if (vm.try_as()) + { + _folderTreeCache = nullptr; + } } } @@ -286,9 +305,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { for (auto&& e : entries) { - // Remove entry from the current layer + // Remove entry from the current layer, + // and add it to the destination folder RequestDeleteEntry(e); - destinationFolder.Entries().Append(e); } } @@ -321,8 +340,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation CurrentView().Append(make(folderEntry)); - // Clear the field after adding the entry + // Reset state after adding the entry AddFolderName({}); + _folderTreeCache = nullptr; _PrintAll(); } @@ -354,10 +374,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _PrintAll(); } - // TODO CARLOS: - // - validate - // - reset cache when needed - Collections::IObservableVector NewTabMenuViewModel::FolderTree() + void NewTabMenuViewModel::GenerateFolderTree() { if (!_folderTreeCache) { @@ -374,7 +391,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation root.Children().Append(winrt::make(entry.as())); } } + _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; } @@ -589,7 +616,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { _Entries = _ConvertToViewModelEntries(_FolderEntry.RawEntries()); - // TODO CARLOS: we need to use this revoker somewhere _entriesChangedRevoker = _Entries.VectorChanged(winrt::auto_revoke, [this](auto&&, const IVectorChangedEventArgs& args) { switch (args.CollectionChange()) { diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h index 01654bb4ef1..9982425c2a6 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.h @@ -23,6 +23,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation public: NewTabMenuViewModel(Model::CascadiaSettings settings); void UpdateSettings(const Model::CascadiaSettings& settings); + void GenerateFolderTree(); bool IsRemainingProfilesEntryMissing() const; bool IsFolderView() const noexcept; @@ -44,8 +45,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation bool CurrentFolderAllowEmpty() const; void CurrentFolderAllowEmpty(bool value); - Windows::Foundation::Collections::IObservableVector AvailableProfiles() { return _Settings.AllProfiles(); } - Windows::Foundation::Collections::IObservableVector FolderTree(); + 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); @@ -60,12 +61,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation private: Model::CascadiaSettings _Settings{ nullptr }; Windows::Foundation::Collections::IObservableVector _rootEntries; - Windows::Foundation::Collections::IObservableVector::VectorChanged_revoker _rootEntriesChangedRevoker; - Windows::Foundation::Collections::IObservableVector _folderTreeCache; - Windows::Foundation::Collections::IObservableVector::VectorChanged_revoker _CurrentViewChangedRevoker; + 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 diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl index 2da949fdab6..5027d79532f 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenuViewModel.idl @@ -20,6 +20,7 @@ namespace Microsoft.Terminal.Settings.Editor { NewTabMenuViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings); void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings); + void GenerateFolderTree(); FolderEntryViewModel CurrentFolder; Boolean IsFolderView { get; }; From 99d2d39812562ea28304434d95e386f0c017cdcf Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Tue, 8 Oct 2024 16:51:42 -0700 Subject: [PATCH 06/22] Add support for action entries --- .../TerminalSettingsEditor/NewTabMenu.cpp | 2 + .../TerminalSettingsEditor/NewTabMenu.h | 1 + .../TerminalSettingsEditor/NewTabMenu.idl | 1 + .../TerminalSettingsEditor/NewTabMenu.xaml | 31 ++++---- .../NewTabMenuViewModel.cpp | 70 ++++++++++++++++--- .../NewTabMenuViewModel.h | 20 +++++- .../NewTabMenuViewModel.idl | 10 ++- .../Resources/en-US/Resources.resw | 4 ++ 8 files changed, 111 insertions(+), 28 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp index 2cab73f4da5..f62321d44d6 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp @@ -123,6 +123,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { case Model::NewTabMenuEntryType::Profile: return ProfileEntryTemplate(); + case Model::NewTabMenuEntryType::Action: + return ActionEntryTemplate(); case Model::NewTabMenuEntryType::Separator: return SeparatorEntryTemplate(); case Model::NewTabMenuEntryType::Folder: diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.h b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h index 990f5a518b2..00a85dfaba2 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.h +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h @@ -52,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item); WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, ProfileEntryTemplate, nullptr); + WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, ActionEntryTemplate, nullptr); WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, SeparatorEntryTemplate, nullptr); WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, FolderEntryTemplate, nullptr); WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, MatchProfilesEntryTemplate, nullptr); diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.idl b/src/cascadia/TerminalSettingsEditor/NewTabMenu.idl index 28a08342fd5..21d9624d3b2 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.idl +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.idl @@ -16,6 +16,7 @@ namespace Microsoft.Terminal.Settings.Editor NewTabMenuEntryTemplateSelector(); Windows.UI.Xaml.DataTemplate ProfileEntryTemplate; + Windows.UI.Xaml.DataTemplate ActionEntryTemplate; Windows.UI.Xaml.DataTemplate SeparatorEntryTemplate; Windows.UI.Xaml.DataTemplate FolderEntryTemplate; Windows.UI.Xaml.DataTemplate MatchProfilesEntryTemplate; diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml index 5cbe56c1497..73401d2edea 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml @@ -19,21 +19,6 @@ - -