From 3afcd575df609989dbd6bc7e257d67b46bd356cf Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Thu, 23 Sep 2021 04:36:43 +0900 Subject: [PATCH 01/35] Fix typo in charsets.hpp (#11300) --- src/terminal/adapter/charsets.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal/adapter/charsets.hpp b/src/terminal/adapter/charsets.hpp index 0a3a649ff..bc81e31e7 100644 --- a/src/terminal/adapter/charsets.hpp +++ b/src/terminal/adapter/charsets.hpp @@ -44,7 +44,7 @@ namespace Microsoft::Console::VirtualTerminal } // Note that the 94-character sets are deliberately defined with a size of - // 95 to avoid having to test the lower bound. We just alway leave the first + // 95 to avoid having to test the lower bound. We just always leave the first // entry - which is not meant to be mapped - as a SPACE or NBSP, which is at // least visually equivalent to leaving it untranslated. From 86ba1fc6c36653988bd1328a85f455c2e488a2e5 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 22 Sep 2021 21:39:34 +0200 Subject: [PATCH 02/35] Fix KeyChord constructor assertion failure during tab dragging (#11306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For some weird reason we sometimes receive a WM_KEYDOWN message without vkey or scanCode if a user drags a tab. The KeyChord constructor has a debug assertion ensuring that all KeyChord either have a valid vkey/scanCode. This is important, because this prevents accidential insertion of invalid KeyChords into classes like ActionMap. ## PR Checklist * [x] Closes #11076 * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * Tab dragging doesn't produce assertion failures anymore ✔️ --- src/cascadia/TerminalControl/TermControl.cpp | 33 +++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index fc74dd0a9..f90190a5d 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -848,6 +848,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } + const auto keyStatus = e.KeyStatus(); + const auto vkey = gsl::narrow_cast(e.OriginalKey()); + const auto scanCode = gsl::narrow_cast(keyStatus.ScanCode); + auto modifiers = _GetPressedModifierKeys(); + + // GH#11076: + // For some weird reason we sometimes receive a WM_KEYDOWN + // message without vkey or scanCode if a user drags a tab. + // The KeyChord constructor has a debug assertion ensuring that all KeyChord + // either have a valid vkey/scanCode. This is important, because this prevents + // accidential insertion of invalid KeyChords into classes like ActionMap. + if (!vkey && !scanCode) + { + e.Handled(true); + return; + } + // Mark the event as handled and do nothing if we're closing, or the key // was the Windows key. // @@ -856,19 +873,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // win32-input-mode, then we'll send all these keystrokes to the // terminal - it's smart enough to ignore the keys it doesn't care // about. - if (_IsClosing() || - e.OriginalKey() == VirtualKey::LeftWindows || - e.OriginalKey() == VirtualKey::RightWindows) - + if (_IsClosing() || vkey == VK_LWIN || vkey == VK_RWIN) { e.Handled(true); return; } - auto modifiers = _GetPressedModifierKeys(); - const auto vkey = gsl::narrow_cast(e.OriginalKey()); - const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); - // Short-circuit isReadOnly check to avoid warning dialog if (_core.IsInReadOnlyMode()) { @@ -876,7 +886,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - if (e.KeyStatus().IsExtendedKey) + if (keyStatus.IsExtendedKey) { modifiers |= ControlKeyStates::EnhancedKey; } @@ -886,8 +896,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // will be sent through the TSFInputControl. See GH#1401 for more // details if (modifiers.IsAltPressed() && - (e.OriginalKey() >= VirtualKey::NumberPad0 && e.OriginalKey() <= VirtualKey::NumberPad9)) - + (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)) { e.Handled(true); return; @@ -917,7 +926,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Manually prevent keyboard navigation with tab. We want to send tab to // the terminal, and we don't want to be able to escape focus of the // control with tab. - e.Handled(e.OriginalKey() == VirtualKey::Tab); + e.Handled(vkey == VK_TAB); } // Method Description: From f04fd089fe9870236d94a8f0d0c6e2769e4ef279 Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Wed, 22 Sep 2021 12:41:01 -0700 Subject: [PATCH 03/35] Fix mouse coordinates when viewport is scrolled for all events, not just pressed (#11290) Does the mouse coordinate adjustment added in #10642 for all the other mouse events as well (moved, released, wheel) Closes #10190 --- .../TerminalControl/ControlInteractivity.cpp | 30 ++++++++++++------- .../TerminalControl/ControlInteractivity.h | 6 ++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index 55d389869..6991d1b40 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -214,12 +214,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else if (_canSendVTMouseInput(modifiers)) { - const auto adjustment = _core->ScrollOffset() > 0 ? _core->BufferHeight() - _core->ScrollOffset() - _core->ViewHeight() : 0; - // If the click happened outside the active region, just don't send any mouse event - if (const auto adjustedY = terminalPosition.y() - adjustment; adjustedY >= 0) - { - _core->SendMouseEvent({ terminalPosition.x(), adjustedY }, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); - } + _sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); } else if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown)) { @@ -287,7 +282,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Short-circuit isReadOnly check to avoid warning dialog if (focused && !_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) { - _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); + _sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); } // GH#4603 - don't modify the selection if the pointer press didn't // actually start _in_ the control bounds. Case in point - someone drags @@ -370,7 +365,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Short-circuit isReadOnly check to avoid warning dialog if (!_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) { - _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); + _sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); return; } @@ -420,11 +415,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation // here with a PointerPoint. However, as of #979, we don't have a // PointerPoint to work with. So, we're just going to do a // mousewheel event manually - return _core->SendMouseEvent(terminalPosition, + return _sendMouseEventHelper(terminalPosition, WM_MOUSEWHEEL, modifiers, ::base::saturated_cast(delta), - toInternalMouseState(buttonState)); + buttonState); } const auto ctrlPressed = modifiers.IsCtrlPressed(); @@ -600,6 +595,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation return til::point{ pixelPosition / fontSize }; } + bool ControlInteractivity::_sendMouseEventHelper(const til::point terminalPosition, + const unsigned int pointerUpdateKind, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const SHORT wheelDelta, + Control::MouseButtonState buttonState) + { + const auto adjustment = _core->ScrollOffset() > 0 ? _core->BufferHeight() - _core->ScrollOffset() - _core->ViewHeight() : 0; + // If the click happened outside the active region, just don't send any mouse event + if (const auto adjustedY = terminalPosition.y() - adjustment; adjustedY >= 0) + { + return _core->SendMouseEvent({ terminalPosition.x(), adjustedY }, pointerUpdateKind, modifiers, wheelDelta, toInternalMouseState(buttonState)); + } + return false; + } + // Method Description: // - Creates an automation peer for the Terminal Control, enabling // accessibility on our control. diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index 937534838..71312afc3 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -142,6 +142,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _sendPastedTextToConnection(std::wstring_view wstr); til::point _getTerminalPosition(const til::point& pixelPosition); + bool _sendMouseEventHelper(const til::point terminalPosition, + const unsigned int pointerUpdateKind, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const SHORT wheelDelta, + Control::MouseButtonState buttonState); + friend class ControlUnitTests::ControlCoreTests; friend class ControlUnitTests::ControlInteractivityTests; }; From 171e0a02423b9022565a7a0e6fbf1e13e6111a91 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 23 Sep 2021 12:44:20 -0500 Subject: [PATCH 04/35] Add shield to tab row when elevated (#11224) ## Summary of the Pull Request Adds a visible indicator that a Terminal window is elevated. This icon can be disabled with `"showAdminShield" false` in the global settings. ## References * spec'd in #8455 * Also in https://github.com/microsoft/terminal/projects/5 * big picture: #5000 ## PR Checklist * [x] Closes #1939 * [x] I work here * [n/a] Tests added/passed * [ ] Requires documentation to be updated - yea probably ## Validation Steps Performed ![image](https://user-images.githubusercontent.com/18356694/133293009-4215e319-fbf9-4ca8-8af5-afe2fa8bb62d.png) ![image](https://user-images.githubusercontent.com/18356694/133292970-90cb17fd-16c7-429a-a25f-8457850eb278.png) --- doc/cascadia/profiles.schema.json | 5 +++ .../Resources/en-US/Resources.resw | 3 ++ src/cascadia/TerminalApp/TabRowControl.h | 4 ++ src/cascadia/TerminalApp/TabRowControl.idl | 4 +- src/cascadia/TerminalApp/TabRowControl.xaml | 11 +++++ src/cascadia/TerminalApp/TerminalPage.cpp | 41 +++++++++++++------ src/cascadia/TerminalApp/TerminalPage.h | 1 + .../GlobalAppSettings.cpp | 6 +++ .../TerminalSettingsModel/GlobalAppSettings.h | 1 + .../GlobalAppSettings.idl | 1 + .../TerminalSettingsModel/defaults.json | 1 + 11 files changed, 64 insertions(+), 14 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 319e0bae7..ad827c79c 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1274,6 +1274,11 @@ "description": "When set to true, the Terminal's notification icon will always be shown in the notification area.", "type": "boolean" }, + "showAdminShield": { + "default": "true", + "description": "When set to true, the Terminal's tab row will display a shield icon when the Terminal is running with administrator privileges", + "type": "boolean" + }, "useAcrylicInTabRow": { "default": "false", "description": "When set to true, the tab row will have an acrylic background with 50% opacity.", diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 86b0b407f..421816f58 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -715,4 +715,7 @@ Don't show again + + This Terminal window is running as Admin + diff --git a/src/cascadia/TerminalApp/TabRowControl.h b/src/cascadia/TerminalApp/TabRowControl.h index 6ffa60532..1e4bd573f 100644 --- a/src/cascadia/TerminalApp/TabRowControl.h +++ b/src/cascadia/TerminalApp/TabRowControl.h @@ -4,6 +4,7 @@ #pragma once #include "winrt/Microsoft.UI.Xaml.Controls.h" +#include "../../cascadia/inc/cppwinrt_utils.h" #include "TabRowControl.g.h" @@ -16,6 +17,9 @@ namespace winrt::TerminalApp::implementation void OnNewTabButtonClick(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const& args); void OnNewTabButtonDrop(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::DragEventArgs const& e); void OnNewTabButtonDragOver(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::DragEventArgs const& e); + + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + WINRT_OBSERVABLE_PROPERTY(bool, ShowElevationShield, _PropertyChangedHandlers, false); }; } diff --git a/src/cascadia/TerminalApp/TabRowControl.idl b/src/cascadia/TerminalApp/TabRowControl.idl index 1415df9a2..4dd1d37bd 100644 --- a/src/cascadia/TerminalApp/TabRowControl.idl +++ b/src/cascadia/TerminalApp/TabRowControl.idl @@ -3,9 +3,11 @@ namespace TerminalApp { - [default_interface] runtimeclass TabRowControl : Windows.UI.Xaml.Controls.ContentPresenter + [default_interface] runtimeclass TabRowControl : Windows.UI.Xaml.Controls.ContentPresenter, + Windows.UI.Xaml.Data.INotifyPropertyChanged { TabRowControl(); Microsoft.UI.Xaml.Controls.TabView TabView { get; }; + Boolean ShowElevationShield; } } diff --git a/src/cascadia/TerminalApp/TabRowControl.xaml b/src/cascadia/TerminalApp/TabRowControl.xaml index 802e7c529..b3b0f58c3 100644 --- a/src/cascadia/TerminalApp/TabRowControl.xaml +++ b/src/cascadia/TerminalApp/TabRowControl.xaml @@ -20,6 +20,17 @@ IsAddTabButtonVisible="false" TabWidthMode="Equal"> + + + + + ().Logic().IsElevated(); + } + CATCH_LOG(); + return result; + }(); + + return isElevated; + } + void TerminalPage::Create() { // Hookup the key bindings @@ -128,19 +151,7 @@ namespace winrt::TerminalApp::implementation _tabView = _tabRow.TabView(); _rearranging = false; - // GH#2455 - Make sure to try/catch calls to Application::Current, - // because that _won't_ be an instance of TerminalApp::App in the - // LocalTests - auto isElevated = false; - try - { - // GH#3581 - There's a platform limitation that causes us to crash when we rearrange tabs. - // Xaml tries to send a drag visual (to wit: a screenshot) to the drag hosting process, - // but that process is running at a different IL than us. - // For now, we're disabling elevated drag. - isElevated = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsElevated(); - } - CATCH_LOG(); + const auto isElevated = IsElevated(); if (_settings.GlobalSettings().UseAcrylicInTabRow()) { @@ -267,6 +278,8 @@ namespace winrt::TerminalApp::implementation // Setup mouse vanish attributes SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &_shouldMouseVanish, false); + _tabRow.ShowElevationShield(IsElevated() && _settings.GlobalSettings().ShowAdminShield()); + // Store cursor, so we can restore it, e.g., after mouse vanishing // (we'll need to adapt this logic once we make cursor context aware) try @@ -2242,6 +2255,8 @@ namespace winrt::TerminalApp::implementation // enabled application-wide, so we don't need to check it each time we // want to create an animation. WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); + + _tabRow.ShowElevationShield(IsElevated() && _settings.GlobalSettings().ShowAdminShield()); } // This is a helper to aid in sorting commands by their `Name`s, alphabetically. diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 7302cf6ed..a9ff78959 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -115,6 +115,7 @@ namespace winrt::TerminalApp::implementation winrt::hstring WindowIdForDisplay() const noexcept; winrt::hstring WindowNameForDisplay() const noexcept; bool IsQuakeWindow() const noexcept; + bool IsElevated() const noexcept; WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 4a8860e3d..53c0fd90f 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -51,6 +51,7 @@ static constexpr std::string_view TrimBlockSelectionKey{ "trimBlockSelection" }; static constexpr std::string_view AlwaysShowNotificationIconKey{ "alwaysShowNotificationIcon" }; static constexpr std::string_view MinimizeToNotificationAreaKey{ "minimizeToNotificationArea" }; static constexpr std::string_view DisabledProfileSourcesKey{ "disabledProfileSources" }; +static constexpr std::string_view ShowAdminShieldKey{ "showAdminShield" }; static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" }; @@ -121,6 +122,8 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_MinimizeToNotificationArea = _MinimizeToNotificationArea; globals->_AlwaysShowNotificationIcon = _AlwaysShowNotificationIcon; globals->_DisabledProfileSources = _DisabledProfileSources; + globals->_ShowAdminShield = _ShowAdminShield; + globals->_UnparsedDefaultProfile = _UnparsedDefaultProfile; globals->_defaultProfile = _defaultProfile; @@ -227,6 +230,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, AlwaysShowNotificationIconKey, _AlwaysShowNotificationIcon); JsonUtils::GetValueForKey(json, DisabledProfileSourcesKey, _DisabledProfileSources); + JsonUtils::GetValueForKey(json, ShowAdminShieldKey, _ShowAdminShield); + static constexpr std::array bindingsKeys{ LegacyKeybindingsKey, ActionsKey }; for (const auto& jsonKey : bindingsKeys) { @@ -324,6 +329,7 @@ Json::Value GlobalAppSettings::ToJson() const JsonUtils::SetValueForKey(json, MinimizeToNotificationAreaKey, _MinimizeToNotificationArea); JsonUtils::SetValueForKey(json, AlwaysShowNotificationIconKey, _AlwaysShowNotificationIcon); JsonUtils::SetValueForKey(json, DisabledProfileSourcesKey, _DisabledProfileSources); + JsonUtils::SetValueForKey(json, ShowAdminShieldKey, _ShowAdminShield); // clang-format on json[JsonKey(ActionsKey)] = _actionMap->ToJson(); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index 87d87445d..32f73cafe 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -99,6 +99,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::GlobalAppSettings, bool, AlwaysShowNotificationIcon, false); INHERITABLE_SETTING(Model::GlobalAppSettings, winrt::Windows::Foundation::Collections::IVector, DisabledProfileSources, nullptr); INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, L""); + INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ShowAdminShield, true); private: #ifdef NDEBUG diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index ea1d7de79..66b2125df 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -84,6 +84,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, MinimizeToNotificationArea); INHERITABLE_SETTING(Boolean, AlwaysShowNotificationIcon); INHERITABLE_SETTING(IVector, DisabledProfileSources); + INHERITABLE_SETTING(Boolean, ShowAdminShield); Windows.Foundation.Collections.IMapView ColorSchemes(); void AddColorScheme(ColorScheme scheme); diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 6261e743a..d6b45016f 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -20,6 +20,7 @@ "showTerminalTitleInTitlebar": true, "tabWidthMode": "equal", "tabSwitcherMode": "inOrder", + "showAdminShield": true, // Miscellaneous "confirmCloseAllTabs": true, From e75f848cf34a3e01a97fb68cdb72ec577061cca1 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Thu, 23 Sep 2021 10:58:31 -0700 Subject: [PATCH 05/35] Keyboard Selection Spec (#2840) This introduces a spec for keyboard selection. This enables the user to create and update a selection without the use of a mouse or stylus. ## References Contributes to #715 --- .github/actions/spelling/expect/expect.txt | 3 + doc/specs/Keyboard-Selection.md | 171 +++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 doc/specs/Keyboard-Selection.md diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index efd6edff8..c12609802 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -194,6 +194,8 @@ cascadia cassert castsi catid +carlos +zamora cazamor CBash cbegin @@ -2690,6 +2692,7 @@ WINDOWPOSCHANGING windowproc windowrect windowsapp +windowsdeveloper windowsinternalstring WINDOWSIZE windowsx diff --git a/doc/specs/Keyboard-Selection.md b/doc/specs/Keyboard-Selection.md new file mode 100644 index 000000000..f9954fca7 --- /dev/null +++ b/doc/specs/Keyboard-Selection.md @@ -0,0 +1,171 @@ +--- +author: Carlos Zamora @carlos-zamora +created on: 2019-08-30 +last updated: 2021-09-17 +issue id: 715 +--- + +# Keyboard Selection + +## Abstract + +This spec describes a new set of non-configurable keybindings that allows the user to update a selection without the use of a mouse or stylus. + +## Inspiration + +ConHost allows the user to modify a selection using the keyboard. Holding `Shift` allows the user to move the second selection endpoint in accordance with the arrow keys. The selection endpoint updates by one cell per key event, allowing the user to refine the selected region. + +Mark mode allows the user to create a selection using only the keyboard, then edit it as mentioned above. + + +## Solution Design + +The fundamental solution design for keyboard selection is that the responsibilities between the Terminal Control and Terminal Core must be very distinct. The Terminal Control is responsible for handling user interaction and directing the Terminal Core to update the selection. The Terminal Core will need to update the selection according to the preferences of the Terminal Control. + +Relatively recently, TerminalControl was split into `TerminalControl`, `ControlInteractivity`, and `ControlCore`. Changes made to `ControlInteractivity`, `ControlCore`, and below propagate functionality to all consumers, meaning that the WPF terminal would benefit from these changes with no additional work required. + +### Fundamental Terminal Control Changes + +`ControlCore::TrySendKeyEvent()` is responsible for handling the key events after key bindings are dealt with in `TermControl`. At the time of writing this spec, there are 2 cases handled in this order: +- Clear the selection (except in a few key scenarios) +- Send Key Event + +The first branch will be updated to _modify_ the selection instead of usually _clearing_ it. This will happen by converting the key event into parameters to forward to `TerminalCore`, which then updates the selection appropriately. + +#### Idea: Make keyboard selection a collection of standard keybindings +One idea is to introduce an `updateSelection` action that conditionally works if a selection is active (similar to the `copy` action). For these key bindings, if there is no selection, the key events are forwarded to the application. + +Thanks to Keybinding Args, there would only be 1 new command: +| Action | Keybinding Args | Description | +|--|--|--| +| `updateSelection` | | If a selection exists, moves the last selection endpoint. | +| | `Enum direction { up, down, left, right }` | The direction the selection will be moved in. | +| | `Enum mode { char, word, view, buffer }` | The context for which to move the selection endpoint to. (defaults to `char`) | + + +By default, the following keybindings will be set: +```JS +// Character Selection +{ "command": {"action": "updateSelection", "direction": "left", "mode": "char" }, "keys": "shift+left" }, +{ "command": {"action": "updateSelection", "direction": "right", "mode": "char" }, "keys": "shift+right" }, +{ "command": {"action": "updateSelection", "direction": "up", "mode": "char" }, "keys": "shift+up" }, +{ "command": {"action": "updateSelection", "direction": "down", "mode": "char" }, "keys": "shift+down" }, + +// Word Selection +{ "command": {"action": "updateSelection", "direction": "left", "mode": "word" }, "keys": "ctrl+shift+left" }, +{ "command": {"action": "updateSelection", "direction": "right", "mode": "word" }, "keys": "ctrl+shift+right" }, + +// Viewport Selection +{ "command": {"action": "updateSelection", "direction": "left", "mode": "view" }, "keys": "shift+home" }, +{ "command": {"action": "updateSelection", "direction": "right", "mode": "view" }, "keys": "shift+end" }, +{ "command": {"action": "updateSelection", "direction": "up", "mode": "view" }, "keys": "shift+pgup" }, +{ "command": {"action": "updateSelection", "direction": "down", "mode": "view" }, "keys": "shift+pgdn" }, + +// Buffer Corner Selection +{ "command": {"action": "updateSelection", "direction": "up", "mode": "buffer" }, "keys": "ctrl+shift+home" }, +{ "command": {"action": "updateSelection", "direction": "down", "mode": "buffer" }, "keys": "ctrl+shift+end" }, +``` +These are in accordance with ConHost's keyboard selection model. + +This idea was abandoned due to several reasons: +1. Keyboard selection should be a standard way to interact with a terminal across all consumers (i.e. WPF control, etc.) +2. There isn't really another set of key bindings that makes sense for this. We already hardcoded ESC as a way to clear the selection. This is just an extension of that. +3. Adding 12 conditionally effective key bindings takes the spot of 12 potential non-conditional key bindings. It would be nice if a different key binding could be set when the selection is not active, but that makes the settings design much more complicated. +4. 12 new items in the command palette is also pretty excessive. +5. If proven wrong when this is in WT Preview, we can revisit this and make them customizable then. It's better to add the ability to customize it later than take it away. + +#### Idea: Make keyboard selection a simulation of mouse selection +It may seem that some effort can be saved by making the keyboard selection act as a simulation of mouse selection. There is a union of mouse and keyboard activity that can be represented in a single set of selection motion interfaces that are commanded by the TermControl's Mouse/Keyboard handler and adapted into appropriate motions in the Terminal Core. + +However, the mouse handler operates by translating a pixel coordinate on the screen to a text buffer coordinate. This would have to be rewritten and the approach was deemed unworthy. + + +### Fundamental Terminal Core Changes + +The Terminal Core will need to expose a `UpdateSelection()` function that is called by the keybinding handler. The following parameters will need to be passed in: +- `enum SelectionDirection`: the direction that the selection endpoint will attempt to move to. Possible values include `Up`, `Down`, `Left`, and `Right`. +- `enum SelectionExpansion`: the selection expansion mode that the selection endpoint will adhere to. Possible values include `Char`, `Word`, `View`, `Buffer`. + +#### Moving by Cell +For `SelectionExpansion = Char`, the selection endpoint will be updated according to the buffer's output pattern. For **horizontal movements**, the selection endpoint will attempt to move left or right. If a viewport boundary is hit, the endpoint will wrap appropriately (i.e.: hitting the left boundary moves it to the last cell of the line above it). + +For **vertical movements**, the selection endpoint will attempt to move up or down. If a **viewport boundary** is hit and there is a scroll buffer, the endpoint will move and scroll accordingly by a line. + +If a **buffer boundary** is hit, the endpoint will not move. In this case, however, the event will still be considered handled. + +**NOTE**: An important thing to handle properly in all cases is wide glyphs. The user should not be allowed to select a portion of a wide glyph; it should be all or none of it. When calling `_ExpandWideGlyphSelection` functions, the result must be saved to the endpoint. + +#### Moving by Word +For `SelectionExpansion = Word`, the selection endpoint will also be updated according to the buffer's output pattern, as above. However, the selection will be updated in accordance with "chunk selection" (performing a double-click and dragging the mouse to expand the selection). For **horizontal movements**, the selection endpoint will be updated according to the `_ExpandDoubleClickSelection` functions. The result must be saved to the endpoint. As before, if a boundary is hit, the endpoint will wrap appropriately. See [Future Considerations](#FutureConsiderations) for how this will interact with line wrapping. + +For **vertical movements**, the movement is a little more complicated than before. The selection will still respond to buffer and viewport boundaries as before. If the user is trying to move up, the selection endpoint will attempt to move up by one line, then selection will be expanded leftwards. Alternatively, if the user is trying to move down, the selection endpoint will attempt to move down by one line, then the selection will be expanded rightwards. + +#### Moving by Viewport +For `SelectionExpansion = View`, the selection endpoint will be updated according to the viewport's height. Horizontal movements will be updated according to the viewport's width, thus resulting in the endpoint being moved to the left/right boundary of the viewport. + +#### Moving by Buffer + +For `SelectionExpansion = Buffer`, the selection endpoint will be moved to the beginning or end of all the text within the buffer. If moving up or left, set the position to 0,0 (the origin of the buffer). If moving down or right, set the position to the last character in the buffer. + + +**NOTE**: In all cases, horizontal movements attempting to move past the left/right viewport boundaries result in a wrap. Vertical movements attempting to move past the top/bottom viewport boundaries will scroll such that the selection is at the edge of the screen. Vertical movements attempting to move past the top/bottom buffer boundaries will be clamped to be within buffer boundaries. + +Every combination of the `SelectionDirection` and `SelectionExpansion` will map to a keybinding. These pairings are shown below in the UI/UX Design --> Keybindings section. + +**NOTE**: If `copyOnSelect` is enabled, we need to make sure we **DO NOT** update the clipboard on every change in selection. The user must explicitly choose to copy the selected text from the buffer. + + +## UI/UX Design + +### Key Bindings + +There will only be 1 new command that needs to be added: +| Action | Keybinding Args | Description | +|--|--|--| +| `selectAll` | | Select the entire text buffer. + +By default, the following key binding will be set: +```JS +{ "command": "selectAll", "keys": "ctrl+shift+a" }, +``` + +## Capabilities + +### Accessibility + +Using the keyboard is generally a more accessible experience than using the mouse. Being able to modify a selection by using the keyboard is a good first step towards making selecting text more accessible. + +### Security + +N/A + +### Reliability + +With regards to the Terminal Core, the newly introduced code should rely on already existing and tested code. Thus no crash-related bugs are expected. + +With regards to Terminal Control and the settings model, crash-related bugs are not expected. However, ensuring that the selection is updated and cleared in general use-case scenarios must be ensured. + +### Compatibility + +N/A + +### Performance, Power, and Efficiency + +## Potential Issues + +### Grapheme Clusters +When grapheme cluster support is inevitably added to the Text Buffer, moving by "cell" is expected to move by "character" or "cluster". This is similar to how wide glyphs are handled today. Either all of it is selected, or none of it. + +## Future considerations + +### Word Selection Wrap +At the time of writing this spec, expanding or moving by word is interrupted by the beginning or end of the line, regardless of the wrap flag being set. In the future, selection and the accessibility models will respect the wrap flag on the text buffer. + +## Mark Mode + +This functionality will be expanded to create a feature similar to Mark Mode. This will allow a user to create a selection using only the keyboard. + + +## Resources + +- https://blogs.windows.com/windowsdeveloper/2014/10/07/console-improvements-in-the-windows-10-technical-preview/ From 3b7049c5b78306a533cd00ff00beb1301a887ef7 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 23 Sep 2021 13:05:38 -0500 Subject: [PATCH 06/35] releng: add New-TerminalStackedChangelog (#11065) I've done this process enough times that I should have written a script to do it a while ago. This one is rough, but the whole changelog process is pretty rough. This script takes multiple revision ranges and produces something that looks like a rough untranslated changelog, with indicators for how many of the provided ranges had the same change (deduplicated by title.) I use a process like this to build the Stable and Preview release notes out of a set of revision ranges. --- .github/actions/spelling/allow/allow.txt | 2 + .github/actions/spelling/allow/names.txt | 1 + .../New-TerminalStackedChangelog.ps1 | 82 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 tools/ReleaseEngineering/New-TerminalStackedChangelog.ps1 diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index e4abdb7d8..35742a611 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -1,6 +1,7 @@ apc calt ccmp +changelog cybersecurity Apc clickable @@ -41,6 +42,7 @@ Lorigin maxed mkmk mru +noreply nje ogonek ok'd diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt index 27ba53635..3635d3723 100644 --- a/.github/actions/spelling/allow/names.txt +++ b/.github/actions/spelling/allow/names.txt @@ -53,6 +53,7 @@ oldnewthing opengl osgwiki pabhojwa +panos paulcam pauldotknopf PGP diff --git a/tools/ReleaseEngineering/New-TerminalStackedChangelog.ps1 b/tools/ReleaseEngineering/New-TerminalStackedChangelog.ps1 new file mode 100644 index 000000000..0c2ad8b5c --- /dev/null +++ b/tools/ReleaseEngineering/New-TerminalStackedChangelog.ps1 @@ -0,0 +1,82 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +################################# +# New-TerminalStackedChangelog generates a markdown file with attribution +# over a set of revision ranges. +# Dustin uses it when he's writing the changelog. +# +## For example, generating the changelog for both 1.9 and 1.10 might look like this: +# $ New-TerminalStackedChangelog 1.9..release-1.9, 1.10..release-1.10 +## The output will be a markdown-like document. Each commit will be converted into a +## single-line entry with attribution and an optional count: +# +# * Foo the bar (thanks @PanosP!) +# * [2] Fix the bug +# +# Entries with a count were present in both changelists. +# +# If you don't have the release tags/branches checked out locally, you might +# need to do something like: +# +# $ New-TerminalStackedChangelog origin/release-1.8..origin/release-1.9 + +[CmdletBinding()] +Param( + [string[]]$RevisionRanges +) + +Function Test-MicrosoftPerson($email) { + Return $email -like "*@microsoft.com" -Or $email -like "pankaj.d*" +} + +Function Generate-Thanks($Entry) { + # We don't need to thank ourselves for doing our jobs + If ($_.Microsoft) { + "" + } ElseIf (-Not [string]::IsNullOrEmpty($_.PossibleUsername)) { + " (thanks @{0}!)" -f $_.PossibleUsername + } Else { + " (thanks @<{0}>!)" -f $_.Email + } +} + +$usernameRegex = [regex]::new("(?:\d+\+)?(?[^@]+)@users.noreply.github.com") +Function Get-PossibleUserName($email) { + $match = $usernameRegex.Match($email) + if ($null -Ne $match) { + return $match.Groups["name"].Value + } + return $null +} + +$Entries = @() + +ForEach ($RevisionRange in $RevisionRanges) { + # --pretty=format notes: + # - %an: author name + # - %x1C: print a literal FS (0x1C), which will be used as a delimiter + # - %ae: author email + # - %x1C: another FS + # - %s: subject, the title of the commit + $NewEntries = & git log $RevisionRange "--pretty=format:%an%x1C%ae%x1C%s" | + ConvertFrom-CSV -Delimiter "`u{001C}" -Header Author,Email,Subject + + $Entries += $NewEntries | % { [PSCustomObject]@{ + Author = $_.Author; + Email = $_.Email; + Subject = $_.Subject; + Microsoft = (Test-MicrosoftPerson $_.Email); + PossibleUsername = (Get-PossibleUserName $_.Email); + } } +} + +$Unique = $Entries | Group-Object Subject | %{ $_.Group[0] | Add-Member Count $_.Count -Force -PassThru } + +$Unique | % { + $c = "" + If ($_.Count -Gt 1) { + $c = "[{0}] " -f $_.Count + } + "* {0}{1}{2}" -f ($c, $_.Subject, (Generate-Thanks $_)) +} From e21eba89328766c94ab2df2a0780a74ea18b9f8e Mon Sep 17 00:00:00 2001 From: snxx Date: Thu, 23 Sep 2021 23:47:07 +0500 Subject: [PATCH 07/35] .editorconfig: add utf-8 encoding (#11190) This commit configures compliant text editors to save our code as UTF-8. --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index 40de22df5..7ffdc78df 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,7 @@ root = true [*] +charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true From c070be12d3639bbe1944658aacb56728ab5ea8c7 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Thu, 23 Sep 2021 12:24:32 -0700 Subject: [PATCH 08/35] Implement Keyboard Selection (#10824) Implements the following keyboard selection non-configurable key bindings: - shift+arrow --> move endpoint by character - ctrl+shift+left/right --> move endpoint by word - shift+home/end --> move to beginning/end of line - ctrl+shift+home/end --> move to beginning/end of buffer This was purposefully done in the ControlCore layer to make keyboard selection an innate part of how the terminal functions (aka a shared component across terminal consumers). ## References #715 - Keyboard Selection #2840 - Spec ## Detailed Description of the Pull Request / Additional comment The most relevant section is `TerminalSelection.cpp`, where we define how each movement operates. It's basically a giant embedded switch-case statement. We leverage a lot of the work done in a11y to perform the movements. ## Validation Steps Performed - General cases: - test all of the key bindings added - Corner cases: - `char`: wide glyph support - `word`: move towards, away, and across the selection pivot - automatically scroll viewport - ESC (and other key combos) are still clearing the selection properly --- doc/cascadia/profiles.schema.json | 2 +- src/buffer/out/textBuffer.cpp | 10 +- src/buffer/out/textBuffer.hpp | 2 +- .../PublicTerminalCore/HwndTerminal.cpp | 4 +- src/cascadia/TerminalControl/ControlCore.cpp | 31 ++- src/cascadia/TerminalCore/Terminal.hpp | 32 ++- .../TerminalCore/TerminalSelection.cpp | 237 +++++++++++++++++- .../TerminalCore/terminalrenderdata.cpp | 2 +- .../UnitTests_TerminalCore/SelectionTest.cpp | 34 +-- src/host/ut_host/TextBufferTests.cpp | 2 +- src/renderer/dx/DxRenderer.cpp | 1 + src/types/UiaTextRangeBase.cpp | 2 +- 12 files changed, 307 insertions(+), 52 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index ad827c79c..293b3dd07 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -847,7 +847,7 @@ } ], "required": [ "actions" ] - }, + }, "CommandPaletteAction": { "description": "Arguments for a commandPalette action", "allOf": [ diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 466e38ecf..3fba7b082 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -1430,12 +1430,13 @@ const til::point TextBuffer::GetGlyphStart(const til::point pos, std::optional limitOptional) const +const til::point TextBuffer::GetGlyphEnd(const til::point pos, bool accessibilityMode, std::optional limitOptional) const { COORD resultPos = pos; const auto bufferSize = GetSize(); @@ -1453,7 +1454,10 @@ const til::point TextBuffer::GetGlyphEnd(const til::point pos, std::optional limitOptional = std::nullopt) const; - const til::point GetGlyphEnd(const til::point pos, std::optional limitOptional = std::nullopt) const; + const til::point GetGlyphEnd(const til::point pos, bool accessibilityMode = false, std::optional limitOptional = std::nullopt) const; bool MoveToNextGlyph(til::point& pos, bool allowBottomExclusive = false, std::optional limitOptional = std::nullopt) const; bool MoveToPreviousGlyph(til::point& pos, std::optional limitOptional = std::nullopt) const; diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index c86f5d621..02025be81 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -549,11 +549,11 @@ try if (multiClickMapper == 3) { - _terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansionMode::Line); + _terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansion::Line); } else if (multiClickMapper == 2) { - _terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansionMode::Word); + _terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansion::Word); } else { diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index e09eb93ae..1e41fdb5c 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -359,21 +359,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation const ControlKeyStates modifiers, const bool keyDown) { - // When there is a selection active, escape should clear it and NOT flow through - // to the terminal. With any other keypress, it should clear the selection AND - // flow through to the terminal. + // Update the selection, if it's present // GH#6423 - don't dismiss selection if the key that was pressed was a // modifier key. We'll wait for a real keystroke to dismiss the // GH #7395 - don't dismiss selection when taking PrintScreen // selection. - // GH#8522, GH#3758 - Only dismiss the selection on key _down_. If we - // dismiss on key up, then there's chance that we'll immediately dismiss + // GH#8522, GH#3758 - Only modify the selection on key _down_. If we + // modify on key up, then there's chance that we'll immediately dismiss // a selection created by an action bound to a keydown. if (HasSelection() && !KeyEvent::IsModifierKey(vkey) && vkey != VK_SNAPSHOT && keyDown) { + // try to update the selection + if (const auto updateSlnParams{ ::Terminal::ConvertKeyEventToUpdateSelectionParams(modifiers, vkey) }) + { + auto lock = _terminal->LockForWriting(); + _terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second); + _renderer->TriggerSelection(); + return true; + } + // GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination. if (!modifiers.IsWinPressed()) { @@ -381,6 +388,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation _renderer->TriggerSelection(); } + // When there is a selection active, escape should clear it and NOT flow through + // to the terminal. With any other keypress, it should clear the selection AND + // flow through to the terminal. if (vkey == VK_ESCAPE) { return true; @@ -1399,18 +1409,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation // handle ALT key _terminal->SetBlockSelection(altEnabled); - ::Terminal::SelectionExpansionMode mode = ::Terminal::SelectionExpansionMode::Cell; + ::Terminal::SelectionExpansion mode = ::Terminal::SelectionExpansion::Char; if (numberOfClicks == 1) { - mode = ::Terminal::SelectionExpansionMode::Cell; + mode = ::Terminal::SelectionExpansion::Char; } else if (numberOfClicks == 2) { - mode = ::Terminal::SelectionExpansionMode::Word; + mode = ::Terminal::SelectionExpansion::Word; } else if (numberOfClicks == 3) { - mode = ::Terminal::SelectionExpansionMode::Line; + mode = ::Terminal::SelectionExpansion::Line; } // Update the selection appropriately @@ -1435,7 +1445,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetSelectionEnd(terminalPosition, mode); selectionNeedsToBeCopied = true; } - else if (mode != ::Terminal::SelectionExpansionMode::Cell || shiftEnabled) + else if (mode != ::Terminal::SelectionExpansion::Char || shiftEnabled) { // If we are handling a double / triple-click or shift+single click // we establish selection using the selected mode @@ -1534,5 +1544,4 @@ namespace winrt::Microsoft::Terminal::Control::implementation return hstring(ss.str()); } - } diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 27e26b7f2..d3b5f1440 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -227,16 +227,30 @@ public: #pragma region TextSelection // These methods are defined in TerminalSelection.cpp - enum class SelectionExpansionMode + enum class SelectionDirection { - Cell, - Word, - Line + Left, + Right, + Up, + Down }; - void MultiClickSelection(const COORD viewportPos, SelectionExpansionMode expansionMode); + + enum class SelectionExpansion + { + Char, + Word, + Line, // Mouse selection only! + Viewport, + Buffer + }; + void MultiClickSelection(const COORD viewportPos, SelectionExpansion expansionMode); void SetSelectionAnchor(const COORD position); - void SetSelectionEnd(const COORD position, std::optional newExpansionMode = std::nullopt); + void SetSelectionEnd(const COORD position, std::optional newExpansionMode = std::nullopt); void SetBlockSelection(const bool isEnabled) noexcept; + void UpdateSelection(SelectionDirection direction, SelectionExpansion mode); + + using UpdateSelectionParams = std::optional>; + static UpdateSelectionParams ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey); const TextBuffer::TextAndColor RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace); #pragma endregion @@ -308,7 +322,7 @@ private: std::optional _selection; bool _blockSelection; std::wstring _wordDelimiters; - SelectionExpansionMode _multiClickSelectionMode; + SelectionExpansion _multiClickSelectionMode; #pragma endregion // TODO: These members are not shared by an alt-buffer. They should be @@ -375,6 +389,10 @@ private: std::pair _PivotSelection(const COORD targetPos, bool& targetStart) const; std::pair _ExpandSelectionAnchors(std::pair anchors) const; COORD _ConvertToBufferCell(const COORD viewportPos) const; + void _MoveByChar(SelectionDirection direction, COORD& pos); + void _MoveByWord(SelectionDirection direction, COORD& pos); + void _MoveByViewport(SelectionDirection direction, COORD& pos); + void _MoveByBuffer(SelectionDirection direction, COORD& pos); #pragma endregion Microsoft::Console::VirtualTerminal::SgrStack _sgrStack; diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp index c8c877032..367a8365a 100644 --- a/src/cascadia/TerminalCore/TerminalSelection.cpp +++ b/src/cascadia/TerminalCore/TerminalSelection.cpp @@ -100,8 +100,8 @@ const bool Terminal::IsBlockSelection() const noexcept // - Perform a multi-click selection at viewportPos expanding according to the expansionMode // Arguments: // - viewportPos: the (x,y) coordinate on the visible viewport -// - expansionMode: the SelectionExpansionMode to dictate the boundaries of the selection anchors -void Terminal::MultiClickSelection(const COORD viewportPos, SelectionExpansionMode expansionMode) +// - expansionMode: the SelectionExpansion to dictate the boundaries of the selection anchors +void Terminal::MultiClickSelection(const COORD viewportPos, SelectionExpansion expansionMode) { // set the selection pivot to expand the selection using SetSelectionEnd() _selection = SelectionAnchors{}; @@ -124,7 +124,7 @@ void Terminal::SetSelectionAnchor(const COORD viewportPos) _selection = SelectionAnchors{}; _selection->pivot = _ConvertToBufferCell(viewportPos); - _multiClickSelectionMode = SelectionExpansionMode::Cell; + _multiClickSelectionMode = SelectionExpansion::Char; SetSelectionEnd(viewportPos); _selection->start = _selection->pivot; @@ -136,7 +136,7 @@ void Terminal::SetSelectionAnchor(const COORD viewportPos) // Arguments: // - viewportPos: the (x,y) coordinate on the visible viewport // - newExpansionMode: overwrites the _multiClickSelectionMode for this function call. Used for ShiftClick -void Terminal::SetSelectionEnd(const COORD viewportPos, std::optional newExpansionMode) +void Terminal::SetSelectionEnd(const COORD viewportPos, std::optional newExpansionMode) { if (!_selection.has_value()) { @@ -210,15 +210,15 @@ std::pair Terminal::_ExpandSelectionAnchors(std::pairGetSize(); switch (_multiClickSelectionMode) { - case SelectionExpansionMode::Line: + case SelectionExpansion::Line: start = { bufferSize.Left(), start.Y }; end = { bufferSize.RightInclusive(), end.Y }; break; - case SelectionExpansionMode::Word: + case SelectionExpansion::Word: start = _buffer->GetWordStart(start, _wordDelimiters); end = _buffer->GetWordEnd(end, _wordDelimiters); break; - case SelectionExpansionMode::Cell: + case SelectionExpansion::Char: default: // no expansion is necessary break; @@ -235,6 +235,229 @@ void Terminal::SetBlockSelection(const bool isEnabled) noexcept _blockSelection = isEnabled; } +Terminal::UpdateSelectionParams Terminal::ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey) +{ + if (mods.IsShiftPressed() && !mods.IsAltPressed()) + { + if (mods.IsCtrlPressed()) + { + // Ctrl + Shift + _ + switch (vkey) + { + case VK_LEFT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Word }; + case VK_RIGHT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Word }; + case VK_HOME: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Buffer }; + case VK_END: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Buffer }; + } + } + else + { + // Shift + _ + switch (vkey) + { + case VK_HOME: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Viewport }; + case VK_END: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Viewport }; + case VK_PRIOR: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Up, SelectionExpansion::Viewport }; + case VK_NEXT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Down, SelectionExpansion::Viewport }; + case VK_LEFT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Char }; + case VK_RIGHT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Char }; + case VK_UP: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Up, SelectionExpansion::Char }; + case VK_DOWN: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Down, SelectionExpansion::Char }; + } + } + } + return std::nullopt; +} + +void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion mode) +{ + // 1. Figure out which endpoint to update + // One of the endpoints is the pivot, signifying that the other endpoint is the one we want to move. + const bool movingEnd{ _selection->start == _selection->pivot }; + auto targetPos{ movingEnd ? _selection->end : _selection->start }; + + // 2. Perform the movement + switch (mode) + { + case SelectionExpansion::Char: + _MoveByChar(direction, targetPos); + break; + case SelectionExpansion::Word: + _MoveByWord(direction, targetPos); + break; + case SelectionExpansion::Viewport: + _MoveByViewport(direction, targetPos); + break; + case SelectionExpansion::Buffer: + _MoveByBuffer(direction, targetPos); + break; + } + + // 3. Actually modify the selection + // NOTE: targetStart doesn't matter here + bool targetStart = false; + std::tie(_selection->start, _selection->end) = _PivotSelection(targetPos, targetStart); + + // 4. Scroll (if necessary) + if (const auto viewport = _GetVisibleViewport(); !viewport.IsInBounds(targetPos)) + { + if (const auto amtAboveView = viewport.Top() - targetPos.Y; amtAboveView > 0) + { + // anchor is above visible viewport, scroll by that amount + _scrollOffset += amtAboveView; + } + else + { + // anchor is below visible viewport, scroll by that amount + const auto amtBelowView = targetPos.Y - viewport.BottomInclusive(); + _scrollOffset -= amtBelowView; + } + _NotifyScrollEvent(); + _buffer->GetRenderTarget().TriggerScroll(); + } +} + +void Terminal::_MoveByChar(SelectionDirection direction, COORD& pos) +{ + switch (direction) + { + case SelectionDirection::Left: + _buffer->GetSize().DecrementInBounds(pos); + pos = _buffer->GetGlyphStart(pos); + break; + case SelectionDirection::Right: + _buffer->GetSize().IncrementInBounds(pos); + pos = _buffer->GetGlyphEnd(pos); + break; + case SelectionDirection::Up: + { + const auto bufferSize{ _buffer->GetSize() }; + pos = { pos.X, std::clamp(base::ClampSub(pos.Y, 1).RawValue(), bufferSize.Top(), bufferSize.BottomInclusive()) }; + break; + } + case SelectionDirection::Down: + { + const auto bufferSize{ _buffer->GetSize() }; + pos = { pos.X, std::clamp(base::ClampAdd(pos.Y, 1).RawValue(), bufferSize.Top(), bufferSize.BottomInclusive()) }; + break; + } + } +} + +void Terminal::_MoveByWord(SelectionDirection direction, COORD& pos) +{ + switch (direction) + { + case SelectionDirection::Left: + const auto wordStartPos{ _buffer->GetWordStart(pos, _wordDelimiters) }; + if (_buffer->GetSize().CompareInBounds(_selection->pivot, pos) < 0) + { + // If we're moving towards the pivot, move one more cell + pos = wordStartPos; + _buffer->GetSize().DecrementInBounds(pos); + } + else if (wordStartPos == pos) + { + // already at the beginning of the current word, + // move to the beginning of the previous word + _buffer->GetSize().DecrementInBounds(pos); + pos = _buffer->GetWordStart(pos, _wordDelimiters); + } + else + { + // move to the beginning of the current word + pos = wordStartPos; + } + break; + case SelectionDirection::Right: + const auto wordEndPos{ _buffer->GetWordEnd(pos, _wordDelimiters) }; + if (_buffer->GetSize().CompareInBounds(pos, _selection->pivot) < 0) + { + // If we're moving towards the pivot, move one more cell + pos = _buffer->GetWordEnd(pos, _wordDelimiters); + _buffer->GetSize().IncrementInBounds(pos); + } + else if (wordEndPos == pos) + { + // already at the end of the current word, + // move to the end of the next word + _buffer->GetSize().IncrementInBounds(pos); + pos = _buffer->GetWordEnd(pos, _wordDelimiters); + } + else + { + // move to the end of the current word + pos = wordEndPos; + } + break; + case SelectionDirection::Up: + _MoveByChar(direction, pos); + pos = _buffer->GetWordStart(pos, _wordDelimiters); + break; + case SelectionDirection::Down: + _MoveByChar(direction, pos); + pos = _buffer->GetWordEnd(pos, _wordDelimiters); + break; + } +} + +void Terminal::_MoveByViewport(SelectionDirection direction, COORD& pos) +{ + const auto bufferSize{ _buffer->GetSize() }; + switch (direction) + { + case SelectionDirection::Left: + pos = { bufferSize.Left(), pos.Y }; + break; + case SelectionDirection::Right: + pos = { bufferSize.RightInclusive(), pos.Y }; + break; + case SelectionDirection::Up: + { + const auto viewportHeight{ _mutableViewport.Height() }; + const auto newY{ base::ClampSub(pos.Y, viewportHeight) }; + pos = newY < bufferSize.Top() ? bufferSize.Origin() : COORD{ pos.X, newY }; + break; + } + case SelectionDirection::Down: + { + const auto viewportHeight{ _mutableViewport.Height() }; + const auto mutableBottom{ _mutableViewport.BottomInclusive() }; + const auto newY{ base::ClampAdd(pos.Y, viewportHeight) }; + pos = newY > mutableBottom ? COORD{ bufferSize.RightInclusive(), mutableBottom } : COORD{ pos.X, newY }; + break; + } + } +} + +void Terminal::_MoveByBuffer(SelectionDirection direction, COORD& pos) +{ + const auto bufferSize{ _buffer->GetSize() }; + switch (direction) + { + case SelectionDirection::Left: + case SelectionDirection::Up: + pos = bufferSize.Origin(); + break; + case SelectionDirection::Right: + case SelectionDirection::Down: + pos = { bufferSize.RightInclusive(), _mutableViewport.BottomInclusive() }; + break; + } +} + // Method Description: // - clear selection data and disable rendering it #pragma warning(disable : 26440) // changing this to noexcept would require a change to ConHost's selection model diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 84b00eadf..6ccb5791d 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -206,7 +206,7 @@ void Terminal::SelectNewRegion(const COORD coordStart, const COORD coordEnd) realCoordEnd.Y -= gsl::narrow(_VisibleStartIndex()); SetSelectionAnchor(realCoordStart); - SetSelectionEnd(realCoordEnd, SelectionExpansionMode::Cell); + SetSelectionEnd(realCoordEnd, SelectionExpansion::Char); } const std::wstring_view Terminal::GetConsoleTitle() const noexcept diff --git a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp index d5e5a8f03..e0dc7acc5 100644 --- a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp @@ -130,7 +130,7 @@ namespace TerminalCoreUnitTests DummyRenderTarget emptyRT; term.Create({ 10, 10 }, scrollback, emptyRT); - term.MultiClickSelection(maxCoord, Terminal::SelectionExpansionMode::Word); + term.MultiClickSelection(maxCoord, Terminal::SelectionExpansion::Word); ValidateSingleRowSelection(term, expected); }; @@ -142,7 +142,7 @@ namespace TerminalCoreUnitTests DummyRenderTarget emptyRT; term.Create({ 10, 10 }, scrollback, emptyRT); - term.MultiClickSelection(maxCoord, Terminal::SelectionExpansionMode::Line); + term.MultiClickSelection(maxCoord, Terminal::SelectionExpansion::Line); ValidateSingleRowSelection(term, expected); }; @@ -501,7 +501,7 @@ namespace TerminalCoreUnitTests // Simulate double click at (x,y) = (5,10) auto clickPos = COORD{ 5, 10 }; - term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Word); + term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Word); // Validate selection area ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, (4 + gsl::narrow(text.size()) - 1), 10 })); @@ -519,7 +519,7 @@ namespace TerminalCoreUnitTests // Simulate click at (x,y) = (5,10) auto clickPos = COORD{ 5, 10 }; - term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Word); + term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Word); // Simulate renderer calling TriggerSelection and acquiring selection area auto selectionRects = term.GetSelectionRects(); @@ -546,7 +546,7 @@ namespace TerminalCoreUnitTests // Simulate click at (x,y) = (15,10) // this is over the '>' char auto clickPos = COORD{ 15, 10 }; - term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Word); + term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Word); // ---Validate selection area--- // "Terminal" is in class 2 @@ -572,7 +572,7 @@ namespace TerminalCoreUnitTests term.Write(text); // Simulate double click at (x,y) = (5,10) - term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansionMode::Word); + term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansion::Word); // Simulate move to (x,y) = (21,10) // @@ -601,7 +601,7 @@ namespace TerminalCoreUnitTests term.Write(text); // Simulate double click at (x,y) = (21,10) - term.MultiClickSelection({ 21, 10 }, Terminal::SelectionExpansionMode::Word); + term.MultiClickSelection({ 21, 10 }, Terminal::SelectionExpansion::Word); // Simulate move to (x,y) = (5,10) // @@ -622,7 +622,7 @@ namespace TerminalCoreUnitTests // Simulate click at (x,y) = (5,10) auto clickPos = COORD{ 5, 10 }; - term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Line); + term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Line); // Validate selection area ValidateSingleRowSelection(term, SMALL_RECT({ 0, 10, 99, 10 })); @@ -636,7 +636,7 @@ namespace TerminalCoreUnitTests // Simulate click at (x,y) = (5,10) auto clickPos = COORD{ 5, 10 }; - term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Line); + term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Line); // Simulate move to (x,y) = (7,10) term.SetSelectionEnd({ 7, 10 }); @@ -653,7 +653,7 @@ namespace TerminalCoreUnitTests // Simulate click at (x,y) = (5,10) auto clickPos = COORD{ 5, 10 }; - term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Line); + term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Line); // Simulate move to (x,y) = (5,11) term.SetSelectionEnd({ 5, 11 }); @@ -691,7 +691,7 @@ namespace TerminalCoreUnitTests // Step 1: Create a selection on "doubleClickMe" { // Simulate double click at (x,y) = (5,10) - term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansionMode::Word); + term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansion::Word); // Validate selection area: "doubleClickMe" selected ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 16, 10 })); @@ -704,7 +704,7 @@ namespace TerminalCoreUnitTests // buffer: doubleClickMe dragThroughHere // ^ ^ // start finish - term.SetSelectionEnd({ 21, 10 }, ::Terminal::SelectionExpansionMode::Cell); + term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Char); // Validate selection area: "doubleClickMe drag" selected ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 21, 10 })); @@ -717,7 +717,7 @@ namespace TerminalCoreUnitTests // buffer: doubleClickMe dragThroughHere // ^ ^ ^ // start click finish - term.SetSelectionEnd({ 21, 10 }, ::Terminal::SelectionExpansionMode::Word); + term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Word); // Validate selection area: "doubleClickMe dragThroughHere" selected ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 32, 10 })); @@ -730,7 +730,7 @@ namespace TerminalCoreUnitTests // buffer: doubleClickMe dragThroughHere | // ^ ^ ^ // start click finish (boundary) - term.SetSelectionEnd({ 21, 10 }, ::Terminal::SelectionExpansionMode::Line); + term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Line); // Validate selection area: "doubleClickMe dragThroughHere..." selected ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 99, 10 })); @@ -743,7 +743,7 @@ namespace TerminalCoreUnitTests // buffer: doubleClickMe dragThroughHere // ^ ^ ^ // start click finish - term.SetSelectionEnd({ 21, 10 }, ::Terminal::SelectionExpansionMode::Word); + term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Word); // Validate selection area: "doubleClickMe dragThroughHere" selected ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 32, 10 })); @@ -825,7 +825,7 @@ namespace TerminalCoreUnitTests // Step 4: Shift+Click at (5,10) { - term.SetSelectionEnd({ 5, 10 }, ::Terminal::SelectionExpansionMode::Cell); + term.SetSelectionEnd({ 5, 10 }, Terminal::SelectionExpansion::Char); // Validate selection area // NOTE: Pivot should still be (10, 10) @@ -834,7 +834,7 @@ namespace TerminalCoreUnitTests // Step 5: Shift+Click back at (20,10) { - term.SetSelectionEnd({ 20, 10 }, ::Terminal::SelectionExpansionMode::Cell); + term.SetSelectionEnd({ 20, 10 }, Terminal::SelectionExpansion::Char); // Validate selection area // NOTE: Pivot should still be (10, 10) diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 92c07f85f..c8dd06793 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -2253,7 +2253,7 @@ void TextBufferTests::GetGlyphBoundaries() _buffer->Write(iter, target); auto start = _buffer->GetGlyphStart(target); - auto end = _buffer->GetGlyphEnd(target); + auto end = _buffer->GetGlyphEnd(target, true); VERIFY_ARE_EQUAL(test.start, start); VERIFY_ARE_EQUAL(wideGlyph ? test.wideGlyphEnd : test.normalEnd, end); diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index d01d99364..de33cd00c 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -493,6 +493,7 @@ void DxEngine::_ComputePixelShaderSettings() noexcept // actual failure from the API itself. [[nodiscard]] HRESULT DxEngine::_CreateSurfaceHandle() noexcept { +#pragma warning(suppress : 26447) wil::unique_hmodule hDComp{ LoadLibraryEx(L"Dcomp.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) }; RETURN_LAST_ERROR_IF(hDComp.get() == nullptr); diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 860dabcd7..08133944c 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -295,7 +295,7 @@ void UiaTextRangeBase::_expandToEnclosingUnit(TextUnit unit) if (unit == TextUnit_Character) { _start = buffer.GetGlyphStart(_start, documentEnd); - _end = buffer.GetGlyphEnd(_start, documentEnd); + _end = buffer.GetGlyphEnd(_start, true, documentEnd); } else if (unit <= TextUnit_Word) { From 0f122ca290125951c21d7c830d73a10337bab6b7 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Thu, 23 Sep 2021 15:14:03 -0700 Subject: [PATCH 09/35] [a11y] Ensure buffer is initialized before interacting with it (#11312) Adds a check before every UIA function call to ensure the terminal (specifically the buffer) is initialized before doing work. Both the `ScreenInfoUiaProvider` and the `UiaTextRange` are now covered. ## References Closes #11135 #10971 & #11042 ## Detailed Description of the Pull Request / Additional comments Originally, I tried applying this heuristic to all the `RuntimeClassInitialize` on `UiaTextRangeBase` with the philosophy of "a range pointing to an invalid buffer is invalid itself", but that caused a regression on [MSFT 33353327](https://microsoft.visualstudio.com/OS/_workitems/edit/33353327). `IUiaData` also has `GetTextBuffer()` return a `TextBuffer&`, which cannot be checked for nullness. Instead, I decided to add a function to `IUiaData` that checks if we have a valid state. Since this is shared with Conhost and Conhost doesn't have this issue, I simply make that function say that it's always in a valid state. ## Validation Steps Performed - [X] Narrator can detect newly created terminals - [X] (On Windows Server 2022) Windows Terminal does not hang on launch --- src/Terminal.wprp | 2 + src/cascadia/TerminalCore/Terminal.hpp | 1 + .../TerminalCore/terminalrenderdata.cpp | 9 +++ src/host/renderData.hpp | 1 + src/host/ut_host/VtIoTests.cpp | 5 ++ src/types/IUiaData.h | 1 + src/types/ScreenInfoUiaProviderBase.cpp | 25 +++--- src/types/TermControlUiaProvider.cpp | 14 ++-- src/types/TermControlUiaTextRange.cpp | 14 ---- src/types/UiaTextRangeBase.cpp | 80 +++++++++++-------- 10 files changed, 83 insertions(+), 69 deletions(-) diff --git a/src/Terminal.wprp b/src/Terminal.wprp index d37c749c9..80a6e730b 100644 --- a/src/Terminal.wprp +++ b/src/Terminal.wprp @@ -12,6 +12,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index d3b5f1440..5eb4db473 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -199,6 +199,7 @@ public: const COORD GetSelectionEnd() const noexcept override; const std::wstring_view GetConsoleTitle() const noexcept override; void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute) override; + const bool IsUiaDataInitialized() const noexcept override; #pragma endregion void SetWriteInputCallback(std::function pfn) noexcept; diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 6ccb5791d..1ab2b5af8 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -250,3 +250,12 @@ bool Terminal::IsScreenReversed() const noexcept { return _screenReversed; } + +const bool Terminal::IsUiaDataInitialized() const noexcept +{ + // GH#11135: Windows Terminal needs to create and return an automation peer + // when a screen reader requests it. However, the terminal might not be fully + // initialized yet. So we use this to check if any crucial components of + // UiaData are not yet initialized. + return !!_buffer; +} diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index 8b858c541..b1699531d 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -69,5 +69,6 @@ public: const COORD GetSelectionAnchor() const noexcept; const COORD GetSelectionEnd() const noexcept; void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr); + const bool IsUiaDataInitialized() const noexcept override { return true; } #pragma endregion }; diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index 07ec963be..6e8d4e286 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -392,6 +392,11 @@ public: { } + const bool IsUiaDataInitialized() const noexcept + { + return true; + } + const std::wstring GetHyperlinkUri(uint16_t /*id*/) const noexcept { return {}; diff --git a/src/types/IUiaData.h b/src/types/IUiaData.h index 9cc6b8d9c..a1ea9990b 100644 --- a/src/types/IUiaData.h +++ b/src/types/IUiaData.h @@ -40,6 +40,7 @@ namespace Microsoft::Console::Types virtual const COORD GetSelectionAnchor() const noexcept = 0; virtual const COORD GetSelectionEnd() const noexcept = 0; virtual void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr) = 0; + virtual const bool IsUiaDataInitialized() const noexcept = 0; }; // See docs/virtual-dtors.md for an explanation of why this is weird. diff --git a/src/types/ScreenInfoUiaProviderBase.cpp b/src/types/ScreenInfoUiaProviderBase.cpp index b5cb4d442..1d15a837e 100644 --- a/src/types/ScreenInfoUiaProviderBase.cpp +++ b/src/types/ScreenInfoUiaProviderBase.cpp @@ -220,21 +220,19 @@ IFACEMETHODIMP ScreenInfoUiaProviderBase::SetFocus() IFACEMETHODIMP ScreenInfoUiaProviderBase::GetSelection(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) { + RETURN_HR_IF_NULL(E_INVALIDARG, ppRetVal); + *ppRetVal = nullptr; + _LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _UnlockConsole(); }); - - RETURN_HR_IF_NULL(E_INVALIDARG, ppRetVal); - *ppRetVal = nullptr; - HRESULT hr = S_OK; + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); // make a safe array + HRESULT hr = S_OK; *ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, 1); - if (*ppRetVal == nullptr) - { - return E_OUTOFMEMORY; - } + RETURN_HR_IF_NULL(E_OUTOFMEMORY, *ppRetVal); WRL::ComPtr range; if (!_pData->IsSelectionActive()) @@ -272,19 +270,18 @@ IFACEMETHODIMP ScreenInfoUiaProviderBase::GetSelection(_Outptr_result_maybenull_ IFACEMETHODIMP ScreenInfoUiaProviderBase::GetVisibleRanges(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) { + RETURN_HR_IF_NULL(E_INVALIDARG, ppRetVal); + *ppRetVal = nullptr; + _LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _UnlockConsole(); }); - - RETURN_HR_IF_NULL(E_INVALIDARG, ppRetVal); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); // make a safe array *ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, 1); - if (*ppRetVal == nullptr) - { - return E_OUTOFMEMORY; - } + RETURN_HR_IF_NULL(E_OUTOFMEMORY, *ppRetVal); WRL::ComPtr range; const auto bufferSize = _pData->GetTextBuffer().GetSize(); diff --git a/src/types/TermControlUiaProvider.cpp b/src/types/TermControlUiaProvider.cpp index c39f7133b..3ced07561 100644 --- a/src/types/TermControlUiaProvider.cpp +++ b/src/types/TermControlUiaProvider.cpp @@ -16,9 +16,6 @@ HRESULT TermControlUiaProvider::RuntimeClassInitialize(_In_ ::Microsoft::Console RETURN_IF_FAILED(ScreenInfoUiaProviderBase::RuntimeClassInitialize(uiaData)); _controlInfo = controlInfo; - - // TODO GitHub #1914: Re-attach Tracing to UIA Tree - //Tracing::s_TraceUia(nullptr, ApiCall::Constructor, nullptr); return S_OK; } @@ -26,11 +23,6 @@ IFACEMETHODIMP TermControlUiaProvider::Navigate(_In_ NavigateDirection direction _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) noexcept { RETURN_HR_IF_NULL(E_INVALIDARG, ppProvider); - - // TODO GitHub #1914: Re-attach Tracing to UIA Tree - /*ApiMsgNavigate apiMsg; - apiMsg.Direction = direction; - Tracing::s_TraceUia(this, ApiCall::Navigate, &apiMsg);*/ *ppProvider = nullptr; if (direction == NavigateDirection_Parent) @@ -122,6 +114,12 @@ HRESULT TermControlUiaProvider::GetSelectionRange(_In_ IRawElementProviderSimple RETURN_HR_IF_NULL(E_INVALIDARG, ppUtr); *ppUtr = nullptr; + _pData->LockConsole(); + auto Unlock = wil::scope_exit([&]() noexcept { + _pData->UnlockConsole(); + }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized() || !_pData->IsSelectionActive()); + const auto start = _pData->GetSelectionAnchor(); // we need to make end exclusive diff --git a/src/types/TermControlUiaTextRange.cpp b/src/types/TermControlUiaTextRange.cpp index ba5128885..5d95dbd88 100644 --- a/src/types/TermControlUiaTextRange.cpp +++ b/src/types/TermControlUiaTextRange.cpp @@ -63,20 +63,6 @@ IFACEMETHODIMP TermControlUiaTextRange::Clone(_Outptr_result_maybenull_ ITextRan return hr; } -#if defined(_DEBUG) && defined(UiaTextRangeBase_DEBUG_MSGS) - OutputDebugString(L"Clone\n"); - std::wstringstream ss; - ss << _id << L" cloned to " << (static_cast(*ppRetVal))->_id; - std::wstring str = ss.str(); - OutputDebugString(str.c_str()); - OutputDebugString(L"\n"); -#endif - // TODO GitHub #1914: Re-attach Tracing to UIA Tree - // tracing - /*ApiMsgClone apiMsg; - apiMsg.CloneId = static_cast(*ppRetVal)->GetId(); - Tracing::s_TraceUia(this, ApiCall::Clone, &apiMsg);*/ - return S_OK; } diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 08133944c..c32a6a900 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -220,15 +220,18 @@ IFACEMETHODIMP UiaTextRangeBase::CompareEndpoints(_In_ TextPatternRangeEndpoint _Out_ int* pRetVal) noexcept try { - RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); + RETURN_HR_IF_NULL(E_INVALIDARG, pRetVal); *pRetVal = 0; + _pData->LockConsole(); + auto Unlock = wil::scope_exit([&]() noexcept { + _pData->UnlockConsole(); + }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); + // get the text range that we're comparing to const UiaTextRangeBase* range = static_cast(pTargetRange); - if (range == nullptr) - { - return E_INVALIDARG; - } + RETURN_HR_IF_NULL(E_INVALIDARG, range); // get endpoint value that we're comparing to const auto other = range->GetEndpoint(targetEndpoint); @@ -240,10 +243,7 @@ try // This is a temporary solution to comparing two UTRs from different TextBuffers // Ensure both endpoints fit in the current buffer. const auto bufferSize = _pData->GetTextBuffer().GetSize(); - if (!bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true)) - { - return E_FAIL; - } + RETURN_HR_IF(E_FAIL, !bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true)); // compare them *pRetVal = bufferSize.CompareInBounds(mine, other, true); @@ -259,6 +259,7 @@ IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexc auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); try { @@ -444,6 +445,12 @@ try RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); *ppRetVal = nullptr; + _pData->LockConsole(); + auto Unlock = wil::scope_exit([&]() noexcept { + _pData->UnlockConsole(); + }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); + // AttributeIDs that require special handling switch (attributeId) { @@ -605,6 +612,12 @@ try RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); *ppRetVal = nullptr; + _pData->LockConsole(); + auto Unlock = wil::scope_exit([&]() noexcept { + _pData->UnlockConsole(); + }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); + const std::wstring queryText{ text, SysStringLen(text) }; const auto bufferSize = _getOptimizedBufferSize(); const auto sensitivity = ignoreCase ? Search::Sensitivity::CaseInsensitive : Search::Sensitivity::CaseSensitive; @@ -730,6 +743,12 @@ try RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); VariantInit(pRetVal); + _pData->LockConsole(); + auto Unlock = wil::scope_exit([&]() noexcept { + _pData->UnlockConsole(); + }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); + // AttributeIDs that require special handling switch (attributeId) { @@ -817,13 +836,14 @@ CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept { + RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); + *ppRetVal = nullptr; + _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); - - RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); - *ppRetVal = nullptr; + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); try { @@ -925,21 +945,19 @@ CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::GetText(_In_ int maxLength, _Out_ BSTR* pRetVal) noexcept try { - RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); + RETURN_HR_IF_NULL(E_INVALIDARG, pRetVal); + RETURN_HR_IF(E_INVALIDARG, maxLength < -1); *pRetVal = nullptr; - if (maxLength < -1) - { - return E_INVALIDARG; - } + _pData->LockConsole(); + auto Unlock = wil::scope_exit([&]() noexcept { + _pData->UnlockConsole(); + }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); const auto maxLengthOpt = (maxLength == -1) ? std::nullopt : std::optional{ maxLength }; - _pData->LockConsole(); - auto Unlock = wil::scope_exit([this]() noexcept { - _pData->UnlockConsole(); - }); const auto text = _getTextValue(maxLengthOpt); Unlock.reset(); @@ -1013,6 +1031,7 @@ try auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); // We can abstract this movement by moving _start // GH#7342: check if we're past the documentEnd @@ -1075,15 +1094,13 @@ IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoin { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = 0; - if (count == 0) - { - return S_OK; - } _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); + RETURN_HR_IF(S_OK, count == 0); // GH#7342: check if we're past the documentEnd // If so, clamp each endpoint to the end of the document. @@ -1141,10 +1158,8 @@ try }); const UiaTextRangeBase* range = static_cast(pTargetRange); - if (range == nullptr) - { - return E_INVALIDARG; - } + RETURN_HR_IF_NULL(E_INVALIDARG, range); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); // TODO GH#5406: create a different UIA parent object for each TextBuffer // This is a temporary solution to comparing two UTRs from different TextBuffers @@ -1152,10 +1167,7 @@ try const auto bufferSize = _pData->GetTextBuffer().GetSize(); const auto mine = GetEndpoint(endpoint); const auto other = range->GetEndpoint(targetEndpoint); - if (!bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true)) - { - return E_FAIL; - } + RETURN_HR_IF(E_FAIL, !bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true)); SetEndpoint(endpoint, range->GetEndpoint(targetEndpoint)); @@ -1171,6 +1183,7 @@ try auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); if (IsDegenerate()) { @@ -1215,6 +1228,7 @@ try auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); + RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); const auto oldViewport = _pData->GetViewport().ToInclusive(); const auto viewportHeight = _getViewportHeight(oldViewport); From 9708a75131f44eaf527e57c4193671841c2ed454 Mon Sep 17 00:00:00 2001 From: Ian O'Neill Date: Fri, 24 Sep 2021 17:17:16 +0100 Subject: [PATCH 10/35] Properly escape constructed `wt` command-lines (#11314) Ensures that command-lines constructed to invoke `wt` are escaped properly. ## PR Checklist * [x] Closes #11273 * [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA * [x] Tests added/passed ## Detailed Description of the Pull Request / Additional comments This was broken in two places - when constructing the command-line in the shell extension and in `NewTerminalArgs::ToCommandline()`. Both places now invoke a shared method to escape the command-line arguments that require it. ## Validation Steps Performed Added a test and additionally: * Invoked the shell extension from `D:\Downloads\With;Semicolon`. * Added a `newWindow` action to `settings.json` as below and ensured the new window opened without erroring. ```json { "command": { "action": "newWindow", "tabTitle": "\";foo\\" }, "keys": "ctrl+shift+s" } ``` --- .../LocalTests_SettingsModel/CommandTests.cpp | 21 ++++++++++- .../ShellExtension/OpenTerminalHere.cpp | 6 +-- .../TerminalSettingsModel/ActionArgs.cpp | 10 ++--- src/cascadia/WinRTUtils/inc/WtExeUtils.h | 37 +++++++++++++++++++ 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp index b25d476e7..da5e3916c 100644 --- a/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp @@ -465,6 +465,10 @@ namespace SettingsModelLocalTests "name":"action7_startingDirectoryWithTrailingSlash", "command": { "action": "newWindow", "startingDirectory":"C:\\", "commandline": "bar.exe" } }, + { + "name":"action8_tabTitleEscaping", + "command": { "action": "newWindow", "tabTitle":"\\\";foo\\" } + } ])" }; const auto commands0Json = VerifyParseSucceeded(commands0String); @@ -473,7 +477,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(0u, commands.Size()); auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(8u, commands.Size()); + VERIFY_ARE_EQUAL(9u, commands.Size()); { auto command = commands.Lookup(L"action0"); @@ -586,5 +590,20 @@ namespace SettingsModelLocalTests L"cmdline: \"%s\"", cmdline.c_str())); VERIFY_ARE_EQUAL(L"--startingDirectory \"C:\\\\\" -- \"bar.exe\"", terminalArgs.ToCommandline()); } + + { + auto command = commands.Lookup(L"action8_tabTitleEscaping"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.ActionAndArgs()); + VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.ActionAndArgs().Action()); + const auto& realArgs = command.ActionAndArgs().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + Log::Comment(NoThrowString().Format( + L"cmdline: \"%s\"", cmdline.c_str())); + VERIFY_ARE_EQUAL(LR"-(--title "\\\"\;foo\\")-", terminalArgs.ToCommandline()); + } } } diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index a92609e70..5f472d72f 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -55,9 +55,7 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray, STARTUPINFOEX siEx{ 0 }; siEx.StartupInfo.cb = sizeof(STARTUPINFOEX); - // Append a "\." to the given path, so that this will work in "C:\" - auto path{ wil::str_printf(LR"-(%s\.)-", pszName.get()) }; - auto cmdline{ wil::str_printf(LR"-("%s" -d "%s")-", GetWtExePath().c_str(), path.c_str()) }; + auto cmdline{ wil::str_printf(LR"-("%s" -d %s)-", GetWtExePath().c_str(), QuoteAndEscapeCommandlineArg(pszName.get()).c_str()) }; RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW( nullptr, // lpApplicationName cmdline.data(), @@ -66,7 +64,7 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray, false, // bInheritHandles EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags nullptr, // lpEnvironment - path.data(), + pszName.get(), &siEx.StartupInfo, // lpStartupInfo &_piClient // lpProcessInformation )); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index dd30f3547..bf60db47e 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -38,6 +38,7 @@ #include "MultipleActionsArgs.g.cpp" #include +#include using namespace winrt::Microsoft::Terminal::Control; @@ -121,15 +122,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation if (!StartingDirectory().empty()) { - // If the directory ends in a '\', we need to add another one on so that the enclosing quote added - // afterwards isn't escaped - const auto trailingBackslashEscape = StartingDirectory().back() == L'\\' ? L"\\" : L""; - ss << fmt::format(L"--startingDirectory \"{}{}\" ", StartingDirectory(), trailingBackslashEscape); + ss << fmt::format(L"--startingDirectory {} ", QuoteAndEscapeCommandlineArg(StartingDirectory())); } if (!TabTitle().empty()) { - ss << fmt::format(L"--title \"{}\" ", TabTitle()); + ss << fmt::format(L"--title {} ", QuoteAndEscapeCommandlineArg(TabTitle())); } if (TabColor()) @@ -152,7 +150,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation if (!ColorScheme().empty()) { - ss << fmt::format(L"--colorScheme \"{}\" ", ColorScheme()); + ss << fmt::format(L"--colorScheme {} ", QuoteAndEscapeCommandlineArg(ColorScheme())); } if (!Commandline().empty()) diff --git a/src/cascadia/WinRTUtils/inc/WtExeUtils.h b/src/cascadia/WinRTUtils/inc/WtExeUtils.h index aed728db5..f4541594c 100644 --- a/src/cascadia/WinRTUtils/inc/WtExeUtils.h +++ b/src/cascadia/WinRTUtils/inc/WtExeUtils.h @@ -104,3 +104,40 @@ _TIL_INLINEPREFIX const std::wstring& GetWtExePath() }(); return exePath; } + +// Method Description: +// - Quotes and escapes the given string so that it can be used as a command-line arg. +// - e.g. given `\";foo\` will return `"\\\"\;foo\\"` so that the caller can construct a command-line +// using something such as `fmt::format(L"wt --title {}", QuoteAndQuoteAndEscapeCommandlineArg(TabTitle()))`. +// Arguments: +// - arg - the command-line argument to quote and escape. +// Return Value: +// - the quoted and escaped command-line argument. +_TIL_INLINEPREFIX std::wstring QuoteAndEscapeCommandlineArg(const std::wstring_view& arg) +{ + std::wstring out; + out.reserve(arg.size() + 2); + out.push_back(L'"'); + + size_t backslashes = 0; + for (const auto ch : arg) + { + if (ch == L'\\') + { + backslashes++; + } + else + { + if (ch == L';' || ch == L'"') + { + out.append(backslashes + 1, L'\\'); + } + backslashes = 0; + } + out.push_back(ch); + } + + out.append(backslashes, L'\\'); + out.push_back(L'"'); + return out; +} From 2be394f421b9b23a15e5573fb02b9c7a25768941 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 24 Sep 2021 18:21:27 +0200 Subject: [PATCH 11/35] Fix layering of fragment profiles (#11325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes layering of fragment profiles without an update key. The previous CascadiaSettings deserializer first assembled all builtin profiles and only then parsed the user's settings.json file. This meant that even though fragment profiles were added to `_allProfiles` unconditionally, they did get layered properly with user profiles regardless, as user profiles were always properly layered. The new CascadiaSettings approach since 168d28b was a direct translation of this approach but this is incorrect: As the new approach reads user profiles first, all inbox profiles, including fragments, must equally use proper layering, instead of adding profiles unconditionally. While this commit fixes the bug it maintains a regression: Duplicate fragment profile GUIDs will not be detected and instead fragments with identical GUID will all be added as parents to a single user profile. I considered to fix this regression, but felt that this new behavior is better than the old one, since a user often can't directly control installed fragments, and is unlikely to occur in practice. This simplifies the implementation. ## PR Checklist * [x] Closes #11323 * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * Fragment layering works ✔️ --- .../TerminalSettingsModel/CascadiaSettings.h | 1 + .../CascadiaSettingsSerialization.cpp | 41 +++++++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index e690f7208..697bdd14d 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -71,6 +71,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation gsl::span> _getNonUserOriginProfiles() const; void _parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings); void _appendProfile(winrt::com_ptr&& profile, ParsedSettings& settings); + static void _addParentProfile(const winrt::com_ptr& profile, ParsedSettings& settings); void _executeGenerator(const IDynamicProfileGenerator& generator); std::unordered_set _ignoredNamespaces; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index b704be14d..d664f4d8e 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -178,22 +178,7 @@ void SettingsLoader::MergeInboxIntoUserSettings() { for (const auto& profile : inboxSettings.profiles) { - if (const auto [it, inserted] = userSettings.profilesByGuid.emplace(profile->Guid(), profile); !inserted) - { - // If inserted is false, we got a matching user profile with identical GUID. - // --> The generated profile is a parent of the existing user profile. - it->second->InsertParent(profile); - } - else - { - // If inserted is true, then this is a generated profile that doesn't exist in the user's settings. - // While emplace() has already created an appropriate entry in .profilesByGuid, we still need to - // add it to .profiles (which is basically a sorted list of .profilesByGuid's values). - // - // When a user modifies a profile they shouldn't modify the (static/constant) - // inbox profile of course. That's why we need to call CreateChild here. - userSettings.profiles.emplace_back(CreateChild(profile)); - } + _addParentProfile(profile, userSettings); } } @@ -229,7 +214,7 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings() } else { - _appendProfile(CreateChild(fragmentProfile), userSettings); + _addParentProfile(fragmentProfile, userSettings); } } @@ -549,6 +534,28 @@ void SettingsLoader::_appendProfile(winrt::com_ptr&& profile, ParsedSet } } +// If the given ParsedSettings instance contains a profile with the given profile's GUID, +// the profile is added as a parent. Otherwise a new child profile is created. +void SettingsLoader::_addParentProfile(const winrt::com_ptr& profile, ParsedSettings& settings) +{ + if (const auto [it, inserted] = settings.profilesByGuid.emplace(profile->Guid(), profile); !inserted) + { + // If inserted is false, we got a matching user profile with identical GUID. + // --> The generated profile is a parent of the existing user profile. + it->second->InsertParent(profile); + } + else + { + // If inserted is true, then this is a generated profile that doesn't exist in the user's settings. + // While emplace() has already created an appropriate entry in .profilesByGuid, we still need to + // add it to .profiles (which is basically a sorted list of .profilesByGuid's values). + // + // When a user modifies a profile they shouldn't modify the (static/constant) + // inbox profile of course. That's why we need to call CreateChild here. + settings.profiles.emplace_back(CreateChild(profile)); + } +} + // As the name implies it executes a generator. // Generated profiles are added to .inboxSettings. Used by GenerateProfiles(). void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator) From 09d0ac768a632bb299acf1fddb797716f38b3ae3 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Mon, 27 Sep 2021 14:27:29 +0100 Subject: [PATCH 12/35] Add an enum-compatible bitset class. (#10492) ## Summary of the Pull Request This introduces a new TIL class that is equivalent in functionality to a `std::bitset`, but where the positions in the bitset are enum values. It also has a few additional methods allowing for setting and testing multiple positions at the same time. The idea is that this class could be used in place of the `WI_SetFlag` and `WI_IsFlagSet` macros when working with sets of flags. ## PR Checklist * [x] Closes #10432 * [x] CLA signed. * [x] Tests added/passed * [ ] Documentation updated. * [ ] Schema updated. * [x] I've discussed this with core contributors already. Issue number where discussion took place: #10432 ## Validation Steps Performed I've added a few unit tests that verify the behaviour of all the new methods that aren't part of `std::bitset`. I've also tried it out as a replacement for the `GridLines` enum used in the renderer, and confirmed that it has all the functionality needed to replace that cleanly. --- .github/actions/spelling/allow/apis.txt | 1 + src/inc/til.h | 1 + src/inc/til/enumset.h | 132 ++++++++++++++ src/til/ut_til/EnumSetTests.cpp | 169 ++++++++++++++++++ src/til/ut_til/til.unit.tests.vcxproj | 1 + src/til/ut_til/til.unit.tests.vcxproj.filters | 1 + 6 files changed, 305 insertions(+) create mode 100644 src/inc/til/enumset.h create mode 100644 src/til/ut_til/EnumSetTests.cpp diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 4b6fbe4f5..19f5fb487 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -25,6 +25,7 @@ DERR dlldata DONTADDTORECENT DWORDLONG +enumset environstrings EXPCMDFLAGS EXPCMDSTATE diff --git a/src/inc/til.h b/src/inc/til.h index 3202a5959..59e07cb8e 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -21,6 +21,7 @@ #include "til/replace.h" #include "til/string.h" #include "til/pmr.h" +#include "til/enumset.h" // Use keywords on TraceLogging providers to specify the category // of event that we are emitting for filtering purposes. diff --git a/src/inc/til/enumset.h b/src/inc/til/enumset.h new file mode 100644 index 000000000..7ff45f368 --- /dev/null +++ b/src/inc/til/enumset.h @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + // By design, this class hides several methods in the std::bitset class + // so they can be called with an enum parameter instead of a size_t, so + // we need to disable the "hides a non-virtual function" warning. +#pragma warning(push) +#pragma warning(disable : 26434) + + // til::enumset is a subclass of std::bitset, storing a fixed size array of + // boolean elements, the positions in the array being identified by values + // from a given enumerated type. By default it holds the same number of + // bits as a size_t value. + template::digits> + class enumset : public std::bitset + { + using _base = std::bitset; + + public: + using reference = typename _base::reference; + + enumset() = default; + + // Method Description: + // - Constructs a new bitset with the given list of positions set to true. + template...>>> + constexpr enumset(const Args... positions) noexcept : + _base((... | (1ULL << static_cast(positions)))) + { + } + + // Method Description: + // - Returns the value of the bit at the given position. + constexpr bool operator[](const Type pos) const + { + return _base::operator[](static_cast(pos)); + } + + // Method Description: + // - Returns a reference to the bit at the given position. + reference operator[](const Type pos) + { + return _base::operator[](static_cast(pos)); + } + + // Method Description: + // - Returns the value of the bit at the given position. + // Throws std::out_of_range if it is not a valid position + // in the bitset. + bool test(const Type pos) const + { + return _base::test(static_cast(pos)); + } + + // Method Description: + // - Returns true if any of the bits are set to true. + bool any() const noexcept + { + return _base::any(); + } + + // Method Description: + // - Returns true if any of the bits in the given positions are true. + template...>>> + bool any(const Args... positions) const noexcept + { + return (enumset{ positions... } & *this) != 0; + } + + // Method Description: + // - Returns true if all of the bits are set to true. + bool all() const noexcept + { + return _base::all(); + } + + // Method Description: + // - Returns true if all of the bits in the given positions are true. + template...>>> + bool all(const Args... positions) const noexcept + { + return (enumset{ positions... } & *this) == enumset{ positions... }; + } + + // Method Description: + // - Sets the bit in the given position to the specified value. + enumset& set(const Type pos, const bool val = true) + { + _base::set(static_cast(pos), val); + return *this; + } + + // Method Description: + // - Resets the bit in the given position to false. + enumset& reset(const Type pos) + { + _base::reset(static_cast(pos)); + return *this; + } + + // Method Description: + // - Flips the bit at the given position. + enumset& flip(const Type pos) + { + _base::flip(static_cast(pos)); + return *this; + } + + // Method Description: + // - Sets all of the bits in the given positions to true. + template + enumset& set_all(const Args... positions) + { + return (..., set(positions)); + } + + // Method Description: + // - Resets all of the bits in the given positions to false. + template + enumset& reset_all(const Args... positions) + { + return (..., reset(positions)); + } + }; +#pragma warning(pop) +} diff --git a/src/til/ut_til/EnumSetTests.cpp b/src/til/ut_til/EnumSetTests.cpp new file mode 100644 index 000000000..2a6e0449b --- /dev/null +++ b/src/til/ut_til/EnumSetTests.cpp @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" + +using namespace WEX::Logging; + +class EnumSetTests +{ + TEST_CLASS(EnumSetTests); + + TEST_METHOD(Constructors) + { + enum class Flags + { + Zero, + One, + Two, + Three, + Four + }; + + { + Log::Comment(L"Default constructor with no bits set"); + til::enumset flags; + VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong()); + } + + { + Log::Comment(L"Constructor with bit 3 set"); + til::enumset flags{ Flags::Three }; + VERIFY_ARE_EQUAL(0b01000u, flags.to_ulong()); + } + + { + Log::Comment(L"Constructor with bits 0, 2, and 4 set"); + til::enumset flags{ Flags::Zero, Flags::Two, Flags::Four }; + VERIFY_ARE_EQUAL(0b10101u, flags.to_ulong()); + } + } + + TEST_METHOD(SetResetFlipMethods) + { + enum class Flags + { + Zero, + One, + Two, + Three, + Four + }; + + Log::Comment(L"Start with no bits set"); + til::enumset flags; + VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong()); + + Log::Comment(L"Set bit 2 to true"); + flags.set(Flags::Two); + VERIFY_ARE_EQUAL(0b00100u, flags.to_ulong()); + + Log::Comment(L"Flip bit 4 to true"); + flags.flip(Flags::Four); + VERIFY_ARE_EQUAL(0b10100u, flags.to_ulong()); + + Log::Comment(L"Set bit 0 to true"); + flags.set(Flags::Zero); + VERIFY_ARE_EQUAL(0b10101u, flags.to_ulong()); + + Log::Comment(L"Reset bit 2 to false, leaving 0 and 4 true"); + flags.reset(Flags::Two); + VERIFY_ARE_EQUAL(0b10001u, flags.to_ulong()); + + Log::Comment(L"Set bit 0 to false, leaving 4 true"); + flags.set(Flags::Zero, false); + VERIFY_ARE_EQUAL(0b10000u, flags.to_ulong()); + + Log::Comment(L"Flip bit 4, leaving all bits false "); + flags.flip(Flags::Four); + VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong()); + + Log::Comment(L"Set bits 0, 3, and 2"); + flags.set_all(Flags::Zero, Flags::Three, Flags::Two); + VERIFY_ARE_EQUAL(0b01101u, flags.to_ulong()); + + Log::Comment(L"Reset bits 3, 4 (already reset), and 0, leaving 2 true"); + flags.reset_all(Flags::Three, Flags::Four, Flags::Zero); + VERIFY_ARE_EQUAL(0b00100u, flags.to_ulong()); + } + + TEST_METHOD(TestMethods) + { + enum class Flags + { + Zero, + One, + Two, + Three, + Four + }; + + Log::Comment(L"Start with bits 0, 2, and 4 set"); + til::enumset flags{ Flags::Zero, Flags::Two, Flags::Four }; + VERIFY_ARE_EQUAL(0b10101u, flags.to_ulong()); + + Log::Comment(L"Test bits 1 and 2 with the test method"); + VERIFY_IS_FALSE(flags.test(Flags::One)); + VERIFY_IS_TRUE(flags.test(Flags::Two)); + + Log::Comment(L"Test bit 3 and 4 with the array operator"); + VERIFY_IS_FALSE(flags[Flags::Three]); + VERIFY_IS_TRUE(flags[Flags::Four]); + + Log::Comment(L"Test if any bits are set"); + VERIFY_IS_TRUE(flags.any()); + Log::Comment(L"Test if either bit 1 or 3 are set"); + VERIFY_IS_FALSE(flags.any(Flags::One, Flags::Three)); + Log::Comment(L"Test if either bit 1 or 4 are set"); + VERIFY_IS_TRUE(flags.any(Flags::One, Flags::Four)); + + Log::Comment(L"Test if all bits are set"); + VERIFY_IS_FALSE(flags.all()); + Log::Comment(L"Test if both bits 0 and 4 are set"); + VERIFY_IS_TRUE(flags.all(Flags::Zero, Flags::Four)); + Log::Comment(L"Test if both bits 0 and 3 are set"); + VERIFY_IS_FALSE(flags.all(Flags::Zero, Flags::Three)); + } + + TEST_METHOD(ArrayReferenceOperator) + { + enum class Flags + { + Zero, + One, + Two, + Three, + Four + }; + + Log::Comment(L"Start with no bits set"); + til::enumset flags; + VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong()); + + Log::Comment(L"Test bit 3 reference is false"); + auto reference = flags[Flags::Three]; + VERIFY_IS_FALSE(reference); + VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong()); + + Log::Comment(L"Set bit 3 reference to true"); + flags.set(Flags::Three); + VERIFY_IS_TRUE(reference); + VERIFY_ARE_EQUAL(0b01000u, flags.to_ulong()); + + Log::Comment(L"Reset bit 3 reference to false"); + flags.reset(Flags::Three); + VERIFY_IS_FALSE(reference); + VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong()); + + Log::Comment(L"Flip bit 3 reference to true"); + reference.flip(); + VERIFY_IS_TRUE(reference); + VERIFY_ARE_EQUAL(0b01000u, flags.to_ulong()); + + Log::Comment(L"Flip bit 3 reference back to false"); + reference.flip(); + VERIFY_IS_FALSE(reference); + VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong()); + } +}; diff --git a/src/til/ut_til/til.unit.tests.vcxproj b/src/til/ut_til/til.unit.tests.vcxproj index 45e812076..48f9a8d3d 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj +++ b/src/til/ut_til/til.unit.tests.vcxproj @@ -17,6 +17,7 @@ + diff --git a/src/til/ut_til/til.unit.tests.vcxproj.filters b/src/til/ut_til/til.unit.tests.vcxproj.filters index 92003b075..2077c3d6c 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj.filters +++ b/src/til/ut_til/til.unit.tests.vcxproj.filters @@ -9,6 +9,7 @@ + From 5fa379e707d83655f3785140f8aadd269d177db1 Mon Sep 17 00:00:00 2001 From: snxx Date: Mon, 27 Sep 2021 18:27:59 +0500 Subject: [PATCH 13/35] .gitattributes: removed php display in the project (#11329) Since `*.inc` uses `cpp`, I changed the display via `.gitattributes`. --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index bf0e54a56..6aa281e5b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,8 @@ ############################################################################### * -text +*.inc linguist-language=cpp + ############################################################################### # Set default behavior for command prompt diff. # From 5542e727d024ddd88497a6e1ae0980f9836fe22f Mon Sep 17 00:00:00 2001 From: Sujal Gupta <55016909+heysujal@users.noreply.github.com> Date: Mon, 27 Sep 2021 20:53:28 +0530 Subject: [PATCH 14/35] fix typo (#11338) --- doc/building.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/building.md b/doc/building.md index b00d7c45f..5a5b46ff0 100644 --- a/doc/building.md +++ b/doc/building.md @@ -15,7 +15,7 @@ Import-Module .\tools\OpenConsole.psm1 Set-MsBuildDevEnvironment Get-Format ``` -After, go to Tools > Options > Text Editor > C++ > Formatting and checking "Use custom clang-format.exe file" in Visual Studio and choose the clang-format.exe in the repository at /packages/clang-format.win-x86.10.0.0/tools/clang-format.exe by clicking "browse" right under the check box. +After, go to Tools > Options > Text Editor > C++ > Formatting and check "Use custom clang-format.exe file" in Visual Studio and choose the clang-format.exe in the repository at /packages/clang-format.win-x86.10.0.0/tools/clang-format.exe by clicking "browse" right under the check box. ### Building in PowerShell From 2d583fc8601cec76631e0f6e29e032e92bd6eb98 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 27 Sep 2021 19:09:53 +0200 Subject: [PATCH 15/35] Fix fragments that update other profiles (#11343) `SettingsLoader::_parse` used to skip profiles which didn't have either a "guid" or "name" field, due to #9962. This is however wrong for fragment loading, as fragments can alternatively use an "updates" field instead of guid/name. `SettingsLoader::_parse` was updated to allow profiles with this alternative field during fragment loading. ## PR Checklist * [x] Closes #11331 * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * Wrote the following to `%LOCALAPPDATA%\Microsoft\Windows Terminal\Fragments\test\test.json`: ```json { "profiles": [ { "updates": "{574e775e-4f2a-5b96-ac1e-a2962a402336}", "background": "#FFD700" } ] } ``` --- .../CascadiaSettings.cpp | 8 ++- .../TerminalSettingsModel/CascadiaSettings.h | 3 +- .../CascadiaSettingsSerialization.cpp | 58 +++++++++---------- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index 61521ade2..d49ff4968 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -553,9 +553,11 @@ Model::Profile CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs& FindProfile(GlobalSettings().DefaultProfile()) : ProfileDefaults(); } - - // For compatibility with the stable version's behavior, return the default by GUID in all other cases. - return FindProfile(GlobalSettings().DefaultProfile()); + else + { + // For compatibility with the stable version's behavior, return the default by GUID in all other cases. + return FindProfile(GlobalSettings().DefaultProfile()); + } } // Method Description: diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 697bdd14d..6ee9d4f37 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -67,9 +67,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static void _rethrowSerializationExceptionWithLocationInfo(const JsonUtils::DeserializationError& e, const std::string_view& settingsString); static Json::Value _parseJSON(const std::string_view& content); static const Json::Value& _getJSONValue(const Json::Value& json, const std::string_view& key) noexcept; - static bool _isValidProfileObject(const Json::Value& profileJson); gsl::span> _getNonUserOriginProfiles() const; - void _parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings); + void _parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings, bool updatesKeyAllowed = false); void _appendProfile(winrt::com_ptr&& profile, ParsedSettings& settings); static void _addParentProfile(const winrt::com_ptr& profile, ParsedSettings& settings); void _executeGenerator(const IDynamicProfileGenerator& generator); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index d664f4d8e..431a54eff 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -201,7 +201,7 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings() try { const auto content = ReadUTF8File(fragmentExt.path()); - _parse(OriginTag::Fragment, source, content, fragmentSettings); + _parse(OriginTag::Fragment, source, content, fragmentSettings, true); for (const auto& fragmentProfile : fragmentSettings.profiles) { @@ -418,17 +418,6 @@ const Json::Value& SettingsLoader::_getJSONValue(const Json::Value& json, const return Json::Value::nullSingleton(); } -// Returns true if the given Json::Value looks like a profile. -// We introduced a bug (GH#9962, fixed in GH#9964) that would result in one or -// more nameless, guid-less profiles being emitted into the user's settings file. -// Those profiles would show up in the list as "Default" later. -bool SettingsLoader::_isValidProfileObject(const Json::Value& profileJson) -{ - return profileJson.isObject() && - (profileJson.isMember(NameKey.data(), NameKey.data() + NameKey.size()) || // has a name (can generate a guid) - profileJson.isMember(GuidKey.data(), GuidKey.data() + GuidKey.size())); // or has a guid -} - // We treat userSettings.profiles as an append-only array and will // append profiles into the userSettings as necessary in this function. // _userProfileCount stores the number of profiles that were in userJSON during construction. @@ -443,7 +432,7 @@ gsl::span> SettingsLoader::_getNonUserOriginProfil } // Parses the given JSON string ("content") and fills a ParsedSettings instance with it. -void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings) +void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings, bool updatesKeyAllowed) { const auto json = content.empty() ? Json::Value{ Json::ValueType::objectValue } : _parseJSON(content); const auto& profilesObject = _getJSONValue(json, ProfilesKey); @@ -492,28 +481,39 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source for (const auto& profileJson : profilesArray) { - if (_isValidProfileObject(profileJson)) + auto profile = Profile::FromJson(profileJson); + profile->Origin(origin); + + // The Guid() generation below depends on the value of Source(). + // --> Provide one if we got one. + if (!source.empty()) { - auto profile = Profile::FromJson(profileJson); - profile->Origin(origin); + profile->Source(source); + } - // The Guid() generation below depends on the value of Source(). - // --> Provide one if we got one. - if (!source.empty()) - { - profile->Source(source); - } - - // The Guid() getter generates one from Name() and Source() if none exists otherwise. - // We want to ensure that every profile has a GUID no matter what, not just to - // cache the value, but also to make them consistently identifiable later on. - if (!profile->HasGuid()) + // The Guid() getter generates one from Name() and Source() if none exists otherwise. + // We want to ensure that every profile has a GUID no matter what, not just to + // cache the value, but also to make them consistently identifiable later on. + if (!profile->HasGuid()) + { + if (profile->HasName()) { profile->Guid(profile->Guid()); } - - _appendProfile(std::move(profile), settings); + else if (!updatesKeyAllowed || profile->Updates() == winrt::guid{}) + { + // We introduced a bug (GH#9962, fixed in GH#9964) that would result in one or + // more nameless, guid-less profiles being emitted into the user's settings file. + // Those profiles would show up in the list as "Default" later. + // + // Fragments however can contain an alternative "updates" key, which works similar to the "guid". + // If updatesKeyAllowed is true (see FindFragmentsAndMergeIntoUserSettings) we permit + // such Guid-less, Name-less profiles as long as they have a valid Updates field. + continue; + } } + + _appendProfile(std::move(profile), settings); } } } From 75e2b5fae7e2f7caef3c634710cf7aced14ffd98 Mon Sep 17 00:00:00 2001 From: Schuyler Rosefield Date: Mon, 27 Sep 2021 17:18:39 -0400 Subject: [PATCH 16/35] Persist window layout cont. save multiple windows (#11083) ## Summary of the Pull Request Continuation of https://github.com/microsoft/terminal/pull/10972 to handle multiple windows, requires that to be merged first. ## References ## PR Checklist * [x] Also closes #766 * [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA * [ ] Tests added/passed * [ ] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx * [x] Schema updated. * [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx ## Detailed Description of the Pull Request / Additional comments Rough changelog: Normally saving is triggered to occur every 30s, or sooner if a window is created/closed. The existing behavior of saving on last close is maintained to bypass that throttling. The automatic saving allows for crash recovery. Additionally all window layouts will be saved upon taking the `quit` action. For loading we will check if we are the first window, that there are any saved layouts, and if the setting is enabled, and then depending on if we were given command line args or startup actions. - create a new window for each saved layout, or - take the first layout for our self and then a new window for each other layout. This also saves the layout when the quit action is taken. Misc changes - A -s,--saved argument was added to the command line to facilitate opening all of the windows with the right settings. This also means that while a terminal session is running you can do wt -s idx to open a copy of window idx. There isn't a stable ordering of which idx each window gets saved as (it is whatever the iteration order of _peasants is), so it is just a cute hack for now. - All position calculation has been moved up to AppHost this does mean we need to awkwardly pass around positions in a couple of unexpected places, but no solution was perfect. - Renamed "Open tabs from a previous session" to "Open windows from a previous session". (not reflected in video below) - Now save runtime tab color and window names - Only enabled for non-elevated windows - Add some change tracking to ApplicationState ## Validation Steps Performed ![output](https://user-images.githubusercontent.com/6185249/131163473-d649d204-a589-41ad-b9d9-c4c0528cb684.gif) --- .github/actions/spelling/allow/microsoft.txt | 1 + src/cascadia/Remoting/GetWindowLayoutArgs.cpp | 5 + src/cascadia/Remoting/GetWindowLayoutArgs.h | 32 ++++ .../Microsoft.Terminal.RemotingLib.vcxproj | 16 +- src/cascadia/Remoting/Monarch.cpp | 37 +++- src/cascadia/Remoting/Monarch.h | 11 +- src/cascadia/Remoting/Monarch.idl | 9 +- src/cascadia/Remoting/Peasant.cpp | 21 +++ src/cascadia/Remoting/Peasant.h | 4 + src/cascadia/Remoting/Peasant.idl | 7 + .../Remoting/QuitAllRequestedArgs.cpp | 5 + src/cascadia/Remoting/QuitAllRequestedArgs.h | 30 +++ src/cascadia/Remoting/WindowManager.cpp | 17 +- src/cascadia/Remoting/WindowManager.h | 4 +- src/cascadia/Remoting/WindowManager.idl | 5 +- .../TerminalApp/AppActionHandlers.cpp | 2 +- .../TerminalApp/AppCommandlineArgs.cpp | 11 ++ src/cascadia/TerminalApp/AppCommandlineArgs.h | 2 + src/cascadia/TerminalApp/AppLogic.cpp | 87 +++++++-- src/cascadia/TerminalApp/AppLogic.h | 11 +- src/cascadia/TerminalApp/AppLogic.idl | 10 +- src/cascadia/TerminalApp/Pane.cpp | 7 +- .../Resources/en-US/Resources.resw | 3 + src/cascadia/TerminalApp/TabManagement.cpp | 4 +- src/cascadia/TerminalApp/TerminalPage.cpp | 126 +++++++------ src/cascadia/TerminalApp/TerminalPage.h | 7 +- src/cascadia/TerminalApp/TerminalPage.idl | 2 +- src/cascadia/TerminalApp/TerminalTab.cpp | 23 ++- .../Resources/en-US/Resources.resw | 4 +- .../TerminalSettingsModel/ActionArgs.h | 6 + .../TerminalSettingsModel/ActionArgs.idl | 2 + .../ApplicationState.cpp | 100 +++++++--- .../TerminalSettingsModel/ApplicationState.h | 12 +- .../ApplicationState.idl | 3 + .../TerminalSettingsModel/FileUtils.cpp | 77 ++++++++ .../TerminalSettingsModel/FileUtils.h | 30 +++ .../UnitTests_Remoting/RemotingTests.cpp | 2 + src/cascadia/WindowsTerminal/AppHost.cpp | 171 +++++++++++++++++- src/cascadia/WindowsTerminal/AppHost.h | 10 +- src/cascadia/WindowsTerminal/BaseWindow.h | 5 + 40 files changed, 788 insertions(+), 133 deletions(-) create mode 100644 src/cascadia/Remoting/GetWindowLayoutArgs.cpp create mode 100644 src/cascadia/Remoting/GetWindowLayoutArgs.h create mode 100644 src/cascadia/Remoting/QuitAllRequestedArgs.cpp create mode 100644 src/cascadia/Remoting/QuitAllRequestedArgs.h diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index 87d7a3d8c..c82254bbb 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -25,6 +25,7 @@ DWINRT enablewttlogging Intelli LKG +LOCKFILE Lxss mfcribbon microsoft diff --git a/src/cascadia/Remoting/GetWindowLayoutArgs.cpp b/src/cascadia/Remoting/GetWindowLayoutArgs.cpp new file mode 100644 index 000000000..f2cc01df4 --- /dev/null +++ b/src/cascadia/Remoting/GetWindowLayoutArgs.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "pch.h" +#include "GetWindowLayoutArgs.h" +#include "GetWindowLayoutArgs.g.cpp" diff --git a/src/cascadia/Remoting/GetWindowLayoutArgs.h b/src/cascadia/Remoting/GetWindowLayoutArgs.h new file mode 100644 index 000000000..06706f60b --- /dev/null +++ b/src/cascadia/Remoting/GetWindowLayoutArgs.h @@ -0,0 +1,32 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- GetWindowLayoutArgs.h + +Abstract: +- This is a helper class for getting the window layout from a peasant. + Depending on if we are running on the monarch or on a peasant we might need + to switch what thread we are executing on. This gives us the option of + either returning the json result synchronously, or as a promise. +--*/ + +#pragma once + +#include "GetWindowLayoutArgs.g.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::Remoting::implementation +{ + struct GetWindowLayoutArgs : public GetWindowLayoutArgsT + { + WINRT_PROPERTY(winrt::hstring, WindowLayoutJson, L""); + WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncOperation, WindowLayoutJsonAsync, nullptr) + }; +} + +namespace winrt::Microsoft::Terminal::Remoting::factory_implementation +{ + BASIC_FACTORY(GetWindowLayoutArgs); +} diff --git a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj index a2d6e8ae6..517c2f56f 100644 --- a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj +++ b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj @@ -12,7 +12,6 @@ - @@ -36,6 +35,12 @@ Peasant.idl + + Peasant.idl + + + Monarch.idl + @@ -71,6 +76,12 @@ Peasant.idl + + Peasant.idl + + + Monarch.idl + Create @@ -128,6 +139,5 @@ - - + \ No newline at end of file diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index c16eb52e9..eed247ab4 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -6,6 +6,7 @@ #include "Monarch.h" #include "CommandlineArgs.h" #include "FindTargetWindowArgs.h" +#include "QuitAllRequestedArgs.h" #include "ProposeCommandlineResult.h" #include "Monarch.g.cpp" @@ -135,12 +136,18 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // - used // Return Value: // - - void Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) + winrt::fire_and_forget Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*args*/) { // Let the process hosting the monarch run any needed logic before // closing all windows. - _QuitAllRequestedHandlers(*this, nullptr); + auto args = winrt::make_self(); + _QuitAllRequestedHandlers(*this, *args); + + if (const auto action = args->BeforeQuitAllAction()) + { + co_await action; + } _quitting.store(true); // Tell all peasants to exit. @@ -994,4 +1001,28 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _forEachPeasant(func, onError); } + + // Method Description: + // - Ask all peasants to return their window layout as json + // Arguments: + // - + // Return Value: + // - The collection of window layouts from each peasant. + Windows::Foundation::Collections::IVector Monarch::GetAllWindowLayouts() + { + std::vector vec; + auto callback = [&](const auto& /*id*/, const auto& p) { + vec.emplace_back(p.GetWindowLayout()); + }; + auto onError = [](auto&& id) { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_GetAllWindowLayouts_Failed", + TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not get a window layout from"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + }; + _forEachPeasant(callback, onError); + + return winrt::single_threaded_vector(std::move(vec)); + } } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 7ec904499..b965d1d2a 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -59,13 +59,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void SummonAllWindows(); bool DoesQuakeWindowExist(); Windows::Foundation::Collections::IVectorView GetPeasantInfos(); + Windows::Foundation::Collections::IVector GetAllWindowLayouts(); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); private: uint64_t _ourPID; @@ -103,8 +104,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void _renameRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args); - void _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); + winrt::fire_and_forget _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& args); // Method Description: // - Helper for doing something on each and every peasant. @@ -177,6 +178,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } _clearOldMruEntries(peasantsToErase); + + // A peasant died, let the app host know that the number of + // windows has changed. + _WindowClosedHandlers(nullptr, nullptr); } } diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index 4eb77695a..f60b3997a 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -31,6 +31,12 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.IReference WindowID; } + [default_interface] runtimeclass QuitAllRequestedArgs + { + QuitAllRequestedArgs(); + Windows.Foundation.IAsyncAction BeforeQuitAllAction; + } + struct PeasantInfo { UInt64 Id; @@ -52,12 +58,13 @@ namespace Microsoft.Terminal.Remoting void SummonAllWindows(); Boolean DoesQuakeWindowExist(); Windows.Foundation.Collections.IVectorView GetPeasantInfos { get; }; + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; - event Windows.Foundation.TypedEventHandler QuitAllRequested; + event Windows.Foundation.TypedEventHandler QuitAllRequested; }; } diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp index a8cb749d6..46fd7ce2e 100644 --- a/src/cascadia/Remoting/Peasant.cpp +++ b/src/cascadia/Remoting/Peasant.cpp @@ -5,6 +5,7 @@ #include "Peasant.h" #include "CommandlineArgs.h" #include "SummonWindowBehavior.h" +#include "GetWindowLayoutArgs.h" #include "Peasant.g.cpp" #include "../../types/inc/utils.hpp" @@ -289,4 +290,24 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } + + // Method Description: + // - Request and return the window layout from the current TerminalPage + // Arguments: + // - + // Return Value: + // - the window layout as a json string + hstring Peasant::GetWindowLayout() + { + auto args = winrt::make_self(); + _GetWindowLayoutRequestedHandlers(nullptr, *args); + if (const auto op = args->WindowLayoutJsonAsync()) + { + // This will fail if called on the UI thread, so the monarch should + // never set WindowLayoutJsonAsync. + auto str = op.get(); + return str; + } + return args->WindowLayoutJson(); + } } diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h index f6f884491..fdb20d942 100644 --- a/src/cascadia/Remoting/Peasant.h +++ b/src/cascadia/Remoting/Peasant.h @@ -36,6 +36,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs(); winrt::Microsoft::Terminal::Remoting::CommandlineArgs InitialArgs(); + + winrt::hstring GetWindowLayout(); + WINRT_PROPERTY(winrt::hstring, WindowName); WINRT_PROPERTY(winrt::hstring, ActiveTabTitle); @@ -49,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); private: Peasant(const uint64_t testPID); diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index 80e24cb2c..ec87c8518 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -30,6 +30,11 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.DateTime ActivatedTime { get; }; }; + [default_interface] runtimeclass GetWindowLayoutArgs { + GetWindowLayoutArgs(); + String WindowLayoutJson; + Windows.Foundation.IAsyncOperation WindowLayoutJsonAsync; + } enum MonitorBehavior { @@ -69,6 +74,7 @@ namespace Microsoft.Terminal.Remoting void RequestHideNotificationIcon(); void RequestQuitAll(); void Quit(); + String GetWindowLayout(); event Windows.Foundation.TypedEventHandler WindowActivated; event Windows.Foundation.TypedEventHandler ExecuteCommandlineRequested; @@ -78,6 +84,7 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler SummonRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; + event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; event Windows.Foundation.TypedEventHandler QuitAllRequested; event Windows.Foundation.TypedEventHandler QuitRequested; }; diff --git a/src/cascadia/Remoting/QuitAllRequestedArgs.cpp b/src/cascadia/Remoting/QuitAllRequestedArgs.cpp new file mode 100644 index 000000000..ed5c39dcf --- /dev/null +++ b/src/cascadia/Remoting/QuitAllRequestedArgs.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "pch.h" +#include "QuitAllRequestedArgs.h" +#include "QuitAllRequestedArgs.g.cpp" diff --git a/src/cascadia/Remoting/QuitAllRequestedArgs.h b/src/cascadia/Remoting/QuitAllRequestedArgs.h new file mode 100644 index 000000000..8c9c26fd2 --- /dev/null +++ b/src/cascadia/Remoting/QuitAllRequestedArgs.h @@ -0,0 +1,30 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- QuitAllRequestedArgs.h + +Abstract: +- This is a helper class for allowing the monarch to run code before telling all + peasants to quit. This way the monarch can raise an event and get back a future + to wait for before continuing. +--*/ + +#pragma once + +#include "QuitAllRequestedArgs.g.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::Remoting::implementation +{ + struct QuitAllRequestedArgs : public QuitAllRequestedArgsT + { + WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncAction, BeforeQuitAllAction, nullptr) + }; +} + +namespace winrt::Microsoft::Terminal::Remoting::factory_implementation +{ + BASIC_FACTORY(QuitAllRequestedArgs); +} diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 9a71fcef0..4cafee145 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -271,7 +271,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); _monarch.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequestedHandlers(*this, nullptr); }); _monarch.HideNotificationIconRequested([this](auto&&, auto&&) { _HideNotificationIconRequestedHandlers(*this, nullptr); }); - _monarch.QuitAllRequested([this](auto&&, auto&&) { _QuitAllRequestedHandlers(*this, nullptr); }); + _monarch.QuitAllRequested({ get_weak(), &WindowManager::_QuitAllRequestedHandlers }); _BecameMonarchHandlers(*this, nullptr); } @@ -318,6 +318,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } + _peasant.GetWindowLayoutRequested({ get_weak(), &WindowManager::_GetWindowLayoutRequestedHandlers }); + TraceLoggingWrite(g_hRemotingProvider, "WindowManager_CreateOurPeasant", TraceLoggingUInt64(_peasant.GetID(), "peasantID", "The ID of our new peasant"), @@ -610,4 +612,17 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { winrt::get_self(_peasant)->ActiveTabTitle(title); } + + Windows::Foundation::Collections::IVector WindowManager::GetAllWindowLayouts() + { + if (_monarch) + { + try + { + return _monarch.GetAllWindowLayouts(); + } + CATCH_LOG() + } + return nullptr; + } } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 3d2eaf6c7..379038750 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -50,6 +50,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::fire_and_forget RequestQuitAll(); bool DoesQuakeWindowExist(); void UpdateActiveTabTitle(winrt::hstring title); + Windows::Foundation::Collections::IVector GetAllWindowLayouts(); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); @@ -57,7 +58,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); private: bool _shouldCreateWindow{ false }; diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index cf15fbb42..2fdfd7e34 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -16,6 +16,8 @@ namespace Microsoft.Terminal.Remoting void SummonAllWindows(); void RequestShowNotificationIcon(); void RequestHideNotificationIcon(); + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); + UInt64 GetNumberOfPeasants(); void RequestQuitAll(); void UpdateActiveTabTitle(String title); @@ -25,8 +27,9 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler BecameMonarch; event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; + event Windows.Foundation.TypedEventHandler QuitAllRequested; + event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; - event Windows.Foundation.TypedEventHandler QuitAllRequested; }; } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 1ef4c0c8a..d99726b39 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -78,7 +78,7 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleCloseWindow(const IInspectable& /*sender*/, const ActionEventArgs& args) { - CloseWindow(false); + _CloseRequestedHandlers(nullptr, nullptr); args.Handled(true); } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 7f2f919a4..3baad1aa0 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -187,6 +187,10 @@ void AppCommandlineArgs::_buildParser() _windowTarget, RS_A(L"CmdWindowTargetArgDesc")); + _app.add_option("-s,--saved", + _loadPersistedLayoutIdx, + RS_A(L"CmdSavedLayoutArgDesc")); + // Subcommands _buildNewTabParser(); _buildSplitPaneParser(); @@ -700,6 +704,7 @@ void AppCommandlineArgs::_resetStateToDefault() _swapPaneDirection = FocusDirection::None; _focusPaneTarget = -1; + _loadPersistedLayoutIdx = -1; // DON'T clear _launchMode here! This will get called once for every // subcommand, so we don't want `wt -F new-tab ; split-pane` clearing out @@ -915,6 +920,12 @@ void AppCommandlineArgs::ValidateStartupCommands() } } } +std::optional AppCommandlineArgs::GetPersistedLayoutIdx() const noexcept +{ + return _loadPersistedLayoutIdx >= 0 ? + std::optional{ static_cast(_loadPersistedLayoutIdx) } : + std::nullopt; +} std::optional AppCommandlineArgs::GetLaunchMode() const noexcept { diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 598c3b8ed..de076ec99 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -39,6 +39,7 @@ public: const std::string& GetExitMessage(); bool ShouldExitEarly() const noexcept; + std::optional GetPersistedLayoutIdx() const noexcept; std::optional GetLaunchMode() const noexcept; int ParseArgs(const winrt::Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); @@ -123,6 +124,7 @@ private: std::string _exitMessage; bool _shouldExitEarly{ false }; + int _loadPersistedLayoutIdx{}; std::string _windowTarget{}; // Are you adding more args or attributes here? If they are not reset in _resetStateToDefault, make sure to reset them in FullResetState diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 08032f1c8..72f25b83e 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -600,13 +600,11 @@ namespace winrt::TerminalApp::implementation winrt::Windows::Foundation::Size proposedSize{}; const float scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); - if (_root->ShouldUsePersistedLayout(_settings)) + if (const auto layout = _root->LoadPersistedLayout(_settings)) { - const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialSize()) + if (layout.InitialSize()) { - proposedSize = layouts.GetAt(0).InitialSize().Value(); + proposedSize = layout.InitialSize().Value(); // The size is saved as a non-scaled real pixel size, // so we need to scale it appropriately. proposedSize.Height = proposedSize.Height * scale; @@ -704,13 +702,11 @@ namespace winrt::TerminalApp::implementation auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; - if (_root->ShouldUsePersistedLayout(_settings)) + if (const auto layout = _root->LoadPersistedLayout(_settings)) { - const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialPosition()) + if (layout.InitialPosition()) { - initialPosition = layouts.GetAt(0).InitialPosition().Value(); + initialPosition = layout.InitialPosition().Value(); } } @@ -1151,10 +1147,22 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - - void AppLogic::WindowCloseButtonClicked() + void AppLogic::CloseWindow(LaunchPosition pos) { if (_root) { + // If persisted layout is enabled and we are the last window closing + // we should save our state. + if (_root->ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(pos); + const auto state = ApplicationState::SharedInstance(); + state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); + } + } + _root->CloseWindow(false); } } @@ -1168,6 +1176,16 @@ namespace winrt::TerminalApp::implementation return {}; } + bool AppLogic::HasCommandlineArguments() const noexcept + { + return _hasCommandLineArguments; + } + + bool AppLogic::HasSettingsStartupActions() const noexcept + { + return _hasSettingsStartupActions; + } + // Method Description: // - Sets the initial commandline to process on startup, and attempts to // parse it. Commands will be parsed into a list of ShortcutActions that @@ -1191,6 +1209,10 @@ namespace winrt::TerminalApp::implementation // then it contains only the executable name and no other arguments. _hasCommandLineArguments = args.size() > 1; _appArgs.ValidateStartupCommands(); + if (const auto idx = _appArgs.GetPersistedLayoutIdx()) + { + _root->SetPersistedLayoutIdx(idx.value()); + } _root->SetStartupActions(_appArgs.GetStartupActions()); // Check if we were started as a COM server for inbound connections of console sessions @@ -1428,6 +1450,40 @@ namespace winrt::TerminalApp::implementation return _settings.GlobalSettings().ActionMap().GlobalHotkeys(); } + bool AppLogic::ShouldUsePersistedLayout() + { + return _root != nullptr ? _root->ShouldUsePersistedLayout(_settings) : false; + } + + void AppLogic::SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts) + { + std::vector converted; + converted.reserve(layouts.Size()); + + for (const auto& json : layouts) + { + if (json != L"") + { + converted.emplace_back(WindowLayout::FromJson(json)); + } + } + + ApplicationState::SharedInstance().PersistedWindowLayouts(winrt::single_threaded_vector(std::move(converted))); + } + + hstring AppLogic::GetWindowLayoutJson(LaunchPosition position) + { + if (_root != nullptr) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(position); + return WindowLayout::ToJson(layout); + } + } + return L""; + } + void AppLogic::IdentifyWindow() { if (_root) @@ -1459,8 +1515,17 @@ namespace winrt::TerminalApp::implementation } } + void AppLogic::SetPersistedLayoutIdx(const uint32_t idx) + { + if (_root) + { + _root->SetPersistedLayoutIdx(idx); + } + } + void AppLogic::SetNumberOfOpenWindows(const uint64_t num) { + _numOpenWindows = num; if (_root) { _root->SetNumberOfOpenWindows(num); diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 08dc77007..17bd61aaa 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -55,6 +55,8 @@ namespace winrt::TerminalApp::implementation void Quit(); + bool HasCommandlineArguments() const noexcept; + bool HasSettingsStartupActions() const noexcept; int32_t SetStartupCommandline(array_view actions); int32_t ExecuteCommandline(array_view actions, const winrt::hstring& cwd); TerminalApp::FindTargetWindowResult FindTargetWindow(array_view actions); @@ -65,12 +67,16 @@ namespace winrt::TerminalApp::implementation bool Fullscreen() const; bool AlwaysOnTop() const; + bool ShouldUsePersistedLayout(); + hstring GetWindowLayoutJson(Microsoft::Terminal::Settings::Model::LaunchPosition position); + void SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts); void IdentifyWindow(); void RenameFailed(); winrt::hstring WindowName(); void WindowName(const winrt::hstring& name); uint64_t WindowId(); void WindowId(const uint64_t& id); + void SetPersistedLayoutIdx(const uint32_t idx); void SetNumberOfOpenWindows(const uint64_t num); bool IsQuakeWindow() const noexcept; @@ -91,7 +97,7 @@ namespace winrt::TerminalApp::implementation void TitlebarClicked(); bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); - void WindowCloseButtonClicked(); + void CloseWindow(Microsoft::Terminal::Settings::Model::LaunchPosition position); winrt::TerminalApp::TaskbarState TaskbarState(); @@ -123,6 +129,8 @@ namespace winrt::TerminalApp::implementation HRESULT _settingsLoadedResult = S_OK; bool _loadedInitialSettings = false; + uint64_t _numOpenWindows{ 0 }; + std::shared_mutex _dialogLock; ::TerminalApp::AppCommandlineArgs _appArgs; @@ -175,6 +183,7 @@ namespace winrt::TerminalApp::implementation FORWARDED_TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs, _root, RenameWindowRequested); FORWARDED_TYPED_EVENT(IsQuakeWindowChanged, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IsQuakeWindowChanged); FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested); + FORWARDED_TYPED_EVENT(CloseRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, CloseRequested); FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu); FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index a600891c4..cfe93321a 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -34,6 +34,8 @@ namespace TerminalApp void RunAsUwp(); Boolean IsElevated(); + Boolean HasCommandlineArguments(); + Boolean HasSettingsStartupActions(); Int32 SetStartupCommandline(String[] commands); Int32 ExecuteCommandline(String[] commands, String cwd); String ParseCommandlineMessage { get; }; @@ -55,6 +57,7 @@ namespace TerminalApp void IdentifyWindow(); String WindowName; UInt64 WindowId; + void SetPersistedLayoutIdx(UInt32 idx); void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); @@ -69,10 +72,14 @@ namespace TerminalApp Boolean GetInitialAlwaysOnTop(); Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension); void TitlebarClicked(); - void WindowCloseButtonClicked(); + void CloseWindow(Microsoft.Terminal.Settings.Model.LaunchPosition position); TaskbarState TaskbarState{ get; }; + Boolean ShouldUsePersistedLayout(); + String GetWindowLayoutJson(Microsoft.Terminal.Settings.Model.LaunchPosition position); + void SaveWindowLayoutJsons(Windows.Foundation.Collections.IVector layouts); + Boolean GetMinimizeToNotificationArea(); Boolean GetAlwaysShowNotificationIcon(); Boolean GetShowTitleInTitlebar(); @@ -99,6 +106,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler SettingsChanged; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler CloseRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; event Windows.Foundation.TypedEventHandler QuitRequested; } diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 0c5705a7b..310d7ca47 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -106,7 +106,12 @@ NewTerminalArgs Pane::GetTerminalArgsForPane() const if (controlSettings.AppliedColorScheme()) { auto name = controlSettings.AppliedColorScheme().Name(); - args.ColorScheme(name); + // Only save the color scheme if it is different than the profile color + // scheme to not override any other profile appearance choices. + if (_profile.DefaultAppearance().ColorSchemeName() != name) + { + args.ColorScheme(name); + } } return args; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 421816f58..272fa9df6 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -381,6 +381,9 @@ Launch the window in focus mode + + This parameter is an internal implementation detail and should not be used. + Specify a terminal window to run the given commandline in. "0" always refers to the current window. diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index f57e3fe9b..e5d1cdc46 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -501,7 +501,9 @@ namespace winrt::TerminalApp::implementation { // If we are supposed to save state, make sure we clear it out // if the user manually closed all tabs. - if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings)) + // Do this only if we are the last window; the monarch will notice + // we are missing and remove us that way otherwise. + if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1) { auto state = ApplicationState::SharedInstance(); state.PersistedWindowLayouts(nullptr); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 74606ab4f..25140362d 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -298,10 +298,37 @@ namespace winrt::TerminalApp::implementation // - true if the ApplicationState should be used. bool TerminalPage::ShouldUsePersistedLayout(CascadiaSettings& settings) const { - // If the setting is enabled, and we are the only window. + // GH#5000 Until there is a separate state file for elevated sessions we should just not + // save at all while in an elevated window. return Feature_PersistedWindowLayout::IsEnabled() && - settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout && - _numOpenWindows == 1; + !IsElevated() && + settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout; + } + + // Method Description; + // - Checks if the current window is configured to load a particular layout + // Arguments: + // - settings: The settings to use as this may be called before the page is + // fully initialized. + // Return Value: + // - non-null if there is a particular saved layout to use + std::optional TerminalPage::LoadPersistedLayoutIdx(CascadiaSettings& settings) const + { + return ShouldUsePersistedLayout(settings) ? _loadFromPersistedLayoutIdx : std::nullopt; + } + + WindowLayout TerminalPage::LoadPersistedLayout(CascadiaSettings& settings) const + { + if (const auto idx = LoadPersistedLayoutIdx(settings)) + { + const auto i = idx.value(); + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (layouts && layouts.Size() > i) + { + return layouts.GetAt(i); + } + } + return nullptr; } winrt::fire_and_forget TerminalPage::NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e) @@ -387,30 +414,13 @@ namespace winrt::TerminalApp::implementation { _startupState = StartupState::InStartup; - // If the user selected to save their tab layout, we are the first - // window opened, and wt was not run with any other arguments, then - // we should use the saved settings. - auto firstActionIsDefault = [](ActionAndArgs action) { - if (action.Action() != ShortcutAction::NewTab) - { - return false; - } - - // If no commands were given, we will have default args - if (const auto args = action.Args().try_as()) - { - NewTerminalArgs defaultArgs{}; - return args.TerminalArgs() == nullptr || args.TerminalArgs().Equals(defaultArgs); - } - - return false; - }; - if (ShouldUsePersistedLayout(_settings) && _startupActions.Size() == 1 && firstActionIsDefault(_startupActions.GetAt(0))) + // If we are provided with an index, the cases where we have + // commandline args and startup actions are already handled. + if (const auto layout = LoadPersistedLayout(_settings)) { - auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).TabLayout() && layouts.GetAt(0).TabLayout().Size() > 0) + if (layout.TabLayout().Size() > 0) { - _startupActions = layouts.GetAt(0).TabLayout(); + _startupActions = layout.TabLayout(); } } @@ -1289,12 +1299,19 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Saves the window position and tab layout to the application state + // - This does not create the InitialPosition field, that needs to be + // added externally. // Arguments: // - // Return Value: - // - - void TerminalPage::PersistWindowLayout() + // - the window layout + WindowLayout TerminalPage::GetWindowLayout() { + if (_startupState != StartupState::Initialized) + { + return nullptr; + } + std::vector actions; for (auto tab : _tabs) @@ -1302,7 +1319,7 @@ namespace winrt::TerminalApp::implementation if (auto terminalTab = _GetTerminalTabImpl(tab)) { auto tabActions = terminalTab->BuildStartupActions(); - actions.insert(actions.end(), tabActions.begin(), tabActions.end()); + actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); } else if (tab.try_as()) { @@ -1311,7 +1328,7 @@ namespace winrt::TerminalApp::implementation OpenSettingsArgs args{ SettingsTarget::SettingsUI }; action.Args(args); - actions.push_back(action); + actions.emplace_back(std::move(action)); } } @@ -1324,7 +1341,18 @@ namespace winrt::TerminalApp::implementation SwitchToTabArgs switchToTabArgs{ idx.value() }; action.Args(switchToTabArgs); - actions.push_back(action); + actions.emplace_back(std::move(action)); + } + + // If the user set a custom name, save it + if (_WindowName != L"") + { + ActionAndArgs action; + action.Action(ShortcutAction::RenameWindow); + RenameWindowArgs args{ _WindowName }; + action.Args(args); + + actions.emplace_back(std::move(action)); } WindowLayout layout{}; @@ -1337,33 +1365,7 @@ namespace winrt::TerminalApp::implementation layout.InitialSize(windowSize); - if (_hostingHwnd) - { - // Get the position of the current window. This includes the - // non-client already. - RECT window{}; - GetWindowRect(_hostingHwnd.value(), &window); - - // We want to remove the non-client area so calculate that. - // We don't have access to the (NonClient)IslandWindow directly so - // just replicate the logic. - const auto windowStyle = static_cast(GetWindowLong(_hostingHwnd.value(), GWL_STYLE)); - - auto dpi = GetDpiForWindow(_hostingHwnd.value()); - RECT nonClientArea{}; - LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&nonClientArea, windowStyle, false, 0, dpi)); - - // The nonClientArea adjustment is negative, so subtract that out. - // This way we save the user-visible location of the terminal. - LaunchPosition pos{}; - pos.X = window.left - nonClientArea.left; - pos.Y = window.top; - - layout.InitialPosition(pos); - } - - auto state = ApplicationState::SharedInstance(); - state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); + return layout; } // Method Description: @@ -1392,8 +1394,9 @@ namespace winrt::TerminalApp::implementation if (ShouldUsePersistedLayout(_settings)) { - PersistWindowLayout(); - // don't delete the ApplicationState when all of the tabs are removed. + // Don't delete the ApplicationState when all of the tabs are removed. + // If there is still a monarch living they will get the event that + // a window closed and trigger a new save without this window. _maintainStateOnTabClose = true; } @@ -3106,6 +3109,11 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::SetPersistedLayoutIdx(const uint32_t idx) + { + _loadFromPersistedLayoutIdx = idx; + } + void TerminalPage::SetNumberOfOpenWindows(const uint64_t num) { _numOpenWindows = num; diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index a9ff78959..8f776a2d7 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -59,6 +59,9 @@ namespace winrt::TerminalApp::implementation void Create(); bool ShouldUsePersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + std::optional LoadPersistedLayoutIdx(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + winrt::Microsoft::Terminal::Settings::Model::WindowLayout LoadPersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout(); winrt::fire_and_forget NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e); @@ -82,7 +85,6 @@ namespace winrt::TerminalApp::implementation bool AlwaysOnTop() const; void SetStartupActions(std::vector& actions); - void PersistWindowLayout(); void SetInboundListener(bool isEmbedding); static std::vector ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); @@ -111,6 +113,7 @@ namespace winrt::TerminalApp::implementation void WindowId(const uint64_t& value); void SetNumberOfOpenWindows(const uint64_t value); + void SetPersistedLayoutIdx(const uint32_t value); winrt::hstring WindowIdForDisplay() const noexcept; winrt::hstring WindowNameForDisplay() const noexcept; @@ -133,6 +136,7 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs); TYPED_EVENT(IsQuakeWindowChanged, IInspectable, IInspectable); TYPED_EVENT(SummonWindowRequested, IInspectable, IInspectable); + TYPED_EVENT(CloseRequested, IInspectable, IInspectable); TYPED_EVENT(OpenSystemMenu, IInspectable, IInspectable); TYPED_EVENT(QuitRequested, IInspectable, IInspectable); @@ -166,6 +170,7 @@ namespace winrt::TerminalApp::implementation bool _isAlwaysOnTop{ false }; winrt::hstring _WindowName{}; uint64_t _WindowId{ 0 }; + std::optional _loadFromPersistedLayoutIdx{}; uint64_t _numOpenWindows{ 0 }; bool _maintainStateOnTabClose{ false }; diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index dd2777add..979483d0a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -33,7 +33,6 @@ namespace TerminalApp UInt64 WindowId; String WindowNameForDisplay { get; }; String WindowIdForDisplay { get; }; - void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); @@ -58,6 +57,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RenameWindowRequested; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler CloseRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; } } diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 53c75eb6f..afb637810 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -450,12 +450,25 @@ namespace winrt::TerminalApp::implementation // 1 for the child after the first split. auto state = _rootPane->BuildStartupActions(0, 1); - ActionAndArgs newTabAction{}; - newTabAction.Action(ShortcutAction::NewTab); - NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() }; - newTabAction.Args(newTabArgs); + { + ActionAndArgs newTabAction{}; + newTabAction.Action(ShortcutAction::NewTab); + NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() }; + newTabAction.Args(newTabArgs); - state.args.emplace(state.args.begin(), std::move(newTabAction)); + state.args.emplace(state.args.begin(), std::move(newTabAction)); + } + + if (_runtimeTabColor) + { + ActionAndArgs setColorAction{}; + setColorAction.Action(ShortcutAction::SetTabColor); + + SetTabColorArgs setColorArgs{ _runtimeTabColor.value() }; + setColorAction.Args(setColorArgs); + + state.args.emplace_back(std::move(setColorAction)); + } // If we only have one arg, we only have 1 pane so we don't need any // special focus logic diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index a21ae08e3..bf0e85ee8 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -304,8 +304,8 @@ An option to choose from for the "First window preference" setting. Open the default profile. - Open tabs from a previous session - An option to choose from for the "First window preference" setting. Reopen the layout from the last session. + Open windows from a previous session + An option to choose from for the "First window preference" setting. Reopen the layouts from the last session. Launch mode diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 94f7cebea..74315c2ab 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -864,6 +864,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct SetTabColorArgs : public SetTabColorArgsT { SetTabColorArgs() = default; + SetTabColorArgs(Windows::UI::Color tabColor) : + _TabColor{ tabColor } {} ACTION_ARG(Windows::Foundation::IReference, TabColor, nullptr); static constexpr std::string_view ColorKey{ "color" }; @@ -1582,6 +1584,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct RenameWindowArgs : public RenameWindowArgsT { RenameWindowArgs() = default; + RenameWindowArgs(winrt::hstring name) : + _Name{ name } {}; ACTION_ARG(winrt::hstring, Name); static constexpr std::string_view NameKey{ "name" }; @@ -1869,9 +1873,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(NewTabArgs); BASIC_FACTORY(MoveFocusArgs); BASIC_FACTORY(MovePaneArgs); + BASIC_FACTORY(SetTabColorArgs); BASIC_FACTORY(SwapPaneArgs); BASIC_FACTORY(SplitPaneArgs); BASIC_FACTORY(SetColorSchemeArgs); + BASIC_FACTORY(RenameWindowArgs); BASIC_FACTORY(ExecuteCommandlineArgs); BASIC_FACTORY(CloseOtherTabsArgs); BASIC_FACTORY(CloseTabsAfterArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 21b1b4b0b..2ea94e84d 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -213,6 +213,7 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass SetTabColorArgs : IActionArgs { + SetTabColorArgs(Windows.UI.Color tabColor); Windows.Foundation.IReference TabColor { get; }; }; @@ -294,6 +295,7 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass RenameWindowArgs : IActionArgs { + RenameWindowArgs(String name); String Name { get; }; }; diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp index 5a00ba2b4..acd876884 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp @@ -60,6 +60,31 @@ using namespace ::Microsoft::Terminal::Settings::Model; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { + winrt::hstring WindowLayout::ToJson(const Model::WindowLayout& layout) + { + JsonUtils::ConversionTrait trait; + auto json = trait.ToJson(layout); + + Json::StreamWriterBuilder wbuilder; + const auto content = Json::writeString(wbuilder, json); + return hstring{ til::u8u16(content) }; + } + + Model::WindowLayout WindowLayout::FromJson(const hstring& str) + { + auto data = til::u16u8(str); + std::string errs; + std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + + Json::Value root; + if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) + { + throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); + } + JsonUtils::ConversionTrait trait; + return trait.FromJson(root); + } + // Returns the application-global ApplicationState object. Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance() { @@ -108,6 +133,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { \ auto state = _state.lock(); \ state->name.emplace(value); \ + state->name##Changed = true; \ } \ \ _throttler(); \ @@ -115,34 +141,50 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN + Json::Value ApplicationState::_getRoot(const locked_hfile& file) const noexcept + { + Json::Value root; + try + { + const auto data = ReadUTF8FileLocked(file); + if (data.empty()) + { + return root; + } + + std::string errs; + std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + + if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) + { + throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); + } + } + CATCH_LOG() + + return root; + } + // Deserializes the state.json at _path into this ApplicationState. // * ANY errors during app state will result in the creation of a new empty state. // * ANY errors during runtime will result in changes being partially ignored. void ApplicationState::_read() const noexcept try { - const auto data = ReadUTF8FileIfExists(_path).value_or(std::string{}); - if (data.empty()) - { - return; - } - - std::string errs; - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; - - Json::Value root; - if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) - { - throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); - } - auto state = _state.lock(); + const auto file = OpenFileReadSharedLocked(_path); + + auto root = _getRoot(file); // GetValueForKey() comes in two variants: // * take a std::optional reference // * return std::optional by value // At the time of writing the former version skips missing fields in the json, // but we want to explicitly clear state fields that were removed from state.json. -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey>(root, key); +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ + if (!state->name##Changed) \ + { \ + state->name = JsonUtils::GetValueForKey>(root, key); \ + } MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN } @@ -152,21 +194,29 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // * Errors are only logged. // * _state->_writeScheduled is set to false, signaling our // setters that _synchronize() needs to be called again. - void ApplicationState::_write() const noexcept + void ApplicationState::_write() noexcept try { - Json::Value root{ Json::objectValue }; - + // re-read the state so that we can only update the properties that were changed. + Json::Value root{}; { - auto state = _state.lock_shared(); -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); + auto state = _state.lock(); + const auto file = OpenFileRWExclusiveLocked(_path); + root = _getRoot(file); + +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ + if (state->name##Changed) \ + { \ + JsonUtils::SetValueForKey(root, key, state->name); \ + state->name##Changed = false; \ + } MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN - } - Json::StreamWriterBuilder wbuilder; - const auto content = Json::writeString(wbuilder, root); - WriteUTF8FileAtomic(_path, content); + Json::StreamWriterBuilder wbuilder; + const auto content = Json::writeString(wbuilder, root); + WriteUTF8FileLocked(file, content); + } } CATCH_LOG() } diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index 71c6a576e..9b4f40f28 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -18,6 +18,7 @@ Abstract: #include #include #include +#include "FileUtils.h" #include // This macro generates all getters and setters for ApplicationState. @@ -33,6 +34,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct WindowLayout : WindowLayoutT { + static winrt::hstring ToJson(const Model::WindowLayout& layout); + static Model::WindowLayout FromJson(const winrt::hstring& json); + WINRT_PROPERTY(Windows::Foundation::Collections::IVector, TabLayout, nullptr); WINRT_PROPERTY(winrt::Windows::Foundation::IReference, InitialPosition, nullptr); WINRT_PROPERTY(winrt::Windows::Foundation::IReference, InitialSize, nullptr); @@ -63,12 +67,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation private: struct state_t { -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) std::optional name{ __VA_ARGS__ }; +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ + std::optional name{ __VA_ARGS__ }; \ + bool name##Changed = false; + MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN }; - void _write() const noexcept; + Json::Value _getRoot(const winrt::Microsoft::Terminal::Settings::Model::locked_hfile& file) const noexcept; + void _write() noexcept; void _read() const noexcept; std::filesystem::path _path; diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.idl b/src/cascadia/TerminalSettingsModel/ApplicationState.idl index 91231a112..119a37979 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.idl +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.idl @@ -15,6 +15,9 @@ namespace Microsoft.Terminal.Settings.Model { WindowLayout(); + static String ToJson(WindowLayout layout); + static WindowLayout FromJson(String json); + Windows.Foundation.Collections.IVector TabLayout; Windows.Foundation.IReference InitialPosition; Windows.Foundation.IReference InitialSize; diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.cpp b/src/cascadia/TerminalSettingsModel/FileUtils.cpp index cf273d5a5..599d29a55 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.cpp +++ b/src/cascadia/TerminalSettingsModel/FileUtils.cpp @@ -39,6 +39,83 @@ namespace winrt::Microsoft::Terminal::Settings::Model return baseSettingsPath; } + locked_hfile OpenFileReadSharedLocked(const std::filesystem::path& path) + { + wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) }; + THROW_LAST_ERROR_IF(!file); + // just lock the entire file + OVERLAPPED sOverlapped; + sOverlapped.Offset = 0; + sOverlapped.OffsetHigh = 0; + // Shared lock + THROW_LAST_ERROR_IF(!LockFileEx(file.get(), + 0, // lock shared, wait to return until lock is obtained + 0, // reserved, does nothing + INT_MAX, // lock INT_MAX bytes + 0, // higher-order bytes, if our state file is greater than 2GB I guess this will be a problem + &sOverlapped)); + return { std::move(file), sOverlapped }; + } + + locked_hfile OpenFileRWExclusiveLocked(const std::filesystem::path& path) + { + wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; + THROW_LAST_ERROR_IF(!file); + // just lock the entire file + OVERLAPPED sOverlapped; + sOverlapped.Offset = 0; + sOverlapped.OffsetHigh = 0; + // Shared lock + THROW_LAST_ERROR_IF(!LockFileEx(file.get(), + LOCKFILE_EXCLUSIVE_LOCK, // lock exclusive, wait to return until lock is obtained + 0, // reserved, does nothing + INT_MAX, // lock INT_MAX bytes + 0, // higher-order bytes, if our state file is greater than 2GB I guess this will be a problem + &sOverlapped)); + return { std::move(file), sOverlapped }; + } + + std::string ReadUTF8FileLocked(const locked_hfile& file) + { + const auto fileSize = GetFileSize(file.get(), nullptr); + THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE); + + // By making our buffer just slightly larger we can detect if + // the file size changed and we've failed to read the full file. + std::string buffer(static_cast(fileSize) + 1, '\0'); + DWORD bytesRead = 0; + THROW_IF_WIN32_BOOL_FALSE(ReadFile(file.get(), buffer.data(), gsl::narrow(buffer.size()), &bytesRead, nullptr)); + + // As mentioned before our buffer was allocated oversized. + buffer.resize(bytesRead); + + if (til::starts_with(buffer, Utf8Bom)) + { + // Yeah this memmove()s the entire content. + // But I don't really want to deal with UTF8 BOMs any more than necessary, + // as basically not a single editor writes a BOM for UTF8. + buffer.erase(0, Utf8Bom.size()); + } + + return buffer; + } + + void WriteUTF8FileLocked(const locked_hfile& file, const std::string_view& content) + { + // truncate the file because we want to overwrite it + SetFilePointer(file.get(), 0, nullptr, FILE_BEGIN); + THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(file.get())); + + const auto fileSize = gsl::narrow(content.size()); + DWORD bytesWritten = 0; + THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), fileSize, &bytesWritten, nullptr)); + + if (bytesWritten != fileSize) + { + THROW_WIN32_MSG(ERROR_WRITE_FAULT, "failed to write whole file"); + } + } + // Tries to read a file somewhat atomically without locking it. // Strips the UTF8 BOM if it exists. std::string ReadUTF8File(const std::filesystem::path& path) diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.h b/src/cascadia/TerminalSettingsModel/FileUtils.h index c003228c3..187051e7c 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.h +++ b/src/cascadia/TerminalSettingsModel/FileUtils.h @@ -1,9 +1,39 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +#pragma once + namespace winrt::Microsoft::Terminal::Settings::Model { + // I couldn't find a wil helper for this so I made it myself + class locked_hfile + { + public: + wil::unique_hfile file; + OVERLAPPED lockedRegion; + + ~locked_hfile() + { + if (file) + { + // Need to unlock the file before it is closed + UnlockFileEx(file.get(), 0, INT_MAX, 0, &lockedRegion); + } + } + + HANDLE get() const noexcept + { + return file.get(); + } + }; + std::filesystem::path GetBaseSettingsPath(); + + locked_hfile OpenFileReadSharedLocked(const std::filesystem::path& path); + locked_hfile OpenFileRWExclusiveLocked(const std::filesystem::path& path); + std::string ReadUTF8FileLocked(const locked_hfile& file); + void WriteUTF8FileLocked(const locked_hfile& file, const std::string_view& content); + std::string ReadUTF8File(const std::filesystem::path& path); std::optional ReadUTF8FileIfExists(const std::filesystem::path& path); void WriteUTF8File(const std::filesystem::path& path, const std::string_view& content); diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 0a534e5a0..2112c58a6 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -76,6 +76,7 @@ namespace RemotingUnitTests void Summon(const Remoting::SummonWindowBehavior& /*args*/) { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void RequestShowNotificationIcon() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void RequestHideNotificationIcon() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; + winrt::hstring GetWindowLayout() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void RequestQuitAll() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void Quit() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, Remoting::WindowActivatedArgs); @@ -88,6 +89,7 @@ namespace RemotingUnitTests TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, Remoting::GetWindowLayoutArgs); }; class RemotingTests diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index e9d9b2416..c2b8dcda2 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -20,6 +20,7 @@ using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace ::Microsoft::Console; using namespace ::Microsoft::Console::Types; +using namespace std::chrono_literals; // This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx // "If the high-order bit is 1, the key is down; otherwise, it is up." @@ -29,7 +30,8 @@ AppHost::AppHost() noexcept : _app{}, _windowManager{}, _logic{ nullptr }, // don't make one, we're going to take a ref on app's - _window{ nullptr } + _window{ nullptr }, + _getWindowLayoutThrottler{} // this will get set if we become the monarch { _logic = _app.Logic(); // get a ref to app's logic @@ -84,6 +86,12 @@ AppHost::AppHost() noexcept : _window->SetAlwaysOnTop(_logic.GetInitialAlwaysOnTop()); _window->MakeWindow(); + _windowManager.GetWindowLayoutRequested([this](auto&&, const winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs& args) { + // The peasants are running on separate threads, so they'll need to + // swap what context they are in to the ui thread to get the actual layout. + args.WindowLayoutJsonAsync(_GetWindowLayoutAsync()); + }); + _windowManager.BecameMonarch({ this, &AppHost::_BecomeMonarch }); if (_windowManager.IsMonarch()) { @@ -220,7 +228,47 @@ void AppHost::_HandleCommandlineArgs() // is created. if (_windowManager.IsMonarch()) { - _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); + const auto numPeasants = _windowManager.GetNumberOfPeasants(); + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (_logic.ShouldUsePersistedLayout() && layouts && layouts.Size() > 0) + { + uint32_t startIdx = 0; + // We want to create a window for every saved layout. + // If we are the only window, and no commandline arguments were provided + // then we should just use the current window to load the first layout. + // Otherwise create this window normally with its commandline, and create + // a new window using the first saved layout information. + // The 2nd+ layout will always get a new window. + if (numPeasants == 1 && !_logic.HasCommandlineArguments() && !_logic.HasSettingsStartupActions()) + { + _logic.SetPersistedLayoutIdx(startIdx); + startIdx += 1; + } + + // Create new windows for each of the other saved layouts. + for (const auto size = layouts.Size(); startIdx < size; startIdx += 1) + { + auto newWindowArgs = fmt::format(L"{0} -w new -s {1}", args[0], startIdx); + + STARTUPINFO si; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + wil::unique_process_information pi; + + LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr, + newWindowArgs.data(), + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + false, // bInheritHandles + DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT, // doCreationFlags + nullptr, // lpEnvironment + nullptr, // lpStartingDirectory + &si, // lpStartupInfo + &pi // lpProcessInformation + )); + } + } + _logic.SetNumberOfOpenWindows(numPeasants); } _logic.WindowName(peasant.WindowName()); _logic.WindowId(peasant.GetID()); @@ -257,7 +305,16 @@ void AppHost::Initialize() // Register the 'X' button of the window for a warning experience of multiple // tabs opened, this is consistent with Alt+F4 closing - _window->WindowCloseButtonClicked([this]() { _logic.WindowCloseButtonClicked(); }); + _window->WindowCloseButtonClicked([this]() { + const auto pos = _GetWindowLaunchPosition(); + _logic.CloseWindow(pos); + }); + // If the user requests a close in another way handle the same as if the 'X' + // was clicked. + _logic.CloseRequested([this](auto&&, auto&&) { + const auto pos = _GetWindowLaunchPosition(); + _logic.CloseWindow(pos); + }); // Add an event handler to plumb clicks in the titlebar area down to the // application layer. @@ -347,6 +404,24 @@ void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*se _window->Close(); } +LaunchPosition AppHost::_GetWindowLaunchPosition() +{ + // Get the position of the current window. This includes the + // non-client already. + const auto window = _window->GetWindowRect(); + + const auto dpi = _window->GetCurrentDpi(); + const auto nonClientArea = _window->GetNonClientFrame(dpi); + + // The nonClientArea adjustment is negative, so subtract that out. + // This way we save the user-visible location of the terminal. + LaunchPosition pos{}; + pos.X = window.left - nonClientArea.left; + pos.Y = window.top; + + return pos; +} + // Method Description: // - Resize the window we're about to create to the appropriate dimensions, as // specified in the settings. This will be called during the handling of @@ -634,6 +709,31 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send _logic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory()); } +// Method Description: +// - Asynchronously get the window layout from the current page. This is +// done async because we need to switch between the ui thread and the calling +// thread. +// - NB: The peasant calling this must not be running on the UI thread, otherwise +// they will crash since they just call .get on the async operation. +// Arguments: +// - +// Return Value: +// - The window layout as a json string. +winrt::Windows::Foundation::IAsyncOperation AppHost::_GetWindowLayoutAsync() +{ + winrt::apartment_context peasant_thread; + + // Use the main thread since we are accessing controls. + co_await winrt::resume_foreground(_logic.GetRoot().Dispatcher()); + const auto pos = _GetWindowLaunchPosition(); + const auto layoutJson = _logic.GetWindowLayoutJson(pos); + + // go back to give the result to the peasant. + co_await peasant_thread; + + co_return layoutJson; +} + // Method Description: // - Event handler for the WindowManager::FindTargetWindowRequested event. The // manager will ask us how to figure out what the target window is for a set @@ -687,8 +787,13 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s // and subscribe for updates if there are any changes to that number. _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); - _windowManager.WindowCreated([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); - _windowManager.WindowClosed([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); + _windowManager.WindowCreated([this](auto&&, auto&&) { + _getWindowLayoutThrottler.value()(); + _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); + _windowManager.WindowClosed([this](auto&&, auto&&) { + _getWindowLayoutThrottler.value()(); + _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); + }); // These events are coming from peasants that become or un-become quake windows. _windowManager.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequested(); }); @@ -696,6 +801,48 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s // If the monarch receives a QuitAll event it will signal this event to be // ran before each peasant is closed. _windowManager.QuitAllRequested({ this, &AppHost::_QuitAllRequested }); + + // The monarch should be monitoring if it should save the window layout. + if (!_getWindowLayoutThrottler.has_value()) + { + // We want at least some delay to prevent the first save from overwriting + // the data as we try load windows initially. + _getWindowLayoutThrottler.emplace(std::move(std::chrono::seconds(10)), std::move([this]() { _SaveWindowLayoutsRepeat(); })); + _getWindowLayoutThrottler.value()(); + } +} + +winrt::Windows::Foundation::IAsyncAction AppHost::_SaveWindowLayouts() +{ + // Make sure we run on a background thread to not block anything. + co_await winrt::resume_background(); + + if (_logic.ShouldUsePersistedLayout()) + { + const auto layoutJsons = _windowManager.GetAllWindowLayouts(); + _logic.SaveWindowLayoutJsons(layoutJsons); + } + + co_return; +} + +winrt::fire_and_forget AppHost::_SaveWindowLayoutsRepeat() +{ + // Make sure we run on a background thread to not block anything. + co_await winrt::resume_background(); + + co_await _SaveWindowLayouts(); + + // Don't need to save too frequently. + co_await 30s; + + // As long as we are supposed to keep saving, request another save. + // This will be delayed by the throttler so that at most one save happens + // per 10 seconds, if a save is requested by another source simultaneously. + if (_getWindowLayoutThrottler.has_value()) + { + _getWindowLayoutThrottler.value()(); + } } void AppHost::_listenForInboundConnections() @@ -1046,10 +1193,18 @@ void AppHost::_RequestQuitAll(const winrt::Windows::Foundation::IInspectable&, } void AppHost::_QuitAllRequested(const winrt::Windows::Foundation::IInspectable&, - const winrt::Windows::Foundation::IInspectable&) + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args) { - // TODO: GH#9800: For now, nothing needs to be done before the monarch closes all windows. - // Later when we have state saving that should go here. + // Make sure that the current timer is destroyed so that it doesn't attempt + // to run while we are in the middle of quitting. + if (_getWindowLayoutThrottler.has_value()) + { + _getWindowLayoutThrottler.reset(); + } + + // Tell the monarch to wait for the window layouts to save before + // everyone quits. + args.BeforeQuitAllAction(_SaveWindowLayouts()); } void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index db8a0bf1b..d51ba227a 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -4,6 +4,7 @@ #include "pch.h" #include "NonClientIslandWindow.h" #include "NotificationIcon.h" +#include class AppHost { @@ -31,7 +32,12 @@ private: bool _shouldCreateWindow{ false }; bool _useNonClientArea{ false }; + std::optional> _getWindowLayoutThrottler; + winrt::Windows::Foundation::IAsyncAction _SaveWindowLayouts(); + winrt::fire_and_forget _SaveWindowLayoutsRepeat(); + void _HandleCommandlineArgs(); + winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition(); void _HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); void _UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable& sender, @@ -53,6 +59,8 @@ private: void _DispatchCommandline(winrt::Windows::Foundation::IInspectable sender, winrt::Microsoft::Terminal::Remoting::CommandlineArgs args); + winrt::Windows::Foundation::IAsyncOperation _GetWindowLayoutAsync(); + void _FindTargetWindow(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args); @@ -95,7 +103,7 @@ private: const winrt::Windows::Foundation::IInspectable& args); void _QuitAllRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args); void _CreateNotificationIcon(); void _DestroyNotificationIcon(); diff --git a/src/cascadia/WindowsTerminal/BaseWindow.h b/src/cascadia/WindowsTerminal/BaseWindow.h index 581b38617..167b309bb 100644 --- a/src/cascadia/WindowsTerminal/BaseWindow.h +++ b/src/cascadia/WindowsTerminal/BaseWindow.h @@ -133,6 +133,11 @@ public: return _window.get(); } + UINT GetCurrentDpi() const noexcept + { + return ::GetDpiForWindow(_window.get()); + } + float GetCurrentDpiScale() const noexcept { const auto dpi = ::GetDpiForWindow(_window.get()); From 43297315ba4cc590648b35628d8a38d11af35c33 Mon Sep 17 00:00:00 2001 From: Schuyler Rosefield Date: Tue, 28 Sep 2021 15:16:05 -0400 Subject: [PATCH 17/35] Add the ability to interact with subtrees of panes (#11153) This commit adds the ability to interact with subtrees of panes. Have you ever thought that you don't have enough regression testing to do? Boy do I have the PR for you! This breaks all kinds of assumptions about what is or is not focused, largely complicated by the fact that a pane is not a proper control. I did my best to cover as many cases as I could, but I wouldn't be surprised if there are some things broken that I am unaware of. Done: - Add `parent` and `child` movement directions to move up and down the tree respectively - When a parent pane is selected it will have borders all around it in addition to any borders the children have. - Fix focus, swap, split, zoom, toggle orientation, resize, and move to all handle interacting with more than one pane. - Similarly the actions for font size changing, closing, read-only, clearing buffer, and changing color scheme will distribute to all children. - This technically leaves control focus on the original control in the focused subtree because panes aren't proper controls themselves. This is also used to make sure we go back down the same path with the `child` movement. - You can zoom a parent pane, and click between different zoomed sub-panes and it won't unzoom you until you use moveFocus or another action. This wasn't explicitly programmed behavior so it is probably buggy (I've quashed a couple at least). It is a natural consequence of showing multiple terminals and allowing you to focus a terminal and a parent separately, since changing the active pane directly does not unzoom. This also means there can be a disconnect between what pane is zoomed and what pane is active. ## Validation Steps Performed Tested focus movement, swapping, moving panes, and zooming. Closes #10733 --- doc/cascadia/profiles.schema.json | 8 +- .../LocalTests_TerminalApp/TabTests.cpp | 15 +- .../TerminalApp/ActionPreviewHandlers.cpp | 86 ++- .../TerminalApp/AppActionHandlers.cpp | 88 +-- src/cascadia/TerminalApp/Pane.cpp | 688 +++++++++++++----- src/cascadia/TerminalApp/Pane.h | 52 +- .../Resources/en-US/Resources.resw | 3 + src/cascadia/TerminalApp/TabManagement.cpp | 37 +- src/cascadia/TerminalApp/TerminalPage.h | 22 +- src/cascadia/TerminalApp/TerminalTab.cpp | 137 +++- src/cascadia/TerminalApp/TerminalTab.h | 1 + src/cascadia/TerminalControl/ControlCore.cpp | 3 + .../TerminalSettingsModel/ActionArgs.cpp | 4 + .../TerminalSettingsModel/ActionArgs.idl | 4 +- .../Resources/en-US/Resources.resw | 6 + .../TerminalSettingsSerializationHelpers.h | 4 +- .../TerminalSettingsModel/defaults.json | 2 + src/renderer/dx/DxRenderer.cpp | 1 + 18 files changed, 797 insertions(+), 364 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 293b3dd07..0a399f17d 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -319,7 +319,9 @@ "previous", "nextInOrder", "previousInOrder", - "first" + "first", + "parent", + "child" ], "type": "string" }, @@ -562,7 +564,7 @@ "direction": { "$ref": "#/definitions/FocusDirection", "default": "left", - "description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane." + "description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane, 'nextInOrder' or 'previousInOrder' to move to the next or previous pane, 'first' to focus the first pane, or 'parent' or 'child' to move up and down the tree." } } } @@ -579,7 +581,7 @@ "direction": { "$ref": "#/definitions/FocusDirection", "default": "left", - "description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane." + "description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane, 'nextInOrder' or 'previousInOrder' to move to the next or previous pane, or 'first' to swap with the first pane." } } } diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 4e3da70aa..ecb40c3eb 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -751,7 +751,7 @@ namespace TerminalAppLocalTests }); VERIFY_SUCCEEDED(result); - Log::Comment(L"Move focus. This will cause us to un-zoom."); + Log::Comment(L"Move focus. We should still be zoomed."); result = RunOnUIThread([&page]() { // Set up action MoveFocusArgs args{ FocusDirection::Left }; @@ -761,7 +761,7 @@ namespace TerminalAppLocalTests auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); - VERIFY_IS_FALSE(firstTab->IsZoomed()); + VERIFY_IS_TRUE(firstTab->IsZoomed()); }); VERIFY_SUCCEEDED(result); } @@ -1357,7 +1357,8 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed to the preview"); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings); + // And we should have stored a function to revert the change. + VERIFY_ARE_EQUAL(1u, page->_restorePreviewFuncs.size()); }); TestOnUIThread([&page]() { @@ -1383,7 +1384,8 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed"); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(nullptr, page->_originalSettings); + // After preview there should be no more restore functions to execute. + VERIFY_ARE_EQUAL(0u, page->_restorePreviewFuncs.size()); }); } @@ -1428,7 +1430,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed to the preview"); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings); }); TestOnUIThread([&page]() { @@ -1451,7 +1452,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be the same as it originally was"); VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(nullptr, page->_originalSettings); }); } @@ -1498,7 +1498,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed to the preview"); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings); }); TestOnUIThread([&page]() { @@ -1522,7 +1521,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed to the preview"); VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings); }); TestOnUIThread([&page]() { @@ -1548,7 +1546,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed"); VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(nullptr, page->_originalSettings); }); } diff --git a/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp b/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp index f09c97553..3fc4ff2ae 100644 --- a/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp +++ b/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp @@ -67,41 +67,17 @@ namespace winrt::TerminalApp::implementation // - void TerminalPage::_EndPreviewColorScheme() { - // Get the focused control - if (const auto& activeControl{ _GetActiveControl() }) + for (const auto& f : _restorePreviewFuncs) { - // Get the runtime settings of the focused control - const auto& controlSettings{ activeControl.Settings().as() }; - - // Get the control's root settings, the ones that we actually - // assigned to it. - auto parentSettings{ controlSettings.GetParent() }; - while (parentSettings.GetParent() != nullptr) - { - parentSettings = parentSettings.GetParent(); - } - - // If the root settings are the same as the ones we stashed, - // then reset the parent of the runtime settings to the stashed - // settings. This condition might be false if the settings - // hot-reloaded while the palette was open. In that case, we - // don't want to reset the settings to what they were _before_ - // the hot-reload. - if (_originalSettings == parentSettings) - { - // Set the original settings as the parent of the control's settings - activeControl.Settings().as().SetParent(_originalSettings); - } - - activeControl.UpdateSettings(); + f(); } - _originalSettings = nullptr; + _restorePreviewFuncs.clear(); } // Method Description: // - Preview handler for the SetColorScheme action. - // - This method will stash the settings of the current control in - // _originalSettings. Then it will create a new TerminalSettings object + // - This method will stash functions to reset the settings of the selected controls in + // _restorePreviewFuncs. Then it will create a new TerminalSettings object // with only the properties from the ColorScheme set. It'll _insert_ a // TerminalSettings between the control's root settings (built from // CascadiaSettings) and the control's runtime settings. That'll cause the @@ -112,33 +88,63 @@ namespace winrt::TerminalApp::implementation // - void TerminalPage::_PreviewColorScheme(const Settings::Model::SetColorSchemeArgs& args) { - // Get the focused control - if (const auto& activeControl{ _GetActiveControl() }) + if (const auto& scheme{ _settings.GlobalSettings().ColorSchemes().TryLookup(args.SchemeName()) }) { - if (const auto& scheme{ _settings.GlobalSettings().ColorSchemes().TryLookup(args.SchemeName()) }) - { + // Clear the saved preview funcs because we don't need to add a restore each time + // the preview color changes, we only need to be able to restore the last one. + _restorePreviewFuncs.clear(); + + _ApplyToActiveControls([&](const auto& control) { // Get the settings of the focused control and stash them - const auto& controlSettings = activeControl.Settings().as(); + const auto& controlSettings = control.Settings().as(); // Make sure to recurse up to the root - if you're doing // this while you're currently previewing a SetColorScheme // action, then the parent of the control's settings is _the // last preview TerminalSettings we inserted! We don't want // to save that one! - _originalSettings = controlSettings.GetParent(); - while (_originalSettings.GetParent() != nullptr) + auto originalSettings = controlSettings.GetParent(); + while (originalSettings.GetParent() != nullptr) { - _originalSettings = _originalSettings.GetParent(); + originalSettings = originalSettings.GetParent(); } // Create a new child for those settings - TerminalSettingsCreateResult fake{ _originalSettings }; + TerminalSettingsCreateResult fake{ originalSettings }; const auto& childStruct = TerminalSettings::CreateWithParent(fake); // Modify the child to have the applied color scheme childStruct.DefaultSettings().ApplyColorScheme(scheme); // Insert that new child as the parent of the control's settings controlSettings.SetParent(childStruct.DefaultSettings()); - activeControl.UpdateSettings(); - } + control.UpdateSettings(); + + // Take a copy of the inputs, since they are pointers anyways. + _restorePreviewFuncs.emplace_back([=]() { + // Get the runtime settings of the focused control + const auto& controlSettings{ control.Settings().as() }; + + // Get the control's root settings, the ones that we actually + // assigned to it. + auto parentSettings{ controlSettings.GetParent() }; + while (parentSettings.GetParent() != nullptr) + { + parentSettings = parentSettings.GetParent(); + } + + // If the root settings are the same as the ones we stashed, + // then reset the parent of the runtime settings to the stashed + // settings. This condition might be false if the settings + // hot-reloaded while the palette was open. In that case, we + // don't want to reset the settings to what they were _before_ + // the hot-reload. + if (originalSettings == parentSettings) + { + // Set the original settings as the parent of the control's settings + control.Settings().as().SetParent(originalSettings); + } + + control.UpdateSettings(); + }); + }); } } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index d99726b39..ae79289fa 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -377,11 +377,10 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - if (const auto& termControl{ _GetActiveControl() }) - { - termControl.AdjustFontSize(realArgs.Delta()); - args.Handled(true); - } + const auto res = _ApplyToActiveControls([&](auto& control) { + control.AdjustFontSize(realArgs.Delta()); + }); + args.Handled(res); } } @@ -395,21 +394,19 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleResetFontSize(const IInspectable& /*sender*/, const ActionEventArgs& args) { - if (const auto& termControl{ _GetActiveControl() }) - { - termControl.ResetFontSize(); - args.Handled(true); - } + const auto res = _ApplyToActiveControls([](auto& control) { + control.ResetFontSize(); + }); + args.Handled(res); } void TerminalPage::_HandleToggleShaderEffects(const IInspectable& /*sender*/, const ActionEventArgs& args) { - if (const auto& termControl{ _GetActiveControl() }) - { - termControl.ToggleShaderEffects(); - args.Handled(true); - } + const auto res = _ApplyToActiveControls([](auto& control) { + control.ToggleShaderEffects(); + }); + args.Handled(res); } void TerminalPage::_HandleToggleFocusMode(const IInspectable& /*sender*/, @@ -452,37 +449,33 @@ namespace winrt::TerminalApp::implementation args.Handled(false); if (const auto& realArgs = args.ActionArgs().try_as()) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto scheme = _settings.GlobalSettings().ColorSchemes().TryLookup(realArgs.SchemeName())) { - if (auto activeControl = activeTab->GetActiveTerminalControl()) - { - if (const auto scheme = _settings.GlobalSettings().ColorSchemes().TryLookup(realArgs.SchemeName())) + const auto res = _ApplyToActiveControls([&](auto& control) { + // Start by getting the current settings of the control + auto controlSettings = control.Settings().as(); + auto parentSettings = controlSettings; + // Those are the _runtime_ settings however. What we + // need to do is: + // + // 1. Blow away any colors set in the runtime settings. + // 2. Apply the color scheme to the parent settings. + // + // 1 is important to make sure that the effects of + // something like `colortool` are cleared when setting + // the scheme. + if (controlSettings.GetParent() != nullptr) { - // Start by getting the current settings of the control - auto controlSettings = activeControl.Settings().as(); - auto parentSettings = controlSettings; - // Those are the _runtime_ settings however. What we - // need to do is: - // - // 1. Blow away any colors set in the runtime settings. - // 2. Apply the color scheme to the parent settings. - // - // 1 is important to make sure that the effects of - // something like `colortool` are cleared when setting - // the scheme. - if (controlSettings.GetParent() != nullptr) - { - parentSettings = controlSettings.GetParent(); - } - - // ApplyColorScheme(nullptr) will clear the old color scheme. - controlSettings.ApplyColorScheme(nullptr); - parentSettings.ApplyColorScheme(scheme); - - activeControl.UpdateSettings(); - args.Handled(true); + parentSettings = controlSettings.GetParent(); } - } + + // ApplyColorScheme(nullptr) will clear the old color scheme. + controlSettings.ApplyColorScheme(nullptr); + parentSettings.ApplyColorScheme(scheme); + + control.UpdateSettings(); + }); + args.Handled(res); } } } @@ -896,11 +889,10 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - if (const auto termControl{ _GetActiveControl() }) - { - termControl.ClearBuffer(realArgs.Clear()); - args.Handled(true); - } + const auto res = _ApplyToActiveControls([&](auto& control) { + control.ClearBuffer(realArgs.Clear()); + }); + args.Handled(res); } } } diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 310d7ca47..82bc861b4 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -39,8 +39,8 @@ Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFo _lastActive{ lastFocused }, _profile{ profile } { - _root.Children().Append(_border); - _border.Child(_control); + _root.Children().Append(_borderFirst); + _borderFirst.Child(_control); _connectionStateChangedToken = _control.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler }); _warningBellToken = _control.WarningBell({ this, &Pane::_ControlWarningBellHandler }); @@ -61,7 +61,52 @@ Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFo // LOAD-BEARING: This will NOT work if the border's BorderBrush is set to // Colors::Transparent! The border won't get Tapped events, and they'll fall // through to something else. - _border.Tapped([this](auto&, auto& e) { + _borderFirst.Tapped([this](auto&, auto& e) { + _FocusFirstChild(); + e.Handled(true); + }); + _borderSecond.Tapped([this](auto&, auto& e) { + _FocusFirstChild(); + e.Handled(true); + }); +} + +Pane::Pane(std::shared_ptr first, + std::shared_ptr second, + const SplitState splitState, + const float splitPosition, + const bool lastFocused) : + _firstChild{ first }, + _secondChild{ second }, + _splitState{ splitState }, + _desiredSplitPosition{ splitPosition }, + _lastActive{ lastFocused } +{ + _CreateRowColDefinitions(); + _borderFirst.Child(_firstChild->GetRootElement()); + _borderSecond.Child(_secondChild->GetRootElement()); + + // Use the unfocused border color as the pane background, so an actual color + // appears behind panes as we animate them sliding in. + _root.Background(s_unfocusedBorderBrush); + + _root.Children().Append(_borderFirst); + _root.Children().Append(_borderSecond); + + _ApplySplitDefinitions(); + + // Register event handlers on our children to handle their Close events + _SetupChildCloseHandlers(); + + // When our border is tapped, make sure to transfer focus to our control. + // LOAD-BEARING: This will NOT work if the border's BorderBrush is set to + // Colors::Transparent! The border won't get Tapped events, and they'll fall + // through to something else. + _borderFirst.Tapped([this](auto&, auto& e) { + _FocusFirstChild(); + e.Handled(true); + }); + _borderSecond.Tapped([this](auto&, auto& e) { _FocusFirstChild(); e.Handled(true); }); @@ -330,18 +375,18 @@ bool Pane::ResizePane(const ResizeDirection& direction) return false; } - // Check if either our first or second child is the currently focused leaf. + // Check if either our first or second child is the currently focused pane. // If it is, and the requested resize direction matches our separator, then // we're the pane that needs to adjust its separator. // If our separator is the wrong direction, then we can't handle it. - const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive; - const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive; + const bool firstIsFocused = _firstChild->_lastActive; + const bool secondIsFocused = _secondChild->_lastActive; if (firstIsFocused || secondIsFocused) { return _Resize(direction); } - // If neither of our children were the focused leaf, then recurse into + // If neither of our children were the focused pane, then recurse into // our children and see if they can handle the resize. // For each child, if it has a focused descendant, try having that child // handle the resize. @@ -390,6 +435,36 @@ std::shared_ptr Pane::NavigateDirection(const std::shared_ptr source return nullptr; } + // Check if moving up or down the tree + if (direction == FocusDirection::Parent) + { + if (const auto parent = _FindParentOfPane(sourcePane)) + { + // Keep a reference to which child we came from + parent->_parentChildPath = sourcePane->weak_from_this(); + + return parent; + } + return nullptr; + } + + if (direction == FocusDirection::Child) + { + if (!sourcePane->_IsLeaf()) + { + auto child = sourcePane->_firstChild; + // If we've recorded path try to go back down it + if (const auto prevFocus = sourcePane->_parentChildPath.lock()) + { + child = prevFocus; + } + // clean up references + sourcePane->_parentChildPath.reset(); + return child; + } + return nullptr; + } + // Previous movement relies on the last used panes if (direction == FocusDirection::Previous) { @@ -413,6 +488,7 @@ std::shared_ptr Pane::NavigateDirection(const std::shared_ptr source return PreviousPane(sourcePane); } + // Fixed movement if (direction == FocusDirection::First) { std::shared_ptr firstPane = nullptr; @@ -482,6 +558,11 @@ std::shared_ptr Pane::NextPane(const std::shared_ptr targetPane) bool foundTarget = false; auto foundNext = WalkTree([&](auto pane) { + // If we are a parent pane we don't want to move to one of our children + if (foundTarget && targetPane->_HasChild(pane)) + { + return false; + } // In case the target pane is the last pane in the tree, keep a reference // to the first leaf so we can wrap around. if (firstLeaf == nullptr && pane->_IsLeaf()) @@ -612,6 +693,12 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) return false; } + // Similarly don't swap if we have a circular reference + if (first->_HasChild(second) || second->_HasChild(first)) + { + return false; + } + std::unique_lock lock{ _createCloseLock }; // Recurse through the tree to find the parent panes of each pane that is @@ -625,8 +712,10 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) // after the pointers were found but before we reached this function. if (firstParent && secondParent) { - // Swap size/display information of the two panes. - std::swap(first->_borders, second->_borders); + // Before we swap anything get the borders for the parents so that + // it can be propagated to the swapped child. + firstParent->_borders = firstParent->_GetCommonBorders(); + secondParent->_borders = secondParent->_GetCommonBorders(); // Replace the old child with new one, and revoke appropriate event // handlers. @@ -644,32 +733,30 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) } // Clear now to ensure that we can add the child's grid to us later parent->_root.Children().Clear(); + parent->_borderFirst.Child(nullptr); + parent->_borderSecond.Child(nullptr); }; // Make sure that the right event handlers are set, and the children // are placed in the appropriate locations in the grid. auto updateParent = [](auto& parent) { + // just always revoke the old helpers since we are making new ones. + parent->_firstChild->Closed(parent->_firstClosedToken); + parent->_secondChild->Closed(parent->_secondClosedToken); parent->_SetupChildCloseHandlers(); parent->_root.Children().Clear(); - parent->_root.Children().Append(parent->_firstChild->GetRootElement()); - parent->_root.Children().Append(parent->_secondChild->GetRootElement()); - // Make sure they have the correct borders, and also that they are - // placed in the right location in the grid. - // This mildly reproduces ApplySplitDefinitions, but is different in - // that it does not want to utilize the parent's border to set child - // borders. - if (parent->_splitState == SplitState::Vertical) - { - Controls::Grid::SetColumn(parent->_firstChild->GetRootElement(), 0); - Controls::Grid::SetColumn(parent->_secondChild->GetRootElement(), 1); - } - else if (parent->_splitState == SplitState::Horizontal) - { - Controls::Grid::SetRow(parent->_firstChild->GetRootElement(), 0); - Controls::Grid::SetRow(parent->_secondChild->GetRootElement(), 1); - } - parent->_firstChild->_UpdateBorders(); - parent->_secondChild->_UpdateBorders(); + parent->_borderFirst.Child(nullptr); + parent->_borderSecond.Child(nullptr); + parent->_borderFirst.Child(parent->_firstChild->GetRootElement()); + parent->_borderSecond.Child(parent->_secondChild->GetRootElement()); + + parent->_root.Children().Append(parent->_borderFirst); + parent->_root.Children().Append(parent->_borderSecond); + + // reset split definitions to clear any set row/column + parent->_root.ColumnDefinitions().Clear(); + parent->_root.RowDefinitions().Clear(); + parent->_CreateRowColDefinitions(); }; // If the firstParent and secondParent are the same, then we are just @@ -681,6 +768,7 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) std::swap(firstParent->_firstChild, firstParent->_secondChild); updateParent(firstParent); + firstParent->_ApplySplitDefinitions(); } else { @@ -690,11 +778,45 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) replaceChild(secondParent, second, first); updateParent(firstParent); updateParent(secondParent); + + // If one of the two parents is a child of the other we only want + // to apply the split definitions to the greatest parent to make + // sure that all panes get the correct borders. if this is not done + // and the ordering happens to be bad one parent's children will lose + // a border. + if (firstParent->_HasChild(secondParent)) + { + firstParent->_ApplySplitDefinitions(); + } + else if (secondParent->_HasChild(firstParent)) + { + secondParent->_ApplySplitDefinitions(); + } + else + { + firstParent->_ApplySplitDefinitions(); + secondParent->_ApplySplitDefinitions(); + } } - // For now the first pane is always the focused pane, so re-focus to - // make sure the cursor is still in the terminal since the root was moved. - first->_FocusFirstChild(); + // Refocus the last pane if there was a pane focused + first->WalkTree([](auto p) { + if (p->_lastActive) + { + p->_Focus(); + return true; + } + return false; + }); + + second->WalkTree([](auto p) { + if (p->_lastActive) + { + p->_Focus(); + return true; + } + return false; + }); return true; } @@ -796,13 +918,13 @@ std::pair Pane::_GetOffsetsForPane(const PaneP if (_splitState == SplitState::Horizontal) { - secondOffset.y += (1 - _desiredSplitPosition) * parentOffset.scaleY; + secondOffset.y += _desiredSplitPosition * parentOffset.scaleY; firstOffset.scaleY *= _desiredSplitPosition; secondOffset.scaleY *= (1 - _desiredSplitPosition); } else { - secondOffset.x += (1 - _desiredSplitPosition) * parentOffset.scaleX; + secondOffset.x += _desiredSplitPosition * parentOffset.scaleX; firstOffset.scaleX *= _desiredSplitPosition; secondOffset.scaleX *= (1 - _desiredSplitPosition); } @@ -1024,10 +1146,15 @@ void Pane::_ControlWarningBellHandler(const winrt::Windows::Foundation::IInspect // - // Return Value: // - -void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& /* sender */, +void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const& /* args */) { - _GotFocusHandlers(shared_from_this()); + FocusState f = FocusState::Programmatic; + if (const auto o = sender.try_as()) + { + f = o.FocusState(); + } + _GotFocusHandlers(shared_from_this(), f); } // Event Description: @@ -1085,8 +1212,8 @@ Controls::Grid Pane::GetRootElement() // Method Description: // - If this is the last focused pane, returns itself. Returns nullptr if this -// is a leaf and it's not focused. If it's a parent, it returns nullptr if no -// children of this pane were the last pane to be focused, or the Pane that +// is a leaf and it's not focused. If it's a parent, it returns nullptr if it nor +// any children of this pane were the last pane to be focused, or the Pane that // _was_ the last pane to be focused (if there was one). // - This Pane's control might not currently be focused, if the tab itself is // not currently focused. @@ -1095,9 +1222,13 @@ Controls::Grid Pane::GetRootElement() // `_lastActive`, else returns this std::shared_ptr Pane::GetActivePane() { + if (_lastActive) + { + return shared_from_this(); + } if (_IsLeaf()) { - return _lastActive ? shared_from_this() : nullptr; + return nullptr; } auto firstFocused = _firstChild->GetActivePane(); @@ -1109,7 +1240,38 @@ std::shared_ptr Pane::GetActivePane() } // Method Description: -// - Gets the TermControl of this pane. If this Pane is not a leaf, this will return nullptr. +// - Gets the TermControl of this pane. If this Pane is not a leaf but is +// focused, this will return the control of the last leaf pane that had focus. +// Otherwise, this will return the control of the first child of this pane. +// Arguments: +// - +// Return Value: +// - nullptr if this Pane is an unfocused parent, otherwise the TermControl of this Pane. +TermControl Pane::GetLastFocusedTerminalControl() +{ + if (!_IsLeaf()) + { + if (_lastActive) + { + std::shared_ptr pane = shared_from_this(); + while (const auto p = pane->_parentChildPath.lock()) + { + if (p->_IsLeaf()) + { + return p->_control; + } + pane = p; + } + // We didn't find our child somehow, they might have closed under us. + } + return _firstChild->GetLastFocusedTerminalControl(); + } + return _control; +} + +// Method Description: +// - Gets the TermControl of this pane. If this Pane is not a leaf this will +// return the nullptr; // Arguments: // - // Return Value: @@ -1139,7 +1301,7 @@ void Pane::ClearActive() // Method Description: // - Sets the "Active" state on this Pane. Only one Pane in a tree of Panes -// should be "active", and that pane should be a leaf. +// should be "active". // - Updates our visuals to match our new state, including highlighting our borders. // Arguments: // - @@ -1201,7 +1363,7 @@ bool Pane::_HasFocusedChild() const noexcept // We're intentionally making this one giant expression, so the compiler // will skip the following lookups if one of the lookups before it returns // true - return (_control && _lastActive) || + return (_lastActive) || (_firstChild && _firstChild->_HasFocusedChild()) || (_secondChild && _secondChild->_HasFocusedChild()); } @@ -1215,7 +1377,30 @@ bool Pane::_HasFocusedChild() const noexcept // - void Pane::UpdateVisuals() { - _border.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush); + // If we are the focused pane, but not a leaf we should add borders + if (!_IsLeaf()) + { + _UpdateBorders(); + } + _borderFirst.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush); + _borderSecond.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush); +} + +// Method Description: +// - Focus the current pane. Also trigger focus on the control, or if not a leaf +// the control belonging to the last focused leaf. +// This makes sure that focus exists within the tab (since panes aren't proper controls) +// Arguments: +// - +// Return Value: +// - +void Pane::_Focus() +{ + _GotFocusHandlers(shared_from_this(), FocusState::Programmatic); + if (const auto& control = GetLastFocusedTerminalControl()) + { + control.Focus(FocusState::Programmatic); + } } // Method Description: @@ -1246,10 +1431,7 @@ void Pane::_FocusFirstChild() // // `wtd -w 0 mf down ; sp` // `wtd -w 0 fp -t 1 ; sp` - - _GotFocusHandlers(shared_from_this()); - - _control.Focus(FocusState::Programmatic); + _Focus(); } else { @@ -1305,7 +1487,7 @@ std::shared_ptr Pane::AttachPane(std::shared_ptr pane, SplitDirectio pane->WalkTree([](auto p) { if (p->_lastActive) { - p->_FocusFirstChild(); + p->_Focus(); return true; } return false; @@ -1345,8 +1527,10 @@ std::shared_ptr Pane::DetachPane(std::shared_ptr pane) // other child. _CloseChild(isFirstChild, true); + // Update the borders on this pane and any children to match if we have + // no parent. detached->_borders = Borders::None; - detached->_UpdateBorders(); + detached->_ApplySplitDefinitions(); // Trigger the detached event on each child detached->WalkTree([](auto pane) { @@ -1397,15 +1581,23 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) auto closedChildClosedToken = closeFirst ? _firstClosedToken : _secondClosedToken; auto remainingChildClosedToken = closeFirst ? _secondClosedToken : _firstClosedToken; + // If we were a parent pane, and we pointed into the now closed child + // clear it. We will set it to something else later if + bool usedToFocusClosedChildsTerminal = false; + if (const auto prev = _parentChildPath.lock()) + { + if (closedChild == prev) + { + _parentChildPath.reset(); + usedToFocusClosedChildsTerminal = true; + } + } + // If the only child left is a leaf, that means we're a leaf now. if (remainingChild->_IsLeaf()) { - // When the remaining child is a leaf, that means both our children were - // previously leaves, and the only difference in their borders is the - // border that we gave them. Take a bitwise AND of those two children to - // remove that border. Other borders the children might have, they - // inherited from us, so the flag will be set for both children. - _borders = _firstChild->_borders & _secondChild->_borders; + // Find what borders need to persist after we close the child + _borders = _GetCommonBorders(); // take the control, profile and id of the pane that _wasn't_ closed. _control = remainingChild->_control; @@ -1426,8 +1618,14 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) // handlers since it is just getting moved. if (!isDetaching) { - closedChild->_control.ConnectionStateChanged(closedChild->_connectionStateChangedToken); - closedChild->_control.WarningBell(closedChild->_warningBellToken); + closedChild->WalkTree([](auto p) { + if (p->_IsLeaf()) + { + p->_control.ConnectionStateChanged(p->_connectionStateChangedToken); + p->_control.WarningBell(p->_warningBellToken); + } + return false; + }); } closedChild->Closed(closedChildClosedToken); @@ -1435,24 +1633,25 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) remainingChild->_control.ConnectionStateChanged(remainingChild->_connectionStateChangedToken); remainingChild->_control.WarningBell(remainingChild->_warningBellToken); - // If either of our children was focused, we want to take that focus from - // them. - _lastActive = _firstChild->_lastActive || _secondChild->_lastActive; + // If we or either of our children was focused, we want to take that + // focus from them. + _lastActive = _lastActive || _firstChild->_lastActive || _secondChild->_lastActive; // Remove all the ui elements of the remaining child. This'll make sure // we can re-attach the TermControl to our Grid. remainingChild->_root.Children().Clear(); - remainingChild->_border.Child(nullptr); + remainingChild->_borderFirst.Child(nullptr); // Reset our UI: _root.Children().Clear(); - _border.Child(nullptr); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); _root.ColumnDefinitions().Clear(); _root.RowDefinitions().Clear(); // Reattach the TermControl to our grid. - _root.Children().Append(_border); - _border.Child(_control); + _root.Children().Append(_borderFirst); + _borderFirst.Child(_control); // Make sure to set our _splitState before focusing the control. If you // fail to do this, when the tab handles the GotFocus event and asks us @@ -1466,7 +1665,7 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) // If we're inheriting the "last active" state from one of our children, // focus our control now. This should trigger our own GotFocus event. - if (_lastActive) + if (usedToFocusClosedChildsTerminal || _lastActive) { _control.Focus(FocusState::Programmatic); @@ -1478,7 +1677,7 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) // the control. Because Tab is relying on GotFocus to know who the // active pane in the tree is, without this call, _no one_ will be // the active pane any longer. - _GotFocusHandlers(shared_from_this()); + _GotFocusHandlers(shared_from_this(), FocusState::Programmatic); } _UpdateBorders(); @@ -1509,13 +1708,20 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) closedChild->Closed(closedChildClosedToken); if (!isDetaching) { - closedChild->_control.ConnectionStateChanged(closedChild->_connectionStateChangedToken); - closedChild->_control.WarningBell(closedChild->_warningBellToken); + closedChild->WalkTree([](auto p) { + if (p->_IsLeaf()) + { + p->_control.ConnectionStateChanged(p->_connectionStateChangedToken); + p->_control.WarningBell(p->_warningBellToken); + } + return false; + }); } // Reset our UI: _root.Children().Clear(); - _border.Child(nullptr); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); _root.ColumnDefinitions().Clear(); _root.RowDefinitions().Clear(); @@ -1539,20 +1745,44 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) // Remove the child's UI elements from the child's grid, so we can // attach them to us instead. remainingChild->_root.Children().Clear(); - remainingChild->_border.Child(nullptr); + remainingChild->_borderFirst.Child(nullptr); + remainingChild->_borderSecond.Child(nullptr); - _root.Children().Append(_firstChild->GetRootElement()); - _root.Children().Append(_secondChild->GetRootElement()); + _borderFirst.Child(_firstChild->GetRootElement()); + _borderSecond.Child(_secondChild->GetRootElement()); + + _root.Children().Append(_borderFirst); + _root.Children().Append(_borderSecond); // Propagate the new borders down to the children. _borders = remainingBorders; _ApplySplitDefinitions(); - // If the closed child was focused, transfer the focus to it's first sibling. - if (closedChild->_lastActive) + // If our child had focus and closed, just transfer to the first remaining + // child + if (closedChild->_HasFocusedChild()) { _FocusFirstChild(); } + // We might not have focus currently, but if our parent does then we + // want to make sure we have a valid path to one of our children. + // We should only update the path if our other child doesn't have focus itself. + else if (usedToFocusClosedChildsTerminal && !_secondChild->_HasFocusedChild()) + { + // update our path to our first remaining leaf + _parentChildPath = _firstChild; + _firstChild->WalkTree([](auto p) { + if (p->_IsLeaf()) + { + return true; + } + p->_parentChildPath = p->_firstChild; + return false; + }); + // This will focus the first terminal, and will set that leaf pane + // to the active pane if we nor one of our parents is not itself focused. + _FocusFirstChild(); + } // Release the pointers that the child was holding. remainingChild->_firstChild = nullptr; @@ -1602,16 +1832,18 @@ winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst) }; // Remove both children from the grid - _root.Children().Clear(); - // Add the remaining child back to the grid, in the right place. - _root.Children().Append(remainingChild->GetRootElement()); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); + if (_splitState == SplitState::Vertical) { - Controls::Grid::SetColumn(remainingChild->GetRootElement(), closeFirst ? 1 : 0); + Controls::Grid::SetColumn(_borderFirst, 0); + Controls::Grid::SetColumn(_borderSecond, 1); } else if (_splitState == SplitState::Horizontal) { - Controls::Grid::SetRow(remainingChild->GetRootElement(), closeFirst ? 1 : 0); + Controls::Grid::SetRow(_borderFirst, 0); + Controls::Grid::SetRow(_borderSecond, 1); } // Create the dummy grid. This grid will be the one we actually animate, @@ -1623,17 +1855,9 @@ winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst) // It should be the size of the closed pane. dummyGrid.Width(removedOriginalSize.Width); dummyGrid.Height(removedOriginalSize.Height); - // Put it where the removed child is - if (_splitState == SplitState::Vertical) - { - Controls::Grid::SetColumn(dummyGrid, closeFirst ? 0 : 1); - } - else if (_splitState == SplitState::Horizontal) - { - Controls::Grid::SetRow(dummyGrid, closeFirst ? 0 : 1); - } - // Add it to the tree - _root.Children().Append(dummyGrid); + + _borderFirst.Child(closeFirst ? dummyGrid : remainingChild->GetRootElement()); + _borderSecond.Child(closeFirst ? remainingChild->GetRootElement() : dummyGrid); // Set up the rows/cols as auto/auto, so they'll only use the size of // the elements in the grid. @@ -1772,9 +1996,9 @@ void Pane::_UpdateBorders() double top = 0, bottom = 0, left = 0, right = 0; Thickness newBorders{ 0 }; - if (_zoomed) + // Zoomed panes, and focused parents should have full borders + if (_zoomed || (!_IsLeaf() && _lastActive)) { - // When the pane is zoomed, manually show all the borders around the window. top = bottom = right = left = PaneBorderSize; } else @@ -1796,11 +2020,31 @@ void Pane::_UpdateBorders() right = PaneBorderSize; } } - _border.BorderThickness(ThicknessHelper::FromLengths(left, top, right, bottom)); + + if (_IsLeaf()) + { + _borderFirst.BorderThickness(ThicknessHelper::FromLengths(left, top, right, bottom)); + } + else + { + // If we are not a leaf we don't want to duplicate the shared border + // between our children. + if (_splitState == SplitState::Vertical) + { + _borderFirst.BorderThickness(ThicknessHelper::FromLengths(left, top, 0, bottom)); + _borderSecond.BorderThickness(ThicknessHelper::FromLengths(0, top, right, bottom)); + } + else + { + _borderFirst.BorderThickness(ThicknessHelper::FromLengths(left, top, right, 0)); + _borderSecond.BorderThickness(ThicknessHelper::FromLengths(left, 0, right, bottom)); + } + } } // Method Description: // - Find the borders for the leaf pane, or the shared borders for child panes. +// - This deliberately ignores if a focused parent has borders. // Arguments: // - // Return Value: @@ -1827,8 +2071,8 @@ void Pane::_ApplySplitDefinitions() { if (_splitState == SplitState::Vertical) { - Controls::Grid::SetColumn(_firstChild->GetRootElement(), 0); - Controls::Grid::SetColumn(_secondChild->GetRootElement(), 1); + Controls::Grid::SetColumn(_borderFirst, 0); + Controls::Grid::SetColumn(_borderSecond, 1); _firstChild->_borders = _borders | Borders::Right; _secondChild->_borders = _borders | Borders::Left; @@ -1839,8 +2083,8 @@ void Pane::_ApplySplitDefinitions() } else if (_splitState == SplitState::Horizontal) { - Controls::Grid::SetRow(_firstChild->GetRootElement(), 0); - Controls::Grid::SetRow(_secondChild->GetRootElement(), 1); + Controls::Grid::SetRow(_borderFirst, 0); + Controls::Grid::SetRow(_borderSecond, 1); _firstChild->_borders = _borders | Borders::Bottom; _secondChild->_borders = _borders | Borders::Top; @@ -1894,6 +2138,7 @@ void Pane::_SetupEntranceAnimation() auto setupAnimation = [&](const auto& size, const bool isFirstChild) { auto child = isFirstChild ? _firstChild : _secondChild; auto childGrid = child->_root; + // If we are splitting a parent pane this may be null auto control = child->_control; // Build up our animation: // * it'll take as long as our duration (200ms) @@ -1949,16 +2194,22 @@ void Pane::_SetupEntranceAnimation() // the parent pane, otherwise use the bottom/right. This is always // the "outside" of the parent pane. childGrid.HorizontalAlignment(isFirstChild ? HorizontalAlignment::Left : HorizontalAlignment::Right); - control.HorizontalAlignment(HorizontalAlignment::Left); - control.Width(isFirstChild ? totalSize : size); + if (control) + { + control.HorizontalAlignment(HorizontalAlignment::Left); + control.Width(isFirstChild ? totalSize : size); + } // When the animation is completed, undo the trickiness from before, to // restore the controls to the behavior they'd usually have. animation.Completed([childGrid, control, root = _secondChild->_root](auto&&, auto&&) { - control.Width(NAN); childGrid.Width(NAN); childGrid.HorizontalAlignment(HorizontalAlignment::Stretch); - control.HorizontalAlignment(HorizontalAlignment::Stretch); + if (control) + { + control.Width(NAN); + control.HorizontalAlignment(HorizontalAlignment::Stretch); + } root.Background(nullptr); }); } @@ -1968,16 +2219,22 @@ void Pane::_SetupEntranceAnimation() // the parent pane, otherwise use the bottom/right. This is always // the "outside" of the parent pane. childGrid.VerticalAlignment(isFirstChild ? VerticalAlignment::Top : VerticalAlignment::Bottom); - control.VerticalAlignment(VerticalAlignment::Top); - control.Height(isFirstChild ? totalSize : size); + if (control) + { + control.VerticalAlignment(VerticalAlignment::Top); + control.Height(isFirstChild ? totalSize : size); + } // When the animation is completed, undo the trickiness from before, to // restore the controls to the behavior they'd usually have. animation.Completed([childGrid, control, root = _secondChild->_root](auto&&, auto&&) { - control.Height(NAN); childGrid.Height(NAN); childGrid.VerticalAlignment(VerticalAlignment::Stretch); - control.VerticalAlignment(VerticalAlignment::Stretch); + if (control) + { + control.Height(NAN); + control.VerticalAlignment(VerticalAlignment::Stretch); + } root.Background(nullptr); }); } @@ -2025,41 +2282,38 @@ std::optional Pane::PreCalculateCanSplit(const std::shared_ptr targe const float splitSize, const winrt::Windows::Foundation::Size availableSpace) const { - if (_IsLeaf()) + if (target.get() == this) { - if (target.get() == this) + const auto firstPercent = 1.0f - splitSize; + const auto secondPercent = splitSize; + // If this pane is a leaf, and it's the pane we're looking for, use + // the available space to calculate which direction to split in. + const Size minSize = _GetMinSize(); + + if (splitType == SplitDirection::Left || splitType == SplitDirection::Right) { - const auto firstPrecent = 1.0f - splitSize; - const auto secondPercent = splitSize; - // If this pane is a leaf, and it's the pane we're looking for, use - // the available space to calculate which direction to split in. - const Size minSize = _GetMinSize(); + const auto widthMinusSeparator = availableSpace.Width - CombinedPaneBorderSize; + const auto newFirstWidth = widthMinusSeparator * firstPercent; + const auto newSecondWidth = widthMinusSeparator * secondPercent; - if (splitType == SplitDirection::Left || splitType == SplitDirection::Right) - { - const auto widthMinusSeparator = availableSpace.Width - CombinedPaneBorderSize; - const auto newFirstWidth = widthMinusSeparator * firstPrecent; - const auto newSecondWidth = widthMinusSeparator * secondPercent; - - return { newFirstWidth > minSize.Width && newSecondWidth > minSize.Width }; - } - - else if (splitType == SplitDirection::Up || splitType == SplitDirection::Down) - { - const auto heightMinusSeparator = availableSpace.Height - CombinedPaneBorderSize; - const auto newFirstHeight = heightMinusSeparator * firstPrecent; - const auto newSecondHeight = heightMinusSeparator * secondPercent; - - return { newFirstHeight > minSize.Height && newSecondHeight > minSize.Height }; - } + return { newFirstWidth > minSize.Width && newSecondWidth > minSize.Width }; } - else + + else if (splitType == SplitDirection::Up || splitType == SplitDirection::Down) { - // If this pane is _any other leaf_, then just return nullopt, to - // indicate that the `target` Pane is not down this branch. - return std::nullopt; + const auto heightMinusSeparator = availableSpace.Height - CombinedPaneBorderSize; + const auto newFirstHeight = heightMinusSeparator * firstPercent; + const auto newSecondHeight = heightMinusSeparator * secondPercent; + + return { newFirstHeight > minSize.Height && newSecondHeight > minSize.Height }; } } + else if (_IsLeaf()) + { + // If this pane is _any other leaf_, then just return nullopt, to + // indicate that the `target` Pane is not down this branch. + return std::nullopt; + } else { // If this pane is a parent, calculate how much space our children will @@ -2103,13 +2357,13 @@ std::pair, std::shared_ptr> Pane::Split(SplitDirecti const Profile& profile, const TermControl& control) { - if (!_IsLeaf()) + if (!_lastActive) { - if (_firstChild->_HasFocusedChild()) + if (_firstChild && _firstChild->_HasFocusedChild()) { return _firstChild->Split(splitType, splitSize, profile, control); } - else if (_secondChild->_HasFocusedChild()) + else if (_secondChild && _secondChild->_HasFocusedChild()) { return _secondChild->Split(splitType, splitSize, profile, control); } @@ -2135,11 +2389,11 @@ bool Pane::ToggleSplitOrientation() return false; } - // Check if either our first or second child is the currently focused leaf. - // If they are then switch the split orientation on the current pane. + // If a parent pane is focused, or if one of its children are a leaf and is + // focused then switch the split orientation on the current pane. const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive; const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive; - if (firstIsFocused || secondIsFocused) + if (_lastActive || firstIsFocused || secondIsFocused) { // Switch the split orientation _splitState = _splitState == SplitState::Horizontal ? SplitState::Vertical : SplitState::Horizontal; @@ -2207,46 +2461,66 @@ std::pair, std::shared_ptr> Pane::_Split(SplitDirect // modify our tree std::unique_lock lock{ _createCloseLock }; - // revoke our handler - the child will take care of the control now. - _control.ConnectionStateChanged(_connectionStateChangedToken); - _connectionStateChangedToken.value = 0; - _control.WarningBell(_warningBellToken); - _warningBellToken.value = 0; + if (_IsLeaf()) + { + // revoke our handler - the child will take care of the control now. + _control.ConnectionStateChanged(_connectionStateChangedToken); + _connectionStateChangedToken.value = 0; + _control.WarningBell(_warningBellToken); + _warningBellToken.value = 0; - // Remove our old GotFocus handler from the control. We don't what the - // control telling us that it's now focused, we want it telling its new - // parent. - _gotFocusRevoker.revoke(); - _lostFocusRevoker.revoke(); - - _splitState = actualSplitType; - _desiredSplitPosition = 1.0f - splitSize; + // Remove our old GotFocus handler from the control. We don't want the + // control telling us that it's now focused, we want it telling its new + // parent. + _gotFocusRevoker.revoke(); + _lostFocusRevoker.revoke(); + } // Remove any children we currently have. We can't add the existing // TermControl to a new grid until we do this. _root.Children().Clear(); - _border.Child(nullptr); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); - // Create two new Panes - // Move our control, guid into the first one. - // Move the new guid, control into the second. - _firstChild = std::make_shared(_profile, _control); - _firstChild->_connectionState = std::exchange(_connectionState, ConnectionState::NotConnected); + // Create a new pane from ourself + if (!_IsLeaf()) + { + // Since we are a parent we don't have borders normally, + // so set them temporarily for when we update our split definition. + _borders = _GetCommonBorders(); + _firstChild->Closed(_firstClosedToken); + _secondChild->Closed(_secondClosedToken); + // If we are not a leaf we should create a new pane that contains our children + auto first = std::make_shared(_firstChild, _secondChild, _splitState, _desiredSplitPosition); + _firstChild = first; + } + else + { + // Move our control, guid into the first one. + _firstChild = std::make_shared(_profile, _control); + _firstChild->_connectionState = std::exchange(_connectionState, ConnectionState::NotConnected); + _profile = nullptr; + _control = { nullptr }; + } + + _splitState = actualSplitType; + _desiredSplitPosition = 1.0f - splitSize; _secondChild = newPane; - // If we want the new pane to be the first child, swap the children if (splitType == SplitDirection::Up || splitType == SplitDirection::Left) { std::swap(_firstChild, _secondChild); } - _profile = nullptr; - _control = { nullptr }; - + _root.ColumnDefinitions().Clear(); + _root.RowDefinitions().Clear(); _CreateRowColDefinitions(); - _root.Children().Append(_firstChild->GetRootElement()); - _root.Children().Append(_secondChild->GetRootElement()); + _borderFirst.Child(_firstChild->GetRootElement()); + _borderSecond.Child(_secondChild->GetRootElement()); + + _root.Children().Append(_borderFirst); + _root.Children().Append(_borderSecond); _ApplySplitDefinitions(); @@ -2281,12 +2555,9 @@ std::pair, std::shared_ptr> Pane::_Split(SplitDirect // - void Pane::Maximize(std::shared_ptr zoomedPane) { - if (_IsLeaf()) - { - _zoomed = (zoomedPane == shared_from_this()); - _UpdateBorders(); - } - else + _zoomed = (zoomedPane == shared_from_this()); + _UpdateBorders(); + if (!_IsLeaf()) { if (zoomedPane == _firstChild || zoomedPane == _secondChild) { @@ -2294,6 +2565,8 @@ void Pane::Maximize(std::shared_ptr zoomedPane) // tree. Easy way: just remove both children. We'll re-attach both // when we un-zoom. _root.Children().Clear(); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); } // Always recurse into both children. If the (un)zoomed pane was one of @@ -2316,20 +2589,21 @@ void Pane::Maximize(std::shared_ptr zoomedPane) // - void Pane::Restore(std::shared_ptr zoomedPane) { - if (_IsLeaf()) - { - _zoomed = false; - _UpdateBorders(); - } - else + _zoomed = false; + _UpdateBorders(); + if (!_IsLeaf()) { if (zoomedPane == _firstChild || zoomedPane == _secondChild) { // When we're un-zooming the pane, we'll need to re-add it to our UI // tree where it originally belonged. easy way: just re-add both. _root.Children().Clear(); - _root.Children().Append(_firstChild->GetRootElement()); - _root.Children().Append(_secondChild->GetRootElement()); + + _borderFirst.Child(_firstChild->GetRootElement()); + _borderSecond.Child(_secondChild->GetRootElement()); + + _root.Children().Append(_borderFirst); + _root.Children().Append(_borderSecond); } // Always recurse into both children. If the (un)zoomed pane was one of @@ -2366,6 +2640,8 @@ void Pane::Id(uint32_t id) noexcept // - The ID of the pane we want to focus bool Pane::FocusPane(const uint32_t id) { + // Always clear the parent child path if we are focusing a leaf + _parentChildPath.reset(); if (_IsLeaf() && id == _id) { // Make sure to use _FocusFirstChild here - that'll properly update the @@ -2385,23 +2661,23 @@ bool Pane::FocusPane(const uint32_t id) } // Method Description: // - Focuses the given pane if it is in the tree. -// This deliberately mirrors FocusPane(id) instead of just calling -// _FocusFirstChild directly. +// - This is different than FocusPane(id) in that it allows focusing +// panes that are not leaves. // Arguments: // - the pane to focus // Return Value: // - true if focus was set bool Pane::FocusPane(const std::shared_ptr pane) { - if (_IsLeaf() && this == pane.get()) + if (this == pane.get()) { - // Make sure to use _FocusFirstChild here - that'll properly update the - // focus if we're in startup. - _FocusFirstChild(); + _Focus(); return true; } else { + // clear the parent child path if we are not the pane being focused. + _parentChildPath.reset(); if (_firstChild && _secondChild) { return _firstChild->FocusPane(pane) || @@ -2411,6 +2687,27 @@ bool Pane::FocusPane(const std::shared_ptr pane) return false; } +// Method Description: +// - Check if this pane contains the the argument as a child anywhere along the tree. +// Arguments: +// - child: the child to search for. +// Return Value: +// - true if the child was found. +bool Pane::_HasChild(const std::shared_ptr child) +{ + if (_IsLeaf()) + { + return false; + } + + if (_firstChild == child || _secondChild == child) + { + return true; + } + + return _firstChild->_HasChild(child) || _secondChild->_HasChild(child); +} + // Method Description: // - Recursive function that finds a pane with the given ID // Arguments: @@ -2906,20 +3203,17 @@ int Pane::GetLeafPaneCount() const noexcept std::optional Pane::PreCalculateAutoSplit(const std::shared_ptr target, const winrt::Windows::Foundation::Size availableSpace) const { - if (_IsLeaf()) + if (target.get() == this) { - if (target.get() == this) - { - //If this pane is a leaf, and it's the pane we're looking for, use - //the available space to calculate which direction to split in. - return availableSpace.Width > availableSpace.Height ? SplitDirection::Right : SplitDirection::Down; - } - else - { - // If this pane is _any other leaf_, then just return nullopt, to - // indicate that the `target` Pane is not down this branch. - return std::nullopt; - } + // If this pane is the pane we are looking for, use the available space + // to calculate which direction to split in. + return availableSpace.Width > availableSpace.Height ? SplitDirection::Right : SplitDirection::Down; + } + else if (_IsLeaf()) + { + // If this pane is _any other leaf_, then just return nullopt, to + // indicate that the `target` Pane is not down this branch. + return std::nullopt; } else { @@ -2971,7 +3265,7 @@ void Pane::CollectTaskbarStates(std::vector& s } } -DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate>); +DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, Pane::gotFocusArgs); DEFINE_EVENT(Pane, LostFocus, _LostFocusHandlers, winrt::delegate>); DEFINE_EVENT(Pane, PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); DEFINE_EVENT(Pane, Detached, _PaneDetachedHandlers, winrt::delegate>); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index ab1309f40..9892a6e8a 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -40,7 +40,8 @@ enum class Borders : int Top = 0x1, Bottom = 0x2, Left = 0x4, - Right = 0x8 + Right = 0x8, + All = 0xF }; DEFINE_ENUM_FLAG_OPERATORS(Borders); @@ -58,7 +59,14 @@ public: const winrt::Microsoft::Terminal::Control::TermControl& control, const bool lastFocused = false); + Pane(std::shared_ptr first, + std::shared_ptr second, + const SplitState splitType, + const float splitPosition, + const bool lastFocused = false); + std::shared_ptr GetActivePane(); + winrt::Microsoft::Terminal::Control::TermControl GetLastFocusedTerminalControl(); winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl(); winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile(); @@ -142,25 +150,43 @@ public: // - true if the predicate returned true on any pane. template //requires std::predicate> - bool WalkTree(F f) + auto WalkTree(F f) -> decltype(f(shared_from_this())) { - if (f(shared_from_this())) - { - return true; - } + using R = std::invoke_result_t>; + static constexpr auto IsVoid = std::is_void_v; - if (!_IsLeaf()) + if constexpr (IsVoid) { - return _firstChild->WalkTree(f) || _secondChild->WalkTree(f); + f(shared_from_this()); + if (!_IsLeaf()) + { + _firstChild->WalkTree(f); + _secondChild->WalkTree(f); + } } + else + { + if (f(shared_from_this())) + { + return true; + } - return false; + if (!_IsLeaf()) + { + return _firstChild->WalkTree(f) || _secondChild->WalkTree(f); + } + + return false; + } } void CollectTaskbarStates(std::vector& states); WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler); - DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate>); + + using gotFocusArgs = winrt::delegate, winrt::Windows::UI::Xaml::FocusState>; + + DECLARE_EVENT(GotFocus, _GotFocusHandlers, gotFocusArgs); DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate>); DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); DECLARE_EVENT(Detached, _PaneDetachedHandlers, winrt::delegate>); @@ -173,7 +199,8 @@ private: struct LayoutSizeNode; winrt::Windows::UI::Xaml::Controls::Grid _root{}; - winrt::Windows::UI::Xaml::Controls::Border _border{}; + winrt::Windows::UI::Xaml::Controls::Border _borderFirst{}; + winrt::Windows::UI::Xaml::Controls::Border _borderSecond{}; winrt::Microsoft::Terminal::Control::TermControl _control{ nullptr }; winrt::Microsoft::Terminal::TerminalConnection::ConnectionState _connectionState{ winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::NotConnected }; static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_focusedBorderBrush; @@ -185,6 +212,7 @@ private: float _desiredSplitPosition; std::optional _id; + std::weak_ptr _parentChildPath{}; bool _lastActive{ false }; winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr }; @@ -205,6 +233,7 @@ private: bool _IsLeaf() const noexcept; bool _HasFocusedChild() const noexcept; void _SetupChildCloseHandlers(); + bool _HasChild(const std::shared_ptr child); std::pair, std::shared_ptr> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType, const float splitSize, @@ -232,6 +261,7 @@ private: void _CloseChild(const bool closeFirst, const bool isDetaching); winrt::fire_and_forget _CloseChildRoutine(const bool closeFirst); + void _Focus(); void _FocusFirstChild(); void _ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/); void _ControlWarningBellHandler(winrt::Windows::Foundation::IInspectable const& sender, diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 272fa9df6..55e51abbb 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -190,6 +190,9 @@ Do you want to close all tabs? + + Multiple panes + Close... diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index e5d1cdc46..a339b2bfe 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -736,31 +736,32 @@ namespace winrt::TerminalApp::implementation { _UnZoomIfNeeded(); - auto pane = terminalTab->GetActivePane(); - if (const auto pane{ terminalTab->GetActivePane() }) { - if (const auto control{ pane->GetTerminalControl() }) + if (pane->ContainsReadOnly()) { - if (control.ReadOnly()) + ContentDialogResult warningResult = co_await _ShowCloseReadOnlyDialog(); + + // If the user didn't explicitly click on close tab - leave + if (warningResult != ContentDialogResult::Primary) { - ContentDialogResult warningResult = co_await _ShowCloseReadOnlyDialog(); - - // If the user didn't explicitly click on close tab - leave - if (warningResult != ContentDialogResult::Primary) - { - co_return; - } - - // Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab - if (control.ReadOnly()) - { - control.ToggleReadOnly(); - } + co_return; } - pane->Close(); + // Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab + pane->WalkTree([](auto p) { + if (const auto control{ p->GetTerminalControl() }) + { + if (control.ReadOnly()) + { + control.ToggleReadOnly(); + } + } + return false; + }); } + + pane->Close(); } } else if (auto index{ _GetFocusedTabIndex() }) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 8f776a2d7..26332453a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -262,6 +262,26 @@ namespace winrt::TerminalApp::implementation bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool _MovePane(const uint32_t tabIdx); + template + bool _ApplyToActiveControls(F f) + { + if (const auto tab{ _GetFocusedTabImpl() }) + { + if (const auto activePane = tab->GetActivePane()) + { + activePane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + f(control); + } + }); + + return true; + } + } + return false; + } + winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); std::optional _GetFocusedTabIndex() const noexcept; TerminalApp::TabBase _GetFocusedTab() const noexcept; @@ -365,7 +385,7 @@ namespace winrt::TerminalApp::implementation void _EndPreviewColorScheme(); void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args); winrt::Microsoft::Terminal::Settings::Model::Command _lastPreviewedCommand{ nullptr }; - winrt::Microsoft::Terminal::Settings::Model::TerminalSettings _originalSettings{ nullptr }; + std::vector> _restorePreviewFuncs{}; HRESULT _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index afb637810..8ef1847ba 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -67,8 +67,11 @@ namespace winrt::TerminalApp::implementation _rootPane->FocusPane(firstId); _activePane = _rootPane->GetActivePane(); } - // Set the active control - _mruPanes.insert(_mruPanes.begin(), _activePane->Id().value()); + // If the focused pane is a leaf, add it to the MRU panes + if (const auto id = _activePane->Id()) + { + _mruPanes.insert(_mruPanes.begin(), id.value()); + } _Setup(); } @@ -180,8 +183,8 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Returns nullptr if no children of this tab were the last control to be - // focused, or the TermControl that _was_ the last control to be focused (if - // there was one). + // focused, the active control of the current pane, or the last active child control + // of the active pane if it is a parent. // - This control might not currently be focused, if the tab itself is not // currently focused. // Arguments: @@ -193,7 +196,7 @@ namespace winrt::TerminalApp::implementation { if (_activePane) { - return _activePane->GetTerminalControl(); + return _activePane->GetLastFocusedTerminalControl(); } return nullptr; } @@ -390,6 +393,10 @@ namespace winrt::TerminalApp::implementation { return _runtimeTabText; } + if (!_activePane->_IsLeaf()) + { + return RS_(L"MultiplePanes"); + } const auto lastFocusedControl = GetActiveTerminalControl(); return lastFocusedControl ? lastFocusedControl.Title() : L""; } @@ -514,19 +521,14 @@ namespace winrt::TerminalApp::implementation // either the first or second child, but this will always return the // original pane first. auto [original, newPane] = _activePane->Split(splitType, splitSize, profile, control); + // The active pane has an id if it is a leaf if (activePaneId) { original->Id(activePaneId.value()); - newPane->Id(_nextPaneId); - ++_nextPaneId; - } - else - { - original->Id(_nextPaneId); - ++_nextPaneId; - newPane->Id(_nextPaneId); - ++_nextPaneId; } + newPane->Id(_nextPaneId); + ++_nextPaneId; + _activePane = original; // Add a event handlers to the new panes' GotFocus event. When the pane @@ -551,8 +553,8 @@ namespace winrt::TerminalApp::implementation // - The removed pane, if the remove succeeded. std::shared_ptr TerminalTab::DetachPane() { - // if we only have one pane, remove it entirely - // and close this tab + // if we only have one pane, or the focused pane is the root, remove it + // entirely and close this tab if (_rootPane == _activePane) { return DetachRoot(); @@ -627,16 +629,12 @@ namespace winrt::TerminalApp::implementation // Add the new pane as an automatic split on the active pane. auto first = _activePane->AttachPane(pane, SplitDirection::Automatic); - // under current assumptions this condition should always be true. + // This will be true if the original _activePane is a leaf pane. + // If it is a parent pane then we don't want to set an ID on it. if (previousId) { first->Id(previousId.value()); } - else - { - first->Id(_nextPaneId); - ++_nextPaneId; - } // Update with event handlers on the new child. _activePane = first; @@ -712,7 +710,10 @@ namespace winrt::TerminalApp::implementation // throughout the entire tree. if (const auto newFocus = _rootPane->NavigateDirection(_activePane, direction, _mruPanes)) { + // Mark that we want the active pane to changed + _changingActivePane = true; const auto res = _rootPane->FocusPane(newFocus); + _changingActivePane = false; if (_zoomedPane) { @@ -735,11 +736,22 @@ namespace winrt::TerminalApp::implementation // - true if two panes were swapped. bool TerminalTab::SwapPane(const FocusDirection& direction) { + // You cannot swap panes with the parent/child pane because of the + // circular reference. + if (direction == FocusDirection::Parent || direction == FocusDirection::Child) + { + return false; + } // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. if (auto neighbor = _rootPane->NavigateDirection(_activePane, direction, _mruPanes)) { - return _rootPane->SwapPanes(_activePane, neighbor); + // SwapPanes will refocus the terminal to make sure that it has focus + // even after moving. + _changingActivePane = true; + const auto res = _rootPane->SwapPanes(_activePane, neighbor); + _changingActivePane = false; + return res; } return false; @@ -747,7 +759,10 @@ namespace winrt::TerminalApp::implementation bool TerminalTab::FocusPane(const uint32_t id) { - return _rootPane->FocusPane(id); + _changingActivePane = true; + const auto res = _rootPane->FocusPane(id); + _changingActivePane = false; + return res; } // Method Description: @@ -1040,7 +1055,7 @@ namespace winrt::TerminalApp::implementation auto weakThis{ get_weak() }; std::weak_ptr weakPane{ pane }; - auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr sender) { + auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr sender, WUX::FocusState focus) { // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; @@ -1048,8 +1063,20 @@ namespace winrt::TerminalApp::implementation { if (sender != tab->_activePane) { - tab->_UpdateActivePane(sender); - tab->_RecalculateAndApplyTabColor(); + auto senderIsChild = tab->_activePane->_HasChild(sender); + + // Only move focus if we the program moved focus, or the + // user moved with their mouse. This is a problem because a + // pane isn't a control itself, and if we have the parent + // focused we are fine if the terminal control is focused, + // but we don't want to update the active pane. + if (!senderIsChild || + (focus == WUX::FocusState::Programmatic && tab->_changingActivePane) || + focus == WUX::FocusState::Pointer) + { + tab->_UpdateActivePane(sender); + tab->_RecalculateAndApplyTabColor(); + } } tab->_focusState = WUX::FocusState::Programmatic; // This tab has gained focus, remove the bell indicator if it is active @@ -1084,8 +1111,19 @@ namespace winrt::TerminalApp::implementation tab->Content(tab->_rootPane->GetRootElement()); tab->ExitZoom(); } + if (auto pane = weakPane.lock()) { + // When a parent pane is selected, but one of its children + // close out under it we still need to update title/focus information + // but the GotFocus handler will rightly see that the _activePane + // did not actually change. Triggering + if (pane != tab->_activePane && !tab->_activePane->_IsLeaf()) + { + co_await winrt::resume_foreground(tab->Content().Dispatcher()); + tab->_UpdateActivePane(tab->_activePane); + } + for (auto i = tab->_mruPanes.begin(); i != tab->_mruPanes.end(); ++i) { if (*i == pane->Id()) @@ -1303,11 +1341,13 @@ namespace winrt::TerminalApp::implementation // - The tab's color, if any std::optional TerminalTab::GetTabColor() { - const auto currControlColor{ GetActiveTerminalControl().TabColor() }; std::optional controlTabColor; - if (currControlColor != nullptr) + if (const auto& control = GetActiveTerminalControl()) { - controlTabColor = currControlColor.Value(); + if (const auto color = control.TabColor()) + { + controlTabColor = color.Value(); + } } // A Tab's color will be the result of layering a variety of sources, @@ -1598,6 +1638,11 @@ namespace winrt::TerminalApp::implementation void TerminalTab::EnterZoom() { + // Clear the content first, because with parent focusing it is possible + // to zoom the root pane, but setting the content will not trigger the + // property changed event since it is the same and you would end up with + // an empty tab. + Content(nullptr); _zoomedPane = _activePane; _rootPane->Maximize(_zoomedPane); // Update the tab header to show the magnifying glass @@ -1606,6 +1651,7 @@ namespace winrt::TerminalApp::implementation } void TerminalTab::ExitZoom() { + Content(nullptr); _rootPane->Restore(_zoomedPane); _zoomedPane = nullptr; // Update the tab header to hide the magnifying glass @@ -1620,13 +1666,34 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Toggle read-only mode on the active pane + // - If a parent pane is selected, this will ensure that all children have + // the same read-only status. void TerminalTab::TogglePaneReadOnly() { - auto control = GetActiveTerminalControl(); - if (control) - { - control.ToggleReadOnly(); - } + auto hasReadOnly = false; + auto allReadOnly = true; + _activePane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + hasReadOnly |= control.ReadOnly(); + allReadOnly &= control.ReadOnly(); + } + }); + _activePane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + // If all controls have the same read only state then just toggle + if (allReadOnly || !hasReadOnly) + { + control.ToggleReadOnly(); + } + // otherwise set to all read only. + else if (!control.ReadOnly()) + { + control.ToggleReadOnly(); + } + } + }); } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 3dbeaa5aa..4c071c0b0 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -139,6 +139,7 @@ namespace winrt::TerminalApp::implementation bool _receivedKeyDown{ false }; bool _iconHidden{ false }; + bool _changingActivePane{ false }; winrt::hstring _runtimeTabText{}; bool _inRename{ false }; diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 1e41fdb5c..534862698 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -469,6 +469,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _renderEngine->ToggleShaderEffects(); } + // Always redraw after toggling effects. This way even if the control + // does not have focus it will update immediately. + _renderer->TriggerRedrawAll(); } // Method Description: diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index bf60db47e..d713825cd 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -298,6 +298,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return RS_(L"MoveFocusPreviousInOrder"); case FocusDirection::First: return RS_(L"MoveFocusFirstPane"); + case FocusDirection::Parent: + return RS_(L"MoveFocusParentPane"); + case FocusDirection::Child: + return RS_(L"MoveFocusChildPane"); } return winrt::hstring{ diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 2ea94e84d..457feefce 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -38,7 +38,9 @@ namespace Microsoft.Terminal.Settings.Model Previous, PreviousInOrder, NextInOrder, - First + First, + Parent, + Child }; enum SplitDirection diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 3ced7eb85..5538d5d22 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -258,6 +258,12 @@ Move focus to the first pane + + Move focus to the parent pane + + + Move focus to the child pane + Swap pane diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index a4503efb7..da934e464 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -373,7 +373,7 @@ struct IntAsFloatPercentConversionTrait : ::Microsoft::Terminal::Settings::Model // Possible FocusDirection values JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FocusDirection) { - JSON_MAPPINGS(8) = { + JSON_MAPPINGS(10) = { pair_type{ "left", ValueType::Left }, pair_type{ "right", ValueType::Right }, pair_type{ "up", ValueType::Up }, @@ -382,6 +382,8 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FocusDirection) pair_type{ "previousInOrder", ValueType::PreviousInOrder }, pair_type{ "nextInOrder", ValueType::NextInOrder }, pair_type{ "first", ValueType::First }, + pair_type{ "parent", ValueType::Parent }, + pair_type{ "child", ValueType::Child }, }; }; diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index d6b45016f..fb2078966 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -354,6 +354,8 @@ { "command": { "action": "moveFocus", "direction": "previousInOrder" } }, { "command": { "action": "moveFocus", "direction": "nextInOrder" } }, { "command": { "action": "moveFocus", "direction": "first" } }, + { "command": { "action": "moveFocus", "direction": "parent" } }, + { "command": { "action": "moveFocus", "direction": "child" } }, { "command": { "action": "swapPane", "direction": "down" } }, { "command": { "action": "swapPane", "direction": "left" } }, { "command": { "action": "swapPane", "direction": "right" } }, diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index de33cd00c..fc8111a63 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -247,6 +247,7 @@ bool DxEngine::_HasTerminalEffects() const noexcept void DxEngine::ToggleShaderEffects() { _terminalEffectsEnabled = !_terminalEffectsEnabled; + _recreateDeviceRequested = true; LOG_IF_FAILED(InvalidateAll()); } From 3b3b72e9cf69d9f26b5763f366510076ba7acb01 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 28 Sep 2021 14:43:51 -0500 Subject: [PATCH 18/35] Replace `null` with `"null"` for types in the schema (#11350) --- doc/cascadia/profiles.schema.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 0a399f17d..8b441d990 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -110,7 +110,7 @@ "description": "Sets the file location of the image to draw over the window background when unfocused.", "oneOf": [ { - "type": ["string", null] + "type": ["string", "null"] }, { "enum": [ @@ -531,13 +531,13 @@ "type": "integer", "default": 0, "description": "Which tab to switch to, with the first being 0" + } } } - } - ], - "required": [ "index" ] - }, - "MovePaneAction": { + ], + "required": [ "index" ] + }, + "MovePaneAction": { "description": "Arguments corresponding to a Move Pane Action", "allOf": [ { "$ref": "#/definitions/ShortcutAction" }, @@ -1444,7 +1444,7 @@ "description": "Sets the file location of the image to draw over the window background.", "oneOf": [ { - "type": ["string", null] + "type": ["string", "null"] }, { "enum": [ From 856f8764ce10cbd17962d619686be7969c262615 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 29 Sep 2021 12:23:38 +0200 Subject: [PATCH 19/35] Fix compatibility issues with profiles.schema.json (#11360) This fixes two issues with profiles.schema.json: * The `$schema` should not end in a `#` * `$defs` is the official reserved keyword for schema re-use See: http://json-schema.org/draft/2020-12/json-schema-core.html ## PR Checklist * [x] I work here * [x] Tests added/passed * [x] Schema updated ## Validation Steps Performed The previous schema didn't pass https://jschon.dev/, the new schema does. --- doc/cascadia/profiles.schema.json | 787 +++++++++++++++++++++--------- 1 file changed, 569 insertions(+), 218 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 8b441d990..ba3387d84 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1,8 +1,8 @@ { "$id": "https://github.com/microsoft/terminal/blob/main/doc/cascadia/profiles.schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Microsoft's Windows Terminal Settings Profile Schema", - "definitions": { + "$defs": { "KeyChordSegment": { "pattern": "^(?:(?:ctrl|alt|shift|win)\\+)*(?:app|backspace|browser_(?:back|forward|refresh|stop|search|favorites|home)|comma|delete|down|end|enter|esc|escape|home|insert|left|menu|minus|pagedown|pageup|period|pgdn|pgup|plus|right|space|tab|up|f(?:1\\d?|2[0-4]?|[3-9])|numpad\\d|numpad_(?:\\d|add|decimal|divide|minus|multiply|period|plus|subtract)|(?:vk|sc)\\((?:[1-9]|1?\\d{2}|2[0-4]\\d|25[0-5])\\)|[^\\s+])(?:\\+(?:ctrl|alt|shift|win))*$", "type": "string", @@ -61,28 +61,42 @@ "type": "string" }, "foreground": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#cccccc", "description": "Sets the text color when unfocused. Overrides \"foreground\" from the color scheme. Uses hex color format: \"#rrggbb\".", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "background": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#0c0c0c", "description": "Sets the background color of the text when unfocused. Overrides \"background\" from the color scheme. Uses hex color format: \"#rrggbb\".", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "selectionBackground": { "oneOf": [ - {"$ref": "#/definitions/Color"}, - { "type": "null" } + { + "$ref": "#/$defs/Color" + }, + { + "type": "null" + } ], "description": "Sets the background color of selected text when unfocused. Overrides selectionBackground set in the color scheme. Uses hex color format: \"#rrggbb\"." }, "cursorColor": { "oneOf": [ - { "$ref": "#/definitions/Color" }, - {"type": "null"} + { + "$ref": "#/$defs/Color" + }, + { + "type": "null" + } ], "description": "Sets the color of the cursor when unfocused. Overrides the cursor color from the color scheme. Uses hex color format: \"#rrggbb\"." }, @@ -103,14 +117,20 @@ "description": "Sets the percentage height of the cursor (when unfocused) starting from the bottom. Only works when cursorShape is set to \"vintage\". Accepts values from 1-100.", "maximum": 100, "minimum": 1, - "type": ["integer","null"], + "type": [ + "integer", + "null" + ], "default": 25 }, "backgroundImage": { "description": "Sets the file location of the image to draw over the window background when unfocused.", "oneOf": [ { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, { "enum": [ @@ -118,7 +138,10 @@ ] } ], - "type": [ "string", "null" ] + "type": [ + "string", + "null" + ] }, "backgroundImageOpacity": { "default": 1.0, @@ -220,7 +243,9 @@ "description": "Sets the DWrite font features for the given font. For example, { \"ss01\": 1, \"liga\":0 } will enable ss01 and disable ligatures.", "type": "object", "patternProperties": { - "^(([A-Za-z0-9]){4})$": { "type": "integer" } + "^(([A-Za-z0-9]){4})$": { + "type": "integer" + } }, "additionalProperties": false }, @@ -228,7 +253,9 @@ "description": "Sets the DWrite font axes for the given font. For example, { \"wght\": 200 } will set the font weight to 200.", "type": "object", "patternProperties": { - "^([A-Za-z]{4})$": { "type": "number" } + "^([A-Za-z]{4})$": { + "type": "number" + } }, "additionalProperties": false } @@ -422,7 +449,7 @@ "description": "The index of the profile in the new tab dropdown (starting at 0)" }, "tabColor": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": null, "description": "If provided, will set the tab's color to the given value" }, @@ -438,9 +465,11 @@ }, "type": "object" }, - "SwitchToAdjacentTabArgs" : { + "SwitchToAdjacentTabArgs": { "oneOf": [ - { "type": "null" }, + { + "type": "null" + }, { "enum": [ "mru", @@ -455,7 +484,7 @@ "properties": { "action": { "description": "The action to execute", - "$ref": "#/definitions/ShortcutActionName" + "$ref": "#/$defs/ShortcutActionName" } }, "required": [ @@ -466,10 +495,15 @@ "AdjustFontSizeAction": { "description": "Arguments corresponding to an Adjust Font Size Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "adjustFontSize" }, + "action": { + "type": "string", + "pattern": "adjustFontSize" + }, "delta": { "type": "integer", "default": 0, @@ -478,15 +512,22 @@ } } ], - "required": [ "delta" ] + "required": [ + "delta" + ] }, "CopyAction": { "description": "Arguments corresponding to a Copy Text Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "copy" }, + "action": { + "type": "string", + "pattern": "copy" + }, "singleLine": { "type": "boolean", "default": false, @@ -497,7 +538,7 @@ "description": "When set to `true`, the color and font formatting of selected text is also copied to your clipboard. When set to `false`, only plain text is copied to your clipboard. An array of specific formats can also be used. Supported array values include `html` and `rtf`. Plain text is always copied. Not setting this value inherits the behavior of the `copyFormatting` global setting.", "oneOf": [ { - "$ref": "#/definitions/CopyFormat" + "$ref": "#/$defs/CopyFormat" }, { "type": "null" @@ -511,11 +552,18 @@ "NewTabAction": { "description": "Arguments corresponding to a New Tab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, - { "$ref": "#/definitions/NewTerminalArgs" }, + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "$ref": "#/$defs/NewTerminalArgs" + }, { "properties": { - "action": { "type":"string", "pattern": "newTab" } + "action": { + "type": "string", + "pattern": "newTab" + } } } ] @@ -523,10 +571,15 @@ "SwitchToTabAction": { "description": "Arguments corresponding to a Switch To Tab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "switchToTab" }, + "action": { + "type": "string", + "pattern": "switchToTab" + }, "index": { "type": "integer", "default": 0, @@ -535,15 +588,22 @@ } } ], - "required": [ "index" ] + "required": [ + "index" + ] }, "MovePaneAction": { "description": "Arguments corresponding to a Move Pane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "movePane" }, + "action": { + "type": "string", + "pattern": "movePane" + }, "index": { "type": "integer", "default": 0, @@ -552,68 +612,94 @@ } } ], - "required": [ "index" ] + "required": [ + "index" + ] }, "MoveFocusAction": { "description": "Arguments corresponding to a Move Focus Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "moveFocus" }, + "action": { + "type": "string", + "pattern": "moveFocus" + }, "direction": { - "$ref": "#/definitions/FocusDirection", + "$ref": "#/$defs/FocusDirection", "default": "left", "description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane, 'nextInOrder' or 'previousInOrder' to move to the next or previous pane, 'first' to focus the first pane, or 'parent' or 'child' to move up and down the tree." } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "SwapPaneAction": { "description": "Arguments corresponding to a Swap Pane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "swapPane" }, + "action": { + "type": "string", + "pattern": "swapPane" + }, "direction": { - "$ref": "#/definitions/FocusDirection", + "$ref": "#/$defs/FocusDirection", "default": "left", "description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane, 'nextInOrder' or 'previousInOrder' to move to the next or previous pane, or 'first' to swap with the first pane." } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "ResizePaneAction": { "description": "Arguments corresponding to a Resize Pane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "resizePane" }, + "action": { + "type": "string", + "pattern": "resizePane" + }, "direction": { - "$ref": "#/definitions/ResizeDirection", + "$ref": "#/$defs/ResizeDirection", "default": "left", "description": "The direction to move the pane separator in." } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "SendInputAction": { "description": "Arguments corresponding to a Send Input Action", "allOf": [ { - "$ref": "#/definitions/ShortcutAction" + "$ref": "#/$defs/ShortcutAction" }, { "properties": { - "action": { "type": "string", "pattern": "sendInput" }, + "action": { + "type": "string", + "pattern": "sendInput" + }, "input": { "type": "string", "default": "", @@ -622,18 +708,27 @@ } } ], - "required": [ "input" ] + "required": [ + "input" + ] }, "SplitPaneAction": { "description": "Arguments corresponding to a Split Pane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, - { "$ref": "#/definitions/NewTerminalArgs" }, + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "$ref": "#/$defs/NewTerminalArgs" + }, { "properties": { - "action": { "type": "string", "pattern": "splitPane" }, + "action": { + "type": "string", + "pattern": "splitPane" + }, "split": { - "$ref": "#/definitions/SplitDirection", + "$ref": "#/$defs/SplitDirection", "default": "auto", "description": "The orientation to split the pane in. Possible values:\n -\"auto\" (splits pane based on remaining space)\n -\"up\" (think [-] and above)\n -\"down\" (think [-] and below)\n -\"left\" (think [|] and to the left)\n -\"right\"(think [|] and to the right)" }, @@ -656,11 +751,14 @@ "description": "Arguments corresponding to a Open Settings Action", "allOf": [ { - "$ref": "#/definitions/ShortcutAction" + "$ref": "#/$defs/ShortcutAction" }, { "properties": { - "action": { "type": "string", "pattern": "openSettings" }, + "action": { + "type": "string", + "pattern": "openSettings" + }, "target": { "type": "string", "default": "settingsUI", @@ -670,7 +768,6 @@ "defaultsFile", "allFiles", "settingsUI" - ] } } @@ -680,12 +777,17 @@ "SetTabColorAction": { "description": "Arguments corresponding to a Set Tab Color Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "setTabColor" }, + "action": { + "type": "string", + "pattern": "setTabColor" + }, "color": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": null, "description": "If provided, will set the tab's color to the given value. If omitted, will reset the tab's color." } @@ -696,10 +798,15 @@ "SetColorSchemeAction": { "description": "Arguments corresponding to a Set Color Scheme Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "setColorScheme" }, + "action": { + "type": "string", + "pattern": "setColorScheme" + }, "colorScheme": { "type": "string", "default": "", @@ -708,15 +815,22 @@ } } ], - "required": [ "colorScheme" ] + "required": [ + "colorScheme" + ] }, "WtAction": { "description": "Arguments corresponding to a wt Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "wt" }, + "action": { + "type": "string", + "pattern": "wt" + }, "commandline": { "type": "string", "default": "", @@ -725,19 +839,30 @@ } } ], - "required": [ "commandline" ] + "required": [ + "commandline" + ] }, "CloseOtherTabsAction": { "description": "Arguments for a closeOtherTabs action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "closeOtherTabs" }, + "action": { + "type": "string", + "pattern": "closeOtherTabs" + }, "index": { "oneOf": [ - { "type": "integer" }, - { "type": "null" } + { + "type": "integer" + }, + { + "type": "null" + } ], "default": null, "description": "Close the tabs other than the one at this index. If no index is provided, use the focused tab's index." @@ -749,14 +874,23 @@ "CloseTabsAfterAction": { "description": "Arguments for a closeTabsAfter action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "closeTabsAfter" }, + "action": { + "type": "string", + "pattern": "closeTabsAfter" + }, "index": { "oneOf": [ - { "type": "integer" }, - { "type": "null" } + { + "type": "integer" + }, + { + "type": "null" + } ], "default": null, "description": "Close the tabs following the tab at this index. If no index is provided, use the focused tab's index." @@ -768,14 +902,23 @@ "CloseTabAction": { "description": "Arguments for a closeTab action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "closeTab" }, + "action": { + "type": "string", + "pattern": "closeTab" + }, "index": { "oneOf": [ - { "type": "integer" }, - { "type": "null" } + { + "type": "integer" + }, + { + "type": "null" + } ], "default": null, "description": "Close the tab at this index. If no index is provided, use the focused tab's index." @@ -787,12 +930,20 @@ "ScrollUpAction": { "description": "Arguments for a scrollUp action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "scrollUp" }, + "action": { + "type": "string", + "pattern": "scrollUp" + }, "rowsToScroll": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "default": null, "description": "Scroll up rowsToScroll lines. If no value is provided, use the system-level defaults." } @@ -803,12 +954,20 @@ "ScrollDownAction": { "description": "Arguments for a scrollDown action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "scrollDown" }, + "action": { + "type": "string", + "pattern": "scrollDown" + }, "rowsToScroll": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "default": null, "description": "Scroll down rowsToScroll lines. If no value is provided, use the system-level defaults." } @@ -819,28 +978,40 @@ "MoveTabAction": { "description": "Arguments for moving a tab", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "moveTab" }, + "action": { + "type": "string", + "pattern": "moveTab" + }, "direction": { - "$ref": "#/definitions/MoveTabDirection", + "$ref": "#/$defs/MoveTabDirection", "description": "The direction to move the tab" } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "MultipleActionsAction": { "description": "Arguments for the multiple actions command", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "multipleActions" }, - "actions" : { - "$ref": "#/definitions/ShortcutAction", + "action": { + "type": "string", + "pattern": "multipleActions" + }, + "actions": { + "$ref": "#/$defs/ShortcutAction", "type": "array", "minItems": 1, "description": "A list of other actions." @@ -848,17 +1019,24 @@ } } ], - "required": [ "actions" ] + "required": [ + "actions" + ] }, "CommandPaletteAction": { "description": "Arguments for a commandPalette action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "commandPalette" }, + "action": { + "type": "string", + "pattern": "commandPalette" + }, "launchMode": { - "$ref": "#/definitions/CommandPaletteLaunchMode", + "$ref": "#/$defs/CommandPaletteLaunchMode", "default": "action", "description": "Toggle command palette in either action or command line mode. If no value is provided, the palette will launch in action mode." } @@ -869,28 +1047,42 @@ "FindMatchAction": { "description": "Arguments corresponding to a Find Match Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "findMatch" }, + "action": { + "type": "string", + "pattern": "findMatch" + }, "direction": { - "$ref": "#/definitions/FindMatchDirection", + "$ref": "#/$defs/FindMatchDirection", "default": "prev", "description": "The direction to search in. \"prev\" will search upwards in the buffer, and \"next\" will search downwards." } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "NewWindowAction": { "description": "Arguments corresponding to a New Window Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, - { "$ref": "#/definitions/NewTerminalArgs" }, + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "$ref": "#/$defs/NewTerminalArgs" + }, { "properties": { - "action": { "type":"string", "pattern": "newWindow" } + "action": { + "type": "string", + "pattern": "newWindow" + } } } ] @@ -898,12 +1090,17 @@ "PrevTabAction": { "description": "Arguments corresponding to a Previous Tab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type":"string", "pattern": "prevTab" }, + "action": { + "type": "string", + "pattern": "prevTab" + }, "tabSwitcherMode": { - "$ref": "#/definitions/SwitchToAdjacentTabArgs", + "$ref": "#/$defs/SwitchToAdjacentTabArgs", "default": null, "description": "Move to the previous tab using \"tabSwitcherMode\". If no mode is provided, use the one globally defined one." } @@ -914,12 +1111,17 @@ "NextTabAction": { "description": "Arguments corresponding to a Next Tab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type":"string", "pattern": "nextTab" }, + "action": { + "type": "string", + "pattern": "nextTab" + }, "tabSwitcherMode": { - "$ref": "#/definitions/SwitchToAdjacentTabArgs", + "$ref": "#/$defs/SwitchToAdjacentTabArgs", "default": null, "description": "Move to the next tab using \"tabSwitcherMode\". If no mode is provided, use the one globally defined one." } @@ -930,10 +1132,15 @@ "RenameTabAction": { "description": "Arguments corresponding to a renameTab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "renameTab" }, + "action": { + "type": "string", + "pattern": "renameTab" + }, "title": { "type": "string", "default": "", @@ -946,10 +1153,15 @@ "RenameWindowAction": { "description": "Arguments corresponding to a renameWindow Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "renameWindow" }, + "action": { + "type": "string", + "pattern": "renameWindow" + }, "name": { "type": "string", "default": "", @@ -962,10 +1174,15 @@ "FocusPaneAction": { "description": "Arguments corresponding to a focusPane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "focusPane" }, + "action": { + "type": "string", + "pattern": "focusPane" + }, "id": { "type": "string", "default": "", @@ -978,10 +1195,15 @@ "GlobalSummonAction": { "description": "This is a special action that works globally in the OS, rather than only in the context of the terminal window. When pressed, this action will summon the terminal window.", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "globalSummon" }, + "action": { + "type": "string", + "pattern": "globalSummon" + }, "desktop": { "type": "string", "default": "toCurrent", @@ -1024,10 +1246,15 @@ "QuakeModeAction": { "description": "This action is a special variation of the globalSummon action. It specifically summons a window called \"_quake\". If you would like to change the behavior of the quakeMode action, we recommended creating a new globalSummon entry.", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "quakeMode" } + "action": { + "type": "string", + "pattern": "quakeMode" + } } } ] @@ -1037,56 +1264,120 @@ "properties": { "command": { "description": "The action executed when the associated key bindings are pressed.", - "oneOf": [ - { "$ref": "#/definitions/AdjustFontSizeAction" }, - { "$ref": "#/definitions/CopyAction" }, - { "$ref": "#/definitions/ShortcutActionName" }, - { "$ref": "#/definitions/NewTabAction" }, - { "$ref": "#/definitions/SwitchToTabAction" }, - { "$ref": "#/definitions/MoveFocusAction" }, - { "$ref": "#/definitions/MovePaneAction" }, - { "$ref": "#/definitions/SwapPaneAction" }, - { "$ref": "#/definitions/ResizePaneAction" }, - { "$ref": "#/definitions/SendInputAction" }, - { "$ref": "#/definitions/SplitPaneAction" }, - { "$ref": "#/definitions/OpenSettingsAction" }, - { "$ref": "#/definitions/SetTabColorAction" }, - { "$ref": "#/definitions/SetColorSchemeAction" }, - { "$ref": "#/definitions/WtAction" }, - { "$ref": "#/definitions/CloseOtherTabsAction" }, - { "$ref": "#/definitions/CloseTabsAfterAction" }, - { "$ref": "#/definitions/CloseTabAction" }, - { "$ref": "#/definitions/ScrollUpAction" }, - { "$ref": "#/definitions/ScrollDownAction" }, - { "$ref": "#/definitions/MoveTabAction" }, - { "$ref": "#/definitions/FindMatchAction" }, - { "$ref": "#/definitions/NewWindowAction" }, - { "$ref": "#/definitions/NextTabAction" }, - { "$ref": "#/definitions/PrevTabAction" }, - { "$ref": "#/definitions/RenameTabAction" }, - { "$ref": "#/definitions/RenameWindowAction" }, - { "$ref": "#/definitions/FocusPaneAction" }, - { "$ref": "#/definitions/GlobalSummonAction" }, - { "$ref": "#/definitions/QuakeModeAction" }, - { "type": "null" } - ] + "oneOf": [ + { + "$ref": "#/$defs/AdjustFontSizeAction" + }, + { + "$ref": "#/$defs/CopyAction" + }, + { + "$ref": "#/$defs/ShortcutActionName" + }, + { + "$ref": "#/$defs/NewTabAction" + }, + { + "$ref": "#/$defs/SwitchToTabAction" + }, + { + "$ref": "#/$defs/MoveFocusAction" + }, + { + "$ref": "#/$defs/MovePaneAction" + }, + { + "$ref": "#/$defs/SwapPaneAction" + }, + { + "$ref": "#/$defs/ResizePaneAction" + }, + { + "$ref": "#/$defs/SendInputAction" + }, + { + "$ref": "#/$defs/SplitPaneAction" + }, + { + "$ref": "#/$defs/OpenSettingsAction" + }, + { + "$ref": "#/$defs/SetTabColorAction" + }, + { + "$ref": "#/$defs/SetColorSchemeAction" + }, + { + "$ref": "#/$defs/WtAction" + }, + { + "$ref": "#/$defs/CloseOtherTabsAction" + }, + { + "$ref": "#/$defs/CloseTabsAfterAction" + }, + { + "$ref": "#/$defs/CloseTabAction" + }, + { + "$ref": "#/$defs/ScrollUpAction" + }, + { + "$ref": "#/$defs/ScrollDownAction" + }, + { + "$ref": "#/$defs/MoveTabAction" + }, + { + "$ref": "#/$defs/FindMatchAction" + }, + { + "$ref": "#/$defs/NewWindowAction" + }, + { + "$ref": "#/$defs/NextTabAction" + }, + { + "$ref": "#/$defs/PrevTabAction" + }, + { + "$ref": "#/$defs/RenameTabAction" + }, + { + "$ref": "#/$defs/RenameWindowAction" + }, + { + "$ref": "#/$defs/FocusPaneAction" + }, + { + "$ref": "#/$defs/GlobalSummonAction" + }, + { + "$ref": "#/$defs/QuakeModeAction" + }, + { + "type": "null" + } + ] }, "keys": { "description": "Defines the key combinations used to call the command. It must be composed of...\n -any number of modifiers (ctrl/alt/shift)\n -a non-modifier key", "oneOf": [ { - "$ref": "#/definitions/KeyChordSegment" + "$ref": "#/$defs/KeyChordSegment" }, { "items": { - "$ref": "#/definitions/KeyChordSegment" + "$ref": "#/$defs/KeyChordSegment" }, "minItems": 1, "type": "array" } ] }, - "icon": { "$ref": "#/definitions/Icon" }, + "icon": { + "$ref": "#/$defs/Icon" + }, "name": { "description": "The name that will appear in the command palette. If one isn't provided, the terminal will attempt to automatically generate a name.\nIf name is a string, it will be the name of the command.\nIf name is a object, the key property of the object will be used to lookup a localized string resource for the command", "properties": { @@ -1111,15 +1402,24 @@ "commands": { "description": "List of commands to execute", "items": { - "$ref": "#/definitions/Keybinding/properties/command" + "$ref": "#/$defs/Keybinding/properties/command" }, "minItems": 1, "type": "array" } }, "anyOf": [ - {"required": ["name","commands"]}, - {"required": ["command"]} + { + "required": [ + "name", + "commands" + ] + }, + { + "required": [ + "command" + ] + } ], "type": "object" }, @@ -1160,7 +1460,7 @@ "copyFormatting": { "default": true, "description": "When set to `true`, the color and font formatting of selected text is also copied to your clipboard. When set to `false`, only plain text is copied to your clipboard. An array of specific formats can also be used. Supported array values include `html` and `rtf`. Plain text is always copied.", - "$ref": "#/definitions/CopyFormat" + "$ref": "#/$defs/CopyFormat" }, "trimBlockSelection": { "default": false, @@ -1198,7 +1498,7 @@ "disabledProfileSources": { "description": "Disables all the dynamic profile generators in this list, preventing them from adding their profiles to the list of profiles on startup.", "items": { - "$ref": "#/definitions/DynamicProfileSource" + "$ref": "#/$defs/DynamicProfileSource" }, "type": "array" }, @@ -1222,7 +1522,7 @@ "type": "integer" }, "initialPosition": { - "$ref": "#/definitions/Coordinates", + "$ref": "#/$defs/Coordinates", "description": "The position of the top left corner of the window upon first load. On a system with multiple displays, these coordinates are relative to the top left of the primary display. If \"launchMode\" is set to \"maximized\" (or \"maximizedFocus\"), the window will be maximized on the monitor specified by those coordinates." }, "initialRows": { @@ -1263,7 +1563,10 @@ "description": "This parameter once allowed you to override the systemwide \"choose how many lines to scroll at one time\" setting. It no longer does so. However, you can customize the number of lines to scroll in \"scrollUp\" and \"scrollDown\" bindings.", "maximum": 999, "minimum": 0, - "type": [ "integer", "string" ], + "type": [ + "integer", + "string" + ], "deprecated": true }, "minimizeToNotificationArea": { @@ -1286,18 +1589,18 @@ "description": "When set to true, the tab row will have an acrylic background with 50% opacity.", "type": "boolean" }, - "actions": { - "description": "Properties are specific to each custom action.", - "items": { - "$ref": "#/definitions/Keybinding" - }, - "type": "array" - }, + "actions": { + "description": "Properties are specific to each custom action.", + "items": { + "$ref": "#/$defs/Keybinding" + }, + "type": "array" + }, "keybindings": { "description": "[deprecated] Use actions instead.", "deprecated": true, "items": { - "$ref": "#/definitions/Keybinding" + "$ref": "#/$defs/Keybinding" }, "type": "array" }, @@ -1425,26 +1728,38 @@ "type": "string" }, "background": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#0c0c0c", "description": "Sets the background color of the text. Overrides \"background\" from the color scheme. Uses hex color format: \"#rrggbb\".", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "unfocusedAppearance": { - "$ref": "#/definitions/AppearanceConfig", + "$ref": "#/$defs/AppearanceConfig", "description": "Sets the appearance of the terminal when it is unfocused.", - "type": ["object", "null"] + "type": [ + "object", + "null" + ] }, "font": { - "$ref": "#/definitions/FontConfig", + "$ref": "#/$defs/FontConfig", "description": "Sets the font options of the terminal.", - "type": ["object", "null"] + "type": [ + "object", + "null" + ] }, "backgroundImage": { "description": "Sets the file location of the image to draw over the window background.", "oneOf": [ { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, { "enum": [ @@ -1452,7 +1767,10 @@ ] } ], - "type": [ "string", "null" ] + "type": [ + "string", + "null" + ] }, "backgroundImageAlignment": { "default": "center", @@ -1491,7 +1809,7 @@ "bellStyle": { "default": "audible", "description": "Controls what happens when the application emits a BEL character. When set to \"all\", the Terminal will play a sound, flash the taskbar icon (if the terminal window is not in focus) and flash the window. An array of specific behaviors can also be used. Supported array values include `audible`, `window` and `taskbar`. When set to \"none\", nothing will happen.", - "$ref": "#/definitions/BellStyle" + "$ref": "#/$defs/BellStyle" }, "closeOnExit": { "default": "graceful", @@ -1521,8 +1839,12 @@ }, "cursorColor": { "oneOf": [ - { "$ref": "#/definitions/Color" }, - {"type": "null"} + { + "$ref": "#/$defs/Color" + }, + { + "type": "null" + } ], "description": "Sets the color of the cursor. Overrides the cursor color from the color scheme. Uses hex color format: \"#rrggbb\"." }, @@ -1530,7 +1852,10 @@ "description": "Sets the percentage height of the cursor starting from the bottom. Only works when cursorShape is set to \"vintage\". Accepts values from 1-100.", "maximum": 100, "minimum": 1, - "type": ["integer","null"], + "type": [ + "integer", + "null" + ], "default": 25 }, "cursorShape": { @@ -1596,13 +1921,16 @@ "deprecated": true }, "foreground": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#cccccc", "description": "Sets the text color. Overrides \"foreground\" from the color scheme. Uses hex color format: \"#rrggbb\".", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "guid": { - "$ref": "#/definitions/ProfileGuid", + "$ref": "#/$defs/ProfileGuid", "description": "Unique identifier of the profile. Written in registry format: \"{00000000-0000-0000-0000-000000000000}\"." }, "hidden": { @@ -1616,7 +1944,9 @@ "minimum": -1, "type": "integer" }, - "icon":{ "$ref": "#/definitions/Icon" }, + "icon": { + "$ref": "#/$defs/Icon" + }, "name": { "description": "Name of the profile. Displays in the dropdown menu.", "minLength": 1, @@ -1653,8 +1983,12 @@ }, "selectionBackground": { "oneOf": [ - {"$ref": "#/definitions/Color"}, - { "type": "null" } + { + "$ref": "#/$defs/Color" + }, + { + "type": "null" + } ], "description": "Sets the background color of selected text. Overrides selectionBackground set in the color scheme. Uses hex color format: \"#rrggbb\"." }, @@ -1670,7 +2004,10 @@ }, "source": { "description": "Stores the name of the profile generator that originated this profile.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "startingDirectory": { "description": "The directory the shell starts in when it is loaded.", @@ -1682,13 +2019,19 @@ "default": false }, "tabColor": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color of the profile's tab. Using the tab color picker will override this color.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "tabTitle": { "description": "If set, will replace the name as the title to pass to the shell on startup. Some shells (like bash) may choose to ignore this initial value, while others (cmd, powershell) may use this value over the lifetime of the application.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "useAcrylic": { "default": false, @@ -1701,7 +2044,7 @@ "ProfileList": { "description": "A list of profiles and the properties specific to each.", "items": { - "$ref": "#/definitions/Profile", + "$ref": "#/$defs/Profile", "required": [ "guid", "name" @@ -1713,11 +2056,11 @@ "description": "A list of profiles and default settings that apply to all of them", "properties": { "list": { - "$ref": "#/definitions/ProfileList" + "$ref": "#/$defs/ProfileList" }, "defaults": { "description": "The default settings that apply to every profile.", - "$ref": "#/definitions/Profile" + "$ref": "#/$defs/Profile" } }, "type": "object" @@ -1733,84 +2076,84 @@ "type": "string" }, "background": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the background color of the color scheme." }, "black": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI black." }, "blue": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI blue." }, "brightBlack": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright black." }, "brightBlue": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright blue." }, "brightCyan": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright cyan." }, "brightGreen": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright green." }, "brightPurple": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright purple." }, "brightRed": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright red." }, "brightWhite": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright white." }, "brightYellow": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright yellow." }, "cursorColor": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#FFFFFF", "description": "Sets the cursor color of the color scheme." }, "cyan": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI cyan." }, "foreground": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the foreground color of the color scheme." }, "green": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI green." }, "purple": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI purple." }, "red": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI red." }, "selectionBackground": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the selection background color of the color scheme." }, "white": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI white." }, "yellow": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI yellow." } }, @@ -1820,17 +2163,25 @@ } }, "allOf": [ - { "$ref": "#/definitions/Globals" }, + { + "$ref": "#/$defs/Globals" + }, { "additionalItems": true, "properties": { "profiles": { "oneOf": [ - { "$ref": "#/definitions/ProfileList" }, - { "$ref": "#/definitions/ProfilesObject" } + { + "$ref": "#/$defs/ProfileList" + }, + { + "$ref": "#/$defs/ProfilesObject" + } ] }, - "schemes": { "$ref": "#/definitions/SchemeList" } + "schemes": { + "$ref": "#/$defs/SchemeList" + } }, "required": [ "profiles", From 69391128201add56e124ec8363b4b5610ed3a7d0 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 29 Sep 2021 05:24:46 -0500 Subject: [PATCH 20/35] Minor typos in 1.12 features in SUI (#11362) * [x] Fixes a bunch of the checkboxes in #11352 * [x] Fixes one of the boxes in #11353 * [x] The opacity warning -> error gibberish was fixed with the change to `DeserializationError` - `asCString` only works if the `JsonValue` is a string already. --- .../TerminalSettingsEditor/Resources/en-US/Resources.resw | 4 ++-- src/cascadia/TerminalSettingsModel/JsonUtils.h | 2 +- .../TerminalSettingsModel/Resources/en-US/Resources.resw | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index bf0e85ee8..c6126240d 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -496,7 +496,7 @@ A description for what the "acrylic opacity" setting does. Presented near "Profile_AcrylicOpacity.Header". - Background Opacity + Background opacity Header for a control to determine the level of opacity for the background of the control. The user can choose to make the background of the app more or less opaque. @@ -1227,7 +1227,7 @@ Header for a control to how text is formatted - Intense Text Style + Intense text style Header for a control to select how "intense" text is formatted (bold, bright, both or none) diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index cc72ce143..cea006b2b 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -101,7 +101,7 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils { public: DeserializationError(const Json::Value& value) : - runtime_error(std::string("failed to deserialize ") + (value.isNull() ? "" : value.asCString())), + runtime_error(std::string("failed to deserialize ") + (value.isNull() ? "" : value.asString())), jsonValue{ value } {} void SetKey(std::string_view newKey) diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 5538d5d22..8bbbbfb7f 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -433,7 +433,7 @@ Break into the debugger - Open Settings... + Open settings... Rename window to "{0}" @@ -456,15 +456,15 @@ {0} will be replaced with a user-specified number - Clear Buffer + Clear buffer A command to clear the entirety of the Terminal output buffer - Clear Viewport + Clear viewport A command to clear the active viewport of the Terminal - Clear Scrollback + Clear scrollback A command to clear the part of the buffer above the viewport From c0574f5eced0ae0aab1a4ddfa92bce62b38c4065 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 29 Sep 2021 05:26:20 -0500 Subject: [PATCH 21/35] Remove the fallback to 50% opacity when only `useAcrylic` is set (#11363) This logic was seemingly redundant. There's two cases I'm looking at here: #### Case 1 ```jsonc "defaults": { "opacity": 35 }, "list": [ { "commandline": "cmd.exe", "name": "Command Prompt" }, ``` In this case, we wouldn't set the `TerminalSettings` Opacity to .35, we'd set it to 1.0, because the profile didn't have an `opactity`. #### Case 2 ```jsonc "defaults": { "useAcrylic": true }, "list": [ { "commandline": "cmd.exe", "name": "Command Prompt" }, ``` In this case we still want to have an acrylic effect. Previously, we'd default this effect to 50% opaque. I'm not sure that we can actually get that anymore. BUT it turns out, we _can_ have 100% opacity and HostBackdropAcrylic. It is very subtle, but is maybe something we should be allowing anyways. It kinda looks like: ![image](https://user-images.githubusercontent.com/18356694/135168469-35d1f55b-58d1-4ee3-a717-76000c2574b9.png) * [x] Fixes #11355 * [x] Regressed in #11180 * [x] I work here --- .../TerminalSettingsModel/TerminalSettings.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index 3d7b2f11e..187b3cedb 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -202,15 +202,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _IntenseIsBold = WI_IsFlagSet(appearance.IntenseTextStyle(), Microsoft::Terminal::Settings::Model::IntenseStyle::Bold); _IntenseIsBright = WI_IsFlagSet(appearance.IntenseTextStyle(), Microsoft::Terminal::Settings::Model::IntenseStyle::Bright); - // If the user set an opacity, then just use that. Otherwise, change the - // default value based off of whether useAcrylic was set or not. If they - // want acrylic, then default to 50%. Otherwise, default to 100% (fully - // opaque) - _Opacity = appearance.HasOpacity() ? - appearance.Opacity() : - UseAcrylic() ? - .5 : - 1.0; + _Opacity = appearance.Opacity(); } // Method Description: From dacff61f8862fa7c28f0244e74555cb2658455ad Mon Sep 17 00:00:00 2001 From: James Holderness Date: Wed, 29 Sep 2021 11:48:32 +0100 Subject: [PATCH 22/35] Use the til::enumset type for the GridLines enum in the renderers (#11345) ## Summary of the Pull Request This replaces the `GridLines` enum in the renderers with a `til::enumset` type, avoiding the need for the various `WI_IsFlagSet` macros and flag operators. ## References This is followup to PR #10492 which introduced the `enumset` class. ## PR Checklist * [ ] Closes #xxx * [x] CLA signed. * [ ] Tests added/passed * [ ] Documentation updated. * [ ] Schema updated. * [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. ## Validation Steps Performed I've manually confirmed that all the different gridlines are still rendering correctly in both the GDI and DX renderers. --- src/interactivity/onecore/BgfxEngine.cpp | 2 +- src/interactivity/onecore/BgfxEngine.hpp | 2 +- src/renderer/base/renderer.cpp | 28 ++++++++++++------------ src/renderer/base/renderer.hpp | 2 +- src/renderer/dx/DxRenderer.cpp | 24 ++++++++++---------- src/renderer/dx/DxRenderer.hpp | 2 +- src/renderer/gdi/gdirenderer.hpp | 2 +- src/renderer/gdi/paint.cpp | 16 +++++++------- src/renderer/inc/IRenderEngine.hpp | 25 ++++++++++----------- src/renderer/uia/UiaRenderer.cpp | 2 +- src/renderer/uia/UiaRenderer.hpp | 2 +- src/renderer/vt/paint.cpp | 2 +- src/renderer/vt/vtrenderer.hpp | 2 +- src/renderer/wddmcon/WddmConRenderer.cpp | 2 +- src/renderer/wddmcon/WddmConRenderer.hpp | 2 +- 15 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index dcd3f923e..ef9e95112 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -166,7 +166,7 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid CATCH_RETURN(); } -[[nodiscard]] HRESULT BgfxEngine::PaintBufferGridLines(GridLines const /*lines*/, +[[nodiscard]] HRESULT BgfxEngine::PaintBufferGridLines(GridLineSet const /*lines*/, COLORREF const /*color*/, size_t const /*cchLine*/, COORD const /*coordTarget*/) noexcept diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index 2b4e787a3..4a01837d9 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -53,7 +53,7 @@ namespace Microsoft::Console::Render const COORD coord, const bool trimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 9654df2aa..13a72d4ba 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -898,50 +898,50 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // Arguments: // - textAttribute: the TextAttribute to generate GridLines from. // Return Value: -// - a GridLines containing all the gridline info from the TextAttribute -IRenderEngine::GridLines Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcept +// - a GridLineSet containing all the gridline info from the TextAttribute +IRenderEngine::GridLineSet Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcept { // Convert console grid line representations into rendering engine enum representations. - IRenderEngine::GridLines lines = IRenderEngine::GridLines::None; + IRenderEngine::GridLineSet lines; if (textAttribute.IsTopHorizontalDisplayed()) { - lines |= IRenderEngine::GridLines::Top; + lines.set(IRenderEngine::GridLines::Top); } if (textAttribute.IsBottomHorizontalDisplayed()) { - lines |= IRenderEngine::GridLines::Bottom; + lines.set(IRenderEngine::GridLines::Bottom); } if (textAttribute.IsLeftVerticalDisplayed()) { - lines |= IRenderEngine::GridLines::Left; + lines.set(IRenderEngine::GridLines::Left); } if (textAttribute.IsRightVerticalDisplayed()) { - lines |= IRenderEngine::GridLines::Right; + lines.set(IRenderEngine::GridLines::Right); } if (textAttribute.IsCrossedOut()) { - lines |= IRenderEngine::GridLines::Strikethrough; + lines.set(IRenderEngine::GridLines::Strikethrough); } if (textAttribute.IsUnderlined()) { - lines |= IRenderEngine::GridLines::Underline; + lines.set(IRenderEngine::GridLines::Underline); } if (textAttribute.IsDoublyUnderlined()) { - lines |= IRenderEngine::GridLines::DoubleUnderline; + lines.set(IRenderEngine::GridLines::DoubleUnderline); } if (textAttribute.IsHyperlink()) { - lines |= IRenderEngine::GridLines::HyperlinkUnderline; + lines.set(IRenderEngine::GridLines::HyperlinkUnderline); } return lines; } @@ -962,7 +962,7 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin const COORD coordTarget) { // Convert console grid line representations into rendering engine enum representations. - IRenderEngine::GridLines lines = Renderer::s_GetGridlines(textAttribute); + auto lines = Renderer::s_GetGridlines(textAttribute); // For now, we dash underline patterns and switch to regular underline on hover // Since we're only rendering pattern links on *hover*, there's no point in checking @@ -975,13 +975,13 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin { if (_pData->GetPatternId(coordTarget).size() > 0) { - lines |= IRenderEngine::GridLines::Underline; + lines.set(IRenderEngine::GridLines::Underline); } } } // Return early if there are no lines to paint. - if (lines != IRenderEngine::GridLines::None) + if (lines.any()) { // Get the current foreground color to render the lines. const COLORREF rgb = _pData->GetAttributeColors(textAttribute).first; diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 10d853d85..1c1fb01ad 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -87,7 +87,7 @@ namespace Microsoft::Console::Render void UpdateLastHoveredInterval(const std::optional::interval>& newInterval); private: - static IRenderEngine::GridLines s_GetGridlines(const TextAttribute& textAttribute) noexcept; + static IRenderEngine::GridLineSet s_GetGridlines(const TextAttribute& textAttribute) noexcept; static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar); void _NotifyPaintFrame(); diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index fc8111a63..bbbdb8b20 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -1706,7 +1706,7 @@ CATCH_RETURN() // - We will draw rightward (+X) from here // Return Value: // - S_OK or relevant DirectX error -[[nodiscard]] HRESULT DxEngine::PaintBufferGridLines(GridLines const lines, +[[nodiscard]] HRESULT DxEngine::PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept @@ -1733,13 +1733,13 @@ try // offset by half the stroke width. For the start coordinate we add half // the stroke width, and for the end coordinate we subtract half the width. const DxFontRenderData::LineMetrics lineMetrics = _fontRenderData->GetLineMetrics(); - if (WI_IsAnyFlagSet(lines, (GridLines::Left | GridLines::Right))) + if (lines.any(GridLines::Left, GridLines::Right)) { const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f; const auto startY = target.y + halfGridlineWidth; const auto endY = target.y + font.height - halfGridlineWidth; - if (WI_IsFlagSet(lines, GridLines::Left)) + if (lines.test(GridLines::Left)) { auto x = target.x + halfGridlineWidth; for (size_t i = 0; i < cchLine; i++, x += font.width) @@ -1748,7 +1748,7 @@ try } } - if (WI_IsFlagSet(lines, GridLines::Right)) + if (lines.test(GridLines::Right)) { auto x = target.x + font.width - halfGridlineWidth; for (size_t i = 0; i < cchLine; i++, x += font.width) @@ -1758,19 +1758,19 @@ try } } - if (WI_IsAnyFlagSet(lines, GridLines::Top | GridLines::Bottom)) + if (lines.any(GridLines::Top, GridLines::Bottom)) { const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f; const auto startX = target.x + halfGridlineWidth; const auto endX = target.x + fullRunWidth - halfGridlineWidth; - if (WI_IsFlagSet(lines, GridLines::Top)) + if (lines.test(GridLines::Top)) { const auto y = target.y + halfGridlineWidth; DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth); } - if (WI_IsFlagSet(lines, GridLines::Bottom)) + if (lines.test(GridLines::Bottom)) { const auto y = target.y + font.height - halfGridlineWidth; DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth); @@ -1780,24 +1780,24 @@ try // In the case of the underline and strikethrough offsets, the stroke width // is already accounted for, so they don't require further adjustments. - if (WI_IsAnyFlagSet(lines, GridLines::Underline | GridLines::DoubleUnderline | GridLines::HyperlinkUnderline)) + if (lines.any(GridLines::Underline, GridLines::DoubleUnderline, GridLines::HyperlinkUnderline)) { const auto halfUnderlineWidth = lineMetrics.underlineWidth / 2.0f; const auto startX = target.x + halfUnderlineWidth; const auto endX = target.x + fullRunWidth - halfUnderlineWidth; const auto y = target.y + lineMetrics.underlineOffset; - if (WI_IsFlagSet(lines, GridLines::Underline)) + if (lines.test(GridLines::Underline)) { DrawLine(startX, y, endX, y, lineMetrics.underlineWidth); } - if (WI_IsFlagSet(lines, GridLines::HyperlinkUnderline)) + if (lines.test(GridLines::HyperlinkUnderline)) { DrawHyperlinkLine(startX, y, endX, y, lineMetrics.underlineWidth); } - if (WI_IsFlagSet(lines, GridLines::DoubleUnderline)) + if (lines.test(GridLines::DoubleUnderline)) { DrawLine(startX, y, endX, y, lineMetrics.underlineWidth); const auto y2 = target.y + lineMetrics.underlineOffset2; @@ -1805,7 +1805,7 @@ try } } - if (WI_IsFlagSet(lines, GridLines::Strikethrough)) + if (lines.test(GridLines::Strikethrough)) { const auto halfStrikethroughWidth = lineMetrics.strikethroughWidth / 2.0f; const auto startX = target.x + halfStrikethroughWidth; diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index 9c96e7f06..de9c69fd0 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -100,7 +100,7 @@ namespace Microsoft::Console::Render bool const fTrimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 4caf62ad6..2e09f374d 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -52,7 +52,7 @@ namespace Microsoft::Console::Render const COORD coord, const bool trimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(const GridLines lines, + [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const COORD coordTarget) noexcept override; diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 347135974..598ce3489 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -472,7 +472,7 @@ using namespace Microsoft::Console::Render; // - coordTarget - The starting X/Y position of the first character to draw on. // Return Value: // - S_OK or suitable GDI HRESULT error or E_FAIL for GDI errors in functions that don't reliably return a specific error code. -[[nodiscard]] HRESULT GdiEngine::PaintBufferGridLines(const GridLines lines, const COLORREF color, const size_t cchLine, const COORD coordTarget) noexcept +[[nodiscard]] HRESULT GdiEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const COORD coordTarget) noexcept { LOG_IF_FAILED(_FlushBufferLines()); @@ -499,7 +499,7 @@ using namespace Microsoft::Console::Render; return PatBlt(_hdcMemoryContext, x, y, w, h, PATCOPY); }; - if (lines & GridLines::Left) + if (lines.test(GridLines::Left)) { auto x = ptTarget.x; for (size_t i = 0; i < cchLine; i++, x += fontWidth) @@ -508,7 +508,7 @@ using namespace Microsoft::Console::Render; } } - if (lines & GridLines::Right) + if (lines.test(GridLines::Right)) { // NOTE: We have to subtract the stroke width from the cell width // to ensure the x coordinate remains inside the clipping rectangle. @@ -519,13 +519,13 @@ using namespace Microsoft::Console::Render; } } - if (lines & GridLines::Top) + if (lines.test(GridLines::Top)) { const auto y = ptTarget.y; RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth)); } - if (lines & GridLines::Bottom) + if (lines.test(GridLines::Bottom)) { // NOTE: We have to subtract the stroke width from the cell height // to ensure the y coordinate remains inside the clipping rectangle. @@ -533,19 +533,19 @@ using namespace Microsoft::Console::Render; RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth)); } - if (lines & (GridLines::Underline | GridLines::DoubleUnderline)) + if (lines.any(GridLines::Underline, GridLines::DoubleUnderline)) { const auto y = ptTarget.y + _lineMetrics.underlineOffset; RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.underlineWidth)); - if (lines & GridLines::DoubleUnderline) + if (lines.test(GridLines::DoubleUnderline)) { const auto y2 = ptTarget.y + _lineMetrics.underlineOffset2; RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y2, widthOfAllCells, _lineMetrics.underlineWidth)); } } - if (lines & GridLines::Strikethrough) + if (lines.test(GridLines::Strikethrough)) { const auto y = ptTarget.y + _lineMetrics.strikethroughOffset; RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth)); diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index bcc98dfc4..4dd69be8a 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -30,18 +30,19 @@ namespace Microsoft::Console::Render class IRenderEngine { public: - enum GridLines + enum class GridLines { - None = 0x0, - Top = 0x1, - Bottom = 0x2, - Left = 0x4, - Right = 0x8, - Underline = 0x10, - DoubleUnderline = 0x20, - Strikethrough = 0x40, - HyperlinkUnderline = 0x80 + None, + Top, + Bottom, + Left, + Right, + Underline, + DoubleUnderline, + Strikethrough, + HyperlinkUnderline }; + using GridLineSet = til::enumset; virtual ~IRenderEngine() = 0; @@ -86,7 +87,7 @@ namespace Microsoft::Console::Render const COORD coord, const bool fTrimLeft, const bool lineWrapped) noexcept = 0; - [[nodiscard]] virtual HRESULT PaintBufferGridLines(const GridLines lines, + [[nodiscard]] virtual HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const COORD coordTarget) noexcept = 0; @@ -118,5 +119,3 @@ namespace Microsoft::Console::Render inline Microsoft::Console::Render::IRenderEngine::~IRenderEngine() {} } - -DEFINE_ENUM_FLAG_OPERATORS(Microsoft::Console::Render::IRenderEngine::GridLines) diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index 2d5d17196..f3206e79d 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -324,7 +324,7 @@ CATCH_RETURN(); // - coordTarget - // Return Value: // - S_FALSE -[[nodiscard]] HRESULT UiaEngine::PaintBufferGridLines(GridLines const /*lines*/, +[[nodiscard]] HRESULT UiaEngine::PaintBufferGridLines(GridLineSet const /*lines*/, COLORREF const /*color*/, size_t const /*cchLine*/, COORD const /*coordTarget*/) noexcept diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 6591579c5..8c438f055 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -55,7 +55,7 @@ namespace Microsoft::Console::Render COORD const coord, bool const fTrimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index f8b8a1266..1b9efb35e 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -142,7 +142,7 @@ using namespace Microsoft::Console::Types; // - coordTarget - The starting X/Y position of the first character to draw on. // Return Value: // - S_OK -[[nodiscard]] HRESULT VtEngine::PaintBufferGridLines(const GridLines /*lines*/, +[[nodiscard]] HRESULT VtEngine::PaintBufferGridLines(const GridLineSet /*lines*/, const COLORREF /*color*/, const size_t /*cchLine*/, const COORD /*coordTarget*/) noexcept diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index bb8f0413f..1965034db 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -65,7 +65,7 @@ namespace Microsoft::Console::Render const COORD coord, const bool trimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(const GridLines lines, + [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const COORD coordTarget) noexcept override; diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index ba67819ab..1ee11d741 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -287,7 +287,7 @@ bool WddmConEngine::IsInitialized() CATCH_RETURN(); } -[[nodiscard]] HRESULT WddmConEngine::PaintBufferGridLines(GridLines const /*lines*/, +[[nodiscard]] HRESULT WddmConEngine::PaintBufferGridLines(GridLineSet const /*lines*/, COLORREF const /*color*/, size_t const /*cchLine*/, COORD const /*coordTarget*/) noexcept diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index 57663d5cb..0da9f5cba 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -45,7 +45,7 @@ namespace Microsoft::Console::Render const COORD coord, const bool trimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; From d25ca26142de99fcd4964a6cce90fd9fb935cfc4 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 29 Sep 2021 16:55:44 -0500 Subject: [PATCH 23/35] Always init the BG opacity with the renderer (#11368) Missed this in #11180. I forgot to init the BG opacity with the renderer on startup, because that matters when you have `"antialiasingMode": "cleartype",`. Repro json ```json { "commandline": "cmd.exe", "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", "hidden": false, "opacity": 35, "antialiasingMode": "cleartype", "padding": "0", "name": "Command Prompt" }, ``` * [x] Fixes #11315 --- src/cascadia/TerminalControl/ControlCore.cpp | 6 ++---- src/cascadia/TerminalControl/TermControl.cpp | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 534862698..efcce89c8 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -274,10 +274,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation _updateAntiAliasingMode(_renderEngine.get()); // GH#5098: Inform the engine of the opacity of the default text background. - if (_settings.UseAcrylic()) - { - _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.Opacity())); - } + // GH#11315: Always do this, even if they don't have acrylic on. + _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.Opacity())); THROW_IF_FAILED(_renderEngine->Enable()); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index f90190a5d..32bd45f6b 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -459,7 +459,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // GH#5098: Inform the engine of the new opacity of the default text background. - _core.SetBackgroundOpacity(::base::saturated_cast(appearance.Opacity())); + _core.SetBackgroundOpacity(appearance.Opacity()); } else { From ba239026f39687ebefe482ed75db5a98534a37a6 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 29 Sep 2021 16:57:58 -0500 Subject: [PATCH 24/35] Allow the entire Tab to be hit testable again (#11369) DESPITE the fact that there's a `Background()` API that we could just call like: ```c++ TabViewItem().Background(deselectedTabBrush); ``` We actually can't, because it will make the part of the tab that doesn't contain the text totally transparent to hit tests. So we actually _do_ still need to set `TabViewItemHeaderBackground` manually. * Regressed in #11240 * Root cause up in https://github.com/microsoft/microsoft-ui-xaml/pull/3769 * [x] closes #11294 --- src/cascadia/TerminalApp/TerminalTab.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 8ef1847ba..aaf8768cd 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -1460,7 +1460,16 @@ namespace winrt::TerminalApp::implementation // TabViewItem().Background() only sets the color of the tab background // when the TabViewItem is unselected. So we still need to set the other // properties ourselves. - TabViewItem().Background(deselectedTabBrush); + // + // GH#11294: DESPITE the fact that there's a Background() API that we + // could just call like: + // + // TabViewItem().Background(deselectedTabBrush); + // + // We actually can't, because it will make the part of the tab that + // doesn't contain the text totally transparent to hit tests. So we + // actually _do_ still need to set TabViewItemHeaderBackground manually. + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackground"), deselectedTabBrush); TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), selectedTabBrush); TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPointerOver"), hoverTabBrush); TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPressed"), selectedTabBrush); @@ -1506,6 +1515,7 @@ namespace winrt::TerminalApp::implementation void TerminalTab::_ClearTabBackgroundColor() { winrt::hstring keys[] = { + L"TabViewItemHeaderBackground", L"TabViewItemHeaderBackgroundSelected", L"TabViewItemHeaderBackgroundPointerOver", L"TabViewItemHeaderForeground", @@ -1526,9 +1536,6 @@ namespace winrt::TerminalApp::implementation } } - // Clear out the Background. - TabViewItem().Background(nullptr); - _RefreshVisualState(); _colorCleared(); } From cf00ad7ad4fbe2f39fa2c5640609afcacdf62524 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 29 Sep 2021 16:58:26 -0500 Subject: [PATCH 25/35] Add automation names to some controls that were missing them (#11364) All these controls didn't have `Name`s assigned, and Accessibility Insights doesn't like that. Their parents did, but the actual focusable elements themselves didn't. So I've just taken the nearby headers for these things and slapped them in as the Automation names for these controls. I verified that each of these automated tests in Accessibility Insights pass again. * Will do the thing to #11155 but we need confirmation before that can be closed. --- .../TerminalSettingsEditor/Appearances.xaml | 9 ++++-- .../TerminalSettingsEditor/Launch.xaml | 6 ++-- .../TerminalSettingsEditor/Profiles.xaml | 12 ++++--- .../Resources/en-US/Resources.resw | 32 +++++++++++++++++++ 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.xaml b/src/cascadia/TerminalSettingsEditor/Appearances.xaml index 7765d6626..e1556e80d 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.xaml +++ b/src/cascadia/TerminalSettingsEditor/Appearances.xaml @@ -72,13 +72,15 @@ two font lists causes a crash within the ComboBox code. As a workaround, introduce two ComboBox controls and only display one at a time. --> - - - - - diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.xaml b/src/cascadia/TerminalSettingsEditor/Profiles.xaml index 24e6a7fe1..0208a8065 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles.xaml @@ -81,7 +81,8 @@ SettingOverrideSource="{x:Bind State.Profile.CommandlineOverrideSource, Mode=OneWay}" Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.IsBaseLayer), Mode=OneWay}"> -