From 8bb831f628d3b8ddaf614c0a4d6f9b0b0533b5f0 Mon Sep 17 00:00:00 2001 From: SEt <70444390+SEt-t@users.noreply.github.com> Date: Wed, 10 Dec 2025 01:46:27 +0300 Subject: [PATCH 1/3] Gracefully handle unavailable TSF from SYSTEM account (#19635) When run from SYSTEM account TSF seems to be unavailable. The only missing step to handle that is check during initialization. Not sure if fail after partial success in `Implementation::Initialize` should also be gracefully handled. Closes #19634 --- src/tsf/Handle.cpp | 6 +++++- src/tsf/Implementation.cpp | 10 ++++++++-- src/tsf/Implementation.h | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/tsf/Handle.cpp b/src/tsf/Handle.cpp index b7cae1d1be..ea1041c11a 100644 --- a/src/tsf/Handle.cpp +++ b/src/tsf/Handle.cpp @@ -12,7 +12,11 @@ Handle Handle::Create() { Handle handle; handle._impl = new Implementation(); - handle._impl->Initialize(); + if (!handle._impl->Initialize()) + { + delete handle._impl; + handle._impl = nullptr; + } return handle; } diff --git a/src/tsf/Implementation.cpp b/src/tsf/Implementation.cpp index 99ea34c01c..2b9e24ab73 100644 --- a/src/tsf/Implementation.cpp +++ b/src/tsf/Implementation.cpp @@ -68,9 +68,14 @@ void Implementation::SetDefaultScopeAlphanumericHalfWidth(bool enable) noexcept s_wantsAnsiInputScope.store(enable, std::memory_order_relaxed); } -void Implementation::Initialize() +bool Implementation::Initialize() { - _categoryMgr = wil::CoCreateInstance(CLSID_TF_CategoryMgr, CLSCTX_INPROC_SERVER); + _categoryMgr = wil::CoCreateInstanceNoThrow(CLSID_TF_CategoryMgr); + if (!_categoryMgr) + { + return false; + } + _displayAttributeMgr = wil::CoCreateInstance(CLSID_TF_DisplayAttributeMgr); // There's no point in calling TF_GetThreadMgr. ITfThreadMgr is a per-thread singleton. @@ -89,6 +94,7 @@ void Implementation::Initialize() THROW_IF_FAILED(_contextSource->AdviseSink(IID_ITfTextEditSink, static_cast(this), &_cookieTextEditSink)); THROW_IF_FAILED(_documentMgr->Push(_context.get())); + return true; } void Implementation::Uninitialize() noexcept diff --git a/src/tsf/Implementation.h b/src/tsf/Implementation.h index 82e84ab888..9270b510d5 100644 --- a/src/tsf/Implementation.h +++ b/src/tsf/Implementation.h @@ -21,7 +21,7 @@ namespace Microsoft::Console::TSF virtual ~Implementation() = default; - void Initialize(); + bool Initialize(); void Uninitialize() noexcept; HWND FindWindowOfActiveTSF() noexcept; void AssociateFocus(IDataProvider* provider); From 45c5370271c02247a2a5bc31e8514006506f2222 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Tue, 9 Dec 2025 16:52:27 -0600 Subject: [PATCH 2/3] When the renderer fails, try to fall back to D2D + WARP; retry changes (#19636) This commit also ups the number of render failures that are permissible to 6 (one try plus 5 retries), and moves us to use an exponential backoff rather than a simple geometric one. It also suppresses the dialog box in case of present failures for Stable users. I feel like the warning dialog should be used for something that the user can actually do something about... Closes #15601 Closes #18198 --- src/cascadia/TerminalControl/ControlCore.cpp | 19 ++++++- src/cascadia/TerminalControl/ControlCore.h | 2 + src/features.xml | 7 +++ src/renderer/atlas/AtlasEngine.r.cpp | 13 +++-- src/renderer/base/renderer.cpp | 55 +++++++++++--------- 5 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 980da4bb60..be7350f11a 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -152,12 +152,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation _renderer->SetBackgroundColorChangedCallback([this]() { _rendererBackgroundColorChanged(); }); _renderer->SetFrameColorChangedCallback([this]() { _rendererTabColorChanged(); }); - _renderer->SetRendererEnteredErrorStateCallback([this]() { RendererEnteredErrorState.raise(nullptr, nullptr); }); + _renderer->SetRendererEnteredErrorStateCallback([this]() { _rendererEnteredErrorState(); }); } UpdateSettings(settings, unfocusedAppearance); } + void ControlCore::_rendererEnteredErrorState() + { + // The first time the renderer fails out (after all of its own retries), switch it to D2D and WARP + // and force it to try again. If it _still_ fails, we can let it halt. + if (_renderFailures++ == 0) + { + const auto lock = _terminal->LockForWriting(); + _renderEngine->SetGraphicsAPI(parseGraphicsAPI(GraphicsAPI::Direct2D)); + _renderEngine->SetSoftwareRendering(true); + _renderer->EnablePainting(); + return; + } + RendererEnteredErrorState.raise(nullptr, nullptr); + } + void ControlCore::_setupDispatcherAndCallbacks() { // Get our dispatcher. If we're hosted in-proc with XAML, this will get @@ -917,6 +932,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering()); // Inform the renderer of our opacity _renderEngine->EnableTransparentBackground(_isBackgroundTransparent()); + _renderFailures = 0; // We may have changed the engine; reset the failure counter. // Trigger a redraw to repaint the window background and tab colors. _renderer->TriggerRedrawAll(true, true); @@ -1983,6 +1999,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { // The lock must be held, because it calls into IRenderData which is shared state. const auto lock = _terminal->LockForWriting(); + _renderFailures = 0; _renderer->EnablePainting(); } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index d793d88b58..112848dc6e 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -344,6 +344,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation safe_void_coroutine _renderEngineSwapChainChanged(const HANDLE handle); void _rendererBackgroundColorChanged(); void _rendererTabColorChanged(); + void _rendererEnteredErrorState(); #pragma endregion void _raiseReadOnlyWarning(); @@ -398,6 +399,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation float _panelWidth{ 0 }; float _panelHeight{ 0 }; float _compositionScale{ 0 }; + uint8_t _renderFailures{ 0 }; bool _forceCursorVisible = false; // Audio stuff. diff --git a/src/features.xml b/src/features.xml index ba54205e3b..4788697703 100644 --- a/src/features.xml +++ b/src/features.xml @@ -68,6 +68,13 @@ + + Feature_AtlasEngineLoudErrors + Atlas Engine can optionally support signaling every presentation failure to its consumer. For now, we only want that to happen in non-Release builds. + AlwaysEnabled + + + Feature_NearbyFontLoading Controls whether fonts in the same directory as the binary are used during rendering. Disabled for conhost so that it doesn't iterate the entire system32 directory. diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index 2df96f0c62..805e14c1e7 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -61,13 +61,18 @@ catch (const wil::ResultException& exception) return E_PENDING; } - if (_p.warningCallback) + if constexpr (Feature_AtlasEngineLoudErrors::IsEnabled()) { - try + // We may fail to present repeatedly, e.g. if there's a short-term device failure. + // We should not bombard the consumer with repeated warning callbacks (where they may present a dialog to the user). + if (_p.warningCallback) { - _p.warningCallback(hr, {}); + try + { + _p.warningCallback(hr, {}); + } + CATCH_LOG() } - CATCH_LOG() } _b.reset(); diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index e32bff3d25..2c0d6e8162 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -12,9 +12,10 @@ using namespace Microsoft::Console::Types; using PointTree = interval_tree::IntervalTree; static constexpr TimerRepr TimerReprMax = std::numeric_limits::max(); -static constexpr DWORD maxRetriesForRenderEngine = 3; -// The renderer will wait this number of milliseconds * how many tries have elapsed before trying again. -static constexpr DWORD renderBackoffBaseTimeMilliseconds = 150; +// We want there to be five retry periods; after the last one, we will mark the render as failed. +static constexpr unsigned int maxRetriesForRenderEngine = 5; +// The renderer will wait this number of milliseconds * 2^tries before trying again. +static constexpr DWORD renderBackoffBaseTimeMilliseconds = 100; // Routine Description: // - Creates a new renderer controller for a console. @@ -326,40 +327,44 @@ DWORD Renderer::_timerToMillis(TimerRepr t) noexcept // - HRESULT S_OK, GDI error, Safe Math error, or state/argument errors. [[nodiscard]] HRESULT Renderer::PaintFrame() { - auto tries = maxRetriesForRenderEngine; - while (tries > 0) + HRESULT hr{ S_FALSE }; + // Attempt zero doesn't count as a retry. We should try maxRetries + 1 times. + for (unsigned int attempt = 0u; attempt <= maxRetriesForRenderEngine; ++attempt) { + if (attempt > 0) [[unlikely]] + { + // Add a bit of backoff. + // Sleep 100, 200, 400, 600, 800ms, 1600ms before failing out and disabling the renderer. + Sleep(renderBackoffBaseTimeMilliseconds * (1 << (attempt - 1))); + } + // BODGY: Optimally we would want to retry per engine, but that causes different // problems (intermittent inconsistent states between text renderer and UIA output, // not being able to lock the cursor location, etc.). - const auto hr = _PaintFrame(); + hr = _PaintFrame(); if (SUCCEEDED(hr)) { break; } LOG_HR_IF(hr, hr != E_PENDING); - - if (--tries == 0) - { - // Stop trying. - _disablePainting(); - if (_pfnRendererEnteredErrorState) - { - _pfnRendererEnteredErrorState(); - } - // If there's no callback, we still don't want to FAIL_FAST: the renderer going black - // isn't near as bad as the entire application aborting. We're a component. We shouldn't - // abort applications that host us. - return S_FALSE; - } - - // Add a bit of backoff. - // Sleep 150ms, 300ms, 450ms before failing out and disabling the renderer. - Sleep(renderBackoffBaseTimeMilliseconds * (maxRetriesForRenderEngine - tries)); } - return S_OK; + if (FAILED(hr)) + { + // Stop trying. + _disablePainting(); + if (_pfnRendererEnteredErrorState) + { + _pfnRendererEnteredErrorState(); + } + // If there's no callback, we still don't want to FAIL_FAST: the renderer going black + // isn't near as bad as the entire application aborting. We're a component. We shouldn't + // abort applications that host us. + hr = S_FALSE; + } + + return hr; } [[nodiscard]] HRESULT Renderer::_PaintFrame() noexcept From 297703d7832ea1286c5d7246830a124c9f25c8e9 Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Tue, 9 Dec 2025 15:42:54 -0800 Subject: [PATCH 3/3] Allow editing actions in the settings UI (#18917) The actions page now has a list of all the commands (default, user, fragments etc) and clicking a command from that page brings you to an "Edit action" page where you can fully view and edit both the action type and any additional arguments. ## Detailed Description of the Pull Request / Additional comments Actions View Model * Added several new view models * `CommandViewModel` (view model for a `Command`), a list of these is created and managed by `ActionsViewModel` * `ActionArgsViewModel` (view model for an `ActionArgs`), created and managed by `CommandViewModel` * `ArgWrapper` (view model for each individual argument inside an `ActionArgs`), created and managed by `ActionArgsViewModel` Actions page * No longer a list of only keybindings, instead it is a list of every command Terminal knows about EditAction page * New page that you get to by clicking a command from the Actions page * Bound to a `CommandViewModel` * Allows editing the type of shortcut action and the command name * Depending on the shortcut action, displays a list of additional arguments allowed for the command with the appropriate templating (bool arguments are switches, flags are checkboxes etc) Closes #19019 --- .../TerminalSettingsEditor/Actions.cpp | 54 +- src/cascadia/TerminalSettingsEditor/Actions.h | 6 +- .../TerminalSettingsEditor/Actions.xaml | 179 +- .../ActionsViewModel.cpp | 1517 ++++++++++++++--- .../TerminalSettingsEditor/ActionsViewModel.h | 336 +++- .../ActionsViewModel.idl | 184 +- .../ArgsTemplateSelectors.cpp | 93 + .../ArgsTemplateSelectors.h | 38 + .../TerminalSettingsEditor/EditAction.cpp | 53 + .../TerminalSettingsEditor/EditAction.h | 34 + .../TerminalSettingsEditor/EditAction.xaml | 740 ++++++++ .../TerminalSettingsEditor/EnumEntry.h | 55 +- .../TerminalSettingsEditor/EnumEntry.idl | 9 + .../TerminalSettingsEditor/MainPage.cpp | 26 +- .../TerminalSettingsEditor/MainPage.h | 3 + .../TerminalSettingsEditor/MainPage.idl | 3 + .../TerminalSettingsEditor/MainPage.xaml | 4 + ...Microsoft.Terminal.Settings.Editor.vcxproj | 17 + ...t.Terminal.Settings.Editor.vcxproj.filters | 1 + .../Resources/en-US/Resources.resw | 322 +++- src/cascadia/TerminalSettingsEditor/Utils.h | 60 +- .../TerminalSettingsModel/ActionArgs.h | 2 +- .../TerminalSettingsModel/ActionArgs.idl | 3 +- .../TerminalSettingsModel/Command.cpp | 9 +- 24 files changed, 3114 insertions(+), 634 deletions(-) create mode 100644 src/cascadia/TerminalSettingsEditor/ArgsTemplateSelectors.cpp create mode 100644 src/cascadia/TerminalSettingsEditor/ArgsTemplateSelectors.h create mode 100644 src/cascadia/TerminalSettingsEditor/EditAction.cpp create mode 100644 src/cascadia/TerminalSettingsEditor/EditAction.h create mode 100644 src/cascadia/TerminalSettingsEditor/EditAction.xaml diff --git a/src/cascadia/TerminalSettingsEditor/Actions.cpp b/src/cascadia/TerminalSettingsEditor/Actions.cpp index c9b12953ad..83a32c4a26 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.cpp +++ b/src/cascadia/TerminalSettingsEditor/Actions.cpp @@ -18,52 +18,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Automation::AutomationProperties::SetName(AddNewButton(), RS_(L"Actions_AddNewTextBlock/Text")); } - Automation::Peers::AutomationPeer Actions::OnCreateAutomationPeer() - { - _ViewModel.OnAutomationPeerAttached(); - return nullptr; - } - void Actions::OnNavigatedTo(const NavigationEventArgs& e) { _ViewModel = e.Parameter().as(); + _ViewModel.CurrentPage(ActionsSubPage::Base); + auto vmImpl = get_self(_ViewModel); + vmImpl->MarkAsVisited(); + _layoutUpdatedRevoker = LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { + // Only let this succeed once. + _layoutUpdatedRevoker.revoke(); - // Subscribe to the view model's FocusContainer event. - // Use the KeyBindingViewModel or index provided in the event to focus the corresponding container - _ViewModel.FocusContainer([this](const auto& /*sender*/, const auto& args) { - if (auto kbdVM{ args.try_as() }) - { - if (const auto& container = KeyBindingsListView().ContainerFromItem(*kbdVM)) - { - container.as().Focus(FocusState::Programmatic); - } - } - else if (const auto& index = args.try_as()) - { - if (const auto& container = KeyBindingsListView().ContainerFromIndex(*index)) - { - container.as().Focus(FocusState::Programmatic); - } - } - }); - - // Subscribe to the view model's UpdateBackground event. - // The view model does not have access to the page resources, so it asks us - // to update the key binding's container background - _ViewModel.UpdateBackground([this](const auto& /*sender*/, const auto& args) { - if (auto kbdVM{ args.try_as() }) - { - if (kbdVM->IsInEditMode()) - { - const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackgroundEditing")).as() }; - kbdVM->ContainerBackground(containerBackground); - } - else - { - const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackground")).as() }; - kbdVM->ContainerBackground(containerBackground); - } - } + AddNewButton().Focus(FocusState::Programmatic); }); TraceLoggingWrite( @@ -74,9 +39,4 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); } - - void Actions::AddNew_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*eventArgs*/) - { - _ViewModel.AddNewKeybinding(); - } } diff --git a/src/cascadia/TerminalSettingsEditor/Actions.h b/src/cascadia/TerminalSettingsEditor/Actions.h index a79003afbf..c88ef0a67b 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.h +++ b/src/cascadia/TerminalSettingsEditor/Actions.h @@ -16,12 +16,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Actions(); void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); - Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); - - void AddNew_Click(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); til::property_changed_event PropertyChanged; WINRT_OBSERVABLE_PROPERTY(Editor::ActionsViewModel, ViewModel, PropertyChanged.raise, nullptr); + + private: + winrt::Windows::UI::Xaml::FrameworkElement::LayoutUpdated_revoker _layoutUpdatedRevoker; }; } diff --git a/src/cascadia/TerminalSettingsEditor/Actions.xaml b/src/cascadia/TerminalSettingsEditor/Actions.xaml index b94977d9a2..56bb5b32be 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Actions.xaml @@ -123,7 +123,7 @@ @@ -138,24 +138,6 @@ - 32 - 14 - - - - + @@ -180,146 +156,25 @@ - - - - - - - + Text="{x:Bind DisplayName, Mode=OneWay}" /> - + Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(FirstKeyChordText)}"> - - - - - - - - - - - - - - - - - - - @@ -331,10 +186,14 @@ HorizontalAlignment="Left" Spacing="8" Style="{StaticResource SettingsStackStyle}"> + - - + + diff --git a/src/cascadia/TerminalSettingsEditor/ActionsViewModel.cpp b/src/cascadia/TerminalSettingsEditor/ActionsViewModel.cpp index d1ab6c4d20..73017a1c48 100644 --- a/src/cascadia/TerminalSettingsEditor/ActionsViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/ActionsViewModel.cpp @@ -4,8 +4,14 @@ #include "pch.h" #include "ActionsViewModel.h" #include "ActionsViewModel.g.cpp" -#include "KeyBindingViewModel.g.cpp" +#include "CommandViewModel.g.cpp" +#include "ArgWrapper.g.cpp" +#include "ActionArgsViewModel.g.cpp" +#include "KeyChordViewModel.g.cpp" #include "../TerminalSettingsModel/AllShortcutActions.h" +#include "EnumEntry.h" +#include "ColorSchemeViewModel.h" +#include "../WinRTUtils/inc/Utils.h" using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; @@ -17,383 +23,1338 @@ using namespace winrt::Windows::UI::Xaml::Data; using namespace winrt::Windows::UI::Xaml::Navigation; using namespace winrt::Microsoft::Terminal::Settings::Model; +// TODO: GH 19056 +// multiple actions +// selection color +// the above arg types aren't implemented yet - they all have multiple values within them +// and require a different approach to binding/displaying. Selection color has color and IsIndex16, +// multiple actions is... multiple actions +// for now, do not support these shortcut actions in the new action editor +inline const std::set UnimplementedShortcutActions = { + winrt::Microsoft::Terminal::Settings::Model::ShortcutAction::MultipleActions, + winrt::Microsoft::Terminal::Settings::Model::ShortcutAction::ColorSelection +}; + namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { - KeyBindingViewModel::KeyBindingViewModel(const IObservableVector& availableActions) : - KeyBindingViewModel(nullptr, availableActions.First().Current(), availableActions) {} + static constexpr std::wstring_view ActionsPageId{ L"page.actions" }; - KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const hstring& actionName, const IObservableVector& availableActions) : - _CurrentKeys{ keys }, - _KeyChordText{ KeyChordSerialization::ToString(keys) }, - _CurrentAction{ actionName }, - _ProposedAction{ box_value(actionName) }, - _AvailableActions{ availableActions } + CommandViewModel::CommandViewModel(const Command& cmd, std::vector keyChordList, const Editor::ActionsViewModel& actionsPageVM, Windows::Foundation::Collections::IMap availableActionsAndNamesMap, Windows::Foundation::Collections::IMap nameToActionMap) : + _command{ cmd }, + _keyChordList{ std::move(keyChordList) }, + _actionsPageVM{ actionsPageVM }, + _availableActionsAndNamesMap{ availableActionsAndNamesMap }, + _nameToActionMap{ nameToActionMap } { + } + + void CommandViewModel::Initialize() + { + const auto actionsPageVM{ _actionsPageVM.get() }; + if (!actionsPageVM) + { + // The parent page is gone, just return early + return; + } + std::vector keyChordVMs; + for (const auto keys : _keyChordList) + { + auto kcVM{ make(keys) }; + _RegisterKeyChordVMEvents(kcVM); + keyChordVMs.push_back(kcVM); + } + _KeyChordList = single_threaded_observable_vector(std::move(keyChordVMs)); + + std::vector shortcutActions; + for (const auto [action, name] : _availableActionsAndNamesMap) + { + shortcutActions.emplace_back(name); + } + std::sort(shortcutActions.begin(), shortcutActions.end()); + _AvailableShortcutActions = single_threaded_observable_vector(std::move(shortcutActions)); + + const auto shortcutActionString = _availableActionsAndNamesMap.Lookup(_command.ActionAndArgs().Action()); + ProposedShortcutActionName(winrt::box_value(shortcutActionString)); + _CreateAndInitializeActionArgsVMHelper(); + // 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. + // This allows us to create a new ActionArgsVM when the shortcut action changes PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) { - const auto viewModelProperty{ args.PropertyName() }; - if (viewModelProperty == L"CurrentKeys") + if (const auto actionsPageVM{ _actionsPageVM.get() }) { - _KeyChordText = KeyChordSerialization::ToString(_CurrentKeys); - _NotifyChanges(L"KeyChordText"); - } - else if (viewModelProperty == L"IsContainerFocused" || - viewModelProperty == L"IsEditButtonFocused" || - viewModelProperty == L"IsHovered" || - viewModelProperty == L"IsAutomationPeerAttached" || - viewModelProperty == L"IsInEditMode") - { - _NotifyChanges(L"ShowEditButton"); - } - else if (viewModelProperty == L"CurrentAction") - { - _NotifyChanges(L"Name"); + const auto viewModelProperty{ args.PropertyName() }; + if (viewModelProperty == L"ProposedShortcutActionName") + { + const auto actionString = unbox_value(ProposedShortcutActionName()); + const auto actionEnum = _nameToActionMap.Lookup(actionString); + const auto emptyArgs = ActionArgFactory::GetEmptyArgsForAction(actionEnum); + // TODO: GH 19056 + // probably need some better default values for empty args and/or validation + // eg. for sendInput, where "input" is a required argument, "input" gets set to an empty string which does not satisfy the requirement + // i.e. if the user hits "save" immediately after switching to sendInput as the action (without adding something to the input field), they'll get an error + // there are some other cases as well + Model::ActionAndArgs newActionAndArgs{ actionEnum, emptyArgs }; + _command.ActionAndArgs(newActionAndArgs); + if (_IsNewCommand) + { + actionsPageVM.RegenerateCommandID(_command); + } + else if (!IsUserAction()) + { + _ReplaceCommandWithUserCopy(true); + return; + } + _CreateAndInitializeActionArgsVMHelper(); + } } }); } - hstring KeyBindingViewModel::EditButtonName() const noexcept { return RS_(L"Actions_EditButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } - hstring KeyBindingViewModel::CancelButtonName() const noexcept { return RS_(L"Actions_CancelButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } - hstring KeyBindingViewModel::AcceptButtonName() const noexcept { return RS_(L"Actions_AcceptButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } - hstring KeyBindingViewModel::DeleteButtonName() const noexcept { return RS_(L"Actions_DeleteButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } - - bool KeyBindingViewModel::ShowEditButton() const noexcept + winrt::hstring CommandViewModel::DisplayName() { - return (IsContainerFocused() || IsEditButtonFocused() || IsHovered() || IsAutomationPeerAttached()) && !IsInEditMode(); + if (_cachedDisplayName.empty()) + { + _cachedDisplayName = _command.Name(); + } + return _cachedDisplayName; } - void KeyBindingViewModel::ToggleEditMode() + winrt::hstring CommandViewModel::Name() + { + return _command.HasName() ? _command.Name() : L""; + } + + void CommandViewModel::Name(const winrt::hstring& newName) + { + _command.Name(newName); + if (newName.empty()) + { + // if the name was cleared, refresh the DisplayName + _NotifyChanges(L"DisplayName", L"DisplayNameAndKeyChordAutomationPropName"); + } + _cachedDisplayName.clear(); + } + + winrt::hstring CommandViewModel::DisplayNameAndKeyChordAutomationPropName() + { + return DisplayName() + L", " + FirstKeyChordText(); + } + + winrt::hstring CommandViewModel::FirstKeyChordText() + { + if (_KeyChordList.Size() != 0) + { + return _KeyChordList.GetAt(0).KeyChordText(); + } + return L""; + } + + winrt::hstring CommandViewModel::ID() + { + return _command.ID(); + } + + bool CommandViewModel::IsUserAction() + { + return _command.Origin() == OriginTag::User; + } + + void CommandViewModel::Edit_Click() + { + EditRequested.raise(*this, *this); + } + + void CommandViewModel::Delete_Click() + { + DeleteRequested.raise(*this, *this); + } + + void CommandViewModel::AddKeybinding_Click() + { + auto kbdVM{ make_self(nullptr) }; + kbdVM->IsInEditMode(true); + _RegisterKeyChordVMEvents(*kbdVM); + KeyChordList().Append(*kbdVM); + } + + winrt::hstring CommandViewModel::ActionNameTextBoxAutomationPropName() + { + return RS_(L"Actions_Name/Text"); + } + + winrt::hstring CommandViewModel::ShortcutActionComboBoxAutomationPropName() + { + return RS_(L"Actions_ShortcutAction/Text"); + } + + winrt::hstring CommandViewModel::AdditionalArgumentsControlAutomationPropName() + { + return RS_(L"Actions_Arguments/Text"); + } + + void CommandViewModel::_RegisterKeyChordVMEvents(Editor::KeyChordViewModel kcVM) + { + const auto id = ID(); + kcVM.AddKeyChordRequested([actionsPageVMWeakRef = _actionsPageVM, id](const Editor::KeyChordViewModel& sender, const Control::KeyChord& keys) { + if (const auto actionsPageVM{ actionsPageVMWeakRef.get() }) + { + actionsPageVM.AttemptAddOrModifyKeyChord(sender, id, keys, nullptr); + } + }); + kcVM.ModifyKeyChordRequested([actionsPageVMWeakRef = _actionsPageVM, id](const Editor::KeyChordViewModel& sender, const Editor::ModifyKeyChordEventArgs& args) { + if (const auto actionsPageVM{ actionsPageVMWeakRef.get() }) + { + actionsPageVM.AttemptAddOrModifyKeyChord(sender, id, args.NewKeys(), args.OldKeys()); + } + }); + kcVM.DeleteKeyChordRequested([&, actionsPageVMWeakRef = _actionsPageVM](const Editor::KeyChordViewModel& sender, const Control::KeyChord& args) { + if (const auto actionsPageVM{ actionsPageVMWeakRef.get() }) + { + std::erase_if(_keyChordList, + [&](const Control::KeyChord& kc) { return kc == args; }); + for (uint32_t i = 0; i < _KeyChordList.Size(); i++) + { + if (_KeyChordList.GetAt(i) == sender) + { + KeyChordList().RemoveAt(i); + break; + } + } + actionsPageVM.DeleteKeyChord(args); + } + }); + kcVM.PropertyChanged([weakThis{ get_weak() }](const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args) { + if (const auto self{ weakThis.get() }) + { + const auto senderVM{ sender.as() }; + const auto propertyName{ args.PropertyName() }; + if (propertyName == L"IsInEditMode") + { + if (!senderVM.IsInEditMode()) + { + self->FocusContainer.raise(*self, senderVM); + } + } + } + }); + } + + void CommandViewModel::_RegisterActionArgsVMEvents(Editor::ActionArgsViewModel actionArgsVM) + { + actionArgsVM.PropagateColorSchemeRequested([weakThis = get_weak()](const IInspectable& /*sender*/, const Editor::ArgWrapper& wrapper) { + if (auto weak = weakThis.get()) + { + if (wrapper) + { + weak->PropagateColorSchemeRequested.raise(*weak, wrapper); + } + } + }); + actionArgsVM.PropagateColorSchemeNamesRequested([weakThis = get_weak()](const IInspectable& /*sender*/, const Editor::ArgWrapper& wrapper) { + if (auto weak = weakThis.get()) + { + if (wrapper) + { + weak->PropagateColorSchemeNamesRequested.raise(*weak, wrapper); + } + } + }); + actionArgsVM.PropagateWindowRootRequested([weakThis = get_weak()](const IInspectable& /*sender*/, const Editor::ArgWrapper& wrapper) { + if (auto weak = weakThis.get()) + { + if (wrapper) + { + weak->PropagateWindowRootRequested.raise(*weak, wrapper); + } + } + }); + actionArgsVM.WrapperValueChanged([weakThis = get_weak()](const IInspectable& /*sender*/, const IInspectable& /*args*/) { + if (auto weak = weakThis.get()) + { + // for new commands, make sure we generate a new ID every time any arg value changes + if (weak->_IsNewCommand) + { + if (const auto actionsPageVM{ weak->_actionsPageVM.get() }) + { + actionsPageVM.RegenerateCommandID(weak->_command); + } + } + else if (!weak->IsUserAction()) + { + weak->_ReplaceCommandWithUserCopy(false); + } + weak->_NotifyChanges(L"DisplayName"); + } + }); + } + + void CommandViewModel::_ReplaceCommandWithUserCopy(bool reinitialize) + { + // the user is attempting to edit an in-box action + // to handle this, we create a new command with the new values that has the same ID as the in-box action + // swap out our underlying command with the copy, tell the ActionsVM that the copy needs to be added to the action map + if (const auto actionsPageVM{ _actionsPageVM.get() }) + { + const auto newCmd = Model::Command::CopyAsUserCommand(_command); + _command = newCmd; + actionsPageVM.AddCopiedCommand(_command); + if (reinitialize) + { + // full reinitialize needed, recreate the action args VM + // (this happens when the shortcut action is being changed on an in-box action) + _CreateAndInitializeActionArgsVMHelper(); + } + else + { + // no need to reinitialize, just swap out the underlying data model + // (this happens when an additional argument is being changed on an in-box action) + auto actionArgsVMImpl{ get_self(_ActionArgsVM) }; + actionArgsVMImpl->ReplaceActionAndArgs(_command.ActionAndArgs()); + } + } + } + + void CommandViewModel::_CreateAndInitializeActionArgsVMHelper() + { + const auto actionArgsVM = make_self(_command.ActionAndArgs()); + _RegisterActionArgsVMEvents(*actionArgsVM); + actionArgsVM->Initialize(); + ActionArgsVM(*actionArgsVM); + _NotifyChanges(L"DisplayName"); + } + + ArgWrapper::ArgWrapper(const Model::ArgDescriptor& descriptor, const Windows::Foundation::IInspectable& value) : + _descriptor{ descriptor } + { + Value(value); + } + + void ArgWrapper::Initialize() + { + if (_descriptor.Type == L"Model::ResizeDirection") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::ResizeDirection().GetView(), + L"Actions_ResizeDirection", + L"Content", + false); + } + else if (_descriptor.Type == L"Model::FocusDirection") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::FocusDirection().GetView(), + L"Actions_FocusDirection", + L"Content", + false); + } + else if (_descriptor.Type == L"SettingsTarget") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::SettingsTarget().GetView(), + L"Actions_SettingsTarget", + L"Content", + false); + } + else if (_descriptor.Type == L"MoveTabDirection") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::MoveTabDirection().GetView(), + L"Actions_MoveTabDirection", + L"Content", + false); + } + else if (_descriptor.Type == L"Microsoft::Terminal::Control::ScrollToMarkDirection") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::ScrollToMarkDirection().GetView(), + L"Actions_ScrollToMarkDirection", + L"Content", + false); + } + else if (_descriptor.Type == L"CommandPaletteLaunchMode") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::CommandPaletteLaunchMode().GetView(), + L"Actions_CommandPaletteLaunchMode", + L"Content", + false); + } + else if (_descriptor.Type == L"SuggestionsSource") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::SuggestionsSource().GetView(), + L"Actions_SuggestionsSource", + L"Content", + false); + } + else if (_descriptor.Type == L"FindMatchDirection") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::FindMatchDirection().GetView(), + L"Actions_FindMatchDirection", + L"Content", + false); + } + else if (_descriptor.Type == L"Model::DesktopBehavior") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::DesktopBehavior().GetView(), + L"Actions_DesktopBehavior", + L"Content", + false); + } + else if (_descriptor.Type == L"Model::MonitorBehavior") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::MonitorBehavior().GetView(), + L"Actions_MonitorBehavior", + L"Content", + false); + } + else if (_descriptor.Type == L"winrt::Microsoft::Terminal::Control::ClearBufferType") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::ClearBufferType().GetView(), + L"Actions_ClearBufferType", + L"Content", + false); + } + else if (_descriptor.Type == L"SelectOutputDirection") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::SelectOutputDirection().GetView(), + L"Actions_SelectOutputDirection", + L"Content", + false); + } + else if (_descriptor.Type == L"Model::SplitDirection") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::SplitDirection().GetView(), + L"Actions_SplitDirection", + L"Content", + false); + } + else if (_descriptor.Type == L"SplitType") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::SplitType().GetView(), + L"Actions_SplitType", + L"Content", + false); + } + else if (_descriptor.Type == L"Windows::Foundation::IReference") + { + _InitializeEnumListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::TabSwitcherMode().GetView(), + L"Actions_TabSwitcherMode", + L"Content", + true); + } + else if (_descriptor.Type == L"Windows::Foundation::IReference") + { + _InitializeFlagListAndValue( + winrt::Microsoft::Terminal::Settings::Model::EnumMappings::CopyFormat().GetView(), + L"Actions_CopyFormat", + L"Content", + true); + } + else if (_descriptor.Type == L"Windows::Foundation::IReference" || + _descriptor.Type == L"Windows::Foundation::IReference") + { + ColorSchemeRequested.raise(*this, *this); + } + else if (_descriptor.TypeHint == Model::ArgTypeHint::ColorScheme) + { + // special case of string, emit an event letting the actionsVM know we need the list of color scheme names + ColorSchemeNamesRequested.raise(*this, *this); + + // even though the arg type is technically a string, we want an enum list for color schemes specifically + std::vector namesList; + const auto currentSchemeName = unbox_value(_Value); + auto nullEntry = winrt::make(RS_(L"Actions_NullEnumValue"), nullptr, -1); + if (currentSchemeName.empty()) + { + _EnumValue = nullEntry; + } + for (const auto colorSchemeName : _ColorSchemeNamesList) + { + // eventually we will want to use localized names for the enum entries, for now just use what the settings model gives us + auto entry = winrt::make(colorSchemeName, winrt::box_value(colorSchemeName)); + namesList.emplace_back(entry); + if (currentSchemeName == colorSchemeName) + { + _EnumValue = entry; + } + } + namesList.emplace_back(std::move(nullEntry)); + _EnumList = winrt::single_threaded_observable_vector(std::move(namesList)); + _NotifyChanges(L"EnumList", L"EnumValue"); + } + } + + safe_void_coroutine ArgWrapper::BrowseForFile_Click(const IInspectable&, const RoutedEventArgs&) + { + WindowRootRequested.raise(*this, *this); + auto lifetime = get_strong(); + + static constexpr winrt::guid clientGuidFiles{ 0xbd00ae34, 0x839b, 0x43f6, { 0x8b, 0x94, 0x12, 0x37, 0x1a, 0xfe, 0xea, 0xb5 } }; + const auto parentHwnd{ reinterpret_cast(_WindowRoot.GetHostingWindow()) }; + auto path = co_await OpenFilePicker(parentHwnd, [](auto&& dialog) { + THROW_IF_FAILED(dialog->SetClientGuid(clientGuidFiles)); + try + { + auto folderShellItem{ winrt::capture(&SHGetKnownFolderItem, FOLDERID_ComputerFolder, KF_FLAG_DEFAULT, nullptr) }; + dialog->SetDefaultFolder(folderShellItem.get()); + } + CATCH_LOG(); // non-fatal + }); + + if (!path.empty()) + { + StringBindBack(path); + } + } + + safe_void_coroutine ArgWrapper::BrowseForFolder_Click(const IInspectable&, const RoutedEventArgs&) + { + WindowRootRequested.raise(*this, *this); + auto lifetime = get_strong(); + + static constexpr winrt::guid clientGuidFolders{ 0xa611027, 0x42be, 0x4665, { 0xaf, 0xf1, 0x3f, 0x22, 0x26, 0xe9, 0xf7, 0x4d } }; + ; + const auto parentHwnd{ reinterpret_cast(_WindowRoot.GetHostingWindow()) }; + auto path = co_await OpenFilePicker(parentHwnd, [](auto&& dialog) { + THROW_IF_FAILED(dialog->SetClientGuid(clientGuidFolders)); + try + { + auto folderShellItem{ winrt::capture(&SHGetKnownFolderItem, FOLDERID_ComputerFolder, KF_FLAG_DEFAULT, nullptr) }; + dialog->SetDefaultFolder(folderShellItem.get()); + } + CATCH_LOG(); // non-fatal + + DWORD flags{}; + THROW_IF_FAILED(dialog->GetOptions(&flags)); + THROW_IF_FAILED(dialog->SetOptions(flags | FOS_PICKFOLDERS)); // folders only + }); + + if (!path.empty()) + { + StringBindBack(path); + } + } + + void ArgWrapper::EnumValue(const Windows::Foundation::IInspectable& enumValue) + { + if (_EnumValue != enumValue) + { + _EnumValue = enumValue; + Value(_EnumValue.as().EnumValue()); + } + } + + winrt::hstring ArgWrapper::UnboxString(const Windows::Foundation::IInspectable& value) + { + return winrt::unbox_value(value); + } + + int32_t ArgWrapper::UnboxInt32(const Windows::Foundation::IInspectable& value) + { + return winrt::unbox_value(value); + } + + float ArgWrapper::UnboxInt32Optional(const Windows::Foundation::IInspectable& value) + { + const auto unboxed = winrt::unbox_value>(value); + if (unboxed) + { + return static_cast(unboxed.Value()); + } + else + { + return NAN; + } + } + + uint32_t ArgWrapper::UnboxUInt32(const Windows::Foundation::IInspectable& value) + { + return winrt::unbox_value(value); + } + + float ArgWrapper::UnboxUInt32Optional(const Windows::Foundation::IInspectable& value) + { + const auto unboxed = winrt::unbox_value>(value); + if (unboxed) + { + return static_cast(unboxed.Value()); + } + else + { + return NAN; + } + } + + float ArgWrapper::UnboxFloat(const Windows::Foundation::IInspectable& value) + { + return winrt::unbox_value(value); + } + + bool ArgWrapper::UnboxBool(const Windows::Foundation::IInspectable& value) + { + return winrt::unbox_value(value); + } + + winrt::Windows::Foundation::IReference ArgWrapper::UnboxBoolOptional(const Windows::Foundation::IInspectable& value) + { + if (!value) + { + return nullptr; + } + return winrt::unbox_value>(value); + } + + winrt::Windows::Foundation::IReference ArgWrapper::UnboxTerminalCoreColorOptional(const Windows::Foundation::IInspectable& value) + { + if (value) + { + return unbox_value>(value); + } + else + { + return nullptr; + } + } + + winrt::Windows::Foundation::IReference ArgWrapper::UnboxWindowsUIColorOptional(const Windows::Foundation::IInspectable& value) + { + if (value) + { + const auto winUIColor = unbox_value>(value).Value(); + const Microsoft::Terminal::Core::Color terminalColor{ winUIColor.R, winUIColor.G, winUIColor.B, winUIColor.A }; + return Windows::Foundation::IReference{ terminalColor }; + } + else + { + return nullptr; + } + } + + void ArgWrapper::StringBindBack(const winrt::hstring& newValue) + { + if (UnboxString(_Value) != newValue) + { + Value(box_value(newValue)); + } + } + + void ArgWrapper::Int32BindBack(const double newValue) + { + if (UnboxInt32(_Value) != newValue) + { + Value(box_value(static_cast(newValue))); + } + } + + void ArgWrapper::Int32OptionalBindBack(const double newValue) + { + if (!isnan(newValue)) + { + const auto currentValue = UnboxInt32Optional(_Value); + if (isnan(currentValue) || static_cast(currentValue) != static_cast(newValue)) + { + Value(box_value(static_cast(newValue))); + } + } + else if (_Value) + { + Value(nullptr); + } + } + + void ArgWrapper::UInt32BindBack(const double newValue) + { + if (UnboxUInt32(_Value) != newValue) + { + Value(box_value(static_cast(newValue))); + } + } + + void ArgWrapper::UInt32OptionalBindBack(const double newValue) + { + if (!isnan(newValue)) + { + const auto currentValue = UnboxUInt32Optional(_Value); + if (isnan(currentValue) || static_cast(currentValue) != static_cast(newValue)) + { + Value(box_value(static_cast(newValue))); + } + } + else if (_Value) + { + Value(nullptr); + } + } + + void ArgWrapper::FloatBindBack(const double newValue) + { + if (UnboxFloat(_Value) != newValue) + { + Value(box_value(static_cast(newValue))); + } + } + + void ArgWrapper::BoolOptionalBindBack(const Windows::Foundation::IReference newValue) + { + if (newValue) + { + const auto currentValue = UnboxBoolOptional(_Value); + if (!currentValue || currentValue.Value() != newValue.Value()) + { + Value(box_value(newValue)); + } + } + else if (_Value) + { + Value(nullptr); + } + } + + void ArgWrapper::TerminalCoreColorBindBack(const winrt::Windows::Foundation::IReference newValue) + { + if (newValue) + { + const auto currentValue = UnboxTerminalCoreColorOptional(_Value); + if (!currentValue || currentValue.Value() != newValue.Value()) + { + Value(box_value(newValue)); + } + } + else if (_Value) + { + Value(nullptr); + } + } + + void ArgWrapper::WindowsUIColorBindBack(const winrt::Windows::Foundation::IReference newValue) + { + if (newValue) + { + const auto terminalCoreColor = unbox_value>(newValue).Value(); + const Windows::UI::Color winuiColor{ + .A = terminalCoreColor.A, + .R = terminalCoreColor.R, + .G = terminalCoreColor.G, + .B = terminalCoreColor.B + }; + // only set to the new value if our current value is not the same + // unfortunately the Value setter does not do this check properly since + // we create a whole new IReference even for the same underlying color + if (_Value) + { + const auto currentValue = unbox_value>(_Value).Value(); + if (currentValue == winuiColor) + { + return; + } + } + Value(box_value(Windows::Foundation::IReference{ winuiColor })); + } + else if (_Value) + { + Value(nullptr); + } + } + + template + void ArgWrapper::_InitializeEnumListAndValue( + const winrt::Windows::Foundation::Collections::IMapView& mappings, + const winrt::hstring& resourceSectionAndType, + const winrt::hstring& resourceProperty, + const bool nullable) + { + std::vector enumList; + std::unordered_set addedEnums; + EnumType unboxedValue{}; + + winrt::Microsoft::Terminal::Settings::Editor::EnumEntry nullEntry = nullable ? winrt::make(RS_(L"Actions_NullEnumValue"), nullptr, -1) : + nullptr; + + if (_Value) + { + unboxedValue = winrt::unbox_value(_Value); + } + + for (const auto& [enumKey, enumValue] : mappings) + { + if (addedEnums.emplace(enumValue).second) + { + winrt::hstring enumName = LocalizedNameForEnumName(resourceSectionAndType, enumKey, resourceProperty); + auto entry = winrt::make( + enumName, winrt::box_value(enumValue), static_cast(enumValue)); + enumList.emplace_back(entry); + if (_Value && unboxedValue == enumValue) + { + _EnumValue = entry; + } + } + } + + std::sort(enumList.begin(), enumList.end(), winrt::Microsoft::Terminal::Settings::Editor::implementation::EnumEntryReverseComparator()); + + if (nullable) + { + enumList.emplace_back(nullEntry); + } + + _EnumList = winrt::single_threaded_observable_vector(std::move(enumList)); + + if (!_EnumValue) + { + _EnumValue = nullable ? nullEntry : _EnumList.GetAt(0); + } + } + + template + void ArgWrapper::_InitializeFlagListAndValue( + const winrt::Windows::Foundation::Collections::IMapView& mappings, + const winrt::hstring& resourceSectionAndType, + const winrt::hstring& resourceProperty, + const bool nullable) + { + std::vector flagList; + std::unordered_set addedEnums; + EnumType unboxedValue{ 0 }; + + winrt::Microsoft::Terminal::Settings::Editor::FlagEntry nullEntry{ nullptr }; + + if (nullable) + { + nullEntry = winrt::make( + RS_(L"Actions_NullEnumValue"), nullptr, true, -1); + if (_Value) + { + if (auto ref = winrt::unbox_value>(_Value)) + { + unboxedValue = ref.Value(); + nullEntry.IsSet(false); + } + } + } + else + { + if (_Value) + { + unboxedValue = winrt::unbox_value(_Value); + } + } + + for (const auto& [flagKey, flagValue] : mappings) + { + if (flagKey != L"all" && flagKey != L"none" && addedEnums.emplace(flagValue).second) + { + winrt::hstring flagName = LocalizedNameForEnumName(resourceSectionAndType, flagKey, resourceProperty); + bool isSet = WI_IsAnyFlagSet(unboxedValue, flagValue); + auto entry = winrt::make(flagName, winrt::box_value(flagValue), isSet, static_cast(flagValue)); + + if (nullable) + { + // The event handler when the flag is nullable is different because we have to update nullEntry + // i.e. if another flag gets turned on, nullEntry needs to be turned off and vice-versa + entry.PropertyChanged([this, flagValue, nullEntry](const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args) { + if (args.PropertyName() == L"IsSet") + { + EnumType local{ 0 }; + if (auto ref = winrt::unbox_value>(_Value)) + { + local = ref.Value(); + } + + auto flagWrapper = sender.as(); + if (flagWrapper.IsSet()) + { + nullEntry.IsSet(false); + WI_SetAllFlags(local, flagValue); + } + else + { + WI_ClearAllFlags(local, flagValue); + } + + Value(winrt::box_value(winrt::Windows::Foundation::IReference(local))); + } + }); + } + else + { + // Non-nullable version + entry.PropertyChanged([this, flagValue](const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args) { + if (args.PropertyName() == L"IsSet") + { + auto flagWrapper = sender.as(); + auto local = winrt::unbox_value(_Value); + + if (flagWrapper.IsSet()) + { + WI_SetAllFlags(local, flagValue); + } + else + { + WI_ClearAllFlags(local, flagValue); + } + + Value(winrt::box_value(local)); + } + }); + } + flagList.emplace_back(entry); + } + } + + std::sort(flagList.begin(), flagList.end(), winrt::Microsoft::Terminal::Settings::Editor::implementation::FlagEntryReverseComparator()); + + if (nullable) + { + nullEntry.PropertyChanged([this](const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args) { + if (args.PropertyName() == L"IsSet") + { + auto wrapper = sender.as(); + if (wrapper.IsSet()) + { + for (const auto& flagEntry : _FlagList) + { + if (flagEntry.FlagName() != RS_(L"Actions_NullEnumValue")) + { + flagEntry.IsSet(false); + } + } + Value(winrt::box_value(winrt::Windows::Foundation::IReference(nullptr))); + } + else + { + Value(winrt::box_value(winrt::Windows::Foundation::IReference(EnumType{ 0 }))); + } + } + }); + flagList.emplace_back(nullEntry); + } + + _FlagList = winrt::single_threaded_observable_vector(std::move(flagList)); + } + + ActionArgsViewModel::ActionArgsViewModel(const Model::ActionAndArgs actionAndArgs) : + _actionAndArgs{ actionAndArgs } + { + } + + void ActionArgsViewModel::Initialize() + { + const auto shortcutArgs = _actionAndArgs.Args().as(); + if (shortcutArgs) + { + const auto shortcutArgsDescriptors = shortcutArgs.GetArgDescriptors(); + std::vector argValues; + uint32_t i = 0; + for (const auto argDescription : shortcutArgsDescriptors) + { + const auto argAtIndex = shortcutArgs.GetArgAt(i); + const auto item = make_self(argDescription, argAtIndex); + item->PropertyChanged([this, i](const IInspectable& sender, const PropertyChangedEventArgs& args) { + const auto itemProperty{ args.PropertyName() }; + if (itemProperty == L"Value") + { + const auto argWrapper = sender.as(); + const auto newValue = argWrapper.Value(); + _actionAndArgs.Args().as().SetArgAt(i, newValue); + WrapperValueChanged.raise(*this, nullptr); + } + }); + item->ColorSchemeRequested([this](const IInspectable& /*sender*/, const Editor::ArgWrapper& wrapper) { + if (wrapper) + { + PropagateColorSchemeRequested.raise(*this, wrapper); + } + }); + item->ColorSchemeNamesRequested([this](const IInspectable& /*sender*/, const Editor::ArgWrapper& wrapper) { + if (wrapper) + { + PropagateColorSchemeNamesRequested.raise(*this, wrapper); + } + }); + item->WindowRootRequested([this](const IInspectable& /*sender*/, const Editor::ArgWrapper& wrapper) { + if (wrapper) + { + PropagateWindowRootRequested.raise(*this, wrapper); + } + }); + item->Initialize(); + argValues.push_back(*item); + i++; + } + + _ArgValues = single_threaded_observable_vector(std::move(argValues)); + } + } + + bool ActionArgsViewModel::HasArgs() const noexcept + { + return _actionAndArgs.Args() != nullptr; + } + + void ActionArgsViewModel::ReplaceActionAndArgs(Model::ActionAndArgs newActionAndArgs) + { + _actionAndArgs = newActionAndArgs; + } + + KeyChordViewModel::KeyChordViewModel(Control::KeyChord currentKeys) + { + CurrentKeys(currentKeys); + } + + void KeyChordViewModel::CurrentKeys(const Control::KeyChord& newKeys) + { + _currentKeys = newKeys; + KeyChordText(Model::KeyChordSerialization::ToString(_currentKeys)); + } + + Control::KeyChord KeyChordViewModel::CurrentKeys() const noexcept + { + return _currentKeys; + } + + void KeyChordViewModel::ToggleEditMode() { // toggle edit mode IsInEditMode(!_IsInEditMode); if (_IsInEditMode) { - // if we're in edit mode, - // - prepopulate the text box with the current keys - // - reset the combo box with the current action - ProposedKeys(_CurrentKeys); - ProposedAction(box_value(_CurrentAction)); + // if we're in edit mode, populate the text box with the current keys + ProposedKeys(_currentKeys); } } - void KeyBindingViewModel::AttemptAcceptChanges() + void KeyChordViewModel::AcceptChanges() { - AttemptAcceptChanges(_ProposedKeys); - } - - void KeyBindingViewModel::AttemptAcceptChanges(const Control::KeyChord newKeys) - { - const auto args{ make_self(_CurrentKeys, // OldKeys - newKeys, // NewKeys - _IsNewlyAdded ? hstring{} : _CurrentAction, // OldAction - unbox_value(_ProposedAction)) }; // NewAction - ModifyKeyBindingRequested.raise(*this, *args); - } - - void KeyBindingViewModel::CancelChanges() - { - if (_IsNewlyAdded) + if (!_currentKeys) { - DeleteNewlyAddedKeyBinding.raise(*this, nullptr); + AddKeyChordRequested.raise(*this, _ProposedKeys); + } + else if (_currentKeys.Modifiers() != _ProposedKeys.Modifiers() || _currentKeys.Vkey() != _ProposedKeys.Vkey()) + { + const auto args{ make_self(_currentKeys, // OldKeys + _ProposedKeys) }; // NewKeys + ModifyKeyChordRequested.raise(*this, *args); } else { + // no changes being requested, toggle edit mode ToggleEditMode(); } } + void KeyChordViewModel::CancelChanges() + { + ToggleEditMode(); + } + + void KeyChordViewModel::DeleteKeyChord() + { + DeleteKeyChordRequested.raise(*this, _currentKeys); + } + + hstring KeyChordViewModel::CancelButtonName() const noexcept { return RS_(L"Actions_CancelButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } + hstring KeyChordViewModel::AcceptButtonName() const noexcept { return RS_(L"Actions_AcceptButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } + hstring KeyChordViewModel::DeleteButtonName() const noexcept { return RS_(L"Actions_DeleteButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } + ActionsViewModel::ActionsViewModel(Model::CascadiaSettings settings) : _Settings{ settings } { - // Populate AvailableActionAndArgs - _AvailableActionMap = single_threaded_map(); - std::vector availableActionAndArgs; - for (const auto& [name, actionAndArgs] : _Settings.ActionMap().AvailableActions()) + // Initialize the action->name and name->action maps before initializing the CommandVMs, they're going to need the maps + _AvailableActionsAndNamesMap = Model::ActionArgFactory::AvailableShortcutActionsAndNames(); + for (const auto unimplemented : UnimplementedShortcutActions) { - availableActionAndArgs.push_back(name); - _AvailableActionMap.Insert(name, actionAndArgs); + _AvailableActionsAndNamesMap.Remove(unimplemented); } - std::sort(begin(availableActionAndArgs), end(availableActionAndArgs)); - _AvailableActionAndArgs = single_threaded_observable_vector(std::move(availableActionAndArgs)); - - // Convert the key bindings from our settings into a view model representation - const auto& keyBindingMap{ _Settings.ActionMap().KeyBindings() }; - std::vector keyBindingList; - keyBindingList.reserve(keyBindingMap.Size()); - for (const auto& [keys, cmd] : keyBindingMap) + std::unordered_map actionNames; + for (const auto [action, name] : _AvailableActionsAndNamesMap) { - // convert the cmd into a KeyBindingViewModel - auto container{ make_self(keys, cmd.Name(), _AvailableActionAndArgs) }; - _RegisterEvents(container); - keyBindingList.push_back(*container); + actionNames.emplace(name, action); } + _NameToActionMap = winrt::single_threaded_map(std::move(actionNames)); - std::sort(begin(keyBindingList), end(keyBindingList), KeyBindingViewModelComparator{}); - _KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList)); + _MakeCommandVMsHelper(); } - void ActionsViewModel::OnAutomationPeerAttached() + Windows::Foundation::Collections::IMap ActionsViewModel::AvailableShortcutActionsAndNames() { - _AutomationPeerAttached = true; - for (const auto& kbdVM : _KeyBindingList) - { - // To create a more accessible experience, we want the "edit" buttons to _always_ - // appear when a screen reader is attached. This ensures that the edit buttons are - // accessible via the UIA tree. - get_self(kbdVM)->IsAutomationPeerAttached(_AutomationPeerAttached); - } + return _AvailableActionsAndNamesMap; } - void ActionsViewModel::AddNewKeybinding() + Windows::Foundation::Collections::IMap ActionsViewModel::NameToActionMap() { - // Create the new key binding and register all of the event handlers. - auto kbdVM{ make_self(_AvailableActionAndArgs) }; - _RegisterEvents(kbdVM); - kbdVM->DeleteNewlyAddedKeyBinding({ this, &ActionsViewModel::_KeyBindingViewModelDeleteNewlyAddedKeyBindingHandler }); - - // Manually add the editing background. This needs to be done in Actions not the view model. - // We also have to do this manually because it hasn't been added to the list yet. - kbdVM->IsInEditMode(true); - // Emit an event to let the page know to update the background of this key binding VM - UpdateBackground.raise(*this, *kbdVM); - - // IMPORTANT: do this _after_ setting IsInEditMode. Otherwise, it'll get deleted immediately - // by the PropertyChangedHandler below (where we delete any IsNewlyAdded items) - kbdVM->IsNewlyAdded(true); - _KeyBindingList.InsertAt(0, *kbdVM); - FocusContainer.raise(*this, *kbdVM); + return _NameToActionMap; } - void ActionsViewModel::_KeyBindingViewModelPropertyChangedHandler(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args) + void ActionsViewModel::UpdateSettings(const Model::CascadiaSettings& settings) { - const auto senderVM{ sender.as() }; - const auto propertyName{ args.PropertyName() }; - if (propertyName == L"IsInEditMode") + _Settings = settings; + + // We want to re-initialize our CommandList, but we want to make sure + // we still have the same CurrentCommand as before (if that command still exists) + + // Store the ID of the current command + const auto currentCommandID = CurrentCommand() ? CurrentCommand().ID() : hstring{}; + + // Re-initialize the command vm list + _MakeCommandVMsHelper(); + + // Re-select the previously selected command if it exists + bool found{ false }; + if (!currentCommandID.empty()) { - if (senderVM.IsInEditMode()) + const auto it = _CommandList.First(); + while (it.HasCurrent()) { - // Ensure that... - // 1. we move focus to the edit mode controls - // 2. any actions that were newly added are removed - // 3. this is the only entry that is in edit mode - for (int32_t i = _KeyBindingList.Size() - 1; i >= 0; --i) + auto cmd = *it; + if (cmd.ID() == currentCommandID) { - const auto& kbdVM{ _KeyBindingList.GetAt(i) }; - if (senderVM == kbdVM) - { - // This is the view model entry that went into edit mode. - // Emit an event to let the page know to move focus to - // this VM's container. - FocusContainer.raise(*this, senderVM); - } - else if (kbdVM.IsNewlyAdded()) - { - // Remove any actions that were newly added - _KeyBindingList.RemoveAt(i); - } - else - { - // Exit edit mode for all other containers - get_self(kbdVM)->DisableEditMode(); - } + CurrentCommand(cmd); + found = true; + break; } + it.MoveNext(); } - else - { - // Emit an event to let the page know to move focus to - // this VM's container. - FocusContainer.raise(*this, senderVM); - } - - // Emit an event to let the page know to update the background of this key binding VM - UpdateBackground.raise(*this, senderVM); + } + if (!found) + { + // didn't have a command, + // so skip over looking through the command + CurrentCommand(nullptr); + CurrentPage(ActionsSubPage::Base); } } - void ActionsViewModel::_KeyBindingViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& keys) + void ActionsViewModel::MarkAsVisited() + { + Model::ApplicationState::SharedInstance().DismissBadge(ActionsPageId); + _NotifyChanges(L"DisplayBadge"); + } + + bool ActionsViewModel::DisplayBadge() const noexcept + { + return !Model::ApplicationState::SharedInstance().BadgeDismissed(ActionsPageId); + } + + void ActionsViewModel::_MakeCommandVMsHelper() + { + const auto& allCommands{ _Settings.ActionMap().AllCommands() }; + std::vector commandList; + commandList.reserve(allCommands.Size()); + for (const auto& cmd : allCommands) + { + if (!UnimplementedShortcutActions.contains(cmd.ActionAndArgs().Action())) + { + std::vector keyChordList = wil::to_vector(_Settings.ActionMap().AllKeyBindingsForAction(cmd.ID())); + auto cmdVM{ make_self(cmd, std::move(keyChordList), *this, _AvailableActionsAndNamesMap, _NameToActionMap) }; + _RegisterCmdVMEvents(cmdVM); + cmdVM->Initialize(); + commandList.push_back(*cmdVM); + } + } + + std::sort(commandList.begin(), commandList.end(), [](const Editor::CommandViewModel& lhs, const Editor::CommandViewModel& rhs) { + return lhs.DisplayName() < rhs.DisplayName(); + }); + _CommandList = single_threaded_observable_vector(std::move(commandList)); + } + + void ActionsViewModel::AddNewCommand() + { + const auto newCmd = Model::Command::NewUserCommand(); + // construct a command using the first shortcut action from our list + const auto shortcutAction = _AvailableActionsAndNamesMap.First().Current().Key(); + const auto args = ActionArgFactory::GetEmptyArgsForAction(shortcutAction); + newCmd.ActionAndArgs(Model::ActionAndArgs{ shortcutAction, args }); + _Settings.ActionMap().AddAction(newCmd, nullptr); + auto cmdVM{ make_self(newCmd, std::vector{}, *this, _AvailableActionsAndNamesMap, _NameToActionMap) }; + cmdVM->IsNewCommand(true); + _RegisterCmdVMEvents(cmdVM); + cmdVM->Initialize(); + _CommandList.Append(*cmdVM); + CurrentCommand(*cmdVM); + CurrentPage(ActionsSubPage::Edit); + } + + void ActionsViewModel::CurrentCommand(const Editor::CommandViewModel& newCommand) + { + _CurrentCommand = newCommand; + } + + Editor::CommandViewModel ActionsViewModel::CurrentCommand() + { + return _CurrentCommand; + } + + void ActionsViewModel::CmdListItemClicked(const IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::Controls::ItemClickEventArgs& e) + { + if (const auto item = e.ClickedItem()) + { + CurrentCommand(item.try_as()); + CurrentPage(ActionsSubPage::Edit); + } + } + + void ActionsViewModel::DeleteKeyChord(const Control::KeyChord& keys) { // Update the settings model - _Settings.ActionMap().DeleteKeyBinding(keys); - - // Find the current container in our list and remove it. - // This is much faster than rebuilding the entire ActionMap. - uint32_t index; - if (_KeyBindingList.IndexOf(senderVM, index)) + assert(keys); + if (keys) { - _KeyBindingList.RemoveAt(index); - - // Focus the new item at this index - if (_KeyBindingList.Size() != 0) - { - const auto newFocusedIndex{ std::clamp(index, 0u, _KeyBindingList.Size() - 1) }; - // Emit an event to let the page know to move focus to - // this VM's container. - FocusContainer.raise(*this, winrt::box_value(newFocusedIndex)); - } + _Settings.ActionMap().DeleteKeyBinding(keys); } } - void ActionsViewModel::_KeyBindingViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args) + void ActionsViewModel::AttemptAddOrModifyKeyChord(const Editor::KeyChordViewModel& senderVM, winrt::hstring commandID, const Control::KeyChord& newKeys, const Control::KeyChord& oldKeys) { - const auto isNewAction{ !args.OldKeys() && args.OldActionName().empty() }; - auto applyChangesToSettingsModel = [=]() { - // If the key chord was changed, - // update the settings model and view model appropriately - // NOTE: we still need to update the view model if we're working with a newly added action - if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey()) + // update settings model + if (oldKeys) { - if (!isNewAction) - { - // update settings model - _Settings.ActionMap().RebindKeys(args.OldKeys(), args.NewKeys()); - } + // if oldKeys is not null, this is a rebinding + // delete oldKeys and then add newKeys + _Settings.ActionMap().DeleteKeyBinding(oldKeys); + } + if (!Model::KeyChordSerialization::ToString(newKeys).empty()) + { + _Settings.ActionMap().AddKeyBinding(newKeys, commandID); // update view model - auto senderVMImpl{ get_self(senderVM) }; - senderVMImpl->CurrentKeys(args.NewKeys()); + auto senderVMImpl{ get_self(senderVM) }; + senderVMImpl->CurrentKeys(newKeys); } - // If the action was changed, - // update the settings model and view model appropriately - // NOTE: no need to check for "isNewAction" here. != already. - if (args.OldActionName() != args.NewActionName()) + // reset the flyout if it's there + if (const auto flyout = senderVM.AcceptChangesFlyout()) { - // convert the action's name into a view model. - const auto& newAction{ _AvailableActionMap.Lookup(args.NewActionName()) }; - - // update settings model - _Settings.ActionMap().RegisterKeyBinding(args.NewKeys(), newAction); - - // update view model - auto senderVMImpl{ get_self(senderVM) }; - senderVMImpl->CurrentAction(args.NewActionName()); - senderVMImpl->IsNewlyAdded(false); + flyout.Hide(); + senderVM.AcceptChangesFlyout(nullptr); } + // toggle edit mode + senderVM.ToggleEditMode(); }; - // Check for this special case: - // we're changing the key chord, - // but the new key chord is already in use - bool conflictFound{ false }; - if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey()) + const auto& conflictingCmd{ _Settings.ActionMap().GetActionByKeyChord(newKeys) }; + if (conflictingCmd) { - const auto& conflictingCmd{ _Settings.ActionMap().GetActionByKeyChord(args.NewKeys()) }; - if (conflictingCmd) - { - conflictFound = true; - // We're about to overwrite another key chord. - // Display a confirmation dialog. - TextBlock errorMessageTB{}; - errorMessageTB.Text(RS_(L"Actions_RenameConflictConfirmationMessage")); - errorMessageTB.TextWrapping(TextWrapping::Wrap); + // We're about to overwrite another key chord. + // Display a confirmation dialog. + TextBlock errorMessageTB{}; + errorMessageTB.Text(RS_(L"Actions_RenameConflictConfirmationMessage")); - const auto conflictingCmdName{ conflictingCmd.Name() }; - TextBlock conflictingCommandNameTB{}; - conflictingCommandNameTB.Text(fmt::format(L"\"{}\"", conflictingCmdName.empty() ? RS_(L"Actions_UnnamedCommandName") : conflictingCmdName)); - conflictingCommandNameTB.FontStyle(Windows::UI::Text::FontStyle::Italic); - conflictingCommandNameTB.TextWrapping(TextWrapping::Wrap); + const auto conflictingCmdName{ conflictingCmd.Name() }; + TextBlock conflictingCommandNameTB{}; + conflictingCommandNameTB.Text(fmt::format(L"\"{}\"", conflictingCmdName.empty() ? RS_(L"Actions_UnnamedCommandName") : conflictingCmdName)); + conflictingCommandNameTB.FontStyle(Windows::UI::Text::FontStyle::Italic); - TextBlock confirmationQuestionTB{}; - confirmationQuestionTB.Text(RS_(L"Actions_RenameConflictConfirmationQuestion")); - confirmationQuestionTB.TextWrapping(TextWrapping::Wrap); + TextBlock confirmationQuestionTB{}; + confirmationQuestionTB.Text(RS_(L"Actions_RenameConflictConfirmationQuestion")); - Button acceptBTN{}; - acceptBTN.Content(box_value(RS_(L"Actions_RenameConflictConfirmationAcceptButton"))); - acceptBTN.Click([=](auto&, auto&) { - // remove conflicting key binding from list view - const auto containerIndex{ _GetContainerIndexByKeyChord(args.NewKeys()) }; - _KeyBindingList.RemoveAt(*containerIndex); + Button acceptBTN{}; + acceptBTN.Content(box_value(RS_(L"Actions_RenameConflictConfirmationAcceptButton"))); + acceptBTN.Click([=](auto&, auto&) { + // update settings model and view model + applyChangesToSettingsModel(); + }); - // remove flyout - senderVM.AcceptChangesFlyout().Hide(); - senderVM.AcceptChangesFlyout(nullptr); + StackPanel flyoutStack{}; + flyoutStack.Children().Append(errorMessageTB); + flyoutStack.Children().Append(conflictingCommandNameTB); + flyoutStack.Children().Append(confirmationQuestionTB); + flyoutStack.Children().Append(acceptBTN); - // update settings model and view model - applyChangesToSettingsModel(); - senderVM.ToggleEditMode(); - }); - - StackPanel flyoutStack{}; - flyoutStack.Children().Append(errorMessageTB); - flyoutStack.Children().Append(conflictingCommandNameTB); - flyoutStack.Children().Append(confirmationQuestionTB); - flyoutStack.Children().Append(acceptBTN); - - // This should match CustomFlyoutPresenterStyle in CommonResources.xaml! - // We don't have access to those resources here, so it's easier to just copy them over. - // This allows the flyout text to wrap - Style acceptChangesFlyoutStyle{ winrt::xaml_typename() }; - Setter horizontalScrollModeStyleSetter{ ScrollViewer::HorizontalScrollModeProperty(), box_value(ScrollMode::Disabled) }; - Setter horizontalScrollBarVisibilityStyleSetter{ ScrollViewer::HorizontalScrollBarVisibilityProperty(), box_value(ScrollBarVisibility::Disabled) }; - acceptChangesFlyoutStyle.Setters().Append(horizontalScrollModeStyleSetter); - acceptChangesFlyoutStyle.Setters().Append(horizontalScrollBarVisibilityStyleSetter); - - Flyout acceptChangesFlyout{}; - acceptChangesFlyout.FlyoutPresenterStyle(acceptChangesFlyoutStyle); - acceptChangesFlyout.Content(flyoutStack); - senderVM.AcceptChangesFlyout(acceptChangesFlyout); - } + Flyout acceptChangesFlyout{}; + acceptChangesFlyout.Content(flyoutStack); + senderVM.AcceptChangesFlyout(acceptChangesFlyout); } - - // if there was a conflict, the flyout we created will handle whether changes need to be propagated - // otherwise, go ahead and apply the changes - if (!conflictFound) + else { // update settings model and view model applyChangesToSettingsModel(); - - // We NEED to toggle the edit mode here, - // so that if nothing changed, we still exit - // edit mode. - senderVM.ToggleEditMode(); } } - void ActionsViewModel::_KeyBindingViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& /*args*/) + void ActionsViewModel::AddCopiedCommand(const Model::Command& newCommand) { - for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i) + // The command VM calls this when the user has edited an in-box action + // newCommand is a copy of the in-box action that was edited, but with OriginTag::User + // add it to the action map + _Settings.ActionMap().AddAction(newCommand, nullptr); + } + + void ActionsViewModel::RegenerateCommandID(const Model::Command& command) + { + _Settings.UpdateCommandID(command, {}); + } + + void ActionsViewModel::_CmdVMEditRequestedHandler(const Editor::CommandViewModel& senderVM, const IInspectable& /*args*/) + { + CurrentCommand(senderVM); + CurrentPage(ActionsSubPage::Edit); + } + + void ActionsViewModel::_CmdVMDeleteRequestedHandler(const Editor::CommandViewModel& senderVM, const IInspectable& /*args*/) + { + for (uint32_t i = 0; i < _CommandList.Size(); i++) { - const auto& kbdVM{ _KeyBindingList.GetAt(i) }; - if (kbdVM == senderVM) + if (_CommandList.GetAt(i) == senderVM) { - _KeyBindingList.RemoveAt(i); - return; + CommandList().RemoveAt(i); + break; + } + } + _Settings.ActionMap().DeleteUserCommand(senderVM.ID()); + CurrentCommand(nullptr); + CurrentPage(ActionsSubPage::Base); + } + + void ActionsViewModel::_CmdVMPropagateColorSchemeRequestedHandler(const IInspectable& /*senderVM*/, const Editor::ArgWrapper& wrapper) + { + if (wrapper) + { + const auto schemes = _Settings.GlobalSettings().ColorSchemes(); + const auto defaultAppearanceSchemeName = _Settings.ProfileDefaults().DefaultAppearance().LightColorSchemeName(); + for (const auto [name, scheme] : schemes) + { + if (name == defaultAppearanceSchemeName) + { + const auto schemeVM = winrt::make(scheme, nullptr, _Settings); + wrapper.DefaultColorScheme(schemeVM); + break; + } } } } - // Method Description: - // - performs a search on KeyBindingList by key chord. - // Arguments: - // - keys - the associated key chord of the command we're looking for - // Return Value: - // - the index of the view model referencing the command. If the command doesn't exist, nullopt - std::optional ActionsViewModel::_GetContainerIndexByKeyChord(const Control::KeyChord& keys) + void ActionsViewModel::_CmdVMPropagateColorSchemeNamesRequestedHandler(const IInspectable& /*senderVM*/, const Editor::ArgWrapper& wrapper) { - for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i) + if (wrapper) { - const auto kbdVM{ get_self(_KeyBindingList.GetAt(i)) }; - const auto& otherKeys{ kbdVM->CurrentKeys() }; - if (otherKeys && keys.Modifiers() == otherKeys.Modifiers() && keys.Vkey() == otherKeys.Vkey()) + std::vector namesList; + const auto schemes = _Settings.GlobalSettings().ColorSchemes(); + for (const auto [name, _] : schemes) { - return i; + namesList.emplace_back(name); } + wrapper.ColorSchemeNamesList(winrt::single_threaded_vector(std::move(namesList))); } - - // TODO GH #6900: - // an expedited search can be done if we use cmd.Name() - // to quickly search through the sorted list. - return std::nullopt; } - void ActionsViewModel::_RegisterEvents(com_ptr& kbdVM) + void ActionsViewModel::_RegisterCmdVMEvents(com_ptr& cmdVM) { - kbdVM->PropertyChanged({ this, &ActionsViewModel::_KeyBindingViewModelPropertyChangedHandler }); - kbdVM->DeleteKeyBindingRequested({ this, &ActionsViewModel::_KeyBindingViewModelDeleteKeyBindingHandler }); - kbdVM->ModifyKeyBindingRequested({ this, &ActionsViewModel::_KeyBindingViewModelModifyKeyBindingHandler }); - kbdVM->IsAutomationPeerAttached(_AutomationPeerAttached); + cmdVM->EditRequested({ this, &ActionsViewModel::_CmdVMEditRequestedHandler }); + cmdVM->DeleteRequested({ this, &ActionsViewModel::_CmdVMDeleteRequestedHandler }); + cmdVM->PropagateColorSchemeRequested({ this, &ActionsViewModel::_CmdVMPropagateColorSchemeRequestedHandler }); + cmdVM->PropagateColorSchemeNamesRequested({ this, &ActionsViewModel::_CmdVMPropagateColorSchemeNamesRequestedHandler }); } } diff --git a/src/cascadia/TerminalSettingsEditor/ActionsViewModel.h b/src/cascadia/TerminalSettingsEditor/ActionsViewModel.h index e78cd946fd..4dffee9eb5 100644 --- a/src/cascadia/TerminalSettingsEditor/ActionsViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ActionsViewModel.h @@ -1,130 +1,304 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ActionsViewModel.h + +Abstract: +- This contains the view models for everything related to the Actions pages (Actions.xaml and EditAction.xaml) +- ActionsViewModel: + - Contains the "current page" enum, which dictates whether we're in the top-level Actions page or the EditAction page + - Contains the full command list, and keeps track of the "current command" that is being edited + - These are in the form of CommandViewModel(s) + - Handles modification to the list of commands, i.e. addition/deletion + - Listens to each CommandViewModel for key chord events for addition/modification/deletion of keychords +- CommandViewModel: + - Constructed with a Model::Command object + - View model for each specific command item + - Contains higher-level detail about the command itself such as name, whether it is a user command, and the shortcut action type + - Contains an ActionArgsViewModel, which it creates according to the shortcut action type + - Recreates the ActionArgsViewModel whenever the shortcut action type changes + - Contains the full keybinding list, in the form of KeyChordViewModel(s) +- ActionArgsViewModel: + - Constructed with a Model::ActionAndArgs object + - Contains a vector of ArgWrapper(s), one ArgWrapper for each arg + - Listens and propagates changes to the ArgWrappers +- ArgWrapper: + - Wrapper for each argument + - Handles binding and bind back logic for the presentation and modification of the argument via the UI + - Has separate binding and bind back logic depending on the type of the argument +- KeyChordViewModel: + - Constructed with a Control::KeyChord object + - Handles binding and bind back logic for the presentation and modification of a keybinding via the UI + +--*/ #pragma once #include "ActionsViewModel.g.h" -#include "KeyBindingViewModel.g.h" -#include "ModifyKeyBindingEventArgs.g.h" +#include "NavigateToCommandArgs.g.h" +#include "CommandViewModel.g.h" +#include "ArgWrapper.g.h" +#include "ActionArgsViewModel.g.h" +#include "KeyChordViewModel.g.h" +#include "ModifyKeyChordEventArgs.g.h" #include "Utils.h" #include "ViewModelHelpers.h" namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { - struct KeyBindingViewModelComparator - { - bool operator()(const Editor::KeyBindingViewModel& lhs, const Editor::KeyBindingViewModel& rhs) const - { - return lhs.Name() < rhs.Name(); - } - }; - - struct ModifyKeyBindingEventArgs : ModifyKeyBindingEventArgsT + struct NavigateToCommandArgs : NavigateToCommandArgsT { public: - ModifyKeyBindingEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys, const hstring oldActionName, const hstring newActionName) : + NavigateToCommandArgs(CommandViewModel command, Editor::IHostedInWindow windowRoot) : + _Command(command), + _WindowRoot(windowRoot) {} + + Editor::IHostedInWindow WindowRoot() const noexcept { return _WindowRoot; } + Editor::CommandViewModel Command() const noexcept { return _Command; } + + private: + Editor::IHostedInWindow _WindowRoot; + Editor::CommandViewModel _Command{ nullptr }; + }; + + struct ModifyKeyChordEventArgs : ModifyKeyChordEventArgsT + { + public: + ModifyKeyChordEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys) : _OldKeys{ oldKeys }, - _NewKeys{ newKeys }, - _OldActionName{ std::move(oldActionName) }, - _NewActionName{ std::move(newActionName) } {} + _NewKeys{ newKeys } {} WINRT_PROPERTY(Control::KeyChord, OldKeys, nullptr); WINRT_PROPERTY(Control::KeyChord, NewKeys, nullptr); - WINRT_PROPERTY(hstring, OldActionName); - WINRT_PROPERTY(hstring, NewActionName); }; - struct KeyBindingViewModel : KeyBindingViewModelT, ViewModelHelper + struct CommandViewModel : CommandViewModelT, ViewModelHelper { public: - KeyBindingViewModel(const Windows::Foundation::Collections::IObservableVector& availableActions); - KeyBindingViewModel(const Control::KeyChord& keys, const hstring& name, const Windows::Foundation::Collections::IObservableVector& availableActions); + CommandViewModel(const winrt::Microsoft::Terminal::Settings::Model::Command& cmd, + std::vector keyChordList, + const Editor::ActionsViewModel& actionsPageVM, + Windows::Foundation::Collections::IMap availableActionsAndNamesMap, + Windows::Foundation::Collections::IMap nameToActionMap); + void Initialize(); - hstring Name() const { return _CurrentAction; } - hstring KeyChordText() const { return _KeyChordText; } + winrt::hstring DisplayName(); + winrt::hstring Name(); + void Name(const winrt::hstring& newName); + winrt::hstring DisplayNameAndKeyChordAutomationPropName(); + + winrt::hstring FirstKeyChordText(); + + winrt::hstring ID(); + bool IsUserAction(); + + void Edit_Click(); + til::typed_event EditRequested; + + void Delete_Click(); + til::typed_event DeleteRequested; + + void AddKeybinding_Click(); + + // UIA text + winrt::hstring ActionNameTextBoxAutomationPropName(); + winrt::hstring ShortcutActionComboBoxAutomationPropName(); + winrt::hstring AdditionalArgumentsControlAutomationPropName(); + + til::typed_event PropagateColorSchemeRequested; + til::typed_event PropagateColorSchemeNamesRequested; + til::typed_event PropagateWindowRootRequested; + til::typed_event FocusContainer; + + VIEW_MODEL_OBSERVABLE_PROPERTY(IInspectable, ProposedShortcutActionName); + VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::ActionArgsViewModel, ActionArgsVM, nullptr); + WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, AvailableShortcutActions, nullptr); + WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, KeyChordList, nullptr); + WINRT_PROPERTY(bool, IsNewCommand, false); + + private: + winrt::hstring _cachedDisplayName; + winrt::Microsoft::Terminal::Settings::Model::Command _command; + std::vector _keyChordList; + weak_ref _actionsPageVM{ nullptr }; + Windows::Foundation::Collections::IMap _availableActionsAndNamesMap; + Windows::Foundation::Collections::IMap _nameToActionMap; + void _RegisterKeyChordVMEvents(Editor::KeyChordViewModel kcVM); + void _RegisterActionArgsVMEvents(Editor::ActionArgsViewModel actionArgsVM); + void _ReplaceCommandWithUserCopy(bool reinitialize); + void _CreateAndInitializeActionArgsVMHelper(); + }; + + struct ArgWrapper : ArgWrapperT, ViewModelHelper + { + public: + ArgWrapper(const Model::ArgDescriptor& descriptor, const Windows::Foundation::IInspectable& value); + void Initialize(); + + winrt::hstring Name() const noexcept { return _descriptor.Name; }; + winrt::hstring Type() const noexcept { return _descriptor.Type; }; + Model::ArgTypeHint TypeHint() const noexcept { return _descriptor.TypeHint; }; + bool Required() const noexcept { return _descriptor.Required; }; + + // We cannot use the macro here because we need to implement additional logic for the setter + Windows::Foundation::IInspectable EnumValue() const noexcept { return _EnumValue; }; + void EnumValue(const Windows::Foundation::IInspectable& value); + Windows::Foundation::Collections::IObservableVector EnumList() const noexcept { return _EnumList; }; + Windows::Foundation::Collections::IObservableVector FlagList() const noexcept { return _FlagList; }; + + // unboxing functions + winrt::hstring UnboxString(const Windows::Foundation::IInspectable& value); + int32_t UnboxInt32(const Windows::Foundation::IInspectable& value); + float UnboxInt32Optional(const Windows::Foundation::IInspectable& value); + uint32_t UnboxUInt32(const Windows::Foundation::IInspectable& value); + float UnboxUInt32Optional(const Windows::Foundation::IInspectable& value); + float UnboxFloat(const Windows::Foundation::IInspectable& value); + bool UnboxBool(const Windows::Foundation::IInspectable& value); + winrt::Windows::Foundation::IReference UnboxBoolOptional(const Windows::Foundation::IInspectable& value); + winrt::Windows::Foundation::IReference UnboxTerminalCoreColorOptional(const Windows::Foundation::IInspectable& value); + winrt::Windows::Foundation::IReference UnboxWindowsUIColorOptional(const Windows::Foundation::IInspectable& value); + + // bind back functions + void StringBindBack(const winrt::hstring& newValue); + void Int32BindBack(const double newValue); + void Int32OptionalBindBack(const double newValue); + void UInt32BindBack(const double newValue); + void UInt32OptionalBindBack(const double newValue); + void FloatBindBack(const double newValue); + void BoolOptionalBindBack(const Windows::Foundation::IReference newValue); + void TerminalCoreColorBindBack(const winrt::Windows::Foundation::IReference newValue); + void WindowsUIColorBindBack(const winrt::Windows::Foundation::IReference newValue); + + safe_void_coroutine BrowseForFile_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); + safe_void_coroutine BrowseForFolder_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); + + // some argWrappers need to know additional information (like the default color scheme or the list of all color scheme names) + // to avoid populating all ArgWrappers with that information, instead we emit an event when we need that information + // (these events then get propagated up to the ActionsVM) and then the actionsVM will populate the value in us + // since there's an actionArgsVM above us and a commandVM above that, the event does get propagated through a few times but that's + // probably better than having every argWrapper contain the information by default + til::typed_event ColorSchemeRequested; + til::typed_event ColorSchemeNamesRequested; + til::typed_event WindowRootRequested; + + VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::ColorSchemeViewModel, DefaultColorScheme, nullptr); + VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::Foundation::IInspectable, Value, nullptr); + WINRT_PROPERTY(Windows::Foundation::Collections::IVector, ColorSchemeNamesList, nullptr); + WINRT_PROPERTY(Editor::IHostedInWindow, WindowRoot, nullptr); + + private: + Model::ArgDescriptor _descriptor; + Windows::Foundation::IInspectable _EnumValue{ nullptr }; + Windows::Foundation::Collections::IObservableVector _EnumList; + Windows::Foundation::Collections::IObservableVector _FlagList; + + template + void _InitializeEnumListAndValue( + const winrt::Windows::Foundation::Collections::IMapView& mappings, + const winrt::hstring& resourceSectionAndType, + const winrt::hstring& resourceProperty, + const bool nullable); + + template + void _InitializeFlagListAndValue( + const winrt::Windows::Foundation::Collections::IMapView& mappings, + const winrt::hstring& resourceSectionAndType, + const winrt::hstring& resourceProperty, + const bool nullable); + }; + + struct ActionArgsViewModel : ActionArgsViewModelT, ViewModelHelper + { + public: + ActionArgsViewModel(const Microsoft::Terminal::Settings::Model::ActionAndArgs actionAndArgs); + void Initialize(); + + bool HasArgs() const noexcept; + void ReplaceActionAndArgs(Model::ActionAndArgs newActionAndArgs); + + til::typed_event WrapperValueChanged; + til::typed_event PropagateColorSchemeRequested; + til::typed_event PropagateColorSchemeNamesRequested; + til::typed_event PropagateWindowRootRequested; + + WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, ArgValues, nullptr); + + private: + Model::ActionAndArgs _actionAndArgs{ nullptr }; + }; + + struct KeyChordViewModel : KeyChordViewModelT, ViewModelHelper + { + public: + KeyChordViewModel(Control::KeyChord CurrentKeys); + + void CurrentKeys(const Control::KeyChord& newKeys); + Control::KeyChord CurrentKeys() const noexcept; + + void ToggleEditMode(); + void AcceptChanges(); + void CancelChanges(); + void DeleteKeyChord(); // UIA Text - hstring EditButtonName() const noexcept; hstring CancelButtonName() const noexcept; hstring AcceptButtonName() const noexcept; hstring DeleteButtonName() const noexcept; - void EnterHoverMode() { IsHovered(true); }; - void ExitHoverMode() { IsHovered(false); }; - void ActionGotFocus() { IsContainerFocused(true); }; - void ActionLostFocus() { IsContainerFocused(false); }; - void EditButtonGettingFocus() { IsEditButtonFocused(true); }; - void EditButtonLosingFocus() { IsEditButtonFocused(false); }; - bool ShowEditButton() const noexcept; - void ToggleEditMode(); - void DisableEditMode() { IsInEditMode(false); } - void AttemptAcceptChanges(); - void AttemptAcceptChanges(const Control::KeyChord newKeys); - void CancelChanges(); - void DeleteKeyBinding() { DeleteKeyBindingRequested.raise(*this, _CurrentKeys); } - - // ProposedAction: the entry selected by the combo box; may disagree with the settings model. - // CurrentAction: the combo box item that maps to the settings model value. - // AvailableActions: the list of options in the combo box; both actions above must be in this list. - // NOTE: ProposedAction and CurrentAction may disagree mainly due to the "edit mode" system in place. - // Current Action serves as... - // 1 - a record of what to set ProposedAction to on a cancellation - // 2 - a form of translation between ProposedAction and the settings model - // We would also need an ActionMap reference to remove this, but this is a better separation - // of responsibilities. - VIEW_MODEL_OBSERVABLE_PROPERTY(IInspectable, ProposedAction); - VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentAction); - WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, AvailableActions, nullptr); - - // ProposedKeys: the keys proposed by the control; may disagree with the settings model. - // CurrentKeys: the key chord bound in the settings model. - VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, ProposedKeys); - VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, CurrentKeys, nullptr); - VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false); - VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsNewlyAdded, false); + VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, ProposedKeys); + VIEW_MODEL_OBSERVABLE_PROPERTY(winrt::hstring, KeyChordText); VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Controls::Flyout, AcceptChangesFlyout, nullptr); - VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsAutomationPeerAttached, false); - VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsHovered, false); - VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsContainerFocused, false); - VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsEditButtonFocused, false); - VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Media::Brush, ContainerBackground, nullptr); public: - til::typed_event ModifyKeyBindingRequested; - til::typed_event DeleteKeyBindingRequested; - til::typed_event DeleteNewlyAddedKeyBinding; + til::typed_event AddKeyChordRequested; + til::typed_event ModifyKeyChordRequested; + til::typed_event DeleteKeyChordRequested; private: - hstring _KeyChordText{}; + Control::KeyChord _currentKeys; }; struct ActionsViewModel : ActionsViewModelT, ViewModelHelper { public: ActionsViewModel(Model::CascadiaSettings settings); + void UpdateSettings(const Model::CascadiaSettings& settings); + void MarkAsVisited(); + bool DisplayBadge() const noexcept; - void OnAutomationPeerAttached(); - void AddNewKeybinding(); + void AddNewCommand(); - til::typed_event FocusContainer; - til::typed_event UpdateBackground; + void CurrentCommand(const Editor::CommandViewModel& newCommand); + Editor::CommandViewModel CurrentCommand(); + void CmdListItemClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::ItemClickEventArgs& e); - WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, KeyBindingList); + void DeleteKeyChord(const Control::KeyChord& keys); + void AttemptAddOrModifyKeyChord(const Editor::KeyChordViewModel& senderVM, winrt::hstring commandID, const Control::KeyChord& newKeys, const Control::KeyChord& oldKeys); + void AddCopiedCommand(const Model::Command& newCommand); + void RegenerateCommandID(const Model::Command& command); + + Windows::Foundation::Collections::IMap AvailableShortcutActionsAndNames(); + Windows::Foundation::Collections::IMap NameToActionMap(); + + WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, CommandList); + WINRT_OBSERVABLE_PROPERTY(ActionsSubPage, CurrentPage, _propertyChangedHandlers, ActionsSubPage::Base); private: - bool _AutomationPeerAttached{ false }; + Editor::CommandViewModel _CurrentCommand{ nullptr }; Model::CascadiaSettings _Settings; - Windows::Foundation::Collections::IObservableVector _AvailableActionAndArgs; - Windows::Foundation::Collections::IMap _AvailableActionMap; + Windows::Foundation::Collections::IMap _AvailableActionsAndNamesMap; + Windows::Foundation::Collections::IMap _NameToActionMap; - std::optional _GetContainerIndexByKeyChord(const Control::KeyChord& keys); - void _RegisterEvents(com_ptr& kbdVM); + void _MakeCommandVMsHelper(); + void _RegisterCmdVMEvents(com_ptr& cmdVM); - void _KeyBindingViewModelPropertyChangedHandler(const Windows::Foundation::IInspectable& senderVM, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); - void _KeyBindingViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& args); - void _KeyBindingViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args); - void _KeyBindingViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& args); + void _CmdVMEditRequestedHandler(const Editor::CommandViewModel& senderVM, const IInspectable& args); + void _CmdVMDeleteRequestedHandler(const Editor::CommandViewModel& senderVM, const IInspectable& args); + void _CmdVMPropagateColorSchemeRequestedHandler(const IInspectable& sender, const Editor::ArgWrapper& wrapper); + void _CmdVMPropagateColorSchemeNamesRequestedHandler(const IInspectable& sender, const Editor::ArgWrapper& wrapper); }; } diff --git a/src/cascadia/TerminalSettingsEditor/ActionsViewModel.idl b/src/cascadia/TerminalSettingsEditor/ActionsViewModel.idl index f091149d77..f2bb6fd848 100644 --- a/src/cascadia/TerminalSettingsEditor/ActionsViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/ActionsViewModel.idl @@ -1,60 +1,186 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import "EnumEntry.idl"; +import "ColorSchemeViewModel.idl"; +import "MainPage.idl"; + namespace Microsoft.Terminal.Settings.Editor { - runtimeclass ModifyKeyBindingEventArgs + [default_interface] runtimeclass ArgsTemplateSelectors : Windows.UI.Xaml.Controls.DataTemplateSelector + { + ArgsTemplateSelectors(); + + Windows.UI.Xaml.DataTemplate Int32Template; + Windows.UI.Xaml.DataTemplate Int32OptionalTemplate; + Windows.UI.Xaml.DataTemplate UInt32Template; + Windows.UI.Xaml.DataTemplate UInt32OptionalTemplate; + Windows.UI.Xaml.DataTemplate FloatTemplate; + Windows.UI.Xaml.DataTemplate SplitSizeTemplate; + Windows.UI.Xaml.DataTemplate StringTemplate; + Windows.UI.Xaml.DataTemplate ColorSchemeTemplate; + Windows.UI.Xaml.DataTemplate FilePickerTemplate; + Windows.UI.Xaml.DataTemplate FolderPickerTemplate; + Windows.UI.Xaml.DataTemplate BoolTemplate; + Windows.UI.Xaml.DataTemplate BoolOptionalTemplate; + Windows.UI.Xaml.DataTemplate EnumTemplate; + Windows.UI.Xaml.DataTemplate FlagTemplate; + Windows.UI.Xaml.DataTemplate TerminalCoreColorOptionalTemplate; + Windows.UI.Xaml.DataTemplate WindowsUIColorOptionalTemplate; + } + + [default_interface] runtimeclass EditAction : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged + { + EditAction(); + CommandViewModel ViewModel { get; }; + } + + runtimeclass NavigateToCommandArgs + { + CommandViewModel Command { get; }; + IHostedInWindow WindowRoot { get; }; + } + + runtimeclass ModifyKeyChordEventArgs { Microsoft.Terminal.Control.KeyChord OldKeys { get; }; Microsoft.Terminal.Control.KeyChord NewKeys { get; }; - String OldActionName { get; }; - String NewActionName { get; }; } - runtimeclass KeyBindingViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged + runtimeclass CommandViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged { // Settings Model side + String Name; + String ID { get; }; + Boolean IsUserAction { get; }; + // keybindings + IObservableVector KeyChordList { get; }; + // action args + ActionArgsViewModel ActionArgsVM { get; }; + + // View-model specific + String DisplayName { get; }; + String FirstKeyChordText { get; }; + String DisplayNameAndKeyChordAutomationPropName { get; }; + + // UI side (command list page) + void Edit_Click(); + + // UI side (edit command page) + IObservableVector AvailableShortcutActions { get; }; + Object ProposedShortcutActionName; + void Delete_Click(); + void AddKeybinding_Click(); + event Windows.Foundation.TypedEventHandler PropagateColorSchemeRequested; + event Windows.Foundation.TypedEventHandler PropagateColorSchemeNamesRequested; + event Windows.Foundation.TypedEventHandler PropagateWindowRootRequested; + event Windows.Foundation.TypedEventHandler FocusContainer; + + // UI side (edit command page, automation property names) + String ActionNameTextBoxAutomationPropName { get; }; + String ShortcutActionComboBoxAutomationPropName { get; }; + String AdditionalArgumentsControlAutomationPropName { get; }; + } + + runtimeclass ArgWrapper : Windows.UI.Xaml.Data.INotifyPropertyChanged + { String Name { get; }; + String Type { get; }; + Microsoft.Terminal.Settings.Model.ArgTypeHint TypeHint { get; }; + Boolean Required { get; }; + IInspectable Value; + IInspectable EnumValue; + Windows.Foundation.Collections.IObservableVector EnumList { get; }; + Windows.Foundation.Collections.IObservableVector FlagList { get; }; + ColorSchemeViewModel DefaultColorScheme; + Windows.Foundation.Collections.IVector ColorSchemeNamesList; + IHostedInWindow WindowRoot; + + // unboxing functions + String UnboxString(Object value); + UInt32 UnboxInt32(Object value); + Single UnboxInt32Optional(Object value); + UInt32 UnboxUInt32(Object value); + Single UnboxUInt32Optional(Object value); + Single UnboxFloat(Object value); + Boolean UnboxBool(Object value); + Windows.Foundation.IReference UnboxBoolOptional(Object value); + Windows.Foundation.IReference UnboxTerminalCoreColorOptional(Object value); + Windows.Foundation.IReference UnboxWindowsUIColorOptional(Object value); + + // bind back functions + void StringBindBack(String newValue); + void Int32BindBack(Double newValue); + void Int32OptionalBindBack(Double newValue); + void UInt32BindBack(Double newValue); + void UInt32OptionalBindBack(Double newValue); + void FloatBindBack(Double newValue); + void BoolOptionalBindBack(Windows.Foundation.IReference newValue); + void TerminalCoreColorBindBack(Windows.Foundation.IReference newValue); + void WindowsUIColorBindBack(Windows.Foundation.IReference newValue); + + void BrowseForFile_Click(IInspectable sender, Windows.UI.Xaml.RoutedEventArgs args); + void BrowseForFolder_Click(IInspectable sender, Windows.UI.Xaml.RoutedEventArgs args); + + event Windows.Foundation.TypedEventHandler ColorSchemeRequested; + event Windows.Foundation.TypedEventHandler ColorSchemeNamesRequested; + event Windows.Foundation.TypedEventHandler WindowRootRequested; + } + + runtimeclass ActionArgsViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged + { + Boolean HasArgs { get; }; + IObservableVector ArgValues; + event Windows.Foundation.TypedEventHandler WrapperValueChanged; + event Windows.Foundation.TypedEventHandler PropagateColorSchemeRequested; + event Windows.Foundation.TypedEventHandler PropagateColorSchemeNamesRequested; + event Windows.Foundation.TypedEventHandler PropagateWindowRootRequested; + } + + runtimeclass KeyChordViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged + { String KeyChordText { get; }; // UI side - Boolean ShowEditButton { get; }; - Boolean IsInEditMode { get; }; - Boolean IsNewlyAdded { get; }; Microsoft.Terminal.Control.KeyChord ProposedKeys; - Object ProposedAction; Windows.UI.Xaml.Controls.Flyout AcceptChangesFlyout; - String EditButtonName { get; }; + Boolean IsInEditMode { get; }; + void ToggleEditMode(); + void AcceptChanges(); + void CancelChanges(); + void DeleteKeyChord(); String CancelButtonName { get; }; String AcceptButtonName { get; }; String DeleteButtonName { get; }; - Windows.UI.Xaml.Media.Brush ContainerBackground { get; }; - void EnterHoverMode(); - void ExitHoverMode(); - void ActionGotFocus(); - void ActionLostFocus(); - void EditButtonGettingFocus(); - void EditButtonLosingFocus(); - IObservableVector AvailableActions { get; }; - void ToggleEditMode(); - void AttemptAcceptChanges(); - void CancelChanges(); - void DeleteKeyBinding(); - - event Windows.Foundation.TypedEventHandler ModifyKeyBindingRequested; - event Windows.Foundation.TypedEventHandler DeleteKeyBindingRequested; + event Windows.Foundation.TypedEventHandler AddKeyChordRequested; + event Windows.Foundation.TypedEventHandler ModifyKeyChordRequested; + event Windows.Foundation.TypedEventHandler DeleteKeyChordRequested; } + enum ActionsSubPage + { + Base = 0, + Edit = 1 + }; + runtimeclass ActionsViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged { ActionsViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings); + void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings); - void OnAutomationPeerAttached(); - void AddNewKeybinding(); + void AddNewCommand(); - IObservableVector KeyBindingList { get; }; - event Windows.Foundation.TypedEventHandler FocusContainer; - event Windows.Foundation.TypedEventHandler UpdateBackground; + ActionsSubPage CurrentPage; + Boolean DisplayBadge { get; }; + + void AttemptAddOrModifyKeyChord(KeyChordViewModel senderVM, String commandID, Microsoft.Terminal.Control.KeyChord newKeys, Microsoft.Terminal.Control.KeyChord oldKeys); + void DeleteKeyChord(Microsoft.Terminal.Control.KeyChord keys); + void AddCopiedCommand(Microsoft.Terminal.Settings.Model.Command newCommand); + void RegenerateCommandID(Microsoft.Terminal.Settings.Model.Command command); + + CommandViewModel CurrentCommand; + IObservableVector CommandList { get; }; + void CmdListItemClicked(IInspectable sender, Windows.UI.Xaml.Controls.ItemClickEventArgs args); } } diff --git a/src/cascadia/TerminalSettingsEditor/ArgsTemplateSelectors.cpp b/src/cascadia/TerminalSettingsEditor/ArgsTemplateSelectors.cpp new file mode 100644 index 0000000000..f61d4d320c --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/ArgsTemplateSelectors.cpp @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ArgsTemplateSelectors.h" +#include "ArgsTemplateSelectors.g.cpp" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + // Method Description: + // - This method is called once command palette decides how to render a filtered command. + // Currently we support two ways to render command, that depend on its palette item type: + // - For TabPalette item we render an icon, a title, and some tab-related indicators like progress bar (as defined by TabItemTemplate) + // - All other items are currently rendered with icon, title and optional key-chord (as defined by GeneralItemTemplate) + // Arguments: + // - item - an instance of filtered command to render + // Return Value: + // - data template to use for rendering + Windows::UI::Xaml::DataTemplate ArgsTemplateSelectors::SelectTemplateCore(const winrt::Windows::Foundation::IInspectable& item, const winrt::Windows::UI::Xaml::DependencyObject& /*container*/) + { + static constexpr std::pair< + std::wstring_view, + til::property ArgsTemplateSelectors::*> + lut[] = { + { L"int32_t", &ArgsTemplateSelectors::Int32Template }, + { L"uint32_t", &ArgsTemplateSelectors::UInt32Template }, + { L"bool", &ArgsTemplateSelectors::BoolTemplate }, + { L"Windows::Foundation::IReference", &ArgsTemplateSelectors::BoolOptionalTemplate }, + { L"Windows::Foundation::IReference", &ArgsTemplateSelectors::Int32OptionalTemplate }, + { L"Windows::Foundation::IReference", &ArgsTemplateSelectors::UInt32OptionalTemplate }, + { L"SuggestionsSource", &ArgsTemplateSelectors::FlagTemplate }, + { L"Windows::Foundation::IReference", &ArgsTemplateSelectors::FlagTemplate }, + { L"Windows::Foundation::IReference", &ArgsTemplateSelectors::TerminalCoreColorOptionalTemplate }, + { L"Windows::Foundation::IReference", &ArgsTemplateSelectors::WindowsUIColorOptionalTemplate }, + { L"Model::ResizeDirection", &ArgsTemplateSelectors::EnumTemplate }, + { L"Model::FocusDirection", &ArgsTemplateSelectors::EnumTemplate }, + { L"SettingsTarget", &ArgsTemplateSelectors::EnumTemplate }, + { L"MoveTabDirection", &ArgsTemplateSelectors::EnumTemplate }, + { L"Microsoft::Terminal::Control::ScrollToMarkDirection", &ArgsTemplateSelectors::EnumTemplate }, + { L"CommandPaletteLaunchMode", &ArgsTemplateSelectors::EnumTemplate }, + { L"FindMatchDirection", &ArgsTemplateSelectors::EnumTemplate }, + { L"Model::DesktopBehavior", &ArgsTemplateSelectors::EnumTemplate }, + { L"Model::MonitorBehavior", &ArgsTemplateSelectors::EnumTemplate }, + { L"winrt::Microsoft::Terminal::Control::ClearBufferType", &ArgsTemplateSelectors::EnumTemplate }, + { L"SelectOutputDirection", &ArgsTemplateSelectors::EnumTemplate }, + { L"Windows::Foundation::IReference", &ArgsTemplateSelectors::EnumTemplate }, + { L"Model::SplitDirection", &ArgsTemplateSelectors::EnumTemplate }, + { L"SplitType", &ArgsTemplateSelectors::EnumTemplate }, + }; + + if (const auto argWrapper{ item.try_as() }) + { + const auto argType = argWrapper.Type(); + if (argType == L"winrt::hstring") + { + // string has some special cases - check the tag + const auto argTag = argWrapper.TypeHint(); + switch (argTag) + { + case Model::ArgTypeHint::ColorScheme: + return ColorSchemeTemplate(); + case Model::ArgTypeHint::FilePath: + return FilePickerTemplate(); + case Model::ArgTypeHint::FolderPath: + return FolderPickerTemplate(); + default: + // no special handling required, just return the normal string template + return StringTemplate(); + } + } + else if (argType == L"float") + { + const auto argTag = argWrapper.TypeHint(); + switch (argTag) + { + case Model::ArgTypeHint::SplitSize: + return SplitSizeTemplate(); + default: + return FloatTemplate(); + } + } + + for (const auto& [type, member] : lut) + { + if (type == argType) + { + return (this->*member)(); + } + } + } + return nullptr; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/ArgsTemplateSelectors.h b/src/cascadia/TerminalSettingsEditor/ArgsTemplateSelectors.h new file mode 100644 index 0000000000..24544dcaec --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/ArgsTemplateSelectors.h @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "ArgsTemplateSelectors.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct ArgsTemplateSelectors : ArgsTemplateSelectorsT + { + ArgsTemplateSelectors() = default; + + Windows::UI::Xaml::DataTemplate SelectTemplateCore(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::UI::Xaml::DependencyObject& = nullptr); + + til::property Int32Template; + til::property Int32OptionalTemplate; + til::property UInt32Template; + til::property UInt32OptionalTemplate; + til::property FloatTemplate; + til::property SplitSizeTemplate; + til::property StringTemplate; + til::property ColorSchemeTemplate; + til::property FilePickerTemplate; + til::property FolderPickerTemplate; + til::property BoolTemplate; + til::property BoolOptionalTemplate; + til::property EnumTemplate; + til::property FlagTemplate; + til::property TerminalCoreColorOptionalTemplate; + til::property WindowsUIColorOptionalTemplate; + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + BASIC_FACTORY(ArgsTemplateSelectors); +} diff --git a/src/cascadia/TerminalSettingsEditor/EditAction.cpp b/src/cascadia/TerminalSettingsEditor/EditAction.cpp new file mode 100644 index 0000000000..9542d39db1 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/EditAction.cpp @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "EditAction.h" +#include "EditAction.g.cpp" +#include "LibraryResources.h" +#include "../TerminalSettingsModel/AllShortcutActions.h" + +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Navigation; + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + EditAction::EditAction() + { + } + + void EditAction::OnNavigatedTo(const NavigationEventArgs& e) + { + const auto args = e.Parameter().as(); + _ViewModel = args.Command(); + _propagateWindowRootRevoker = _ViewModel.PropagateWindowRootRequested( + winrt::auto_revoke, + [windowRoot = args.WindowRoot()](const IInspectable&, const Editor::ArgWrapper& wrapper) { + if (wrapper) + { + wrapper.WindowRoot(windowRoot); + } + }); + auto weakThis = get_weak(); + _focusContainerRevoker = _ViewModel.FocusContainer( + winrt::auto_revoke, + [weakThis](const auto&, const auto& args) { + if (auto page{ weakThis.get() }) + { + if (auto kcVM{ args.try_as() }) + { + if (const auto& container = page->KeyChordListView().ContainerFromItem(*kcVM)) + { + container.as().Focus(FocusState::Programmatic); + } + } + } + }); + _layoutUpdatedRevoker = LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { + // Only let this succeed once. + _layoutUpdatedRevoker.revoke(); + + CommandNameTextBox().Focus(FocusState::Programmatic); + }); + } +} diff --git a/src/cascadia/TerminalSettingsEditor/EditAction.h b/src/cascadia/TerminalSettingsEditor/EditAction.h new file mode 100644 index 0000000000..767d36c5ad --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/EditAction.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "EditAction.g.h" +#include "ActionsViewModel.h" +#include "Utils.h" +#include "ViewModelHelpers.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct EditAction : public HasScrollViewer, EditActionT + { + public: + EditAction(); + void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); + + til::property_changed_event PropertyChanged; + + WINRT_OBSERVABLE_PROPERTY(Editor::CommandViewModel, ViewModel, PropertyChanged.raise, nullptr); + + private: + friend struct EditActionT; // for Xaml to bind events + winrt::Windows::UI::Xaml::FrameworkElement::LayoutUpdated_revoker _layoutUpdatedRevoker; + Editor::CommandViewModel::PropagateWindowRootRequested_revoker _propagateWindowRootRevoker; + Editor::CommandViewModel::FocusContainer_revoker _focusContainerRevoker; + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + BASIC_FACTORY(EditAction); +} diff --git a/src/cascadia/TerminalSettingsEditor/EditAction.xaml b/src/cascadia/TerminalSettingsEditor/EditAction.xaml new file mode 100644 index 0000000000..caa1c9d8cc --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/EditAction.xaml @@ -0,0 +1,740 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 32 + 14 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/EnumEntry.h b/src/cascadia/TerminalSettingsEditor/EnumEntry.h index fe45a58396..3c3588a0db 100644 --- a/src/cascadia/TerminalSettingsEditor/EnumEntry.h +++ b/src/cascadia/TerminalSettingsEditor/EnumEntry.h @@ -17,6 +17,7 @@ Author(s): #pragma once #include "EnumEntry.g.h" +#include "FlagEntry.g.h" #include "Utils.h" namespace winrt::Microsoft::Terminal::Settings::Editor::implementation @@ -26,7 +27,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { bool operator()(const Editor::EnumEntry& lhs, const Editor::EnumEntry& rhs) const { - return lhs.EnumValue().as() < rhs.EnumValue().as(); + return lhs.IntValue() < rhs.IntValue(); } }; @@ -35,7 +36,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { bool operator()(const Editor::EnumEntry& lhs, const Editor::EnumEntry& rhs) const { - return lhs.EnumValue().as() > rhs.EnumValue().as(); + return lhs.IntValue() > rhs.IntValue(); } }; @@ -46,6 +47,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _EnumName{ enumName }, _EnumValue{ enumValue } {} + EnumEntry(const winrt::hstring enumName, const winrt::Windows::Foundation::IInspectable& enumValue, const int32_t intValue) : + _EnumName{ enumName }, + _EnumValue{ enumValue }, + _IntValue{ intValue } {} + hstring ToString() { return EnumName(); @@ -54,5 +60,50 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation til::property_changed_event PropertyChanged; WINRT_OBSERVABLE_PROPERTY(winrt::hstring, EnumName, PropertyChanged.raise); WINRT_OBSERVABLE_PROPERTY(winrt::Windows::Foundation::IInspectable, EnumValue, PropertyChanged.raise); + WINRT_PROPERTY(int32_t, IntValue, 0); + }; + + template + struct FlagEntryComparator + { + bool operator()(const Editor::FlagEntry& lhs, const Editor::FlagEntry& rhs) const + { + return lhs.FlagValue().as() < rhs.FlagValue().as(); + } + }; + + template + struct FlagEntryReverseComparator + { + bool operator()(const Editor::FlagEntry& lhs, const Editor::FlagEntry& rhs) const + { + return lhs.FlagValue().as() > rhs.FlagValue().as(); + } + }; + + struct FlagEntry : FlagEntryT + { + public: + FlagEntry(const winrt::hstring flagName, const winrt::Windows::Foundation::IInspectable& flagValue, const bool isSet) : + _FlagName{ flagName }, + _FlagValue{ flagValue }, + _IsSet{ isSet } {} + + FlagEntry(const winrt::hstring flagName, const winrt::Windows::Foundation::IInspectable& flagValue, const bool isSet, const int32_t intValue) : + _FlagName{ flagName }, + _FlagValue{ flagValue }, + _IsSet{ isSet }, + _IntValue{ intValue } {} + + hstring ToString() + { + return FlagName(); + } + + til::property_changed_event PropertyChanged; + WINRT_OBSERVABLE_PROPERTY(winrt::hstring, FlagName, PropertyChanged.raise); + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::Foundation::IInspectable, FlagValue, PropertyChanged.raise); + WINRT_OBSERVABLE_PROPERTY(bool, IsSet, PropertyChanged.raise); + WINRT_PROPERTY(int32_t, IntValue, 0); }; } diff --git a/src/cascadia/TerminalSettingsEditor/EnumEntry.idl b/src/cascadia/TerminalSettingsEditor/EnumEntry.idl index ce92d75762..87139c8a3c 100644 --- a/src/cascadia/TerminalSettingsEditor/EnumEntry.idl +++ b/src/cascadia/TerminalSettingsEditor/EnumEntry.idl @@ -7,5 +7,14 @@ namespace Microsoft.Terminal.Settings.Editor { String EnumName { get; }; IInspectable EnumValue { get; }; + Int32 IntValue { get; }; + } + + [default_interface] runtimeclass FlagEntry : Windows.UI.Xaml.Data.INotifyPropertyChanged, Windows.Foundation.IStringable + { + String FlagName { get; }; + IInspectable FlagValue { get; }; + Int32 IntValue { get; }; + Boolean IsSet; } } diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 6906477044..bebbbf57b7 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -113,6 +113,24 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } }); + _actionsVM = winrt::make(_settingsClone); + _actionsViewModelChangedRevoker = _actionsVM.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) { + const auto settingName{ args.PropertyName() }; + if (settingName == L"CurrentPage") + { + if (_actionsVM.CurrentPage() == ActionsSubPage::Edit) + { + contentFrame().Navigate(xaml_typename(), winrt::make(_actionsVM.CurrentCommand(), *this)); + const auto crumb = winrt::make(box_value(actionsTag), RS_(L"Nav_EditAction/Content"), BreadcrumbSubPage::Actions_Edit); + _breadcrumbs.Append(crumb); + } + else if (_actionsVM.CurrentPage() == ActionsSubPage::Base) + { + _Navigate(winrt::hstring{ actionsTag }, BreadcrumbSubPage::None); + } + } + }); + auto extensionsVMImpl = winrt::make_self(_settingsClone, _colorSchemesPageVM); extensionsVMImpl->NavigateToProfileRequested({ this, &MainPage::_NavigateToProfileHandler }); extensionsVMImpl->NavigateToColorSchemeRequested({ this, &MainPage::_NavigateToColorSchemeHandler }); @@ -189,6 +207,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _InitializeProfilesList(); // Update the Nav State with the new version of the settings _colorSchemesPageVM.UpdateSettings(_settingsClone); + _actionsVM.UpdateSettings(_settingsClone); _newTabMenuPageVM.UpdateSettings(_settingsClone); _extensionsVM.UpdateSettings(_settingsClone, _colorSchemesPageVM); @@ -486,9 +505,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } else if (clickedItemTag == actionsTag) { - contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Actions/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); + contentFrame().Navigate(xaml_typename(), _actionsVM); + + if (subPage == BreadcrumbSubPage::Actions_Edit && _actionsVM.CurrentCommand() != nullptr) + { + _actionsVM.CurrentPage(ActionsSubPage::Edit); + } } else if (clickedItemTag == newTabMenuTag) { diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.h b/src/cascadia/TerminalSettingsEditor/MainPage.h index ff87f4910b..7c8558b479 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.h +++ b/src/cascadia/TerminalSettingsEditor/MainPage.h @@ -44,6 +44,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Windows::Foundation::Collections::IObservableVector Breadcrumbs() noexcept; Editor::ExtensionsViewModel ExtensionsVM() const noexcept { return _extensionsVM; } + Editor::ActionsViewModel ActionsVM() const noexcept { return _actionsVM; } til::typed_event OpenJson; til::typed_event> ShowLoadWarningsDialog; @@ -78,11 +79,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void _MoveXamlParsedNavItemsIntoItemSource(); winrt::Microsoft::Terminal::Settings::Editor::ColorSchemesPageViewModel _colorSchemesPageVM{ nullptr }; + winrt::Microsoft::Terminal::Settings::Editor::ActionsViewModel _actionsVM{ nullptr }; winrt::Microsoft::Terminal::Settings::Editor::NewTabMenuViewModel _newTabMenuPageVM{ nullptr }; winrt::Microsoft::Terminal::Settings::Editor::ExtensionsViewModel _extensionsVM{ nullptr }; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _profileViewModelChangedRevoker; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _colorSchemesPageViewModelChangedRevoker; + Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _actionsViewModelChangedRevoker; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ntmViewModelChangedRevoker; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _extensionsViewModelChangedRevoker; }; diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.idl b/src/cascadia/TerminalSettingsEditor/MainPage.idl index 20f3bab869..5c023b54e9 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.idl +++ b/src/cascadia/TerminalSettingsEditor/MainPage.idl @@ -2,6 +2,7 @@ // Licensed under the MIT license. import "Extensions.idl"; +import "ActionsViewModel.idl"; namespace Microsoft.Terminal.Settings.Editor { @@ -22,6 +23,7 @@ namespace Microsoft.Terminal.Settings.Editor Profile_Terminal, Profile_Advanced, ColorSchemes_Edit, + Actions_Edit, NewTabMenu_Folder, Extensions_Extension }; @@ -47,6 +49,7 @@ namespace Microsoft.Terminal.Settings.Editor Windows.Foundation.Collections.IObservableVector Breadcrumbs { get; }; ExtensionsViewModel ExtensionsVM { get; }; + ActionsViewModel ActionsVM { get; }; Windows.UI.Xaml.Media.Brush BackgroundBrush { get; }; } diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.xaml b/src/cascadia/TerminalSettingsEditor/MainPage.xaml index 77c1bd8488..3601358ef3 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.xaml +++ b/src/cascadia/TerminalSettingsEditor/MainPage.xaml @@ -149,6 +149,10 @@ + + + Actions.xaml + + ActionsViewModel.idl + Code + + + EditAction.xaml + AddProfile.xaml @@ -168,6 +175,9 @@ Designer + + Designer + Designer @@ -238,6 +248,13 @@ Actions.xaml + + ActionsViewModel.idl + Code + + + EditAction.xaml + AddProfile.xaml diff --git a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters index f49f4c9bc7..9324bd6dde 100644 --- a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters +++ b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters @@ -50,6 +50,7 @@ + diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 6aad91ec7d..fbf9003f7a 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -690,7 +690,11 @@ Actions - Header for the "actions" menu item. This navigates to a page that lets you see and modify commands, key bindings, and actions that can be done in the app. + Header for the "actions" menu item. This navigates to a page that lets you see the available commands in the app. + + + Edit Action... + Header for the "edit action" page. This is the page that lets you modify a specific command and its key bindings. Extensions @@ -1808,6 +1812,42 @@ Delete the unfocused appearance for this profile. A description for what the delete unfocused appearance button does. + + Learn more about actions + Disclaimer presented at the top of the actions page to redirect the user to documentation regarding actions. + + + Delete action + Button label that deletes the selected action. + + + Action name + Label for the text box that edits the action name. + + + Action name + Placeholder text for the text box where the user can edit the action name. + + + Action type + Label for the combo box that edits the action type. + + + Keybindings + Name for a control which contains the list of keybindings for the current command. + + + Additional arguments + Label for the list of editable arguments for the currently selected action. + + + Keybindings + Label for the list of editable keybindings for the current command. + + + Add keybinding + Button label that adds a keybinding to the current action. + Yes, delete key binding Button label that confirms deletion of a key binding entry. @@ -1816,6 +1856,14 @@ Are you sure you want to delete this key binding? Confirmation message displayed when the user attempts to delete a key binding entry. + + Yes, delete action + Button label that confirms deletion of an action. + + + Are you sure you want to delete this action? + Confirmation message displayed when the user attempts to delete an action. + Invalid key chord. Please enter a valid key chord. Error message displayed when an invalid key chord is input by the user. @@ -1860,6 +1908,278 @@ Action Label for a control that sets the action of a key binding. + + Use global setting + An option to choose from for nullable enums. Clears the enum value. + + + HTML + An option to choose from for the "copy format". Copies content in HTML format. + + + RTF + An option to choose from for the "copy format". Copies content in Rich Text Format (RTF). + + + Automatic + An option to choose from for the "split direction". Automatically determines the split direction. + + + Up + An option to choose from for the "split direction". Splits upward. + + + Right + An option to choose from for the "split direction". Splits to the right. + + + Down + An option to choose from for the "split direction". Splits downward. + + + Left + An option to choose from for the "split direction". Splits to the left. + + + Vertical + An option to choose from for the "split direction". Splits vertically. + + + Horizontal + An option to choose from for the "split direction". Splits horizontally. + + + Manual + An option to choose from for the "split type". Creates a manual split. + + + Duplicate + An option to choose from for the "split type". Creates a split by duplicating the current session. + + + None + An option to choose from for the "resize direction". None option. + + + Left + An option to choose from for the "resize direction". Left option. + + + Right + An option to choose from for the "resize direction". Right option. + + + Up + An option to choose from for the "resize direction". Up option. + + + Down + An option to choose from for the "resize direction". Down option. + + + None + An option to choose from for the "focus direction". None option. + + + Left + An option to choose from for the "focus direction". Left option. + + + Right + An option to choose from for the "focus direction". Right option. + + + Up + An option to choose from for the "focus direction". Up option. + + + Down + An option to choose from for the "focus direction". Down option. + + + Previous + An option to choose from for the "focus direction". Previous option. + + + Previous In Order + An option to choose from for the "focus direction". Previous in order option. + + + Next In Order + An option to choose from for the "focus direction". Next in order option. + + + First + An option to choose from for the "focus direction". First option. + + + Parent + An option to choose from for the "focus direction". Parent option. + + + Child + An option to choose from for the "focus direction". Child option. + + + Settings File + An option to choose from for the "settings target". Targets the settings file. + + + Defaults File + An option to choose from for the "settings target". Targets the defaults file. + + + All Files + An option to choose from for the "settings target". Targets all files. + + + Settings UI + An option to choose from for the "settings target". Targets the settings UI. + + + Directory + An option to choose from for the "settings target". Targets the directory. + + + None + An option to choose from for the "move tab direction". No movement. + + + Forward + An option to choose from for the "move tab direction". Moves the tab forward. + + + Backward + An option to choose from for the "move tab direction". Moves the tab backward. + + + Previous + An option to choose from for the "scroll to mark direction". Scrolls to the previous mark. + + + Next + An option to choose from for the "scroll to mark direction". Scrolls to the next mark. + + + First + An option to choose from for the "scroll to mark direction". Scrolls to the first mark. + + + Last + An option to choose from for the "scroll to mark direction". Scrolls to the last mark. + + + Action + An option to choose from for the "command palette launch mode". Launches in action mode. + + + Command Line + An option to choose from for the "command palette launch mode". Launches in command line mode. + + + None + An option to choose from for the "suggestions source". No suggestions source. + + + Tasks + An option to choose from for the "suggestions source". Suggestions come from tasks. + + + Snippets + An option to choose from for the "suggestions source". Suggestions come from snippets. + + + Command History + An option to choose from for the "suggestions source". Suggestions come from command history. + + + Directory History + An option to choose from for the "suggestions source". Suggestions come from directory history. + + + Quick Fixes + An option to choose from for the "suggestions source". Suggestions come from quick fixes. + + + All + An option to choose from for the "suggestions source". Includes all suggestion sources. + + + None + An option to choose from for the "find match direction". No direction selected. + + + Next + An option to choose from for the "find match direction". Finds the next match. + + + Previous + An option to choose from for the "find match direction". Finds the previous match. + + + Any + An option to choose from for the "desktop behavior". Applies to any desktop. + + + To Current + An option to choose from for the "desktop behavior". Moves to the current desktop. + + + On Current + An option to choose from for the "desktop behavior". Stays on the current desktop. + + + Any + An option to choose from for the "monitor behavior". Applies to any monitor. + + + To Current + An option to choose from for the "monitor behavior". Moves to the current monitor. + + + To Mouse + An option to choose from for the "monitor behavior". Moves to the monitor where the mouse is located. + + + Screen + An option to choose from for the "clear buffer type". Clears only the screen. + + + Scrollback + An option to choose from for the "clear buffer type". Clears only the scrollback buffer. + + + All + An option to choose from for the "clear buffer type". Clears both the screen and the scrollback buffer. + + + Previous + An option to choose from for the "select output direction". Selects the previous output. + + + Next + An option to choose from for the "select output direction". Selects the next output. + + + Most Recently Used + An option to choose from for the "tab switcher mode". Switches tabs based on most recently used order. + + + In Order + An option to choose from for the "tab switcher mode". Switches tabs in sequential order. + + + Disabled + An option to choose from for the "tab switcher mode". Disables tab switching. + + + No color + Label for a button directing the user to opt out of choosing a color. + + + Browse... + Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + Input your desired keyboard shortcut. Help text directing users how to use the "KeyChordListener" control. Pressing a keyboard shortcut will be recorded by this control. diff --git a/src/cascadia/TerminalSettingsEditor/Utils.h b/src/cascadia/TerminalSettingsEditor/Utils.h index 37fe4a81ec..a4148303f8 100644 --- a/src/cascadia/TerminalSettingsEditor/Utils.h +++ b/src/cascadia/TerminalSettingsEditor/Utils.h @@ -9,38 +9,38 @@ // being its localized name, and also initializes the enum to EnumEntry // map that's required to tell XAML what enum value the currently active // setting has. -#define INITIALIZE_BINDABLE_ENUM_SETTING(name, enumMappingsName, enumType, resourceSectionAndType, resourceProperty) \ - do \ - { \ - std::vector name##List; \ - _##name##Map = winrt::single_threaded_map(); \ - auto enumMapping##name = winrt::Microsoft::Terminal::Settings::Model::EnumMappings::enumMappingsName(); \ - for (auto [key, value] : enumMapping##name) \ - { \ - auto enumName = LocalizedNameForEnumName(resourceSectionAndType, key, resourceProperty); \ - auto entry = winrt::make(enumName, winrt::box_value(value)); \ - name##List.emplace_back(entry); \ - _##name##Map.Insert(value, entry); \ - } \ - std::sort(name##List.begin(), name##List.end(), EnumEntryComparator()); \ - _##name##List = winrt::single_threaded_observable_vector(std::move(name##List)); \ +#define INITIALIZE_BINDABLE_ENUM_SETTING(name, enumMappingsName, enumType, resourceSectionAndType, resourceProperty) \ + do \ + { \ + std::vector name##List; \ + _##name##Map = winrt::single_threaded_map(); \ + auto enumMapping##name = winrt::Microsoft::Terminal::Settings::Model::EnumMappings::enumMappingsName(); \ + for (auto [key, value] : enumMapping##name) \ + { \ + auto enumName = LocalizedNameForEnumName(resourceSectionAndType, key, resourceProperty); \ + auto entry = winrt::make(enumName, winrt::box_value(value), static_cast(value)); \ + name##List.emplace_back(entry); \ + _##name##Map.Insert(value, entry); \ + } \ + std::sort(name##List.begin(), name##List.end(), EnumEntryComparator()); \ + _##name##List = winrt::single_threaded_observable_vector(std::move(name##List)); \ } while (0); -#define INITIALIZE_BINDABLE_ENUM_SETTING_REVERSE_ORDER(name, enumMappingsName, enumType, resourceSectionAndType, resourceProperty) \ - do \ - { \ - std::vector name##List; \ - _##name##Map = winrt::single_threaded_map(); \ - auto enumMapping##name = winrt::Microsoft::Terminal::Settings::Model::EnumMappings::enumMappingsName(); \ - for (auto [key, value] : enumMapping##name) \ - { \ - auto enumName = LocalizedNameForEnumName(resourceSectionAndType, key, resourceProperty); \ - auto entry = winrt::make(enumName, winrt::box_value(value)); \ - name##List.emplace_back(entry); \ - _##name##Map.Insert(value, entry); \ - } \ - std::sort(name##List.begin(), name##List.end(), EnumEntryReverseComparator()); \ - _##name##List = winrt::single_threaded_observable_vector(std::move(name##List)); \ +#define INITIALIZE_BINDABLE_ENUM_SETTING_REVERSE_ORDER(name, enumMappingsName, enumType, resourceSectionAndType, resourceProperty) \ + do \ + { \ + std::vector name##List; \ + _##name##Map = winrt::single_threaded_map(); \ + auto enumMapping##name = winrt::Microsoft::Terminal::Settings::Model::EnumMappings::enumMappingsName(); \ + for (auto [key, value] : enumMapping##name) \ + { \ + auto enumName = LocalizedNameForEnumName(resourceSectionAndType, key, resourceProperty); \ + auto entry = winrt::make(enumName, winrt::box_value(value), static_cast(value)); \ + name##List.emplace_back(entry); \ + _##name##Map.Insert(value, entry); \ + } \ + std::sort(name##List.begin(), name##List.end(), EnumEntryReverseComparator()); \ + _##name##List = winrt::single_threaded_observable_vector(std::move(name##List)); \ } while (0); // This macro must be used alongside INITIALIZE_BINDABLE_ENUM_SETTING. diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 6ac762fb71..45ba2cb144 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -306,7 +306,7 @@ protected: \ #define SPLIT_PANE_ARGS(X) \ X(Model::SplitDirection, SplitDirection, "split", false, ArgTypeHint::None, SplitDirection::Automatic) \ X(SplitType, SplitMode, "splitMode", false, ArgTypeHint::None, SplitType::Manual) \ - X(float, SplitSize, "size", false, ArgTypeHint::None, 0.5f) + X(float, SplitSize, "size", false, ArgTypeHint::SplitSize, 0.5f) //////////////////////////////////////////////////////////////////////////////// diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index a58e95c6cb..67c241e239 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -10,7 +10,8 @@ namespace Microsoft.Terminal.Settings.Model None = 0, FilePath, FolderPath, - ColorScheme + ColorScheme, + SplitSize }; struct ArgDescriptor diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index 112e4ea19f..c012617a4c 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -238,7 +238,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { if (!_name.has_value() || _name->name != value) { - _name = CommandNameOrResource{ .name = std::wstring{ value } }; + if (value.empty()) + { + _name.reset(); + } + else + { + _name = CommandNameOrResource{ .name = std::wstring{ value } }; + } } }