From 293c36d42faef1b5168ede4665e54cf9e75bc50b Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Fri, 16 Jul 2021 10:18:40 -0700 Subject: [PATCH 01/90] Fix unfocused appearance editor not appearing/disappearing correctly (#10675) ## Summary of the Pull Request Sends the additional xaml notification when the user presses the '+' or delete button for unfocused appearances ## PR Checklist * [x] Closes #10673 * [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 * [ ] Schema updated. * [x] I work here ## Validation Steps Performed It works now --- src/cascadia/TerminalSettingsEditor/Profiles.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.cpp b/src/cascadia/TerminalSettingsEditor/Profiles.cpp index d49fc2476..1c105798c 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles.cpp @@ -275,7 +275,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _unfocusedAppearanceViewModel.Schemes(schemes); _unfocusedAppearanceViewModel.WindowRoot(windowRoot); - _NotifyChanges(L"UnfocusedAppearance", L"HasUnfocusedAppearance"); + _NotifyChanges(L"UnfocusedAppearance", L"HasUnfocusedAppearance", L"ShowUnfocusedAppearance"); } void ProfileViewModel::DeleteUnfocusedAppearance() @@ -284,7 +284,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _unfocusedAppearanceViewModel = nullptr; - _NotifyChanges(L"HasUnfocusedAppearance"); + _NotifyChanges(L"UnfocusedAppearance", L"HasUnfocusedAppearance", L"ShowUnfocusedAppearance"); } Editor::AppearanceViewModel ProfileViewModel::UnfocusedAppearance() From 89479091218339de42a7fb3ee2476d23be1b9fce Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Fri, 16 Jul 2021 15:11:55 -0700 Subject: [PATCH 02/90] Add a KeyChordListener to the Settings UI (#10652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Replaces the key chord editor in the actions page with a listener instead of a plain text box. ## References #6900 - Settings UI Epic ## Detailed Description of the Pull Request / Additional comments - `Actions` page: - Replace `Keys` with `CurrentKeys` for consistency with `Action`/`CurrentAction` - `ProposedKeys` is now a `Control::KeyChord` - removes key chord validation (now we don't need it) - removes accept/cancel shortcuts (nowhere we could use it now) - `KeyChordListener`: - `Keys`: dependency property that hooks us up to a system to the committed setting value - this is the key binding view model, which propagates the change to the settings model clone on "accept changes" - We bind to `PreviewKeyDown` to intercept the key event _before_ some special key bindings are handled (i.e. "select all" in the text box) - `CoreWindow` is used to get the modifier keys because (1) it's easier than updating on each key press and (2) that approach resulted in a strange bug where the Alt key-up event was not detected - `LosingFocus` means that we have completed our operation and want to commit our changes to the key binding view model - `KeyDown` does most of the magic of updating `Keys`. We filter out any key chords that could be problematic (i.e. Shift+Tab and Tab for keyboard navigation) ## Validation Steps Performed - Tested a few key chords: - ✅single key: X - ✅key with modifier(s): Ctrl+Alt+X - ❌plain modifier: Ctrl - ✅key that is used by text box: Ctrl+A - ✅key that is used by Windows Terminal: Alt+F4 - ❌key that is taken by Windows OS: Windows+X - ✅key that is not taken by Windows OS: Windows+Shift+X - Known issue: - global key taken by Windows Terminal: (i.e. quake mode keybinding) - Behavior: global key binding executed - Expected: key chord recorded ## Demo ![Key Chord Listener Demo](https://user-images.githubusercontent.com/11050425/125538094-08ea4eaa-21eb-4488-a74c-6ce65324cdf1.gif) --- .../TerminalSettingsEditor/Actions.cpp | 78 ++-------- src/cascadia/TerminalSettingsEditor/Actions.h | 13 +- .../TerminalSettingsEditor/Actions.idl | 2 +- .../TerminalSettingsEditor/Actions.xaml | 21 ++- .../KeyChordListener.cpp | 135 ++++++++++++++++++ .../TerminalSettingsEditor/KeyChordListener.h | 29 ++++ .../KeyChordListener.idl | 13 ++ .../KeyChordListener.xaml | 17 +++ ...Microsoft.Terminal.Settings.Editor.vcxproj | 13 ++ ...t.Terminal.Settings.Editor.vcxproj.filters | 6 +- .../Resources/en-US/Resources.resw | 16 +++ src/cascadia/TerminalSettingsEditor/pch.h | 2 + 12 files changed, 259 insertions(+), 86 deletions(-) create mode 100644 src/cascadia/TerminalSettingsEditor/KeyChordListener.cpp create mode 100644 src/cascadia/TerminalSettingsEditor/KeyChordListener.h create mode 100644 src/cascadia/TerminalSettingsEditor/KeyChordListener.idl create mode 100644 src/cascadia/TerminalSettingsEditor/KeyChordListener.xaml diff --git a/src/cascadia/TerminalSettingsEditor/Actions.cpp b/src/cascadia/TerminalSettingsEditor/Actions.cpp index ffd2ae343..4f65c8cba 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.cpp +++ b/src/cascadia/TerminalSettingsEditor/Actions.cpp @@ -25,8 +25,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation KeyBindingViewModel(nullptr, availableActions.First().Current(), availableActions) {} KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const hstring& actionName, const IObservableVector& availableActions) : - _Keys{ keys }, - _KeyChordText{ Model::KeyChordSerialization::ToString(keys) }, + _CurrentKeys{ keys }, + _KeyChordText{ KeyChordSerialization::ToString(keys) }, _CurrentAction{ actionName }, _ProposedAction{ box_value(actionName) }, _AvailableActions{ availableActions } @@ -36,9 +36,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // unique view model members. PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) { const auto viewModelProperty{ args.PropertyName() }; - if (viewModelProperty == L"Keys") + if (viewModelProperty == L"CurrentKeys") { - _KeyChordText = Model::KeyChordSerialization::ToString(_Keys); + _KeyChordText = KeyChordSerialization::ToString(_CurrentKeys); _NotifyChanges(L"KeyChordText"); } else if (viewModelProperty == L"IsContainerFocused" || @@ -75,8 +75,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // if we're in edit mode, // - pre-populate the text box with the current keys // - reset the combo box with the current action - ProposedKeys(KeyChordText()); - ProposedAction(box_value(CurrentAction())); + ProposedKeys(_CurrentKeys); + ProposedAction(box_value(_CurrentAction)); } } @@ -85,35 +85,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation AttemptAcceptChanges(_ProposedKeys); } - void KeyBindingViewModel::AttemptAcceptChanges(hstring newKeyChordText) + void KeyBindingViewModel::AttemptAcceptChanges(const Control::KeyChord newKeys) { - try - { - // empty string --> don't accept changes - if (newKeyChordText.empty()) - { - return; - } - - // ModifyKeyBindingEventArgs - const auto args{ make_self(_Keys, // OldKeys - KeyChordSerialization::FromString(newKeyChordText), // NewKeys: Attempt to convert the provided key chord text - _IsNewlyAdded ? hstring{} : _CurrentAction, // OldAction - unbox_value(_ProposedAction)) }; // - _ModifyKeyBindingRequestedHandlers(*this, *args); - } - catch (hresult_invalid_argument) - { - // Converting the text into a key chord failed. - // Don't accept the changes. - // TODO GH #6900: - // This is tricky. I still haven't found a way to reference the - // key chord text box. It's hidden behind the data template. - // Ideally, some kind of notification would alert the user, but - // to make it look nice, we need it to somehow target the text box. - // Alternatively, we want a full key chord editor/listener. - // If we implement that, we won't need this validation or error message. - } + const auto args{ make_self(_CurrentKeys, // OldKeys + newKeys, // NewKeys + _IsNewlyAdded ? hstring{} : _CurrentAction, // OldAction + unbox_value(_ProposedAction)) }; // NewAction + _ModifyKeyBindingRequestedHandlers(*this, *args); } void KeyBindingViewModel::CancelChanges() @@ -179,34 +157,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList)); } - void Actions::KeyChordEditor_KeyDown(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) - { - const auto& senderTB{ sender.as() }; - const auto& kbdVM{ senderTB.DataContext().as() }; - if (e.OriginalKey() == VirtualKey::Enter) - { - // Fun fact: this is happening _before_ "_ProposedKeys" gets updated - // with the two-way data binding. So we need to directly extract the text - // and tell the view model to update itself. - get_self(kbdVM)->AttemptAcceptChanges(senderTB.Text()); - - // For an unknown reason, when 'AcceptChangesFlyout' is set in the code above, - // the flyout isn't shown, forcing the 'Enter' key to do nothing. - // To get around this, detect if the flyout was set, and display it - // on the text box. - if (kbdVM.AcceptChangesFlyout() != nullptr) - { - kbdVM.AcceptChangesFlyout().ShowAt(senderTB); - } - e.Handled(true); - } - else if (e.OriginalKey() == VirtualKey::Escape) - { - kbdVM.CancelChanges(); - e.Handled(true); - } - } - void Actions::AddNew_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*eventArgs*/) { // Create the new key binding and register all of the event handlers. @@ -314,7 +264,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // update view model auto senderVMImpl{ get_self(senderVM) }; - senderVMImpl->Keys(args.NewKeys()); + senderVMImpl->CurrentKeys(args.NewKeys()); } // If the action was changed, @@ -418,7 +368,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i) { const auto kbdVM{ get_self(_KeyBindingList.GetAt(i)) }; - const auto& otherKeys{ kbdVM->Keys() }; + const auto& otherKeys{ kbdVM->CurrentKeys() }; if (keys.Modifiers() == otherKeys.Modifiers() && keys.Vkey() == otherKeys.Vkey()) { return i; diff --git a/src/cascadia/TerminalSettingsEditor/Actions.h b/src/cascadia/TerminalSettingsEditor/Actions.h index 14d060a8a..9c4d37cf6 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.h +++ b/src/cascadia/TerminalSettingsEditor/Actions.h @@ -60,9 +60,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void ToggleEditMode(); void DisableEditMode() { IsInEditMode(false); } void AttemptAcceptChanges(); - void AttemptAcceptChanges(hstring newKeyChordText); + void AttemptAcceptChanges(const Control::KeyChord newKeys); void CancelChanges(); - void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _Keys); } + void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _CurrentKeys); } // ProposedAction: the entry selected by the combo box; may disagree with the settings model. // CurrentAction: the combo box item that maps to the settings model value. @@ -77,10 +77,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentAction); WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, AvailableActions, nullptr); - // ProposedKeys: the text shown in the text box; may disagree with the settings model. - // Keys: the key chord bound in the settings model. - VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProposedKeys); - VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, Keys, nullptr); + // ProposedKeys: the keys proposed by the control; may disagree with the settings model. + // CurrentKeys: the key chord bound in the settings model. + VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, ProposedKeys); + VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, CurrentKeys, nullptr); VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false); VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsNewlyAdded, false); @@ -114,7 +114,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); - void KeyChordEditor_KeyDown(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); void AddNew_Click(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); diff --git a/src/cascadia/TerminalSettingsEditor/Actions.idl b/src/cascadia/TerminalSettingsEditor/Actions.idl index 98d8ee3e6..31c8570cc 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.idl +++ b/src/cascadia/TerminalSettingsEditor/Actions.idl @@ -23,7 +23,7 @@ namespace Microsoft.Terminal.Settings.Editor Boolean ShowEditButton { get; }; Boolean IsInEditMode { get; }; Boolean IsNewlyAdded { get; }; - String ProposedKeys; + Microsoft.Terminal.Control.KeyChord ProposedKeys; Object ProposedAction; Windows.UI.Xaml.Controls.Flyout AcceptChangesFlyout; String EditButtonName { get; }; diff --git a/src/cascadia/TerminalSettingsEditor/Actions.xaml b/src/cascadia/TerminalSettingsEditor/Actions.xaml index d1d75c8a0..e4dd4ccc7 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Actions.xaml @@ -133,13 +133,9 @@ 32 15 @@ -202,7 +198,8 @@ Visibility="{x:Bind IsInEditMode, Mode=OneWay, Converter={StaticResource InvertedBooleanToVisibilityConverter}}" /> - - - + + + + + + + #include #include "MySettings.g.h" @@ -12,9 +14,6 @@ namespace winrt::SampleApp::implementation { struct MySettings : MySettingsT { - public: - MySettings() = default; - // --------------------------- Core Settings --------------------------- // All of these settings are defined in ICoreSettings. @@ -88,6 +87,14 @@ namespace winrt::SampleApp::implementation winrt::Microsoft::Terminal::Core::Color GetColorTableEntry(int32_t index) noexcept { return _ColorTable.at(index); } std::array ColorTable() { return _ColorTable; } void ColorTable(std::array /*colors*/) {} + + MySettings() + { + const auto campbellSpan = ::Microsoft::Console::Utils::CampbellColorTable(); + std::transform(campbellSpan.begin(), campbellSpan.end(), _ColorTable.begin(), [](auto&& color) { + return static_cast(til::color{ color }); + }); + } }; } diff --git a/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj index d49f6c697..23b157e9d 100644 --- a/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj +++ b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj @@ -48,6 +48,10 @@ true true + + + {18D09A24-8240-42D6-8CB6-236EEE820263} + diff --git a/src/cascadia/TerminalApp/DebugTapConnection.cpp b/src/cascadia/TerminalApp/DebugTapConnection.cpp index 0a71c8bff..54f327b3e 100644 --- a/src/cascadia/TerminalApp/DebugTapConnection.cpp +++ b/src/cascadia/TerminalApp/DebugTapConnection.cpp @@ -19,6 +19,7 @@ namespace winrt::Microsoft::TerminalApp::implementation _wrappedConnection{ std::move(wrappedConnection) } { } + void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) {} ~DebugInputTapConnection() = default; void Start() { diff --git a/src/cascadia/TerminalApp/DebugTapConnection.h b/src/cascadia/TerminalApp/DebugTapConnection.h index 16e3fc82e..c5156afc4 100644 --- a/src/cascadia/TerminalApp/DebugTapConnection.h +++ b/src/cascadia/TerminalApp/DebugTapConnection.h @@ -13,6 +13,7 @@ namespace winrt::Microsoft::TerminalApp::implementation { public: explicit DebugTapConnection(Microsoft::Terminal::TerminalConnection::ITerminalConnection wrappedConnection); + void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/){}; ~DebugTapConnection(); void Start(); void WriteInput(hstring const& data); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index d781e6fe8..75e95291c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -797,13 +797,14 @@ namespace winrt::TerminalApp::implementation // TODO GH#4661: Replace this with directly using the AzCon when our VT is better std::filesystem::path azBridgePath{ wil::GetModuleFileNameW(nullptr) }; azBridgePath.replace_filename(L"TerminalAzBridge.exe"); - connection = TerminalConnection::ConptyConnection(azBridgePath.wstring(), - L".", - L"Azure", - nullptr, - settings.InitialRows(), - settings.InitialCols(), - winrt::guid()); + connection = TerminalConnection::ConptyConnection(); + connection.Initialize(TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.wstring(), + L".", + L"Azure", + nullptr, + ::base::saturated_cast(settings.InitialRows()), + ::base::saturated_cast(settings.InitialCols()), + winrt::guid())); } else @@ -833,14 +834,14 @@ namespace winrt::TerminalApp::implementation std::filesystem::path cwd{ cwdString }; cwd /= settings.StartingDirectory().c_str(); - auto conhostConn = TerminalConnection::ConptyConnection( - settings.Commandline(), - winrt::hstring{ cwd.c_str() }, - settings.StartingTitle(), - envMap.GetView(), - settings.InitialRows(), - settings.InitialCols(), - winrt::guid()); + auto conhostConn = TerminalConnection::ConptyConnection(); + conhostConn.Initialize(TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(), + winrt::hstring{ cwd.wstring() }, + settings.StartingTitle(), + envMap.GetView(), + ::base::saturated_cast(settings.InitialRows()), + ::base::saturated_cast(settings.InitialCols()), + winrt::guid())); sessionGuid = conhostConn.Guid(); connection = conhostConn; diff --git a/src/cascadia/TerminalAzBridge/main.cpp b/src/cascadia/TerminalAzBridge/main.cpp index df51677d7..3137109e9 100644 --- a/src/cascadia/TerminalAzBridge/main.cpp +++ b/src/cascadia/TerminalAzBridge/main.cpp @@ -96,7 +96,11 @@ int wmain(int /*argc*/, wchar_t** /*argv*/) const auto size = GetConsoleScreenSize(conOut); - AzureConnection azureConn{ gsl::narrow_cast(size.Y), gsl::narrow_cast(size.X) }; + AzureConnection azureConn{}; + winrt::Windows::Foundation::Collections::ValueSet vs{}; + vs.Insert(L"initialRows", winrt::Windows::Foundation::PropertyValue::CreateUInt32(gsl::narrow_cast(size.Y))); + vs.Insert(L"initialCols", winrt::Windows::Foundation::PropertyValue::CreateUInt32(gsl::narrow_cast(size.X))); + azureConn.Initialize(vs); const auto state = RunConnectionToCompletion(azureConn, conOut, conIn); diff --git a/src/cascadia/TerminalAzBridge/pch.h b/src/cascadia/TerminalAzBridge/pch.h index 8a1dc650f..5b8835166 100644 --- a/src/cascadia/TerminalAzBridge/pch.h +++ b/src/cascadia/TerminalAzBridge/pch.h @@ -33,6 +33,7 @@ Abstract: #include #include +#include #include #include diff --git a/src/cascadia/TerminalConnection/AzureConnection.cpp b/src/cascadia/TerminalConnection/AzureConnection.cpp index 2798896a2..d5fe0bdd7 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.cpp +++ b/src/cascadia/TerminalConnection/AzureConnection.cpp @@ -71,11 +71,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation return (AzureClientID != L"0"); } - AzureConnection::AzureConnection(const uint32_t initialRows, const uint32_t initialCols) : - _initialRows{ initialRows }, - _initialCols{ initialCols }, - _expiry{} + void AzureConnection::Initialize(const Windows::Foundation::Collections::ValueSet& settings) { + if (settings) + { + _initialRows = winrt::unbox_value_or(settings.TryLookup(L"initialRows").try_as(), _initialRows); + _initialCols = winrt::unbox_value_or(settings.TryLookup(L"initialCols").try_as(), _initialCols); + } } // Method description: diff --git a/src/cascadia/TerminalConnection/AzureConnection.h b/src/cascadia/TerminalConnection/AzureConnection.h index b2afd99ab..7c51294f8 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.h +++ b/src/cascadia/TerminalConnection/AzureConnection.h @@ -21,7 +21,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { static winrt::guid ConnectionType() noexcept; static bool IsAzureConnectionAvailable() noexcept; - AzureConnection(const uint32_t rows, const uint32_t cols); + + AzureConnection() = default; + void Initialize(const Windows::Foundation::Collections::ValueSet& settings); void Start(); void WriteInput(hstring const& data); diff --git a/src/cascadia/TerminalConnection/AzureConnection.idl b/src/cascadia/TerminalConnection/AzureConnection.idl index 6bd6e4930..ecaface97 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.idl +++ b/src/cascadia/TerminalConnection/AzureConnection.idl @@ -10,7 +10,7 @@ namespace Microsoft.Terminal.TerminalConnection static Guid ConnectionType { get; }; static Boolean IsAzureConnectionAvailable(); - AzureConnection(UInt32 rows, UInt32 columns); + AzureConnection(); }; } diff --git a/src/cascadia/TerminalConnection/ConnectionInformation.cpp b/src/cascadia/TerminalConnection/ConnectionInformation.cpp new file mode 100644 index 000000000..5ae37fad6 --- /dev/null +++ b/src/cascadia/TerminalConnection/ConnectionInformation.cpp @@ -0,0 +1,66 @@ +#include "pch.h" +#include "ConnectionInformation.h" +#include "ConnectionInformation.g.cpp" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + ConnectionInformation::ConnectionInformation(hstring const& className, + const Windows::Foundation::Collections::ValueSet& settings) : + _ClassName{ className }, + _Settings{ settings } + { + } + + // Function Description: + // - Create an instance of the connection specified in the + // ConnectionInformation, and Initialize it. + // - This static method allows the content process to create a connection + // from information that lives in the window process. + // Arguments: + // - info: A ConnectionInformation object that possibly lives out-of-proc, + // containing the name of the WinRT class we should activate for this + // connection, and a bag of setting to use to initialize that object. + // Return Value: + // - + TerminalConnection::ITerminalConnection ConnectionInformation::CreateConnection(TerminalConnection::ConnectionInformation info) + try + { + Windows::Foundation::IInspectable inspectable{}; + + const auto name = static_cast(winrt::get_abi(info.ClassName())); + const auto pointer = winrt::put_abi(inspectable); + +#pragma warning(push) +#pragma warning(disable : 26490) + // C++/WinRT just loves it's void**, nothing we can do here _except_ reinterpret_cast + ::IInspectable** raw = reinterpret_cast<::IInspectable**>(pointer); +#pragma warning(pop) + + // RoActivateInstance() will try to create an instance of the object, + // who's fully qualified name is the string in Name(). + // + // The class has to be activatable. For the Terminal, this is easy + // enough - we're not hosting anything that's not already in our + // manifest, or living as a .dll & .winmd SxS. + // + // When we get to extensions (GH#4000), we may want to revisit. + if (LOG_IF_FAILED(RoActivateInstance(name, raw))) + { + return nullptr; + } + + // Now that thing we made, make sure it's actually a ITerminalConnection + if (const auto connection{ inspectable.try_as() }) + { + // Initialize it, and return it. + connection.Initialize(info.Settings()); + return connection; + } + return nullptr; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + return nullptr; + } +} diff --git a/src/cascadia/TerminalConnection/ConnectionInformation.h b/src/cascadia/TerminalConnection/ConnectionInformation.h new file mode 100644 index 000000000..75bdd23d7 --- /dev/null +++ b/src/cascadia/TerminalConnection/ConnectionInformation.h @@ -0,0 +1,43 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- ConnectionInformation.h + +Abstract: +- This is a helper object for storing both the name of a type of connection, and + a bag of settings to use to initialize that connection. +- This helper is used primarily in cross-proc scenarios, to allow the window + process to tell the content process the name of the connection type it wants + created, and how to set that connection up. This is done so the connection can + live entirely in the content process, without having to go through the window + process at all. +--*/ + +#pragma once +#include "../inc/cppwinrt_utils.h" +#include "ConnectionInformation.g.h" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + struct ConnectionInformation : ConnectionInformationT + { + ConnectionInformation(hstring const& className, + const Windows::Foundation::Collections::ValueSet& settings); + + static TerminalConnection::ITerminalConnection CreateConnection(TerminalConnection::ConnectionInformation info); + + winrt::hstring ClassName() const { return _ClassName; } + void ClassName(const winrt::hstring& value) { _ClassName = value; } + + WINRT_PROPERTY(Windows::Foundation::Collections::ValueSet, Settings); + + private: + winrt::hstring _ClassName{}; + }; +} +namespace winrt::Microsoft::Terminal::TerminalConnection::factory_implementation +{ + BASIC_FACTORY(ConnectionInformation); +} diff --git a/src/cascadia/TerminalConnection/ConnectionInformation.idl b/src/cascadia/TerminalConnection/ConnectionInformation.idl new file mode 100644 index 000000000..832336eaf --- /dev/null +++ b/src/cascadia/TerminalConnection/ConnectionInformation.idl @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ITerminalConnection.idl"; + +namespace Microsoft.Terminal.TerminalConnection +{ + [default_interface] runtimeclass ConnectionInformation + { + ConnectionInformation(String className, Windows.Foundation.Collections.ValueSet settings); + String ClassName { get; }; + Windows.Foundation.Collections.ValueSet Settings { get; }; + + static ITerminalConnection CreateConnection(ConnectionInformation info); + } + +} diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index a5666f782..2be231e80 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -116,17 +116,23 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // add additional WT env vars like WT_SETTINGS, WT_DEFAULTS and WT_PROFILE_ID for (auto item : _environment) { - auto key = item.Key(); - auto value = item.Value(); - - // avoid clobbering WSLENV - if (std::wstring_view{ key } == L"WSLENV") + try { - auto current = environment[L"WSLENV"]; - value = current + L":" + value; - } + auto key = item.Key(); + // This will throw if the value isn't a string. If that + // happens, then just skip this entry. + auto value = winrt::unbox_value(item.Value()); - environment.insert_or_assign(key.c_str(), value.c_str()); + // avoid clobbering WSLENV + if (std::wstring_view{ key } == L"WSLENV") + { + auto current = environment[L"WSLENV"]; + value = current + L":" + value; + } + + environment.insert_or_assign(key.c_str(), value.c_str()); + } + CATCH_LOG(); } } @@ -219,24 +225,54 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation _piClient.hProcess = hClientProcess; } - ConptyConnection::ConptyConnection(const hstring& commandline, - const hstring& startingDirectory, - const hstring& startingTitle, - const Windows::Foundation::Collections::IMapView& environment, - const uint32_t initialRows, - const uint32_t initialCols, - const guid& initialGuid) : - _initialRows{ initialRows }, - _initialCols{ initialCols }, - _commandline{ commandline }, - _startingDirectory{ startingDirectory }, - _startingTitle{ startingTitle }, - _environment{ environment }, - _guid{ initialGuid }, - _u8State{}, - _u16Str{}, - _buffer{} + // Function Description: + // - Helper function for constructing a ValueSet that we can use to get our settings from. + Windows::Foundation::Collections::ValueSet ConptyConnection::CreateSettings(const winrt::hstring& cmdline, + const winrt::hstring& startingDirectory, + const winrt::hstring& startingTitle, + Windows::Foundation::Collections::IMapView const& environment, + uint32_t rows, + uint32_t columns, + winrt::guid const& guid) { + Windows::Foundation::Collections::ValueSet vs{}; + + vs.Insert(L"commandline", Windows::Foundation::PropertyValue::CreateString(cmdline)); + vs.Insert(L"startingDirectory", Windows::Foundation::PropertyValue::CreateString(startingDirectory)); + vs.Insert(L"startingTitle", Windows::Foundation::PropertyValue::CreateString(startingTitle)); + vs.Insert(L"initialRows", Windows::Foundation::PropertyValue::CreateUInt32(rows)); + vs.Insert(L"initialCols", Windows::Foundation::PropertyValue::CreateUInt32(columns)); + vs.Insert(L"guid", Windows::Foundation::PropertyValue::CreateGuid(guid)); + + if (environment) + { + Windows::Foundation::Collections::ValueSet env{}; + for (const auto& [k, v] : environment) + { + env.Insert(k, Windows::Foundation::PropertyValue::CreateString(v)); + } + vs.Insert(L"environment", env); + } + return vs; + } + + void ConptyConnection::Initialize(const Windows::Foundation::Collections::ValueSet& settings) + { + if (settings) + { + // For the record, the following won't crash: + // auto bad = unbox_value_or(settings.TryLookup(L"foo").try_as(), nullptr); + // It'll just return null + + _commandline = winrt::unbox_value_or(settings.TryLookup(L"commandline").try_as(), _commandline); + _startingDirectory = winrt::unbox_value_or(settings.TryLookup(L"startingDirectory").try_as(), _startingDirectory); + _startingTitle = winrt::unbox_value_or(settings.TryLookup(L"startingTitle").try_as(), _startingTitle); + _initialRows = winrt::unbox_value_or(settings.TryLookup(L"initialRows").try_as(), _initialRows); + _initialCols = winrt::unbox_value_or(settings.TryLookup(L"initialCols").try_as(), _initialCols); + _guid = winrt::unbox_value_or(settings.TryLookup(L"guid").try_as(), _guid); + _environment = settings.TryLookup(L"environment").try_as(); + } + if (_guid == guid{}) { _guid = Utils::CreateGuid(); diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h index f53b4a56e..8f82a617b 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.h +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -26,14 +26,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const HANDLE hServerProcess, const HANDLE hClientProcess); - ConptyConnection( - const hstring& cmdline, - const hstring& startingDirectory, - const hstring& startingTitle, - const Windows::Foundation::Collections::IMapView& environment, - const uint32_t rows, - const uint32_t cols, - const guid& guid); + ConptyConnection() noexcept = default; + void Initialize(const Windows::Foundation::Collections::ValueSet& settings); + static winrt::fire_and_forget final_release(std::unique_ptr connection); void Start(); @@ -49,6 +44,14 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation static winrt::event_token NewConnection(NewConnectionHandler const& handler); static void NewConnection(winrt::event_token const& token); + static Windows::Foundation::Collections::ValueSet CreateSettings(const winrt::hstring& cmdline, + const winrt::hstring& startingDirectory, + const winrt::hstring& startingTitle, + Windows::Foundation::Collections::IMapView const& environment, + uint32_t rows, + uint32_t columns, + winrt::guid const& guid); + WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler); private: @@ -60,10 +63,10 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation uint32_t _initialRows{}; uint32_t _initialCols{}; - hstring _commandline; - hstring _startingDirectory; - hstring _startingTitle; - Windows::Foundation::Collections::IMapView _environment; + hstring _commandline{}; + hstring _startingDirectory{}; + hstring _startingTitle{}; + Windows::Foundation::Collections::ValueSet _environment{ nullptr }; guid _guid{}; // A unique session identifier for connected client hstring _clientName{}; // The name of the process hosted by this ConPTY connection (as of launch). @@ -77,9 +80,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation wil::unique_static_pseudoconsole_handle _hPC; wil::unique_threadpool_wait _clientExitWait; - til::u8state _u8State; - std::wstring _u16Str; - std::array _buffer; + til::u8state _u8State{}; + std::wstring _u16Str{}; + std::array _buffer{}; DWORD _OutputThread(); }; diff --git a/src/cascadia/TerminalConnection/ConptyConnection.idl b/src/cascadia/TerminalConnection/ConptyConnection.idl index 766c7263a..a1cfa9790 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.idl +++ b/src/cascadia/TerminalConnection/ConptyConnection.idl @@ -7,11 +7,19 @@ namespace Microsoft.Terminal.TerminalConnection { [default_interface] runtimeclass ConptyConnection : ITerminalConnection { - ConptyConnection(String cmdline, String startingDirectory, String startingTitle, IMapView environment, UInt32 rows, UInt32 columns, Guid guid); + ConptyConnection(); Guid Guid { get; }; static event NewConnectionHandler NewConnection; static void StartInboundListener(); static void StopInboundListener(); + + static Windows.Foundation.Collections.ValueSet CreateSettings(String cmdline, + String startingDirectory, + String startingTitle, + IMapView environment, + UInt32 rows, + UInt32 columns, + Guid guid); }; } diff --git a/src/cascadia/TerminalConnection/EchoConnection.h b/src/cascadia/TerminalConnection/EchoConnection.h index 0f034457d..e29f07429 100644 --- a/src/cascadia/TerminalConnection/EchoConnection.h +++ b/src/cascadia/TerminalConnection/EchoConnection.h @@ -18,6 +18,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation void Resize(uint32_t rows, uint32_t columns) noexcept; void Close() noexcept; + void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) const noexcept {}; + ConnectionState State() const noexcept { return ConnectionState::Connected; } WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler); diff --git a/src/cascadia/TerminalConnection/ITerminalConnection.idl b/src/cascadia/TerminalConnection/ITerminalConnection.idl index e485cc06d..28de4f520 100644 --- a/src/cascadia/TerminalConnection/ITerminalConnection.idl +++ b/src/cascadia/TerminalConnection/ITerminalConnection.idl @@ -17,6 +17,8 @@ namespace Microsoft.Terminal.TerminalConnection interface ITerminalConnection { + void Initialize(Windows.Foundation.Collections.ValueSet settings); + void Start(); void WriteInput(String data); void Resize(UInt32 rows, UInt32 columns); diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj index 8af1cf09a..5cfd57c40 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -13,6 +13,9 @@ + + ConnectionInformation.idl + AzureConnection.idl @@ -28,6 +31,9 @@ + + ConnectionInformation.idl + AzureConnection.idl @@ -43,6 +49,7 @@ + diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp index c7d9cd345..949c22e3b 100644 --- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp +++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp @@ -24,6 +24,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _TerminalOutputHandlers(PreviewText); } + void PreviewConnection::Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) noexcept + { + } + void PreviewConnection::WriteInput(hstring const& /*data*/) { } diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h index 0c04664f0..7d4c773d7 100644 --- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h +++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h @@ -22,6 +22,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation public: PreviewConnection() noexcept; + void Initialize(const Windows::Foundation::Collections::ValueSet& settings) noexcept; void Start() noexcept; void WriteInput(hstring const& data); void Resize(uint32_t rows, uint32_t columns) noexcept; diff --git a/src/cascadia/UnitTests_Control/MockConnection.h b/src/cascadia/UnitTests_Control/MockConnection.h index 14b024df1..1b7b36131 100644 --- a/src/cascadia/UnitTests_Control/MockConnection.h +++ b/src/cascadia/UnitTests_Control/MockConnection.h @@ -16,6 +16,7 @@ namespace ControlUnitTests public: MockConnection() noexcept = default; + void Initialize(const winrt::Windows::Foundation::Collections::ValueSet& /*settings*/){}; void Start() noexcept {}; void WriteInput(winrt::hstring const& data) { From 5f2ac4e3e774b87b982d6a665aa6bdc52c589753 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 20 Jul 2021 11:19:48 -0500 Subject: [PATCH 09/90] Add tracelogging for drag&drop on the new tab button (#10726) As discussed in team sync. Is this a mysterious dark pattern we didn't know about? * [x] closes #10721 * [x] I work here * [x] doesn't need tests * [x] doesn't need docs * see also #10160 --- src/cascadia/TerminalApp/TerminalPage.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 75e95291c..d1efbcac4 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -264,6 +264,13 @@ namespace winrt::TerminalApp::implementation NewTerminalArgs args; args.StartingDirectory(winrt::hstring{ pathText }); this->_OpenNewTerminal(args); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabByDragDrop", + TraceLoggingDescription("Event emitted when the user drag&drops onto the new tab button"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); } } From 9c1331ab2ec517e832cf8fd4e0c88275103acc9f Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 20 Jul 2021 11:26:35 -0500 Subject: [PATCH 10/90] Re-evaluate the size of the quake window when it's summoned to a monitor (#10674) ## Summary of the Pull Request When the quake window is moved to another monitor, re-evaluate it's size for that monitor. ## References * megathread: #8888 * Similar, but not the same: #10274 ## PR Checklist * [x] Closes #10182 * [x] I work here * [ ] Tests added/passed * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments We'll probably need to do this in a few more places, but I'm breaking PRs into small chunks for easier reviews. ## Validation Steps Performed Summoned the window to a bunch of different resolutions. Where it would use the wrong size before, it no longer does. --- src/cascadia/WindowsTerminal/IslandWindow.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 33b250c3d..90433a861 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -1406,6 +1406,13 @@ void IslandWindow::_moveToMonitor(const MONITORINFO activeMonitor) currentWindowRect.width(), currentWindowRect.height(), SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE); + + // GH#10274, GH#10182: Re-evaluate the size of the quake window when we + // move to another monitor. + if (IsQuakeWindow()) + { + _enterQuakeMode(); + } } } From 79115e20585fcaafb703427c10d1a3365ac54c7b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 20 Jul 2021 19:00:49 +0200 Subject: [PATCH 11/90] Fix building with v143 toolchain (#10727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual Studio 2022 Preview recently released the v143 toolchain. C4189 is now flagging several unused variables, which breaks our build. ## PR Checklist * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * CascadiaPackage builds ✔️ * All tests build ✔️ --- src/buffer/out/textBuffer.cpp | 6 +++--- src/cascadia/TerminalApp/Pane.cpp | 1 - src/cascadia/TerminalCore/Terminal.cpp | 6 ------ .../UnitTests_Remoting/RemotingTests.cpp | 2 +- .../ConptyRoundtripTests.cpp | 14 ------------- src/cascadia/WindowsTerminal/IslandWindow.cpp | 2 -- src/host/stream.cpp | 2 -- src/interactivity/base/EventSynthesis.cpp | 1 - src/interactivity/win32/windowproc.cpp | 1 - src/renderer/vt/state.cpp | 2 -- src/til/ut_til/MathTests.cpp | 20 ++++--------------- src/til/ut_til/PointTests.cpp | 1 - 12 files changed, 8 insertions(+), 50 deletions(-) diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 76d127086..58de15c20 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -418,7 +418,6 @@ bool TextBuffer::InsertCharacter(const std::wstring_view chars, // Store character and double byte data CharRow& charRow = Row.GetCharRow(); - short const cBufferWidth = GetSize().Width(); try { @@ -1650,13 +1649,14 @@ const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF, if (!cell.DbcsAttr().IsTrailing()) { - selectionText.append(cell.Chars()); + const auto chars = cell.Chars(); + selectionText.append(chars); if (copyTextColor) { const auto cellData = cell.TextAttr(); const auto [CellFgAttr, CellBkAttr] = GetAttributeColors(cellData); - for (const wchar_t wch : cell.Chars()) + for (size_t j = 0; j < chars.size(); ++j) { selectionFgAttr.push_back(CellFgAttr); selectionBkAttr.push_back(CellBkAttr); diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index cb8f4278a..2dda1a442 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -911,7 +911,6 @@ winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst) auto removedChild = closeFirst ? _firstChild : _secondChild; auto remainingChild = closeFirst ? _secondChild : _firstChild; const bool splitWidth = _splitState == SplitState::Vertical; - const auto totalSize = splitWidth ? _root.ActualWidth() : _root.ActualHeight(); Size removedOriginalSize{ ::base::saturated_cast(removedChild->_root.ActualWidth()), diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 9cc4aea47..68e8424ed 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -230,16 +230,10 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance) } const auto dx = ::base::ClampSub(viewportSize.X, oldDimensions.X); - - const auto oldTop = _mutableViewport.Top(); - const short newBufferHeight = ::base::ClampAdd(viewportSize.Y, _scrollbackLines); COORD bufferSize{ viewportSize.X, newBufferHeight }; - // Save cursor's relative height versus the viewport - const short sCursorHeightInViewportBefore = ::base::ClampSub(_buffer->GetCursor().GetPosition().Y, _mutableViewport.Top()); - // This will be used to determine where the viewport should be in the new buffer. const short oldViewportTop = _mutableViewport.Top(); short newViewportTop = oldViewportTop; diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 708653d01..28b502927 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -1656,7 +1656,7 @@ namespace RemotingUnitTests auto m0 = make_private(monarch0PID); auto p1 = make_private(peasant1PID); - auto p2 = make_private(peasant1PID); + auto p2 = make_private(peasant2PID); p1->WindowName(L"one"); p2->WindowName(L"two"); diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 3421a8dea..1543ea632 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -1077,7 +1077,6 @@ void ConptyRoundtripTests::PassthroughClearAll() } auto verifyBuffer = [&](const TextBuffer& tb, const til::rectangle viewport, const bool afterClear = false) { - const auto firstRow = viewport.top(); const auto width = viewport.width(); // "~" rows @@ -1781,7 +1780,6 @@ void ConptyRoundtripTests::ClearHostTrickeryTest() END_TEST_METHOD_PROPERTIES(); constexpr int PaintEveryNewline = 0; constexpr int PaintAfterAllNewlines = 1; - constexpr int DontPaintAfterNewlines = 2; INIT_TEST_PROPERTY(int, paintEachNewline, L"Any of: manually PaintFrame after each newline is emitted, once at the end of all newlines, or not at all"); INIT_TEST_PROPERTY(bool, cursorOnNextLine, L"Either leave the cursor on the first line, or place it on the second line of the buffer"); @@ -2562,7 +2560,6 @@ void ConptyRoundtripTests::ResizeRepaintVimExeBuffer() sm.ProcessString(L"BBB"); sm.ProcessString(L"\r\n"); - const auto end = 2 * hostView.Height(); for (auto i = 2; i < hostView.BottomInclusive(); i++) { // IMPORTANT! The way vim writes these blank lines is as '~' followed by @@ -2581,10 +2578,6 @@ void ConptyRoundtripTests::ResizeRepaintVimExeBuffer() drawVim(); - const auto firstTextLength = TerminalViewWidth - 2; - const auto spacesLength = 3; - const auto secondTextLength = 1; - auto verifyBuffer = [&](const TextBuffer& tb, const til::rectangle viewport) { const auto firstRow = viewport.top(); const auto width = viewport.width(); @@ -2696,7 +2689,6 @@ void ConptyRoundtripTests::ClsAndClearHostClearsScrollbackTest() } auto verifyBuffer = [&](const TextBuffer& tb, const til::rectangle viewport, const bool afterClear = false) { - const auto firstRow = viewport.top(); const auto width = viewport.width(); // "~" rows @@ -2910,8 +2902,6 @@ void ConptyRoundtripTests::ResizeInitializeBufferWithDefaultAttrs() // { static_cast(XTERM_GREEN_ATTR) }); terminalGreenAttrs.SetIndexedBackground(XTERM_GREEN_ATTR); - const size_t width = static_cast(TerminalViewWidth); - // Use an initial ^[[m to start printing with default-on-default sm.ProcessString(L"\x1b[m"); @@ -3043,8 +3033,6 @@ void ConptyRoundtripTests::NewLinesAtBottomWithBackground() terminalBlueAttrs.SetIndexedForeground(XTERM_GREEN_ATTR); terminalBlueAttrs.SetIndexedBackground(XTERM_BLUE_ATTR); - const size_t width = static_cast(TerminalViewWidth); - // We're going to print 4 more rows than the entire height of the viewport, // causing the buffer to circle 4 times. This is 2 extra iterations of the // two lines we're printing per iteration. @@ -3162,7 +3150,6 @@ void ConptyRoundtripTests::WrapNewLineAtBottom() // timings for the frame affect the results. In this test we'll be printing // a bunch of paired lines. These values control when the PaintFrame calls // will occur: - constexpr int DontPaint = 0; // Only paint at the end of all the output constexpr int PaintAfterBothLines = 1; // Paint after each pair of lines is output constexpr int PaintEveryLine = 2; // Paint after each and every line is output. @@ -3343,7 +3330,6 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS() // timings for the frame affect the results. In this test we'll be printing // a bunch of paired lines. These values control when the PaintFrame calls // will occur: - constexpr int DontPaint = 0; // Only paint at the end of all the output constexpr int PaintAfterBothLines = 1; // Paint after each pair of lines is output constexpr int PaintEveryLine = 2; // Paint after each and every line is output. diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 90433a861..6278b6c20 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -287,8 +287,6 @@ LRESULT IslandWindow::_OnMoving(const WPARAM /*wParam*/, const LPARAM lParam) void IslandWindow::Initialize() { - const bool initialized = (_interopWindowHandle != nullptr); - _source = DesktopWindowXamlSource{}; auto interop = _source.as(); diff --git a/src/host/stream.cpp b/src/host/stream.cpp index 4b0ba6c67..9d5fe4e14 100644 --- a/src/host/stream.cpp +++ b/src/host/stream.cpp @@ -707,8 +707,6 @@ size_t RetrieveNumberOfSpaces(_In_ SHORT sOriginalCursorPositionX, return STATUS_BUFFER_TOO_SMALL; } - const size_t OutputBufferSize = buffer.size_bytes(); - if (readHandleState.IsInputPending()) { return _ReadPendingInput(inputBuffer, diff --git a/src/interactivity/base/EventSynthesis.cpp b/src/interactivity/base/EventSynthesis.cpp index 9d1c217d9..e73c06727 100644 --- a/src/interactivity/base/EventSynthesis.cpp +++ b/src/interactivity/base/EventSynthesis.cpp @@ -181,7 +181,6 @@ std::deque> Microsoft::Console::Interactivity::Synthes UNICODE_NULL, LEFT_ALT_PRESSED)); - const int radix = 10; std::wstring wstr{ wch }; const auto convertedChars = ConvertToA(codepage, wstr); if (convertedChars.size() == 1) diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp index 649cc1f01..ad88dc7fa 100644 --- a/src/interactivity/win32/windowproc.cpp +++ b/src/interactivity/win32/windowproc.cpp @@ -181,7 +181,6 @@ using namespace Microsoft::Console::Types; // First retrieve the new DPI and the current DPI. DWORD const dpiProposed = (WORD)wParam; - DWORD const dpiCurrent = g.dpi; // Now we need to get what the font size *would be* if we had this new DPI. We need to ask the renderer about that. const FontInfo& fiCurrent = ScreenInfo.GetCurrentFont(); diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index 5fc185338..93ad4ade6 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -163,8 +163,6 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, // - S_OK or suitable HRESULT error from writing pipe. [[nodiscard]] HRESULT VtEngine::_WriteTerminalAscii(const std::wstring_view wstr) noexcept { - const size_t cchActual = wstr.length(); - std::string needed; needed.reserve(wstr.size()); diff --git a/src/til/ut_til/MathTests.cpp b/src/til/ut_til/MathTests.cpp index 056d03edc..6e031fb44 100644 --- a/src/til/ut_til/MathTests.cpp +++ b/src/til/ut_til/MathTests.cpp @@ -44,10 +44,7 @@ class MathTests _RunCases(til::math::truncating, cases); - const auto fn = []() { - const auto v = til::math::details::truncating_t::cast(NAN); - }; - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + VERIFY_THROWS_SPECIFIC(til::math::details::truncating_t::cast(NAN), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); } TEST_METHOD(Ceiling) @@ -65,10 +62,7 @@ class MathTests _RunCases(til::math::ceiling, cases); - const auto fn = []() { - const auto v = til::math::details::ceiling_t::cast(NAN); - }; - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + VERIFY_THROWS_SPECIFIC(til::math::details::ceiling_t::cast(NAN), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); } TEST_METHOD(Flooring) @@ -86,10 +80,7 @@ class MathTests _RunCases(til::math::flooring, cases); - const auto fn = []() { - const auto v = til::math::details::flooring_t::cast(NAN); - }; - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + VERIFY_THROWS_SPECIFIC(til::math::details::flooring_t::cast(NAN), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); } TEST_METHOD(Rounding) @@ -107,10 +98,7 @@ class MathTests _RunCases(til::math::rounding, cases); - const auto fn = []() { - const auto v = til::math::details::rounding_t::cast(NAN); - }; - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + VERIFY_THROWS_SPECIFIC(til::math::details::rounding_t::cast(NAN), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); } TEST_METHOD(NormalIntegers) diff --git a/src/til/ut_til/PointTests.cpp b/src/til/ut_til/PointTests.cpp index 00a25b193..a98531bd1 100644 --- a/src/til/ut_til/PointTests.cpp +++ b/src/til/ut_til/PointTests.cpp @@ -765,7 +765,6 @@ class PointTests Log::Comment(L"4.) Division by zero"); { - constexpr ptrdiff_t bigSize = std::numeric_limits().max(); const til::point pt{ 1, 1 }; const int scale = 0; From 0fefdac414f4e1f31ac6bd9a8ad68543c588264d Mon Sep 17 00:00:00 2001 From: Mim van den Bos Date: Tue, 20 Jul 2021 21:04:18 +0200 Subject: [PATCH 12/90] Add background color to grid to prevent animation overflow (#10716) ## Summary of the Pull Request Add an explicit background color to part of the settings UI to prevent animation overflow. The previous solution (adding a ScrollViewer) caused problems. ## References #10619 adds a ScrollViewer for one of the issues in #10609 ## PR Checklist * [x] Closes #10664 * [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 * [ ] 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 ## Validation Steps Performed Visually confirmed the animation doesn't overflow, changed the theme and confirmed the colors are responsive. Confirmed the extra scrollbar is gone. --- src/cascadia/TerminalSettingsEditor/MainPage.xaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.xaml b/src/cascadia/TerminalSettingsEditor/MainPage.xaml index 23f66358d..83ad8ad90 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.xaml +++ b/src/cascadia/TerminalSettingsEditor/MainPage.xaml @@ -116,11 +116,12 @@ - - - + + From 5a5902d580662d9f3bc1bc8cb4dab3b0d670b5be Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 20 Jul 2021 16:04:41 -0500 Subject: [PATCH 13/90] Prevent the quake window's borders from hanging onto adjacent monitors (#10676) ## Summary of the Pull Request We were making the quake window exactly the width of the monitor it was on, but that didn't account for the 1px of border on either side. ## References * megathread: #8888 ## PR Checklist * [x] Closes #10201 * [x] I work here * [ ] Tests added/passed * [n/a] Requires documentation to be updated ## Validation Steps Performed It happened before, it doesn't anymore. --- src/cascadia/WindowsTerminal/IslandWindow.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 6278b6c20..4236102f0 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -1460,12 +1460,15 @@ void IslandWindow::_enterQuakeMode() const til::size ncSize{ GetTotalNonClientExclusiveSize(dpix) }; const til::size availableSpace = desktopDimensions + ncSize; + // GH#10201 - The borders are still visible in quake mode, so make us 1px + // smaller on either side to account for that, so they don't hang onto + // adjacent monitors. const til::point origin{ - ::base::ClampSub(nearestMonitorInfo.rcWork.left, (ncSize.width() / 2)), + ::base::ClampSub(nearestMonitorInfo.rcWork.left, (ncSize.width() / 2)) + 1, (nearestMonitorInfo.rcWork.top) }; const til::size dimensions{ - availableSpace.width(), + availableSpace.width() - 2, availableSpace.height() / 2 }; From 6ce2543a94d38075800eba43d072a8ae9b822f6f Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Tue, 20 Jul 2021 14:39:55 -0700 Subject: [PATCH 14/90] Fix mouse coordinates when viewport is scrolled (#10642) ## Summary of the Pull Request Adjust the y-coordinate of the mouse coordinates we send based on how much the viewport has been scrolled ## Validation Steps Performed Validated: cannot repro the issue in #10190 Closes #10190 --- .../TerminalControl/ControlInteractivity.cpp | 7 +- .../ControlInteractivityTests.cpp | 90 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index a08640a22..56c668698 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -214,7 +214,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else if (_canSendVTMouseInput(modifiers)) { - _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, toInternalMouseState(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) + { + _core->SendMouseEvent({ terminalPosition.x(), adjustedY }, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); + } } else if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown)) { diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp index f4c893117..bf6eaede1 100644 --- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -32,6 +32,7 @@ namespace ControlUnitTests TEST_METHOD(ScrollWithSelection); TEST_METHOD(TestScrollWithTrackpad); TEST_METHOD(TestQuickDragOnSelect); + TEST_METHOD(PointerClickOutsideActiveRegion); TEST_CLASS_SETUP(ClassSetup) { @@ -544,4 +545,93 @@ namespace ControlUnitTests COORD expectedAnchor{ 0, 0 }; VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor()); } + + void ControlInteractivityTests::PointerClickOutsideActiveRegion() + { + // This is a test for GH#10642 + WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{}; + + auto [settings, conn] = _createSettingsAndConnection(); + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + _standardInit(core, interactivity); + + // For this test, don't use any modifiers + const auto modifiers = ControlKeyStates(); + const Control::MouseButtonState leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown }; + const Control::MouseButtonState noMouseDown{}; + + const til::size fontSize{ 9, 21 }; + + interactivity->_rowsToScroll = 1; + int expectedTop = 0; + int expectedViewHeight = 20; + int expectedBufferHeight = 20; + + auto scrollChangedHandler = [&](auto&&, const Control::ScrollPositionChangedArgs& args) mutable { + VERIFY_ARE_EQUAL(expectedTop, args.ViewTop()); + VERIFY_ARE_EQUAL(expectedViewHeight, args.ViewHeight()); + VERIFY_ARE_EQUAL(expectedBufferHeight, args.BufferSize()); + }; + core->ScrollPositionChanged(scrollChangedHandler); + interactivity->ScrollPositionChanged(scrollChangedHandler); + + for (int i = 0; i < 40; ++i) + { + Log::Comment(NoThrowString().Format(L"Writing line #%d", i)); + // The \r\n in the 19th loop will cause the view to start moving + if (i >= 19) + { + expectedTop++; + expectedBufferHeight++; + } + + conn->WriteInput(L"Foo\r\n"); + } + // We printed that 40 times, but the final \r\n bumped the view down one MORE row. + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + VERIFY_ARE_EQUAL(21, core->ScrollOffset()); + VERIFY_ARE_EQUAL(20, core->ViewHeight()); + VERIFY_ARE_EQUAL(41, core->BufferHeight()); + + expectedBufferHeight = 41; + expectedTop = 21; + + Log::Comment(L"Scroll up 10 times"); + for (int i = 0; i < 11; ++i) + { + expectedTop--; + interactivity->MouseWheel(modifiers, + WHEEL_DELTA, + til::point{ 0, 0 }, + noMouseDown); + } + + // Enable VT mouse event tracking + conn->WriteInput(L"\x1b[?1003;1006h"); + + // Mouse clicks in the inactive region (i.e. the top 10 rows in this case) should not register + Log::Comment(L"Click on the terminal"); + const til::point terminalPosition0{ 4, 4 }; + const til::point cursorPosition0 = terminalPosition0 * fontSize; + interactivity->PointerPressed(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + 0, // timestamp + modifiers, + cursorPosition0); + Log::Comment(L"Verify that there's not yet a selection"); + + VERIFY_IS_FALSE(core->HasSelection()); + + Log::Comment(L"Drag the mouse"); + // move the mouse as if to make a selection + const til::point terminalPosition1{ 10, 4 }; + const til::point cursorPosition1 = terminalPosition1 * fontSize; + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition1); + Log::Comment(L"Verify that there's still no selection"); + VERIFY_IS_FALSE(core->HasSelection()); + } } From 10b12ac90ce81b1a1bbb0e7918d1046e27ab5445 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 21 Jul 2021 00:34:51 +0200 Subject: [PATCH 15/90] Introduce vk() and sc() key chord specifiers (#10666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces an alternative to specifying key bindings as a combination of key modifiers and a character. It allows you to specify an explicit virtual key as `vk(nnn)`. Additionally this commit makes it possible to bind actions to scan codes. As scan code 41 appears to be the button below the Escape key on virtually all keyboards, we'll be able to bind the quake mode hotkey to `win+sc(41)` and have it work consistently across most if not all keyboard layouts. ## PR Checklist * [x] Closes #7539, Closes #10203 * [x] I work here * [x] Tests added/passed ## Validation Steps Performed The following was tested both on US and DE keyboard layouts: * Ctrl+, opens settings ✔️ * Win+` opens quake mode window ✔️ * Ctrl+plus/minus increase/decrease font size ✔️ --- .github/actions/spelling/allow/allow.txt | 5 +- .github/actions/spelling/allow/apis.txt | 3 + .github/actions/spelling/expect/expect.txt | 2 + doc/cascadia/profiles.schema.json | 8 +- .../DeserializationTests.cpp | 18 +- .../KeyBindingsTests.cpp | 231 +++++++++--------- .../TerminalSettingsTests.cpp | 24 +- src/cascadia/TerminalApp/CommandPalette.cpp | 19 +- src/cascadia/TerminalApp/TerminalPage.cpp | 12 +- src/cascadia/TerminalControl/KeyChord.cpp | 49 ++-- src/cascadia/TerminalControl/KeyChord.h | 14 +- src/cascadia/TerminalControl/KeyChord.idl | 6 +- src/cascadia/TerminalControl/TermControl.cpp | 3 + .../KeyChordListener.cpp | 4 +- .../TerminalSettingsModel/ActionMap.cpp | 31 ++- .../TerminalSettingsModel/ActionMap.h | 6 +- .../KeyChordSerialization.cpp | 73 +++++- .../defaults-universal.json | 4 +- .../TerminalSettingsModel/defaults.json | 6 +- src/cascadia/WindowsTerminal/AppHost.cpp | 104 ++++---- src/cascadia/WindowsTerminal/AppHost.h | 10 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 80 +++--- src/cascadia/WindowsTerminal/IslandWindow.h | 14 +- src/inc/til/string.h | 71 ++++++ src/til/ut_til/string.cpp | 50 ++++ tools/GenerateHeaderForJson.ps1 | 36 ++- 26 files changed, 546 insertions(+), 337 deletions(-) diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 9b7a8417b..d361ba3e4 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -1,10 +1,10 @@ -Apc apc +Apc clickable copyable dalet -Dcs dcs +Dcs dialytika dje downside @@ -52,6 +52,7 @@ tonos tshe UIs und +unregister versioned We'd wildcards diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 67ef96545..87d479f7f 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -63,6 +63,7 @@ IObject iosfwd IPackage IPeasant +isspace IStorage istream IStringable @@ -133,6 +134,7 @@ STDCPP STDMETHOD strchr streambuf +strtoul Stubless Subheader Subpage @@ -151,6 +153,7 @@ userenv wcsstr wcstoui winmain +wmemcmp wpc wsregex wwinmain diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 371dc9c62..e6f4d18b6 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1502,6 +1502,7 @@ nfe nlength Nls NLSMODE +nnn NOACTIVATE NOAPPLYNOW NOCLIP @@ -1582,6 +1583,7 @@ NTVDM ntverp NTWIN nuget +nullability nullness nullonfailure nullopt diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index b7b9d2a97..7c8e32c79 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -4,13 +4,13 @@ "title": "Microsoft's Windows Terminal Settings Profile Schema", "definitions": { "KeyChordSegment": { - "pattern": "^(?(?ctrl|alt|shift|win)(?:\\+(?ctrl|alt|shift|win)(?))?(?:\\+(?ctrl|alt|shift|win)(?|\\k))?(?:\\+(?ctrl|alt|shift|win)(?|\\k|\\k))?\\+)?(?[^\\s+]|app|menu|backspace|tab|enter|esc|escape|space|pgup|pageup|pgdn|pagedown|end|home|left|up|right|down|insert|delete|(?\", where each modifier is optional, separated by + symbols, and keyName is either one of the names listed in the table below, or any single key character. The string should be written in full lowercase.\napp, menu\tMENU key\nbackspace\tBACKSPACE key\ntab\tTAB key\nenter\tENTER key\nesc, escape\tESC key\nspace\tSPACEBAR\npgup, pageup\tPAGE UP key\npgdn, pagedown\tPAGE DOWN key\nend\tEND key\nhome\tHOME key\nleft\tLEFT ARROW key\nup\tUP ARROW key\nright\tRIGHT ARROW key\ndown\tDOWN ARROW key\ninsert\tINS key\ndelete\tDEL key\nnumpad_0-numpad_9, numpad0-numpad9\tNumeric keypad keys 0 to 9. Can't be combined with the shift modifier.\nnumpad_multiply\tNumeric keypad MULTIPLY key (*)\nnumpad_plus, numpad_add\tNumeric keypad ADD key (+)\nnumpad_minus, numpad_subtract\tNumeric keypad SUBTRACT key (-)\nnumpad_period, numpad_decimal\tNumeric keypad DECIMAL key (.). Can't be combined with the shift modifier.\nnumpad_divide\tNumeric keypad DIVIDE key (/)\nf1-f24\tF1 to F24 function keys\nplus\tADD key (+)" + "description": "The string should fit the format \"[ctrl+][alt+][shift+][win+]\", where each modifier is optional. KeyName is either any single key character, an explicit virtual key or scan code in the form vk(nnn) and sc(nnn) respectively, or one of the special names listed at https://docs.microsoft.com/en-us/windows/terminal/customize-settings/actions#accepted-modifiers-and-keys" }, "Color": { "default": "#", - "pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", + "pattern": "^#[A-Fa-f0-9]{3}(?:[A-Fa-f0-9]{3})?$", "type": "string", "format": "color" }, @@ -596,7 +596,7 @@ "defaultsFile", "allFiles", "settingsUI" - + ] } } diff --git a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp index 62df2a1d2..bff6e7e17 100644 --- a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp @@ -1971,9 +1971,9 @@ namespace SettingsModelLocalTests auto settings = implementation::CascadiaSettings::FromJson(settingsObject); VERIFY_ARE_EQUAL(3u, settings->_globals->_actionMap->_KeyMap.size()); - VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('a') })); - VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('b') })); - VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('A'), 0 })); + VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('B'), 0 })); + VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); for (const auto& warning : settings->_globals->_keybindingsWarnings) { @@ -2124,7 +2124,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(1u, nameMap.Size()); { - KeyChord kc{ true, false, false, static_cast('A') }; + KeyChord kc{ true, false, false, false, static_cast('A'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2141,7 +2141,7 @@ namespace SettingsModelLocalTests Log::Comment(L"Note that we're skipping ctrl+B, since that doesn't have `keys` set."); { - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2155,7 +2155,7 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { - KeyChord kc{ true, false, false, static_cast('D') }; + KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2169,7 +2169,7 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { - KeyChord kc{ true, false, false, static_cast('E') }; + KeyChord kc{ true, false, false, false, static_cast('E'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2183,7 +2183,7 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { - KeyChord kc{ true, false, false, static_cast('F') }; + KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2841,7 +2841,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); VERIFY_ARE_EQUAL(1u, nameMap.Size()); - const KeyChord expectedKeyChord{ true, false, true, static_cast('W') }; + const KeyChord expectedKeyChord{ true, false, true, false, static_cast('W'), 0 }; { // Verify NameMap returns correct value const auto& cmd{ nameMap.TryLookup(L"foo") }; diff --git a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp index 47a59d0fd..a5479f6a7 100644 --- a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp @@ -36,6 +36,7 @@ namespace SettingsModelLocalTests TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") END_TEST_CLASS() + TEST_METHOD(KeyChords); TEST_METHOD(ManyKeysSameAction); TEST_METHOD(LayerKeybindings); TEST_METHOD(UnbindKeybindings); @@ -61,6 +62,59 @@ namespace SettingsModelLocalTests } }; + void KeyBindingsTests::KeyChords() + { + struct testCase + { + VirtualKeyModifiers modifiers; + int32_t vkey; + int32_t scanCode; + std::wstring_view expected; + }; + + static constexpr std::array testCases{ + testCase{ + VirtualKeyModifiers::None, + 'A', + 0, + L"a", + }, + testCase{ + VirtualKeyModifiers::Control, + 'A', + 0, + L"ctrl+a", + }, + testCase{ + VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, + VK_OEM_PLUS, + 0, + L"ctrl+shift+plus", + }, + testCase{ + VirtualKeyModifiers::Control | VirtualKeyModifiers::Menu | VirtualKeyModifiers::Shift | VirtualKeyModifiers::Windows, + 255, + 0, + L"ctrl+shift+alt+win+vk(255)", + }, + testCase{ + VirtualKeyModifiers::Windows, + 0, + 123, + L"ctrl+shift+alt+win+sc(123)", + }, + }; + + for (const auto& tc : testCases) + { + KeyChord expectedKeyChord{ tc.modifiers, tc.vkey, tc.scanCode }; + const auto actualString = KeyChordSerialization::ToString(expectedKeyChord); + VERIFY_ARE_EQUAL(tc.expected, actualString); + const auto actualKeyChord = KeyChordSerialization::FromString(actualString); + VERIFY_ARE_EQUAL(expectedKeyChord, actualKeyChord); + } + } + void KeyBindingsTests::ManyKeysSameAction() { const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" }; @@ -75,7 +129,6 @@ namespace SettingsModelLocalTests const auto bindings2Json = VerifyParseSucceeded(bindings2String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); @@ -99,7 +152,6 @@ namespace SettingsModelLocalTests const auto bindings2Json = VerifyParseSucceeded(bindings2String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); @@ -129,7 +181,6 @@ namespace SettingsModelLocalTests const auto bindings5Json = VerifyParseSucceeded(bindings5String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); @@ -142,7 +193,7 @@ namespace SettingsModelLocalTests L"Try unbinding a key using `\"unbound\"` to unbind the key")); actionMap->LayerJson(bindings2Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); Log::Comment(NoThrowString().Format( L"Try unbinding a key using `null` to unbind the key")); @@ -152,7 +203,7 @@ namespace SettingsModelLocalTests // Then try layering in the bad setting actionMap->LayerJson(bindings3Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); Log::Comment(NoThrowString().Format( L"Try unbinding a key using an unrecognized command to unbind the key")); @@ -162,7 +213,7 @@ namespace SettingsModelLocalTests // Then try layering in the bad setting actionMap->LayerJson(bindings4Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); Log::Comment(NoThrowString().Format( L"Try unbinding a key using a straight up invalid value to unbind the key")); @@ -172,13 +223,13 @@ namespace SettingsModelLocalTests // Then try layering in the bad setting actionMap->LayerJson(bindings5Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); Log::Comment(NoThrowString().Format( L"Try unbinding a key that wasn't bound at all")); actionMap->LayerJson(bindings2Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); } void KeyBindingsTests::TestArbitraryArgs() @@ -203,7 +254,6 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(10u, actionMap->_KeyMap.size()); @@ -211,10 +261,9 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` without args parses as Copy(SingleLine=false)")); - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -222,10 +271,9 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` with args parses them correctly")); - KeyChord kc{ true, false, true, static_cast('C') }; + KeyChord kc{ true, false, true, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -233,10 +281,9 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` with args parses them correctly")); - KeyChord kc{ false, true, true, static_cast('C') }; + KeyChord kc{ false, true, true, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_TRUE(realArgs.SingleLine()); } @@ -244,11 +291,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `newTab` without args parses as NewTab(Index=null)")); - KeyChord kc{ true, false, false, static_cast('T') }; + KeyChord kc{ true, false, false, false, static_cast('T'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_NULL(realArgs.TerminalArgs().ProfileIndex()); @@ -256,11 +302,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `newTab` parses args correctly")); - KeyChord kc{ true, false, true, static_cast('T') }; + KeyChord kc{ true, false, true, false, static_cast('T'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex()); @@ -270,11 +315,10 @@ namespace SettingsModelLocalTests Log::Comment(NoThrowString().Format( L"Verify that `newTab` with an index greater than the legacy " L"args afforded parses correctly")); - KeyChord kc{ true, false, true, static_cast('Y') }; + KeyChord kc{ true, false, true, false, static_cast('Y'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex()); @@ -284,11 +328,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` ignores args it doesn't understand")); - KeyChord kc{ true, false, true, static_cast('B') }; + KeyChord kc{ true, false, true, false, static_cast('B'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -296,11 +339,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` null as it's `args` parses as the default option")); - KeyChord kc{ true, false, true, static_cast('B') }; + KeyChord kc{ true, false, true, false, static_cast('B'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -308,11 +350,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `adjustFontSize` with a positive delta parses args correctly")); - KeyChord kc{ true, false, false, static_cast('F') }; + KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::AdjustFontSize, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(1, realArgs.Delta()); } @@ -320,11 +361,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `adjustFontSize` with a negative delta parses args correctly")); - KeyChord kc{ true, false, false, static_cast('G') }; + KeyChord kc{ true, false, false, false, static_cast('G'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::AdjustFontSize, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(-1, realArgs.Delta()); } @@ -342,44 +382,39 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(4u, actionMap->_KeyMap.size()); { - KeyChord kc{ true, false, false, static_cast('D') }; + KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); } { - KeyChord kc{ true, false, false, static_cast('E') }; + KeyChord kc{ true, false, false, false, static_cast('E'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); } { - KeyChord kc{ true, false, false, static_cast('G') }; + KeyChord kc{ true, false, false, false, static_cast('G'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); } { - KeyChord kc{ true, false, false, static_cast('H') }; + KeyChord kc{ true, false, false, false, static_cast('H'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); } @@ -396,37 +431,33 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(3u, actionMap->_KeyMap.size()); { - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.TabColor()); } { - KeyChord kc{ true, false, false, static_cast('D') }; + KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.TabColor()); // Remember that COLORREFs are actually BBGGRR order, while the string is in #RRGGBB order VERIFY_ARE_EQUAL(til::color(0x563412), til::color(realArgs.TabColor().Value())); } { - KeyChord kc{ true, false, false, static_cast('F') }; + KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.TabColor()); } @@ -441,16 +472,14 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); { - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -470,63 +499,56 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(6u, actionMap->_KeyMap.size()); { - KeyChord kc{ false, false, false, static_cast(VK_UP) }; + KeyChord kc{ false, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.RowsToScroll()); } { - KeyChord kc{ false, false, false, static_cast(VK_DOWN) }; + KeyChord kc{ false, false, false, false, static_cast(VK_DOWN), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.RowsToScroll()); } { - KeyChord kc{ true, false, false, static_cast(VK_UP) }; + KeyChord kc{ true, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.RowsToScroll()); } { - KeyChord kc{ true, false, false, static_cast(VK_DOWN) }; + KeyChord kc{ true, false, false, false, static_cast(VK_DOWN), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.RowsToScroll()); } { - KeyChord kc{ true, false, true, static_cast(VK_UP) }; + KeyChord kc{ true, false, true, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.RowsToScroll()); VERIFY_ARE_EQUAL(10u, realArgs.RowsToScroll().Value()); } { - KeyChord kc{ true, false, true, static_cast(VK_DOWN) }; + KeyChord kc{ true, false, true, false, static_cast(VK_DOWN), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.RowsToScroll()); VERIFY_ARE_EQUAL(10u, realArgs.RowsToScroll().Value()); @@ -535,7 +557,6 @@ namespace SettingsModelLocalTests const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "scrollDown", "rowsToScroll": -1 } }])" }; const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString); auto invalidActionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(invalidActionMap); VERIFY_ARE_EQUAL(0u, invalidActionMap->_KeyMap.size()); VERIFY_THROWS(invalidActionMap->LayerJson(bindingsInvalidJson);, std::exception); } @@ -551,26 +572,23 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(2u, actionMap->_KeyMap.size()); { - KeyChord kc{ false, false, false, static_cast(VK_UP) }; + KeyChord kc{ false, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::MoveTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.Direction(), MoveTabDirection::Forward); } { - KeyChord kc{ false, false, false, static_cast(VK_DOWN) }; + KeyChord kc{ false, false, false, false, static_cast(VK_DOWN), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::MoveTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.Direction(), MoveTabDirection::Backward); } @@ -584,7 +602,6 @@ namespace SettingsModelLocalTests const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "moveTab", "direction": "bad" } }])" }; const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString); auto invalidActionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(invalidActionMap); VERIFY_ARE_EQUAL(0u, invalidActionMap->_KeyMap.size()); VERIFY_THROWS(invalidActionMap->LayerJson(bindingsInvalidJson);, std::exception); } @@ -601,35 +618,31 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(3u, actionMap->_KeyMap.size()); { - KeyChord kc{ false, false, false, static_cast(VK_UP) }; + KeyChord kc{ false, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ToggleCommandPalette, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.LaunchMode(), CommandPaletteLaunchMode::Action); } { - KeyChord kc{ true, false, false, static_cast(VK_UP) }; + KeyChord kc{ true, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ToggleCommandPalette, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.LaunchMode(), CommandPaletteLaunchMode::Action); } { - KeyChord kc{ true, false, true, static_cast(VK_UP) }; + KeyChord kc{ true, false, true, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ToggleCommandPalette, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.LaunchMode(), CommandPaletteLaunchMode::CommandLine); } @@ -637,7 +650,6 @@ namespace SettingsModelLocalTests const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "commandPalette", "launchMode": "bad" } }])" }; const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString); auto invalidActionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(invalidActionMap); VERIFY_ARE_EQUAL(0u, invalidActionMap->_KeyMap.size()); VERIFY_THROWS(invalidActionMap->LayerJson(bindingsInvalidJson);, std::exception); } @@ -669,7 +681,6 @@ namespace SettingsModelLocalTests }; auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); { @@ -677,7 +688,7 @@ namespace SettingsModelLocalTests actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::CloseWindow) }; - VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('A') }, kbd); + VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('A'), 0 }, kbd); } { Log::Comment(L"command with args"); @@ -688,7 +699,7 @@ namespace SettingsModelLocalTests args->SingleLine(true); const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::CopyText, *args) }; - VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('B') }, kbd); + VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('B'), 0 }, kbd); } { Log::Comment(L"command with new terminal args"); @@ -700,7 +711,7 @@ namespace SettingsModelLocalTests auto args{ winrt::make_self(*newTerminalArgs) }; const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::NewTab, *args) }; - VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('C') }, kbd); + VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('C'), 0 }, kbd); } { Log::Comment(L"command with hidden args"); @@ -708,7 +719,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(4u, actionMap->_KeyMap.size()); const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::ToggleCommandPalette) }; - VerifyKeyChordEquality({ VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, static_cast('P') }, kbd); + VerifyKeyChordEquality({ VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, static_cast('P'), 0 }, kbd); } } } diff --git a/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp index 459fadc18..d0eadb9c2 100644 --- a/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp @@ -113,7 +113,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(12u, actionMapImpl->_KeyMap.size()); { - KeyChord kc{ true, false, false, static_cast('A') }; + KeyChord kc{ true, false, false, false, static_cast('A'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -134,7 +134,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('B') }; + KeyChord kc{ true, false, false, false, static_cast('B'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -156,7 +156,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -178,7 +178,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('D') }; + KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -200,7 +200,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('E') }; + KeyChord kc{ true, false, false, false, static_cast('E'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -222,7 +222,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('F') }; + KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -245,7 +245,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('G') }; + KeyChord kc{ true, false, false, false, static_cast('G'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -265,7 +265,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('H') }; + KeyChord kc{ true, false, false, false, static_cast('H'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -287,7 +287,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('I') }; + KeyChord kc{ true, false, false, false, static_cast('I'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -310,7 +310,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('J') }; + KeyChord kc{ true, false, false, false, static_cast('J'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -332,7 +332,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('K') }; + KeyChord kc{ true, false, false, false, static_cast('K'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -355,7 +355,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('L') }; + KeyChord kc{ true, false, false, false, static_cast('L'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); diff --git a/src/cascadia/TerminalApp/CommandPalette.cpp b/src/cascadia/TerminalApp/CommandPalette.cpp index 651158122..bd6badf65 100644 --- a/src/cascadia/TerminalApp/CommandPalette.cpp +++ b/src/cascadia/TerminalApp/CommandPalette.cpp @@ -250,10 +250,12 @@ namespace winrt::TerminalApp::implementation void CommandPalette::_previewKeyDownHandler(IInspectable const& /*sender*/, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) { - auto key = e.OriginalKey(); - auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); - auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); - auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); + const auto key = e.OriginalKey(); + const auto scanCode = e.KeyStatus().ScanCode; + const auto coreWindow = CoreWindow::GetForCurrentThread(); + const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); + const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); // Some keypresses such as Tab, Return, Esc, and Arrow Keys are ignored by controls because // they're not considered input key presses. While they don't raise KeyDown events, @@ -264,7 +266,7 @@ namespace winrt::TerminalApp::implementation // a really widely used keyboard navigation key. if (_currentMode == CommandPaletteMode::TabSwitchMode && _actionMap) { - winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, static_cast(key) }; + winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, false, static_cast(key), static_cast(scanCode) }; if (const auto cmd{ _actionMap.GetActionByKeyChord(kc) }) { if (cmd.ActionAndArgs().Action() == ShortcutAction::PrevTab) @@ -402,9 +404,10 @@ namespace winrt::TerminalApp::implementation // - void CommandPalette::_anchorKeyUpHandler() { - auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); - auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); - auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); + const auto coreWindow = CoreWindow::GetForCurrentThread(); + const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); + const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); if (!ctrlDown && !altDown && !shiftDown) { diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index d1efbcac4..7d22fff52 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -934,12 +934,14 @@ namespace winrt::TerminalApp::implementation // - void TerminalPage::_KeyDownHandler(Windows::Foundation::IInspectable const& /*sender*/, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) { - auto key = e.OriginalKey(); - auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); - auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); - auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); + const auto key = e.OriginalKey(); + const auto scanCode = e.KeyStatus().ScanCode; + const auto coreWindow = CoreWindow::GetForCurrentThread(); + const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); + const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); - winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, static_cast(key) }; + winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, false, static_cast(key), static_cast(scanCode) }; if (const auto cmd{ _settings.ActionMap().GetActionByKeyChord(kc) }) { if (CommandPalette().Visibility() == Visibility::Visible && cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) diff --git a/src/cascadia/TerminalControl/KeyChord.cpp b/src/cascadia/TerminalControl/KeyChord.cpp index b8faab323..4dd61d6fa 100644 --- a/src/cascadia/TerminalControl/KeyChord.cpp +++ b/src/cascadia/TerminalControl/KeyChord.cpp @@ -10,32 +10,27 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; namespace winrt::Microsoft::Terminal::Control::implementation { - KeyChord::KeyChord() noexcept : - _modifiers{ 0 }, - _vkey{ 0 } + static VirtualKeyModifiers modifiersFromBooleans(bool ctrl, bool alt, bool shift, bool win) + { + VirtualKeyModifiers modifiers = VirtualKeyModifiers::None; + WI_SetFlagIf(modifiers, VirtualKeyModifiers::Control, ctrl); + WI_SetFlagIf(modifiers, VirtualKeyModifiers::Menu, alt); + WI_SetFlagIf(modifiers, VirtualKeyModifiers::Shift, shift); + WI_SetFlagIf(modifiers, VirtualKeyModifiers::Windows, win); + return modifiers; + } + + KeyChord::KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey, int32_t scanCode) noexcept : + _modifiers{ modifiersFromBooleans(ctrl, alt, shift, win) }, + _vkey{ vkey }, + _scanCode{ scanCode } { } - KeyChord::KeyChord(bool ctrl, bool alt, bool shift, int32_t vkey) noexcept : - _modifiers{ (ctrl ? VirtualKeyModifiers::Control : VirtualKeyModifiers::None) | - (alt ? VirtualKeyModifiers::Menu : VirtualKeyModifiers::None) | - (shift ? VirtualKeyModifiers::Shift : VirtualKeyModifiers::None) }, - _vkey{ vkey } - { - } - - KeyChord::KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey) noexcept : - _modifiers{ (ctrl ? VirtualKeyModifiers::Control : VirtualKeyModifiers::None) | - (alt ? VirtualKeyModifiers::Menu : VirtualKeyModifiers::None) | - (shift ? VirtualKeyModifiers::Shift : VirtualKeyModifiers::None) | - (win ? VirtualKeyModifiers::Windows : VirtualKeyModifiers::None) }, - _vkey{ vkey } - { - } - - KeyChord::KeyChord(VirtualKeyModifiers const& modifiers, int32_t vkey) noexcept : + KeyChord::KeyChord(const VirtualKeyModifiers modifiers, int32_t vkey, int32_t scanCode) noexcept : _modifiers{ modifiers }, - _vkey{ vkey } + _vkey{ vkey }, + _scanCode{ scanCode } { } @@ -58,4 +53,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _vkey = value; } + + int32_t KeyChord::ScanCode() noexcept + { + return _scanCode; + } + + void KeyChord::ScanCode(int32_t value) noexcept + { + _scanCode = value; + } } diff --git a/src/cascadia/TerminalControl/KeyChord.h b/src/cascadia/TerminalControl/KeyChord.h index d659d03cf..23d42462d 100644 --- a/src/cascadia/TerminalControl/KeyChord.h +++ b/src/cascadia/TerminalControl/KeyChord.h @@ -9,19 +9,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation { struct KeyChord : KeyChordT { - KeyChord() noexcept; - KeyChord(winrt::Windows::System::VirtualKeyModifiers const& modifiers, int32_t vkey) noexcept; - KeyChord(bool ctrl, bool alt, bool shift, int32_t vkey) noexcept; - KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey) noexcept; + KeyChord() noexcept = default; + KeyChord(const winrt::Windows::System::VirtualKeyModifiers modifiers, int32_t vkey, int32_t scanCode) noexcept; + KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey, int32_t scanCode) noexcept; winrt::Windows::System::VirtualKeyModifiers Modifiers() noexcept; void Modifiers(winrt::Windows::System::VirtualKeyModifiers const& value) noexcept; int32_t Vkey() noexcept; void Vkey(int32_t value) noexcept; + int32_t ScanCode() noexcept; + void ScanCode(int32_t value) noexcept; private: - winrt::Windows::System::VirtualKeyModifiers _modifiers; - int32_t _vkey; + winrt::Windows::System::VirtualKeyModifiers _modifiers{}; + int32_t _vkey{}; + int32_t _scanCode{}; }; } diff --git a/src/cascadia/TerminalControl/KeyChord.idl b/src/cascadia/TerminalControl/KeyChord.idl index a1df50ddd..fb2325ad8 100644 --- a/src/cascadia/TerminalControl/KeyChord.idl +++ b/src/cascadia/TerminalControl/KeyChord.idl @@ -7,11 +7,11 @@ namespace Microsoft.Terminal.Control runtimeclass KeyChord { KeyChord(); - KeyChord(Windows.System.VirtualKeyModifiers modifiers, Int32 vkey); - KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Int32 vkey); - KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Boolean win, Int32 vkey); + KeyChord(Windows.System.VirtualKeyModifiers modifiers, Int32 vkey, Int32 scanCode); + KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Boolean win, Int32 vkey, Int32 scanCode); Windows.System.VirtualKeyModifiers Modifiers; Int32 Vkey; + Int32 ScanCode; } } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 1b873bf3c..435dbc178 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -798,7 +798,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation modifiers.IsCtrlPressed(), modifiers.IsAltPressed(), modifiers.IsShiftPressed(), + modifiers.IsWinPressed(), VK_F7, + 0, }); } @@ -927,6 +929,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation modifiers.IsShiftPressed(), modifiers.IsWinPressed(), vkey, + scanCode, }); if (!success) { diff --git a/src/cascadia/TerminalSettingsEditor/KeyChordListener.cpp b/src/cascadia/TerminalSettingsEditor/KeyChordListener.cpp index f85dca27c..9fc417ce3 100644 --- a/src/cascadia/TerminalSettingsEditor/KeyChordListener.cpp +++ b/src/cascadia/TerminalSettingsEditor/KeyChordListener.cpp @@ -18,6 +18,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { DependencyProperty KeyChordListener::_KeysProperty{ nullptr }; + // The ModifierKeys have been sorted by value. + // Not just binary search, but also your CPU likes sorted data. static constexpr std::array ModifierKeys{ VirtualKey::Shift, VirtualKey::Control, @@ -129,7 +131,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } // Permitted key events are used to update _Keys - Keys({ modifiers, static_cast(key) }); + Keys({ modifiers, static_cast(key), 0 }); e.Handled(true); } } diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index d9eb81be0..b0740f8d3 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -513,9 +513,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // If we had to find one from a layer above that, parent->_MaskingActions // would have found it, so we inherit it for free! const auto& inheritedCmd{ parent->_GetActionByID(actionID) }; - if (inheritedCmd.has_value() && inheritedCmd.value()) + if (inheritedCmd && *inheritedCmd) { - const auto& inheritedCmdImpl{ get_self(inheritedCmd.value()) }; + const auto& inheritedCmdImpl{ get_self(*inheritedCmd) }; maskingCmd = *inheritedCmdImpl->Copy(); _MaskingActions.emplace(actionID, maskingCmd); } @@ -683,18 +683,24 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // - keys: the key chord of the command to search for // Return Value: // - the command with the given key chord - // - nullptr if the key chord is explicitly unbound + // - nullptr if the key chord doesn't exist Model::Command ActionMap::GetActionByKeyChord(Control::KeyChord const& keys) const { - // Check the current layer - const auto cmd{ _GetActionByKeyChordInternal(keys) }; - if (cmd.has_value()) + const auto modifiers = keys.Modifiers(); + + // The "keys" given to us can contain both a Vkey, as well as a ScanCode. + // For instance our UI code fills out a KeyChord with all available information. + // But our _KeyMap only contains KeyChords that contain _either_ a Vkey or ScanCode. + // Due to this we'll have to call _GetActionByKeyChordInternal twice. + if (auto vkey = keys.Vkey()) { - return *cmd; + if (auto command = _GetActionByKeyChordInternal({ modifiers, vkey, 0 })) + { + return *command; + } } - // This key chord is not explicitly bound - return nullptr; + return _GetActionByKeyChordInternal({ modifiers, 0, keys.ScanCode() }).value_or(nullptr); } // Method Description: @@ -709,8 +715,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation std::optional ActionMap::_GetActionByKeyChordInternal(Control::KeyChord const& keys) const { // Check the current layer - const auto actionIDPair{ _KeyMap.find(keys) }; - if (actionIDPair != _KeyMap.end()) + if (const auto actionIDPair = _KeyMap.find(keys); actionIDPair != _KeyMap.end()) { // the command was explicitly bound, // return what we found (invalid commands exposed as nullptr) @@ -723,7 +728,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation for (const auto& parent : _parents) { const auto& inheritedCmd{ parent->_GetActionByKeyChordInternal(keys) }; - if (inheritedCmd.has_value()) + if (inheritedCmd) { return *inheritedCmd; } @@ -765,7 +770,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto hash{ Hash(actionAndArgs) }; if (const auto& cmd{ _GetActionByID(hash) }) { - return cmd.value().Keys(); + return cmd->Keys(); } // Check our parents diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index 32983767e..c81bd14c3 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -36,7 +36,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { std::size_t operator()(const Control::KeyChord& key) const { - return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(key.Modifiers(), key.Vkey()); + return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(key.Modifiers(), key.Vkey(), key.ScanCode()); } }; @@ -44,7 +44,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { bool operator()(const Control::KeyChord& lhs, const Control::KeyChord& rhs) const { - return lhs.Modifiers() == rhs.Modifiers() && lhs.Vkey() == rhs.Vkey(); + return lhs.Modifiers() == rhs.Modifiers() && lhs.Vkey() == rhs.Vkey() && lhs.ScanCode() == rhs.ScanCode(); } }; @@ -79,7 +79,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation private: std::optional _GetActionByID(const InternalActionID actionID) const; - std::optional _GetActionByKeyChordInternal(Control::KeyChord const& keys) const; + std::optional _GetActionByKeyChordInternal(const Control::KeyChord& keys) const; void _PopulateAvailableActionsWithStandardCommands(std::unordered_map& availableActions, std::unordered_set& visitedActionIDs) const; void _PopulateNameMapWithSpecialCommands(std::unordered_map& nameMap) const; diff --git a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp index 0cc62460c..8e0b7162f 100644 --- a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp @@ -17,6 +17,8 @@ constexpr std::wstring_view SHIFT_KEY{ L"shift" }; constexpr std::wstring_view ALT_KEY{ L"alt" }; constexpr std::wstring_view WIN_KEY{ L"win" }; +// If you modify this list you should modify the +// KeyChordSegment description in profiles.schema.json. #define VKEY_NAME_PAIRS(XX) \ XX(VK_RETURN, L"enter") \ XX(VK_TAB, L"tab") \ @@ -73,7 +75,33 @@ constexpr std::wstring_view WIN_KEY{ L"win" }; XX(VK_NUMPAD7, L"numpad7", L"numpad_7") \ XX(VK_NUMPAD8, L"numpad8", L"numpad_8") \ XX(VK_NUMPAD9, L"numpad9", L"numpad_9") \ - XX(VK_OEM_PLUS, L"plus") + XX(VK_OEM_PLUS, L"plus") /* '+' any country */ \ + XX(VK_OEM_COMMA, L"comma") /* ',' any country */ \ + XX(VK_OEM_MINUS, L"minus") /* '-' any country */ \ + XX(VK_OEM_PERIOD, L"period") /* '.' any country */ + +constexpr std::wstring_view vkeyPrefix{ L"vk(" }; +constexpr std::wstring_view scanCodePrefix{ L"sc(" }; +constexpr std::wstring_view codeSuffix{ L")" }; + +// Parses a vk(nnn) or sc(nnn) key chord part. +// If the part doesn't contain either of these two this function returns 0. +// For invalid arguments we throw an exception. +static int32_t parseNumericCode(const std::wstring_view& str, const std::wstring_view& prefix, const std::wstring_view& suffix) +{ + if (!til::ends_with(str, suffix) || !til::starts_with(str, prefix)) + { + return 0; + } + + const auto value = til::from_wchars({ str.data() + prefix.size(), str.size() - prefix.size() - suffix.size() }); + if (value > 0 && value < 256) + { + return gsl::narrow_cast(value); + } + + throw winrt::hresult_invalid_argument(L"Invalid numeric argument to vk() or sc()"); +} // Function Description: // - Deserializes the given string into a new KeyChord instance. If this @@ -109,6 +137,7 @@ static KeyChord _fromString(std::wstring_view wstr) VirtualKeyModifiers modifiers = VirtualKeyModifiers::None; int32_t vkey = 0; + int32_t scanCode = 0; while (!wstr.empty()) { @@ -132,10 +161,9 @@ static KeyChord _fromString(std::wstring_view wstr) } else { - if (vkey) + if (vkey || scanCode) { - // Key bindings like Ctrl+A+B are not valid. - throw winrt::hresult_invalid_argument(); + throw winrt::hresult_invalid_argument(L"Key bindings like Ctrl+A+B are not valid"); } // Characters 0-9, a-z, A-Z directly map to virtual keys. @@ -149,6 +177,22 @@ static KeyChord _fromString(std::wstring_view wstr) } } + // vk() allows a user to specify a virtual key code + // and sc() allows them to specify a scan code manually. + // + // ctrl+vk(0x09) for instance is the same as ctrl+tab, while win+sc(41) specifies + // a key binding which is (seemingly) always bound to the key below Esc. + vkey = parseNumericCode(part, vkeyPrefix, codeSuffix); + if (vkey) + { + continue; + } + scanCode = parseNumericCode(part, scanCodePrefix, codeSuffix); + if (scanCode) + { + continue; + } + // nameToVkey contains a few more mappings like "F11". if (const auto it = nameToVkey.find(part); it != nameToVkey.end()) { @@ -172,11 +216,11 @@ static KeyChord _fromString(std::wstring_view wstr) } } - throw winrt::hresult_invalid_argument(); + throw winrt::hresult_invalid_argument(L"Invalid key binding"); } } - return KeyChord{ modifiers, vkey }; + return KeyChord{ modifiers, vkey, scanCode }; } // Function Description: @@ -204,6 +248,7 @@ static std::wstring _toString(const KeyChord& chord) const auto modifiers = chord.Modifiers(); const auto vkey = chord.Vkey(); + const auto scanCode = chord.ScanCode(); std::wstring buffer; // Add modifiers @@ -228,6 +273,14 @@ static std::wstring _toString(const KeyChord& chord) buffer.push_back(L'+'); } + if (scanCode) + { + buffer.append(scanCodePrefix); + buffer.append(std::to_wstring(scanCode)); + buffer.append(codeSuffix); + return buffer; + } + // Quick lookup: ranges of vkeys that correlate directly to a key. if ((vkey >= L'0' && vkey <= L'9') || (vkey >= L'A' && vkey <= L'Z')) { @@ -248,6 +301,14 @@ static std::wstring _toString(const KeyChord& chord) return buffer; } + if (vkey) + { + buffer.append(vkeyPrefix); + buffer.append(std::to_wstring(vkey)); + buffer.append(codeSuffix); + return buffer; + } + return {}; } diff --git a/src/cascadia/TerminalSettingsModel/defaults-universal.json b/src/cascadia/TerminalSettingsModel/defaults-universal.json index 320c6b0e7..4aa5128d7 100644 --- a/src/cascadia/TerminalSettingsModel/defaults-universal.json +++ b/src/cascadia/TerminalSettingsModel/defaults-universal.json @@ -170,8 +170,8 @@ { "command": "scrollUpPage", "keys": "ctrl+shift+pgup" }, // Visual Adjustments - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+=" }, - { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+-" }, + { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus" }, + { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+minus" }, { "command": "resetFontSize", "keys": "ctrl+0" }, // Other commands diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 84f52f287..0003e0b72 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -298,7 +298,7 @@ { "command": "commandPalette", "keys":"ctrl+shift+p" }, { "command": "identifyWindow" }, { "command": "openWindowRenamer" }, - { "command": "quakeMode", "keys":"win+`" }, + { "command": "quakeMode", "keys":"win+sc(41)" }, // Tab Management // "command": "closeTab" is unbound by default. @@ -362,8 +362,8 @@ { "command": "scrollToBottom", "keys": "ctrl+shift+end" }, // Visual Adjustments - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+=" }, - { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+-" }, + { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus" }, + { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+minus" }, { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+numpad_plus" }, { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+numpad_minus" }, { "command": "resetFontSize", "keys": "ctrl+0" }, diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 8566f2219..53e21ccdd 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -669,23 +669,30 @@ void AppHost::_listenForInboundConnections() winrt::fire_and_forget AppHost::_setupGlobalHotkeys() { // The hotkey MUST be registered on the main thread. It will fail otherwise! - co_await winrt::resume_foreground(_logic.GetRoot().Dispatcher(), - winrt::Windows::UI::Core::CoreDispatcherPriority::Normal); + co_await winrt::resume_foreground(_logic.GetRoot().Dispatcher()); - // Remove all the already registered hotkeys before setting up the new ones. - _window->UnsetHotkeys(_hotkeys); - - _hotkeyActions = _logic.GlobalHotkeys(); - _hotkeys.clear(); - for (const auto& [k, v] : _hotkeyActions) + // Unregister all previously registered hotkeys. + // + // RegisterHotKey(), will not unregister hotkeys automatically. + // If a hotkey with a given HWND and ID combination already exists + // then a duplicate one will be added, which we don't want. + // (Additionally we want to remove hotkeys that were removed from the settings.) + for (int i = 0, count = gsl::narrow_cast(_hotkeys.size()); i < count; ++i) { - if (k != nullptr) - { - _hotkeys.push_back(k); - } + _window->UnregisterHotKey(i); } - _window->SetGlobalHotkeys(_hotkeys); + _hotkeys.clear(); + + // Re-register all current hotkeys. + for (const auto& [keyChord, cmd] : _logic.GlobalHotkeys()) + { + if (auto summonArgs = cmd.ActionAndArgs().Args().try_as()) + { + _window->RegisterHotKey(gsl::narrow_cast(_hotkeys.size()), keyChord); + _hotkeys.emplace_back(summonArgs); + } + } } // Method Description: @@ -706,47 +713,40 @@ void AppHost::_GlobalHotkeyPressed(const long hotkeyIndex) { return; } - // Lookup the matching keychord - Control::KeyChord kc = _hotkeys.at(hotkeyIndex); - // Get the stored Command for that chord - if (const auto& cmd{ _hotkeyActions.Lookup(kc) }) + + const auto& summonArgs = til::at(_hotkeys, hotkeyIndex); + Remoting::SummonWindowSelectionArgs args{ summonArgs.Name() }; + + // desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false + // desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false + // desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true + args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent); + args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent); + args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility()); + args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration()); + + switch (summonArgs.Monitor()) { - if (const auto& summonArgs{ cmd.ActionAndArgs().Args().try_as() }) - { - Remoting::SummonWindowSelectionArgs args{ summonArgs.Name() }; + case Settings::Model::MonitorBehavior::Any: + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + break; + case Settings::Model::MonitorBehavior::ToCurrent: + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToCurrent); + break; + case Settings::Model::MonitorBehavior::ToMouse: + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToMouse); + break; + } - // desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false - // desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false - // desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true - args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent); - args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent); - args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility()); - args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration()); - - switch (summonArgs.Monitor()) - { - case Settings::Model::MonitorBehavior::Any: - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); - break; - case Settings::Model::MonitorBehavior::ToCurrent: - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToCurrent); - break; - case Settings::Model::MonitorBehavior::ToMouse: - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToMouse); - break; - } - - _windowManager.SummonWindow(args); - if (args.FoundMatch()) - { - // Excellent, the window was found. We have nothing else to do here. - } - else - { - // We should make the window ourselves. - _createNewTerminalWindow(summonArgs); - } - } + _windowManager.SummonWindow(args); + if (args.FoundMatch()) + { + // Excellent, the window was found. We have nothing else to do here. + } + else + { + // We should make the window ourselves. + _createNewTerminalWindow(summonArgs); } } diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index a8511dbc2..5f6d10ca5 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -20,19 +20,17 @@ public: bool HasWindow(); private: - bool _useNonClientArea; - std::unique_ptr _window; winrt::TerminalApp::App _app; winrt::TerminalApp::AppLogic _logic; - bool _shouldCreateWindow{ false }; winrt::Microsoft::Terminal::Remoting::WindowManager _windowManager{ nullptr }; - std::vector _hotkeys{}; - winrt::Windows::Foundation::Collections::IMapView _hotkeyActions{ nullptr }; - + std::vector _hotkeys; winrt::com_ptr _desktopManager{ nullptr }; + bool _shouldCreateWindow{ false }; + bool _useNonClientArea{ false }; + void _HandleCommandlineArgs(); void _HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 4236102f0..230b9f6bf 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -966,62 +966,62 @@ void IslandWindow::_SetIsFullscreen(const bool fullscreenEnabled) } // Method Description: -// - Call UnregisterHotKey once for each entry in hotkeyList, to unset all the bound global hotkeys. -// Arguments: -// - hotkeyList: a list of hotkeys to unbind +// - Call UnregisterHotKey once for each previously registered hotkey. // Return Value: // - -void IslandWindow::UnsetHotkeys(const std::vector& hotkeyList) +void IslandWindow::UnregisterHotKey(const int index) noexcept { - TraceLoggingWrite(g_hWindowsTerminalProvider, - "UnsetHotkeys", - TraceLoggingDescription("Emitted when clearing previously set hotkeys"), - TraceLoggingInt64(hotkeyList.size(), "numHotkeys", "The number of hotkeys to unset"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + TraceLoggingWrite( + g_hWindowsTerminalProvider, + "UnregisterAllHotKeys", + TraceLoggingDescription("Emitted when clearing previously set hotkeys"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - for (int i = 0; i < ::base::saturated_cast(hotkeyList.size()); i++) - { - LOG_IF_WIN32_BOOL_FALSE(UnregisterHotKey(_window.get(), i)); - } + LOG_IF_WIN32_BOOL_FALSE(::UnregisterHotKey(_window.get(), index)); } // Method Description: -// - Call RegisterHotKey once for each entry in hotkeyList, to attempt to -// register that keybinding as a global hotkey. +// - Call RegisterHotKey to attempt to register that keybinding as a global hotkey. // - When these keys are pressed, we'll get a WM_HOTKEY message with the payload // containing the index we registered here. +// - Call UnregisterHotKey() before registering your hotkeys. +// See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey#remarks // Arguments: -// - hotkeyList: a list of hotkeys to bind +// - hotkey: The key-combination to register. // Return Value: // - -void IslandWindow::SetGlobalHotkeys(const std::vector& hotkeyList) +void IslandWindow::RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept { - TraceLoggingWrite(g_hWindowsTerminalProvider, - "SetGlobalHotkeys", - TraceLoggingDescription("Emitted when setting hotkeys"), - TraceLoggingInt64(hotkeyList.size(), "numHotkeys", "The number of hotkeys to set"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - int index = 0; - for (const auto& hotkey : hotkeyList) + TraceLoggingWrite( + g_hWindowsTerminalProvider, + "RegisterHotKey", + TraceLoggingDescription("Emitted when setting hotkeys"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + auto vkey = hotkey.Vkey(); + if (!vkey) + { + vkey = MapVirtualKeyW(hotkey.ScanCode(), MAPVK_VSC_TO_VK); + } + if (!vkey) + { + return; + } + + auto hotkeyFlags = MOD_NOREPEAT; { const auto modifiers = hotkey.Modifiers(); - const auto hotkeyFlags = MOD_NOREPEAT | - (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows) ? MOD_WIN : 0) | - (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu) ? MOD_ALT : 0) | - (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control) ? MOD_CONTROL : 0) | - (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift) ? MOD_SHIFT : 0); - - // TODO GH#8888: We should display a warning of some kind if this fails. - // This can fail if something else already bound this hotkey. - LOG_IF_WIN32_BOOL_FALSE(RegisterHotKey(_window.get(), - index, - hotkeyFlags, - hotkey.Vkey())); - - index++; + WI_SetFlagIf(hotkeyFlags, MOD_WIN, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows)); + WI_SetFlagIf(hotkeyFlags, MOD_ALT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu)); + WI_SetFlagIf(hotkeyFlags, MOD_CONTROL, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control)); + WI_SetFlagIf(hotkeyFlags, MOD_SHIFT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift)); } + + // TODO GH#8888: We should display a warning of some kind if this fails. + // This can fail if something else already bound this hotkey. + LOG_IF_WIN32_BOOL_FALSE(::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey)); } // Method Description: diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 0e5859bc4..dc6c48c84 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -38,8 +38,8 @@ public: void FlashTaskbar(); void SetTaskbarProgress(const size_t state, const size_t progress); - void UnsetHotkeys(const std::vector& hotkeyList); - void SetGlobalHotkeys(const std::vector& hotkeyList); + void UnregisterHotKey(const int index) noexcept; + void RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept; winrt::fire_and_forget SummonWindow(winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior args); @@ -65,8 +65,8 @@ protected: HWND _interopWindowHandle; winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; - winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; + wil::com_ptr _taskbar; std::function _pfnCreateCallback; std::function _pfnSnapDimensionCallback; @@ -79,9 +79,9 @@ protected: bool _alwaysOnTop{ false }; bool _fullscreen{ false }; bool _fWasMaximizedBeforeFullscreen{ false }; - RECT _rcWindowBeforeFullscreen; - RECT _rcWorkBeforeFullscreen; - UINT _dpiBeforeFullscreen; + RECT _rcWindowBeforeFullscreen{}; + RECT _rcWorkBeforeFullscreen{}; + UINT _dpiBeforeFullscreen{ 96 }; virtual void _SetIsBorderless(const bool borderlessEnabled); virtual void _SetIsFullscreen(const bool fullscreenEnabled); @@ -90,8 +90,6 @@ protected: LONG _getDesiredWindowStyle() const; - wil::com_ptr _taskbar; - void _OnGetMinMaxInfo(const WPARAM wParam, const LPARAM lParam); long _calculateTotalSize(const bool isWidth, const long clientSize, const long nonClientSize); diff --git a/src/inc/til/string.h b/src/inc/til/string.h index 4bcbf3d5e..13299e73e 100644 --- a/src/inc/til/string.h +++ b/src/inc/til/string.h @@ -73,6 +73,77 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return ends_with<>(str, prefix); } + inline constexpr unsigned long from_wchars_error = ULONG_MAX; + + // Just like std::wcstoul, but without annoying locales and null-terminating strings. + // It has been fuzz-tested against clang's strtoul implementation. + _TIL_INLINEPREFIX unsigned long from_wchars(const std::wstring_view& str) noexcept + { + static constexpr unsigned long maximumValue = ULONG_MAX / 16; + + // We don't have to test ptr for nullability, as we only access it under either condition: + // * str.length() > 0, for determining the base + // * ptr != end, when parsing the characters; if ptr is null, length will be 0 and thus end == ptr +#pragma warning(push) +#pragma warning(disable : 26429) // Symbol 'ptr' is never tested for nullness, it can be marked as not_null +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead + auto ptr = str.data(); + const auto end = ptr + str.length(); + unsigned long base = 10; + unsigned long accumulator = 0; + unsigned long value = ULONG_MAX; + + if (str.length() > 1 && *ptr == L'0') + { + base = 8; + ptr++; + + if (str.length() > 2 && (*ptr == L'x' || *ptr == L'X')) + { + base = 16; + ptr++; + } + } + + if (ptr == end) + { + return from_wchars_error; + } + + for (;; accumulator *= base) + { + value = ULONG_MAX; + if (*ptr >= L'0' && *ptr <= L'9') + { + value = *ptr - L'0'; + } + else if (*ptr >= L'A' && *ptr <= L'F') + { + value = *ptr - L'A' + 10; + } + else if (*ptr >= L'a' && *ptr <= L'f') + { + value = *ptr - L'a' + 10; + } + else + { + return from_wchars_error; + } + + accumulator += value; + if (accumulator >= maximumValue) + { + return from_wchars_error; + } + + if (++ptr == end) + { + return accumulator; + } + } +#pragma warning(pop) + } + // Just like std::tolower, but without annoying locales. template constexpr T tolower_ascii(T c) diff --git a/src/til/ut_til/string.cpp b/src/til/ut_til/string.cpp index eb4fbe488..c25a19972 100644 --- a/src/til/ut_til/string.cpp +++ b/src/til/ut_til/string.cpp @@ -53,6 +53,56 @@ class StringTests VERIFY_IS_TRUE(til::ends_with("0abc", "abc")); } + // Normally this would be the spot where you'd find a TEST_METHOD(from_wchars). + // I didn't quite trust my coding skills and thus opted to use fuzz-testing. + // The below function was used to test from_wchars for unsafety and conformance with clang's strtoul. + // The test was run as: + // clang++ -fsanitize=address,undefined,fuzzer -std=c++17 file.cpp + // and was run for 20min across 16 jobs in parallel. +#if 0 + extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) + { + while (size > 0 && (isspace(*data) || *data == '+' || *data == '-')) + { + --size; + ++data; + } + + if (size == 0 || size > 127) + { + return 0; + } + + char narrow_buffer[128]; + wchar_t wide_buffer[128]; + + memcpy(narrow_buffer, data, size); + for (size_t i = 0; i < size; ++i) + { + wide_buffer[i] = data[i]; + } + + // strtoul requires a null terminator + narrow_buffer[size] = 0; + wide_buffer[size] = 0; + + char* end; + const auto expected = strtoul(narrow_buffer, &end, 0); + if (end != narrow_buffer + size || expected >= ULONG_MAX / 16) + { + return 0; + } + + const auto actual = from_wchars({ wide_buffer, size }); + if (expected != actual) + { + __builtin_trap(); + } + + return 0; + } +#endif + TEST_METHOD(tolower_ascii) { for (wchar_t ch = 0; ch < 128; ++ch) diff --git a/tools/GenerateHeaderForJson.ps1 b/tools/GenerateHeaderForJson.ps1 index 1116275e0..953ca8594 100644 --- a/tools/GenerateHeaderForJson.ps1 +++ b/tools/GenerateHeaderForJson.ps1 @@ -2,34 +2,26 @@ # the contents of that json files as a constexpr string_view in the header. param ( - [parameter(Mandatory=$true, Position=0)] + [parameter(Mandatory = $true)] [string]$JsonFile, - [parameter(Mandatory=$true, Position=1)] + [parameter(Mandatory = $true)] [string]$OutPath, - [parameter(Mandatory=$true, Position=2)] + [parameter(Mandatory = $true)] [string]$VariableName ) -# Load the xml files. +$fullPath = Resolve-Path $JsonFile $jsonData = Get-Content $JsonFile -Write-Output "// Copyright (c) Microsoft Corporation" | Out-File -FilePath $OutPath -Encoding ASCII -Write-Output "// Licensed under the MIT license." | Out-File -FilePath $OutPath -Encoding ASCII -Append -Write-Output "" | Out-File -FilePath $OutPath -Encoding ASCII -Append -Write-Output "// THIS IS AN AUTO-GENERATED FILE" | Out-File -FilePath $OutPath -Encoding ASCII -Append -Write-Output "// Generated from " | Out-File -FilePath $OutPath -Encoding ASCII -Append -NoNewline -$fullPath = Resolve-Path -Path $JsonFile -Write-Output $fullPath.Path | Out-File -FilePath $OutPath -Encoding ASCII -Append -Write-Output "constexpr std::string_view $($VariableName){ " | Out-File -FilePath $OutPath -Encoding ASCII -Append - -# Write each line escaped on its own, as it's own literal. This file is _very -# big_, so big that it cannot fit in a single string literal :O The compiler is, -# however, smart enough to just concatenate all these literals into one big -# string. -$jsonData | foreach { - Write-Output "R`"($_`n)`"" | Out-File -FilePath $OutPath -Encoding ASCII -Append -} -Write-Output "};" | Out-File -FilePath $OutPath -Encoding ASCII -Append - +@( + "// Copyright (c) Microsoft Corporation", + "// Licensed under the MIT license.", + "", + "// THIS IS AN AUTO-GENERATED FILE", + "// Generated from $($fullPath.Path)", + "constexpr std::string_view $($VariableName){", + ($jsonData | ForEach-Object { "R`"#($_`n)#`"" }), + "};" +) | Out-File -FilePath $OutPath -Encoding utf8 From 8779249b121591e81a51e1bfcb2dbec30943da01 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 21 Jul 2021 07:59:57 +0200 Subject: [PATCH 16/90] Release unneeded memory more eagerly from conhost (#10738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `_CONSOLE_API_MSG` buffer is resized to cover an entire message. Later on any UTF-8 data is cached in a separate temporary buffer inside `til::u8state` to prevent lone surrogate pairs. Both cases are problematic as neither buffer is freed after the read has finished. Passing a 100MB buffer to conhost once will thus cause it to continue using ~220MB of physical memory until the conhost process exits. This change releases unneeded memory as soon as the requested buffer size has halved. In practice this means that once a command has returned all buffers will shrink, as the shell commonly sends very small messages. ## PR Checklist * [x] Closes #10731 * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * Buffers aren't reallocated during printing ✔️ * Buffers shrink after printing finished ✔️ --- src/inc/til/u8u16convert.h | 8 ++++++++ src/server/ApiMessage.cpp | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/inc/til/u8u16convert.h b/src/inc/til/u8u16convert.h index b4d3cd9f8..af6e4f9b7 100644 --- a/src/inc/til/u8u16convert.h +++ b/src/inc/til/u8u16convert.h @@ -55,6 +55,14 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" RETURN_HR_IF(E_ABORT, !base::CheckAdd(in.length(), _partialsLen).AssignIfValid(&capacity)); _buffer.clear(); + + // If we were previously called with a huge buffer we have an equally large _buffer. + // We shouldn't just keep this huge buffer around, if no one needs it anymore. + if (_buffer.capacity() > 16 * 1024 && (_buffer.capacity() >> 1) > capacity) + { + _buffer.shrink_to_fit(); + } + _buffer.reserve(capacity); // copy UTF-8 code units that were remaining from the previous call (if any) diff --git a/src/server/ApiMessage.cpp b/src/server/ApiMessage.cpp index 598111c88..9287965d3 100644 --- a/src/server/ApiMessage.cpp +++ b/src/server/ApiMessage.cpp @@ -104,7 +104,14 @@ try { RETURN_HR_IF(E_FAIL, State.ReadOffset > Descriptor.InputSize); - ULONG const cbReadSize = Descriptor.InputSize - State.ReadOffset; + const ULONG cbReadSize = Descriptor.InputSize - State.ReadOffset; + + // If we were previously called with a huge buffer we have an equally large _inputBuffer. + // We shouldn't just keep this huge buffer around, if no one needs it anymore. + if (_inputBuffer.capacity() > 16 * 1024 && (_inputBuffer.capacity() >> 1) > cbReadSize) + { + _inputBuffer.shrink_to_fit(); + } _inputBuffer.resize(cbReadSize); @@ -145,10 +152,17 @@ try ULONG cbWriteSize = Descriptor.OutputSize - State.WriteOffset; RETURN_IF_FAILED(ULongMult(cbWriteSize, cbFactor, &cbWriteSize)); + // If we were previously called with a huge buffer we have an equally large _outputBuffer. + // We shouldn't just keep this huge buffer around, if no one needs it anymore. + if (_outputBuffer.capacity() > 16 * 1024 && (_outputBuffer.capacity() >> 1) > cbWriteSize) + { + _outputBuffer.shrink_to_fit(); + } + _outputBuffer.resize(cbWriteSize); // 0 it out. - std::fill(_outputBuffer.begin(), _outputBuffer.end(), (BYTE)0); + std::fill_n(_outputBuffer.data(), _outputBuffer.size(), BYTE(0)); State.OutputBuffer = _outputBuffer.data(); State.OutputBufferSize = cbWriteSize; From d1f152adcfba5e9dc211e9ec0658b2fa1cefcd3a Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Wed, 21 Jul 2021 15:41:11 -0700 Subject: [PATCH 17/90] Don't auto-generate the hidden field when creating profile stubs (#10714) ## Summary of the Pull Request We no longer automatically write the 'hidden' field for profile stubs we create **Note**: This does not retroactively remove the automatically generated hidden fields in current settings files ## PR Checklist * [x] Closes #10539 * [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 * [ ] Schema updated. * [x] I work here ## Validation Steps Performed Deleted the ubuntu stub in my settings file, booted up terminal, new created stub did not have the hidden field. Created a fragment that overrides the hidden field and it worked. --- src/cascadia/TerminalSettingsModel/Profile.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/Profile.cpp b/src/cascadia/TerminalSettingsModel/Profile.cpp index 430be7b24..a684acc95 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.cpp +++ b/src/cascadia/TerminalSettingsModel/Profile.cpp @@ -237,8 +237,6 @@ Json::Value Profile::GenerateStub() const stub[JsonKey(SourceKey)] = winrt::to_string(source); } - stub[JsonKey(HiddenKey)] = Hidden(); - return stub; } From 41ade2c57e9e554dd2b7c63df22cec13afb6ec01 Mon Sep 17 00:00:00 2001 From: Michael Niksa Date: Thu, 22 Jul 2021 05:51:30 -0700 Subject: [PATCH 18/90] Pass inbound handoff message via heap so it cannot race out of scope by the time it reaches the ConsoleIoThread (#10751) Pass inbound handoff message via heap so it cannot race out of scope by the time it reaches the ConsoleIoThread ## PR Checklist * [x] Closes #10251 * [x] I work here. * [x] Manually verified somewhat ## Detailed Description of the Pull Request / Additional comments - `OpenConsole.exe` is started in response to the OS `conhost.exe` request for a handoff and prepares an Out Of Proc Multithreaded COM server. - A COM thread from the pool inside `OpenConsole.exe` picks up the inbound message and allocates some stack space for the `CONSOLE_API_MSG` coming in - That COM thread calls down to set up the I/O thread that will pump the console driver handle and passes a pointer to the stack-allocated `CONSOLE_API_MSG` as the `LPVOID` parameter for starting the thread. Now one of two things happen: 1. The I/O thread is scheduled pretty much immediately (or soon enough that the COM thread hasn't messed with the stack space), picks up the pointer to the COM thread's stack with `CONSOLE_API_MSG`, and processes the initial message correctly. 2. The COM thread continues and finalizes the handoff message to `conhost.exe` declaring success. It then pops stack and "frees" the memory space. If it doesn't manage to overwrite it, we're still good. If it does, then things go crazy. This fix changes it so that the `CONSOLE_API_MSG` is sent into the heap before being passed to the other thread so it's in a known location that won't be freed or overwritten unexpectedly. ## Validation Steps Performed - [x] - Confirmed that many handoffs from the run box seem to work alright on my system after this change. - [x] - Confirmed that many tab creations/splits seem to work alright on my system after this change. - [x] - Would prefer if @ianjoneill could try to F5 this branch to build/deploy it, set it as default, and see if it makes it go away completely... but I'm pretty confident it is this based on the dumps provided either way. --- src/host/srvinit.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/host/srvinit.cpp b/src/host/srvinit.cpp index 5aaab774e..4d17ddfe1 100644 --- a/src/host/srvinit.cpp +++ b/src/host/srvinit.cpp @@ -336,8 +336,27 @@ HRESULT ConsoleCreateIoThread(_In_ HANDLE Server, RETURN_IF_FAILED(g.pDeviceComm->SetServerInformation(&ServerInformation)); } + // Ensure that whatever we're giving to the new thread is on the heap so it cannot + // go out of scope by the time that thread starts. + // (e.g. if someone sent us a pointer to stack memory... that could happen + // ask me how I know... :| ) + std::unique_ptr heapConnectMessage; + if (connectMessage) + { + // Allocate and copy onto the heap + heapConnectMessage = std::make_unique(*connectMessage); + + // Set the pointer that `CreateThread` uses to the heap space + connectMessage = heapConnectMessage.get(); + } + HANDLE const hThread = CreateThread(nullptr, 0, ConsoleIoThread, connectMessage, 0, nullptr); RETURN_HR_IF(E_HANDLE, hThread == nullptr); + + // If we successfully started the other thread, it's that guy's problem to free the connect message. + // (If we didn't make one, it should be no problem to release the empty unique_ptr.) + heapConnectMessage.release(); + LOG_IF_FAILED(SetThreadDescription(hThread, L"Console Driver Message IO Thread")); LOG_IF_WIN32_BOOL_FALSE(CloseHandle(hThread)); // The thread will run on its own and close itself. Free the associated handle. @@ -871,7 +890,11 @@ DWORD WINAPI ConsoleIoThread(LPVOID lpParameter) // If we were given a message on startup, process that in our context and then continue with the IO loop normally. if (lpParameter) { - ReceiveMsg = *(PCONSOLE_API_MSG)lpParameter; + // Capture the incoming lpParameter into a unique_ptr so we can appropriately + // free the heap memory when we're done getting the important bits out of it below. + std::unique_ptr capturedMessage{ static_cast(lpParameter) }; + + ReceiveMsg = *capturedMessage.get(); ReceiveMsg._pApiRoutines = &globals.api; ReceiveMsg._pDeviceComm = globals.pDeviceComm; IoSorter::ServiceIoOperation(&ReceiveMsg, &ReplyMsg); From cf97a9f7729f26372a103358b31f7701d552563c Mon Sep 17 00:00:00 2001 From: Schuyler Rosefield Date: Thu, 22 Jul 2021 08:53:03 -0400 Subject: [PATCH 19/90] Preliminary work to add Swap Panes functionality (GH Issues 1000, 4922) (#10638) ## Summary of the Pull Request Add functionality to swap a pane with an adjacent (Up/Down/Left/Right) neighbor. ## References This work potentially touches on: #1000 #2398 and #4922 ## PR Checklist * [x] Closes a component of #1000 (partially, comment), #4922 (partially, `SwapPanes` function is added but not hooked up, no detach functionality) * [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA * [x] 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 Its been a while since I've written C++ code, and it is my first time working on a Windows application. I hope that I have not made too many mistakes. Work currently done: - Add boilerplate/infrastructure for argument parsing, hotkeys, event handling - Adds the `MovePane` function that finds the focused pane, and then tries to find a pane that is visually adjacent to according to direction. - First pass at the `SwapPanes` function that swaps the tree location of two panes - First working version of helpers `_FindFocusAndNeighbor` and `_FindNeighborFromFocus` that search the tree for the currently focused pane, and then climbs back up the tree to try to find a sibling pane that is adjacent to it. - An `_IsAdjacent' function that tests whether two panes, given their relative offsets, are adjacent to each other according to the direction. Next steps: - Once working these functions (`_FindFocusAndNeighbor`, etc) could be utilized to also solve #2398 by updating the `NavigateFocus` function. - Do we want default hotkeys for the new actions? ## Validation Steps Performed At this point, compilation and manual testing of functionality (with hotkeys) by creating panes, adding distinguishers to each pane, and then swapping them around to confirm they went to the right location. --- doc/cascadia/profiles.schema.json | 19 + .../CommandlineTest.cpp | 119 +++++ .../LocalTests_TerminalApp/TabTests.cpp | 208 +++++++++ .../TerminalApp/AppActionHandlers.cpp | 18 + .../TerminalApp/AppCommandlineArgs.cpp | 52 +++ src/cascadia/TerminalApp/AppCommandlineArgs.h | 4 + src/cascadia/TerminalApp/Pane.cpp | 406 ++++++++++++++++++ src/cascadia/TerminalApp/Pane.h | 35 ++ .../Resources/en-US/Resources.resw | 10 + src/cascadia/TerminalApp/TerminalPage.cpp | 16 + src/cascadia/TerminalApp/TerminalPage.h | 1 + src/cascadia/TerminalApp/TerminalTab.cpp | 33 ++ src/cascadia/TerminalApp/TerminalTab.h | 1 + .../TerminalSettingsModel/ActionAndArgs.cpp | 2 + .../TerminalSettingsModel/ActionArgs.cpp | 27 ++ .../TerminalSettingsModel/ActionArgs.h | 61 +++ .../TerminalSettingsModel/ActionArgs.idl | 6 + .../AllShortcutActions.h | 2 + .../Resources/en-US/Resources.resw | 12 +- .../TerminalSettingsModel/defaults.json | 7 +- 20 files changed, 1037 insertions(+), 2 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 7c8e32c79..e2cf5d71c 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -238,6 +238,7 @@ "identifyWindow", "identifyWindows", "moveFocus", + "movePane", "moveTab", "newTab", "newWindow", @@ -514,6 +515,23 @@ ], "required": [ "direction" ] }, + "MovePaneAction": { + "description": "Arguments corresponding to a Move Pane Action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "movePane" }, + "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." + } + } + } + ], + "required": [ "direction" ] + }, "ResizePaneAction": { "description": "Arguments corresponding to a Resize Pane Action", "allOf": [ @@ -952,6 +970,7 @@ { "$ref": "#/definitions/NewTabAction" }, { "$ref": "#/definitions/SwitchToTabAction" }, { "$ref": "#/definitions/MoveFocusAction" }, + { "$ref": "#/definitions/MovePaneAction" }, { "$ref": "#/definitions/ResizePaneAction" }, { "$ref": "#/definitions/SendInputAction" }, { "$ref": "#/definitions/SplitPaneAction" }, diff --git a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp index f62002e6f..c5d853d32 100644 --- a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp +++ b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp @@ -56,6 +56,7 @@ namespace TerminalAppLocalTests TEST_METHOD(ParseComboCommandlineIntoArgs); TEST_METHOD(ParseFocusTabArgs); TEST_METHOD(ParseMoveFocusArgs); + TEST_METHOD(ParseMovePaneArgs); TEST_METHOD(ParseArgumentsWithParsingTerminators); TEST_METHOD(ParseFocusPaneArgs); @@ -1207,6 +1208,124 @@ namespace TerminalAppLocalTests } } + void CommandlineTest::ParseMovePaneArgs() + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:useShortForm", L"{false, true}") + END_TEST_METHOD_PROPERTIES() + + INIT_TEST_PROPERTY(bool, useShortForm, L"If true, use `mp` instead of `move-pane`"); + const wchar_t* subcommand = useShortForm ? L"mp" : L"move-pane"; + + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand }; + Log::Comment(NoThrowString().Format( + L"Just the subcommand, without a direction, should fail.")); + + _buildCommandlinesExpectFailureHelper(appArgs, 1u, rawCommands); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"left" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction()); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"right" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction()); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"up" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Up, myArgs.Direction()); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"down" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Down, myArgs.Direction()); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"badDirection" }; + Log::Comment(NoThrowString().Format( + L"move-pane with an invalid direction should fail.")); + _buildCommandlinesExpectFailureHelper(appArgs, 1u, rawCommands); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"left", L";", subcommand, L"right" }; + _buildCommandlinesHelper(appArgs, 2u, rawCommands); + + VERIFY_ARE_EQUAL(3u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction()); + + actionAndArgs = appArgs._startupActions.at(2); + VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction()); + } + } + void CommandlineTest::ParseFocusPaneArgs() { BEGIN_TEST_METHOD_PROPERTIES() diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index f56deecd4..078df3c29 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -82,6 +82,8 @@ namespace TerminalAppLocalTests TEST_METHOD(MoveFocusFromZoomedPane); TEST_METHOD(CloseZoomedPane); + TEST_METHOD(MovePanes); + TEST_METHOD(NextMRUTab); TEST_METHOD(VerifyCommandPaletteTabSwitcherOrder); @@ -817,6 +819,212 @@ namespace TerminalAppLocalTests VERIFY_SUCCEEDED(result); } + void TabTests::MovePanes() + { + auto page = _commonSetup(); + + Log::Comment(L"Setup 4 panes."); + // Create the following layout + // ------------------- + // | 1 | 2 | + // | | | + // ------------------- + // | 3 | 4 | + // | | | + // ------------------- + uint32_t firstId = 0, secondId = 0, thirdId = 0, fourthId = 0; + TestOnUIThread([&]() { + VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + firstId = tab->_activePane->Id().value(); + // We start with 1 tab, split vertically to get + // ------------------- + // | 1 | 2 | + // | | | + // ------------------- + page->_SplitPane(SplitState::Vertical, SplitType::Duplicate, 0.5f, nullptr); + secondId = tab->_activePane->Id().value(); + }); + Sleep(250); + TestOnUIThread([&]() { + // After this the `2` pane is focused, go back to `1` being focused + page->_MoveFocus(FocusDirection::Left); + }); + Sleep(250); + TestOnUIThread([&]() { + // Split again to make the 3rd tab + // ------------------- + // | 1 | | + // | | | + // ---------| 2 | + // | 3 | | + // | | | + // ------------------- + page->_SplitPane(SplitState::Horizontal, SplitType::Duplicate, 0.5f, nullptr); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + // Split again to make the 3rd tab + thirdId = tab->_activePane->Id().value(); + }); + Sleep(250); + TestOnUIThread([&]() { + // After this the `3` pane is focused, go back to `2` being focused + page->_MoveFocus(FocusDirection::Right); + }); + Sleep(250); + TestOnUIThread([&]() { + // Split to create the final pane + // ------------------- + // | 1 | 2 | + // | | | + // ------------------- + // | 3 | 4 | + // | | | + // ------------------- + page->_SplitPane(SplitState::Horizontal, SplitType::Duplicate, 0.5f, nullptr); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + fourthId = tab->_activePane->Id().value(); + }); + + Sleep(250); + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // just to be complete, make sure we actually have 4 different ids + VERIFY_ARE_NOT_EQUAL(firstId, fourthId); + VERIFY_ARE_NOT_EQUAL(secondId, fourthId); + VERIFY_ARE_NOT_EQUAL(thirdId, fourthId); + VERIFY_ARE_NOT_EQUAL(firstId, thirdId); + VERIFY_ARE_NOT_EQUAL(secondId, thirdId); + VERIFY_ARE_NOT_EQUAL(firstId, secondId); + }); + + // Gratuitous use of sleep to make sure that the UI has updated properly + // after each operation. + Sleep(250); + // Now try to move the pane through the tree + Log::Comment(L"Move pane to the left. This should swap panes 3 and 4"); + // ------------------- + // | 1 | 2 | + // | | | + // ------------------- + // | 4 | 3 | + // | | | + // ------------------- + TestOnUIThread([&]() { + // Set up action + MovePaneArgs args{ FocusDirection::Left }; + ActionEventArgs eventArgs{ args }; + + page->_HandleMovePane(nullptr, eventArgs); + }); + + Sleep(250); + + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // Our currently focused pane should be `4` + VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value()); + + // Inspect the tree to make sure we swapped + VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_firstChild->_secondChild->Id().value()); + VERIFY_ARE_EQUAL(thirdId, tab->_rootPane->_secondChild->_secondChild->Id().value()); + }); + + Sleep(250); + + Log::Comment(L"Move pane to up. This should swap panes 1 and 4"); + // ------------------- + // | 4 | 2 | + // | | | + // ------------------- + // | 1 | 3 | + // | | | + // ------------------- + TestOnUIThread([&]() { + // Set up action + MovePaneArgs args{ FocusDirection::Up }; + ActionEventArgs eventArgs{ args }; + + page->_HandleMovePane(nullptr, eventArgs); + }); + + Sleep(250); + + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // Our currently focused pane should be `4` + VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value()); + + // Inspect the tree to make sure we swapped + VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_firstChild->_firstChild->Id().value()); + VERIFY_ARE_EQUAL(firstId, tab->_rootPane->_firstChild->_secondChild->Id().value()); + }); + + Sleep(250); + + Log::Comment(L"Move pane to the right. This should swap panes 2 and 4"); + // ------------------- + // | 2 | 4 | + // | | | + // ------------------- + // | 1 | 3 | + // | | | + // ------------------- + TestOnUIThread([&]() { + // Set up action + MovePaneArgs args{ FocusDirection::Right }; + ActionEventArgs eventArgs{ args }; + + page->_HandleMovePane(nullptr, eventArgs); + }); + + Sleep(250); + + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // Our currently focused pane should be `4` + VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value()); + + // Inspect the tree to make sure we swapped + VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_secondChild->_firstChild->Id().value()); + VERIFY_ARE_EQUAL(secondId, tab->_rootPane->_firstChild->_firstChild->Id().value()); + }); + + Sleep(250); + + Log::Comment(L"Move pane down. This should swap panes 3 and 4"); + // ------------------- + // | 2 | 3 | + // | | | + // ------------------- + // | 1 | 4 | + // | | | + // ------------------- + TestOnUIThread([&]() { + // Set up action + MovePaneArgs args{ FocusDirection::Down }; + ActionEventArgs eventArgs{ args }; + + page->_HandleMovePane(nullptr, eventArgs); + }); + + Sleep(250); + + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // Our currently focused pane should be `4` + VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value()); + + // Inspect the tree to make sure we swapped + VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_secondChild->_secondChild->Id().value()); + VERIFY_ARE_EQUAL(thirdId, tab->_rootPane->_secondChild->_firstChild->Id().value()); + }); + } + void TabTests::NextMRUTab() { // This is a test for GH#8025 - we want to make sure that we can do both diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index cdc92aaf6..5b045ec98 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -313,6 +313,24 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleMovePane(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + if (realArgs.Direction() == FocusDirection::None) + { + // Do nothing + args.Handled(false); + } + else + { + _MovePane(realArgs.Direction()); + args.Handled(true); + } + } + } + void TerminalPage::_HandleCopyText(const IInspectable& /*sender*/, const ActionEventArgs& args) { diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index f0545654c..c820fa09b 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -192,6 +192,7 @@ void AppCommandlineArgs::_buildParser() _buildSplitPaneParser(); _buildFocusTabParser(); _buildMoveFocusParser(); + _buildMovePaneParser(); _buildFocusPaneParser(); } @@ -398,6 +399,54 @@ void AppCommandlineArgs::_buildMoveFocusParser() setupSubcommand(_moveFocusShort); } +// Method Description: +// - Adds the `move-pane` subcommand and related options to the commandline parser. +// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane` +// Arguments: +// - +// Return Value: +// - +void AppCommandlineArgs::_buildMovePaneParser() +{ + _movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc")); + _movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc")); + + auto setupSubcommand = [this](auto* subcommand) { + std::map map = { + { "left", FocusDirection::Left }, + { "right", FocusDirection::Right }, + { "up", FocusDirection::Up }, + { "down", FocusDirection::Down } + }; + + auto* directionOpt = subcommand->add_option("direction", + _movePaneDirection, + RS_A(L"CmdMovePaneDirectionArgDesc")); + + directionOpt->transform(CLI::CheckedTransformer(map, CLI::ignore_case)); + directionOpt->required(); + // When ParseCommand is called, if this subcommand was provided, this + // callback function will be triggered on the same thread. We can be sure + // that `this` will still be safe - this function just lets us know this + // command was parsed. + subcommand->callback([&, this]() { + if (_movePaneDirection != FocusDirection::None) + { + MovePaneArgs args{ _movePaneDirection }; + + ActionAndArgs actionAndArgs{}; + actionAndArgs.Action(ShortcutAction::MovePane); + actionAndArgs.Args(args); + + _startupActions.push_back(std::move(actionAndArgs)); + } + }); + }; + + setupSubcommand(_movePaneCommand); + setupSubcommand(_movePaneShort); +} + // Method Description: // - Adds the `focus-pane` subcommand and related options to the commandline parser. // - Additionally adds the `fp` subcommand, which is just a shortened version of `focus-pane` @@ -574,6 +623,8 @@ bool AppCommandlineArgs::_noCommandsProvided() *_focusTabShort || *_moveFocusCommand || *_moveFocusShort || + *_movePaneCommand || + *_movePaneShort || *_focusPaneCommand || *_focusPaneShort || *_newPaneShort.subcommand || @@ -607,6 +658,7 @@ void AppCommandlineArgs::_resetStateToDefault() _focusPrevTab = false; _moveFocusDirection = FocusDirection::None; + _movePaneDirection = FocusDirection::None; _focusPaneTarget = -1; diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 17593c156..81dc564ba 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -82,6 +82,8 @@ private: CLI::App* _focusTabShort; CLI::App* _moveFocusCommand; CLI::App* _moveFocusShort; + CLI::App* _movePaneCommand; + CLI::App* _movePaneShort; CLI::App* _focusPaneCommand; CLI::App* _focusPaneShort; @@ -95,6 +97,7 @@ private: bool _suppressApplicationTitle{ false }; winrt::Microsoft::Terminal::Settings::Model::FocusDirection _moveFocusDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; + winrt::Microsoft::Terminal::Settings::Model::FocusDirection _movePaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; // _commandline will contain the command line with which we'll be spawning a new terminal std::vector _commandline; @@ -128,6 +131,7 @@ private: void _buildSplitPaneParser(); void _buildFocusTabParser(); void _buildMoveFocusParser(); + void _buildMovePaneParser(); void _buildFocusPaneParser(); bool _noCommandsProvided(); void _resetStateToDefault(); diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 2dda1a442..1ffbaa787 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -302,6 +302,382 @@ bool Pane::NavigateFocus(const FocusDirection& direction) return false; } +// Method Description: +// - Attempts to find the parent pane of the provided pane. +// Arguments: +// - pane: The pane to search for. +// Return Value: +// - the parent of `pane` if pane is in this tree. +std::shared_ptr Pane::_FindParentOfPane(const std::shared_ptr pane) +{ + if (_IsLeaf()) + { + return nullptr; + } + + if (_firstChild == pane || _secondChild == pane) + { + return shared_from_this(); + } + + if (auto p = _firstChild->_FindParentOfPane(pane)) + { + return p; + } + + return _secondChild->_FindParentOfPane(pane); +} + +// Method Description: +// - Attempts to swap the location of the two given panes in the tree. +// Searches the tree starting at this pane to find the parent pane for each of +// the arguments, and if both parents are found, replaces the appropriate +// child in each. +// Arguments: +// - first: A pointer to the first pane to switch. +// - second: A pointer to the second pane to switch. +// Return Value: +// - true if a swap was performed. +bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) +{ + // If there is nothing to swap, just return. + if (first == second || _IsLeaf()) + { + return false; + } + + std::unique_lock lock{ _createCloseLock }; + + // Recurse through the tree to find the parent panes of each pane that is + // being swapped. + std::shared_ptr firstParent = _FindParentOfPane(first); + std::shared_ptr secondParent = _FindParentOfPane(second); + + // We should have found either no elements, or both elements. + // If we only found one parent then the pane SwapPane was called on did not + // contain both panes as leaves, as could happen if the tree was modified + // 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); + + // Replace the old child with new one, and revoke appropriate event + // handlers. + auto replaceChild = [](auto& parent, auto oldChild, auto newChild) { + // Revoke the old handlers + if (parent->_firstChild == oldChild) + { + parent->_firstChild->Closed(parent->_firstClosedToken); + parent->_firstChild = newChild; + } + else if (parent->_secondChild == oldChild) + { + parent->_secondChild->Closed(parent->_secondClosedToken); + parent->_secondChild = newChild; + } + // Clear now to ensure that we can add the child's grid to us later + parent->_root.Children().Clear(); + }; + + // 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) { + 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(); + }; + + // If the firstParent and secondParent are the same, then we are just + // swapping the first child and second child of that parent. + if (firstParent == secondParent) + { + firstParent->_firstChild->Closed(firstParent->_firstClosedToken); + firstParent->_secondChild->Closed(firstParent->_secondClosedToken); + std::swap(firstParent->_firstChild, firstParent->_secondChild); + + updateParent(firstParent); + } + else + { + // Replace both children before updating display to ensure + // that the grid elements are not attached to multiple panes + replaceChild(firstParent, first, second); + replaceChild(secondParent, second, first); + updateParent(firstParent); + updateParent(secondParent); + } + + return true; + } + + return false; +} + +// Method Description: +// - Given two panes, test whether the `direction` side of first is adjacent to second. +// Arguments: +// - first: The reference pane. +// - second: the pane to test adjacency with. +// - direction: The direction to search in from the reference pane. +// Return Value: +// - true if the two panes are adjacent. +bool Pane::_IsAdjacent(const std::shared_ptr first, + const Pane::PanePoint firstOffset, + const std::shared_ptr second, + const Pane::PanePoint secondOffset, + const FocusDirection& direction) const +{ + // Since float equality is tricky (arithmetic is non-associative, commutative), + // test if the two numbers are within an epsilon distance of each other. + auto floatEqual = [](float left, float right) { + return abs(left - right) < 1e-4F; + }; + + // When checking containment in a range, the range is half-closed, i.e. [x, x+w). + // If the direction is left test that the left side of the first element is + // next to the right side of the second element, and that the top left + // corner of the first element is within the second element's height + if (direction == FocusDirection::Left) + { + auto sharesBorders = floatEqual(firstOffset.x, secondOffset.x + gsl::narrow_cast(second->GetRootElement().ActualWidth())); + auto withinHeight = (firstOffset.y >= secondOffset.y) && (firstOffset.y < secondOffset.y + gsl::narrow_cast(second->GetRootElement().ActualHeight())); + + return sharesBorders && withinHeight; + } + // If the direction is right test that the right side of the first element is + // next to the left side of the second element, and that the top left + // corner of the first element is within the second element's height + else if (direction == FocusDirection::Right) + { + auto sharesBorders = floatEqual(firstOffset.x + gsl::narrow_cast(first->GetRootElement().ActualWidth()), secondOffset.x); + auto withinHeight = (firstOffset.y >= secondOffset.y) && (firstOffset.y < secondOffset.y + gsl::narrow_cast(second->GetRootElement().ActualHeight())); + + return sharesBorders && withinHeight; + } + // If the direction is up test that the top side of the first element is + // next to the bottom side of the second element, and that the top left + // corner of the first element is within the second element's width + else if (direction == FocusDirection::Up) + { + auto sharesBorders = floatEqual(firstOffset.y, secondOffset.y + gsl::narrow_cast(second->GetRootElement().ActualHeight())); + auto withinWidth = (firstOffset.x >= secondOffset.x) && (firstOffset.x < secondOffset.x + gsl::narrow_cast(second->GetRootElement().ActualWidth())); + + return sharesBorders && withinWidth; + } + // If the direction is down test that the bottom side of the first element is + // next to the top side of the second element, and that the top left + // corner of the first element is within the second element's width + else if (direction == FocusDirection::Down) + { + auto sharesBorders = floatEqual(firstOffset.y + gsl::narrow_cast(first->GetRootElement().ActualHeight()), secondOffset.y); + auto withinWidth = (firstOffset.x >= secondOffset.x) && (firstOffset.x < secondOffset.x + gsl::narrow_cast(second->GetRootElement().ActualWidth())); + + return sharesBorders && withinWidth; + } + return false; +} + +// Method Description: +// - Given the focused pane, and its relative position in the tree, attempt to +// find its visual neighbor within the current pane's tree. +// The neighbor, if it exists, will be a leaf pane. +// Arguments: +// - direction: The direction to search in from the focused pane. +// - focus: the focused pane +// - focusIsSecondSide: If the focused pane is on the "second" side (down/right of split) +// relative to the branch being searched +// - offset: the offset of the current pane +// Return Value: +// - A tuple of Panes, the first being the focused pane if found, and the second +// being the adjacent pane if it exists, and a bool that represents if the move +// goes out of bounds. +Pane::FocusNeighborSearch Pane::_FindNeighborForPane(const FocusDirection& direction, + FocusNeighborSearch searchResult, + const bool focusIsSecondSide, + const Pane::PanePoint offset) +{ + // Test if the move will go out of boundaries. E.g. if the focus is already + // on the second child of some pane and it attempts to move right, there + // can't possibly be a neighbor to be found in the first child. + if ((focusIsSecondSide && (direction == FocusDirection::Right || direction == FocusDirection::Down)) || + (!focusIsSecondSide && (direction == FocusDirection::Left || direction == FocusDirection::Up))) + { + return searchResult; + } + + // If we are a leaf node test if we adjacent to the focus node + if (_IsLeaf()) + { + if (_IsAdjacent(searchResult.focus, searchResult.focusOffset, shared_from_this(), offset, direction)) + { + searchResult.neighbor = shared_from_this(); + } + return searchResult; + } + + auto firstOffset = offset; + auto secondOffset = offset; + // The second child has an offset depending on the split + if (_splitState == SplitState::Horizontal) + { + secondOffset.y += gsl::narrow_cast(_firstChild->GetRootElement().ActualHeight()); + } + else + { + secondOffset.x += gsl::narrow_cast(_firstChild->GetRootElement().ActualWidth()); + } + auto focusNeighborSearch = _firstChild->_FindNeighborForPane(direction, searchResult, focusIsSecondSide, firstOffset); + if (focusNeighborSearch.neighbor) + { + return focusNeighborSearch; + } + + return _secondChild->_FindNeighborForPane(direction, searchResult, focusIsSecondSide, secondOffset); +} + +// Method Description: +// - Searches the tree to find the currently focused pane, and if it exists, the +// visually adjacent pane by direction. +// Arguments: +// - direction: The direction to search in from the focused pane. +// - offset: The offset, with the top-left corner being (0,0), that the current pane is relative to the root. +// Return Value: +// - The (partial) search result. If the search was successful, the focus and its neighbor will be returned. +// Otherwise, the neighbor will be null and the focus will be null/non-null if it was found. +Pane::FocusNeighborSearch Pane::_FindFocusAndNeighbor(const FocusDirection& direction, const Pane::PanePoint offset) +{ + // If we are the currently focused pane, return ourselves + if (_IsLeaf()) + { + return { _lastActive ? shared_from_this() : nullptr, nullptr, offset }; + } + + // Search the first child, which has no offset from the parent pane + auto firstOffset = offset; + auto secondOffset = offset; + // The second child has an offset depending on the split + if (_splitState == SplitState::Horizontal) + { + secondOffset.y += gsl::narrow_cast(_firstChild->GetRootElement().ActualHeight()); + } + else + { + secondOffset.x += gsl::narrow_cast(_firstChild->GetRootElement().ActualWidth()); + } + + auto focusNeighborSearch = _firstChild->_FindFocusAndNeighbor(direction, firstOffset); + // If we have both the focus element and its neighbor, we are done + if (focusNeighborSearch.focus && focusNeighborSearch.neighbor) + { + return focusNeighborSearch; + } + // if we only found the focus, then we search the second branch for the + // neighbor. + if (focusNeighborSearch.focus) + { + // If we can possibly have both sides of a direction, check if the sibling has the neighbor + if (DirectionMatchesSplit(direction, _splitState)) + { + return _secondChild->_FindNeighborForPane(direction, focusNeighborSearch, false, secondOffset); + } + return focusNeighborSearch; + } + + // If we didn't find the focus at all, we need to search the second branch + // for the focus (and possibly its neighbor). + focusNeighborSearch = _secondChild->_FindFocusAndNeighbor(direction, secondOffset); + // We found both so we are done. + if (focusNeighborSearch.focus && focusNeighborSearch.neighbor) + { + return focusNeighborSearch; + } + // We only found the focus, which means that its neighbor might be in the + // first branch. + if (focusNeighborSearch.focus) + { + // If we can possibly have both sides of a direction, check if the sibling has the neighbor + if (DirectionMatchesSplit(direction, _splitState)) + { + return _firstChild->_FindNeighborForPane(direction, focusNeighborSearch, true, firstOffset); + } + return focusNeighborSearch; + } + + return { nullptr, nullptr, offset }; +} + +// Method Description: +// - Attempts to swap places of the focused pane with one of our children. This +// will swap with the visually adjacent leaf pane if one exists in the +// direction requested, maintaining the existing tree structure. +// This breaks down into a few possible cases +// - If the move direction would encounter the edge of the pane, no move occurs +// - If the focused pane has a single neighbor according to the direction, +// then it will swap with it. +// - If the focused pane has multiple neighbors, it will swap with the +// first-most leaf of the neighboring panes. +// Arguments: +// - direction: The direction to move the focused pane in. +// Return Value: +// - true if we or a child handled this pane move request. +bool Pane::MovePane(const FocusDirection& direction) +{ + // If we're a leaf, do nothing. We can't possibly swap anything. + if (_IsLeaf()) + { + return false; + } + + // If we get a request to move to the previous pane return false because + // that needs to be handled at the tab level. + if (direction == FocusDirection::Previous) + { + return false; + } + + // If the move direction does not match the split direction, the focused pane + // and its neighbor must necessarily be contained within the same child. + if (!DirectionMatchesSplit(direction, _splitState)) + { + return _firstChild->MovePane(direction) || _secondChild->MovePane(direction); + } + + // Since the direction is the same as our split, it is possible that we must + // swap a pane from one child to the other child. + // We now must keep track of state while we recurse. + auto focusNeighborPair = _FindFocusAndNeighbor(direction, { 0, 0 }); + + // Once we have found the focused pane and its neighbor, wherever they may + // be, we can swap them. + if (focusNeighborPair.focus && focusNeighborPair.neighbor) + { + auto swapped = SwapPanes(focusNeighborPair.focus, focusNeighborPair.neighbor); + focusNeighborPair.focus->_FocusFirstChild(); + return swapped; + } + + return false; +} + // Method Description: // - Called when our attached control is closed. Triggers listeners to our close // event, if we're a leaf pane. @@ -1625,6 +2001,36 @@ bool Pane::FocusPane(const uint32_t id) return false; } +// Method Description: +// - Recursive function that finds a pane with the given ID +// Arguments: +// - The ID of the pane we want to find +// Return Value: +// - A pointer to the pane with the given ID, if found. +std::shared_ptr Pane::FindPane(const uint32_t id) +{ + if (_IsLeaf()) + { + if (id == _id) + { + return shared_from_this(); + } + } + else + { + if (auto pane = _firstChild->FindPane(id)) + { + return pane; + } + if (auto pane = _secondChild->FindPane(id)) + { + return pane; + } + } + + return nullptr; +} + // Method Description: // - Gets the size in pixels of each of our children, given the full size they // should fill. Since these children own their own separators (borders), this diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 6e7f801b7..85c166ded 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -22,6 +22,12 @@ #include "../../cascadia/inc/cppwinrt_utils.h" +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class TabTests; +}; + enum class Borders : int { None = 0x0, @@ -56,6 +62,8 @@ public: void Relayout(); bool ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool MovePane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool SwapPanes(std::shared_ptr first, std::shared_ptr second); std::pair, std::shared_ptr> Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, const float splitSize, @@ -79,6 +87,7 @@ public: std::optional Id() noexcept; void Id(uint32_t id) noexcept; bool FocusPane(const uint32_t id); + std::shared_ptr FindPane(const uint32_t id); bool ContainsReadOnly() const; @@ -88,6 +97,8 @@ public: DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); private: + struct PanePoint; + struct FocusNeighborSearch; struct SnapSizeResult; struct SnapChildrenSizeResult; struct LayoutSizeNode; @@ -139,6 +150,15 @@ private: bool _Resize(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); bool _NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + std::shared_ptr _FindParentOfPane(const std::shared_ptr pane); + bool _IsAdjacent(const std::shared_ptr first, const PanePoint firstOffset, const std::shared_ptr second, const PanePoint secondOffset, const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction) const; + FocusNeighborSearch _FindNeighborForPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction, + FocusNeighborSearch searchResult, + const bool focusIsSecondSide, + const PanePoint offset); + FocusNeighborSearch _FindFocusAndNeighbor(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction, + const PanePoint offset); + void _CloseChild(const bool closeFirst); winrt::fire_and_forget _CloseChildRoutine(const bool closeFirst); @@ -200,6 +220,19 @@ private: static void _SetupResources(); + struct PanePoint + { + float x; + float y; + }; + + struct FocusNeighborSearch + { + std::shared_ptr focus; + std::shared_ptr neighbor; + PanePoint focusOffset; + }; + struct SnapSizeResult { float lower; @@ -236,4 +269,6 @@ private: private: void _AssignChildNode(std::unique_ptr& nodeField, const LayoutSizeNode* const newNode); }; + + friend class ::TerminalAppLocalTests::TabTests; }; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index d891839fa..f2de64480 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -359,6 +359,16 @@ The direction to move focus in + + Swap the focused pane with the adjacent pane in the specified direction + + + An alias for the "move-pane" subcommand. + {Locked="\"move-pane\""} + + + The direction to move the focused pane in + Launch the window in focus mode diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 7d22fff52..2fed60f5e 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1133,6 +1133,22 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - Attempt to swap the positions of the focused pane with another pane. + // See Pane::MovePane for details. + // Arguments: + // - direction: The direction to move the focused pane in. + // Return Value: + // - + void TerminalPage::_MovePane(const FocusDirection& direction) + { + if (const auto terminalTab{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + terminalTab->MovePane(direction); + } + } + TermControl TerminalPage::_GetActiveControl() { if (const auto terminalTab{ _GetFocusedTabImpl() }) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 6d7370b9a..26d43e8f6 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -235,6 +235,7 @@ namespace winrt::TerminalApp::implementation void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); bool _SelectTab(const uint32_t tabIndex); void _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + void _MovePane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); std::optional _GetFocusedTabIndex() const noexcept; diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 4a0983940..559fb959e 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -486,6 +486,10 @@ namespace winrt::TerminalApp::implementation { if (direction == FocusDirection::Previous) { + if (_mruPanes.size() < 2) + { + return; + } // To get to the previous pane, get the id of the previous pane and focus to that _rootPane->FocusPane(_mruPanes.at(1)); } @@ -497,6 +501,35 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - Attempts to swap the location of the focused pane with another pane + // according to direction. When there are multiple adjacent panes it will + // select the first one (top-left-most). + // Arguments: + // - direction: The direction to move the pane in. + // Return Value: + // - + void TerminalTab::MovePane(const FocusDirection& direction) + { + if (direction == FocusDirection::Previous) + { + if (_mruPanes.size() < 2) + { + return; + } + if (auto lastPane = _rootPane->FindPane(_mruPanes.at(1))) + { + _rootPane->SwapPanes(_activePane, lastPane); + } + } + else + { + // NOTE: This _must_ be called on the root pane, so that it can propagate + // throughout the entire tree. + _rootPane->MovePane(direction); + } + } + bool TerminalTab::FocusPane(const uint32_t id) { return _rootPane->FocusPane(id); diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 7167f74a6..dfc9ff676 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -53,6 +53,7 @@ namespace winrt::TerminalApp::implementation void ResizeContent(const winrt::Windows::Foundation::Size& newSize); void ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); void NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + void MovePane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool FocusPane(const uint32_t id); void UpdateSettings(const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, const GUID& profile); diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 430980a6c..f6c397605 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -20,6 +20,7 @@ static constexpr std::string_view DuplicateTabKey{ "duplicateTab" }; static constexpr std::string_view ExecuteCommandlineKey{ "wt" }; static constexpr std::string_view FindKey{ "find" }; static constexpr std::string_view MoveFocusKey{ "moveFocus" }; +static constexpr std::string_view MovePaneKey{ "movePane" }; static constexpr std::string_view NewTabKey{ "newTab" }; static constexpr std::string_view NextTabKey{ "nextTab" }; static constexpr std::string_view OpenNewTabDropdownKey{ "openNewTabDropdown" }; @@ -319,6 +320,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::Find, RS_(L"FindCommandKey") }, { ShortcutAction::Invalid, L"" }, { ShortcutAction::MoveFocus, RS_(L"MoveFocusCommandKey") }, + { ShortcutAction::MovePane, RS_(L"MovePaneCommandKey") }, { ShortcutAction::NewTab, RS_(L"NewTabCommandKey") }, { ShortcutAction::NextTab, RS_(L"NextTabCommandKey") }, { ShortcutAction::OpenNewTabDropdown, RS_(L"OpenNewTabDropdownCommandKey") }, diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 6f253973b..d38772d0b 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -12,6 +12,7 @@ #include "SwitchToTabArgs.g.cpp" #include "ResizePaneArgs.g.cpp" #include "MoveFocusArgs.g.cpp" +#include "MovePaneArgs.g.cpp" #include "AdjustFontSizeArgs.g.cpp" #include "SendInputArgs.g.cpp" #include "SplitPaneArgs.g.cpp" @@ -282,6 +283,32 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation }; } + winrt::hstring MovePaneArgs::GenerateName() const + { + winrt::hstring directionString; + switch (Direction()) + { + case FocusDirection::Left: + directionString = RS_(L"DirectionLeft"); + break; + case FocusDirection::Right: + directionString = RS_(L"DirectionRight"); + break; + case FocusDirection::Up: + directionString = RS_(L"DirectionUp"); + break; + case FocusDirection::Down: + directionString = RS_(L"DirectionDown"); + break; + case FocusDirection::Previous: + return RS_(L"MovePaneToLastUsedPane"); + } + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"MovePaneWithArgCommandKey")), + directionString) + }; + } + winrt::hstring AdjustFontSizeArgs::GenerateName() const { // If the amount is just 1 (or -1), we'll just return "Increase font diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index e3ce97e40..4861ea805 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -12,6 +12,7 @@ #include "SwitchToTabArgs.g.h" #include "ResizePaneArgs.g.h" #include "MoveFocusArgs.g.h" +#include "MovePaneArgs.g.h" #include "AdjustFontSizeArgs.g.h" #include "SendInputArgs.g.h" #include "SplitPaneArgs.g.h" @@ -451,6 +452,65 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } }; + struct MovePaneArgs : public MovePaneArgsT + { + MovePaneArgs() = default; + MovePaneArgs(Model::FocusDirection direction) : + _Direction{ direction } {}; + + ACTION_ARG(Model::FocusDirection, Direction, FocusDirection::None); + + static constexpr std::string_view DirectionKey{ "direction" }; + + public: + hstring GenerateName() const; + + bool Equals(const IActionArgs& other) + { + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_Direction == _Direction; + } + return false; + }; + static FromJsonResult FromJson(const Json::Value& json) + { + // LOAD BEARING: Not using make_self here _will_ break you in the future! + auto args = winrt::make_self(); + JsonUtils::GetValueForKey(json, DirectionKey, args->_Direction); + if (args->Direction() == FocusDirection::None) + { + return { nullptr, { SettingsLoadWarnings::MissingRequiredParameter } }; + } + else + { + return { *args, {} }; + } + } + static Json::Value ToJson(const IActionArgs& val) + { + if (!val) + { + return {}; + } + Json::Value json{ Json::ValueType::objectValue }; + const auto args{ get_self(val) }; + JsonUtils::SetValueForKey(json, DirectionKey, args->_Direction); + return json; + } + IActionArgs Copy() const + { + auto copy{ winrt::make_self() }; + copy->_Direction = _Direction; + return *copy; + } + size_t Hash() const + { + return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(Direction()); + } + }; + struct AdjustFontSizeArgs : public AdjustFontSizeArgsT { AdjustFontSizeArgs() = default; @@ -1647,6 +1707,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(NewTerminalArgs); BASIC_FACTORY(NewTabArgs); BASIC_FACTORY(MoveFocusArgs); + BASIC_FACTORY(MovePaneArgs); BASIC_FACTORY(SplitPaneArgs); BASIC_FACTORY(SetColorSchemeArgs); BASIC_FACTORY(ExecuteCommandlineArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 5279c557a..9e418a335 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -158,6 +158,12 @@ namespace Microsoft.Terminal.Settings.Model FocusDirection FocusDirection { get; }; }; + [default_interface] runtimeclass MovePaneArgs : IActionArgs + { + MovePaneArgs(FocusDirection direction); + FocusDirection Direction { get; }; + }; + [default_interface] runtimeclass AdjustFontSizeArgs : IActionArgs { Int32 Delta { get; }; diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index cc3559037..ca795bf6c 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -48,6 +48,7 @@ ON_ALL_ACTIONS(ScrollToBottom) \ ON_ALL_ACTIONS(ResizePane) \ ON_ALL_ACTIONS(MoveFocus) \ + ON_ALL_ACTIONS(MovePane) \ ON_ALL_ACTIONS(Find) \ ON_ALL_ACTIONS(ToggleShaderEffects) \ ON_ALL_ACTIONS(ToggleFocusMode) \ @@ -87,6 +88,7 @@ ON_ALL_ACTIONS_WITH_ARGS(FindMatch) \ ON_ALL_ACTIONS_WITH_ARGS(GlobalSummon) \ ON_ALL_ACTIONS_WITH_ARGS(MoveFocus) \ + ON_ALL_ACTIONS_WITH_ARGS(MovePane) \ ON_ALL_ACTIONS_WITH_ARGS(MoveTab) \ ON_ALL_ACTIONS_WITH_ARGS(NewTab) \ ON_ALL_ACTIONS_WITH_ARGS(NewWindow) \ diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index d44ecc5dd..fb1c79cb9 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -246,6 +246,16 @@ Move focus to the last used pane + + Move pane + + + Move pane {0} + {0} will be replaced with one of the four directions "DirectionLeft", "DirectionRight", "DirectionUp", "DirectionDown" + + + Move pane to the last used pane + New tab @@ -417,4 +427,4 @@ Windows Console Host Name describing the usage of the classic windows console as the terminal UI. (`conhost.exe`) - \ No newline at end of file + diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 0003e0b72..acb276401 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -343,7 +343,12 @@ { "command": { "action": "moveFocus", "direction": "left" }, "keys": "alt+left" }, { "command": { "action": "moveFocus", "direction": "right" }, "keys": "alt+right" }, { "command": { "action": "moveFocus", "direction": "up" }, "keys": "alt+up" }, - { "command": { "action": "moveFocus", "direction": "previous" }, "keys": "ctrl+alt+left" }, + { "command": { "action": "moveFocus", "direction": "previous" }, "keys": "ctrl+alt+left"}, + { "command": { "action": "movePane", "direction": "down" } }, + { "command": { "action": "movePane", "direction": "left" } }, + { "command": { "action": "movePane", "direction": "right" } }, + { "command": { "action": "movePane", "direction": "up" } }, + { "command": { "action": "movePane", "direction": "previous"} }, { "command": "togglePaneZoom" }, { "command": "toggleReadOnlyMode" }, From 335f69e099179d533ffa3a1376035f1a63637de0 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 22 Jul 2021 08:48:36 -0500 Subject: [PATCH 20/90] Clamp the `focusTab` action to the number of available tabs (#10651) ## Summary of the Pull Request When we perform a `focusTab` action, we currently do nothing if the parameter was greater than the number of tabs. This PR changes that behavior. Now, `focus-tab -t 999999` will always focus the last tab, instead of silently doing nothing. ## PR Checklist * [x] Closes #9369 * [x] I work here * [x] Tests added/passed * [n/a] Requires documentation to be updated ## Validation Steps Performed * [x] ran tests * [x] validated commandline manually --- .../LocalTests_TerminalApp/TabTests.cpp | 53 +++++++++++++++++++ src/cascadia/TerminalApp/TabManagement.cpp | 31 +++++------ src/cascadia/TerminalApp/TerminalPage.h | 2 +- .../TerminalSettingsModel/ActionArgs.cpp | 5 ++ .../Resources/en-US/Resources.resw | 3 ++ .../TerminalSettingsModel/defaults.json | 2 +- 6 files changed, 79 insertions(+), 17 deletions(-) diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 078df3c29..9d7653dc8 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -95,6 +95,8 @@ namespace TerminalAppLocalTests TEST_METHOD(TestPreviewDismissScheme); TEST_METHOD(TestPreviewSchemeWhilePreviewing); + TEST_METHOD(TestClampSwitchToTab); + TEST_CLASS_SETUP(ClassSetup) { return true; @@ -1550,4 +1552,55 @@ namespace TerminalAppLocalTests }); } + void TabTests::TestClampSwitchToTab() + { + Log::Comment(L"Test that switching to a tab index higher than the number of tabs just clamps to the last tab."); + + auto page = _commonSetup(); + VERIFY_IS_NOT_NULL(page); + + Log::Comment(L"Create a second tab"); + TestOnUIThread([&page]() { + NewTerminalArgs newTerminalArgs{ 1 }; + page->_OpenNewTab(newTerminalArgs); + }); + VERIFY_ARE_EQUAL(2u, page->_tabs.Size()); + + Log::Comment(L"Create a third tab"); + TestOnUIThread([&page]() { + NewTerminalArgs newTerminalArgs{ 2 }; + page->_OpenNewTab(newTerminalArgs); + }); + VERIFY_ARE_EQUAL(3u, page->_tabs.Size()); + + TestOnUIThread([&page]() { + auto focusedTabIndexOpt{ page->_GetFocusedTabIndex() }; + VERIFY_IS_TRUE(focusedTabIndexOpt.has_value()); + VERIFY_ARE_EQUAL(2u, focusedTabIndexOpt.value()); + }); + + TestOnUIThread([&page]() { + Log::Comment(L"Switch to the first tab"); + page->_SelectTab(0); + }); + + TestOnUIThread([&page]() { + auto focusedTabIndexOpt{ page->_GetFocusedTabIndex() }; + + VERIFY_IS_TRUE(focusedTabIndexOpt.has_value()); + VERIFY_ARE_EQUAL(0u, focusedTabIndexOpt.value()); + }); + + TestOnUIThread([&page]() { + Log::Comment(L"Switch to the tab 6, which is greater than number of tabs. This should switch to the third tab"); + page->_SelectTab(6); + }); + + TestOnUIThread([&page]() { + auto focusedTabIndexOpt{ page->_GetFocusedTabIndex() }; + VERIFY_IS_TRUE(focusedTabIndexOpt.has_value()); + VERIFY_ARE_EQUAL(2u, focusedTabIndexOpt.value()); + }); + } + } diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 718d8d6e2..1c753aa43 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -497,24 +497,25 @@ namespace winrt::TerminalApp::implementation // TerminalPage::_OnTabSelectionChanged // Return Value: // true iff we were able to select that tab index, false otherwise - bool TerminalPage::_SelectTab(const uint32_t tabIndex) + bool TerminalPage::_SelectTab(uint32_t tabIndex) { - if (tabIndex >= 0 && tabIndex < _tabs.Size()) - { - auto tab{ _tabs.GetAt(tabIndex) }; - if (_startupState == StartupState::InStartup) - { - _tabView.SelectedItem(tab.TabViewItem()); - _UpdatedSelectedTab(tab); - } - else - { - _SetFocusedTab(tab); - } + // GH#9369 - if the argument is out of range, then clamp to the number + // of available tabs. Previously, we'd just silently do nothing if the + // value was greater than the number of tabs. + tabIndex = std::clamp(tabIndex, 0u, _tabs.Size() - 1); - return true; + auto tab{ _tabs.GetAt(tabIndex) }; + if (_startupState == StartupState::InStartup) + { + _tabView.SelectedItem(tab.TabViewItem()); + _UpdatedSelectedTab(tab); } - return false; + else + { + _SetFocusedTab(tab); + } + + return true; } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 26d43e8f6..c93835054 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -233,7 +233,7 @@ namespace winrt::TerminalApp::implementation void _ResizeTabContent(const winrt::Windows::Foundation::Size& newSize); void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); - bool _SelectTab(const uint32_t tabIndex); + bool _SelectTab(uint32_t tabIndex); void _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); void _MovePane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index d38772d0b..7bc057742 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -228,6 +228,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::hstring SwitchToTabArgs::GenerateName() const { + if (TabIndex() == UINT32_MAX) + { + return RS_(L"SwitchToLastTabCommandKey"); + } + return winrt::hstring{ fmt::format(L"{}, index:{}", RS_(L"SwitchToTabCommandKey"), TabIndex()) }; diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index fb1c79cb9..99af72c23 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -365,6 +365,9 @@ Switch to tab + + Switch to the last tab + Search for tab... diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index acb276401..1cc6b3162 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -329,7 +329,7 @@ { "command": { "action": "switchToTab", "index": 5 }, "keys": "ctrl+alt+6" }, { "command": { "action": "switchToTab", "index": 6 }, "keys": "ctrl+alt+7" }, { "command": { "action": "switchToTab", "index": 7 }, "keys": "ctrl+alt+8" }, - { "command": { "action": "switchToTab", "index": 8 }, "keys": "ctrl+alt+9" }, + { "command": { "action": "switchToTab", "index": 4294967295 }, "keys": "ctrl+alt+9" }, // Pane Management { "command": "closePane", "keys": "ctrl+shift+w" }, From 4c16cb278ed8b7ba46ff6fd4cf4c48fb095cb1a7 Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Thu, 22 Jul 2021 16:15:44 -0700 Subject: [PATCH 21/90] Allow users to set font features and font axes (#10525) Adds support for users to be able to set font features and axes (see the spec for more details!) ## Detailed Description **CustomTextLayout** - Asks the `DxFontRenderData` for the font features when getting glyphs - _If any features have been set/updated, we always skip the "isTextSimple" shortcut_ - Asks the `_formatInUse` for any font axes when mapping characters in `_AnalyzeFontFallback` **DxFontRenderData** - Stores a map of font features (initialized to the [standard feature list]) - Stores a map of font axes - Has methods to add font features/axes to the map or update existing ones - Has methods to retrieve the font features/axes - Sets the font axes in the `IDWriteTextFormat` when creating it ## Validation Steps Performed It works! [standard feature list]: https://github.com/fdwr/TextLayoutSampler/blob/ac5aef67d1cc0cb67c5e3be29b30bda5a90c3e2b/DrawableObject.ixx#L802 Specified in #10457 Related to #1790 Closes #759 Closes #5828 --- .github/actions/spelling/allow/allow.txt | 8 + src/cascadia/TerminalControl/ControlCore.cpp | 57 +++-- .../TerminalControl/IControlSettings.idl | 2 + .../CascadiaSettings.cpp | 2 + .../TerminalSettingsModel/FontConfig.cpp | 8 + .../TerminalSettingsModel/FontConfig.h | 5 + .../TerminalSettingsModel/FontConfig.idl | 5 + .../TerminalSettingsModel/JsonUtils.h | 104 +++++++++ .../TerminalSettings.cpp | 2 + .../TerminalSettingsModel/TerminalSettings.h | 5 + .../UnitTests_Control/MockControlSettings.h | 6 + src/renderer/dx/CustomTextLayout.cpp | 121 ++++++---- src/renderer/dx/CustomTextLayout.h | 2 +- src/renderer/dx/DxFontRenderData.cpp | 209 +++++++++++++++++- src/renderer/dx/DxFontRenderData.h | 33 ++- src/renderer/dx/DxRenderer.cpp | 17 +- src/renderer/dx/DxRenderer.hpp | 1 + 17 files changed, 529 insertions(+), 58 deletions(-) diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index d361ba3e4..4ca9009f8 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -1,6 +1,9 @@ apc +calt +ccmp Apc clickable +clig copyable dalet dcs @@ -14,6 +17,7 @@ dzhe Enum'd formattings ftp +fvar geeksforgeeks ghe gje @@ -27,7 +31,9 @@ It'd kje liga lje +locl maxed +mkmk mru nje ogonek @@ -37,10 +43,12 @@ postmodern ptys qof qps +rclt reimplementation reserialization reserialize reserializes +rlig runtimes shcha slnt diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 27724fa7c..bf106322a 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -155,6 +155,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Set up the DX Engine auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); _renderer->AddRenderEngine(dxEngine.get()); + _renderEngine = std::move(dxEngine); // Initialize our font with the renderer // We don't have to care about DPI. We'll get a change message immediately if it's not 96 @@ -168,12 +169,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Then, using the font, get the number of characters that can fit. // Resize our terminal connection to match that size, and initialize the terminal with that size. const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize); - LOG_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); + LOG_IF_FAILED(_renderEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); // Update DxEngine's SelectionBackground - dxEngine->SetSelectionBackground(til::color{ _settings.SelectionBackground() }); + _renderEngine->SetSelectionBackground(til::color{ _settings.SelectionBackground() }); - const auto vp = dxEngine->GetViewportInCharacters(viewInPixels); + const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels); const auto width = vp.Width(); const auto height = vp.Height(); _connection.Resize(height, width); @@ -188,27 +189,26 @@ namespace winrt::Microsoft::Terminal::Control::implementation // after Enable, then it'll be possible to paint the frame once // _before_ the warning handler is set up, and then warnings from // the first paint will be ignored! - dxEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1)); + _renderEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1)); // Tell the DX Engine to notify us when the swap chain changes. // We do this after we initially set the swapchain so as to avoid unnecessary callbacks (and locking problems) - dxEngine->SetCallback(std::bind(&ControlCore::_renderEngineSwapChainChanged, this)); + _renderEngine->SetCallback(std::bind(&ControlCore::_renderEngineSwapChainChanged, this)); - dxEngine->SetRetroTerminalEffect(_settings.RetroTerminalEffect()); - dxEngine->SetPixelShaderPath(_settings.PixelShaderPath()); - dxEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); - dxEngine->SetSoftwareRendering(_settings.SoftwareRendering()); + _renderEngine->SetRetroTerminalEffect(_settings.RetroTerminalEffect()); + _renderEngine->SetPixelShaderPath(_settings.PixelShaderPath()); + _renderEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); + _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering()); - _updateAntiAliasingMode(dxEngine.get()); + _updateAntiAliasingMode(_renderEngine.get()); // GH#5098: Inform the engine of the opacity of the default text background. if (_settings.UseAcrylic()) { - dxEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); + _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); } - THROW_IF_FAILED(dxEngine->Enable()); - _renderEngine = std::move(dxEngine); + THROW_IF_FAILED(_renderEngine->Enable()); _initializedTerminal = true; } // scope for TerminalLock @@ -603,9 +603,34 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetFontInfo(_actualFont); - // TODO: MSFT:20895307 If the font doesn't exist, this doesn't - // actually fail. We need a way to gracefully fallback. - _renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont); + if (_renderEngine) + { + std::unordered_map featureMap; + if (const auto fontFeatures = _settings.FontFeatures()) + { + featureMap.reserve(fontFeatures.Size()); + + for (const auto& [tag, param] : fontFeatures) + { + featureMap.emplace(tag, param); + } + } + std::unordered_map axesMap; + if (const auto fontAxes = _settings.FontAxes()) + { + axesMap.reserve(fontAxes.Size()); + + for (const auto& [axis, value] : fontAxes) + { + axesMap.emplace(axis, value); + } + } + + // TODO: MSFT:20895307 If the font doesn't exist, this doesn't + // actually fail. We need a way to gracefully fallback. + LOG_IF_FAILED(_renderEngine->UpdateDpi(newDpi)); + LOG_IF_FAILED(_renderEngine->UpdateFont(_desiredFont, _actualFont, featureMap, axesMap)); + } // If the actual font isn't what was requested... if (_actualFont.GetFaceName() != _desiredFont.GetFaceName()) diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl index 1bbc17809..56aab7395 100644 --- a/src/cascadia/TerminalControl/IControlSettings.idl +++ b/src/cascadia/TerminalControl/IControlSettings.idl @@ -36,6 +36,8 @@ namespace Microsoft.Terminal.Control Int32 FontSize; Windows.UI.Text.FontWeight FontWeight; String Padding; + Windows.Foundation.Collections.IMap FontFeatures; + Windows.Foundation.Collections.IMap FontAxes; Microsoft.Terminal.Control.IKeyBindings KeyBindings; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index ee685d659..de64a749a 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -335,6 +335,8 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate DUPLICATE_SETTING_MACRO_SUB(font, target, FontFace); DUPLICATE_SETTING_MACRO_SUB(font, target, FontSize); DUPLICATE_SETTING_MACRO_SUB(font, target, FontWeight); + DUPLICATE_SETTING_MACRO_SUB(font, target, FontFeatures); + DUPLICATE_SETTING_MACRO_SUB(font, target, FontAxes); } { diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.cpp b/src/cascadia/TerminalSettingsModel/FontConfig.cpp index a7c22a581..2d5f1bcd9 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.cpp +++ b/src/cascadia/TerminalSettingsModel/FontConfig.cpp @@ -14,6 +14,8 @@ static constexpr std::string_view FontInfoKey{ "font" }; static constexpr std::string_view FontFaceKey{ "face" }; static constexpr std::string_view FontSizeKey{ "size" }; static constexpr std::string_view FontWeightKey{ "weight" }; +static constexpr std::string_view FontFeaturesKey{ "features" }; +static constexpr std::string_view FontAxesKey{ "axes" }; static constexpr std::string_view LegacyFontFaceKey{ "fontFace" }; static constexpr std::string_view LegacyFontSizeKey{ "fontSize" }; static constexpr std::string_view LegacyFontWeightKey{ "fontWeight" }; @@ -29,6 +31,8 @@ winrt::com_ptr FontConfig::CopyFontInfo(const winrt::com_ptr_FontFace = source->_FontFace; fontInfo->_FontSize = source->_FontSize; fontInfo->_FontWeight = source->_FontWeight; + fontInfo->_FontAxes = source->_FontAxes; + fontInfo->_FontFeatures = source->_FontFeatures; return fontInfo; } @@ -39,6 +43,8 @@ Json::Value FontConfig::ToJson() const JsonUtils::SetValueForKey(json, FontFaceKey, _FontFace); JsonUtils::SetValueForKey(json, FontSizeKey, _FontSize); JsonUtils::SetValueForKey(json, FontWeightKey, _FontWeight); + JsonUtils::SetValueForKey(json, FontAxesKey, _FontAxes); + JsonUtils::SetValueForKey(json, FontFeaturesKey, _FontFeatures); return json; } @@ -65,6 +71,8 @@ void FontConfig::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(fontInfoJson, FontFaceKey, _FontFace); JsonUtils::GetValueForKey(fontInfoJson, FontSizeKey, _FontSize); JsonUtils::GetValueForKey(fontInfoJson, FontWeightKey, _FontWeight); + JsonUtils::GetValueForKey(fontInfoJson, FontFeaturesKey, _FontFeatures); + JsonUtils::GetValueForKey(fontInfoJson, FontAxesKey, _FontAxes); } else { diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.h b/src/cascadia/TerminalSettingsModel/FontConfig.h index 5ed2bef3d..61f816dba 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.h +++ b/src/cascadia/TerminalSettingsModel/FontConfig.h @@ -23,6 +23,9 @@ Author(s): #include "IInheritable.h" #include +using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; + namespace winrt::Microsoft::Terminal::Settings::Model::implementation { struct FontConfig : FontConfigT, IInheritable @@ -39,6 +42,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::FontConfig, hstring, FontFace, DEFAULT_FONT_FACE); INHERITABLE_SETTING(Model::FontConfig, int32_t, FontSize, DEFAULT_FONT_SIZE); INHERITABLE_SETTING(Model::FontConfig, Windows::UI::Text::FontWeight, FontWeight, DEFAULT_FONT_WEIGHT); + INHERITABLE_SETTING(Model::FontConfig, IFontAxesMap, FontAxes); + INHERITABLE_SETTING(Model::FontConfig, IFontFeatureMap, FontFeatures); private: winrt::weak_ref _sourceProfile; diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.idl b/src/cascadia/TerminalSettingsModel/FontConfig.idl index 7bca0ef11..37d2aeb80 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.idl +++ b/src/cascadia/TerminalSettingsModel/FontConfig.idl @@ -8,6 +8,8 @@ import "Profile.idl"; _BASE_INHERITABLE_SETTING(Type, Name); \ Microsoft.Terminal.Settings.Model.FontConfig Name##OverrideSource { get; } +#define COMMA , + namespace Microsoft.Terminal.Settings.Model { [default_interface] runtimeclass FontConfig { @@ -16,5 +18,8 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_FONT_SETTING(String, FontFace); INHERITABLE_FONT_SETTING(Int32, FontSize); INHERITABLE_FONT_SETTING(Windows.UI.Text.FontWeight, FontWeight); + + INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontFeatures); + INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontAxes); } } diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index efb3c0764..fa8dbcb49 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -177,6 +177,58 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils } }; + template + struct ConversionTrait> + { + std::unordered_map FromJson(const Json::Value& json) const + { + std::unordered_map val; + val.reserve(json.size()); + + ConversionTrait trait; + for (auto it = json.begin(), end = json.end(); it != end; ++it) + { + GetValue(*it, val[it.name()], trait); + } + + return val; + } + + bool CanConvert(const Json::Value& json) const + { + if (!json.isObject()) + { + return false; + } + ConversionTrait trait; + for (const auto& v : json) + { + if (!trait.CanConvert(v)) + { + return false; + } + } + return true; + } + + Json::Value ToJson(const std::unordered_map& val) + { + Json::Value json{ Json::objectValue }; + + for (const auto& [k, v] : val) + { + SetValueForKey(json, k, v); + } + + return json; + } + + std::string TypeDescription() const + { + return fmt::format("map (string, {})", ConversionTrait{}.TypeDescription()); + } + }; + #ifdef WINRT_BASE_H template<> struct ConversionTrait : public ConversionTrait @@ -206,6 +258,58 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils return ConversionTrait::CanConvert(json) || json.isNull(); } }; + + template + struct ConversionTrait> + { + winrt::Windows::Foundation::Collections::IMap FromJson(const Json::Value& json) const + { + std::unordered_map val; + val.reserve(json.size()); + + ConversionTrait trait; + for (auto it = json.begin(), end = json.end(); it != end; ++it) + { + GetValue(*it, val[winrt::to_hstring(it.name())], trait); + } + + return winrt::single_threaded_map(std::move(val)); + } + + bool CanConvert(const Json::Value& json) const + { + if (!json.isObject()) + { + return false; + } + ConversionTrait trait; + for (const auto& v : json) + { + if (!trait.CanConvert(v)) + { + return false; + } + } + return true; + } + + Json::Value ToJson(const winrt::Windows::Foundation::Collections::IMap& val) + { + Json::Value json{ Json::objectValue }; + + for (const auto& [k, v] : val) + { + SetValueForKey(json, til::u16u8(k), v); + } + + return json; + } + + std::string TypeDescription() const + { + return fmt::format("map (string, {})", ConversionTrait{}.TypeDescription()); + } + }; #endif template<> diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index a0ec41a43..124975fb8 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -279,6 +279,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _FontFace = profile.FontInfo().FontFace(); _FontSize = profile.FontInfo().FontSize(); _FontWeight = profile.FontInfo().FontWeight(); + _FontFeatures = profile.FontInfo().FontFeatures(); + _FontAxes = profile.FontInfo().FontAxes(); _Padding = profile.Padding(); _Commandline = profile.Commandline(); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index 5e87d4151..66e805589 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -21,6 +21,9 @@ Author(s): #include #include +using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; + // fwdecl unittest classes namespace SettingsModelLocalTests { @@ -123,6 +126,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, int32_t, FontSize, DEFAULT_FONT_SIZE); INHERITABLE_SETTING(Model::TerminalSettings, winrt::Windows::UI::Text::FontWeight, FontWeight); + INHERITABLE_SETTING(Model::TerminalSettings, IFontAxesMap, FontAxes); + INHERITABLE_SETTING(Model::TerminalSettings, IFontFeatureMap, FontFeatures); INHERITABLE_SETTING(Model::TerminalSettings, hstring, BackgroundImage); INHERITABLE_SETTING(Model::TerminalSettings, double, BackgroundImageOpacity, 1.0); diff --git a/src/cascadia/UnitTests_Control/MockControlSettings.h b/src/cascadia/UnitTests_Control/MockControlSettings.h index f3c193018..94b8f2a99 100644 --- a/src/cascadia/UnitTests_Control/MockControlSettings.h +++ b/src/cascadia/UnitTests_Control/MockControlSettings.h @@ -8,6 +8,9 @@ Licensed under the MIT license. #include #include +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; +using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; + namespace ControlUnitTests { class MockControlSettings : public winrt::implements @@ -80,6 +83,9 @@ namespace ControlUnitTests WINRT_PROPERTY(winrt::hstring, PixelShaderPath); + WINRT_PROPERTY(IFontFeatureMap, FontFeatures); + WINRT_PROPERTY(IFontAxesMap, FontAxes); + private: std::array _ColorTable; diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index 0a42ecaea..bf60bce33 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -357,7 +357,7 @@ CATCH_RETURN() _glyphIndices.resize(totalGlyphsArrayCount); } - if (_isEntireTextSimple) + if (_isEntireTextSimple && !_fontRenderData->DidUserSetFeatures()) { // When the entire text is simple, we can skip GetGlyphs and directly retrieve glyph indices and // advances(in font design unit). With the help of font metrics, we can calculate the actual glyph @@ -396,10 +396,18 @@ CATCH_RETURN() std::vector textProps(textLength); std::vector glyphProps(maxGlyphCount); + // Get the features to apply to the font + auto features = _fontRenderData->DefaultFontFeatures(); + DWRITE_FONT_FEATURE* featureList = features.data(); + DWRITE_TYPOGRAPHIC_FEATURES typographicFeatures = { &featureList[0], gsl::narrow(features.size()) }; + DWRITE_TYPOGRAPHIC_FEATURES const* typographicFeaturesPointer = &typographicFeatures; + const uint32_t fontFeatureLengths[] = { textLength }; + // Get the glyphs from the text, retrying if needed. int tries = 0; +#pragma warning(suppress : 26485) // so we can pass in the fontFeatureLengths to GetGlyphs without the analyzer complaining HRESULT hr = S_OK; do { @@ -412,9 +420,9 @@ CATCH_RETURN() &run.script, _localeName.data(), (run.isNumberSubstituted) ? _numberSubstitution.Get() : nullptr, - nullptr, // features - nullptr, // featureLengths - 0, // featureCount + &typographicFeaturesPointer, // features + &fontFeatureLengths[0], // featureLengths + 1, // featureCount maxGlyphCount, // maxGlyphCount &_glyphClusters.at(textStart), &textProps.at(0), @@ -462,9 +470,9 @@ CATCH_RETURN() (run.bidiLevel & 1), // isRightToLeft &run.script, _localeName.data(), - nullptr, // features - nullptr, // featureRangeLengths - 0, // featureRanges + &typographicFeaturesPointer, // features + &fontFeatureLengths[0], // featureLengths + 1, // featureCount &_glyphAdvances.at(glyphStart), &_glyphOffsets.at(glyphStart)); @@ -1264,29 +1272,71 @@ CATCH_RETURN(); fallback = _fontRenderData->SystemFontFallback(); } - // Walk through and analyze the entire string - while (textLength > 0) + ::Microsoft::WRL::ComPtr fallback1; + ::Microsoft::WRL::ComPtr format3; + + // If the OS supports IDWriteFontFallback1 and IDWriteTextFormat3, we can use the + // newer MapCharacters to apply axes of variation to the font + if (!FAILED(_formatInUse->QueryInterface(IID_PPV_ARGS(&format3))) && !FAILED(fallback->QueryInterface(IID_PPV_ARGS(&fallback1)))) { - UINT32 mappedLength = 0; - ::Microsoft::WRL::ComPtr mappedFont; - FLOAT scale = 0.0f; + const auto axesVector = _fontRenderData->GetAxisVector(weight, stretch, style, format3.Get()); + // Walk through and analyze the entire string + while (textLength > 0) + { + UINT32 mappedLength = 0; + ::Microsoft::WRL::ComPtr mappedFont; + FLOAT scale = 0.0f; - fallback->MapCharacters(source, - textPosition, - textLength, - collection.Get(), - familyName.data(), - weight, - style, - stretch, - &mappedLength, - &mappedFont, - &scale); + fallback1->MapCharacters(source, + textPosition, + textLength, + collection.Get(), + familyName.data(), + axesVector.data(), + gsl::narrow(axesVector.size()), + &mappedLength, + &scale, + &mappedFont); - RETURN_IF_FAILED(_SetMappedFont(textPosition, mappedLength, mappedFont.Get(), scale)); + RETURN_IF_FAILED(_SetMappedFontFace(textPosition, mappedLength, mappedFont, scale)); - textPosition += mappedLength; - textLength -= mappedLength; + textPosition += mappedLength; + textLength -= mappedLength; + } + } + else + { + // The chunk of code below is very similar to the one above, unfortunately this needs + // to stay for Win7 compatibility reasons. It is also not possible to combine the two + // because they call different versions of MapCharacters + + // Walk through and analyze the entire string + while (textLength > 0) + { + UINT32 mappedLength = 0; + ::Microsoft::WRL::ComPtr mappedFont; + FLOAT scale = 0.0f; + + fallback->MapCharacters(source, + textPosition, + textLength, + collection.Get(), + familyName.data(), + weight, + style, + stretch, + &mappedLength, + &mappedFont, + &scale); + + RETURN_LAST_ERROR_IF(!mappedFont); + ::Microsoft::WRL::ComPtr face; + RETURN_IF_FAILED(mappedFont->CreateFontFace(&face)); + RETURN_IF_FAILED(_SetMappedFontFace(textPosition, mappedLength, face, scale)); + + textPosition += mappedLength; + textLength -= mappedLength; + } } } CATCH_RETURN(); @@ -1300,14 +1350,14 @@ CATCH_RETURN(); // Arguments: // - textPosition - the index to start the substring operation // - textLength - the length of the substring operation -// - font - the font that applies to the substring range +// - fontFace - the fontFace that applies to the substring range // - scale - the scale of the font to apply // Return Value: // - S_OK or appropriate STL/GSL failure code. -[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetMappedFont(UINT32 textPosition, - UINT32 textLength, - _In_ IDWriteFont* const font, - FLOAT const scale) +[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetMappedFontFace(UINT32 textPosition, + UINT32 textLength, + const ::Microsoft::WRL::ComPtr& fontFace, + FLOAT const scale) { try { @@ -1317,14 +1367,9 @@ CATCH_RETURN(); { auto& run = _FetchNextRun(textLength); - if (font != nullptr) + if (fontFace != nullptr) { - // Get font face from font metadata - ::Microsoft::WRL::ComPtr face; - RETURN_IF_FAILED(font->CreateFontFace(&face)); - - // QI for Face5 interface from base face interface, store into run - RETURN_IF_FAILED(face.As(&run.fontFace)); + RETURN_IF_FAILED(fontFace.As(&run.fontFace)); } else { diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h index 2ac04278b..d1cbabfdb 100644 --- a/src/renderer/dx/CustomTextLayout.h +++ b/src/renderer/dx/CustomTextLayout.h @@ -126,7 +126,7 @@ namespace Microsoft::Console::Render void _OrderRuns(); [[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeFontFallback(IDWriteTextAnalysisSource* const source, UINT32 textPosition, UINT32 textLength); - [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFont(UINT32 textPosition, UINT32 textLength, IDWriteFont* const font, FLOAT const scale); + [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFontFace(UINT32 textPosition, UINT32 textLength, const ::Microsoft::WRL::ComPtr& fontFace, FLOAT const scale); [[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeBoxDrawing(gsl::not_null const source, UINT32 textPosition, UINT32 textLength); [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetBoxEffect(UINT32 textPosition, UINT32 textLength); diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index c91b95dd1..c6b1dbf9b 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -12,6 +12,7 @@ static constexpr float POINTS_PER_INCH = 72.0f; static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" }; static constexpr std::wstring_view FALLBACK_LOCALE = L"en-us"; +static constexpr size_t TAG_LENGTH = 4; using namespace Microsoft::Console::Render; @@ -93,6 +94,11 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr return _defaultFontInfo.GetStretch(); } +[[nodiscard]] const std::vector& DxFontRenderData::DefaultFontFeatures() const noexcept +{ + return _featureVector; +} + [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() { return TextFormatWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch()); @@ -178,7 +184,7 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr // - dpi - The DPI of the screen // Return Value: // - S_OK or relevant DirectX error -[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi) noexcept +[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi, const std::unordered_map& features, const std::unordered_map& axes) noexcept { try { @@ -193,6 +199,9 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL); + _SetFeatures(features); + _SetAxes(axes); + _BuildFontRenderData(desired, actual, dpi); } CATCH_RETURN(); @@ -441,6 +450,195 @@ try } CATCH_RETURN() +// Routine Description: +// - Returns whether the user set or updated any of the font features to be applied +bool DxFontRenderData::DidUserSetFeatures() const noexcept +{ + return _didUserSetFeatures; +} + +// Routine Description: +// - Updates our internal map of font features with the given features +// - NOTE TO CALLER: Make sure to call _BuildFontRenderData after calling this for the feature changes +// to take place +// Arguments: +// - features - the features to update our map with +void DxFontRenderData::_SetFeatures(const std::unordered_map& features) +{ + // Populate the feature map with the standard list first + std::unordered_map featureMap{ + { DWRITE_MAKE_FONT_FEATURE_TAG('r', 'l', 'i', 'g'), 1 }, // Required Ligatures + { DWRITE_MAKE_FONT_FEATURE_TAG('r', 'c', 'l', 't'), 1 }, // Required Contextual Alternates + { DWRITE_MAKE_FONT_FEATURE_TAG('l', 'o', 'c', 'l'), 1 }, // Localized Forms + { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'c', 'm', 'p'), 1 }, // Glyph Composition / Decomposition + { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'a', 'l', 't'), 1 }, // Contextual Alternates + { DWRITE_MAKE_FONT_FEATURE_TAG('l', 'i', 'g', 'a'), 1 }, // Standard Ligatures + { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'l', 'i', 'g'), 1 }, // Contextual Ligatures + { DWRITE_MAKE_FONT_FEATURE_TAG('k', 'e', 'r', 'n'), 1 }, // Kerning + { DWRITE_MAKE_FONT_FEATURE_TAG('m', 'a', 'r', 'k'), 1 }, // Mark Positioning + { DWRITE_MAKE_FONT_FEATURE_TAG('m', 'k', 'm', 'k'), 1 }, // Mark to Mark Positioning + { DWRITE_MAKE_FONT_FEATURE_TAG('d', 'i', 's', 't'), 1 } // Distances + }; + + // Update our feature map with the provided features + if (!features.empty()) + { + for (const auto [tag, param] : features) + { + if (tag.length() == TAG_LENGTH) + { + featureMap.insert_or_assign(DWRITE_MAKE_FONT_FEATURE_TAG(til::at(tag, 0), til::at(tag, 1), til::at(tag, 2), til::at(tag, 3)), param); + } + } + _didUserSetFeatures = true; + } + else + { + _didUserSetFeatures = false; + } + + // Convert the data to DWRITE_FONT_FEATURE and store it in a vector for CustomTextLayout + _featureVector.clear(); + for (const auto [tag, param] : featureMap) + { + _featureVector.push_back(DWRITE_FONT_FEATURE{ tag, param }); + } +} + +// Routine Description: +// - Updates our internal map of font axes with the given axes +// - NOTE TO CALLER: Make sure to call _BuildFontRenderData after calling this for the axes changes +// to take place +// Arguments: +// - axes - the axes to update our map with +void DxFontRenderData::_SetAxes(const std::unordered_map& axes) +{ + _axesVector.clear(); + + // Update our axis map with the provided axes +#pragma warning(suppress : 26445) // the analyzer doesn't like reference to string_view + for (const auto& [axis, value] : axes) + { + if (axis.length() == TAG_LENGTH) + { + const auto dwriteTag = DWRITE_MAKE_FONT_AXIS_TAG(til::at(axis, 0), til::at(axis, 1), til::at(axis, 2), til::at(axis, 3)); + _axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ dwriteTag, value }); + } + } +} + +// Method Description: +// - Converts a DWRITE_FONT_STRETCH enum into the corresponding float value to +// create a DWRITE_FONT_AXIS_VALUE with +// Arguments: +// - fontStretch: the old DWRITE_FONT_STRETCH enum to be converted into an axis value +// Return value: +// - The float value corresponding to the passed in fontStretch +float DxFontRenderData::_FontStretchToWidthAxisValue(DWRITE_FONT_STRETCH fontStretch) noexcept +{ + // 10 elements from DWRITE_FONT_STRETCH_UNDEFINED (0) to DWRITE_FONT_STRETCH_ULTRA_EXPANDED (9) + static constexpr auto fontStretchEnumToVal = std::array{ 100.0f, 50.0f, 62.5f, 75.0f, 87.5f, 100.0f, 112.5f, 125.0f, 150.0f, 200.0f }; + + if (gsl::narrow_cast(fontStretch) > fontStretchEnumToVal.size()) + { + fontStretch = DWRITE_FONT_STRETCH_NORMAL; + } + + return til::at(fontStretchEnumToVal, fontStretch); +} + +// Method Description: +// - Converts a DWRITE_FONT_STYLE enum into the corresponding float value to +// create a DWRITE_FONT_AXIS_VALUE with +// Arguments: +// - fontStyle: the old DWRITE_FONT_STYLE enum to be converted into an axis value +// Return value: +// - The float value corresponding to the passed in fontStyle +float DxFontRenderData::_FontStyleToSlantFixedAxisValue(DWRITE_FONT_STYLE fontStyle) noexcept +{ + // DWRITE_FONT_STYLE_NORMAL (0), DWRITE_FONT_STYLE_OBLIQUE (1), DWRITE_FONT_STYLE_ITALIC (2) + static constexpr auto fontStyleEnumToVal = std::array{ 0.0f, -20.0f, -12.0f }; + + // Both DWRITE_FONT_STYLE_OBLIQUE and DWRITE_FONT_STYLE_ITALIC default to having slant. + // Though an italic font technically need not have slant (there exist upright ones), the + // vast majority of italic fonts are also slanted. Ideally the slant comes from the + // 'slnt' value in the STAT or fvar table, or the post table italic angle. + + if (gsl::narrow_cast(fontStyle) > fontStyleEnumToVal.size()) + { + fontStyle = DWRITE_FONT_STYLE_NORMAL; + } + + return til::at(fontStyleEnumToVal, fontStyle); +} + +// Method Description: +// - Fill any missing axis values that might be known but were unspecified, such as omitting +// the 'wght' axis tag but specifying the old DWRITE_FONT_WEIGHT enum +// - This function will only be called with a valid IDWriteTextFormat3 +// (on platforms where IDWriteTextFormat3 is supported) +// Arguments: +// - fontWeight: the old DWRITE_FONT_WEIGHT enum to be converted into an axis value +// - fontStretch: the old DWRITE_FONT_STRETCH enum to be converted into an axis value +// - fontStyle: the old DWRITE_FONT_STYLE enum to be converted into an axis value +// - fontSize: the number to convert into an axis value +// - format: the IDWriteTextFormat3 to get the defined axes from +// Return value: +// - The fully formed axes vector +#pragma warning(suppress : 26429) // the analyzer doesn't detect that our FAIL_FAST_IF_NULL macro \ + // checks format for nullness +std::vector DxFontRenderData::GetAxisVector(const DWRITE_FONT_WEIGHT fontWeight, + const DWRITE_FONT_STRETCH fontStretch, + const DWRITE_FONT_STYLE fontStyle, + IDWriteTextFormat3* format) +{ + FAIL_FAST_IF_NULL(format); + + const auto axesCount = format->GetFontAxisValueCount(); + std::vector axesVector; + axesVector.resize(axesCount); + format->GetFontAxisValues(axesVector.data(), axesCount); + + auto axisTagPresence = AxisTagPresence::None; + for (const auto& fontAxisValue : axesVector) + { + switch (fontAxisValue.axisTag) + { + case DWRITE_FONT_AXIS_TAG_WEIGHT: + WI_SetFlag(axisTagPresence, AxisTagPresence::Weight); + break; + case DWRITE_FONT_AXIS_TAG_WIDTH: + WI_SetFlag(axisTagPresence, AxisTagPresence::Width); + break; + case DWRITE_FONT_AXIS_TAG_ITALIC: + WI_SetFlag(axisTagPresence, AxisTagPresence::Italic); + break; + case DWRITE_FONT_AXIS_TAG_SLANT: + WI_SetFlag(axisTagPresence, AxisTagPresence::Slant); + break; + } + } + + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Weight)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WEIGHT, gsl::narrow(fontWeight) }); + } + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Width)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WIDTH, _FontStretchToWidthAxisValue(fontStretch) }); + } + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Italic)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_ITALIC, (fontStyle == DWRITE_FONT_STYLE_ITALIC ? 1.0f : 0.0f) }); + } + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Slant)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_SLANT, _FontStyleToSlantFixedAxisValue(fontStyle) }); + } + + return axesVector; +} + // Routine Description: // - Build the needed data for rendering according to the font used // Arguments: @@ -649,5 +847,14 @@ Microsoft::WRL::ComPtr DxFontRenderData::_BuildTextFormat(con _fontSize, localeName.data(), &format)); + + // If the OS supports IDWriteTextFormat3, set the font axes + ::Microsoft::WRL::ComPtr format3; + if (!_axesVector.empty() && !FAILED(format->QueryInterface(IID_PPV_ARGS(&format3)))) + { + DWRITE_FONT_AXIS_VALUE const* axesList = _axesVector.data(); + format3->SetFontAxisValues(axesList, gsl::narrow(_axesVector.size())); + } + return format; } diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h index 2cf5fc7be..379fd6036 100644 --- a/src/renderer/dx/DxFontRenderData.h +++ b/src/renderer/dx/DxFontRenderData.h @@ -16,6 +16,16 @@ namespace Microsoft::Console::Render { + enum class AxisTagPresence : BYTE + { + None = 0x00, + Weight = 0x01, + Width = 0x02, + Italic = 0x04, + Slant = 0x08, + }; + DEFINE_ENUM_FLAG_OPERATORS(AxisTagPresence); + class DxFontRenderData { public: @@ -51,6 +61,9 @@ namespace Microsoft::Console::Render // The stretch of default font [[nodiscard]] DWRITE_FONT_STRETCH DefaultFontStretch() noexcept; + // The font features of the default font + [[nodiscard]] const std::vector& DefaultFontFeatures() const noexcept; + // The DirectWrite format object representing the size and other text properties to be applied (by default) [[nodiscard]] Microsoft::WRL::ComPtr DefaultTextFormat(); @@ -70,19 +83,37 @@ namespace Microsoft::Console::Render DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch); - [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi) noexcept; + [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi, const std::unordered_map& features = {}, const std::unordered_map& axes = {}) noexcept; [[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept; + bool DidUserSetFeatures() const noexcept; + + std::vector GetAxisVector(const DWRITE_FONT_WEIGHT fontWeight, + const DWRITE_FONT_STRETCH fontStretch, + const DWRITE_FONT_STYLE fontStyle, + IDWriteTextFormat3* format); + private: using FontAttributeMapKey = uint32_t; + bool _didUserSetFeatures{ false }; + // The font features to apply to the text + std::vector _featureVector; + + // The font axes to apply to the text + std::vector _axesVector; + // We use this to identify font variants with different attributes. static FontAttributeMapKey _ToMapKey(DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch) noexcept { return (weight << 16) | (style << 8) | stretch; }; + void _SetFeatures(const std::unordered_map& features); + void _SetAxes(const std::unordered_map& axes); + float _FontStretchToWidthAxisValue(DWRITE_FONT_STRETCH fontStretch) noexcept; + float _FontStyleToSlantFixedAxisValue(DWRITE_FONT_STYLE fontStyle) noexcept; void _BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi); Microsoft::WRL::ComPtr _BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName); diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 03904814e..7748b1556 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -1978,15 +1978,30 @@ CATCH_RETURN() // Routine Description: // - Updates the font used for drawing +// - This is the version that complies with the IRenderEngine interface // Arguments: // - pfiFontInfoDesired - Information specifying the font that is requested // - fiFontInfo - Filled with the nearest font actually chosen for drawing // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept +{ + return UpdateFont(pfiFontInfoDesired, fiFontInfo, {}, {}); +} + +// Routine Description: +// - Updates the font used for drawing +// Arguments: +// - pfiFontInfoDesired - Information specifying the font that is requested +// - fiFontInfo - Filled with the nearest font actually chosen for drawing +// - features - The map of font features to use +// - axes - The map of font axes to use +// Return Value: +// - S_OK or relevant DirectX error +[[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept try { - RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi)); + RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi, features, axes)); // Prepare the text layout. _customLayout = WRL::Make(_fontRenderData.get()); diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index cbd34ea3b..b74f0ba0f 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -109,6 +109,7 @@ namespace Microsoft::Console::Render const gsl::not_null pData, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; + [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; From 3ffaa1714ae3b7a879c9c7201ef8f5bb538bdc00 Mon Sep 17 00:00:00 2001 From: Schuyler Rosefield Date: Thu, 22 Jul 2021 20:09:47 -0400 Subject: [PATCH 22/90] Update NavigateFocus function to use new visual-based navigation (#10756) ## Summary of the Pull Request Uses the new logic to find visual neighbors of a pane to find which pane is the target when the move-focus commands are used. ## References It sounds like this logic will be refined later to meet #4692 ## PR Checklist * [x] Closes #2398 * [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 * [ ] 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 ## Validation Steps Performed Created a grid of panes and confirmed that focus movement went to the right quadrant instead of just the first child of the sibling. --- src/cascadia/TerminalApp/Pane.cpp | 74 +++++++------------------------ src/cascadia/TerminalApp/Pane.h | 1 - 2 files changed, 15 insertions(+), 60 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 1ffbaa787..a2c071146 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -213,41 +213,6 @@ bool Pane::ResizePane(const ResizeDirection& direction) return false; } -// Method Description: -// - Attempts to handle moving focus to one of our children. If our split -// direction isn't appropriate for the move direction, then we'll return -// false, to try and let our parent handle the move. If our child we'd move -// focus to is already focused, we'll also return false, to again let our -// parent try and handle the focus movement. -// Arguments: -// - direction: The direction to move the focus in. -// Return Value: -// - true if we handled this focus move request. -bool Pane::_NavigateFocus(const FocusDirection& direction) -{ - if (!DirectionMatchesSplit(direction, _splitState)) - { - return false; - } - - const bool focusSecond = (direction == FocusDirection::Right) || (direction == FocusDirection::Down); - - const auto newlyFocusedChild = focusSecond ? _secondChild : _firstChild; - - // If the child we want to move focus to is _already_ focused, return false, - // to try and let our parent figure it out. - if (newlyFocusedChild->_HasFocusedChild()) - { - return false; - } - - // Transfer focus to our child, and update the focus of our tree. - newlyFocusedChild->_FocusFirstChild(); - UpdateVisuals(); - - return true; -} - // Method Description: // - Attempts to move focus to one of our children. If we have a focused child, // we'll try to move the focus in the direction requested. @@ -255,8 +220,8 @@ bool Pane::_NavigateFocus(const FocusDirection& direction) // direction, we'll return false. This will indicate to our parent that they // should try and move the focus themselves. In this way, the focus can move // up and down the tree to the correct pane. -// - This method is _very_ similar to ResizePane. Both are trying to find the -// right separator to move (focus) in a direction. +// - This method is _very_ similar to MovePane. Both are trying to find the +// right pane to move (focus) in a direction. // Arguments: // - direction: The direction to move the focus in. // Return Value: @@ -270,33 +235,24 @@ bool Pane::NavigateFocus(const FocusDirection& direction) return false; } - // Check if either our first or second child is the currently focused leaf. - // If it is, and the requested move direction matches our separator, then - // we're the pane that needs to handle this focus move. - const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive; - const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive; - if (firstIsFocused || secondIsFocused) + // If the focus direction does not match the split direction, the focused pane + // and its neighbor must necessarily be contained within the same child. + if (!DirectionMatchesSplit(direction, _splitState)) { - return _NavigateFocus(direction); + return _firstChild->NavigateFocus(direction) || _secondChild->NavigateFocus(direction); } - // If neither of our children were the focused leaf, then recurse into - // our children and see if they can handle the focus move. - // For each child, if it has a focused descendant, try having that child - // handle the focus move. - // If the child wasn't able to handle the focus move, it's possible that - // there were no descendants with a separator the correct direction. If - // our separator _is_ the correct direction, then we should be the pane - // to move focus into our other child. Otherwise, just return false, as - // we couldn't handle it either. - if ((!_firstChild->_IsLeaf()) && _firstChild->_HasFocusedChild()) - { - return _firstChild->NavigateFocus(direction) || _NavigateFocus(direction); - } + // Since the direction is the same as our split, it is possible that we must + // move focus from from one child to another child. + // We now must keep track of state while we recurse. + auto focusNeighborPair = _FindFocusAndNeighbor(direction, { 0, 0 }); - if ((!_secondChild->_IsLeaf()) && _secondChild->_HasFocusedChild()) + // Once we have found the focused pane and its neighbor, wherever they may + // be we can update the focus. + if (focusNeighborPair.focus && focusNeighborPair.neighbor) { - return _secondChild->NavigateFocus(direction) || _NavigateFocus(direction); + focusNeighborPair.neighbor->_FocusFirstChild(); + return true; } return false; diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 85c166ded..a8bc3f443 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -148,7 +148,6 @@ private: void _UpdateBorders(); bool _Resize(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); - bool _NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); std::shared_ptr _FindParentOfPane(const std::shared_ptr pane); bool _IsAdjacent(const std::shared_ptr first, const PanePoint firstOffset, const std::shared_ptr second, const PanePoint secondOffset, const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction) const; From 20e88d3e3edeccacea76b37945fee5275e196e29 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 23 Jul 2021 20:19:07 +0200 Subject: [PATCH 23/90] Fix conhost UseDx mode (#10770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the UseDx mode for conhost. In order to add support for UseDx without calling `SetWindowSize`, responsibility for resizing `_invalidMap` has been moved to occur only when the renderer itself recognizes a new size. Furthermore `InvalidateAll` is now the central point to invalidate `_invalidMap`. ## Validation Steps Performed * Enabling `UseDx` enables the DxEngine for conhost ✔️ * Resizing windows in conhost works ✔️ * Resizing windows in WT works ✔️ Closes #5455 --- src/interactivity/win32/window.cpp | 5 ++--- src/renderer/dx/DxRenderer.cpp | 24 ++++++++++-------------- src/renderer/dx/DxRenderer.hpp | 1 - 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp index 75235fc94..e5ea81735 100644 --- a/src/interactivity/win32/window.cpp +++ b/src/interactivity/win32/window.cpp @@ -31,9 +31,6 @@ #if TIL_FEATURE_CONHOSTDXENGINE_ENABLED #include "../../renderer/dx/DxRenderer.hpp" -#else -// Forward-declare this so we don't blow up later. -struct DxEngine; #endif #include "../inc/ServiceLocator.hpp" @@ -214,7 +211,9 @@ void Window::_UpdateSystemMetrics() const const bool useDx = pSettings->GetUseDx(); GdiEngine* pGdiEngine = nullptr; +#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED [[maybe_unused]] DxEngine* pDxEngine = nullptr; +#endif try { #if TIL_FEATURE_CONHOSTDXENGINE_ENABLED diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 7748b1556..b633ab7a4 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -61,7 +61,6 @@ using namespace Microsoft::Console::Types; // TODO GH 2683: The default constructor should not throw. DxEngine::DxEngine() : RenderEngineBase(), - _invalidateFullRows{ true }, _pool{ til::pmr::get_default_resource() }, _invalidMap{ &_pool }, _invalidScroll{}, @@ -583,6 +582,9 @@ try _displaySizePixels = _GetClientSize(); + _invalidMap.resize(_displaySizePixels / _fontRenderData->GlyphCell()); + RETURN_IF_FAILED(InvalidateAll()); + // Get the other device types so we have deeper access to more functionality // in our pipeline than by just walking straight from the D3D device. @@ -963,8 +965,6 @@ CATCH_RETURN() try { _sizeTarget = Pixels; - - _invalidMap.resize(_sizeTarget / _fontRenderData->GlyphCell(), true); return S_OK; } CATCH_RETURN(); @@ -1047,14 +1047,10 @@ HANDLE DxEngine::GetSwapChainHandle() void DxEngine::_InvalidateRectangle(const til::rectangle& rc) { - auto invalidate = rc; - - if (_invalidateFullRows) - { - invalidate = til::rectangle{ til::point{ static_cast(0), rc.top() }, til::size{ _invalidMap.size().width(), rc.height() } }; - } - - _invalidMap.set(invalidate); + const auto size = _invalidMap.size(); + const auto topLeft = til::point{ 0, std::min(size.height(), rc.top()) }; + const auto bottomRight = til::point{ size.width(), std::min(size.height(), rc.bottom()) }; + _invalidMap.set({ topLeft, bottomRight }); } bool DxEngine::_IsAllInvalid() const noexcept @@ -1273,7 +1269,7 @@ try // so the entire frame is repainted. if (_FullRepaintNeeded()) { - _invalidMap.set_all(); + RETURN_IF_FAILED(InvalidateAll()); } if (TraceLoggingProviderEnabled(g_hDxRenderProvider, WINEVENT_LEVEL_VERBOSE, TIL_KEYWORD_TRACE)) @@ -1322,8 +1318,8 @@ try // And persist the new size. _displaySizePixels = clientSize; - // Mark this as the first frame on the new target. We can't use incremental drawing on the first frame. - _firstFrame = true; + _invalidMap.resize(clientSize / _fontRenderData->GlyphCell()); + RETURN_IF_FAILED(InvalidateAll()); } _d2dDeviceContext->BeginDraw(); diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index b74f0ba0f..1351e61de 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -169,7 +169,6 @@ namespace Microsoft::Console::Render uint16_t _hyperlinkHoveredId; bool _firstFrame; - bool _invalidateFullRows; std::pmr::unsynchronized_pool_resource _pool; til::pmr::bitmap _invalidMap; til::point _invalidScroll; From 3a71ead75790339371277702e5edf1eaf01df665 Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Mon, 26 Jul 2021 09:27:07 -0700 Subject: [PATCH 24/90] Remove some unnecessary font features from our default feature list (#10774) Turns out, DWrite will automatically turn some features on even if they weren't included in the feature vector passed into it. Remove these features from our default list for easier readability. --- src/renderer/dx/DxFontRenderData.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index c6b1dbf9b..fb3e75138 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -467,17 +467,10 @@ void DxFontRenderData::_SetFeatures(const std::unordered_map featureMap{ - { DWRITE_MAKE_FONT_FEATURE_TAG('r', 'l', 'i', 'g'), 1 }, // Required Ligatures - { DWRITE_MAKE_FONT_FEATURE_TAG('r', 'c', 'l', 't'), 1 }, // Required Contextual Alternates - { DWRITE_MAKE_FONT_FEATURE_TAG('l', 'o', 'c', 'l'), 1 }, // Localized Forms - { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'c', 'm', 'p'), 1 }, // Glyph Composition / Decomposition { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'a', 'l', 't'), 1 }, // Contextual Alternates { DWRITE_MAKE_FONT_FEATURE_TAG('l', 'i', 'g', 'a'), 1 }, // Standard Ligatures { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'l', 'i', 'g'), 1 }, // Contextual Ligatures - { DWRITE_MAKE_FONT_FEATURE_TAG('k', 'e', 'r', 'n'), 1 }, // Kerning - { DWRITE_MAKE_FONT_FEATURE_TAG('m', 'a', 'r', 'k'), 1 }, // Mark Positioning - { DWRITE_MAKE_FONT_FEATURE_TAG('m', 'k', 'm', 'k'), 1 }, // Mark to Mark Positioning - { DWRITE_MAKE_FONT_FEATURE_TAG('d', 'i', 's', 't'), 1 } // Distances + { DWRITE_MAKE_FONT_FEATURE_TAG('k', 'e', 'r', 'n'), 1 } // Kerning }; // Update our feature map with the provided features From 862217b04bae69f98703709abf3de8d7ecb8ff18 Mon Sep 17 00:00:00 2001 From: Michael Niksa Date: Mon, 26 Jul 2021 12:31:48 -0700 Subject: [PATCH 25/90] [DefApp] Teach connection and tab to negotiate initial size (#10772) - For tabs started from the Terminal, the initial sizing information is passed into the connection and used to establish the PTY. Those parameters are given over to the `OpenConsole.exe` acting as PTY to establish the initial buffer/window size. - However, for tabs started from outside, the PTY is created with some default buffer information FIRST as the Terminal hasn't even been involved yet. As such, when the Terminal gets that connection, it must tell the PTY to resize just as it connects to match the window size it's about to use. - Ongoing resize operations in the Terminal did and still work fine because they transmitted the updated size with the `ResizePseudoConsole` API. ## Validation Steps Performed - [x] Confirmed existing tabs opening have correct initial size in PTY (like with CMD `mode con` command) - [x] Confirmed inbound cmd tabs have correct initial size in PTY via `mode con` command per bug repro Closes #9811 --- src/cascadia/TerminalApp/TabManagement.cpp | 8 ++++++++ .../TerminalConnection/ConptyConnection.cpp | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 1c753aa43..dd3a7aa7c 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -105,6 +105,14 @@ namespace winrt::TerminalApp::implementation // Create a connection based on the values in our settings object if we weren't given one. auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profileGuid, settings.DefaultSettings()); + // If we had an `existingConnection`, then this is an inbound handoff from somewhere else. + // We need to tell it about our size information so it can match the dimensions of what + // we are about to present. + if (existingConnection) + { + connection.Resize(settings.DefaultSettings().InitialRows(), settings.DefaultSettings().InitialCols()); + } + TerminalConnection::ITerminalConnection debugConnection{ nullptr }; if (_settings.GlobalSettings().DebugFeaturesEnabled()) { diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index 2be231e80..1589f708d 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -289,12 +289,21 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { _transitionToState(ConnectionState::Connecting); + const COORD dimensions{ gsl::narrow_cast(_initialCols), gsl::narrow_cast(_initialRows) }; + + // If we do not have pipes already, then this is a fresh connection... not an inbound one that is a received + // handoff from an already-started PTY process. if (!_inPipe) { - const COORD dimensions{ gsl::narrow_cast(_initialCols), gsl::narrow_cast(_initialRows) }; THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK | PSEUDOCONSOLE_WIN32_INPUT_MODE, &_inPipe, &_outPipe, &_hPC)); THROW_IF_FAILED(_LaunchAttachedClient()); } + // But if it was an inbound handoff... attempt to synchronize the size of it with what our connection + // window is expecting it to be on the first layout. + else + { + THROW_IF_FAILED(ConptyResizePseudoConsole(_hPC.get(), dimensions)); + } _startTime = std::chrono::high_resolution_clock::now(); @@ -423,11 +432,14 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation void ConptyConnection::Resize(uint32_t rows, uint32_t columns) { - if (!_hPC) + // If we haven't started connecting at all, it's still fair to update + // the initial rows and columns before we set things up. + if (!_isStateAtOrBeyond(ConnectionState::Connecting)) { _initialRows = rows; _initialCols = columns; } + // Otherwise, we can really only dispatch a resize if we're already connected. else if (_isConnected()) { THROW_IF_FAILED(ConptyResizePseudoConsole(_hPC.get(), { Utils::ClampToShortMax(columns, 1), Utils::ClampToShortMax(rows, 1) })); From d43a14c63fc0192d84bde4f6b510d232777c454e Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Mon, 26 Jul 2021 20:14:59 -0500 Subject: [PATCH 26/90] Replace the placeholder release build with our real one (#10778) This pull request ports our old release pipeline from Azure DevOps' editor to real YAML. It includes the following changes on top of a straight-up "export" from Azure: - Converts all queue-time variables into form-based parameters - Adds a "matrix" build strategy for Configs * Platforms - Renames all jobs to have reasonable names - The YAML generator has a bug where it inlines scripts *and* file paths if a task had both; remove old inlines - Removes dead rules - Fixes the WPF build to include the apiset impostor - Migrates the access token into the environment for the one build stage that needs it - Cleans up some of the online script logic - Removes all of the "!is pull request?" checks --- build/pipelines/release.yml | 518 ++++++++++++++++-- .../pipelines/templates/build-console-int.yml | 31 -- .../templates/release-sign-and-bundle.yml | 74 --- 3 files changed, 481 insertions(+), 142 deletions(-) delete mode 100644 build/pipelines/templates/build-console-int.yml delete mode 100644 build/pipelines/templates/release-sign-and-bundle.yml diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index 847846f83..ff6d49cf7 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -1,48 +1,492 @@ +# This build should never run as CI or against a pull request. trigger: none pr: none +pool: + name: Package ES Standard Build + +parameters: + - name: branding + displayName: "Branding (Build Type)" + type: string + default: Release + values: + - Release + - Preview + - name: buildTerminal + displayName: "Build Windows Terminal MSIX" + type: boolean + default: true + - name: buildTerminalVPack + displayName: "Build Windows Terminal VPack" + type: boolean + default: false + - name: buildWPF + displayName: "Build Terminal WPF Control" + type: boolean + default: false + - name: pgoBuildMode + displayName: "PGO Build Mode" + type: string + default: Optimize + values: + - Optimize + - Instrument + - None + + - name: buildConfigurations + type: object + default: + - Release + - name: buildPlatforms + type: object + default: + - x64 + - x86 + - arm64 + variables: - baseYearForVersioning: 2019 # Used by build-console-int - versionMajor: 0 - versionMinor: 1 + TerminalInternalPackageVersion: "0.0.7" -# When we move off PackageES for Versioning, we'll need to switch -# name to this format. For now, though, we need to use DayOfYear.Rev -# to unique our builds, as mandated by PackageES's Setup task. -# name: '$(versionMajor).$(versionMinor).$(DayOfYear)$(Rev:r).0' -# -# Build name/version number above must end with .0 to make the -# store publication machinery happy. -name: 'Terminal_$(date:yyMM).$(date:dd)$(rev:rrr)' - -# Build Arguments: -# WindowsTerminalOfficialBuild=[true,false] -# true - this is running on our build agent -# false - running locally -# WindowsTerminalBranding=[Dev,Preview,Release] -# - Development build resources (default) -# Preview - Preview build resources -# Release - regular build resources +name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) +resources: + repositories: + - repository: self + type: git + ref: main jobs: - - template: ./templates/build-console-audit-job.yml - parameters: - platform: x64 +- job: Build + strategy: + matrix: + ${{ each config in parameters.buildConfigurations }}: + ${{ each platform in parameters.buildPlatforms }}: + ${{ config }}_${{ platform }}: + BuildConfiguration: ${{ config }} + BuildPlatform: ${{ platform }} + displayName: Build + cancelTimeoutInMinutes: 1 + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: True + - task: PkgESSetupBuild@10 + displayName: Package ES - Setup Build + inputs: + useDfs: false + productName: OpenConsole + disableOutputRedirect: true + - task: PowerShell@2 + displayName: Rationalize Build Platform + inputs: + targetType: inline + script: >- + $Arch = "$(BuildPlatform)" - - template: ./templates/build-console-int.yml - parameters: - platform: x64 - additionalBuildArguments: /p:WindowsTerminalOfficialBuild=true;WindowsTerminalBranding=Preview + If ($Arch -Eq "x86") { $Arch = "Win32" } - - template: ./templates/build-console-int.yml - parameters: - platform: x86 - additionalBuildArguments: /p:WindowsTerminalOfficialBuild=true;WindowsTerminalBranding=Preview + Write-Host "##vso[task.setvariable variable=RationalizedBuildPlatform]${Arch}" + - task: NuGetToolInstaller@1 + displayName: Use NuGet 5.10 + inputs: + versionSpec: 5.10 + - task: NuGetCommand@2 + displayName: NuGet custom + inputs: + command: custom + selectOrConfig: config + nugetConfigPath: NuGet.Config + arguments: restore OpenConsole.sln -SolutionDirectory $(Build.SourcesDirectory) + - task: UniversalPackages@0 + displayName: Download terminal-internal Universal Package + inputs: + feedListDownload: 2b3f8893-a6e8-411f-b197-a9e05576da48 + packageListDownload: e82d490c-af86-4733-9dc4-07b772033204 + versionListDownload: $(TerminalInternalPackageVersion) + - task: TouchdownBuildTask@1 + displayName: Download Localization Files + inputs: + teamId: 7105 + authId: $(TouchdownAppId) + authKey: $(TouchdownAppKey) + resourceFilePath: >- + src\cascadia\TerminalApp\Resources\en-US\Resources.resw - - template: ./templates/build-console-int.yml - parameters: - platform: arm64 - additionalBuildArguments: /p:WindowsTerminalOfficialBuild=true;WindowsTerminalBranding=Preview + src\cascadia\TerminalControl\Resources\en-US\Resources.resw - - template: ./templates/check-formatting.yml + src\cascadia\TerminalConnection\Resources\en-US\Resources.resw - - template: ./templates/release-sign-and-bundle.yml + src\cascadia\TerminalSettingsModel\Resources\en-US\Resources.resw + + src\cascadia\TerminalSettingsEditor\Resources\en-US\Resources.resw + + src\cascadia\WindowsTerminalUniversal\Resources\en-US\Resources.resw + + src\cascadia\CascadiaPackage\Resources\en-US\Resources.resw + appendRelativeDir: true + localizationTarget: false + pseudoSetting: Included + - task: PowerShell@2 + displayName: Move Loc files one level up + inputs: + targetType: inline + script: >- + $Files = Get-ChildItem . -R -Filter 'Resources.resw' | ? FullName -Like '*en-US\*\Resources.resw' + + $Files | % { Move-Item -Verbose $_.Directory $_.Directory.Parent.Parent -EA:Ignore } + pwsh: true + - task: PowerShell@2 + displayName: Generate NOTICE.html from NOTICE.md + inputs: + filePath: .\build\scripts\Generate-ThirdPartyNotices.ps1 + arguments: -MarkdownNoticePath .\NOTICE.md -OutputPath .\src\cascadia\CascadiaPackage\NOTICE.html + pwsh: true + - ${{ if eq(parameters.pgoBuildMode, 'Optimize') }}: + - task: PowerShell@2 + displayName: Restore PGO Database + inputs: + filePath: tools/PGODatabase/restore-pgodb.ps1 + workingDirectory: $(Build.SourcesDirectory)\tools\PGODatabase + - ${{ if eq(parameters.buildTerminal, true) }}: + - task: VSBuild@1 + displayName: Build solution **\OpenConsole.sln + inputs: + solution: '**\OpenConsole.sln' + vsVersion: 16.0 + msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }} /t:Terminal\CascadiaPackage;Terminal\WindowsTerminalUniversal /p:WindowsTerminalReleaseBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + clean: true + maximumCpuCount: true + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: binlog' + condition: failed() + continueOnError: True + inputs: + PathtoPublish: $(Build.SourcesDirectory)\msbuild.binlog + ArtifactName: binlog-$(BuildPlatform) + - ${{ if eq(parameters.pgoBuildMode, 'Optimize') }}: + - task: PowerShell@2 + displayName: Validate binaries are optimized + condition: and(succeeded(), eq(variables['BuildPlatform'], 'x64')) + inputs: + targetType: inline + script: >- + $Binaries = 'OpenConsole.exe', 'WindowsTerminal.exe', 'TerminalApp.dll', 'TerminalConnection.dll', 'Microsoft.Terminal.Control.dll', 'Microsoft.Terminal.Remoting.dll', 'Microsoft.Terminal.Settings.Editor.dll', 'Microsoft.Terminal.Settings.Model.dll' + + foreach ($BinFile in $Binaries) { + + & "$(Build.SourcesDirectory)\tools\PGODatabase\verify-pgo.ps1" "$(Build.SourcesDirectory)/src/cascadia/CascadiaPackage/bin/$(BuildPlatform)/$(BuildConfiguration)/$BinFile" + + } + - task: PowerShell@2 + displayName: Check MSIX for common regressions + inputs: + targetType: inline + script: >- + $Package = Get-ChildItem -Recurse -Filter "CascadiaPackage_*.msix" + + .\build\scripts\Test-WindowsTerminalPackage.ps1 -Verbose -Path $Package.FullName + pwsh: true + - ${{ if eq(parameters.buildWPF, true) }}: + - task: VSBuild@1 + displayName: Build solution **\OpenConsole.sln for PublicTerminalCore + condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) + inputs: + solution: '**\OpenConsole.sln' + vsVersion: 16.0 + msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }} /p:WindowsTerminalReleaseBuild=true /t:Terminal\wpf\PublicTerminalCore + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + - task: PowerShell@2 + displayName: Source Index PDBs + inputs: + filePath: build\scripts\Index-Pdbs.ps1 + arguments: -SearchDir '$(Build.SourcesDirectory)' -SourceRoot '$(Build.SourcesDirectory)' -recursive -Verbose -CommitId $(Build.SourceVersion) + errorActionPreference: silentlyContinue + - task: ComponentGovernanceComponentDetection@0 + displayName: Component Detection + - task: PowerShell@2 + displayName: Run Unit Tests + condition: and(succeeded(), or(eq(variables['BuildPlatform'], 'x64'), eq(variables['BuildPlatform'], 'x86'))) + enabled: False + inputs: + filePath: build\scripts\Run-Tests.ps1 + arguments: -MatchPattern '*unit.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' + - task: PowerShell@2 + displayName: Run Feature Tests + condition: and(succeeded(), eq(variables['BuildPlatform'], 'x64')) + enabled: False + inputs: + filePath: build\scripts\Run-Tests.ps1 + arguments: -MatchPattern '*feature.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' + - ${{ if eq(parameters.buildTerminal, true) }}: + - task: CopyFiles@2 + displayName: Copy *.appx/*.msix to Artifacts + inputs: + Contents: >- + **/*.appx + + **/*.msix + + **/*.appxsym + + !**/Microsoft.VCLibs*.appx + TargetFolder: $(Build.ArtifactStagingDirectory)/appx + OverWrite: true + flattenFolders: true + - task: PublishBuildArtifacts@1 + displayName: Publish Artifact (appx) + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory)/appx + ArtifactName: appx-$(BuildPlatform)-$(BuildConfiguration) + - ${{ if eq(parameters.buildWPF, true) }}: + - task: CopyFiles@2 + displayName: Copy PublicTerminalCore.dll to Artifacts + condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) + inputs: + Contents: >- + **/PublicTerminalCore.dll + + **/api-ms-win-core-synch-l1-2-0.dll + TargetFolder: $(Build.ArtifactStagingDirectory)/wpf + OverWrite: true + flattenFolders: true + - task: PublishBuildArtifacts@1 + displayName: Publish Artifact (PublicTerminalCore) + condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory)/wpf + ArtifactName: wpf-dll-$(BuildPlatform)-$(BuildConfiguration) + - task: PublishSymbols@2 + displayName: Publish symbols path + continueOnError: True + inputs: + SearchPattern: '**/*.pdb' + IndexSources: false + SymbolServerType: TeamServices + +- ${{ if eq(parameters.buildTerminal, true) }}: + - job: BundleAndSign + displayName: Create and sign AppX/MSIX bundles + dependsOn: Build + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: True + - task: PkgESSetupBuild@10 + displayName: Package ES - Setup Build + inputs: + useDfs: false + productName: OpenConsole + disableOutputRedirect: true + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts (*.appx, *.msix) + inputs: + downloadType: specific + itemPattern: >- + **/*.msix + + **/*.appx + extractTars: false + - task: PowerShell@2 + displayName: Create WindowsTerminal*.msixbundle + inputs: + filePath: build\scripts\Create-AppxBundle.ps1 + arguments: -InputPath "$(System.ArtifactsDirectory)" -ProjectName CascadiaPackage -BundleVersion 0.0.0.0 -OutputPath "$(System.ArtifactsDirectory)\Microsoft.WindowsTerminal_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" + - task: PowerShell@2 + displayName: Create WindowsTerminalUniversal*.msixbundle + inputs: + filePath: build\scripts\Create-AppxBundle.ps1 + arguments: -InputPath "$(System.ArtifactsDirectory)" -ProjectName WindowsTerminalUniversal -BundleVersion $(XES_APPXMANIFESTVERSION) -OutputPath "$(System.ArtifactsDirectory)\Microsoft.WindowsTerminalUniversal_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" + - task: EsrpCodeSigning@1 + displayName: Submit *.msixbundle to ESRP for code signing + inputs: + ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + FolderPath: $(System.ArtifactsDirectory) + Pattern: Microsoft.WindowsTerminal*.msixbundle + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "Dynamic", + "CertTemplateName": "WINMSAPP1ST", + "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + "OperationCode": "SigntoolSign", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "http://www.microsoft.com", + "FileDigest": "/fd \"SHA256\"", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "Dynamic", + "CertTemplateName": "WINMSAPP1ST", + "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + "OperationCode": "SigntoolVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: appxbundle-signed' + inputs: + PathtoPublish: $(System.ArtifactsDirectory) + ArtifactName: appxbundle-signed + +- ${{ if eq(parameters.buildWPF, true) }}: + - job: PackageAndSignWPF + strategy: + matrix: + ${{ each config in parameters.buildConfigurations }}: + ${{ config }}: + BuildConfiguration: ${{ config }} + displayName: Create NuGet Package (WPF Terminal Control) + dependsOn: Build + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: True + - task: PkgESSetupBuild@10 + displayName: Package ES - Setup Build + inputs: + useDfs: false + productName: OpenConsole + disableOutputRedirect: true + - task: DownloadBuildArtifacts@0 + displayName: Download x86 PublicTerminalCore + inputs: + artifactName: wpf-dll-x86-$(BuildConfiguration) + itemPattern: '**/*.dll' + downloadPath: bin\Win32\$(BuildConfiguration)\ + extractTars: false + - task: DownloadBuildArtifacts@0 + displayName: Download x64 PublicTerminalCore + inputs: + artifactName: wpf-dll-x64-$(BuildConfiguration) + itemPattern: '**/*.dll' + downloadPath: bin\x64\$(BuildConfiguration)\ + extractTars: false + - task: PowerShell@2 + displayName: Move downloaded artifacts up a level + inputs: + targetType: inline + # Find all artifact files and move them up a directory. Ugh. + script: >- + Get-ChildItem bin -Recurse -Directory -Filter wpf-dll-* | % { + $_ | Get-ChildItem -Recurse -File | % { + Move-Item -Verbose $_.FullName $_.Directory.Parent.FullName + } + } + - task: NuGetToolInstaller@1 + displayName: Use NuGet 5.10.0 + inputs: + versionSpec: 5.10.0 + - task: NuGetCommand@2 + displayName: NuGet restore copy + inputs: + selectOrConfig: config + nugetConfigPath: NuGet.Config + - task: VSBuild@1 + displayName: Build solution **\OpenConsole.sln for WPF Control + inputs: + solution: '**\OpenConsole.sln' + vsVersion: 16.0 + msbuildArgs: /p:WindowsTerminalReleaseBuild=$(UseReleaseBranding);Version=$(XES_PACKAGEVERSIONNUMBER) /t:Pack + platform: Any CPU + configuration: $(BuildConfiguration) + maximumCpuCount: true + - task: PublishSymbols@2 + displayName: Publish symbols path + continueOnError: True + inputs: + SearchPattern: '**/*.pdb' + IndexSources: false + SymbolServerType: TeamServices + SymbolsArtifactName: Symbols_WPF_$(BuildConfiguration) + - task: CopyFiles@2 + displayName: Copy *.nupkg to Artifacts + inputs: + Contents: '**/*Wpf*.nupkg' + TargetFolder: $(Build.ArtifactStagingDirectory)/nupkg + OverWrite: true + flattenFolders: true + - task: EsrpCodeSigning@1 + displayName: Submit *.nupkg to ESRP for code signing + inputs: + ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + FolderPath: $(Build.ArtifactStagingDirectory)/nupkg + Pattern: '*.nupkg' + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + - task: PublishBuildArtifacts@1 + displayName: Publish Artifact (nupkg) + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory)\nupkg + ArtifactName: wpf-nupkg-$(BuildConfiguration) + +- ${{ if eq(parameters.buildTerminalVPack, true) }}: + - job: VPack + displayName: Create Windows vPack + dependsOn: BundleAndSign + steps: + - checkout: self + clean: true + submodules: true + - task: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + - task: DownloadBuildArtifacts@0 + displayName: Download Build Artifacts + inputs: + artifactName: appxbundle-signed + extractTars: false + - task: PowerShell@2 + displayName: Rename and stage packages for vpack + inputs: + targetType: inline + script: >- + # Rename to known/fixed name for Windows build system + + Get-ChildItem Microsoft.WindowsTerminal_*.msixbundle | Rename-Item -NewName { 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle' } + + + # Create vpack directory and place item inside + + mkdir WindowsTerminal.app + + mv Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle .\WindowsTerminal.app\ + workingDirectory: $(System.ArtifactsDirectory)\appxbundle-signed + - task: PkgESVPack@10 + displayName: 'Package ES - VPack' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + sourceDirectory: $(System.ArtifactsDirectory)\appxbundle-signed\WindowsTerminal.app + description: Windows Terminal pre-install application + pushPkgName: WindowsTerminal.app + owner: condev +... diff --git a/build/pipelines/templates/build-console-int.yml b/build/pipelines/templates/build-console-int.yml deleted file mode 100644 index cc051df3f..000000000 --- a/build/pipelines/templates/build-console-int.yml +++ /dev/null @@ -1,31 +0,0 @@ -parameters: - configuration: 'Release' - platform: '' - additionalBuildArguments: '' - -jobs: -- job: Build${{ parameters.platform }}${{ parameters.configuration }} - displayName: Build ${{ parameters.platform }} ${{ parameters.configuration }} - variables: - BuildConfiguration: ${{ parameters.configuration }} - BuildPlatform: ${{ parameters.platform }} - PGOBuildMode: 'Optimize' - - pool: - name: Package ES Lab E - demands: - - msbuild - - visualstudio - - vstest - - steps: - - task: PkgESSetupBuild@10 - displayName: 'Package ES - Setup Build' - inputs: - useDfs: false - productName: WindowsTerminal - disableOutputRedirect: true - - - template: build-console-steps.yml - parameters: - additionalBuildArguments: "/p:XesUseOneStoreVersioning=true;XesBaseYearForStoreVersion=$(baseYearForVersioning) ${{ parameters.additionalBuildArguments }}" diff --git a/build/pipelines/templates/release-sign-and-bundle.yml b/build/pipelines/templates/release-sign-and-bundle.yml deleted file mode 100644 index ced32a05f..000000000 --- a/build/pipelines/templates/release-sign-and-bundle.yml +++ /dev/null @@ -1,74 +0,0 @@ -parameters: - configuration: 'Release' - -jobs: -- job: SignDeploy${{ parameters.configuration }} - displayName: Sign and Deploy for ${{ parameters.configuration }} - - dependsOn: - - Buildx64AuditMode - - Buildx64Release - - Buildx86Release - - Buildarm64Release - - CodeFormatCheck - condition: | - and - ( - in(dependencies.Buildx64AuditMode.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), - in(dependencies.Buildx64Release.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), - in(dependencies.Buildx86Release.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), - in(dependencies.Buildarm64Release.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), - in(dependencies.CodeFormatCheck.result, 'Succeeded', 'SucceededWithIssues', 'Skipped') - ) - - variables: - BuildConfiguration: ${{ parameters.configuration }} - AppxProjectName: CascadiaPackage - AppxBundleName: Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle - - pool: - name: Package ES Lab E - - steps: - - checkout: self - clean: true - - - task: PkgESSetupBuild@10 - displayName: 'Package ES - Setup Build' - inputs: - useDfs: false - productName: WindowsTerminal - disableOutputRedirect: true - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: 'Component Detection' - - - task: DownloadBuildArtifacts@0 - displayName: Download AppX artifacts - inputs: - artifactName: 'appx-$(BuildConfiguration)' - itemPattern: | - **/*.appx - **/*.msix - downloadPath: '$(Build.ArtifactStagingDirectory)\appx' - - - task: PowerShell@2 - displayName: 'Create $(AppxBundleName)' - inputs: - targetType: filePath - filePath: '.\build\scripts\Create-AppxBundle.ps1' - arguments: | - -InputPath "$(Build.ArtifactStagingDirectory)\appx" -ProjectName $(AppxProjectName) -BundleVersion 0.0.0.0 -OutputPath "$(Build.ArtifactStagingDirectory)\$(AppxBundleName)" - - - task: PkgESCodeSign@10 - displayName: 'Package ES - SignConfig.WindowsTerminal.xml' - inputs: - signConfigXml: 'build\config\SignConfig.WindowsTerminal.xml' - inPathRoot: '$(Build.ArtifactStagingDirectory)' - outPathRoot: '$(Build.ArtifactStagingDirectory)\signed' - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Signed AppX' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)\signed' - ArtifactName: 'appxbundle-signed-$(BuildConfiguration)' From 37e0614554ddc77aee735bf82d0341646d6395be Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Tue, 27 Jul 2021 23:09:56 +0800 Subject: [PATCH 27/90] Optimize hot path in textBufferCellIterator (#10621) ## Summary of the Pull Request ## References The `+=` operator is an extremely hot path under heavily output load. This PR aims to optimize its speed. ## PR Checklist * [ ] Supports #10563 * [ ] 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 * [ ] 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 ## Validation Steps Performed --- .github/actions/spelling/expect/expect.txt | 1 + src/buffer/out/OutputCellView.hpp | 17 +++- src/buffer/out/textBufferCellIterator.cpp | 106 +++++++++++++++++++-- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index e6f4d18b6..3d5b56d5e 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1021,6 +1021,7 @@ IAction IApi IApplication IBase +ICache icacls iccex icch diff --git a/src/buffer/out/OutputCellView.hpp b/src/buffer/out/OutputCellView.hpp index 06c9b86af..c2a7a6e3f 100644 --- a/src/buffer/out/OutputCellView.hpp +++ b/src/buffer/out/OutputCellView.hpp @@ -6,7 +6,7 @@ Module Name: - OutputCellView.hpp Abstract: -- Read-only view into a single cell of data that someone is attempting to write into the output buffer. +- Read view into a single cell of data that someone is attempting to write into the output buffer. - This is done for performance reasons (avoid heap allocs and copies). Author: @@ -36,6 +36,21 @@ public: TextAttribute TextAttr() const noexcept; TextAttributeBehavior TextAttrBehavior() const noexcept; + void UpdateText(const std::wstring_view& view) noexcept + { + _view = view; + }; + + void UpdateDbcsAttribute(const DbcsAttribute& dbcsAttr) noexcept + { + _dbcsAttr = dbcsAttr; + } + + void UpdateTextAttribute(const TextAttribute& textAttr) noexcept + { + _textAttr = textAttr; + } + bool operator==(const OutputCellView& view) const noexcept; bool operator!=(const OutputCellView& view) const noexcept; diff --git a/src/buffer/out/textBufferCellIterator.cpp b/src/buffer/out/textBufferCellIterator.cpp index 64ed7acef..64caf0c98 100644 --- a/src/buffer/out/textBufferCellIterator.cpp +++ b/src/buffer/out/textBufferCellIterator.cpp @@ -94,20 +94,93 @@ bool TextBufferCellIterator::operator!=(const TextBufferCellIterator& it) const // - Reference to self after movement. TextBufferCellIterator& TextBufferCellIterator::operator+=(const ptrdiff_t& movement) { + // Note that this method is called intensively when the terminal is under heavy load. + // We need to aggressively optimize it, comparing to the -= operator. ptrdiff_t move = movement; - auto newPos = _pos; - while (move > 0 && !_exceeded) + if (move < 0) { - _exceeded = !_bounds.IncrementInBounds(newPos); + // Early branching to leave the rare case to -= operator. + // This helps reducing the instruction count within this method, which is good for instruction cache. + return *this -= -move; + } + + // The remaining code in this function is functionally equivalent to: + // auto newPos = _pos; + // while (move > 0 && !_exceeded) + // { + // _exceeded = !_bounds.IncrementInBounds(newPos); + // move--; + // } + // _SetPos(newPos); + // + // _SetPos() necessitates calling _GenerateView() and thus the construction + // of a new OutputCellView(). This has a high performance impact (ICache spill?). + // The code below inlines _bounds.IncrementInBounds as well as SetPos. + // In the hot path (_pos.Y doesn't change) we modify the _view directly. + + // Hoist these integers which will be used frequently later. + const auto boundsRightInclusive = _bounds.RightInclusive(); + const auto boundsLeft = _bounds.Left(); + const auto boundsBottomInclusive = _bounds.BottomInclusive(); + const auto boundsTop = _bounds.Top(); + const auto oldX = _pos.X; + const auto oldY = _pos.Y; + + // Under MSVC writing the individual members of a COORD generates worse assembly + // compared to having them be local variables. This causes a performance impact. + auto newX = oldX; + auto newY = oldY; + + while (move > 0) + { + if (newX == boundsRightInclusive) + { + newX = boundsLeft; + newY++; + if (newY > boundsBottomInclusive) + { + newY = boundsTop; + _exceeded = true; + break; + } + } + else + { + newX++; + _exceeded = false; + } move--; } - while (move < 0 && !_exceeded) + + if (_exceeded) { - _exceeded = !_bounds.DecrementInBounds(newPos); - move++; + // Early return because nothing needs to be done here. + return *this; } - _SetPos(newPos); - return (*this); + + if (newY == oldY) + { + // hot path + const auto diff = gsl::narrow_cast(newX) - gsl::narrow_cast(oldX); + _attrIter += diff; + _view.UpdateTextAttribute(*_attrIter); + + const CharRow& charRow = _pRow->GetCharRow(); + _view.UpdateText(charRow.GlyphAt(newX)); + _view.UpdateDbcsAttribute(charRow.DbcsAttrAt(newX)); + _pos.X = newX; + } + else + { + // cold path (_GenerateView is slow) + _pRow = s_GetRow(_buffer, { newX, newY }); + _attrIter = _pRow->GetAttrRow().cbegin() + newX; + _pos.X = newX; + _pos.Y = newY; + _GenerateView(); + } + + return *this; } // Routine Description: @@ -118,7 +191,22 @@ TextBufferCellIterator& TextBufferCellIterator::operator+=(const ptrdiff_t& move // - Reference to self after movement. TextBufferCellIterator& TextBufferCellIterator::operator-=(const ptrdiff_t& movement) { - return this->operator+=(-movement); + ptrdiff_t move = movement; + if (move < 0) + { + return (*this) += (-move); + } + + auto newPos = _pos; + while (move > 0 && !_exceeded) + { + _exceeded = !_bounds.DecrementInBounds(newPos); + move--; + } + _SetPos(newPos); + + _GenerateView(); + return (*this); } // Routine Description: From 3f5f37d910a2c8fde3bb5123c9516f5f9e38af2b Mon Sep 17 00:00:00 2001 From: Floris Westerman Date: Tue, 27 Jul 2021 19:11:51 +0200 Subject: [PATCH 28/90] Fix: Multimedia Key Hotkey Support (#10801) ## Summary of the Pull Request Fixes/implements #10058 according to directions in that issue: added support for browser navigation keys to be used in actions. ## References ## PR Checklist * [x] Closes #10058 * [x] CLA signed. * [x] Tests added/passed * [x] Documentation updated: . If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: https://github.com/MicrosoftDocs/terminal/pull/371 * [x] Schema updated. * [x] I've discussed this with core contributors already. According to instructions in #10058 ## Detailed Description of the Pull Request / Additional comments The mouse back/forward keys do not correspond to the keys added here. That would be a nice (but more complicated) addition, I'll add an issue for it. --- doc/cascadia/profiles.schema.json | 2 +- .../TerminalSettingsModel/KeyChordSerialization.cpp | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index e2cf5d71c..96e7c6cfc 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -4,7 +4,7 @@ "title": "Microsoft's Windows Terminal Settings Profile Schema", "definitions": { "KeyChordSegment": { - "pattern": "^(?:(?:ctrl|alt|shift|win)\\+)*(?:app|backspace|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))*$", + "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", "description": "The string should fit the format \"[ctrl+][alt+][shift+][win+]\", where each modifier is optional. KeyName is either any single key character, an explicit virtual key or scan code in the form vk(nnn) and sc(nnn) respectively, or one of the special names listed at https://docs.microsoft.com/en-us/windows/terminal/customize-settings/actions#accepted-modifiers-and-keys" }, diff --git a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp index 8e0b7162f..378f92bde 100644 --- a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp @@ -78,7 +78,14 @@ constexpr std::wstring_view WIN_KEY{ L"win" }; XX(VK_OEM_PLUS, L"plus") /* '+' any country */ \ XX(VK_OEM_COMMA, L"comma") /* ',' any country */ \ XX(VK_OEM_MINUS, L"minus") /* '-' any country */ \ - XX(VK_OEM_PERIOD, L"period") /* '.' any country */ + XX(VK_OEM_PERIOD, L"period") /* '.' any country */ \ + XX(VK_BROWSER_BACK, L"browser_back") \ + XX(VK_BROWSER_FORWARD, L"browser_forward") \ + XX(VK_BROWSER_REFRESH, L"browser_refresh") \ + XX(VK_BROWSER_STOP, L"browser_stop") \ + XX(VK_BROWSER_SEARCH, L"browser_search") \ + XX(VK_BROWSER_FAVORITES, L"browser_favorites") \ + XX(VK_BROWSER_HOME, L"browser_home") constexpr std::wstring_view vkeyPrefix{ L"vk(" }; constexpr std::wstring_view scanCodePrefix{ L"sc(" }; From 10222a2ba20255e629322dfdd5274b664b41122b Mon Sep 17 00:00:00 2001 From: Floris Westerman Date: Thu, 29 Jul 2021 00:05:32 +0200 Subject: [PATCH 29/90] Passing through moveFocus keys when moving to another pane failed (#10806) ## Summary of the Pull Request Implementation of #6219 with a small tweak, not just passing the keys when no panes are present, but passing on the keys when there is no other pane to move to. This enables another usecase: 2 panes in terminal split vertically; in one of these panes running tmux with two panes that are split horizontally. This allows the user to still navigate between tmux panes even though they have terminal panes open. ## References Not that I know of ## PR Checklist * [x] Closes #6219 * [x] CLA signed. * [x] Tests added/passed * [x] Documentation updated. I don't think that's necessary * [x] Schema updated. N/A * [ ] 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. ## Detailed Description of the Pull Request / Additional comments Implementation by propagating the boolean indicating success of moving focus all the way to the action handler, where this result will determine whether the action will be considered handled or not. When the action is not handled, the keychord will be propagated to the terminal. ## Validation Steps Performed Manual testing; all relevant unit tests still work --- src/cascadia/TerminalApp/AppActionHandlers.cpp | 7 +++++-- src/cascadia/TerminalApp/TerminalPage.cpp | 8 +++++--- src/cascadia/TerminalApp/TerminalPage.h | 2 +- src/cascadia/TerminalApp/TerminalTab.cpp | 11 ++++++----- src/cascadia/TerminalApp/TerminalTab.h | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 5b045ec98..8535d88fe 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -307,8 +307,11 @@ namespace winrt::TerminalApp::implementation } else { - _MoveFocus(realArgs.FocusDirection()); - args.Handled(true); + // Mark as handled only when the move succeeded (e.g. when there + // is a pane to move to), otherwise mark as unhandled so the + // keychord can propagate to the terminal (GH#6129) + const auto moveSucceeded = _MoveFocus(realArgs.FocusDirection()); + args.Handled(moveSucceeded); } } } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 2fed60f5e..09cc175d6 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1123,14 +1123,16 @@ namespace winrt::TerminalApp::implementation // Arguments: // - direction: The direction to move the focus in. // Return Value: - // - - void TerminalPage::_MoveFocus(const FocusDirection& direction) + // - Whether changing the focus succeeded. This allows a keychord to propagate + // to the terminal when no other panes are present (GH#6219) + bool TerminalPage::_MoveFocus(const FocusDirection& direction) { if (const auto terminalTab{ _GetFocusedTabImpl() }) { _UnZoomIfNeeded(); - terminalTab->NavigateFocus(direction); + return terminalTab->NavigateFocus(direction); } + return false; } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index c93835054..ea068a894 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -234,7 +234,7 @@ namespace winrt::TerminalApp::implementation void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); bool _SelectTab(uint32_t tabIndex); - void _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); void _MovePane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 559fb959e..9590fcd3f 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -481,23 +481,24 @@ namespace winrt::TerminalApp::implementation // Arguments: // - direction: The direction to move the focus in. // Return Value: - // - - void TerminalTab::NavigateFocus(const FocusDirection& direction) + // - Whether changing the focus succeeded. This allows a keychord to propagate + // to the terminal when no other panes are present (GH#6219) + bool TerminalTab::NavigateFocus(const FocusDirection& direction) { if (direction == FocusDirection::Previous) { if (_mruPanes.size() < 2) { - return; + return false; } // To get to the previous pane, get the id of the previous pane and focus to that - _rootPane->FocusPane(_mruPanes.at(1)); + return _rootPane->FocusPane(_mruPanes.at(1)); } else { // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. - _rootPane->NavigateFocus(direction); + return _rootPane->NavigateFocus(direction); } } diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index dfc9ff676..68c041ab1 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -52,7 +52,7 @@ namespace winrt::TerminalApp::implementation void ResizeContent(const winrt::Windows::Foundation::Size& newSize); void ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); - void NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); void MovePane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool FocusPane(const uint32_t id); From b1bcc59230311a915f7da50c754991e870b7c026 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 28 Jul 2021 17:15:22 -0500 Subject: [PATCH 30/90] Shift the island up by 1px when maximized (#10746) For inexplicable reasons, the top row of pixels on our tabs, new tab button, and caption buttons is totally unclickable. The mouse simply refuses to interact with them. So when we're maximized, on certain monitor configurations, this results in the top row of pixels not reacting to clicks at all. To obey Fitt's Law, we're gonna hackily shift the entire island up one pixel. That will result in the top row of pixels in the window actually being the _second_ row of pixels for those buttons, which will make them clickable. It's perhaps not the right fix, but it works. After discussion, we think this is a fine fix for this. We don't think anyone's going to miss the top row of pixels on the TabView. The original bug is painful enough for the subset of users it impacts that this is an acceptable trade. Should a better fix be found, we can absolutely do that instead. Closes #7422 --- .github/actions/spelling/allow/allow.txt | 1 + .../WindowsTerminal/NonClientIslandWindow.cpp | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 4ca9009f8..035a98148 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -15,6 +15,7 @@ downsides dze dzhe Enum'd +Fitt formattings ftp fvar diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index eb4e6400e..a1f5e6801 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -360,7 +360,21 @@ void NonClientIslandWindow::_OnMaximizeChange() noexcept // sizes of our child XAML Islands to match our new sizing. void NonClientIslandWindow::_UpdateIslandPosition(const UINT windowWidth, const UINT windowHeight) { - const auto topBorderHeight = Utils::ClampToShortMax(_GetTopBorderHeight(), 0); + const auto originalTopHeight = _GetTopBorderHeight(); + // GH#7422 + // !! BODGY !! + // + // For inexplicable reasons, the top row of pixels on our tabs, new tab + // button, and caption buttons is totally un-clickable. The mouse simply + // refuses to interact with them. So when we're maximized, on certain + // monitor configurations, this results in the top row of pixels not + // reacting to clicks at all. To obey Fitt's Law, we're gonna shift + // the entire island up one pixel. That will result in the top row of pixels + // in the window actually being the _second_ row of pixels for those + // buttons, which will make them clickable. It's perhaps not the right fix, + // but it works. + // _GetTopBorderHeight() returns 0 when we're maximized. + const short topBorderHeight = ::base::saturated_cast((originalTopHeight == 0) ? -1 : originalTopHeight); const COORD newIslandPos = { 0, topBorderHeight }; From f058b08fdebcbe127640482c704f56a6731f27a0 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 28 Jul 2021 17:18:58 -0500 Subject: [PATCH 31/90] Account for the window borders when restoring from fullscreen (#10737) ## Summary of the Pull Request When we're restoring from fullscreen, we do a little adjustment to make sure to clamp the window bounds within the bounds of the active monitor. We unfortunately didn't account for the size of the non-client area (the invisible borders around our 1px border). This didn't matter most of the time, but if the window was within ~8px of the side of the monitor (any side), then restoring from fullscreen would actually move it to the wrong place. As it turns out, the `_quake` window is within ~8px of the edges of the monitor _very often_. ## References * regressed in #9737 ## PR Checklist * [x] Closes #10199 * [x] I work here * [ ] Tests added/passed * [n/a] Requires documentation to be updated ## Validation Steps Performed The repro in the bug was fairly straightforward. It doesn't happen anymore. --- src/cascadia/WindowsTerminal/IslandWindow.cpp | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 230b9f6bf..27ed12f1e 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -884,23 +884,39 @@ void IslandWindow::_RestoreFullscreenPosition(const RECT rcWork) rcWork.left - _rcWorkBeforeFullscreen.left, rcWork.top - _rcWorkBeforeFullscreen.top); + const til::size ncSize{ GetTotalNonClientExclusiveSize(dpiWindow) }; + + RECT rcWorkAdjusted = rcWork; + + // GH#10199 - adjust the size of the "work" rect by the size of our borders. + // We want to make sure the window is restored within the bounds of the + // monitor we're on, but it's totally fine if the invisible borders are + // outside the monitor. + const auto halfWidth{ ncSize.width() / 2 }; + const auto halfHeight{ ncSize.height() / 2 }; + + rcWorkAdjusted.left -= halfWidth; + rcWorkAdjusted.right += halfWidth; + rcWorkAdjusted.top -= halfHeight; + rcWorkAdjusted.bottom += halfHeight; + // Enforce that our position is entirely within the bounds of our work area. // Prefer the top-left be on-screen rather than bottom-right (right before left, bottom before top). - if (rcRestore.right > rcWork.right) + if (rcRestore.right > rcWorkAdjusted.right) { - OffsetRect(&rcRestore, rcWork.right - rcRestore.right, 0); + OffsetRect(&rcRestore, rcWorkAdjusted.right - rcRestore.right, 0); } - if (rcRestore.left < rcWork.left) + if (rcRestore.left < rcWorkAdjusted.left) { - OffsetRect(&rcRestore, rcWork.left - rcRestore.left, 0); + OffsetRect(&rcRestore, rcWorkAdjusted.left - rcRestore.left, 0); } - if (rcRestore.bottom > rcWork.bottom) + if (rcRestore.bottom > rcWorkAdjusted.bottom) { - OffsetRect(&rcRestore, 0, rcWork.bottom - rcRestore.bottom); + OffsetRect(&rcRestore, 0, rcWorkAdjusted.bottom - rcRestore.bottom); } - if (rcRestore.top < rcWork.top) + if (rcRestore.top < rcWorkAdjusted.top) { - OffsetRect(&rcRestore, 0, rcWork.top - rcRestore.top); + OffsetRect(&rcRestore, 0, rcWorkAdjusted.top - rcRestore.top); } // Show the window at the computed position. From 4b45bb8df1f90e6fc463172cb861bf968e162e77 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 28 Jul 2021 17:27:09 -0500 Subject: [PATCH 32/90] Fix a pair of TermControl dragging bugs (#10650) ## Summary of the Pull Request This fixes two bugs related to dragging into the bounds of the `TermControl`. Although the fixes are fairly small, I'm batching them up, because I don't want to stack 2 more PRs on top of #10051. * #9109 - This is fixed by only starting an autoscroll if the click&drag actually started within the bounds of the control. * #4603 - Building on the above change, only modify the selection when the drag started in the control. ## References * srsly go read #10051. ## PR Checklist * [x] Closes #9109 * [x] Closes #4603 * [x] I work here * [x] Test added * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments This is kind of annoying that the auto-scrolling is handled by the TermControl, but it uses a timer that's still a WinUI construct. We only want to start the auto-scrolling behavior when the drag started _inside_ the control. Otherwise, in the tab drag scenario, dragging into the bounds of the TermControl will trick it into thinking it should start a scroll. --- .../TerminalControl/ControlInteractivity.cpp | 9 +- .../TerminalControl/ControlInteractivity.h | 3 +- .../TerminalControl/ControlInteractivity.idl | 4 +- src/cascadia/TerminalControl/TermControl.cpp | 19 +++- src/cascadia/TerminalControl/TermControl.h | 4 +- .../ControlInteractivityTests.cpp | 90 +++++++++++++++++-- 6 files changed, 115 insertions(+), 14 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index 56c668698..dc3f61490 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -279,7 +279,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation const unsigned int pointerUpdateKind, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool focused, - const til::point pixelPosition) + const til::point pixelPosition, + const bool pointerPressedInBounds) { const til::point terminalPosition = _getTerminalPosition(pixelPosition); @@ -288,7 +289,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); } - else if (focused && WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown)) + // GH#4603 - don't modify the selection if the pointer press didn't + // actually start _in_ the control bounds. Case in point - someone drags + // a file into the bounds of the control. That shouldn't send the + // selection into space. + else if (focused && pointerPressedInBounds && WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown)) { if (_singleClickTouchdownPos) { diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index 1c86a52d6..937534838 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -58,7 +58,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation const unsigned int pointerUpdateKind, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool focused, - const til::point pixelPosition); + const til::point pixelPosition, + const bool pointerPressedInBounds); void TouchMoved(const til::point newTouchPoint, const bool focused); diff --git a/src/cascadia/TerminalControl/ControlInteractivity.idl b/src/cascadia/TerminalControl/ControlInteractivity.idl index 385eefdbd..6261b773d 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.idl +++ b/src/cascadia/TerminalControl/ControlInteractivity.idl @@ -39,7 +39,9 @@ namespace Microsoft.Terminal.Control UInt32 pointerUpdateKind, Microsoft.Terminal.Core.ControlKeyStates modifiers, Boolean focused, - Microsoft.Terminal.Core.Point pixelPosition); + Microsoft.Terminal.Core.Point pixelPosition, + Boolean pointerPressedInBounds); + void TouchMoved(Microsoft.Terminal.Core.Point newTouchPoint, Boolean focused); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 435dbc178..e1ea1d7a0 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1052,6 +1052,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation Focus(FocusState::Pointer); } + // Mark that this pointer event actually started within our bounds. + // We'll need this later, for PointerMoved events. + _pointerPressedInBounds = true; + if (type == Windows::Devices::Input::PointerDeviceType::Touch) { const auto contactRect = point.Properties().ContactRect(); @@ -1104,10 +1108,19 @@ namespace winrt::Microsoft::Terminal::Control::implementation TermControl::GetPointerUpdateKind(point), ControlKeyStates(args.KeyModifiers()), _focused, - pixelPosition); + pixelPosition, + _pointerPressedInBounds); - if (_focused && point.Properties().IsLeftButtonPressed()) + // GH#9109 - Only start an auto-scroll when the drag actually + // started within our bounds. Otherwise, someone could start a drag + // outside the terminal control, drag into the padding, and trick us + // into starting to scroll. + if (_focused && _pointerPressedInBounds && point.Properties().IsLeftButtonPressed()) { + // We want to find the distance relative to the bounds of the + // SwapChainPanel, not the entire control. If they drag out of + // the bounds of the text, into the padding, we still what that + // to auto-scroll const double cursorBelowBottomDist = cursorPosition.Y - SwapChainPanel().Margin().Top - SwapChainPanel().ActualHeight(); const double cursorAboveTopDist = -1 * cursorPosition.Y + SwapChainPanel().Margin().Top; @@ -1157,6 +1170,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } + _pointerPressedInBounds = false; + const auto ptr = args.Pointer(); const auto point = args.GetCurrentPoint(*this); const auto cursorPosition = point.Position(); diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index a074e020e..6c9aa6757 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -167,11 +167,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::shared_ptr> _updateScrollBar; bool _isInternalScrollBarUpdate; - // Auto scroll occurs when user, while selecting, drags cursor outside viewport. View is then scrolled to 'follow' the cursor. + // Auto scroll occurs when user, while selecting, drags cursor outside + // viewport. View is then scrolled to 'follow' the cursor. double _autoScrollVelocity; std::optional _autoScrollingPointerPoint; Windows::UI::Xaml::DispatcherTimer _autoScrollTimer; std::optional _lastAutoScrollUpdateTime; + bool _pointerPressedInBounds{ false }; winrt::Windows::UI::Composition::ScalarKeyFrameAnimation _bellLightAnimation{ nullptr }; Windows::UI::Xaml::DispatcherTimer _bellLightTimer{ nullptr }; diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp index bf6eaede1..14801722e 100644 --- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -32,6 +32,9 @@ namespace ControlUnitTests TEST_METHOD(ScrollWithSelection); TEST_METHOD(TestScrollWithTrackpad); TEST_METHOD(TestQuickDragOnSelect); + + TEST_METHOD(TestDragSelectOutsideBounds); + TEST_METHOD(PointerClickOutsideActiveRegion); TEST_CLASS_SETUP(ClassSetup) @@ -288,7 +291,8 @@ namespace ControlUnitTests WM_LBUTTONDOWN, //pointerUpdateKind modifiers, true, // focused, - cursorPosition1); + cursorPosition1, + true); Log::Comment(L"Verify that there's one selection"); VERIFY_IS_TRUE(core->HasSelection()); VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size()); @@ -300,7 +304,8 @@ namespace ControlUnitTests WM_LBUTTONDOWN, //pointerUpdateKind modifiers, true, // focused, - cursorPosition2); + cursorPosition2, + true); Log::Comment(L"Verify that there's now two selections (one on each row)"); VERIFY_IS_TRUE(core->HasSelection()); VERIFY_ARE_EQUAL(2u, core->_terminal->GetSelectionRects().size()); @@ -333,7 +338,8 @@ namespace ControlUnitTests WM_LBUTTONDOWN, //pointerUpdateKind modifiers, true, // focused, - cursorPosition4); + cursorPosition4, + true); Log::Comment(L"Verify that there's now one selection"); VERIFY_IS_TRUE(core->HasSelection()); VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size()); @@ -388,7 +394,8 @@ namespace ControlUnitTests WM_LBUTTONDOWN, //pointerUpdateKind modifiers, true, // focused, - cursorPosition1); + cursorPosition1, + true); Log::Comment(L"Verify that there's one selection"); VERIFY_IS_TRUE(core->HasSelection()); VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size()); @@ -536,7 +543,8 @@ namespace ControlUnitTests WM_LBUTTONDOWN, //pointerUpdateKind modifiers, true, // focused, - cursorPosition1); + cursorPosition1, + true); Log::Comment(L"Verify that there's one selection"); VERIFY_IS_TRUE(core->HasSelection()); VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size()); @@ -546,6 +554,74 @@ namespace ControlUnitTests VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor()); } + void ControlInteractivityTests::TestDragSelectOutsideBounds() + { + // This is a test for GH#4603 + + auto [settings, conn] = _createSettingsAndConnection(); + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + _standardInit(core, interactivity); + + // For this test, don't use any modifiers + const auto modifiers = ControlKeyStates(); + const Control::MouseButtonState leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown }; + const Control::MouseButtonState noMouseDown{}; + + const til::size fontSize{ 9, 21 }; + Log::Comment(L"Click on the terminal"); + const til::point cursorPosition0{ 6, 0 }; + interactivity->PointerPressed(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + 0, // timestamp + modifiers, + cursorPosition0); + + Log::Comment(L"Verify that there's not yet a selection"); + VERIFY_IS_FALSE(core->HasSelection()); + + VERIFY_IS_TRUE(interactivity->_singleClickTouchdownPos.has_value()); + VERIFY_ARE_EQUAL(cursorPosition0, interactivity->_singleClickTouchdownPos.value()); + + Log::Comment(L"Drag the mouse a lot. This simulates dragging the mouse real fast."); + const til::point cursorPosition1{ 6 + fontSize.width() * 2, 0 }; + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition1, + true); + Log::Comment(L"Verify that there's one selection"); + VERIFY_IS_TRUE(core->HasSelection()); + VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size()); + + Log::Comment(L"Verify that it started on the first cell we clicked on, not the one we dragged to"); + COORD expectedAnchor{ 0, 0 }; + VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor()); + COORD expectedEnd{ 2, 0 }; + VERIFY_ARE_EQUAL(expectedEnd, core->_terminal->GetSelectionEnd()); + + interactivity->PointerReleased(noMouseDown, + WM_LBUTTONUP, + modifiers, + cursorPosition1); + + VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor()); + VERIFY_ARE_EQUAL(expectedEnd, core->_terminal->GetSelectionEnd()); + + Log::Comment(L"Simulate dragging the mouse into the control, without first clicking into the control"); + const til::point cursorPosition2{ fontSize.width() * 10, 0 }; + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition2, + false); + + Log::Comment(L"The selection should be unchanged."); + VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor()); + VERIFY_ARE_EQUAL(expectedEnd, core->_terminal->GetSelectionEnd()); + } + void ControlInteractivityTests::PointerClickOutsideActiveRegion() { // This is a test for GH#10642 @@ -561,7 +637,6 @@ namespace ControlUnitTests const Control::MouseButtonState noMouseDown{}; const til::size fontSize{ 9, 21 }; - interactivity->_rowsToScroll = 1; int expectedTop = 0; int expectedViewHeight = 20; @@ -630,7 +705,8 @@ namespace ControlUnitTests WM_LBUTTONDOWN, //pointerUpdateKind modifiers, true, // focused, - cursorPosition1); + cursorPosition1, + true); Log::Comment(L"Verify that there's still no selection"); VERIFY_IS_FALSE(core->HasSelection()); } From 34a6b1913ca978849d93116e84bdc49691e5c54e Mon Sep 17 00:00:00 2001 From: Ian O'Neill Date: Mon, 2 Aug 2021 19:44:39 +0100 Subject: [PATCH 33/90] Set drag and drop on '+' tooltip text based on keyboard modifiers (#10841) Sets the tooltip text on the '+' button based on the keyboard modifiers when dragging and dropping. ## Validation Steps Performed Manually tested - dragged a directory onto the '+ button and saw that * The text changed when `shift` was pressed * The text changed when `alt` was pressed * The text changed back when `shift` or `alt` were released Closes #10722 --- .../TerminalApp/Resources/en-US/Resources.resw | 8 +++++++- src/cascadia/TerminalApp/TabRowControl.cpp | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index f2de64480..9fa780a3a 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -662,4 +662,10 @@ Open a new tab in given starting directory - + + Open a new window with given starting directory + + + Split the window and start in given directory + + \ No newline at end of file diff --git a/src/cascadia/TerminalApp/TabRowControl.cpp b/src/cascadia/TerminalApp/TabRowControl.cpp index 6cd9cd8ad..fa9a5ff98 100644 --- a/src/cascadia/TerminalApp/TabRowControl.cpp +++ b/src/cascadia/TerminalApp/TabRowControl.cpp @@ -61,8 +61,19 @@ namespace winrt::TerminalApp::implementation // Make sure to set the AcceptedOperation, so that we can later receive the path in the Drop event e.AcceptedOperation(DataPackageOperation::Copy); - // Sets custom UI text - e.DragUIOverride().Caption(RS_(L"DropPathTabRun/Text")); + const auto modifiers = static_cast(e.Modifiers()); + if (WI_IsFlagSet(modifiers, static_cast(DragDrop::DragDropModifiers::Alt))) + { + e.DragUIOverride().Caption(RS_(L"DropPathTabSplit/Text")); + } + else if (WI_IsFlagSet(modifiers, static_cast(DragDrop::DragDropModifiers::Shift))) + { + e.DragUIOverride().Caption(RS_(L"DropPathTabNewWindow/Text")); + } + else + { + e.DragUIOverride().Caption(RS_(L"DropPathTabRun/Text")); + } // Sets if the caption is visible e.DragUIOverride().IsCaptionVisible(true); From fc64ff3029dbd88fb0b6270f72c3be51661b94b4 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 2 Aug 2021 21:02:59 +0200 Subject: [PATCH 34/90] Vectorize TextColor::GetColor (#10779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I was watching a video about vectorized instructions and I wanted to try out some new things, as I had never written AVX code before. This commit is the result of this tiny Thursday morning detour into AVX land. It improves performance of `TextColor::GetColor` by about 3x. ## Validation Steps Performed * Default colors are still properly shifted +8 ✔️ --- .github/actions/spelling/expect/expect.txt | 6 ++ src/buffer/out/TextAttribute.cpp | 2 +- src/buffer/out/TextAttribute.hpp | 2 +- src/buffer/out/TextColor.cpp | 66 +++++++++++++-- src/buffer/out/TextColor.h | 7 +- src/buffer/out/lib/bufferout.vcxproj | 2 +- .../out/ut_textbuffer/TextAttributeTests.cpp | 71 +++++++--------- .../out/ut_textbuffer/TextColorTests.cpp | 83 ++++++++----------- .../TerminalCore/terminalrenderdata.cpp | 11 +-- src/host/consoleInformation.cpp | 11 +-- src/host/settings.cpp | 10 --- src/host/settings.hpp | 8 +- src/host/telemetry.cpp | 4 +- 13 files changed, 154 insertions(+), 129 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 3d5b56d5e..94f8fc911 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -105,6 +105,7 @@ autoscrolling Autowrap AVerify AVI +AVX awch azuredevopspodcast azzle @@ -190,6 +191,7 @@ CARETBLINKINGENABLED CARRIAGERETURN cascadia cassert +castsi catid cazamor CBash @@ -270,6 +272,7 @@ Cmdlet cmdline CMOUSEBUTTONS cmp +cmpeq cmt cmyk CNL @@ -1261,6 +1264,7 @@ lnkd lnkfile LNM LOADONCALL +loadu LOBYTE localappdata localhost @@ -1421,6 +1425,7 @@ MOUSEFIRST MOUSEHWHEEL MOUSEMOVE mousewheel +movemask MOVESTART msb msbuild @@ -2531,6 +2536,7 @@ vcvarsall vcxitems vcxproj vec +vectorized VERCTRL versioning VERTBAR diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index 2791bc352..c406f10ba 100644 --- a/src/buffer/out/TextAttribute.cpp +++ b/src/buffer/out/TextAttribute.cpp @@ -98,7 +98,7 @@ bool TextAttribute::IsLegacy() const noexcept // - blinkingIsFaint: true if blinking should be interpreted as faint. // Return Value: // - the foreground and background colors that should be displayed. -std::pair TextAttribute::CalculateRgbColors(const gsl::span colorTable, +std::pair TextAttribute::CalculateRgbColors(const std::array& colorTable, const COLORREF defaultFgColor, const COLORREF defaultBgColor, const bool reverseScreenMode, diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp index e99360da2..8bb1ff243 100644 --- a/src/buffer/out/TextAttribute.hpp +++ b/src/buffer/out/TextAttribute.hpp @@ -64,7 +64,7 @@ public: static TextAttribute StripErroneousVT16VersionsOfLegacyDefaults(const TextAttribute& attribute) noexcept; WORD GetLegacyAttributes() const noexcept; - std::pair CalculateRgbColors(const gsl::span colorTable, + std::pair CalculateRgbColors(const std::array& colorTable, const COLORREF defaultFgColor, const COLORREF defaultBgColor, const bool reverseScreenMode = false, diff --git a/src/buffer/out/TextColor.cpp b/src/buffer/out/TextColor.cpp index 204050229..5a628384d 100644 --- a/src/buffer/out/TextColor.cpp +++ b/src/buffer/out/TextColor.cpp @@ -50,6 +50,9 @@ constexpr std::array Index256ToIndex16 = { // clang-format on +// We should only need 4B for TextColor. Any more than that is just waste. +static_assert(sizeof(TextColor) == 4); + bool TextColor::CanBeBrightened() const noexcept { return IsIndex16() || IsDefault(); @@ -138,15 +141,12 @@ void TextColor::SetDefault() noexcept // - brighten: if true, we'll brighten a dark color table index. // Return Value: // - a COLORREF containing the real value of this TextColor. -COLORREF TextColor::GetColor(gsl::span colorTable, - const COLORREF defaultColor, - bool brighten) const noexcept +COLORREF TextColor::GetColor(const std::array& colorTable, const COLORREF defaultColor, bool brighten) const noexcept { if (IsDefault()) { if (brighten) { - FAIL_FAST_IF(colorTable.size() < 16); // See MSFT:20266024 for context on this fix. // Additionally todo MSFT:20271956 to fix this better for 19H2+ // If we're a default color, check to see if the defaultColor exists @@ -156,6 +156,58 @@ COLORREF TextColor::GetColor(gsl::span colorTable, // (Settings::_DefaultForeground==INVALID_COLOR, and the index // from _wFillAttribute is being used instead.) // If we find a match, return instead the bright version of this color + + static_assert(sizeof(COLORREF) * 8 == 32, "The vectorized code broke. If you can't fix COLORREF, just remove the vectorized code."); + +#pragma warning(push) +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). +#ifdef __AVX2__ + // I wrote this vectorized code one day, because the sun was shining so nicely. + // There's no other reason for this to exist here, except for being pretty. + // This code implements the exact same for loop you can find below, but is ~3x faster. + // + // A brief explanation for people unfamiliar with vectorized instructions: + // Vectorized instructions, like "SSE" or "AVX", allow you to run + // common operations like additions, multiplications, comparisons, + // or bitwise operations concurrently on multiple values at once. + // + // We want to find the given defaultColor in the first 8 values of colorTable. + // Coincidentally a COLORREF is a DWORD and 8 of them are exactly 256 bits. + // -- The size of a single AVX register. + // + // Thus, the code works like this: + // 1. Load all 8 DWORDs at once into one register + // 2. Set the same defaultColor 8 times in another register + // 3. Compare all 8 values at once + // The result is either 0xffffffff or 0x00000000. + // 4. Extract the most significant bit of each DWORD + // Assuming that no duplicate colors exist in colorTable, + // the result will be something like 0b00100000. + // 5. Use BitScanForward (bsf) to find the index of the most significant 1 bit. + const auto haystack = _mm256_loadu_si256(reinterpret_cast(colorTable.data())); // 1. + const auto needle = _mm256_set1_epi32(__builtin_bit_cast(int, defaultColor)); // 2. + const auto result = _mm256_cmpeq_epi32(haystack, needle); // 3. + const auto mask = _mm256_movemask_ps(_mm256_castsi256_ps(result)); // 4. + unsigned long index; + return _BitScanForward(&index, mask) ? til::at(colorTable, static_cast(index) + 8) : defaultColor; // 5. +#elif _M_AMD64 + // If you look closely this SSE2 algorithm is the exact same as the AVX one. + // The two differences are that we need to: + // * do everything twice, because SSE is limited to 128 bits and not 256. + // * use _mm_packs_epi32 to merge two 128 bits vectors into one in step 3.5. + // _mm_packs_epi32 takes two SSE registers and truncates all 8 DWORDs into 8 WORDs, + // the latter of which fits into a single register (which is then used in the identical step 4). + const auto haystack1 = _mm_loadu_si128(reinterpret_cast(colorTable.data() + 0)); + const auto haystack2 = _mm_loadu_si128(reinterpret_cast(colorTable.data() + 4)); + const auto needle = _mm_set1_epi32(__builtin_bit_cast(int, defaultColor)); + const auto result1 = _mm_cmpeq_epi32(haystack1, needle); + const auto result2 = _mm_cmpeq_epi32(haystack2, needle); + const auto result = _mm_packs_epi32(result1, result2); // 3.5 + const auto mask = _mm_movemask_ps(_mm_castsi128_ps(result)); + unsigned long index; + return _BitScanForward(&index, mask) ? til::at(colorTable, static_cast(index) + 8) : defaultColor; +#else for (size_t i = 0; i < 8; i++) { if (til::at(colorTable, i) == defaultColor) @@ -163,6 +215,8 @@ COLORREF TextColor::GetColor(gsl::span colorTable, return til::at(colorTable, i + 8); } } +#endif +#pragma warning(pop) } return defaultColor; @@ -199,7 +253,7 @@ BYTE TextColor::GetLegacyIndex(const BYTE defaultIndex) const noexcept } else if (IsIndex256()) { - return Index256ToIndex16.at(GetIndex()); + return til::at(Index256ToIndex16, GetIndex()); } else { @@ -208,7 +262,7 @@ BYTE TextColor::GetLegacyIndex(const BYTE defaultIndex) const noexcept const BYTE compressedRgb = (_red & 0b11100000) + ((_green >> 3) & 0b00011100) + ((_blue >> 6) & 0b00000011); - return CompressedRgbToIndex16.at(compressedRgb); + return til::at(CompressedRgbToIndex16, compressedRgb); } } diff --git a/src/buffer/out/TextColor.h b/src/buffer/out/TextColor.h index e8dcfcd82..ad984702f 100644 --- a/src/buffer/out/TextColor.h +++ b/src/buffer/out/TextColor.h @@ -86,10 +86,7 @@ public: void SetIndex(const BYTE index, const bool isIndex256) noexcept; void SetDefault() noexcept; - COLORREF GetColor(gsl::span colorTable, - const COLORREF defaultColor, - const bool brighten = false) const noexcept; - + COLORREF GetColor(const std::array& colorTable, const COLORREF defaultColor, bool brighten = false) const noexcept; BYTE GetLegacyIndex(const BYTE defaultIndex) const noexcept; constexpr BYTE GetIndex() const noexcept @@ -157,5 +154,3 @@ namespace WEX } } #endif - -static_assert(sizeof(TextColor) <= 4 * sizeof(BYTE), "We should only need 4B for an entire TextColor. Any more than that is just waste"); diff --git a/src/buffer/out/lib/bufferout.vcxproj b/src/buffer/out/lib/bufferout.vcxproj index 2a5ea58de..cfe899c73 100644 --- a/src/buffer/out/lib/bufferout.vcxproj +++ b/src/buffer/out/lib/bufferout.vcxproj @@ -44,7 +44,7 @@ - + diff --git a/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp b/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp index 44acc3551..d0f0362d3 100644 --- a/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp +++ b/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp @@ -23,11 +23,9 @@ class TextAttributeTests TEST_METHOD(TestReverseDefaultColors); TEST_METHOD(TestRoundtripDefaultColors); - static const int COLOR_TABLE_SIZE = 16; - COLORREF _colorTable[COLOR_TABLE_SIZE]; + std::array _colorTable; COLORREF _defaultFg = RGB(1, 2, 3); COLORREF _defaultBg = RGB(4, 5, 6); - gsl::span _GetTableView(); }; bool TextAttributeTests::ClassSetup() @@ -51,11 +49,6 @@ bool TextAttributeTests::ClassSetup() return true; } -gsl::span TextAttributeTests::_GetTableView() -{ - return gsl::span(&_colorTable[0], COLOR_TABLE_SIZE); -} - void TextAttributeTests::TestRoundtripLegacy() { WORD expectedLegacy = FOREGROUND_BLUE | BACKGROUND_RED; @@ -133,23 +126,22 @@ void TextAttributeTests::TestTextAttributeColorGetters() const COLORREF faintRed = RGB(127, 0, 0); const COLORREF green = RGB(0, 255, 0); TextAttribute attr(red, green); - auto view = _GetTableView(); // verify that calculated foreground/background are the same as the direct // values when reverse video is not set VERIFY_IS_FALSE(attr.IsReverseVideo()); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(red, green), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(red, green), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // with reverse video set, calculated foreground/background values should be // switched while getters stay the same attr.SetReverseVideo(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(green, red), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(green, red), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // reset the reverse video attr.SetReverseVideo(false); @@ -158,17 +150,17 @@ void TextAttributeTests::TestTextAttributeColorGetters() // while the background and getters stay the same attr.SetFaint(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(faintRed, green), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(faintRed, green), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // with reverse video set, calculated foreground/background values should be // switched, and the background fainter, while getters stay the same attr.SetReverseVideo(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(green, faintRed), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(green, faintRed), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // reset the reverse video and faint attributes attr.SetReverseVideo(false); @@ -178,17 +170,17 @@ void TextAttributeTests::TestTextAttributeColorGetters() // background, while getters stay the same attr.SetInvisible(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(green, green), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(green, green), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // with reverse video set, the calculated background value should match // the foreground, while getters stay the same attr.SetReverseVideo(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(red, red), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(red, red), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); } void TextAttributeTests::TestReverseDefaultColors() @@ -196,40 +188,39 @@ void TextAttributeTests::TestReverseDefaultColors() const COLORREF red = RGB(255, 0, 0); const COLORREF green = RGB(0, 255, 0); TextAttribute attr{}; - auto view = _GetTableView(); // verify that calculated foreground/background are the same as the direct // values when reverse video is not set VERIFY_IS_FALSE(attr.IsReverseVideo()); - VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // with reverse video set, calculated foreground/background values should be // switched while getters stay the same attr.SetReverseVideo(true); VERIFY_IS_TRUE(attr.IsReverseVideo()); - VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(_defaultBg, _defaultFg), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultBg, _defaultFg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); attr.SetForeground(red); VERIFY_IS_TRUE(attr.IsReverseVideo()); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(_defaultBg, red), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultBg, red), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); attr.Invert(); VERIFY_IS_FALSE(attr.IsReverseVideo()); attr.SetDefaultForeground(); attr.SetBackground(green); - VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, green), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, green), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); } void TextAttributeTests::TestRoundtripDefaultColors() diff --git a/src/buffer/out/ut_textbuffer/TextColorTests.cpp b/src/buffer/out/ut_textbuffer/TextColorTests.cpp index 8d068658b..d419bfc33 100644 --- a/src/buffer/out/ut_textbuffer/TextColorTests.cpp +++ b/src/buffer/out/ut_textbuffer/TextColorTests.cpp @@ -23,11 +23,9 @@ class TextColorTests TEST_METHOD(TestRgbColor); TEST_METHOD(TestChangeColor); - static const int COLOR_TABLE_SIZE = 16; - COLORREF _colorTable[COLOR_TABLE_SIZE]; + std::array _colorTable; COLORREF _defaultFg = RGB(1, 2, 3); COLORREF _defaultBg = RGB(4, 5, 6); - gsl::span _GetTableView(); }; bool TextColorTests::ClassSetup() @@ -51,11 +49,6 @@ bool TextColorTests::ClassSetup() return true; } -gsl::span TextColorTests::_GetTableView() -{ - return gsl::span(&_colorTable[0], COLOR_TABLE_SIZE); -} - void TextColorTests::TestDefaultColor() { TextColor defaultColor; @@ -64,18 +57,16 @@ void TextColorTests::TestDefaultColor() VERIFY_IS_FALSE(defaultColor.IsLegacy()); VERIFY_IS_FALSE(defaultColor.IsRgb()); - auto view = _GetTableView(); - - auto color = defaultColor.GetColor(view, _defaultFg, false); + auto color = defaultColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_defaultFg, color); - color = defaultColor.GetColor(view, _defaultFg, true); + color = defaultColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_defaultFg, color); - color = defaultColor.GetColor(view, _defaultBg, false); + color = defaultColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_defaultBg, color); - color = defaultColor.GetColor(view, _defaultBg, true); + color = defaultColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_defaultBg, color); } @@ -87,18 +78,16 @@ void TextColorTests::TestDarkIndexColor() VERIFY_IS_TRUE(indexColor.IsLegacy()); VERIFY_IS_FALSE(indexColor.IsRgb()); - auto view = _GetTableView(); - - auto color = indexColor.GetColor(view, _defaultFg, false); + auto color = indexColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_colorTable[7], color); - color = indexColor.GetColor(view, _defaultFg, true); + color = indexColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = indexColor.GetColor(view, _defaultBg, false); + color = indexColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_colorTable[7], color); - color = indexColor.GetColor(view, _defaultBg, true); + color = indexColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); } @@ -110,18 +99,16 @@ void TextColorTests::TestBrightIndexColor() VERIFY_IS_TRUE(indexColor.IsLegacy()); VERIFY_IS_FALSE(indexColor.IsRgb()); - auto view = _GetTableView(); - - auto color = indexColor.GetColor(view, _defaultFg, false); + auto color = indexColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = indexColor.GetColor(view, _defaultFg, true); + color = indexColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = indexColor.GetColor(view, _defaultBg, false); + color = indexColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = indexColor.GetColor(view, _defaultBg, true); + color = indexColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); } @@ -134,18 +121,16 @@ void TextColorTests::TestRgbColor() VERIFY_IS_FALSE(rgbColor.IsLegacy()); VERIFY_IS_TRUE(rgbColor.IsRgb()); - auto view = _GetTableView(); - - auto color = rgbColor.GetColor(view, _defaultFg, false); + auto color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(myColor, color); } @@ -158,57 +143,55 @@ void TextColorTests::TestChangeColor() VERIFY_IS_FALSE(rgbColor.IsLegacy()); VERIFY_IS_TRUE(rgbColor.IsRgb()); - auto view = _GetTableView(); - - auto color = rgbColor.GetColor(view, _defaultFg, false); + auto color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(myColor, color); rgbColor.SetDefault(); - color = rgbColor.GetColor(view, _defaultFg, false); + color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_defaultFg, color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_defaultFg, color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_defaultBg, color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_defaultBg, color); rgbColor.SetIndex(7, false); - color = rgbColor.GetColor(view, _defaultFg, false); + color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_colorTable[7], color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_colorTable[7], color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); rgbColor.SetIndex(15, false); - color = rgbColor.GetColor(view, _defaultFg, false); + color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); } diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 5566e7c8d..9c4436029 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -45,11 +45,12 @@ const TextAttribute Terminal::GetDefaultBrushColors() noexcept std::pair Terminal::GetAttributeColors(const TextAttribute& attr) const noexcept { _blinkingState.RecordBlinkingUsage(attr); - auto colors = attr.CalculateRgbColors({ _colorTable.data(), _colorTable.size() }, - _defaultFg, - _defaultBg, - _screenReversed, - _blinkingState.IsBlinkingFaint()); + auto colors = attr.CalculateRgbColors( + _colorTable, + _defaultFg, + _defaultBg, + _screenReversed, + _blinkingState.IsBlinkingFaint()); colors.first |= 0xff000000; // We only care about alpha for the default BG (which enables acrylic) // If the bg isn't the default bg color, or reverse video is enabled, make it fully opaque. diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp index 81ef9faa7..112905921 100644 --- a/src/host/consoleInformation.cpp +++ b/src/host/consoleInformation.cpp @@ -259,11 +259,12 @@ COLORREF CONSOLE_INFORMATION::GetDefaultBackground() const noexcept std::pair CONSOLE_INFORMATION::LookupAttributeColors(const TextAttribute& attr) const noexcept { _blinkingState.RecordBlinkingUsage(attr); - return attr.CalculateRgbColors(Get256ColorTable(), - GetDefaultForeground(), - GetDefaultBackground(), - IsScreenReversed(), - _blinkingState.IsBlinkingFaint()); + return attr.CalculateRgbColors( + GetColorTable(), + GetDefaultForeground(), + GetDefaultBackground(), + IsScreenReversed(), + _blinkingState.IsBlinkingFaint()); } // Method Description: diff --git a/src/host/settings.cpp b/src/host/settings.cpp index af1de09e3..a9fb5d093 100644 --- a/src/host/settings.cpp +++ b/src/host/settings.cpp @@ -726,16 +726,6 @@ void Settings::SetHistoryNoDup(const bool bHistoryNoDup) _bHistoryNoDup = bHistoryNoDup; } -gsl::span Settings::Get16ColorTable() const -{ - return Get256ColorTable().subspan(0, 16); -} - -gsl::span Settings::Get256ColorTable() const -{ - return { _colorTable.data(), _colorTable.size() }; -} - void Settings::SetColorTableEntry(const size_t index, const COLORREF ColorValue) { _colorTable.at(index) = ColorValue; diff --git a/src/host/settings.hpp b/src/host/settings.hpp index 25eb06dac..e8468da6b 100644 --- a/src/host/settings.hpp +++ b/src/host/settings.hpp @@ -159,8 +159,12 @@ public: bool GetHistoryNoDup() const; void SetHistoryNoDup(const bool fHistoryNoDup); - gsl::span Get16ColorTable() const; - gsl::span Get256ColorTable() const; + // The first 16 items of the color table are the same as the 16-color palette. + inline const std::array& GetColorTable() const noexcept + { + return _colorTable; + } + void SetColorTableEntry(const size_t index, const COLORREF ColorValue); COLORREF GetColorTableEntry(const size_t index) const; diff --git a/src/host/telemetry.cpp b/src/host/telemetry.cpp index 7b4ce210b..7924f2f69 100644 --- a/src/host/telemetry.cpp +++ b/src/host/telemetry.cpp @@ -419,7 +419,7 @@ void Telemetry::WriteFinalTraceLog() TraceLoggingBool(gci.GetQuickEdit(), "QuickEdit"), TraceLoggingValue(gci.GetWindowAlpha(), "WindowAlpha"), TraceLoggingBool(gci.GetWrapText(), "WrapText"), - TraceLoggingUInt32Array((UINT32 const*)gci.Get16ColorTable().data(), (UINT16)gci.Get16ColorTable().size(), "ColorTable"), + TraceLoggingUInt32Array((UINT32 const*)gci.GetColorTable().data(), 16, "ColorTable"), TraceLoggingValue(gci.CP, "CodePageInput"), TraceLoggingValue(gci.OutputCP, "CodePageOutput"), TraceLoggingValue(gci.GetFontSize().X, "FontSizeX"), @@ -453,7 +453,7 @@ void Telemetry::WriteFinalTraceLog() TraceLoggingValue(gci.GetShowWindow(), "ShowWindow"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - static_assert(sizeof(UINT32) == sizeof(gci.Get16ColorTable()[0]), "gci.Get16ColorTable()"); + static_assert(sizeof(UINT32) == sizeof(gci.GetColorTable()[0]), "gci.Get16ColorTable()"); // I could use the TraceLoggingUIntArray, but then we would have to know the order of the enums on the backend. // So just log each enum count separately with its string representation which makes it more human readable. From a151607c79909bbf716be3e78d37aafa527ea6d9 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 2 Aug 2021 14:42:57 -0500 Subject: [PATCH 35/90] Recalculate quake window size when snapping across monitors (#10744) ## Summary of the Pull Request win+shift+arrows can be used to move windows to adjacent monitors. When that happens, we'll new re-calculate the size of the window for the new monitor. ## References * megathread: #8888 ## PR Checklist * [x] Closes #10274 * [x] I work here * [ ] Tests added/passed * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments In `WM_WINDOWPOSCHANGING`, the OS says "hey, I'm about to do {something} to your window. You cool with that?". We handle that message by: 1. checking if the window was _moved_ as a part of this message 2. getting the monitor that the window will be moved onto 3. If that monitor is different than the monitor the window is currently on, then * calculate how big the quake window should be on that monitor * tell the OS that's where we'd like to be. ## Validation Steps Performed * win+shift+arrows works right now * normal quake summoning still works right --- src/cascadia/WindowsTerminal/IslandWindow.cpp | 98 +++++++++++++++++-- src/cascadia/WindowsTerminal/IslandWindow.h | 2 + 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 27ed12f1e..50fd2d156 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -274,7 +274,9 @@ LRESULT IslandWindow::_OnSizing(const WPARAM wParam, const LPARAM lParam) LRESULT IslandWindow::_OnMoving(const WPARAM /*wParam*/, const LPARAM lParam) { LPRECT winRect = reinterpret_cast(lParam); - // If we're the quake window, prevent moving the window + + // If we're the quake window, prevent moving the window. If we don't do + // this, then Alt+Space...Move will still be able to move the window. if (IsQuakeWindow()) { // Stuff our current window into the lParam, and return true. This @@ -506,6 +508,61 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize case WM_THEMECHANGED: UpdateWindowIconForActiveMetrics(_window.get()); return 0; + case WM_WINDOWPOSCHANGING: + { + // GH#10274 - if the quake window gets moved to another monitor via aero + // snap (win+shift+arrows), then re-adjust the size for the new monitor. + if (IsQuakeWindow()) + { + // Retrieve the suggested dimensions and make a rect and size. + LPWINDOWPOS lpwpos = (LPWINDOWPOS)lparam; + + // We only need to apply restrictions if the position is changing. + // The SWP_ flags are confusing to read. This is + // "if we're not moving the window, do nothing." + if (WI_IsFlagSet(lpwpos->flags, SWP_NOMOVE)) + { + break; + } + // Figure out the suggested dimensions and position. + RECT rcSuggested; + rcSuggested.left = lpwpos->x; + rcSuggested.top = lpwpos->y; + rcSuggested.right = rcSuggested.left + lpwpos->cx; + rcSuggested.bottom = rcSuggested.top + lpwpos->cy; + + // Find the bounds of the current monitor, and the monitor that + // we're suggested to be on. + + HMONITOR current = MonitorFromWindow(_window.get(), MONITOR_DEFAULTTONEAREST); + MONITORINFO currentInfo; + currentInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(current, ¤tInfo); + + HMONITOR proposed = MonitorFromRect(&rcSuggested, MONITOR_DEFAULTTONEAREST); + MONITORINFO proposedInfo; + proposedInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(proposed, &proposedInfo); + + // If the monitor changed... + if (til::rectangle{ proposedInfo.rcMonitor } != + til::rectangle{ currentInfo.rcMonitor }) + { + const auto newWindowRect{ _getQuakeModeSize(proposed) }; + + // Inform User32 that we want to be placed at the position + // and dimensions that _getQuakeModeSize returned. When we + // snap across monitor boundaries, this will re-evaluate our + // size for the new monitor. + lpwpos->x = newWindowRect.left(); + lpwpos->y = newWindowRect.top(); + lpwpos->cx = newWindowRect.width(); + lpwpos->cy = newWindowRect.height(); + + return 0; + } + } + } case CM_NOTIFY_FROM_TRAY: { switch (LOWORD(lparam)) @@ -1448,6 +1505,13 @@ void IslandWindow::IsQuakeWindow(bool isQuakeWindow) noexcept } } +// Method Description: +// - Enter quake mode for the monitor this window is currently on. This involves +// resizing it to the top half of the monitor. +// Arguments: +// - +// Return Value: +// - void IslandWindow::_enterQuakeMode() { if (!_window) @@ -1457,6 +1521,29 @@ void IslandWindow::_enterQuakeMode() RECT windowRect = GetWindowRect(); HMONITOR hmon = MonitorFromRect(&windowRect, MONITOR_DEFAULTTONEAREST); + + // Get the size and position of the window that we should occupy + const auto newRect{ _getQuakeModeSize(hmon) }; + + SetWindowPos(GetHandle(), + HWND_TOP, + newRect.left(), + newRect.top(), + newRect.width(), + newRect.height(), + SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOACTIVATE); +} + +// Method Description: +// - Get the size and position of the window that a "quake mode" should occupy +// on the given monitor. +// - The window will occupy the top half of the monitor. +// Arguments: +// - +// Return Value: +// - +til::rectangle IslandWindow::_getQuakeModeSize(HMONITOR hmon) +{ MONITORINFO nearestMonitorInfo; UINT dpix = USER_DEFAULT_SCREEN_DPI; @@ -1488,14 +1575,7 @@ void IslandWindow::_enterQuakeMode() availableSpace.height() / 2 }; - const til::rectangle newRect{ origin, dimensions }; - SetWindowPos(GetHandle(), - HWND_TOP, - newRect.left(), - newRect.top(), - newRect.width(), - newRect.height(), - SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOACTIVATE); + return til::rectangle{ origin, dimensions }; } DEFINE_EVENT(IslandWindow, DragRegionClicked, _DragRegionClickedHandlers, winrt::delegate<>); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index dc6c48c84..8e4a0ec4d 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -109,7 +109,9 @@ protected: void _moveToMonitor(const MONITORINFO activeMonitor); bool _isQuakeWindow{ false }; + void _enterQuakeMode(); + til::rectangle _getQuakeModeSize(HMONITOR hmon); void _summonWindowRoutineBody(winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior args); From 6936ee15fe8da9b9835a431df24f06e61e4d2412 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Mon, 2 Aug 2021 20:56:12 +0100 Subject: [PATCH 36/90] Make the alt buffer inherit cursor state from the main buffer (#10843) When switching to the alt buffer, the starting cursor position, style, and visibility is meant to be inherited from the main buffer. Similarly, when returning to the main buffer, any changes made to those attributes should be copied back (with the exception of the cursor position, which is restored to its original state). This PR makes sure we handle that cursor state correctly. At some point I'd like to move the cursor state out of the `SCREEN_INFORMATION` class, which would make this inheritance problem a non-issue. For now, though, I've just made it copy the state from the main buffer when creating the alt buffer, and copy it back when returning to the main buffer. ## Validation Steps Performed I've added some unit tests to verify the cursor state is inherited correctly when switching to the alt buffer and back again. I also had to make a small change to one of the existing alt buffer test that relied on the initial cursor position being at 0;0, which is no longer the case. I've verified that the test case in issue #3545 is now working correctly. I've also confirmed that this fixes a problem in the _notcurses_ demo, where the cursor was showing when it should have been hidden. Closes #3545 --- src/host/screenInfo.cpp | 18 ++++++- src/host/ut_host/ScreenBufferTests.cpp | 70 ++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 81bbfd391..2506bf450 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -1904,10 +1904,17 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const ppsiNewScreenBuffer); if (NT_SUCCESS(Status)) { - // Update the alt buffer's cursor style to match our own. + // Update the alt buffer's cursor style, visibility, and position to match our own. auto& myCursor = GetTextBuffer().GetCursor(); auto* const createdBuffer = *ppsiNewScreenBuffer; - createdBuffer->GetTextBuffer().GetCursor().SetStyle(myCursor.GetSize(), myCursor.GetColor(), myCursor.GetType()); + auto& altCursor = createdBuffer->GetTextBuffer().GetCursor(); + altCursor.SetStyle(myCursor.GetSize(), myCursor.GetColor(), myCursor.GetType()); + altCursor.SetIsVisible(myCursor.IsVisible()); + altCursor.SetBlinkingAllowed(myCursor.IsBlinkingAllowed()); + // The new position should match the viewport-relative position of the main buffer. + auto altCursorPos = myCursor.GetPosition(); + altCursorPos.Y -= GetVirtualViewport().Top(); + altCursor.SetPosition(altCursorPos); s_InsertScreenBuffer(createdBuffer); @@ -1998,6 +2005,13 @@ void SCREEN_INFORMATION::UseMainScreenBuffer() s_RemoveScreenBuffer(psiAlt); // this will also delete the alt buffer // deleting the alt buffer will give the GetSet back to its main + // Copy the alt buffer's cursor style and visibility back to the main buffer. + const auto& altCursor = psiAlt->GetTextBuffer().GetCursor(); + auto& mainCursor = psiMain->GetTextBuffer().GetCursor(); + mainCursor.SetStyle(altCursor.GetSize(), altCursor.GetColor(), altCursor.GetType()); + mainCursor.SetIsVisible(altCursor.IsVisible()); + mainCursor.SetBlinkingAllowed(altCursor.IsBlinkingAllowed()); + // Tell the VT MouseInput handler that we're in the main buffer now gci.GetActiveInputBuffer()->GetTerminalInput().UseMainScreenBuffer(); } diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index d8f2359db..0e31c2b32 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -89,6 +89,8 @@ class ScreenBufferTests TEST_METHOD(MultipleAlternateBuffersFromMainCreationTest); + TEST_METHOD(AlternateBufferCursorInheritanceTest); + TEST_METHOD(TestReverseLineFeed); TEST_METHOD(TestResetClearTabStops); @@ -344,6 +346,71 @@ void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest() } } +void ScreenBufferTests::AlternateBufferCursorInheritanceTest() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + auto& mainBuffer = gci.GetActiveOutputBuffer(); + auto& mainCursor = mainBuffer.GetTextBuffer().GetCursor(); + + Log::Comment(L"Set the cursor attributes in the main buffer."); + auto mainCursorPos = COORD{ 3, 5 }; + auto mainCursorVisible = false; + auto mainCursorSize = 33u; + auto mainCursorColor = RGB(1, 2, 3); + auto mainCursorType = CursorType::DoubleUnderscore; + auto mainCursorBlinking = false; + mainCursor.SetPosition(mainCursorPos); + mainCursor.SetIsVisible(mainCursorVisible); + mainCursor.SetStyle(mainCursorSize, mainCursorColor, mainCursorType); + mainCursor.SetBlinkingAllowed(mainCursorBlinking); + + Log::Comment(L"Switch to the alternate buffer."); + VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); + auto& altBuffer = gci.GetActiveOutputBuffer(); + auto& altCursor = altBuffer.GetTextBuffer().GetCursor(); + auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); + + Log::Comment(L"Confirm the cursor position is inherited from the main buffer."); + VERIFY_ARE_EQUAL(mainCursorPos, altCursor.GetPosition()); + Log::Comment(L"Confirm the cursor visibility is inherited from the main buffer."); + VERIFY_ARE_EQUAL(mainCursorVisible, altCursor.IsVisible()); + Log::Comment(L"Confirm the cursor style is inherited from the main buffer."); + VERIFY_ARE_EQUAL(mainCursorSize, altCursor.GetSize()); + VERIFY_ARE_EQUAL(mainCursorColor, altCursor.GetColor()); + VERIFY_ARE_EQUAL(mainCursorType, altCursor.GetType()); + VERIFY_ARE_EQUAL(mainCursorBlinking, altCursor.IsBlinkingAllowed()); + + Log::Comment(L"Set the cursor attributes in the alt buffer."); + auto altCursorPos = COORD{ 5, 3 }; + auto altCursorVisible = true; + auto altCursorSize = 66u; + auto altCursorColor = RGB(3, 2, 1); + auto altCursorType = CursorType::EmptyBox; + auto altCursorBlinking = true; + altCursor.SetPosition(altCursorPos); + altCursor.SetIsVisible(altCursorVisible); + altCursor.SetStyle(altCursorSize, altCursorColor, altCursorType); + altCursor.SetBlinkingAllowed(altCursorBlinking); + + Log::Comment(L"Switch back to the main buffer."); + useMain.release(); + altBuffer.UseMainScreenBuffer(); + VERIFY_ARE_EQUAL(&mainBuffer, &gci.GetActiveOutputBuffer()); + + Log::Comment(L"Confirm the cursor position is restored to what it was."); + VERIFY_ARE_EQUAL(mainCursorPos, mainCursor.GetPosition()); + Log::Comment(L"Confirm the cursor visibility is inherited from the alt buffer."); + VERIFY_ARE_EQUAL(altCursorVisible, mainCursor.IsVisible()); + Log::Comment(L"Confirm the cursor style is inherited from the alt buffer."); + VERIFY_ARE_EQUAL(altCursorSize, mainCursor.GetSize()); + VERIFY_ARE_EQUAL(altCursorColor, mainCursor.GetColor()); + VERIFY_ARE_EQUAL(altCursorType, mainCursor.GetType()); + VERIFY_ARE_EQUAL(altCursorBlinking, mainCursor.IsBlinkingAllowed()); +} + void ScreenBufferTests::TestReverseLineFeed() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -4948,6 +5015,9 @@ void ScreenBufferTests::ClearAlternateBuffer() auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); + // Set the position to home, otherwise it's inherited from the main buffer. + VERIFY_SUCCEEDED(altBuffer.SetCursorPosition({ 0, 0 }, true)); + WriteText(altBuffer.GetTextBuffer()); VerifyText(altBuffer.GetTextBuffer()); From a2a605050f56573b084229c94eb2d9293a955705 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Mon, 2 Aug 2021 15:39:11 -0500 Subject: [PATCH 37/90] When launching wsl, promote the starting directory to --cd (#9223) This commit introduces a hack to ConptyConnection for launching WSL. When we detect that WSL is being launched (either "wsl" or "wsl.exe", unqialified or _specifically_ from the current OS's System32 directory), we will promote the startingDirectory specified at launch time into a commandline argument. Why do we want to switch to `--cd`? With the current design of ConptyConnection and WSL, there are some significant limitations: * `startingDirectory` cannot be a WSL path, which forces users to use weird tricks such as setting the starting directory to `\\wsl$\Distro\home\user`. * WSL occasionally fails to launch in time to handle a `\\wsl$` path, which makes us spawn in a strange location (or no location at all). (This fix will only address the second one until a WSL update is released that adds support for `--cd $LINUX_PATH`.) We will not do the promotion if any of the following are true: * the commandline contains `--cd` already * the commandline contains a bare `~` * This was a commonly-used workaround that forced wsl to start in the user's home directory. It conflicts with --cd. * wsl is not spelled properly (`WSL` and `WSL.EXE` are unacceptable) * an absolute path to wsl outside the system32 directory is provided We chose the do this trick in the connection layer, the latest possible point, because it captures the most use cases. We could have done it earlier, but the options were quite limiting. They are: * Generate WSL profiles with startingDirectory set to the home folder * We can't do this because we do not know the user's home folder path. * Generate WSL profiles with `--cd` in them. * This only works for unmodified profiles. * This only works for generated profiles. * Users cannot override the commandline without breaking it. * Users cannot specify a startingDirectory (!) since the one on the commandline wins. * Set a flag on generated WSL profiles to request this trick * This only works for generated profiles. Users who create their own WSL profiles couldn't set startingDirectory and have it work the same. Patching the commandline, hacky though it may be, seemed to be the most compatible option. Eventually, we can even support `wt -d ~ wsl`! ## Validation Steps Performed Manual validation for the following cases: ```c++ // MUST MANGLE auto a01 = _tryMangleStartingDirectoryForWSL(LR"(wsl)", L"SENTINEL"); auto a02 = _tryMangleStartingDirectoryForWSL(LR"(wsl -d X)", L"SENTINEL"); auto a03 = _tryMangleStartingDirectoryForWSL(LR"(wsl -d X ~/bin/sh)", L"SENTINEL"); auto a04 = _tryMangleStartingDirectoryForWSL(LR"(wsl.exe)", L"SENTINEL"); auto a05 = _tryMangleStartingDirectoryForWSL(LR"(wsl.exe -d X)", L"SENTINEL"); auto a06 = _tryMangleStartingDirectoryForWSL(LR"(wsl.exe -d X ~/bin/sh)", L"SENTINEL"); auto a07 = _tryMangleStartingDirectoryForWSL(LR"("wsl")", L"SENTINEL"); auto a08 = _tryMangleStartingDirectoryForWSL(LR"("wsl.exe")", L"SENTINEL"); auto a09 = _tryMangleStartingDirectoryForWSL(LR"("wsl" -d X)", L"SENTINEL"); auto a10 = _tryMangleStartingDirectoryForWSL(LR"("wsl.exe" -d X)", L"SENTINEL"); auto a11 = _tryMangleStartingDirectoryForWSL(LR"("C:\Windows\system32\wsl.exe" -d X)", L"SENTINEL"); auto a12 = _tryMangleStartingDirectoryForWSL(LR"("C:\windows\system32\wsl" -d X)", L"SENTINEL"); auto a13 = _tryMangleStartingDirectoryForWSL(LR"(wsl ~/bin)", L"SENTINEL"); // MUST NOT MANGLE auto a14 = _tryMangleStartingDirectoryForWSL(LR"("C:\wsl.exe" -d X)", L"SENTINEL"); auto a15 = _tryMangleStartingDirectoryForWSL(LR"(C:\wsl.exe)", L"SENTINEL"); auto a16 = _tryMangleStartingDirectoryForWSL(LR"(wsl --cd C:\)", L"SENTINEL"); auto a17 = _tryMangleStartingDirectoryForWSL(LR"(wsl ~)", L"SENTINEL"); auto a18 = _tryMangleStartingDirectoryForWSL(LR"(wsl ~ -d Ubuntu)", L"SENTINEL"); ``` We don't have anywhere to put TerminalConnection unit tests :| Closes #592. --- src/cascadia/TerminalApp/TerminalPage.cpp | 14 ++-- .../TerminalConnection/ConptyConnection.cpp | 71 ++++++++++++++++++- .../TerminalSettingsModel/Profile.cpp | 7 +- 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 09cc175d6..653ae6915 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -837,13 +837,19 @@ namespace winrt::TerminalApp::implementation // construction, because the connection might not spawn the child // process until later, on another thread, after we've already // restored the CWD to it's original value. - std::wstring cwdString{ wil::GetCurrentDirectoryW() }; - std::filesystem::path cwd{ cwdString }; - cwd /= settings.StartingDirectory().c_str(); + winrt::hstring newWorkingDirectory{ settings.StartingDirectory() }; + if (newWorkingDirectory.size() <= 1 || + !(newWorkingDirectory[0] == L'~' || newWorkingDirectory[0] == L'/')) + { // We only want to resolve the new WD against the CWD if it doesn't look like a Linux path (see GH#592) + std::wstring cwdString{ wil::GetCurrentDirectoryW() }; + std::filesystem::path cwd{ cwdString }; + cwd /= settings.StartingDirectory().c_str(); + newWorkingDirectory = winrt::hstring{ cwd.wstring() }; + } auto conhostConn = TerminalConnection::ConptyConnection(); conhostConn.Initialize(TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(), - winrt::hstring{ cwd.wstring() }, + newWorkingDirectory, settings.StartingTitle(), envMap.GetView(), ::base::saturated_cast(settings.InitialRows()), diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index 1589f708d..d82b6be0a 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -57,6 +57,72 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation return S_OK; } + // Function Description: + // - Promotes a starting directory provided to a WSL invocation to a commandline argument. + // This is necessary because WSL has some modicum of support for linux-side directories (!) which + // CreateProcess never will. + static std::tuple _tryMangleStartingDirectoryForWSL(std::wstring_view commandLine, std::wstring_view startingDirectory) + { + do + { + if (startingDirectory.size() > 0 && commandLine.size() >= 3) + { // "wsl" is three characters; this is a safe bet. no point in doing it if there's no starting directory though! + // Find the first space, quote or the end of the string -- we'll look for wsl before that. + const auto terminator{ commandLine.find_first_of(LR"(" )", 1) }; // look past the first character in case it starts with " + const auto start{ til::at(commandLine, 0) == L'"' ? 1 : 0 }; + const std::filesystem::path executablePath{ commandLine.substr(start, terminator - start) }; + const auto executableFilename{ executablePath.filename().wstring() }; + if (executableFilename == L"wsl" || executableFilename == L"wsl.exe") + { + // We've got a WSL -- let's just make sure it's the right one. + if (executablePath.has_parent_path()) + { + std::wstring systemDirectory{}; + if (FAILED(wil::GetSystemDirectoryW(systemDirectory))) + { + break; // just bail out. + } + if (executablePath.parent_path().wstring() != systemDirectory) + { + break; // it wasn't in system32! + } + } + else + { + // assume that unqualified WSL is the one in system32 (minor danger) + } + + const auto arguments{ terminator == std::wstring_view::npos ? std::wstring_view{} : commandLine.substr(terminator + 1) }; + if (arguments.find(L"--cd") != std::wstring_view::npos) + { + break; // they've already got a --cd! + } + + const auto tilde{ arguments.find_first_of(L'~') }; + if (tilde != std::wstring_view::npos) + { + if (tilde + 1 == arguments.size() || til::at(arguments, tilde + 1) == L' ') + { + // We want to suppress --cd if they have added a bare ~ to their commandline (they conflict). + break; + } + // Tilde followed by non-space should be okay (like, wsl -d Debian ~/blah.sh) + } + + return { + fmt::format(LR"("{}" --cd "{}" {})", executablePath.wstring(), startingDirectory, arguments), + std::wstring{} + }; + } + } + } while (false); + + return { + std::wstring{ commandLine }, + std::wstring{ startingDirectory } + }; + } + // Function Description: // - launches the client application attached to the new pseudoconsole HRESULT ConptyConnection::_LaunchAttachedClient() noexcept @@ -163,11 +229,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation siEx.StartupInfo.lpTitle = mutableTitle.data(); } - const wchar_t* const startingDirectory = _startingDirectory.size() > 0 ? _startingDirectory.c_str() : nullptr; + auto [newCommandLine, newStartingDirectory] = _tryMangleStartingDirectoryForWSL(cmdline, _startingDirectory); + const wchar_t* const startingDirectory = newStartingDirectory.size() > 0 ? newStartingDirectory.c_str() : nullptr; RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW( nullptr, - cmdline.data(), + newCommandLine.data(), nullptr, // lpProcessAttributes nullptr, // lpThreadAttributes false, // bInheritHandles diff --git a/src/cascadia/TerminalSettingsModel/Profile.cpp b/src/cascadia/TerminalSettingsModel/Profile.cpp index a684acc95..d90a65884 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.cpp +++ b/src/cascadia/TerminalSettingsModel/Profile.cpp @@ -448,11 +448,6 @@ winrt::Microsoft::Terminal::Settings::Model::FontConfig Profile::FontInfo() // - the function returns an evaluated version of %userprofile% to avoid blocking the session from starting. std::wstring Profile::EvaluateStartingDirectory(const std::wstring& directory) { - // First expand path - DWORD numCharsInput = ExpandEnvironmentStrings(directory.c_str(), nullptr, 0); - std::unique_ptr evaluatedPath = std::make_unique(numCharsInput); - THROW_LAST_ERROR_IF(0 == ExpandEnvironmentStrings(directory.c_str(), evaluatedPath.get(), numCharsInput)); - // Prior to GH#9541, we'd validate that the user's startingDirectory existed // here. If it was invalid, we'd gracefully fall back to %USERPROFILE%. // @@ -463,7 +458,7 @@ std::wstring Profile::EvaluateStartingDirectory(const std::wstring& directory) // // If the path is eventually invalid, we'll display warning in the // ConptyConnection when the process fails to launch. - return std::wstring(evaluatedPath.get(), numCharsInput); + return wil::ExpandEnvironmentStringsW(directory.c_str()); } // Function Description: From 94166942cce5c776d69f511e8d6aa041b2f84237 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 2 Aug 2021 22:54:46 +0200 Subject: [PATCH 38/90] Fix font changes not resizing _invalidMap (#10856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `_invalidMap` size is dependent on both `clientSize` as well as `glyphCellSize` and must be resized when either changes. ## PR Checklist * [x] Closes #10855 * [x] I work here ## Validation Steps Performed * Changing font size with Ctrl+Mousewheel in fullscreen works ✔️ --- src/renderer/dx/DxRenderer.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index b633ab7a4..8d8f238e1 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -582,9 +582,6 @@ try _displaySizePixels = _GetClientSize(); - _invalidMap.resize(_displaySizePixels / _fontRenderData->GlyphCell()); - RETURN_IF_FAILED(InvalidateAll()); - // Get the other device types so we have deeper access to more functionality // in our pipeline than by just walking straight from the D3D device. @@ -1288,6 +1285,7 @@ try if (_isEnabled) { const auto clientSize = _GetClientSize(); + const auto glyphCellSize = _fontRenderData->GlyphCell(); // If we don't have device resources or if someone has requested that we // recreate the device... then make new resources. (Create will dump the old ones.) @@ -1317,8 +1315,11 @@ try // And persist the new size. _displaySizePixels = clientSize; + } - _invalidMap.resize(clientSize / _fontRenderData->GlyphCell()); + if (const auto size = clientSize / glyphCellSize; size != _invalidMap.size()) + { + _invalidMap.resize(size); RETURN_IF_FAILED(InvalidateAll()); } @@ -1337,7 +1338,7 @@ try _ShouldForceGrayscaleAA(), _dwriteFactory.Get(), spacing, - _fontRenderData->GlyphCell(), + glyphCellSize, _d2dDeviceContext->GetSize(), std::nullopt, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); From 9ba20805ec4da6494a7e2c85666e007441419cb7 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Mon, 2 Aug 2021 22:04:17 +0100 Subject: [PATCH 39/90] Sanitize C1 control chars in SetConsoleTitle API (#10847) When the `SetContoleTitle` API is called with a title containing control characters, we need to filter out those characters before we can forward the title change over conpty as an escape sequence. If we don't do that, the receiving terminal will end up executing the control characters instead of updating the title. We were already filtering out the C0 control characters, but with this PR we're now filtering out C1 controls characters as well. I've simply updated the sanitizing routine in `DoSrvSetConsoleTitleW` to filter our characters in the range `0x80` to `0x9F`. This is in addition to the C0 range (`0x00` to `0x1F`) that was already excluded. ## Validation Steps Performed I've added a conpty unit test that calls `DoSrvSetConsoleTitleW` with titles containing a variety of C0 and C1 controls characters, and which verifies that those characters are stripped from the title forwarded to conpty. I've also confirmed that the test case in issue #10312 is now working correctly in Windows Terminal. Closes #10312 --- src/host/getset.cpp | 14 ++++------- src/host/ut_host/ConptyOutputTests.cpp | 35 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/host/getset.cpp b/src/host/getset.cpp index dc0459f6d..6e9e70191 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -1931,15 +1931,11 @@ void DoSrvPrivateRefreshWindow(_In_ const SCREEN_INFORMATION& screenInfo) // to embed control characters in that string. if (gci.IsInVtIoMode()) { - std::wstring sanitized; - sanitized.reserve(title.size()); - for (size_t i = 0; i < title.size(); i++) - { - if (title.at(i) >= UNICODE_SPACE) - { - sanitized.push_back(title.at(i)); - } - } + std::wstring sanitized{ title }; + sanitized.erase(std::remove_if(sanitized.begin(), sanitized.end(), [](auto ch) { + return ch < UNICODE_SPACE || (ch > UNICODE_DEL && ch < UNICODE_NBSP); + }), + sanitized.end()); gci.SetTitle({ sanitized }); } diff --git a/src/host/ut_host/ConptyOutputTests.cpp b/src/host/ut_host/ConptyOutputTests.cpp index a76d5c4f8..d412fb562 100644 --- a/src/host/ut_host/ConptyOutputTests.cpp +++ b/src/host/ut_host/ConptyOutputTests.cpp @@ -119,6 +119,7 @@ class ConptyOutputTests TEST_METHOD(WriteTwoLinesUsesNewline); TEST_METHOD(WriteAFewSimpleLines); TEST_METHOD(InvalidateUntilOneBeforeEnd); + TEST_METHOD(SetConsoleTitleWithControlChars); private: bool _writeCallback(const char* const pch, size_t const cch); @@ -364,3 +365,37 @@ void ConptyOutputTests::InvalidateUntilOneBeforeEnd() VERIFY_SUCCEEDED(renderer.PaintFrame()); } + +void ConptyOutputTests::SetConsoleTitleWithControlChars() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:control", L"{0x00, 0x0A, 0x1B, 0x80, 0x9B, 0x9C}") + END_TEST_METHOD_PROPERTIES() + + int control; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"control", control)); + + auto& g = ServiceLocator::LocateGlobals(); + auto& renderer = *g.pRender; + + Log::Comment(NoThrowString().Format( + L"SetConsoleTitle with a control character (0x%02X) embedded in the text", control)); + + std::wstringstream titleText; + titleText << L"Hello " << wchar_t(control) << L"World!"; + VERIFY_SUCCEEDED(DoSrvSetConsoleTitleW(titleText.str())); + + // This is the standard init sequences for the first frame. + expectedOutput.push_back("\x1b[2J"); + expectedOutput.push_back("\x1b[m"); + expectedOutput.push_back("\x1b[H"); + + // The title change is propagated as an OSC 0 sequence. + // Control characters are stripped, so it's always "Hello World". + expectedOutput.push_back("\x1b]0;Hello World!\a"); + + // This is also part of the standard init sequence. + expectedOutput.push_back("\x1b[?25h"); + + VERIFY_SUCCEEDED(renderer.PaintFrame()); +} From e7108332f72a3c14c72a65484d091c4e79ab28b2 Mon Sep 17 00:00:00 2001 From: Schuyler Rosefield Date: Mon, 2 Aug 2021 17:04:57 -0400 Subject: [PATCH 40/90] Add the ability to toggle a pane's split direction (#10713) ## Summary of the Pull Request Add the ability to toggle a pane's split direction - Switch from horizontal to vertical split (and vice versa) - Propogate new borders through to children. ## References #10665 ## PR Checklist * [x] Closes #10665 * [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 ## Validation Steps Performed Ran terminal, created multiple panes in different orientations, ran command through command palate and verified that they displayed properly in the new orientation. --- doc/cascadia/profiles.schema.json | 1 + .../TerminalApp/AppActionHandlers.cpp | 7 ++ src/cascadia/TerminalApp/Pane.cpp | 66 ++++++++++- src/cascadia/TerminalApp/Pane.h | 2 + src/cascadia/TerminalApp/TerminalPage.cpp | 15 +++ src/cascadia/TerminalApp/TerminalPage.h | 1 + src/cascadia/TerminalApp/TerminalTab.cpp | 12 ++ src/cascadia/TerminalApp/TerminalTab.h | 1 + .../TerminalSettingsModel/ActionAndArgs.cpp | 2 + .../AllShortcutActions.h | 107 +++++++++--------- .../Resources/en-US/Resources.resw | 3 + .../TerminalSettingsModel/defaults.json | 1 + 12 files changed, 159 insertions(+), 59 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 96e7c6cfc..2b078d00d 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -271,6 +271,7 @@ "toggleFocusMode", "toggleFullscreen", "togglePaneZoom", + "toggleSplitOrientation", "toggleReadOnlyMode", "toggleShaderEffects", "wt", diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 8535d88fe..60401122c 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -161,6 +161,13 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleToggleSplitOrientation(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + _ToggleSplitOrientation(); + args.Handled(true); + } + void TerminalPage::_HandleTogglePaneZoom(const IInspectable& /*sender*/, const ActionEventArgs& args) { diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index a2c071146..59dacf6b9 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -1449,8 +1449,26 @@ void Pane::_UpdateBorders() _border.BorderThickness(ThicknessHelper::FromLengths(left, top, right, bottom)); } +// Method Description: +// - Find the borders for the leaf pane, or the shared borders for child panes. +// Arguments: +// - +// Return Value: +// - +Borders Pane::_GetCommonBorders() +{ + if (_IsLeaf()) + { + return _borders; + } + + return _firstChild->_GetCommonBorders() & _secondChild->_GetCommonBorders(); +} + // Method Description: // - Sets the row/column of our child UI elements, to match our current split type. +// - In case the split definition or parent borders were changed, this recursively +// updates the children as well. // Arguments: // - // Return Value: @@ -1466,9 +1484,8 @@ void Pane::_ApplySplitDefinitions() _secondChild->_borders = _borders | Borders::Left; _borders = Borders::None; - _UpdateBorders(); - _firstChild->_UpdateBorders(); - _secondChild->_UpdateBorders(); + _firstChild->_ApplySplitDefinitions(); + _secondChild->_ApplySplitDefinitions(); } else if (_splitState == SplitState::Horizontal) { @@ -1479,10 +1496,10 @@ void Pane::_ApplySplitDefinitions() _secondChild->_borders = _borders | Borders::Top; _borders = Borders::None; - _UpdateBorders(); - _firstChild->_UpdateBorders(); - _secondChild->_UpdateBorders(); + _firstChild->_ApplySplitDefinitions(); + _secondChild->_ApplySplitDefinitions(); } + _UpdateBorders(); } // Method Description: @@ -1743,6 +1760,43 @@ std::pair, std::shared_ptr> Pane::Split(SplitState s return _Split(splitType, splitSize, profile, control); } +// Method Description: +// - Toggle the split orientation of the currently focused pane +// Arguments: +// - +// Return Value: +// - true if a split was changed +bool Pane::ToggleSplitOrientation() +{ + // If we are a leaf there is no split to toggle. + if (_IsLeaf()) + { + 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. + const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive; + const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive; + if (firstIsFocused || secondIsFocused) + { + // Switch the split orientation + _splitState = _splitState == SplitState::Horizontal ? SplitState::Vertical : SplitState::Horizontal; + + // then update the borders and positioning on ourselves and our children. + _borders = _GetCommonBorders(); + // Since we changed if we are using rows/columns, make sure we remove the old definitions + _root.ColumnDefinitions().Clear(); + _root.RowDefinitions().Clear(); + _CreateRowColDefinitions(); + _ApplySplitDefinitions(); + + return true; + } + + return _firstChild->ToggleSplitOrientation() || _secondChild->ToggleSplitOrientation(); +} + // Method Description: // - Converts an "automatic" split type into either Vertical or Horizontal, // based upon the current dimensions of the Pane. diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index a8bc3f443..eed685d32 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -69,6 +69,7 @@ public: const float splitSize, const GUID& profile, const winrt::Microsoft::Terminal::Control::TermControl& control); + bool ToggleSplitOrientation(); float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; std::optional PreCalculateAutoSplit(const std::shared_ptr target, const winrt::Windows::Foundation::Size parentSize) const; @@ -146,6 +147,7 @@ private: void _ApplySplitDefinitions(); void _SetupEntranceAnimation(); void _UpdateBorders(); + Borders _GetCommonBorders(); bool _Resize(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 653ae6915..fc552d767 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1316,6 +1316,21 @@ namespace winrt::TerminalApp::implementation CATCH_LOG(); } + // Method Description: + // - Switches the split orientation of the currently focused pane. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_ToggleSplitOrientation() + { + if (const auto terminalTab{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + terminalTab->ToggleSplitOrientation(); + } + } + // Method Description: // - Attempt to move a separator between panes, as to resize each child on // either size of the separator. See Pane::ResizePane for details. diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index ea068a894..aa2db9529 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -255,6 +255,7 @@ namespace winrt::TerminalApp::implementation const float splitSize = 0.5f, const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr); void _ResizePane(const Microsoft::Terminal::Settings::Model::ResizeDirection& direction); + void _ToggleSplitOrientation(); void _ScrollPage(ScrollDirection scrollDirection); void _ScrollToBufferEdge(ScrollDirection scrollDirection); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 9590fcd3f..4e420c5c2 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -440,6 +440,17 @@ namespace winrt::TerminalApp::implementation _UpdateActivePane(second); } + // Method Description: + // - Find the currently active pane, and then switch the split direction of + // its parent. E.g. switch from Horizontal to Vertical. + // Return Value: + + // - + void TerminalTab::ToggleSplitOrientation() + { + _rootPane->ToggleSplitOrientation(); + } + // Method Description: // - See Pane::CalcSnappedDimension float TerminalTab::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const @@ -1222,6 +1233,7 @@ namespace winrt::TerminalApp::implementation EnterZoom(); } } + void TerminalTab::EnterZoom() { _zoomedPane = _activePane; diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 68c041ab1..3ef935364 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -38,6 +38,7 @@ namespace winrt::TerminalApp::implementation const GUID& profile, winrt::Microsoft::Terminal::Control::TermControl& control); + void ToggleSplitOrientation(); winrt::fire_and_forget UpdateIcon(const winrt::hstring iconPath); winrt::fire_and_forget HideIcon(const bool hide); diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index f6c397605..0bd4b174a 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -49,6 +49,7 @@ static constexpr std::string_view ToggleCommandPaletteKey{ "commandPalette" }; static constexpr std::string_view ToggleFocusModeKey{ "toggleFocusMode" }; static constexpr std::string_view ToggleFullscreenKey{ "toggleFullscreen" }; static constexpr std::string_view TogglePaneZoomKey{ "togglePaneZoom" }; +static constexpr std::string_view ToggleSplitOrientationKey{ "toggleSplitOrientation" }; static constexpr std::string_view LegacyToggleRetroEffectKey{ "toggleRetroEffect" }; static constexpr std::string_view ToggleShaderEffectsKey{ "toggleShaderEffects" }; static constexpr std::string_view MoveTabKey{ "moveTab" }; @@ -349,6 +350,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::ToggleFocusMode, RS_(L"ToggleFocusModeCommandKey") }, { ShortcutAction::ToggleFullscreen, RS_(L"ToggleFullscreenCommandKey") }, { ShortcutAction::TogglePaneZoom, RS_(L"TogglePaneZoomCommandKey") }, + { ShortcutAction::ToggleSplitOrientation, RS_(L"ToggleSplitOrientationCommandKey") }, { ShortcutAction::ToggleShaderEffects, RS_(L"ToggleShaderEffectsCommandKey") }, { ShortcutAction::MoveTab, L"" }, // Intentionally omitted, must be generated by GenerateName { ShortcutAction::BreakIntoDebugger, RS_(L"BreakIntoDebuggerCommandKey") }, diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index ca795bf6c..71c74fccb 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -23,59 +23,60 @@ // each action. This is _NOT_ something that should be used when any individual // case should be customized. -#define ALL_SHORTCUT_ACTIONS \ - ON_ALL_ACTIONS(CopyText) \ - ON_ALL_ACTIONS(PasteText) \ - ON_ALL_ACTIONS(OpenNewTabDropdown) \ - ON_ALL_ACTIONS(DuplicateTab) \ - ON_ALL_ACTIONS(NewTab) \ - ON_ALL_ACTIONS(CloseWindow) \ - ON_ALL_ACTIONS(CloseTab) \ - ON_ALL_ACTIONS(ClosePane) \ - ON_ALL_ACTIONS(NextTab) \ - ON_ALL_ACTIONS(PrevTab) \ - ON_ALL_ACTIONS(SendInput) \ - ON_ALL_ACTIONS(SplitPane) \ - ON_ALL_ACTIONS(TogglePaneZoom) \ - ON_ALL_ACTIONS(SwitchToTab) \ - ON_ALL_ACTIONS(AdjustFontSize) \ - ON_ALL_ACTIONS(ResetFontSize) \ - ON_ALL_ACTIONS(ScrollUp) \ - ON_ALL_ACTIONS(ScrollDown) \ - ON_ALL_ACTIONS(ScrollUpPage) \ - ON_ALL_ACTIONS(ScrollDownPage) \ - ON_ALL_ACTIONS(ScrollToTop) \ - ON_ALL_ACTIONS(ScrollToBottom) \ - ON_ALL_ACTIONS(ResizePane) \ - ON_ALL_ACTIONS(MoveFocus) \ - ON_ALL_ACTIONS(MovePane) \ - ON_ALL_ACTIONS(Find) \ - ON_ALL_ACTIONS(ToggleShaderEffects) \ - ON_ALL_ACTIONS(ToggleFocusMode) \ - ON_ALL_ACTIONS(ToggleFullscreen) \ - ON_ALL_ACTIONS(ToggleAlwaysOnTop) \ - ON_ALL_ACTIONS(OpenSettings) \ - ON_ALL_ACTIONS(SetColorScheme) \ - ON_ALL_ACTIONS(SetTabColor) \ - ON_ALL_ACTIONS(OpenTabColorPicker) \ - ON_ALL_ACTIONS(RenameTab) \ - ON_ALL_ACTIONS(OpenTabRenamer) \ - ON_ALL_ACTIONS(ExecuteCommandline) \ - ON_ALL_ACTIONS(ToggleCommandPalette) \ - ON_ALL_ACTIONS(CloseOtherTabs) \ - ON_ALL_ACTIONS(CloseTabsAfter) \ - ON_ALL_ACTIONS(TabSearch) \ - ON_ALL_ACTIONS(MoveTab) \ - ON_ALL_ACTIONS(BreakIntoDebugger) \ - ON_ALL_ACTIONS(TogglePaneReadOnly) \ - ON_ALL_ACTIONS(FindMatch) \ - ON_ALL_ACTIONS(NewWindow) \ - ON_ALL_ACTIONS(IdentifyWindow) \ - ON_ALL_ACTIONS(IdentifyWindows) \ - ON_ALL_ACTIONS(RenameWindow) \ - ON_ALL_ACTIONS(OpenWindowRenamer) \ - ON_ALL_ACTIONS(GlobalSummon) \ - ON_ALL_ACTIONS(QuakeMode) \ +#define ALL_SHORTCUT_ACTIONS \ + ON_ALL_ACTIONS(CopyText) \ + ON_ALL_ACTIONS(PasteText) \ + ON_ALL_ACTIONS(OpenNewTabDropdown) \ + ON_ALL_ACTIONS(DuplicateTab) \ + ON_ALL_ACTIONS(NewTab) \ + ON_ALL_ACTIONS(CloseWindow) \ + ON_ALL_ACTIONS(CloseTab) \ + ON_ALL_ACTIONS(ClosePane) \ + ON_ALL_ACTIONS(NextTab) \ + ON_ALL_ACTIONS(PrevTab) \ + ON_ALL_ACTIONS(SendInput) \ + ON_ALL_ACTIONS(SplitPane) \ + ON_ALL_ACTIONS(ToggleSplitOrientation) \ + ON_ALL_ACTIONS(TogglePaneZoom) \ + ON_ALL_ACTIONS(SwitchToTab) \ + ON_ALL_ACTIONS(AdjustFontSize) \ + ON_ALL_ACTIONS(ResetFontSize) \ + ON_ALL_ACTIONS(ScrollUp) \ + ON_ALL_ACTIONS(ScrollDown) \ + ON_ALL_ACTIONS(ScrollUpPage) \ + ON_ALL_ACTIONS(ScrollDownPage) \ + ON_ALL_ACTIONS(ScrollToTop) \ + ON_ALL_ACTIONS(ScrollToBottom) \ + ON_ALL_ACTIONS(ResizePane) \ + ON_ALL_ACTIONS(MoveFocus) \ + ON_ALL_ACTIONS(MovePane) \ + ON_ALL_ACTIONS(Find) \ + ON_ALL_ACTIONS(ToggleShaderEffects) \ + ON_ALL_ACTIONS(ToggleFocusMode) \ + ON_ALL_ACTIONS(ToggleFullscreen) \ + ON_ALL_ACTIONS(ToggleAlwaysOnTop) \ + ON_ALL_ACTIONS(OpenSettings) \ + ON_ALL_ACTIONS(SetColorScheme) \ + ON_ALL_ACTIONS(SetTabColor) \ + ON_ALL_ACTIONS(OpenTabColorPicker) \ + ON_ALL_ACTIONS(RenameTab) \ + ON_ALL_ACTIONS(OpenTabRenamer) \ + ON_ALL_ACTIONS(ExecuteCommandline) \ + ON_ALL_ACTIONS(ToggleCommandPalette) \ + ON_ALL_ACTIONS(CloseOtherTabs) \ + ON_ALL_ACTIONS(CloseTabsAfter) \ + ON_ALL_ACTIONS(TabSearch) \ + ON_ALL_ACTIONS(MoveTab) \ + ON_ALL_ACTIONS(BreakIntoDebugger) \ + ON_ALL_ACTIONS(TogglePaneReadOnly) \ + ON_ALL_ACTIONS(FindMatch) \ + ON_ALL_ACTIONS(NewWindow) \ + ON_ALL_ACTIONS(IdentifyWindow) \ + ON_ALL_ACTIONS(IdentifyWindows) \ + ON_ALL_ACTIONS(RenameWindow) \ + ON_ALL_ACTIONS(OpenWindowRenamer) \ + ON_ALL_ACTIONS(GlobalSummon) \ + ON_ALL_ACTIONS(QuakeMode) \ ON_ALL_ACTIONS(FocusPane) #define ALL_SHORTCUT_ACTIONS_WITH_ARGS \ diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 99af72c23..ad8762ed3 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -387,6 +387,9 @@ Toggle fullscreen + + Toggle pane split orientation + Toggle pane zoom diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 1cc6b3162..6d8cdceb7 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -350,6 +350,7 @@ { "command": { "action": "movePane", "direction": "up" } }, { "command": { "action": "movePane", "direction": "previous"} }, { "command": "togglePaneZoom" }, + { "command": "toggleSplitOrientation" }, { "command": "toggleReadOnlyMode" }, // Clipboard Integration From cccaab8545f4d9b8918b08c2fd7d6d2d4d79639e Mon Sep 17 00:00:00 2001 From: Ian O'Neill Date: Tue, 3 Aug 2021 19:16:07 +0100 Subject: [PATCH 41/90] Fix drag and drop on '+' button for drive letters (#10842) Fixes dragging and dropping drive letters onto the '+' button. Manually tested - dragging and dropping the `C:\` drive onto the '+' button works when creating a new tab, splitting or creating a new window. Dragging and dropping a regular directory still works. Closes #10723 --- .../LocalTests_SettingsModel/CommandTests.cpp | 21 ++++++++++++++++++- src/cascadia/TerminalApp/TerminalPage.cpp | 10 +-------- .../TerminalSettingsModel/ActionArgs.cpp | 5 ++++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp index ce5f06014..bdc4d5692 100644 --- a/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp @@ -397,6 +397,10 @@ namespace SettingsModelLocalTests "name":"action6", "command": { "action": "newWindow", "startingDirectory":"C:\\foo", "commandline": "bar.exe" } }, + { + "name":"action7_startingDirectoryWithTrailingSlash", + "command": { "action": "newWindow", "startingDirectory":"C:\\", "commandline": "bar.exe" } + }, ])" }; const auto commands0Json = VerifyParseSucceeded(commands0String); @@ -405,7 +409,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(7u, commands.Size()); + VERIFY_ARE_EQUAL(8u, commands.Size()); { auto command = commands.Lookup(L"action0"); @@ -503,5 +507,20 @@ namespace SettingsModelLocalTests L"cmdline: \"%s\"", cmdline.c_str())); VERIFY_ARE_EQUAL(L"--startingDirectory \"C:\\foo\" -- \"bar.exe\"", terminalArgs.ToCommandline()); } + + { + auto command = commands.Lookup(L"action7_startingDirectoryWithTrailingSlash"); + 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(L"--startingDirectory \"C:\\\\\" -- \"bar.exe\"", terminalArgs.ToCommandline()); + } } } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index fc552d767..281189920 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -253,16 +253,8 @@ namespace winrt::TerminalApp::implementation path = path.parent_path(); } - std::wstring pathText = path.wstring(); - - // Handle edge case of "C:\\", seems like the "StartingDirectory" doesn't like path which ends with '\' - if (pathText.back() == L'\\') - { - pathText.erase(std::prev(pathText.end())); - } - NewTerminalArgs args; - args.StartingDirectory(winrt::hstring{ pathText }); + args.StartingDirectory(winrt::hstring{ path.wstring() }); this->_OpenNewTerminal(args); TraceLoggingWrite( diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 7bc057742..9baaaf240 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -118,7 +118,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation if (!StartingDirectory().empty()) { - ss << fmt::format(L"--startingDirectory \"{}\" ", StartingDirectory()); + // 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); } if (!TabTitle().empty()) From 8ab3422b57231451eacf2e32c437478f44975b83 Mon Sep 17 00:00:00 2001 From: Marcel Wagner Date: Wed, 4 Aug 2021 00:25:23 +0200 Subject: [PATCH 42/90] [settings-editor] Switch to function bindings instead of Converter objects (#10846) ## Validation Steps Performed Clicked around, validated that settings still behave the same (as far as I can tell with my limited terminal configuration expertise) Closes #10387 --- .../TerminalSettingsEditor/Actions.xaml | 7 +- .../TerminalSettingsEditor/Appearances.h | 13 +++ .../TerminalSettingsEditor/Appearances.idl | 5 + .../TerminalSettingsEditor/Appearances.xaml | 24 ++-- .../ColorLightenConverter.cpp | 33 ------ .../ColorLightenConverter.h | 9 -- .../TerminalSettingsEditor/ColorSchemes.xaml | 13 +-- .../ColorToBrushConverter.cpp | 28 ----- .../ColorToBrushConverter.h | 30 ----- .../ColorToHexConverter.cpp | 30 ----- .../ColorToHexConverter.h | 30 ----- .../TerminalSettingsEditor/Converters.cpp | 106 ++++++++++++++++++ .../TerminalSettingsEditor/Converters.h | 33 ++++++ .../TerminalSettingsEditor/Converters.idl | 73 +++--------- .../FontWeightConverter.cpp | 32 ------ .../FontWeightConverter.h | 30 ----- .../GlobalAppearance.xaml | 4 +- .../InvertedBooleanConverter.cpp | 28 ----- .../InvertedBooleanConverter.h | 9 -- .../InvertedBooleanToVisibilityConverter.cpp | 28 ----- .../InvertedBooleanToVisibilityConverter.h | 9 -- ...Microsoft.Terminal.Settings.Editor.vcxproj | 81 ++----------- ...t.Terminal.Settings.Editor.vcxproj.filters | 13 +-- .../PaddingConverter.cpp | 65 ----------- .../TerminalSettingsEditor/PaddingConverter.h | 9 -- .../PercentageConverter.cpp | 32 ------ .../PercentageConverter.h | 30 ----- .../TerminalSettingsEditor/Profiles.h | 10 ++ .../TerminalSettingsEditor/Profiles.idl | 5 + .../TerminalSettingsEditor/Profiles.xaml | 25 ++--- .../ReadOnlyActions.xaml | 3 - .../StringIsEmptyConverter.cpp | 30 ----- .../StringIsEmptyConverter.h | 9 -- .../StringIsNotDesktopConverter.cpp | 50 --------- .../StringIsNotDesktopConverter.h | 11 -- .../TerminalSettingsModel/GlobalAppSettings.h | 6 + .../GlobalAppSettings.idl | 3 + 37 files changed, 231 insertions(+), 725 deletions(-) delete mode 100644 src/cascadia/TerminalSettingsEditor/ColorLightenConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/ColorLightenConverter.h delete mode 100644 src/cascadia/TerminalSettingsEditor/ColorToBrushConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/ColorToBrushConverter.h delete mode 100644 src/cascadia/TerminalSettingsEditor/ColorToHexConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/ColorToHexConverter.h create mode 100644 src/cascadia/TerminalSettingsEditor/Converters.cpp create mode 100644 src/cascadia/TerminalSettingsEditor/Converters.h delete mode 100644 src/cascadia/TerminalSettingsEditor/FontWeightConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/FontWeightConverter.h delete mode 100644 src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.h delete mode 100644 src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.h delete mode 100644 src/cascadia/TerminalSettingsEditor/PaddingConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/PaddingConverter.h delete mode 100644 src/cascadia/TerminalSettingsEditor/PercentageConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/PercentageConverter.h delete mode 100644 src/cascadia/TerminalSettingsEditor/StringIsEmptyConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/StringIsEmptyConverter.h delete mode 100644 src/cascadia/TerminalSettingsEditor/StringIsNotDesktopConverter.cpp delete mode 100644 src/cascadia/TerminalSettingsEditor/StringIsNotDesktopConverter.h diff --git a/src/cascadia/TerminalSettingsEditor/Actions.xaml b/src/cascadia/TerminalSettingsEditor/Actions.xaml index e4dd4ccc7..11c1f65e4 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Actions.xaml @@ -158,7 +158,6 @@ - + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(IsInEditMode), Mode=OneWay}" /> + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(IsInEditMode), Mode=OneWay}"> + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(IsNewlyAdded), Mode=OneWay}"> diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.h b/src/cascadia/TerminalSettingsEditor/Appearances.h index db00e79c4..6860b0361 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.h +++ b/src/cascadia/TerminalSettingsEditor/Appearances.h @@ -51,6 +51,19 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation public: AppearanceViewModel(const Model::AppearanceConfig& appearance); + void SetFontWeightFromDouble(double fontWeight) + { + FontWeight(winrt::Microsoft::Terminal::Settings::Editor::Converters::DoubleToFontWeight(fontWeight)); + } + void SetBackgroundImageOpacityFromPercentageValue(double percentageValue) + { + BackgroundImageOpacity(winrt::Microsoft::Terminal::Settings::Editor::Converters::PercentageValueToPercentage(percentageValue)); + } + void SetBackgroundImagePath(winrt::hstring path) + { + BackgroundImagePath(path); + } + // background image bool UseDesktopBGImage(); void UseDesktopBGImage(const bool useDesktop); diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.idl b/src/cascadia/TerminalSettingsEditor/Appearances.idl index a4faa7a30..ddc83abed 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.idl +++ b/src/cascadia/TerminalSettingsEditor/Appearances.idl @@ -23,6 +23,10 @@ namespace Microsoft.Terminal.Settings.Editor { Boolean IsDefault; + void SetFontWeightFromDouble(Double fontWeight); + void SetBackgroundImageOpacityFromPercentageValue(Double percentageValue); + void SetBackgroundImagePath(String path); + Boolean UseDesktopBGImage; Boolean BackgroundImageSettingsVisible { get; }; @@ -68,5 +72,6 @@ namespace Microsoft.Terminal.Settings.Editor Windows.Foundation.Collections.IObservableVector FontWeightList { get; }; IInspectable CurrentFontFace { get; }; + Windows.UI.Xaml.Controls.Slider BIOpacitySlider { get; }; } } diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.xaml b/src/cascadia/TerminalSettingsEditor/Appearances.xaml index f31e625ec..d49de2051 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.xaml +++ b/src/cascadia/TerminalSettingsEditor/Appearances.xaml @@ -34,16 +34,6 @@ - - - - - - - - - - @@ -87,7 +77,7 @@ SelectedItem="{x:Bind CurrentFontFace, Mode=OneWay}" SelectionChanged="FontFace_SelectionChanged" Style="{StaticResource ComboBoxSettingStyle}" - Visibility="{x:Bind ShowAllFonts, Mode=OneWay, Converter={StaticResource InvertedBooleanToVisibilityConverter}}" /> + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(ShowAllFonts), Mode=OneWay}" /> + Value="{x:Bind local:Converters.FontWeightToDouble(Appearance.FontWeight), BindBack=Appearance.SetFontWeightFromDouble, Mode=TwoWay}" /> - + Text="{x:Bind local:Converters.StringFallBackToEmptyString('desktopWallpaper', Appearance.BackgroundImagePath), Mode=TwoWay, BindBack=Appearance.SetBackgroundImagePath}" /> - - - - - @@ -106,7 +101,7 @@ + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(IsRenaming), Mode=OneWay}"> (value))); - } - - Foundation::IInspectable ColorToBrushConverter::ConvertBack(Foundation::IInspectable const& /*value*/, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - throw hresult_not_implemented(); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/ColorToBrushConverter.h b/src/cascadia/TerminalSettingsEditor/ColorToBrushConverter.h deleted file mode 100644 index 94c20db6d..000000000 --- a/src/cascadia/TerminalSettingsEditor/ColorToBrushConverter.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "ColorToBrushConverter.g.h" -#include "Utils.h" - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - struct ColorToBrushConverter : ColorToBrushConverterT - { - ColorToBrushConverter() = default; - - Windows::Foundation::IInspectable Convert(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - - Windows::Foundation::IInspectable ConvertBack(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - }; -} - -namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation -{ - BASIC_FACTORY(ColorToBrushConverter); -} diff --git a/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.cpp b/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.cpp deleted file mode 100644 index 1d9379ab3..000000000 --- a/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "ColorToHexConverter.h" -#include "ColorToHexConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable ColorToHexConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - til::color color{ winrt::unbox_value(value) }; - auto hex = winrt::to_hstring(color.ToHexString().data()); - return winrt::box_value(hex); - } - - Foundation::IInspectable ColorToHexConverter::ConvertBack(Foundation::IInspectable const& /*value*/, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - throw hresult_not_implemented(); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.h b/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.h deleted file mode 100644 index 53e2963df..000000000 --- a/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "ColorToHexConverter.g.h" -#include "Utils.h" - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - struct ColorToHexConverter : ColorToHexConverterT - { - ColorToHexConverter() = default; - - Windows::Foundation::IInspectable Convert(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - - Windows::Foundation::IInspectable ConvertBack(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - }; -} - -namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation -{ - BASIC_FACTORY(ColorToHexConverter); -} diff --git a/src/cascadia/TerminalSettingsEditor/Converters.cpp b/src/cascadia/TerminalSettingsEditor/Converters.cpp new file mode 100644 index 000000000..d9cff642e --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/Converters.cpp @@ -0,0 +1,106 @@ +#include "pch.h" +#include "Converters.h" +#if __has_include("Converters.g.cpp") +#include "Converters.g.cpp" +#endif + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + winrt::hstring Converters::AppendPercentageSign(double value) + { + const auto number{ value }; + return to_hstring((int)number) + L"%"; + } + + winrt::Windows::UI::Xaml::Media::SolidColorBrush Converters::ColorToBrush(winrt::Windows::UI::Color color) + { + return Windows::UI::Xaml::Media::SolidColorBrush(color); + } + + winrt::Windows::UI::Text::FontWeight Converters::DoubleToFontWeight(double value) + { + return winrt::Windows::UI::Text::FontWeight{ base::ClampedNumeric(value) }; + } + + double Converters::FontWeightToDouble(winrt::Windows::UI::Text::FontWeight fontWeight) + { + return fontWeight.Weight; + } + + bool Converters::InvertBoolean(bool value) + { + return !value; + } + + winrt::Windows::UI::Xaml::Visibility Converters::InvertedBooleanToVisibility(bool value) + { + return value ? winrt::Windows::UI::Xaml::Visibility::Collapsed : winrt::Windows::UI::Xaml::Visibility::Visible; + } + + winrt::Windows::UI::Color Converters::LightenColor(winrt::Windows::UI::Color color) + { + color.A = 128; // halfway transparent + return color; + } + + double Converters::MaxValueFromPaddingString(winrt::hstring paddingString) + { + const wchar_t singleCharDelim = L','; + std::wstringstream tokenStream(paddingString.c_str()); + std::wstring token; + double maxVal = 0; + size_t* idx = nullptr; + + // Get padding values till we run out of delimiter separated values in the stream + // Non-numeral values detected will default to 0 + // std::getline will not throw exception unless flags are set on the wstringstream + // std::stod will throw invalid_argument exception if the input is an invalid double value + // std::stod will throw out_of_range exception if the input value is more than DBL_MAX + try + { + while (std::getline(tokenStream, token, singleCharDelim)) + { + // std::stod internally calls wcstod which handles whitespace prefix (which is ignored) + // & stops the scan when first char outside the range of radix is encountered + // We'll be permissive till the extent that stod function allows us to be by default + // Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail + const auto curVal = std::stod(token, idx); + if (curVal > maxVal) + { + maxVal = curVal; + } + } + } + catch (...) + { + // If something goes wrong, even if due to a single bad padding value, we'll return default 0 padding + maxVal = 0; + LOG_CAUGHT_EXCEPTION(); + } + + return maxVal; + } + + int Converters::PercentageToPercentageValue(double value) + { + return base::ClampMul(value, 100u); + } + + double Converters::PercentageValueToPercentage(double value) + { + return base::ClampDiv(value, 100); + } + + bool Converters::StringsAreNotEqual(winrt::hstring expected, winrt::hstring actual) + { + return expected != actual; + } + winrt::Windows::UI::Xaml::Visibility Converters::StringNotEmptyToVisibility(winrt::hstring value) + { + return value.empty() ? winrt::Windows::UI::Xaml::Visibility::Collapsed : winrt::Windows::UI::Xaml::Visibility::Visible; + } + winrt::hstring Converters::StringFallBackToEmptyString(winrt::hstring expected, winrt::hstring actual) + { + return expected == actual ? expected : L""; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/Converters.h b/src/cascadia/TerminalSettingsEditor/Converters.h new file mode 100644 index 000000000..d7f99cbf2 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/Converters.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "Converters.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct Converters : ConvertersT + { + static winrt::hstring AppendPercentageSign(double value); + static winrt::Windows::UI::Text::FontWeight DoubleToFontWeight(double value); + static winrt::Windows::UI::Xaml::Media::SolidColorBrush ColorToBrush(winrt::Windows::UI::Color color); + static double FontWeightToDouble(winrt::Windows::UI::Text::FontWeight fontWeight); + static bool InvertBoolean(bool value); + static winrt::Windows::UI::Xaml::Visibility InvertedBooleanToVisibility(bool value); + static winrt::Windows::UI::Color LightenColor(winrt::Windows::UI::Color color); + static double MaxValueFromPaddingString(winrt::hstring paddingString); + static int PercentageToPercentageValue(double value); + static double PercentageValueToPercentage(double value); + static bool StringsAreNotEqual(winrt::hstring expected, winrt::hstring actual); + static winrt::Windows::UI::Xaml::Visibility StringNotEmptyToVisibility(winrt::hstring value); + static winrt::hstring StringFallBackToEmptyString(winrt::hstring expected, winrt::hstring actual); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + struct Converters : ConvertersT + { + }; +} diff --git a/src/cascadia/TerminalSettingsEditor/Converters.idl b/src/cascadia/TerminalSettingsEditor/Converters.idl index 89eb05791..a5b79d5fc 100644 --- a/src/cascadia/TerminalSettingsEditor/Converters.idl +++ b/src/cascadia/TerminalSettingsEditor/Converters.idl @@ -4,63 +4,22 @@ namespace Microsoft.Terminal.Settings.Editor { - runtimeclass ColorLightenConverter : [default] Windows.UI.Xaml.Data.IValueConverter + [bindable] + [default_interface] static runtimeclass Converters { - ColorLightenConverter(); - }; - runtimeclass FontWeightConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - FontWeightConverter(); - }; - - runtimeclass InvertedBooleanConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - InvertedBooleanConverter(); - }; - - runtimeclass InvertedBooleanToVisibilityConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - InvertedBooleanToVisibilityConverter(); - }; - - runtimeclass ColorToBrushConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - ColorToBrushConverter(); - }; - - runtimeclass ColorToHexConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - ColorToHexConverter(); - }; - - runtimeclass PercentageConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - PercentageConverter(); - }; - - runtimeclass PercentageSignConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - PercentageSignConverter(); - }; - - runtimeclass StringIsEmptyConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - StringIsEmptyConverter(); - }; - - runtimeclass PaddingConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - PaddingConverter(); - }; - - runtimeclass StringIsNotDesktopConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - StringIsNotDesktopConverter(); - }; - - runtimeclass DesktopWallpaperToEmptyStringConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - DesktopWallpaperToEmptyStringConverter(); - }; + static String AppendPercentageSign(Double value); + static Windows.UI.Text.FontWeight DoubleToFontWeight(Double value); + static Windows.UI.Xaml.Media.SolidColorBrush ColorToBrush(Windows.UI.Color color); + static Double FontWeightToDouble(Windows.UI.Text.FontWeight fontWeight); + static Boolean InvertBoolean(Boolean value); + static Windows.UI.Xaml.Visibility InvertedBooleanToVisibility(Boolean value); + static Windows.UI.Color LightenColor(Windows.UI.Color color); + static Double MaxValueFromPaddingString(String paddingString); + static Int32 PercentageToPercentageValue(Double value); + static Double PercentageValueToPercentage(Double value); + static Boolean StringsAreNotEqual(String expected, String actual); + static Windows.UI.Xaml.Visibility StringNotEmptyToVisibility(String value); + static String StringFallBackToEmptyString(String expected, String actual); + } } diff --git a/src/cascadia/TerminalSettingsEditor/FontWeightConverter.cpp b/src/cascadia/TerminalSettingsEditor/FontWeightConverter.cpp deleted file mode 100644 index 5b39eded1..000000000 --- a/src/cascadia/TerminalSettingsEditor/FontWeightConverter.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "FontWeightConverter.h" -#include "FontWeightConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; -using namespace winrt::Windows::UI::Text; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable FontWeightConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - const auto weight{ winrt::unbox_value(value) }; - return winrt::box_value(weight.Weight); - } - - Foundation::IInspectable FontWeightConverter::ConvertBack(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - const auto sliderVal{ winrt::unbox_value(value) }; - FontWeight weight{ base::ClampedNumeric(sliderVal) }; - return winrt::box_value(weight); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/FontWeightConverter.h b/src/cascadia/TerminalSettingsEditor/FontWeightConverter.h deleted file mode 100644 index 249e7987d..000000000 --- a/src/cascadia/TerminalSettingsEditor/FontWeightConverter.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "FontWeightConverter.g.h" -#include "Utils.h" - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - struct FontWeightConverter : FontWeightConverterT - { - FontWeightConverter() = default; - - Windows::Foundation::IInspectable Convert(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - - Windows::Foundation::IInspectable ConvertBack(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - }; -} - -namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation -{ - BASIC_FACTORY(FontWeightConverter); -} diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml index ae030da7c..c4f4bf2b4 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml @@ -21,8 +21,6 @@ x:DataType="local:EnumEntry"> - - @@ -79,7 +77,7 @@ - + diff --git a/src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.cpp b/src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.cpp deleted file mode 100644 index d0bb98075..000000000 --- a/src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "InvertedBooleanConverter.h" -#include "InvertedBooleanConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable InvertedBooleanConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - return winrt::box_value(!winrt::unbox_value(value)); - } - - Foundation::IInspectable InvertedBooleanConverter::ConvertBack(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - return winrt::box_value(!winrt::unbox_value(value)); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.h b/src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.h deleted file mode 100644 index 9da6768fe..000000000 --- a/src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.h +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "InvertedBooleanConverter.g.h" -#include "../inc/cppwinrt_utils.h" - -DECLARE_CONVERTER(winrt::Microsoft::Terminal::Settings::Editor, InvertedBooleanConverter); diff --git a/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.cpp b/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.cpp deleted file mode 100644 index fd3c9e6fc..000000000 --- a/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "InvertedBooleanToVisibilityConverter.h" -#include "InvertedBooleanToVisibilityConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable InvertedBooleanToVisibilityConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - return winrt::box_value(winrt::unbox_value(value) ? Visibility::Collapsed : Visibility::Visible); - } - - Foundation::IInspectable InvertedBooleanToVisibilityConverter::ConvertBack(Foundation::IInspectable const& /*value*/, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - throw hresult_not_implemented(); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.h b/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.h deleted file mode 100644 index 90c6bbb74..000000000 --- a/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.h +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "InvertedBooleanToVisibilityConverter.g.h" -#include "../inc/cppwinrt_utils.h" - -DECLARE_CONVERTER(winrt::Microsoft::Terminal::Settings::Editor, InvertedBooleanToVisibilityConverter); diff --git a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj index 4861cb1f4..e60fccb32 100644 --- a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj +++ b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj @@ -42,41 +42,9 @@ AddProfile.xaml - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - KeyChordListener.xaml - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - + Converters.idl + Code EnumEntry.idl @@ -91,6 +59,9 @@ Interaction.xaml + + KeyChordListener.xaml + Launch.xaml @@ -171,41 +142,9 @@ AddProfile.xaml - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - KeyChordListener.xaml - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - + Converters.idl + Code GlobalAppearance.xaml @@ -217,6 +156,9 @@ Interaction.xaml + + KeyChordListener.xaml + Launch.xaml @@ -339,7 +281,6 @@ true false - false @@ -379,4 +320,4 @@ - + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters index 66e8b687d..3b0b9186e 100644 --- a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters +++ b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters @@ -10,16 +10,10 @@ - - Converters - - - Converters - @@ -49,9 +43,4 @@ - - - {00f725c8-41b4-40a8-995e-8ee2e49a4a4c} - - - + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsEditor/PaddingConverter.cpp b/src/cascadia/TerminalSettingsEditor/PaddingConverter.cpp deleted file mode 100644 index 6c0e3472c..000000000 --- a/src/cascadia/TerminalSettingsEditor/PaddingConverter.cpp +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "PaddingConverter.h" -#include "PaddingConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; -using namespace winrt::Windows::UI::Text; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable PaddingConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - const auto& padding = winrt::unbox_value(value); - - const wchar_t singleCharDelim = L','; - std::wstringstream tokenStream(padding.c_str()); - std::wstring token; - double maxVal = 0; - size_t* idx = nullptr; - - // Get padding values till we run out of delimiter separated values in the stream - // Non-numeral values detected will default to 0 - // std::getline will not throw exception unless flags are set on the wstringstream - // std::stod will throw invalid_argument exception if the input is an invalid double value - // std::stod will throw out_of_range exception if the input value is more than DBL_MAX - try - { - while (std::getline(tokenStream, token, singleCharDelim)) - { - // std::stod internally calls wcstod which handles whitespace prefix (which is ignored) - // & stops the scan when first char outside the range of radix is encountered - // We'll be permissive till the extent that stod function allows us to be by default - // Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail - const auto curVal = std::stod(token, idx); - if (curVal > maxVal) - { - maxVal = curVal; - } - } - } - catch (...) - { - // If something goes wrong, even if due to a single bad padding value, we'll return default 0 padding - maxVal = 0; - LOG_CAUGHT_EXCEPTION(); - } - - return winrt::box_value(maxVal); - } - - Foundation::IInspectable PaddingConverter::ConvertBack(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - const auto padding{ winrt::unbox_value(value) }; - return winrt::box_value(winrt::to_hstring(padding)); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/PaddingConverter.h b/src/cascadia/TerminalSettingsEditor/PaddingConverter.h deleted file mode 100644 index c56dd6d9f..000000000 --- a/src/cascadia/TerminalSettingsEditor/PaddingConverter.h +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "PaddingConverter.g.h" -#include "../inc/cppwinrt_utils.h" - -DECLARE_CONVERTER(winrt::Microsoft::Terminal::Settings::Editor, PaddingConverter); diff --git a/src/cascadia/TerminalSettingsEditor/PercentageConverter.cpp b/src/cascadia/TerminalSettingsEditor/PercentageConverter.cpp deleted file mode 100644 index b414532bf..000000000 --- a/src/cascadia/TerminalSettingsEditor/PercentageConverter.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "PercentageConverter.h" -#include "PercentageConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable PercentageConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - const auto decimal{ winrt::unbox_value(value) }; - const unsigned int number{ base::ClampMul(decimal, 100u) }; - return winrt::box_value(number); - } - - Foundation::IInspectable PercentageConverter::ConvertBack(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - const auto number{ winrt::unbox_value(value) }; - const auto decimal{ base::ClampDiv(number, 100) }; - return winrt::box_value(decimal); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/PercentageConverter.h b/src/cascadia/TerminalSettingsEditor/PercentageConverter.h deleted file mode 100644 index e3aa16bbf..000000000 --- a/src/cascadia/TerminalSettingsEditor/PercentageConverter.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "PercentageConverter.g.h" -#include "Utils.h" - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - struct PercentageConverter : PercentageConverterT - { - PercentageConverter() = default; - - Windows::Foundation::IInspectable Convert(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - - Windows::Foundation::IInspectable ConvertBack(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - }; -} - -namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation -{ - BASIC_FACTORY(PercentageConverter); -} diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.h b/src/cascadia/TerminalSettingsEditor/Profiles.h index c39786bfd..65a151cb0 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.h +++ b/src/cascadia/TerminalSettingsEditor/Profiles.h @@ -20,6 +20,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Model::TerminalSettings TermSettings() const; + void SetAcrylicOpacityPercentageValue(double value) + { + AcrylicOpacity(winrt::Microsoft::Terminal::Settings::Editor::Converters::PercentageValueToPercentage(value)); + }; + + void SetPadding(double value) + { + Padding(to_hstring(value)); + } + // starting directory bool UseParentProcessDirectory(); void UseParentProcessDirectory(const bool useParent); diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.idl b/src/cascadia/TerminalSettingsEditor/Profiles.idl index ff8fc6565..4ecc48899 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.idl +++ b/src/cascadia/TerminalSettingsEditor/Profiles.idl @@ -19,6 +19,9 @@ namespace Microsoft.Terminal.Settings.Editor Windows.Foundation.Collections.IObservableVector MonospaceFontList { get; }; Microsoft.Terminal.Settings.Model.TerminalSettings TermSettings { get; }; + void SetAcrylicOpacityPercentageValue(Double value); + void SetPadding(Double value); + Boolean CanDeleteProfile { get; }; Boolean IsBaseLayer; Boolean UseParentProcessDirectory; @@ -105,5 +108,7 @@ namespace Microsoft.Terminal.Settings.Editor IInspectable CurrentScrollState; Windows.Foundation.Collections.IObservableVector ScrollStateList { get; }; + + Windows.UI.Xaml.Controls.Slider AcrylicOpacitySlider { get; }; } } diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.xaml b/src/cascadia/TerminalSettingsEditor/Profiles.xaml index e2fb38987..763532256 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles.xaml @@ -33,17 +33,6 @@ - - - - - - - - - - - @@ -78,7 +67,7 @@ --> + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.IsBaseLayer), Mode=OneWay}"> @@ -90,7 +79,7 @@ ClearSettingValue="{x:Bind State.Profile.ClearCommandline}" HasSettingValue="{x:Bind State.Profile.HasCommandline, Mode=OneWay}" SettingOverrideSource="{x:Bind State.Profile.CommandlineOverrideSource, Mode=OneWay}" - Visibility="{x:Bind State.Profile.IsBaseLayer, Mode=OneWay, Converter={StaticResource InvertedBooleanToVisibilityConverter}}"> + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.IsBaseLayer), Mode=OneWay}"> @@ -150,7 +139,7 @@ + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.IsBaseLayer), Mode=OneWay}"> @@ -280,10 +269,10 @@ + Value="{x:Bind local:Converters.PercentageToPercentageValue(State.Profile.AcrylicOpacity), BindBack=State.Profile.SetAcrylicOpacityPercentageValue, Mode=TwoWay}" /> + Text="{x:Bind local:Converters.AppendPercentageSign(AcrylicOpacitySlider.Value), Mode=OneWay}" /> @@ -307,7 +296,7 @@ + Value="{x:Bind local:Converters.MaxValueFromPaddingString(State.Profile.Padding), BindBack=State.Profile.SetPadding, Mode=TwoWay}" /> @@ -333,7 +322,7 @@ Margin="32,0,0,0" Click="CreateUnfocusedAppearance_Click" Style="{StaticResource BaseButtonStyle}" - Visibility="{x:Bind State.Profile.HasUnfocusedAppearance, Mode=OneWay, Converter={StaticResource InvertedBooleanToVisibilityConverter}}"> + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.HasUnfocusedAppearance), Mode=OneWay}"> diff --git a/src/cascadia/TerminalSettingsEditor/ReadOnlyActions.xaml b/src/cascadia/TerminalSettingsEditor/ReadOnlyActions.xaml index ec1a685fb..eef40cd8d 100644 --- a/src/cascadia/TerminalSettingsEditor/ReadOnlyActions.xaml +++ b/src/cascadia/TerminalSettingsEditor/ReadOnlyActions.xaml @@ -17,9 +17,6 @@ - - - the index returned by _BitScanForward must be divided by 2. const auto haystack1 = _mm_loadu_si128(reinterpret_cast(colorTable.data() + 0)); const auto haystack2 = _mm_loadu_si128(reinterpret_cast(colorTable.data() + 4)); const auto needle = _mm_set1_epi32(__builtin_bit_cast(int, defaultColor)); const auto result1 = _mm_cmpeq_epi32(haystack1, needle); const auto result2 = _mm_cmpeq_epi32(haystack2, needle); const auto result = _mm_packs_epi32(result1, result2); // 3.5 - const auto mask = _mm_movemask_ps(_mm_castsi128_ps(result)); + const auto mask = _mm_movemask_epi8(result); unsigned long index; - return _BitScanForward(&index, mask) ? til::at(colorTable, static_cast(index) + 8) : defaultColor; + return _BitScanForward(&index, mask) ? til::at(colorTable, static_cast(index / 2) + 8) : defaultColor; #else for (size_t i = 0; i < 8; i++) { From 2eb659717c0941626610faa4f3326f7f6a309a94 Mon Sep 17 00:00:00 2001 From: Michael Niksa Date: Wed, 4 Aug 2021 10:00:41 -0700 Subject: [PATCH 44/90] Move to 1ES engineering pools (#10854) Move to 1ES engineering pools ## PR Checklist * [x] Closes #10734 * [x] I work here * [x] If the builds still work, the tests pass. (release and PR builds...) ## Validation Steps Performed - [x] Run the builds associated with this PR - [x] Force run a release build off this branch - [x] Force run a PGO training build off this branch --- build/pipelines/release.yml | 19 +++++++------------ .../templates/build-console-audit-job.yml | 9 ++++++--- .../pipelines/templates/build-console-ci.yml | 9 ++++++--- .../pipelines/templates/build-console-pgo.yml | 9 ++++++--- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index ff6d49cf7..e200db166 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -2,8 +2,9 @@ trigger: none pr: none -pool: - name: Package ES Standard Build +pool: + name: WinDevPool-L + demands: ImageOverride -equals WinDevVS16-latest parameters: - name: branding @@ -70,11 +71,9 @@ jobs: clean: true submodules: true persistCredentials: True - - task: PkgESSetupBuild@10 + - task: PkgESSetupBuild@12 displayName: Package ES - Setup Build inputs: - useDfs: false - productName: OpenConsole disableOutputRedirect: true - task: PowerShell@2 displayName: Rationalize Build Platform @@ -275,11 +274,9 @@ jobs: clean: true submodules: true persistCredentials: True - - task: PkgESSetupBuild@10 + - task: PkgESSetupBuild@12 displayName: Package ES - Setup Build inputs: - useDfs: false - productName: OpenConsole disableOutputRedirect: true - task: DownloadBuildArtifacts@0 displayName: Download Artifacts (*.appx, *.msix) @@ -354,11 +351,9 @@ jobs: clean: true submodules: true persistCredentials: True - - task: PkgESSetupBuild@10 + - task: PkgESSetupBuild@12 displayName: Package ES - Setup Build inputs: - useDfs: false - productName: OpenConsole disableOutputRedirect: true - task: DownloadBuildArtifacts@0 displayName: Download x86 PublicTerminalCore @@ -480,7 +475,7 @@ jobs: mv Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle .\WindowsTerminal.app\ workingDirectory: $(System.ArtifactsDirectory)\appxbundle-signed - - task: PkgESVPack@10 + - task: PkgESVPack@12 displayName: 'Package ES - VPack' env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) diff --git a/build/pipelines/templates/build-console-audit-job.yml b/build/pipelines/templates/build-console-audit-job.yml index 1c9a90d6e..2f1283f34 100644 --- a/build/pipelines/templates/build-console-audit-job.yml +++ b/build/pipelines/templates/build-console-audit-job.yml @@ -8,9 +8,12 @@ jobs: variables: BuildConfiguration: AuditMode BuildPlatform: ${{ parameters.platform }} - pool: "windevbuildagents" - # The public pool is also an option! - # pool: { vmImage: windows-2019 } + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPoolOSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPool-L + demands: ImageOverride -equals WinDevVS16-latest steps: - checkout: self diff --git a/build/pipelines/templates/build-console-ci.yml b/build/pipelines/templates/build-console-ci.yml index 0b9ce8685..0ff8b6b54 100644 --- a/build/pipelines/templates/build-console-ci.yml +++ b/build/pipelines/templates/build-console-ci.yml @@ -11,9 +11,12 @@ jobs: variables: BuildConfiguration: ${{ parameters.configuration }} BuildPlatform: ${{ parameters.platform }} - pool: "windevbuildagents" - # The public pool is also an option! - # pool: { vmImage: windows-2019 } + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPoolOSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPool-L + demands: ImageOverride -equals WinDevVS16-latest steps: - template: build-console-steps.yml diff --git a/build/pipelines/templates/build-console-pgo.yml b/build/pipelines/templates/build-console-pgo.yml index 8af4ec5d8..1e33e82c8 100644 --- a/build/pipelines/templates/build-console-pgo.yml +++ b/build/pipelines/templates/build-console-pgo.yml @@ -12,9 +12,12 @@ jobs: BuildConfiguration: ${{ parameters.configuration }} BuildPlatform: ${{ parameters.platform }} PGOBuildMode: 'Instrument' - pool: "windevbuildagents" - # The public pool is also an option! - # pool: { vmImage: windows-2019 } + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPoolOSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPool-L + demands: ImageOverride -equals WinDevVS16-latest steps: - template: build-console-steps.yml From 2bd46701005609a500a6e68b6089b4c8264a3006 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Thu, 5 Aug 2021 14:08:51 +0100 Subject: [PATCH 45/90] Fix a use-after-free crash when returning from the alt buffer (#10878) ## Summary of the Pull Request When switching from the alt buffer back to the main buffer, we need to copy certain cursor attributes from the one to the other. However, this copying was taking place after the alt buffer had been freed, and thus could result in the app crashing. This PR simply moves that code up a bit so it's prior to the buffer being freed. ## References PR #10843 added the code that introduced this problem. ## 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. Issue number where discussion took place: #xxx ## Validation Steps Performed I was able to reproduce the crash when using a debug build, and confirmed that the crash no longer occurred after this PR was applied. I also checked that the cursor attributes were still being correctly copied back when returning from the alt buffer. --- src/host/screenInfo.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 2506bf450..5323cb574 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -2002,8 +2002,6 @@ void SCREEN_INFORMATION::UseMainScreenBuffer() SCREEN_INFORMATION* psiAlt = psiMain->_psiAlternateBuffer; psiMain->_psiAlternateBuffer = nullptr; - s_RemoveScreenBuffer(psiAlt); // this will also delete the alt buffer - // deleting the alt buffer will give the GetSet back to its main // Copy the alt buffer's cursor style and visibility back to the main buffer. const auto& altCursor = psiAlt->GetTextBuffer().GetCursor(); @@ -2012,6 +2010,9 @@ void SCREEN_INFORMATION::UseMainScreenBuffer() mainCursor.SetIsVisible(altCursor.IsVisible()); mainCursor.SetBlinkingAllowed(altCursor.IsBlinkingAllowed()); + s_RemoveScreenBuffer(psiAlt); // this will also delete the alt buffer + // deleting the alt buffer will give the GetSet back to its main + // Tell the VT MouseInput handler that we're in the main buffer now gci.GetActiveInputBuffer()->GetTerminalInput().UseMainScreenBuffer(); } From 0b4839d94d4429a34275bc4aafb96484791ec5f1 Mon Sep 17 00:00:00 2001 From: Kayla Cinnamon Date: Thu, 5 Aug 2021 06:46:24 -0700 Subject: [PATCH 46/90] Add Split Tab option to tab context menu (#10832) ## Summary of the Pull Request Adds the Split Tab option to the tab context menu. Clicking this option will `auto` split the active pane of the tab into a duplicate pane. Clicking on an unfocused tab and splitting it will bring that tab into focus and split its active pane. We could make this a flyout from the context menu to let people choose horizontal/vertical split in the future if it's requested. I'm also wondering if this should be called Split Pane instead of Split Tab? ## References #1912 ## PR Checklist * [x] Closes #5025 * [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 * [ ] 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 https://user-images.githubusercontent.com/48369326/127691919-aae4683a-212a-4525-a0eb-a61c877461ed.mp4 ## Validation Steps Performed --- .../Resources/en-US/Resources.resw | 5 ++- src/cascadia/TerminalApp/TabManagement.cpp | 24 ++++++++++++ src/cascadia/TerminalApp/TerminalPage.cpp | 39 ++++++++++++++++--- src/cascadia/TerminalApp/TerminalPage.h | 7 ++++ src/cascadia/TerminalApp/TerminalTab.cpp | 19 +++++++++ src/cascadia/TerminalApp/TerminalTab.h | 1 + 6 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 9fa780a3a..5f325b1a6 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -662,10 +662,13 @@ Open a new tab in given starting directory + + Split Tab + Open a new window with given starting directory Split the window and start in given directory - \ No newline at end of file + diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index dd3a7aa7c..b93f50cab 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -192,6 +192,16 @@ namespace winrt::TerminalApp::implementation } }); + newTabImpl->SplitTabRequested([weakTab, weakThis{ get_weak() }]() { + auto page{ weakThis.get() }; + auto tab{ weakTab.get() }; + + if (page && tab) + { + page->_SplitTab(*tab); + } + }); + auto tabViewItem = newTabImpl->TabViewItem(); _tabView.TabItems().Append(tabViewItem); @@ -357,6 +367,20 @@ namespace winrt::TerminalApp::implementation CATCH_LOG(); } + // Method Description: + // - Sets the specified tab as the focused tab and splits its active pane + // Arguments: + // - tab: tab to split + void TerminalPage::_SplitTab(TerminalTab& tab) + { + try + { + _SetFocusedTab(tab); + _SplitPane(tab, SplitState::Automatic, SplitType::Duplicate); + } + CATCH_LOG(); + } + // Method Description: // - Removes the tab (both TerminalControl and XAML) after prompting for approval // Arguments: diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 281189920..96e5f38e0 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1238,6 +1238,33 @@ namespace winrt::TerminalApp::implementation return; } + _SplitPane(*focusedTab, splitType, splitMode, splitSize, newTerminalArgs); + } + + // Method Description: + // - Split the focused pane of the given tab, either horizontally or vertically, and place the + // given TermControl into the newly created pane. + // - If splitType == SplitState::None, this method does nothing. + // Arguments: + // - tab: The tab that is going to be split. + // - splitType: one value from the TerminalApp::SplitState enum, indicating how the + // new pane should be split from its parent. + // - splitMode: value from TerminalApp::SplitType enum, indicating the profile to be used in the newly split pane. + // - newTerminalArgs: An object that may contain a blob of parameters to + // control which profile is created and with possible other + // configurations. See CascadiaSettings::BuildSettings for more details. + void TerminalPage::_SplitPane(TerminalTab& tab, + const SplitState splitType, + const SplitType splitMode, + const float splitSize, + const NewTerminalArgs& newTerminalArgs) + { + // Do nothing if we're requesting no split. + if (splitType == SplitState::None) + { + return; + } + try { TerminalSettingsCreateResult controlSettings{ nullptr }; @@ -1246,12 +1273,12 @@ namespace winrt::TerminalApp::implementation if (splitMode == SplitType::Duplicate) { - std::optional current_guid = focusedTab->GetFocusedProfile(); + std::optional current_guid = tab.GetFocusedProfile(); if (current_guid) { profileFound = true; controlSettings = TerminalSettings::CreateWithProfileByID(_settings, current_guid.value(), *_bindings); - const auto workingDirectory = focusedTab->GetActiveTerminalControl().WorkingDirectory(); + const auto workingDirectory = tab.GetActiveTerminalControl().WorkingDirectory(); const auto validWorkingDirectory = !workingDirectory.empty(); if (validWorkingDirectory) { @@ -1287,10 +1314,10 @@ namespace winrt::TerminalApp::implementation auto realSplitType = splitType; if (realSplitType == SplitState::Automatic) { - realSplitType = focusedTab->PreCalculateAutoSplit(availableSpace); + realSplitType = tab.PreCalculateAutoSplit(availableSpace); } - const auto canSplit = focusedTab->PreCalculateCanSplit(realSplitType, splitSize, availableSpace); + const auto canSplit = tab.PreCalculateCanSplit(realSplitType, splitSize, availableSpace); if (!canSplit) { return; @@ -1299,11 +1326,11 @@ namespace winrt::TerminalApp::implementation auto newControl = _InitControl(controlSettings, controlConnection); // Hookup our event handlers to the new terminal - _RegisterTerminalEvents(newControl, *focusedTab); + _RegisterTerminalEvents(newControl, tab); _UnZoomIfNeeded(); - focusedTab->SplitPane(realSplitType, splitSize, realGuid, newControl); + tab.SplitPane(realSplitType, splitSize, realGuid, newControl); } CATCH_LOG(); } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index aa2db9529..d6476b5ad 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -219,6 +219,8 @@ namespace winrt::TerminalApp::implementation void _DuplicateFocusedTab(); void _DuplicateTab(const TerminalTab& tab); + void _SplitTab(TerminalTab& tab); + winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::TabBase tab); void _CloseTabAtIndex(uint32_t index); void _RemoveTab(const winrt::TerminalApp::TabBase& tab); @@ -254,6 +256,11 @@ namespace winrt::TerminalApp::implementation const Microsoft::Terminal::Settings::Model::SplitType splitMode = Microsoft::Terminal::Settings::Model::SplitType::Manual, const float splitSize = 0.5f, const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr); + void _SplitPane(TerminalTab& tab, + const Microsoft::Terminal::Settings::Model::SplitState splitType, + const Microsoft::Terminal::Settings::Model::SplitType splitMode = Microsoft::Terminal::Settings::Model::SplitType::Manual, + const float splitSize = 0.5f, + const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr); void _ResizePane(const Microsoft::Terminal::Settings::Model::ResizeDirection& direction); void _ToggleSplitOrientation(); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 4e420c5c2..ad4595a1b 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -936,12 +936,30 @@ namespace winrt::TerminalApp::implementation duplicateTabMenuItem.Icon(duplicateTabSymbol); } + Controls::MenuFlyoutItem splitTabMenuItem; + { + // "Split Tab" + Controls::FontIcon splitTabSymbol; + splitTabSymbol.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); + splitTabSymbol.Glyph(L"\xF246"); // ViewDashboard + + splitTabMenuItem.Click([weakThis](auto&&, auto&&) { + if (auto tab{ weakThis.get() }) + { + tab->_SplitTabRequestedHandlers(); + } + }); + splitTabMenuItem.Text(RS_(L"SplitTabText")); + splitTabMenuItem.Icon(splitTabSymbol); + } + // Build the menu Controls::MenuFlyout contextMenuFlyout; Controls::MenuFlyoutSeparator menuSeparator; contextMenuFlyout.Items().Append(chooseColorMenuItem); contextMenuFlyout.Items().Append(renameTabMenuItem); contextMenuFlyout.Items().Append(duplicateTabMenuItem); + contextMenuFlyout.Items().Append(splitTabMenuItem); contextMenuFlyout.Items().Append(menuSeparator); // GH#5750 - When the context menu is dismissed with ESC, toss the focus @@ -1315,4 +1333,5 @@ namespace winrt::TerminalApp::implementation DEFINE_EVENT(TerminalTab, ColorCleared, _colorCleared, winrt::delegate<>); DEFINE_EVENT(TerminalTab, TabRaiseVisualBell, _TabRaiseVisualBellHandlers, winrt::delegate<>); DEFINE_EVENT(TerminalTab, DuplicateRequested, _DuplicateRequestedHandlers, winrt::delegate<>); + DEFINE_EVENT(TerminalTab, SplitTabRequested, _SplitTabRequestedHandlers, winrt::delegate<>); } diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 3ef935364..f3cee7555 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -94,6 +94,7 @@ namespace winrt::TerminalApp::implementation DECLARE_EVENT(ColorCleared, _colorCleared, winrt::delegate<>); DECLARE_EVENT(TabRaiseVisualBell, _TabRaiseVisualBellHandlers, winrt::delegate<>); DECLARE_EVENT(DuplicateRequested, _DuplicateRequestedHandlers, winrt::delegate<>); + DECLARE_EVENT(SplitTabRequested, _SplitTabRequestedHandlers, winrt::delegate<>); TYPED_EVENT(TaskbarProgressChanged, IInspectable, IInspectable); private: From 76793b1e3fd303d681016d019407c509e91575aa Mon Sep 17 00:00:00 2001 From: Leon Liang Date: Thu, 5 Aug 2021 10:05:21 -0700 Subject: [PATCH 47/90] [DefApp] Move from Monarch multi instance servers to Peasant single instance servers (#10823) - Monarch no longer sets itself up as a `CTerminalHandoff` multi instance server by default - In fact, `CTerminalHandoff` will only ever be a single instance server - When COM needs a `CTerminalHandoff`, it launches `wt.exe -embedding`, which gets picked up by the Monarch and then gets handed off to itself/peasant depending on user settings. - Peasant now recognizes the `-embedding` commandline and will start a `CTerminalHandoff` single instance listener, and receives the connection into a new tab. Closes #10358 --- .../TerminalApp/AppActionHandlers.cpp | 4 +- src/cascadia/TerminalApp/AppLogic.cpp | 5 ++ src/cascadia/TerminalApp/TabManagement.cpp | 6 +- src/cascadia/TerminalApp/TerminalPage.cpp | 69 ++++++++++--------- src/cascadia/TerminalApp/TerminalPage.h | 4 +- .../TerminalConnection/CTerminalHandoff.cpp | 27 ++++++-- .../TerminalConnection/CTerminalHandoff.h | 6 +- src/cascadia/WindowsTerminal/AppHost.cpp | 3 - 8 files changed, 74 insertions(+), 50 deletions(-) diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 60401122c..f88017e1e 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -264,12 +264,12 @@ namespace winrt::TerminalApp::implementation { if (args == nullptr) { - _OpenNewTab(nullptr); + LOG_IF_FAILED(_OpenNewTab(nullptr)); args.Handled(true); } else if (const auto& realArgs = args.ActionArgs().try_as()) { - _OpenNewTab(realArgs.TerminalArgs()); + LOG_IF_FAILED(_OpenNewTab(realArgs.TerminalArgs())); args.Handled(true); } } diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 441fbd4de..e9350ef95 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -1229,6 +1229,11 @@ namespace winrt::TerminalApp::implementation auto actions = winrt::single_threaded_vector(std::move(appArgs.GetStartupActions())); _root->ProcessStartupActions(actions, false, cwd); + + if (appArgs.IsHandoffListener()) + { + _root->SetInboundListener(true); + } } // Return the result of parsing with commandline, though it may or may not be used. return result; diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index b93f50cab..dd9e9dc96 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -56,7 +56,7 @@ namespace winrt::TerminalApp::implementation // - existingConnection: An optional connection that is already established to a PTY // for this tab to host instead of creating one. // If not defined, the tab will create the connection. - void TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection) + HRESULT TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection) try { const auto profileGuid{ _settings.GetProfileForArgs(newTerminalArgs) }; @@ -89,8 +89,10 @@ namespace winrt::TerminalApp::implementation TraceLoggingWideString(schemeName.data(), "SchemeName", "Color scheme set in the settings"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + + return S_OK; } - CATCH_LOG(); + CATCH_RETURN(); // Method Description: // - Creates a new tab with the given settings. If the tab bar is not being diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 96e5f38e0..3bbf27eab 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -19,6 +19,8 @@ #include "RenameWindowRequestedArgs.g.cpp" #include "../inc/WindowingBehavior.h" +#include + using namespace winrt; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::UI::Xaml; @@ -348,34 +350,12 @@ namespace winrt::TerminalApp::implementation winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::StartInboundListener(); } // If we failed to start the listener, it will throw. - // We should fail fast here or the Terminal will be in a very strange state. - // We only start the listener if the Terminal was started with the COM server - // `-Embedding` flag and we make no tabs as a result. - // Therefore, if the listener cannot start itself up to make that tab with - // the inbound connection that caused the COM activation in the first place... - // we would be left with an empty terminal frame with no tabs. - // Instead, crash out so COM sees the server die and things unwind - // without a weird empty frame window. + // We don't want to fail fast here because if a peasant has some trouble with + // starting the listener, we don't want it to crash and take all its tabs down + // with it. catch (...) { - // However, we cannot always fail fast because of MSFT:33501832. Sometimes the COM catalog - // tears the state between old and new versions and fails here for that reason. - // As we're always becoming an inbound server in the monarch, even when COM didn't strictly - // ask us yet...we might just crash always. - // Instead... we're going to differentiate. If COM started us... we will fail fast - // so it sees the process die and falls back. - // If we were just starting normally as a Monarch and opportunistically listening for - // inbound connections... then we'll just log the failure and move on assuming - // the version state is torn and will fix itself whenever the packaging upgrade - // tasks decide to clean up. - if (_isEmbeddingInboundListener) - { - FAIL_FAST_CAUGHT_EXCEPTION(); - } - else - { - LOG_CAUGHT_EXCEPTION(); - } + LOG_CAUGHT_EXCEPTION(); } } } @@ -759,7 +739,7 @@ namespace winrt::TerminalApp::implementation } else { - this->_OpenNewTab(newTerminalArgs); + LOG_IF_FAILED(this->_OpenNewTab(newTerminalArgs)); } } @@ -2405,13 +2385,38 @@ namespace winrt::TerminalApp::implementation return _isAlwaysOnTop; } - void TerminalPage::_OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection) + HRESULT TerminalPage::_OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection) { - // TODO: GH 9458 will give us more context so we can try to choose a better profile. - _OpenNewTab(nullptr, connection); + // We need to be on the UI thread in order for _OpenNewTab to run successfully. + // HasThreadAccess will return true if we're currently on a UI thread and false otherwise. + // When we're on a COM thread, we'll need to dispatch the calls to the UI thread + // and wait on it hence the locking mechanism. + if (Dispatcher().HasThreadAccess()) + { + // TODO: GH 9458 will give us more context so we can try to choose a better profile. + auto hr = _OpenNewTab(nullptr, connection); - // Request a summon of this window to the foreground - _SummonWindowRequestedHandlers(*this, nullptr); + // Request a summon of this window to the foreground + _SummonWindowRequestedHandlers(*this, nullptr); + + return hr; + } + else + { + til::latch latch{ 1 }; + HRESULT finalVal = S_OK; + + Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [&]() { + finalVal = _OpenNewTab(nullptr, connection); + + _SummonWindowRequestedHandlers(*this, nullptr); + + latch.count_down(); + }); + + latch.wait(); + return finalVal; + } } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index d6476b5ad..bb1a6f13e 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -188,7 +188,7 @@ namespace winrt::TerminalApp::implementation void _CreateNewTabFlyout(); void _OpenNewTabDropdown(); - void _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); void _CreateNewTabFromSettings(GUID profileGuid, const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(GUID profileGuid, Microsoft::Terminal::Settings::Model::TerminalSettings settings); @@ -344,7 +344,7 @@ namespace winrt::TerminalApp::implementation winrt::Microsoft::Terminal::Settings::Model::Command _lastPreviewedCommand{ nullptr }; winrt::Microsoft::Terminal::Settings::Model::TerminalSettings _originalSettings{ nullptr }; - void _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); + HRESULT _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); void _WindowRenamerActionClick(const IInspectable& sender, const IInspectable& eventArgs); diff --git a/src/cascadia/TerminalConnection/CTerminalHandoff.cpp b/src/cascadia/TerminalConnection/CTerminalHandoff.cpp index 0138921fd..5023a8188 100644 --- a/src/cascadia/TerminalConnection/CTerminalHandoff.cpp +++ b/src/cascadia/TerminalConnection/CTerminalHandoff.cpp @@ -11,6 +11,8 @@ using namespace Microsoft::WRL; static NewHandoffFunction _pfnHandoff = nullptr; // The registration ID of the class object for clean up later static DWORD g_cTerminalHandoffRegistration = 0; +// Mutex so we only do start/stop/establish one at a time. +static std::shared_mutex _mtx; // Routine Description: // - Starts listening for TerminalHandoff requests by registering @@ -19,9 +21,11 @@ static DWORD g_cTerminalHandoffRegistration = 0; // - pfnHandoff - Function to callback when a handoff is received // Return Value: // - S_OK, E_NOT_VALID_STATE (start called when already started) or relevant COM registration error. -HRESULT CTerminalHandoff::s_StartListening(NewHandoffFunction pfnHandoff) noexcept +HRESULT CTerminalHandoff::s_StartListening(NewHandoffFunction pfnHandoff) try { + std::unique_lock lock{ _mtx }; + RETURN_HR_IF(E_NOT_VALID_STATE, _pfnHandoff != nullptr); const auto classFactory = Make>(); @@ -31,7 +35,7 @@ try ComPtr unk; RETURN_IF_FAILED(classFactory.As(&unk)); - RETURN_IF_FAILED(CoRegisterClassObject(__uuidof(CTerminalHandoff), unk.Get(), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &g_cTerminalHandoffRegistration)); + RETURN_IF_FAILED(CoRegisterClassObject(__uuidof(CTerminalHandoff), unk.Get(), CLSCTX_LOCAL_SERVER, REGCLS_SINGLEUSE, &g_cTerminalHandoffRegistration)); _pfnHandoff = pfnHandoff; @@ -46,8 +50,10 @@ CATCH_RETURN() // - // Return Value: // - S_OK, E_NOT_VALID_STATE (stop called when not started), or relevant COM class revoke error -HRESULT CTerminalHandoff::s_StopListening() noexcept +HRESULT CTerminalHandoff::s_StopListening() { + std::unique_lock lock{ _mtx }; + RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _pfnHandoff); _pfnHandoff = nullptr; @@ -91,10 +97,19 @@ static HRESULT _duplicateHandle(const HANDLE in, HANDLE& out) noexcept // - E_NOT_VALID_STATE if a event handler is not registered before calling. `::DuplicateHandle` // error codes if we cannot manage to make our own copy of handles to retain. Or S_OK/error // from the registered handler event function. -HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client) noexcept +HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client) { + // Stash a local copy of _pfnHandoff before we stop listening. + auto localPfnHandoff = _pfnHandoff; + + // Because we are REGCLS_SINGLEUSE... we need to `CoRevokeClassObject` after we handle this ONE call. + // COM does not automatically clean that up for us. We must do it. + s_StopListening(); + + std::unique_lock lock{ _mtx }; + // Report an error if no one registered a handoff function before calling this. - RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _pfnHandoff); + RETURN_HR_IF_NULL(E_NOT_VALID_STATE, localPfnHandoff); // Duplicate the handles from what we received. // The contract with COM specifies that any HANDLEs we receive from the caller belong @@ -108,5 +123,5 @@ HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE sign RETURN_IF_FAILED(_duplicateHandle(client, client)); // Call registered handler from when we started listening. - return _pfnHandoff(in, out, signal, ref, server, client); + return localPfnHandoff(in, out, signal, ref, server, client); } diff --git a/src/cascadia/TerminalConnection/CTerminalHandoff.h b/src/cascadia/TerminalConnection/CTerminalHandoff.h index 2e173e0a3..2ba644f41 100644 --- a/src/cascadia/TerminalConnection/CTerminalHandoff.h +++ b/src/cascadia/TerminalConnection/CTerminalHandoff.h @@ -37,12 +37,12 @@ struct __declspec(uuid(__CLSID_CTerminalHandoff)) HANDLE signal, HANDLE ref, HANDLE server, - HANDLE client) noexcept override; + HANDLE client) override; #pragma endregion - static HRESULT s_StartListening(NewHandoffFunction pfnHandoff) noexcept; - static HRESULT s_StopListening() noexcept; + static HRESULT s_StartListening(NewHandoffFunction pfnHandoff); + static HRESULT s_StopListening(); }; // Disable warnings from the CoCreatableClass macro as the value it provides for diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 53e21ccdd..092fe8eae 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -656,9 +656,6 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s const winrt::Windows::Foundation::IInspectable& /*args*/) { _setupGlobalHotkeys(); - - // The monarch is just going to be THE listener for inbound connections. - _listenForInboundConnections(); } void AppHost::_listenForInboundConnections() From dcbf7c74f18a344ae255b62dbec31c2965a0433f Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 5 Aug 2021 23:33:44 +0200 Subject: [PATCH 48/90] Reload settings when the input method changes (#10876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `VkKeyScanW` as well as `MapVirtualKeyW` are used throughout the project, but are input method sensitive functions. Since #10666 `win+sc(41)` is used as the quake mode keybinding, which is then mapped to a virtual key in order to call `RegisterHotKey`. This mapping is highly dependent on the input method and the quake mode key binding will fail to work once the input method was changed. ## PR Checklist * [x] Closes #10729 * [x] I work here * [ ] Tests added/passed ## Validation Steps Performed * win+` opens quake window before & after changing keyboard layout ✔️ * keyboard layout changes while WT is minimized trigger reloaded ✔️ --- src/cascadia/TerminalApp/AppLogic.cpp | 4 ++ src/cascadia/TerminalApp/AppLogic.h | 16 ++++--- .../TerminalApp/LanguageProfileNotifier.cpp | 42 +++++++++++++++++++ .../TerminalApp/LanguageProfileNotifier.h | 21 ++++++++++ .../TerminalApp/TerminalAppLib.vcxproj | 12 +++--- .../TerminalAppLib.vcxproj.filters | 25 ++++------- src/cascadia/TerminalApp/pch.h | 1 + 7 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 src/cascadia/TerminalApp/LanguageProfileNotifier.cpp create mode 100644 src/cascadia/TerminalApp/LanguageProfileNotifier.h diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index e9350ef95..a9ac10d8f 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -209,6 +209,10 @@ namespace winrt::TerminalApp::implementation self->_ReloadSettings(); } }); + + _languageProfileNotifier = winrt::make_self([this]() { + _reloadSettings->Run(); + }); } // Method Description: diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 3d7782039..6d2e36d9e 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -5,8 +5,9 @@ #include "AppLogic.g.h" #include "FindTargetWindowResult.g.h" -#include "TerminalPage.h" #include "Jumplist.h" +#include "LanguageProfileNotifier.h" +#include "TerminalPage.h" #include #include @@ -110,12 +111,8 @@ namespace winrt::TerminalApp::implementation // ALSO: If you add any UIElements as roots here, make sure they're // updated in _ApplyTheme. The root currently is _root. winrt::com_ptr _root{ nullptr }; - Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; - wil::unique_folder_change_reader_nothrow _reader; - std::shared_ptr> _reloadSettings; - til::throttled_func_trailing<> _reloadState; winrt::hstring _settingsLoadExceptionText; HRESULT _settingsLoadedResult = S_OK; bool _loadedInitialSettings = false; @@ -124,6 +121,15 @@ namespace winrt::TerminalApp::implementation ::TerminalApp::AppCommandlineArgs _appArgs; ::TerminalApp::AppCommandlineArgs _settingsAppArgs; + + std::shared_ptr> _reloadSettings; + til::throttled_func_trailing<> _reloadState; + + // These fields invoke _reloadSettings and must be destroyed before _reloadSettings. + // (C++ destroys members in reverse-declaration-order.) + winrt::com_ptr _languageProfileNotifier; + wil::unique_folder_change_reader_nothrow _reader; + static TerminalApp::FindTargetWindowResult _doFindTargetWindow(winrt::array_view args, const Microsoft::Terminal::Settings::Model::WindowingMode& windowingBehavior); diff --git a/src/cascadia/TerminalApp/LanguageProfileNotifier.cpp b/src/cascadia/TerminalApp/LanguageProfileNotifier.cpp new file mode 100644 index 000000000..9655947a4 --- /dev/null +++ b/src/cascadia/TerminalApp/LanguageProfileNotifier.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "LanguageProfileNotifier.h" + +using namespace winrt::TerminalApp::implementation; + +LanguageProfileNotifier::LanguageProfileNotifier(std::function&& callback) : + _callback{ std::move(callback) }, + _currentKeyboardLayout{ GetKeyboardLayout(0) } +{ + const auto manager = wil::CoCreateInstance(CLSID_TF_ThreadMgr); + _source = manager.query(); + if (FAILED(_source->AdviseSink(IID_ITfInputProcessorProfileActivationSink, static_cast(this), &_cookie))) + { + _cookie = TF_INVALID_COOKIE; + THROW_LAST_ERROR(); + } +} + +LanguageProfileNotifier::~LanguageProfileNotifier() +{ + if (_cookie != TF_INVALID_COOKIE) + { + _source->UnadviseSink(_cookie); + } +} + +STDMETHODIMP LanguageProfileNotifier::OnActivated(DWORD /*dwProfileType*/, LANGID /*langid*/, REFCLSID /*clsid*/, REFGUID /*catid*/, REFGUID /*guidProfile*/, HKL hkl, DWORD /*dwFlags*/) +{ + if (hkl && hkl != _currentKeyboardLayout) + { + _currentKeyboardLayout = hkl; + try + { + _callback(); + } + CATCH_RETURN(); + } + return S_OK; +} diff --git a/src/cascadia/TerminalApp/LanguageProfileNotifier.h b/src/cascadia/TerminalApp/LanguageProfileNotifier.h new file mode 100644 index 000000000..865330455 --- /dev/null +++ b/src/cascadia/TerminalApp/LanguageProfileNotifier.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace winrt::TerminalApp::implementation +{ + class LanguageProfileNotifier : public winrt::implements + { + public: + explicit LanguageProfileNotifier(std::function&& callback); + ~LanguageProfileNotifier(); + STDMETHODIMP OnActivated(DWORD dwProfileType, LANGID langid, REFCLSID clsid, REFGUID catid, REFGUID guidProfile, HKL hkl, DWORD dwFlags); + + private: + std::function _callback; + wil::com_ptr _source; + DWORD _cookie = TF_INVALID_COOKIE; + HKL _currentKeyboardLayout; + }; +} diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index 67317f4e4..25cd7cfa3 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -53,7 +53,7 @@ Designer - + Designer @@ -74,6 +74,7 @@ + MinMaxCloseControl.xaml @@ -112,7 +113,7 @@ HighlightedTextControl.xaml - + ColorPickupFlyout.xaml @@ -139,7 +140,7 @@ AppLogic.idl - + @@ -149,6 +150,7 @@ + MinMaxCloseControl.xaml @@ -195,7 +197,7 @@ HighlightedTextControl.xaml - + ColorPickupFlyout.xaml @@ -280,7 +282,7 @@ HighlightedTextControl.xaml Code - + ColorPickupFlyout.xaml Code diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters index 1511e14d9..f041b68d7 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters @@ -13,9 +13,6 @@ pane - - tab - pane @@ -23,7 +20,6 @@ - tab @@ -49,10 +45,10 @@ highlightedText + + - - app @@ -60,9 +56,6 @@ pane - - tab - @@ -92,14 +85,13 @@ highlightedText + + app - - settings - settings @@ -107,9 +99,6 @@ settings - - tab - tab @@ -125,6 +114,9 @@ tab + + + @@ -160,9 +152,6 @@ commandPalette - - commandPalette - commandPalette diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h index 299d7de63..1f84f821e 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -66,6 +66,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalAppProvider); #include #include +#include #include #include From 90ff261c35a1b3e50267576f5cbdc66733731101 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Fri, 6 Aug 2021 21:41:02 +0100 Subject: [PATCH 49/90] Add support for downloadable soft fonts (#10011) This PR adds conhost support for downloadable soft fonts - also known as dynamically redefinable character sets (DRCS) - using the `DECDLD` escape sequence. These fonts are typically designed to work on a specific terminal model, and each model tends to have a different character cell size. So in order to support as many models as possible, the code attempts to detect the original target size of the font, and then scale the glyphs to fit our current cell size. Once a font has been downloaded to the terminal, it can be designated in the same way you would a standard character set, using an `SCS` escape sequence. The identification string for the set is defined by the `DECDLD` sequence. Internally we map the characters in this set to code points `U+EF20` to `U+EF7F` in the Unicode private use are (PUA). Then in the renderer, any characters in that range are split off into separate runs, which get painted with a special font. The font itself is dynamically generated as an in-memory resource, constructed from the downloaded character bitmaps which have been scaled to the appropriate size. If no soft fonts are in use, then no mapping of the PUA code points will take place, so this shouldn't interfere with anyone using those code points for something else, as along as they aren't also trying to use soft fonts. I also tried to pick a PUA range that hadn't already been snatched up by Nerd Fonts, but if we do receive reports of a conflict, it's easy enough to change. ## Validation Steps Performed I added an adapter test that runs through a bunch of parameter variations for the `DECDLD` sequence, to make sure we're correctly detecting the font sizes for most of the known DEC terminal models. I've also tested manually on a wide range of existing fonts, of varying dimensions, and from multiple sources, and made sure they all worked reasonably well. Closes #9164 --- .github/actions/spelling/allow/apis.txt | 1 + .github/actions/spelling/expect/expect.txt | 14 + src/host/getset.cpp | 24 + src/host/getset.h | 4 + src/host/outputStream.cpp | 15 + src/host/outputStream.hpp | 4 + src/host/ut_host/VtRendererTests.cpp | 50 +- src/interactivity/onecore/BgfxEngine.cpp | 1 + src/interactivity/onecore/BgfxEngine.hpp | 1 + src/renderer/base/FontResource.cpp | 285 +++++++++ src/renderer/base/RenderEngineBase.cpp | 7 + src/renderer/base/lib/base.vcxproj | 2 + src/renderer/base/lib/base.vcxproj.filters | 6 + src/renderer/base/renderer.cpp | 52 +- src/renderer/base/renderer.hpp | 13 +- src/renderer/base/sources.inc | 1 + src/renderer/dx/DxRenderer.cpp | 2 + src/renderer/dx/DxRenderer.hpp | 1 + src/renderer/gdi/gdirenderer.hpp | 15 +- src/renderer/gdi/paint.cpp | 4 + src/renderer/gdi/state.cpp | 63 +- src/renderer/inc/FontResource.hpp | 45 ++ src/renderer/inc/IRenderEngine.hpp | 4 + src/renderer/inc/IRenderer.hpp | 4 + src/renderer/inc/RenderEngineBase.hpp | 4 + src/renderer/uia/UiaRenderer.cpp | 2 + src/renderer/uia/UiaRenderer.hpp | 1 + src/renderer/vt/Xterm256Engine.cpp | 2 + src/renderer/vt/Xterm256Engine.hpp | 1 + src/renderer/vt/XtermEngine.cpp | 2 + src/renderer/vt/XtermEngine.hpp | 1 + src/renderer/vt/vtrenderer.hpp | 1 + src/renderer/wddmcon/WddmConRenderer.cpp | 1 + src/renderer/wddmcon/WddmConRenderer.hpp | 1 + src/terminal/adapter/DispatchTypes.hpp | 40 ++ src/terminal/adapter/FontBuffer.cpp | 603 ++++++++++++++++++ src/terminal/adapter/FontBuffer.hpp | 95 +++ src/terminal/adapter/ITermDispatch.hpp | 11 + src/terminal/adapter/adaptDispatch.cpp | 87 +++ src/terminal/adapter/adaptDispatch.hpp | 11 + src/terminal/adapter/charsets.hpp | 11 + src/terminal/adapter/conGetSet.hpp | 4 + src/terminal/adapter/lib/adapter.vcxproj | 2 + .../adapter/lib/adapter.vcxproj.filters | 6 + src/terminal/adapter/sources.inc | 1 + src/terminal/adapter/termDispatch.hpp | 9 + src/terminal/adapter/terminalOutput.cpp | 234 ++++--- src/terminal/adapter/terminalOutput.hpp | 7 + .../adapter/ut_adapter/adapterTest.cpp | 238 +++++++ .../parser/OutputStateMachineEngine.cpp | 19 +- .../parser/OutputStateMachineEngine.hpp | 7 +- 51 files changed, 1903 insertions(+), 116 deletions(-) create mode 100644 src/renderer/base/FontResource.cpp create mode 100644 src/renderer/inc/FontResource.hpp create mode 100644 src/terminal/adapter/FontBuffer.cpp create mode 100644 src/terminal/adapter/FontBuffer.hpp diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 87d479f7f..85121e065 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -133,6 +133,7 @@ SRWLOCK STDCPP STDMETHOD strchr +strcpy streambuf strtoul Stubless diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 94f8fc911..05ccc895a 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -169,6 +169,7 @@ brandings BRK Browsable bsearch +Bspace bstr BTNFACE buf @@ -270,10 +271,12 @@ cmder CMDEXT Cmdlet cmdline +cmh CMOUSEBUTTONS cmp cmpeq cmt +cmw cmyk CNL cnt @@ -406,11 +409,13 @@ csbiex csharp CSHORT CSIDL +Cspace csproj Csr csrmsg CSRSS csrutil +css cstdarg cstddef cstdio @@ -509,6 +514,7 @@ DECAWM DECCKM DECCOLM DECDHL +DECDLD DECDWL DECEKBD DECID @@ -789,6 +795,7 @@ FONTENUMPROC FONTFACE FONTFAMILY FONTHEIGHT +FONTINFO fontlist FONTOK FONTSIZE @@ -902,6 +909,7 @@ github gitlab gle globals +GLYPHENTRY gmail GMEM GNUC @@ -950,6 +958,7 @@ hdrstop HEIGHTSCROLL hfile hfont +hfontresource hglobal hhh HHmm @@ -1272,6 +1281,7 @@ locsrc locstudio Loewen LOGFONT +LOGFONTA LOGFONTW logissue lowercased @@ -1935,6 +1945,7 @@ realloc reamapping rects redef +redefinable Redir redirector redist @@ -1980,6 +1991,7 @@ rfc rftp rgb rgba +RGBCOLOR rgbi rgci rgfae @@ -2149,6 +2161,7 @@ SIGDN SINGLEFLAG SINGLETHREADED siup +sixel SIZEBOX sizeof SIZESCROLL @@ -2754,6 +2767,7 @@ WTo wtof wtoi WTs +WTSOFTFONT wtw wtypes Wubi diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 6e9e70191..9c3ccde44 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -1639,6 +1639,30 @@ void DoSrvEndHyperlink(SCREEN_INFORMATION& screenInfo) screenInfo.GetTextBuffer().SetCurrentAttributes(attr); } +// Routine Description: +// - A private API call for updating the active soft font. +// Arguments: +// - bitPattern - An array of scanlines representing all the glyphs in the font. +// - cellSize - The cell size for an individual glyph. +// - centeringHint - The horizontal extent that glyphs are offset from center. +// Return Value: +// - S_OK if we succeeded, otherwise the HRESULT of the failure. +[[nodiscard]] HRESULT DoSrvUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept +{ + try + { + auto* pRender = ServiceLocator::LocateGlobals().pRender; + if (pRender) + { + pRender->UpdateSoftFont(bitPattern, cellSize, centeringHint); + } + return S_OK; + } + CATCH_RETURN(); +} + // Routine Description: // - A private API call for forcing the renderer to repaint the screen. If the // input screen buffer is not the active one, then just do nothing. We only diff --git a/src/host/getset.h b/src/host/getset.h index 43b8d5fef..1fb48e286 100644 --- a/src/host/getset.h +++ b/src/host/getset.h @@ -55,6 +55,10 @@ void DoSrvAddHyperlink(SCREEN_INFORMATION& screenInfo, void DoSrvEndHyperlink(SCREEN_INFORMATION& screenInfo); +[[nodiscard]] HRESULT DoSrvUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept; + void DoSrvPrivateRefreshWindow(const SCREEN_INFORMATION& screenInfo); [[nodiscard]] HRESULT DoSrvSetConsoleOutputCodePage(const unsigned int codepage); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 3129f0a1a..a70b48afd 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -818,3 +818,18 @@ bool ConhostInternalGetSet::PrivateEndHyperlink() const DoSrvEndHyperlink(_io.GetActiveOutputBuffer()); return true; } + +// Routine Description: +// - Replaces the active soft font with the given bit pattern. +// Arguments: +// - bitPattern - An array of scanlines representing all the glyphs in the font. +// - cellSize - The cell size for an individual glyph. +// - centeringHint - The horizontal extent that glyphs are offset from center. +// Return Value: +// - true if successful (see DoSrvUpdateSoftFont). false otherwise. +bool ConhostInternalGetSet::PrivateUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept +{ + return SUCCEEDED(DoSrvUpdateSoftFont(bitPattern, cellSize, centeringHint)); +} diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 34048e026..99889baca 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -147,6 +147,10 @@ public: bool PrivateAddHyperlink(const std::wstring_view uri, const std::wstring_view params) const override; bool PrivateEndHyperlink() const override; + bool PrivateUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept override; + private: Microsoft::Console::IIoProvider& _io; }; diff --git a/src/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp index b17853805..a12dd8673 100644 --- a/src/host/ut_host/VtRendererTests.cpp +++ b/src/host/ut_host/VtRendererTests.cpp @@ -420,6 +420,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;2;5;6;7m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x00030201, 0x00070605 }, &renderData, + false, false)); TestPaint(*engine, [&]() { @@ -428,6 +429,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;2;7;8;9m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x00030201, 0x00090807 }, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -435,6 +437,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[38;2;10;11;12m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x000c0b0a, 0x00090807 }, &renderData, + false, false)); }); @@ -444,6 +447,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x000c0b0a, 0x00090807 }, &renderData, + false, false)); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback }); @@ -458,6 +462,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); TestPaint(*engine, [&]() { @@ -469,6 +474,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -477,6 +483,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -485,6 +492,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;2;19;161;14m"); // Background RGB(19,161,14) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -493,6 +501,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[38;2;193;156;0m"); // Foreground RGB(193,156,0) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -501,6 +510,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[49m"); // Background default VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -509,6 +519,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[38;5;7m"); // Foreground DARK_WHITE (256-Color Index) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -517,6 +528,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;5;1m"); // Background DARK_RED (256-Color Index) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -525,6 +537,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[39m"); // Background default VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -533,6 +546,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); }); @@ -542,6 +556,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback }); @@ -795,7 +810,7 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() Log::Comment(L"----Start With All Attributes Reset----"); TextAttribute textAttributes = {}; qExpectedInput.push_back("\x1b[m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); switch (renditionAttribute) { @@ -841,29 +856,29 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() break; } qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Foreground----"); textAttributes.SetIndexedForeground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[32m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Foreground and Retain Rendition----"); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Background----"); textAttributes.SetIndexedBackground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[42m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Background and Retain Rendition----"); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); VerifyExpectedInputsDrained(); } @@ -1081,6 +1096,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); TestPaint(*engine, [&]() { @@ -1092,6 +1108,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1100,6 +1117,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1108,6 +1126,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[42m"); // Background DARK_GREEN VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1116,6 +1135,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[33m"); // Foreground DARK_YELLOW VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1125,6 +1145,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[33m"); // Reapply foreground DARK_YELLOW VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1133,6 +1154,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1141,6 +1163,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1150,6 +1173,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[41m"); // Reapply background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1158,6 +1182,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); }); @@ -1167,6 +1192,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback }); @@ -1304,7 +1330,7 @@ void VtRendererTest::XtermTestAttributesAcrossReset() Log::Comment(L"----Start With All Attributes Reset----"); TextAttribute textAttributes = {}; qExpectedInput.push_back("\x1b[m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); switch (renditionAttribute) { @@ -1322,29 +1348,29 @@ void VtRendererTest::XtermTestAttributesAcrossReset() break; } qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Foreground----"); textAttributes.SetIndexedForeground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[32m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Foreground and Retain Rendition----"); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Background----"); textAttributes.SetIndexedBackground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[42m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Background and Retain Rendition----"); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); VerifyExpectedInputsDrained(); } diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index cf690ecd3..dcd3f923e 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -196,6 +196,7 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid [[nodiscard]] HRESULT BgfxEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, bool const /*isSettingDefaultBrushes*/) noexcept { _currentLegacyColorAttribute = textAttributes.GetLegacyAttributes(); diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index 4fddcfb0b..2b4e787a3 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -60,6 +60,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, bool const isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; diff --git a/src/renderer/base/FontResource.cpp b/src/renderer/base/FontResource.cpp new file mode 100644 index 000000000..38263033a --- /dev/null +++ b/src/renderer/base/FontResource.cpp @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "../inc/FontResource.hpp" + +using namespace Microsoft::Console::Render; + +namespace +{ + // The structures below are based on the Windows 3.0 font file format, which + // was documented in Microsoft Knowledge Base article Q65123. Although no + // longer hosted by Microsoft, it can still be found at the following URL: + // https://web.archive.org/web/20140820153410/http://support.microsoft.com/kb/65123 + + // For now we're only using fixed pitch single color fonts, but the rest + // of the flags are included here for completeness. + static constexpr DWORD DFF_FIXED = 0x0001; + static constexpr DWORD DFF_PROPORTIONAL = 0x0002; + static constexpr DWORD DFF_1COLOR = 0x0010; + static constexpr DWORD DFF_16COLOR = 0x0020; + static constexpr DWORD DFF_256COLOR = 0x0040; + static constexpr DWORD DFF_RGBCOLOR = 0x0080; + + // DRCS soft fonts only require 96 characters at most. + static constexpr size_t CHAR_COUNT = 96; + +#pragma pack(push, 1) + struct GLYPHENTRY + { + WORD geWidth; + DWORD geOffset; + }; + + struct FONTINFO + { + WORD dfVersion; + DWORD dfSize; + CHAR dfCopyright[60]; + WORD dfType; + WORD dfPoints; + WORD dfVertRes; + WORD dfHorizRes; + WORD dfAscent; + WORD dfInternalLeading; + WORD dfExternalLeading; + BYTE dfItalic; + BYTE dfUnderline; + BYTE dfStrikeOut; + WORD dfWeight; + BYTE dfCharSet; + WORD dfPixWidth; + WORD dfPixHeight; + BYTE dfPitchAndFamily; + WORD dfAvgWidth; + WORD dfMaxWidth; + BYTE dfFirstChar; + BYTE dfLastChar; + BYTE dfDefaultChar; + BYTE dfBreakChar; + WORD dfWidthBytes; + DWORD dfDevice; + DWORD dfFace; + DWORD dfBitsPointer; + DWORD dfBitsOffset; + BYTE dfReserved; + DWORD dfFlags; + WORD dfAspace; + WORD dfBspace; + WORD dfCspace; + DWORD dfColorPointer; + DWORD dfReserved1[4]; + GLYPHENTRY dfCharTable[CHAR_COUNT]; + CHAR szFaceName[LF_FACESIZE]; + }; +#pragma pack(pop) +} + +FontResource::FontResource(const gsl::span bitPattern, + const til::size sourceSize, + const til::size targetSize, + const size_t centeringHint) : + _bitPattern{ bitPattern.begin(), bitPattern.end() }, + _sourceSize{ sourceSize }, + _targetSize{ targetSize }, + _centeringHint{ centeringHint } +{ +} + +void FontResource::SetTargetSize(const til::size targetSize) +{ + if (_targetSize != targetSize) + { + _targetSize = targetSize; + _fontHandle = nullptr; + } +} + +FontResource::operator HFONT() +{ + if (!_fontHandle && !_bitPattern.empty()) + { + _regenerateFont(); + } + return _fontHandle.get(); +} + +void FontResource::_regenerateFont() +{ + const auto targetWidth = _targetSize.width(); + const auto targetHeight = _targetSize.height(); + const auto charSizeInBytes = (targetWidth + 7) / 8 * targetHeight; + + const DWORD fontBitmapSize = charSizeInBytes * CHAR_COUNT; + const DWORD fontResourceSize = sizeof(FONTINFO) + fontBitmapSize; + + auto fontResourceBuffer = std::vector(fontResourceSize); + void* fontResourceBufferPointer = fontResourceBuffer.data(); + auto& fontResource = *static_cast(fontResourceBufferPointer); + + fontResource.dfVersion = 0x300; + fontResource.dfSize = fontResourceSize; + fontResource.dfWeight = FW_NORMAL; + fontResource.dfCharSet = OEM_CHARSET; + fontResource.dfPixWidth = targetWidth; + fontResource.dfPixHeight = targetHeight; + fontResource.dfPitchAndFamily = FIXED_PITCH | FF_DONTCARE; + fontResource.dfAvgWidth = targetWidth; + fontResource.dfMaxWidth = targetWidth; + fontResource.dfFirstChar = L' '; + fontResource.dfLastChar = fontResource.dfFirstChar + CHAR_COUNT - 1; + fontResource.dfFace = offsetof(FONTINFO, szFaceName); + fontResource.dfBitsOffset = sizeof(FONTINFO); + fontResource.dfFlags = DFF_FIXED | DFF_1COLOR; + + // We use an atomic counter to create a locally-unique name for the font. + static std::atomic faceNameCounter; + sprintf_s(fontResource.szFaceName, "WTSOFTFONT%016llX", faceNameCounter++); + + // Each character has a fixed size and position in the font bitmap, but we + // still need to fill in the header table with that information. + for (auto i = 0u; i < std::size(fontResource.dfCharTable); i++) + { + const auto charOffset = fontResource.dfBitsOffset + charSizeInBytes * i; + fontResource.dfCharTable[i].geOffset = charOffset; + fontResource.dfCharTable[i].geWidth = targetWidth; + } + + // Raster fonts aren't generally scalable, so we need to resize the bit + // patterns for the character glyphs to the requested target size, and + // copy the results into the resource structure. + auto fontResourceSpan = gsl::span(fontResourceBuffer); + _resizeBitPattern(fontResourceSpan.subspan(fontResource.dfBitsOffset)); + + DWORD fontCount = 0; + _resourceHandle.reset(AddFontMemResourceEx(&fontResource, fontResourceSize, nullptr, &fontCount)); + LOG_HR_IF_NULL(E_FAIL, _resourceHandle.get()); + + // Once the resource has been registered, we should be able to create the + // font by using the same name and attributes as were set in the resource. + LOGFONTA logFont = {}; + logFont.lfHeight = fontResource.dfPixHeight; + logFont.lfWidth = fontResource.dfPixWidth; + logFont.lfCharSet = fontResource.dfCharSet; + logFont.lfOutPrecision = OUT_RASTER_PRECIS; + logFont.lfPitchAndFamily = fontResource.dfPitchAndFamily; + strcpy_s(logFont.lfFaceName, fontResource.szFaceName); + _fontHandle.reset(CreateFontIndirectA(&logFont)); + LOG_HR_IF_NULL(E_FAIL, _fontHandle.get()); +} + +void FontResource::_resizeBitPattern(gsl::span targetBuffer) +{ + auto sourceWidth = _sourceSize.width(); + auto targetWidth = _targetSize.width(); + const auto sourceHeight = _sourceSize.height(); + const auto targetHeight = _targetSize.height(); + + // If the text in the font is not perfectly centered, the _centeringHint + // gives us the offset needed to correct that misalignment. So to ensure + // that any inserted or deleted columns are evenly spaced around the center + // point of the glyphs, we need to adjust the source and target widths by + // that amount (proportionally) before calculating the scaling increments. + targetWidth -= std::lround((double)_centeringHint * targetWidth / sourceWidth); + sourceWidth -= gsl::narrow_cast(_centeringHint); + + // The way the scaling works is by iterating over the target range, and + // calculating the source offsets that correspond to each target position. + // We achieve that by incrementing the source offset every iteration by an + // integer value that is the quotient of the source and target dimensions. + // Because this is an integer division, we're going to be off by a certain + // fraction on each iteration, so we need to keep track of that accumulated + // error using the modulus of the division. Once the error total exceeds + // the target dimension (more or less), we add another pixel to compensate + // for the error, and reset the error total. + const auto createIncrementFunction = [](const auto sourceDimension, const auto targetDimension) { + const auto increment = sourceDimension / targetDimension; + const auto errorIncrement = sourceDimension % targetDimension * 2; + const auto errorThreshold = targetDimension * 2 - std::min(sourceDimension, targetDimension); + const auto errorReset = targetDimension * 2; + + return [=](auto& errorTotal) { + errorTotal += errorIncrement; + if (errorTotal > errorThreshold) + { + errorTotal -= errorReset; + return increment + 1; + } + return increment; + }; + }; + const auto columnIncrement = createIncrementFunction(sourceWidth, targetWidth); + const auto lineIncrement = createIncrementFunction(sourceHeight, targetHeight); + + // Once we've calculated the scaling increments, taking the centering hint + // into account, we reset the target width back to its original value. + targetWidth = _targetSize.width(); + + auto targetBufferPointer = targetBuffer.begin(); + for (auto ch = 0; ch < CHAR_COUNT; ch++) + { + // Bits are read from the source from left to right - MSB to LSB. The source + // column is a single bit representing the 1-based position. The reason for + // this will become clear in the mask calculation below. + auto sourceColumn = 1 << 16; + auto sourceColumnError = 0; + + // The target format expects the character bitmaps to be laid out in columns + // of 8 bits. So we generate 8 bits from each scanline until we've covered + // the full target height. Then we start again from the top with the next 8 + // bits of the line, until we've covered the full target width. + for (auto targetX = 0; targetX < targetWidth; targetX += 8) + { + auto sourceLine = std::next(_bitPattern.begin(), ch * sourceHeight); + auto sourceLineError = 0; + + // Since we're going to be reading from the same horizontal offset for each + // target line, we save the state here so we can reset it every iteration. + const auto initialSourceColumn = sourceColumn; + const auto initialSourceColumnError = sourceColumnError; + + for (auto targetY = 0; targetY < targetHeight; targetY++) + { + sourceColumn = initialSourceColumn; + sourceColumnError = initialSourceColumnError; + + // For a particular target line, we calculate the span of source lines from + // which it is derived, then OR those values together. We don't want the + // source value to be zero, though, so we must read at least one line. + const auto lineSpan = lineIncrement(sourceLineError); + auto sourceValue = 0; + for (auto i = 0; i < std::max(lineSpan, 1); i++) + { + sourceValue |= sourceLine[i]; + } + std::advance(sourceLine, lineSpan); + + // From the combined value of the source lines, we now need to extract eight + // bits to make up the next byte in the target at the current X offset. + byte targetValue = 0; + for (auto targetBit = 0; targetBit < 8; targetBit++) + { + targetValue <<= 1; + if (targetX + targetBit < targetWidth) + { + // As with the line iteration, we first need to calculate the span of source + // columns from which the target bit is derived. We shift our source column + // position right by that amount to determine the next column position, then + // subtract those two values to obtain a mask. For example, if we're reading + // from columns 6 to 3 (exclusively), the initial column position is 1<<6, + // the next column position is 1<<3, so the mask is 64-8=56, or 00111000. + // Again we don't want this mask to be zero, so if the span is zero, we need + // to shift an additional bit to make sure we cover at least one column. + const auto columnSpan = columnIncrement(sourceColumnError); + const auto nextSourceColumn = sourceColumn >> columnSpan; + const auto sourceMask = sourceColumn - (nextSourceColumn >> (columnSpan ? 0 : 1)); + sourceColumn = nextSourceColumn; + targetValue |= (sourceValue & sourceMask) ? 1 : 0; + } + } + *(targetBufferPointer++) = targetValue; + } + } + } +} diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index f31fff625..a32b4b525 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -36,6 +36,13 @@ HRESULT RenderEngineBase::UpdateTitle(const std::wstring_view newTitle) noexcept return hr; } +HRESULT RenderEngineBase::UpdateSoftFont(const gsl::span /*bitPattern*/, + const SIZE /*cellSize*/, + const size_t /*centeringHint*/) noexcept +{ + return S_FALSE; +} + HRESULT RenderEngineBase::PrepareRenderInfo(const RenderFrameInfo& /*info*/) noexcept { return S_FALSE; diff --git a/src/renderer/base/lib/base.vcxproj b/src/renderer/base/lib/base.vcxproj index f38398cfc..2a9ee32cb 100644 --- a/src/renderer/base/lib/base.vcxproj +++ b/src/renderer/base/lib/base.vcxproj @@ -15,6 +15,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/src/renderer/base/lib/base.vcxproj.filters b/src/renderer/base/lib/base.vcxproj.filters index 1ec046c43..baff50352 100644 --- a/src/renderer/base/lib/base.vcxproj.filters +++ b/src/renderer/base/lib/base.vcxproj.filters @@ -27,6 +27,9 @@ Source Files + + Source Files + Source Files @@ -65,6 +68,9 @@ Header Files\inc + + Header Files\inc + Header Files\inc diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index b53bc6ead..863bcd24f 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -144,7 +144,7 @@ try }); // A. Prep Colors - RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, _pData->GetDefaultBrushColors(), true)); + RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, _pData->GetDefaultBrushColors(), false, true)); // B. Perform Scroll Operations RETURN_IF_FAILED(_PerformScrolling(pEngine)); @@ -526,6 +526,35 @@ void Renderer::TriggerFontChange(const int iDpi, const FontInfoDesired& FontInfo _NotifyPaintFrame(); } +// Routine Description: +// - Called when the active soft font has been updated. +// Arguments: +// - bitPattern - An array of scanlines representing all the glyphs in the font. +// - cellSize - The cell size for an individual glyph. +// - centeringHint - The horizontal extent that glyphs are offset from center. +// Return Value: +// - +void Renderer::UpdateSoftFont(const gsl::span bitPattern, const SIZE cellSize, const size_t centeringHint) +{ + // We reserve PUA code points U+EF20 to U+EF7F for soft fonts, but the range + // that we test for in _IsSoftFontChar will depend on the size of the active + // bitPattern. If it's empty (i.e. no soft font is set), then nothing will + // match, and those code points will be treated the same as everything else. + const auto softFontCharCount = cellSize.cy ? bitPattern.size() / cellSize.cy : 0; + _lastSoftFontChar = _firstSoftFontChar + softFontCharCount - 1; + + for (const auto pEngine : _rgpEngines) + { + LOG_IF_FAILED(pEngine->UpdateSoftFont(bitPattern, cellSize, centeringHint)); + } + TriggerRedrawAll(); +} + +bool Renderer::s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar) +{ + return v.size() == 1 && v.front() >= firstSoftFontChar && v.front() <= lastSoftFontChar; +} + // Routine Description: // - Get the information on what font we would be using if we decided to create a font with the given parameters // - This is for use with speculative calculations. @@ -740,6 +769,8 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, auto color = it->TextAttr(); // Retrieve the first pattern id auto patternIds = _pData->GetPatternId(target); + // Determine whether we're using a soft font. + auto usingSoftFont = s_IsSoftFontChar(it->Chars(), _firstSoftFontChar, _lastSoftFontChar); // And hold the point where we should start drawing. auto screenPoint = target; @@ -756,8 +787,8 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // Hold onto the current pattern id as well const auto currentPatternId = patternIds; - // Update the drawing brushes with our color. - THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, false)); + // Update the drawing brushes with our color and font usage. + THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, usingSoftFont, false)); // Advance the point by however many columns we've just outputted and reset the accumulator. screenPoint.X += gsl::narrow(cols); @@ -786,15 +817,18 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, { COORD thisPoint{ screenPoint.X + gsl::narrow(cols), screenPoint.Y }; const auto thisPointPatterns = _pData->GetPatternId(thisPoint); - if (color != it->TextAttr() || patternIds != thisPointPatterns) + const auto thisUsingSoftFont = s_IsSoftFontChar(it->Chars(), _firstSoftFontChar, _lastSoftFontChar); + const auto changedPatternOrFont = patternIds != thisPointPatterns || usingSoftFont != thisUsingSoftFont; + if (color != it->TextAttr() || changedPatternOrFont) { auto newAttr{ it->TextAttr() }; // foreground doesn't matter for runs of spaces (!) // if we trick it . . . we call Paint far fewer times for cmatrix - if (!_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, globalInvert) || patternIds != thisPointPatterns) + if (!_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, globalInvert) || changedPatternOrFont) { color = newAttr; patternIds = thisPointPatterns; + usingSoftFont = thisUsingSoftFont; break; // vend this run } } @@ -1183,17 +1217,21 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) // Arguments: // - pEngine - Which engine is being updated // - textAttributes - The 16 color foreground/background combination to set +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes - Alerts that the default brushes are being set which will // impact whether or not to include the hung window/erase window brushes in this operation // and can affect other draw state that wants to know the default color scheme. // (Usually only happens when the default is changed, not when each individual color is swapped in a multi-color run.) // Return Value: // - -[[nodiscard]] HRESULT Renderer::_UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute textAttributes, const bool isSettingDefaultBrushes) +[[nodiscard]] HRESULT Renderer::_UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, + const TextAttribute textAttributes, + const bool usingSoftFont, + const bool isSettingDefaultBrushes) { // The last color needs to be each engine's responsibility. If it's local to this function, // then on the next engine we might not update the color. - return pEngine->UpdateDrawingBrushes(textAttributes, _pData, isSettingDefaultBrushes); + return pEngine->UpdateDrawingBrushes(textAttributes, _pData, usingSoftFont, isSettingDefaultBrushes); } // Routine Description: diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 6f8f6d7a8..eb8eadf62 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -65,6 +65,10 @@ namespace Microsoft::Console::Render const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) override; + void UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) override; + [[nodiscard]] HRESULT GetProposedFont(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) override; @@ -120,7 +124,10 @@ namespace Microsoft::Console::Render void _PaintOverlays(_In_ IRenderEngine* const pEngine); void _PaintOverlay(IRenderEngine& engine, const RenderOverlay& overlay); - [[nodiscard]] HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute attr, const bool isSettingDefaultBrushes); + [[nodiscard]] HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, + const TextAttribute attr, + const bool usingSoftFont, + const bool isSettingDefaultBrushes); [[nodiscard]] HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine); @@ -138,6 +145,10 @@ namespace Microsoft::Console::Render [[nodiscard]] std::optional _GetCursorInfo(); [[nodiscard]] HRESULT _PrepareRenderInfo(_In_ IRenderEngine* const pEngine); + const size_t _firstSoftFontChar = 0xEF20; + size_t _lastSoftFontChar = 0; + static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar); + // Helper functions to diagnose issues with painting and layout. // These are only actually effective/on in Debug builds when the flag is set using an attached debugger. bool _fDebug = false; diff --git a/src/renderer/base/sources.inc b/src/renderer/base/sources.inc index 9aeaf8c65..e3f3d2171 100644 --- a/src/renderer/base/sources.inc +++ b/src/renderer/base/sources.inc @@ -29,6 +29,7 @@ SOURCES = \ ..\FontInfo.cpp \ ..\FontInfoBase.cpp \ ..\FontInfoDesired.cpp \ + ..\FontResource.cpp \ ..\RenderEngineBase.cpp \ ..\renderer.cpp \ ..\thread.cpp \ diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 8d8f238e1..e40a40ffd 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -1907,11 +1907,13 @@ CATCH_RETURN() // Arguments: // - textAttributes - Text attributes to use for the brush color // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes - Lets us know that these are the default brushes to paint the swapchain background or selection // Return Value: // - S_OK or relevant DirectX error. [[nodiscard]] HRESULT DxEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool /*usingSoftFont*/, const bool isSettingDefaultBrushes) noexcept { // GH#5098: If we're rendering with cleartype text, we need to always render diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index 1351e61de..6fdf2fe7f 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -107,6 +107,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 230b09c4f..4caf62ad6 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -15,6 +15,7 @@ Author(s): #pragma once #include "../inc/RenderEngineBase.hpp" +#include "../inc/FontResource.hpp" namespace Microsoft::Console::Render { @@ -61,9 +62,13 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; + [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept override; [[nodiscard]] HRESULT UpdateDpi(const int iDpi) noexcept override; [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; @@ -95,6 +100,7 @@ namespace Microsoft::Console::Render HFONT _hfont; HFONT _hfontItalic; TEXTMETRICW _tmFontMetrics; + FontResource _softFont; static const size_t s_cPolyTextCache = 80; POLYTEXTW _pPolyText[s_cPolyTextCache]; @@ -130,7 +136,14 @@ namespace Microsoft::Console::Render COLORREF _lastFg; COLORREF _lastBg; - bool _lastFontItalic; + + enum class FontType : size_t + { + Default, + Italic, + Soft + }; + FontType _lastFontType; XFORM _currentLineTransform; LineRendition _currentLineRendition; diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 089e9777e..347135974 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -336,6 +336,9 @@ using namespace Microsoft::Console::Render; auto& polyWidth = _polyWidths.emplace_back(); polyWidth.reserve(cchLine); + // If we have a soft font, we only use the character's lower 7 bits. + const auto softFontCharMask = _lastFontType == FontType::Soft ? L'\x7F' : ~0; + // Sum up the total widths the entire line/run is expected to take while // copying the pixel widths into a structure to direct GDI how many pixels to use per character. size_t cchCharWidths = 0; @@ -347,6 +350,7 @@ using namespace Microsoft::Console::Render; const auto text = cluster.GetText(); polyString += text; + polyString.back() &= softFontCharMask; polyWidth.push_back(gsl::narrow(cluster.GetColumns()) * coordFontSize.X); cchCharWidths += polyWidth.back(); polyWidth.append(text.size() - 1, 0); diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp index 3311e7585..04a5df7d5 100644 --- a/src/renderer/gdi/state.cpp +++ b/src/renderer/gdi/state.cpp @@ -29,7 +29,7 @@ GdiEngine::GdiEngine() : _fInvalidRectUsed(false), _lastFg(INVALID_COLOR), _lastBg(INVALID_COLOR), - _lastFontItalic(false), + _lastFontType(FontType::Default), _currentLineTransform(IDENTITY_XFORM), _currentLineRendition(LineRendition::SingleWidth), _fPaintStarted(false), @@ -148,8 +148,8 @@ GdiEngine::~GdiEngine() LOG_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, _hfont)); } - // Record the fact that the selected font is not italic. - _lastFontItalic = false; + // Record the fact that the selected font is the default. + _lastFontType = FontType::Default; if (nullptr != hdcRealWindow) { @@ -269,12 +269,14 @@ GdiEngine::~GdiEngine() // Arguments: // - textAttributes - Text attributes to use for the brush color // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes - Lets us know that the default brushes are being set so we can update the DC background // and the hung app background painting color // Return Value: // - S_OK if set successfully or relevant GDI error via HRESULT. [[nodiscard]] HRESULT GdiEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept { RETURN_IF_FAILED(_FlushBufferLines()); @@ -304,12 +306,25 @@ GdiEngine::~GdiEngine() RETURN_IF_FAILED(s_SetWindowLongWHelper(_hwndTargetWindow, GWL_CONSOLE_BKCOLOR, colorBackground)); } - // If the italic attribute has changed, select an appropriate font variant. - const auto fontItalic = textAttributes.IsItalic(); - if (fontItalic != _lastFontItalic) + // If the font type has changed, select an appropriate font variant or soft font. + const auto usingItalicFont = textAttributes.IsItalic(); + const auto fontType = usingSoftFont ? FontType::Soft : usingItalicFont ? FontType::Italic : FontType::Default; + if (fontType != _lastFontType) { - SelectFont(_hdcMemoryContext, fontItalic ? _hfontItalic : _hfont); - _lastFontItalic = fontItalic; + switch (fontType) + { + case FontType::Soft: + SelectFont(_hdcMemoryContext, _softFont); + break; + case FontType::Italic: + SelectFont(_hdcMemoryContext, _hfontItalic); + break; + case FontType::Default: + default: + SelectFont(_hdcMemoryContext, _hfont); + break; + } + _lastFontType = fontType; } return S_OK; @@ -331,8 +346,8 @@ GdiEngine::~GdiEngine() // Select into DC RETURN_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, hFont.get())); - // Record the fact that the selected font is not italic. - _lastFontItalic = false; + // Record the fact that the selected font is the default. + _lastFontType = FontType::Default; // Save off the font metrics for various other calculations RETURN_HR_IF(E_FAIL, !(GetTextMetricsW(_hdcMemoryContext, &_tmFontMetrics))); @@ -419,11 +434,39 @@ GdiEngine::~GdiEngine() _isTrueTypeFont = Font.IsTrueTypeFont(); _fontCodepage = Font.GetCodePage(); + // Inform the soft font of the change in size. + _softFont.SetTargetSize(_GetFontSize()); + LOG_IF_FAILED(InvalidateAll()); return S_OK; } +// Routine Description: +// - This method will replace the active soft font with the given bit pattern. +// Arguments: +// - bitPattern - An array of scanlines representing all the glyphs in the font. +// - cellSize - The cell size for an individual glyph. +// - centeringHint - The horizontal extent that glyphs are offset from center. +// Return Value: +// - S_OK if successful. E_FAIL if there was an error. +[[nodiscard]] HRESULT GdiEngine::UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept +{ + // If the soft font is currently selected, replace it with the default font. + if (_lastFontType == FontType::Soft) + { + RETURN_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, _hfont)); + _lastFontType = FontType::Default; + } + + // Create a new font resource with the updated pattern, or delete if empty. + _softFont = { bitPattern, cellSize, _GetFontSize(), centeringHint }; + + return S_OK; +} + // Routine Description: // - This method will modify the DPI we're using for scaling calculations. // Arguments: diff --git a/src/renderer/inc/FontResource.hpp b/src/renderer/inc/FontResource.hpp new file mode 100644 index 000000000..aa159ed34 --- /dev/null +++ b/src/renderer/inc/FontResource.hpp @@ -0,0 +1,45 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FontResource.hpp + +Abstract: +- This manages the construction of in-memory font resources for the VT soft fonts. +--*/ + +#pragma once + +namespace wil +{ + typedef unique_any unique_hfontresource; +} + +namespace Microsoft::Console::Render +{ + class FontResource + { + public: + FontResource(const gsl::span bitPattern, + const til::size sourceSize, + const til::size targetSize, + const size_t centeringHint); + FontResource() = default; + ~FontResource() = default; + FontResource& operator=(FontResource&&) = default; + void SetTargetSize(const til::size targetSize); + operator HFONT(); + + private: + void _regenerateFont(); + void _resizeBitPattern(gsl::span targetBuffer); + + std::vector _bitPattern; + til::size _sourceSize; + til::size _targetSize; + size_t _centeringHint{ 0 }; + wil::unique_hfontresource _resourceHandle; + wil::unique_hfont _fontHandle; + }; +} diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 3d6631e41..bcc98dfc4 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -96,9 +96,13 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept = 0; + [[nodiscard]] virtual HRESULT UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateDpi(const int iDpi) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept = 0; diff --git a/src/renderer/inc/IRenderer.hpp b/src/renderer/inc/IRenderer.hpp index 79058c5ae..a3894bdb7 100644 --- a/src/renderer/inc/IRenderer.hpp +++ b/src/renderer/inc/IRenderer.hpp @@ -50,6 +50,10 @@ namespace Microsoft::Console::Render const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) = 0; + virtual void UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) = 0; + [[nodiscard]] virtual HRESULT GetProposedFont(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) = 0; diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 725347d82..6ad59fec3 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -38,6 +38,10 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateTitle(const std::wstring_view newTitle) noexcept override; + [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept override; + [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; [[nodiscard]] HRESULT ResetLineTransform() noexcept override; diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index ba7ca8e5f..2d5d17196 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -364,11 +364,13 @@ CATCH_RETURN(); // Arguments: // - textAttributes - // - pData - +// - usingSoftFont - // - isSettingDefaultBrushes - // Return Value: // - S_FALSE since we do nothing [[nodiscard]] HRESULT UiaEngine::UpdateDrawingBrushes(const TextAttribute& /*textAttributes*/, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, const bool /*isSettingDefaultBrushes*/) noexcept { return S_FALSE; diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 3ead4673b..6591579c5 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -62,6 +62,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; diff --git a/src/renderer/vt/Xterm256Engine.cpp b/src/renderer/vt/Xterm256Engine.cpp index 91e220c33..4edece715 100644 --- a/src/renderer/vt/Xterm256Engine.cpp +++ b/src/renderer/vt/Xterm256Engine.cpp @@ -20,12 +20,14 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe, // Arguments: // - textAttributes - Text attributes to use for the colors and character rendition // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes: indicates if we should change the background color of // the window. Unused for VT // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT Xterm256Engine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool /*usingSoftFont*/, const bool /*isSettingDefaultBrushes*/) noexcept { RETURN_IF_FAILED(VtEngine::_RgbUpdateDrawingBrushes(textAttributes)); diff --git a/src/renderer/vt/Xterm256Engine.hpp b/src/renderer/vt/Xterm256Engine.hpp index b1ed02332..4e5913e07 100644 --- a/src/renderer/vt/Xterm256Engine.hpp +++ b/src/renderer/vt/Xterm256Engine.hpp @@ -30,6 +30,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT ManuallyClearScrollback() noexcept override; diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index a948acd82..1c09225fa 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -137,12 +137,14 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // Arguments: // - textAttributes - Text attributes to use for the colors and character rendition // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes: indicates if we should change the background color of // the window. Unused for VT // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT XtermEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, const bool /*isSettingDefaultBrushes*/) noexcept { // The base xterm mode only knows about 16 colors diff --git a/src/renderer/vt/XtermEngine.hpp b/src/renderer/vt/XtermEngine.hpp index 1dcf2d173..a46458ad6 100644 --- a/src/renderer/vt/XtermEngine.hpp +++ b/src/renderer/vt/XtermEngine.hpp @@ -40,6 +40,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT PaintBufferLine(gsl::span const clusters, const COORD coord, diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index f7b0a788b..bb8f0413f 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -75,6 +75,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept = 0; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, _Out_ FontInfo& pfiFontInfo) noexcept override; diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index ed2c027ee..ba67819ab 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -307,6 +307,7 @@ bool WddmConEngine::IsInitialized() [[nodiscard]] HRESULT WddmConEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, bool const /*isSettingDefaultBrushes*/) noexcept { _currentLegacyColorAttribute = textAttributes.GetLegacyAttributes(); diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index 6898f1ece..57663d5cb 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -52,6 +52,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, bool const isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 1938791d4..b22634637 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -434,6 +434,46 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes DependsOnMode }; + enum class DrcsEraseControl : size_t + { + AllChars = 0, + ReloadedChars = 1, + AllRenditions = 2 + }; + + enum class DrcsCellMatrix : size_t + { + Default = 0, + Invalid = 1, + Size5x10 = 2, + Size6x10 = 3, + Size7x10 = 4 + }; + + enum class DrcsFontSet : size_t + { + Default = 0, + Size80x24 = 1, + Size132x24 = 2, + Size80x36 = 11, + Size132x36 = 12, + Size80x48 = 21, + Size132x48 = 22 + }; + + enum class DrcsFontUsage : size_t + { + Default = 0, + Text = 1, + FullCell = 2 + }; + + enum class DrcsCharsetSize : size_t + { + Size94 = 0, + Size96 = 1 + }; + constexpr short s_sDECCOLMSetColumns = 132; constexpr short s_sDECCOLMResetColumns = 80; diff --git a/src/terminal/adapter/FontBuffer.cpp b/src/terminal/adapter/FontBuffer.cpp new file mode 100644 index 000000000..688325b27 --- /dev/null +++ b/src/terminal/adapter/FontBuffer.cpp @@ -0,0 +1,603 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "FontBuffer.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +FontBuffer::FontBuffer() noexcept +{ + SetEraseControl(DispatchTypes::DrcsEraseControl::AllRenditions); +}; + +bool FontBuffer::SetEraseControl(const DispatchTypes::DrcsEraseControl eraseControl) noexcept +{ + switch (eraseControl) + { + case DispatchTypes::DrcsEraseControl::AllChars: + case DispatchTypes::DrcsEraseControl::AllRenditions: + // Setting the current cell matrix to an invalid value will guarantee + // that it's different from the pending cell matrix, and any change in + // the font attributes will force the buffer to be cleared. + _cellMatrix = DispatchTypes::DrcsCellMatrix::Invalid; + return true; + case DispatchTypes::DrcsEraseControl::ReloadedChars: + return true; + default: + return false; + } +} + +bool FontBuffer::SetAttributes(const DispatchTypes::DrcsCellMatrix cellMatrix, + const VTParameter cellHeight, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage) noexcept +{ + auto valid = true; + + if (valid) + { + // We don't yet support screen sizes in which the font is horizontally + // or vertically compressed, so there is not much value in storing a + // separate font for each of the screen sizes. However, we still need + // to use these values to determine the cell size for which the font + // was originally targeted, so we can resize it appropriately. + switch (fontSet) + { + case DispatchTypes::DrcsFontSet::Default: + case DispatchTypes::DrcsFontSet::Size80x24: + _columnsPerPage = 80; + _linesPerPage = 24; + break; + case DispatchTypes::DrcsFontSet::Size80x36: + _columnsPerPage = 80; + _linesPerPage = 36; + break; + case DispatchTypes::DrcsFontSet::Size80x48: + _columnsPerPage = 80; + _linesPerPage = 48; + break; + case DispatchTypes::DrcsFontSet::Size132x24: + _columnsPerPage = 132; + _linesPerPage = 24; + break; + case DispatchTypes::DrcsFontSet::Size132x36: + _columnsPerPage = 132; + _linesPerPage = 36; + break; + case DispatchTypes::DrcsFontSet::Size132x48: + _columnsPerPage = 132; + _linesPerPage = 48; + break; + default: + valid = false; + break; + } + } + + if (valid) + { + switch (fontUsage) + { + case DispatchTypes::DrcsFontUsage::Default: + case DispatchTypes::DrcsFontUsage::Text: + _isTextFont = true; + break; + case DispatchTypes::DrcsFontUsage::FullCell: + _isTextFont = false; + break; + default: + valid = false; + break; + } + } + + if (valid) + { + switch (cellMatrix) + { + case DispatchTypes::DrcsCellMatrix::Invalid: + valid = false; + break; + case DispatchTypes::DrcsCellMatrix::Size5x10: + // Size 5x10 is only valid for text fonts. + valid = _isTextFont; + _sizeDeclaredAsMatrix = true; + _declaredWidth = 5; + _declaredHeight = 10; + break; + case DispatchTypes::DrcsCellMatrix::Size6x10: + // Size 6x10 is only valid for text fonts, + // unless it's a VT240 in 132-column mode. + valid = _isTextFont || _columnsPerPage == 132; + _sizeDeclaredAsMatrix = true; + _declaredWidth = 6; + _declaredHeight = 10; + break; + case DispatchTypes::DrcsCellMatrix::Size7x10: + // Size 7x10 is only valid for text fonts. + valid = _isTextFont; + _sizeDeclaredAsMatrix = true; + _declaredWidth = 7; + _declaredHeight = 10; + break; + case DispatchTypes::DrcsCellMatrix::Default: + default: + // If we aren't given one of the predefined matrix sizes, then the + // matrix parameter is a pixel width, and height is obtained from the + // height parameter. This also applies for the default of 0, since a + // 0 width is treated as unknown (we'll try and estimate the expected + // width), and the height parameter can still give us the height. + _sizeDeclaredAsMatrix = false; + _declaredWidth = static_cast(cellMatrix); + _declaredHeight = cellHeight.value_or(0); + valid = (_declaredWidth <= MAX_WIDTH && _declaredHeight <= MAX_HEIGHT); + break; + } + } + + // Save the pending attributes, but don't update the current values until we + // are sure we have a valid sequence that can replace the current buffer. + _pendingCellMatrix = cellMatrix; + _pendingCellHeight = cellHeight.value_or(0); + _pendingFontSet = fontSet; + _pendingFontUsage = fontUsage; + + // Reset the used dimensions. These values will be determined by the extent + // of the sixel data that we receive in the following string sequence. + _usedWidth = 0; + _usedHeight = 0; + + return valid; +} + +bool FontBuffer::SetStartChar(const VTParameter startChar, + const DispatchTypes::DrcsCharsetSize charsetSize) noexcept +{ + switch (charsetSize) + { + case DispatchTypes::DrcsCharsetSize::Size94: + _startChar = startChar.value_or(1); + break; + case DispatchTypes::DrcsCharsetSize::Size96: + _startChar = startChar.value_or(0); + break; + default: + return false; + } + + _currentChar = _startChar; + _pendingCharsetSize = charsetSize; + _charsetIdInitialized = false; + _charsetIdBuilder.Clear(); + + return true; +} + +void FontBuffer::AddSixelData(const wchar_t ch) +{ + if (!_charsetIdInitialized) + { + _buildCharsetId(ch); + } + else if (ch >= L'?' && ch <= L'~') + { + _addSixelValue(ch - L'?'); + } + else if (ch == L'/') + { + _endOfSixelLine(); + } + else if (ch == L';') + { + _endOfCharacter(); + } +} + +bool FontBuffer::FinalizeSixelData() +{ + // If the charset ID hasn't been initialized this isn't a valid update. + RETURN_BOOL_IF_FALSE(_charsetIdInitialized); + + // Flush the current line to make sure we take all the used positions + // into account when calculating the font dimensions. + _endOfSixelLine(); + + // If the buffer has been cleared, we'll need to recalculate the dimensions + // using the latest attributes, adjust the character bit patterns to fit + // their true size, and fill in unused buffer positions with an error glyph. + if (_bufferCleared) + { + std::tie(_fullWidth, _fullHeight, _textWidth) = _calculateDimensions(); + _packAndCenterBitPatterns(); + _fillUnusedCharacters(); + } + + return true; +} + +gsl::span FontBuffer::GetBitPattern() const noexcept +{ + return { _buffer.data(), MAX_CHARS * _fullHeight }; +} + +til::size FontBuffer::GetCellSize() const +{ + return { _fullWidth, _fullHeight }; +} + +size_t FontBuffer::GetTextCenteringHint() const noexcept +{ + return _textCenteringHint; +} + +VTID FontBuffer::GetDesignation() const noexcept +{ + return _charsetId; +} + +void FontBuffer::_buildCharsetId(const wchar_t ch) +{ + // Note that we ignore any characters that are not valid in this state. + if (ch >= 0x20 && ch <= 0x2F) + { + _charsetIdBuilder.AddIntermediate(ch); + } + else if (ch >= 0x30 && ch <= 0x7E) + { + _pendingCharsetId = _charsetIdBuilder.Finalize(ch); + _charsetIdInitialized = true; + _prepareCharacterBuffer(); + } +} + +void FontBuffer::_prepareCharacterBuffer() +{ + // If any of the attributes have changed since the last time characters + // were downloaded, the font dimensions will need to be recalculated, and + // the buffer will need to be cleared. Otherwise we'll just be adding to + // the existing font, assuming the current dimensions. + if (_cellMatrix != _pendingCellMatrix || + _cellHeight != _pendingCellHeight || + _fontSet != _pendingFontSet || + _fontUsage != _pendingFontUsage || + _charsetSize != _pendingCharsetSize || + _charsetId != _pendingCharsetId) + { + // Replace the current attributes with the pending values. + _cellMatrix = _pendingCellMatrix; + _cellHeight = _pendingCellHeight; + _fontSet = _pendingFontSet; + _fontUsage = _pendingFontUsage; + _charsetSize = _pendingCharsetSize; + _charsetId = _pendingCharsetId; + + // Reset the font dimensions to the maximum supported size, since we + // can't be certain of the intended size until we've received all of + // the sixel data. These values will be recalculated once we can work + // out the terminal type that the font was originally designed for. + _fullWidth = MAX_WIDTH; + _fullHeight = MAX_HEIGHT; + _textWidth = MAX_WIDTH; + _textOffset = 0; + + // Clear the buffer. + _buffer.fill(0); + _bufferCleared = true; + } + else + { + _bufferCleared = false; + } + + _prepareNextCharacter(); +} + +void FontBuffer::_prepareNextCharacter() +{ + _lastChar = _currentChar; + _currentCharBuffer = std::next(_buffer.begin(), _currentChar * _fullHeight); + _sixelColumn = 0; + _sixelRow = 0; + + // If the buffer hasn't been cleared, we'll need to clear each character + // position individually, before adding any new sixel data. + if (!_bufferCleared && _currentChar < MAX_CHARS) + { + std::fill_n(_currentCharBuffer, _fullHeight, uint16_t{ 0 }); + } +} + +void FontBuffer::_addSixelValue(const size_t value) noexcept +{ + if (_currentChar < MAX_CHARS && _sixelColumn < _textWidth) + { + // Each sixel updates six pixels of a single column, so we setup a bit + // mask for the column we want to update, and then set that bit in each + // row for which there is a corresponding "on" bit in the input value. + const auto outputColumnBit = (0x8000 >> (_sixelColumn + _textOffset)); + auto outputIterator = _currentCharBuffer; + auto inputValueMask = 1; + for (size_t i = 0; i < 6 && _sixelRow + i < _fullHeight; i++) + { + *outputIterator |= (value & inputValueMask) ? outputColumnBit : 0; + outputIterator++; + inputValueMask <<= 1; + } + } + _sixelColumn++; +} + +void FontBuffer::_endOfSixelLine() +{ + // Move down six rows to the get to the next sixel position. + std::advance(_currentCharBuffer, 6); + _sixelRow += 6; + + // Keep track of the maximum width and height covered by the sixel data. + _usedWidth = std::max(_usedWidth, _sixelColumn); + _usedHeight = std::max(_usedHeight, _sixelRow); + + // Reset the column number to the start of the next line. + _sixelColumn = 0; +} + +void FontBuffer::_endOfCharacter() +{ + _endOfSixelLine(); + _currentChar++; + _prepareNextCharacter(); +} + +std::tuple FontBuffer::_calculateDimensions() const +{ + // If the size is declared as a matrix, this is most likely a VT2xx font, + // typically with a cell size of 10x10. However, in 132-column mode, the + // VT240 has a cell size of 6x10, but that's only for widths of 6 or less. + if (_sizeDeclaredAsMatrix) + { + if (_columnsPerPage == 132 && _declaredWidth <= 6) + { + // 6x10 cell with no clipping. + return { 6, 10, 0 }; + } + else + { + // 10x10 cell with text clipped to 8 pixels. + return { 10, 10, 8 }; + } + } + + // If we've been given explicit dimensions, and this is not a text font, + // then we assume those dimensions are the exact cell size. + if (_declaredWidth && _declaredHeight && !_isTextFont) + { + // Since this is not a text font, no clipping is required. + return { _declaredWidth, _declaredHeight, 0 }; + } + + // For most of the cases that follow, a text font will be clipped within + // the bounds of the declared width (if given). There are only a few cases + // where we'll need to use a hard-coded text width, and that's when the + // font appears to be targeting a VT2xx. + const auto textWidth = _isTextFont ? _declaredWidth : 0; + + // If the lines per page isn't 24, this must be targeting a VT420 or VT5xx. + // The cell width is 6 for 132 columns, and 10 for 80 columns. + // The cell height is 8 for 48 lines and 10 for 36 lines. + if (_linesPerPage != 24) + { + const auto cellWidth = _columnsPerPage == 132 ? 6 : 10; + const auto cellHeight = _linesPerPage == 48 ? 8 : 10; + return { cellWidth, cellHeight, textWidth }; + } + + // Now we're going to test whether the dimensions are in range for a number + // of known terminals. We use the declared dimensions if given, otherwise + // estimate the size from the used sixel values. If comparing a sixel-based + // height, though, we need to round up the target cell height to account for + // the fact that our used height will always be a multiple of six. + const auto inRange = [=](const size_t cellWidth, const size_t cellHeight) { + const auto sixelHeight = (cellHeight + 5) / 6 * 6; + const auto heightInRange = _declaredHeight ? _declaredHeight <= cellHeight : _usedHeight <= sixelHeight; + const auto widthInRange = _declaredWidth ? _declaredWidth <= cellWidth : _usedWidth <= cellWidth; + return heightInRange && widthInRange; + }; + + // In the case of a VT2xx font, you could only use a matrix size (which + // we've dealt with above), or a default size, so the tests below are only + // applicable for a VT2xx when no explicit dimensions have been declared. + const auto noDeclaredSize = _declaredWidth == 0 && _declaredHeight == 0; + + if (_columnsPerPage == 80) + { + if (inRange(8, 10) && noDeclaredSize) + { + // VT2xx - 10x10 cell with text clipped to 8 pixels. + return { 10, 10, 8 }; + } + else if (inRange(15, 12)) + { + // VT320 - 15x12 cell with default text width. + return { 15, 12, textWidth }; + } + else if (inRange(10, 16)) + { + // VT420 & VT5xx - 10x16 cell with default text width. + return { 10, 16, textWidth }; + } + else if (inRange(10, 20)) + { + // VT340 - 10x20 cell with default text width. + return { 10, 20, textWidth }; + } + else if (inRange(12, 30)) + { + // VT382 - 12x30 cell with default text width. + return { 12, 30, textWidth }; + } + else + { + // If all else fails, assume the maximum size. + return { MAX_WIDTH, MAX_HEIGHT, textWidth }; + } + } + else + { + if (inRange(6, 10) && noDeclaredSize) + { + // VT240 - 6x10 cell with no clipping. + return { 6, 10, 0 }; + } + else if (inRange(9, 12)) + { + // VT320 - 9x12 cell with default text width. + return { 9, 12, textWidth }; + } + else if (inRange(6, 16)) + { + // VT420 & VT5xx - 6x16 cell with default text width. + return { 6, 16, textWidth }; + } + else if (inRange(6, 20)) + { + // VT340 - 6x20 cell with default text width. + return { 6, 20, textWidth }; + } + else if (inRange(7, 30)) + { + // VT382 - 7x30 cell with default text width. + return { 7, 30, textWidth }; + } + else + { + // If all else fails, assume the maximum size. + return { MAX_WIDTH, MAX_HEIGHT, textWidth }; + } + } +} + +void FontBuffer::_packAndCenterBitPatterns() +{ + // If this is a text font, we'll clip the bits up to the text width and + // center them within the full cell width. For a full cell font we'll just + // use all of the bits, and no offset will be required. + _textWidth = _textWidth ? _textWidth : _fullWidth; + _textWidth = std::min(_textWidth, _fullWidth); + _textOffset = (_fullWidth - _textWidth) / 2; + const auto textClippingMask = ~(0xFFFF >> _textWidth); + + // If the text is given an explicit width, we check to what extent the + // content is offset from center. Knowing that information will enable the + // renderer to scale the font more symmetrically. + _textCenteringHint = _declaredWidth ? _fullWidth - (_declaredWidth + _textOffset * 2) : 0; + + // Initially the characters are written to the buffer assuming the maximum + // cell height, but now that we know the true height, we need to pack the + // buffer data so that each character occupies the exact number of scanlines + // that are required. + for (auto srcLine = 0u, dstLine = 0u; srcLine < _buffer.size(); srcLine++) + { + if ((srcLine % MAX_HEIGHT) < _fullHeight) + { + auto characterScanline = til::at(_buffer, srcLine); + characterScanline &= textClippingMask; + characterScanline >>= _textOffset; + til::at(_buffer, dstLine++) = characterScanline; + } + } +} + +void FontBuffer::_fillUnusedCharacters() +{ + // Every character in the buffer that hasn't been uploaded will be replaced + // with an error glyph (a reverse question mark). This includes every + // character prior to the start char, or after the last char. + const auto errorPattern = _generateErrorGlyph(); + for (auto ch = 0u; ch < MAX_CHARS; ch++) + { + if (ch < _startChar || ch > _lastChar) + { + auto charBuffer = std::next(_buffer.begin(), ch * _fullHeight); + std::copy_n(errorPattern.begin(), _fullHeight, charBuffer); + } + } +} + +std::array FontBuffer::_generateErrorGlyph() +{ + // We start with a bit pattern for a reverse question mark covering the + // maximum font resolution that we might need. + constexpr std::array inputBitPattern = { + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b001111111110000, + 0b011111111111000, + 0b111000000011100, + 0b111000000011100, + 0b111000000000000, + 0b111000000000000, + 0b111100000000000, + 0b011111000000000, + 0b000011110000000, + 0b000001110000000, + 0b000001110000000, + 0b000001110000000, + 0b000001110000000, + 0b000000000000000, + 0b000001110000000, + 0b000001110000000, + 0b000001110000000, + }; + + // Then for each possible width and height, we have hard-coded bit masks + // indicating a range of columns and rows to select from the base bitmap + // to produce a scaled down version of reasonable quality. + constexpr std::array widthMasks = { + // clang-format off + 0, 1, 3, 32771, 8457, 9481, 9545, 9673, 42441, 26061, + 58829, 28141, 60909, 63453, 63485, 65533, 65535 + // clang-format on + }; + constexpr std::array heightMasks = { + // clang-format off + 0, 1, 3, 7, 15, 1613952, 10002560, 10002816, 10068352, 10068353, + 26845569, 26847617, 26847619, 26864003, 28961155, 28961219, + 28961731, 62516163, 62516167, 129625031, 129625039, 129756111, + 263973839, 263974863, 268169167, 536604623, 536608719, 536608735, + 536870879, 1073741791, 2147483615, 2147483647, 4294967295 + // clang-format on + }; + + const auto widthMask = widthMasks.at(_fullWidth); + const auto heightMask = heightMasks.at(_fullHeight); + + auto outputBitPattern = std::array{}; + auto outputIterator = outputBitPattern.begin(); + for (auto y = 0; y < MAX_HEIGHT; y++) + { + const auto yBit = (1 << y); + if (heightMask & yBit) + { + const uint16_t inputScanline = til::at(inputBitPattern, y); + uint16_t outputScanline = 0; + for (auto x = MAX_WIDTH; x-- > 0;) + { + const auto xBit = 1 << x; + if (widthMask & xBit) + { + outputScanline <<= 1; + outputScanline |= (inputScanline & xBit) ? 1 : 0; + } + } + outputScanline <<= (MAX_WIDTH - _fullWidth); + *(outputIterator++) = outputScanline; + } + } + return outputBitPattern; +} diff --git a/src/terminal/adapter/FontBuffer.hpp b/src/terminal/adapter/FontBuffer.hpp new file mode 100644 index 000000000..bdd6fff72 --- /dev/null +++ b/src/terminal/adapter/FontBuffer.hpp @@ -0,0 +1,95 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FontBuffer.hpp + +Abstract: +- This manages the construction and storage of font definitions for the VT DECDLD control sequence. +--*/ + +#pragma once + +#include "DispatchTypes.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class FontBuffer + { + public: + FontBuffer() noexcept; + ~FontBuffer() = default; + bool SetEraseControl(const DispatchTypes::DrcsEraseControl eraseControl) noexcept; + bool SetAttributes(const DispatchTypes::DrcsCellMatrix cellMatrix, + const VTParameter cellHeight, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage) noexcept; + bool SetStartChar(const VTParameter startChar, + const DispatchTypes::DrcsCharsetSize charsetSize) noexcept; + void AddSixelData(const wchar_t ch); + bool FinalizeSixelData(); + + gsl::span GetBitPattern() const noexcept; + til::size GetCellSize() const; + size_t GetTextCenteringHint() const noexcept; + VTID GetDesignation() const noexcept; + + private: + static constexpr size_t MAX_WIDTH = 16; + static constexpr size_t MAX_HEIGHT = 32; + static constexpr size_t MAX_CHARS = 96; + + void _buildCharsetId(const wchar_t ch); + void _prepareCharacterBuffer(); + void _prepareNextCharacter(); + void _addSixelValue(const size_t value) noexcept; + void _endOfSixelLine(); + void _endOfCharacter(); + + std::tuple _calculateDimensions() const; + void _packAndCenterBitPatterns(); + void _fillUnusedCharacters(); + std::array _generateErrorGlyph(); + + DispatchTypes::DrcsCellMatrix _cellMatrix; + DispatchTypes::DrcsCellMatrix _pendingCellMatrix; + size_t _cellHeight; + size_t _pendingCellHeight; + bool _sizeDeclaredAsMatrix; + size_t _declaredWidth; + size_t _declaredHeight; + size_t _usedWidth; + size_t _usedHeight; + size_t _fullWidth; + size_t _fullHeight; + size_t _textWidth; + size_t _textOffset; + size_t _textCenteringHint; + + DispatchTypes::DrcsFontSet _fontSet; + DispatchTypes::DrcsFontSet _pendingFontSet; + DispatchTypes::DrcsFontUsage _fontUsage; + DispatchTypes::DrcsFontUsage _pendingFontUsage; + size_t _linesPerPage; + size_t _columnsPerPage; + bool _isTextFont; + + DispatchTypes::DrcsCharsetSize _charsetSize; + DispatchTypes::DrcsCharsetSize _pendingCharsetSize; + VTID _charsetId{ 0 }; + VTID _pendingCharsetId{ 0 }; + bool _charsetIdInitialized; + VTIDBuilder _charsetIdBuilder; + size_t _startChar; + size_t _lastChar; + size_t _currentChar; + + using buffer_type = std::array; + buffer_type _buffer; + buffer_type::iterator _currentCharBuffer; + bool _bufferCleared; + size_t _sixelColumn; + size_t _sixelRow; + }; +} diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index fe53e49d8..b0efd81d8 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -23,6 +23,8 @@ namespace Microsoft::Console::VirtualTerminal class Microsoft::Console::VirtualTerminal::ITermDispatch { public: + using StringHandler = std::function; + #pragma warning(push) #pragma warning(disable : 26432) // suppress rule of 5 violation on interface because tampering with this is fraught with peril virtual ~ITermDispatch() = 0; @@ -130,6 +132,15 @@ public: virtual bool EndHyperlink() = 0; virtual bool DoConEmuAction(const std::wstring_view string) = 0; + + virtual StringHandler DownloadDRCS(const size_t fontNumber, + const VTParameter startChar, + const DispatchTypes::DrcsEraseControl eraseControl, + const DispatchTypes::DrcsCellMatrix cellMatrix, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage, + const VTParameter cellHeight, + const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD }; inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() {} #pragma warning(pop) diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 4bd30cbc8..50db520c0 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -1884,6 +1884,7 @@ bool AdaptDispatch::SoftReset() // - Performs a communications line disconnect. // - Clears UDKs. // - Clears a down-line-loaded character set. +// * The soft font is reset in the renderer and the font buffer is deleted. // - Clears the screen. // * This is like Erase in Display (3), also clearing scrollback, as well as ED(2) // - Returns the cursor to the upper-left corner of the screen. @@ -1929,6 +1930,10 @@ bool AdaptDispatch::HardReset() // Delete all current tab stops and reapply _ResetTabStops(); + // Clear the soft font in the renderer and delete the font buffer. + success = _pConApi->PrivateUpdateSoftFont({}, {}, false) && success; + _fontBuffer = nullptr; + // GH#2715 - If all this succeeded, but we're in a conpty, return `false` to // make the state machine propagate this RIS sequence to the connected // terminal application. We've reset our state, but the connected terminal @@ -2440,6 +2445,88 @@ bool AdaptDispatch::DoConEmuAction(const std::wstring_view /*string*/) noexcept return false; } +// Method Description: +// - DECDLD - Downloads one or more characters of a dynamically redefinable +// character set (DRCS) with a specified pixel pattern. The pixel array is +// transmitted in sixel format via the returned StringHandler function. +// Arguments: +// - fontNumber - The buffer number into which the font will be loaded. +// - startChar - The first character in the set that will be replaced. +// - eraseControl - Which characters to erase before loading the new data. +// - cellMatrix - The character cell width (sometimes also height in legacy formats). +// - fontSet - The screen size for which the font is designed. +// - fontUsage - Whether it is a text font or a full-cell font. +// - cellHeight - The character cell height (if not defined by cellMatrix). +// - charsetSize - Whether the character set is 94 or 96 characters. +// Return Value: +// - a function to receive the pixel data or nullptr if parameters are invalid +ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const size_t fontNumber, + const VTParameter startChar, + const DispatchTypes::DrcsEraseControl eraseControl, + const DispatchTypes::DrcsCellMatrix cellMatrix, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage, + const VTParameter cellHeight, + const DispatchTypes::DrcsCharsetSize charsetSize) +{ + // If we're a conpty, we're just going to ignore the operation for now. + // There's no point in trying to pass it through without also being able + // to pass through the character set designations. + if (_pConApi->IsConsolePty()) + { + return nullptr; + } + + // The font buffer is created on demand. + if (!_fontBuffer) + { + _fontBuffer = std::make_unique(); + } + + // Only one font buffer is supported, so only 0 (default) and 1 are valid. + auto success = fontNumber <= 1; + success = success && _fontBuffer->SetEraseControl(eraseControl); + success = success && _fontBuffer->SetAttributes(cellMatrix, cellHeight, fontSet, fontUsage); + success = success && _fontBuffer->SetStartChar(startChar, charsetSize); + + // If any of the parameters are invalid, we return a null handler to let + // the state machine know we want to ignore the subsequent data string. + if (!success) + { + return nullptr; + } + + return [=](const auto ch) { + // We pass the data string straight through to the font buffer class + // until we receive an ESC, indicating the end of the string. At that + // point we can finalize the buffer, and if valid, update the renderer + // with the constructed bit pattern. + if (ch != AsciiChars::ESC) + { + _fontBuffer->AddSixelData(ch); + } + else if (_fontBuffer->FinalizeSixelData()) + { + // We also need to inform the character set mapper of the ID that + // will map to this font (we only support one font buffer so there + // will only ever be one active dynamic character set). + if (charsetSize == DispatchTypes::DrcsCharsetSize::Size96) + { + _termOutput.SetDrcs96Designation(_fontBuffer->GetDesignation()); + } + else + { + _termOutput.SetDrcs94Designation(_fontBuffer->GetDesignation()); + } + const auto bitPattern = _fontBuffer->GetBitPattern(); + const auto cellSize = _fontBuffer->GetCellSize(); + const auto centeringHint = _fontBuffer->GetTextCenteringHint(); + _pConApi->PrivateUpdateSoftFont(bitPattern, cellSize, centeringHint); + } + return true; + }; +} + // Routine Description: // - Determines whether we should pass any sequence that manipulates // TerminalInput's input generator through the PTY. It encapsulates diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 5ea582343..7eeade5cb 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -18,6 +18,7 @@ Author(s): #include "DispatchCommon.hpp" #include "conGetSet.hpp" #include "adaptDefaults.hpp" +#include "FontBuffer.hpp" #include "terminalOutput.hpp" #include "..\..\types\inc\sgrStack.hpp" @@ -130,6 +131,15 @@ namespace Microsoft::Console::VirtualTerminal bool DoConEmuAction(const std::wstring_view string) noexcept override; + StringHandler DownloadDRCS(const size_t fontNumber, + const VTParameter startChar, + const DispatchTypes::DrcsEraseControl eraseControl, + const DispatchTypes::DrcsCellMatrix cellMatrix, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage, + const VTParameter cellHeight, + const DispatchTypes::DrcsCharsetSize charsetSize) override; // DECDLD + private: enum class ScrollDirection { @@ -187,6 +197,7 @@ namespace Microsoft::Console::VirtualTerminal std::unique_ptr _pConApi; std::unique_ptr _pDefaults; TerminalOutput _termOutput; + std::unique_ptr _fontBuffer; std::optional _initialCodePage; // We have two instances of the saved cursor state, because we need diff --git a/src/terminal/adapter/charsets.hpp b/src/terminal/adapter/charsets.hpp index 4ead5f14b..0a3a649ff 100644 --- a/src/terminal/adapter/charsets.hpp +++ b/src/terminal/adapter/charsets.hpp @@ -43,6 +43,11 @@ namespace Microsoft::Console::VirtualTerminal return rhs == lhs; } + // 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 + // entry - which is not meant to be mapped - as a SPACE or NBSP, which is at + // least visually equivalent to leaving it untranslated. + typedef CharSet AsciiBasedCharSet; typedef CharSet Latin1BasedCharSet94; typedef CharSet Latin1BasedCharSet96; @@ -1051,5 +1056,11 @@ namespace Microsoft::Console::VirtualTerminal { L'\x7e', L'\u00fc' }, // Latin Small Letter U With Diaeresis }; + // We're reserving 96 characters (U+EF20 to U+EF7F) from the Unicode + // Private Use Area for our dynamically redefinable characters sets. + static constexpr auto DRCS_BASE_CHAR = L'\uEF20'; + static constexpr auto Drcs94 = CharSet{ { DRCS_BASE_CHAR, '\x20' } }; + static constexpr auto Drcs96 = CharSet{}; + #pragma warning(pop) } diff --git a/src/terminal/adapter/conGetSet.hpp b/src/terminal/adapter/conGetSet.hpp index d604d8e06..e69ab753f 100644 --- a/src/terminal/adapter/conGetSet.hpp +++ b/src/terminal/adapter/conGetSet.hpp @@ -107,5 +107,9 @@ namespace Microsoft::Console::VirtualTerminal virtual bool PrivateAddHyperlink(const std::wstring_view uri, const std::wstring_view params) const = 0; virtual bool PrivateEndHyperlink() const = 0; + + virtual bool PrivateUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) = 0; }; } diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj index eeb6f4574..1626e7a7d 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -12,6 +12,7 @@ + @@ -27,6 +28,7 @@ + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters index 4675a7e3a..fb644781d 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj.filters +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -39,6 +39,9 @@ Source Files + + Source Files + @@ -83,6 +86,9 @@ Header Files + + Header Files + diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc index 0fadc810f..8b3ae4f43 100644 --- a/src/terminal/adapter/sources.inc +++ b/src/terminal/adapter/sources.inc @@ -32,6 +32,7 @@ PRECOMPILED_INCLUDE = ..\precomp.h SOURCES= \ ..\adaptDispatch.cpp \ ..\DispatchCommon.cpp \ + ..\FontBuffer.cpp \ ..\InteractDispatch.cpp \ ..\adaptDispatchGraphics.cpp \ ..\terminalOutput.cpp \ diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index d82a28c22..1fcb83a4e 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -123,4 +123,13 @@ public: bool EndHyperlink() noexcept override { return false; } bool DoConEmuAction(const std::wstring_view /*string*/) noexcept override { return false; } + + StringHandler DownloadDRCS(const size_t /*fontNumber*/, + const VTParameter /*startChar*/, + const DispatchTypes::DrcsEraseControl /*eraseControl*/, + const DispatchTypes::DrcsCellMatrix /*cellMatrix*/, + const DispatchTypes::DrcsFontSet /*fontSet*/, + const DispatchTypes::DrcsFontUsage /*fontUsage*/, + const VTParameter /*cellHeight*/, + const DispatchTypes::DrcsCharsetSize /*charsetSize*/) noexcept override { return nullptr; } }; diff --git a/src/terminal/adapter/terminalOutput.cpp b/src/terminal/adapter/terminalOutput.cpp index 14de84e13..944ff88b7 100644 --- a/src/terminal/adapter/terminalOutput.cpp +++ b/src/terminal/adapter/terminalOutput.cpp @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#include -#include +#include "precomp.h" #include "charsets.hpp" #include "terminalOutput.hpp" #include "strsafe.h" @@ -19,91 +18,30 @@ TerminalOutput::TerminalOutput() noexcept bool TerminalOutput::Designate94Charset(size_t gsetNumber, const VTID charset) { - switch (charset) - { - case VTID("B"): // US ASCII - case VTID("1"): // Alternate Character ROM - return _SetTranslationTable(gsetNumber, Ascii); - case VTID("0"): // DEC Special Graphics - case VTID("2"): // Alternate Character ROM Special Graphics - return _SetTranslationTable(gsetNumber, DecSpecialGraphics); - case VTID("<"): // DEC Supplemental - return _SetTranslationTable(gsetNumber, DecSupplemental); - case VTID("A"): // British NRCS - return _SetTranslationTable(gsetNumber, BritishNrcs); - case VTID("4"): // Dutch NRCS - return _SetTranslationTable(gsetNumber, DutchNrcs); - case VTID("5"): // Finnish NRCS - case VTID("C"): // (fallback) - return _SetTranslationTable(gsetNumber, FinnishNrcs); - case VTID("R"): // French NRCS - return _SetTranslationTable(gsetNumber, FrenchNrcs); - case VTID("f"): // French NRCS (ISO update) - return _SetTranslationTable(gsetNumber, FrenchNrcsIso); - case VTID("9"): // French Canadian NRCS - case VTID("Q"): // (fallback) - return _SetTranslationTable(gsetNumber, FrenchCanadianNrcs); - case VTID("K"): // German NRCS - return _SetTranslationTable(gsetNumber, GermanNrcs); - case VTID("Y"): // Italian NRCS - return _SetTranslationTable(gsetNumber, ItalianNrcs); - case VTID("6"): // Norwegian/Danish NRCS - case VTID("E"): // (fallback) - return _SetTranslationTable(gsetNumber, NorwegianDanishNrcs); - case VTID("`"): // Norwegian/Danish NRCS (ISO standard) - return _SetTranslationTable(gsetNumber, NorwegianDanishNrcsIso); - case VTID("Z"): // Spanish NRCS - return _SetTranslationTable(gsetNumber, SpanishNrcs); - case VTID("7"): // Swedish NRCS - case VTID("H"): // (fallback) - return _SetTranslationTable(gsetNumber, SwedishNrcs); - case VTID("="): // Swiss NRCS - return _SetTranslationTable(gsetNumber, SwissNrcs); - case VTID("&4"): // DEC Cyrillic - return _SetTranslationTable(gsetNumber, DecCyrillic); - case VTID("&5"): // Russian NRCS - return _SetTranslationTable(gsetNumber, RussianNrcs); - case VTID("\"?"): // DEC Greek - return _SetTranslationTable(gsetNumber, DecGreek); - case VTID("\">"): // Greek NRCS - return _SetTranslationTable(gsetNumber, GreekNrcs); - case VTID("\"4"): // DEC Hebrew - return _SetTranslationTable(gsetNumber, DecHebrew); - case VTID("%="): // Hebrew NRCS - return _SetTranslationTable(gsetNumber, HebrewNrcs); - case VTID("%0"): // DEC Turkish - return _SetTranslationTable(gsetNumber, DecTurkish); - case VTID("%2"): // Turkish NRCS - return _SetTranslationTable(gsetNumber, TurkishNrcs); - case VTID("%5"): // DEC Supplemental - return _SetTranslationTable(gsetNumber, DecSupplemental); - case VTID("%6"): // Portuguese NRCS - return _SetTranslationTable(gsetNumber, PortugueseNrcs); - default: - return false; - } + const auto translationTable = _LookupTranslationTable94(charset); + RETURN_BOOL_IF_FALSE(!translationTable.empty()); + return _SetTranslationTable(gsetNumber, translationTable); } bool TerminalOutput::Designate96Charset(size_t gsetNumber, const VTID charset) { - switch (charset) - { - case VTID("A"): // ISO Latin-1 Supplemental - case VTID("<"): // (UPSS when assigned to Latin-1) - return _SetTranslationTable(gsetNumber, Latin1); - case VTID("B"): // ISO Latin-2 Supplemental - return _SetTranslationTable(gsetNumber, Latin2); - case VTID("L"): // ISO Latin-Cyrillic Supplemental - return _SetTranslationTable(gsetNumber, LatinCyrillic); - case VTID("F"): // ISO Latin-Greek Supplemental - return _SetTranslationTable(gsetNumber, LatinGreek); - case VTID("H"): // ISO Latin-Hebrew Supplemental - return _SetTranslationTable(gsetNumber, LatinHebrew); - case VTID("M"): // ISO Latin-5 Supplemental - return _SetTranslationTable(gsetNumber, Latin5); - default: - return false; - } + const auto translationTable = _LookupTranslationTable96(charset); + RETURN_BOOL_IF_FALSE(!translationTable.empty()); + return _SetTranslationTable(gsetNumber, translationTable); +} + +void TerminalOutput::SetDrcs94Designation(const VTID charset) +{ + _ReplaceDrcsTable(_LookupTranslationTable94(charset), Drcs94); + _drcsId = charset; + _drcsTranslationTable = Drcs94; +} + +void TerminalOutput::SetDrcs96Designation(const VTID charset) +{ + _ReplaceDrcsTable(_LookupTranslationTable96(charset), Drcs96); + _drcsId = charset; + _drcsTranslationTable = Drcs96; } #pragma warning(suppress : 26440) // Suppress spurious "function can be declared noexcept" warning @@ -186,9 +124,139 @@ wchar_t TerminalOutput::TranslateKey(const wchar_t wch) const noexcept return wchFound; } +const std::wstring_view TerminalOutput::_LookupTranslationTable94(const VTID charset) const +{ + // Note that the DRCS set can be designated with either a 94 or 96 sequence, + // regardless of the actual size of the set. This isn't strictly correct, + // but there is existing software that depends on this behavior. + if (charset == _drcsId) + { + return _drcsTranslationTable; + } + switch (charset) + { + case VTID("B"): // US ASCII + case VTID("1"): // Alternate Character ROM + return Ascii; + case VTID("0"): // DEC Special Graphics + case VTID("2"): // Alternate Character ROM Special Graphics + return DecSpecialGraphics; + case VTID("<"): // DEC Supplemental + return DecSupplemental; + case VTID("A"): // British NRCS + return BritishNrcs; + case VTID("4"): // Dutch NRCS + return DutchNrcs; + case VTID("5"): // Finnish NRCS + case VTID("C"): // (fallback) + return FinnishNrcs; + case VTID("R"): // French NRCS + return FrenchNrcs; + case VTID("f"): // French NRCS (ISO update) + return FrenchNrcsIso; + case VTID("9"): // French Canadian NRCS + case VTID("Q"): // (fallback) + return FrenchCanadianNrcs; + case VTID("K"): // German NRCS + return GermanNrcs; + case VTID("Y"): // Italian NRCS + return ItalianNrcs; + case VTID("6"): // Norwegian/Danish NRCS + case VTID("E"): // (fallback) + return NorwegianDanishNrcs; + case VTID("`"): // Norwegian/Danish NRCS (ISO standard) + return NorwegianDanishNrcsIso; + case VTID("Z"): // Spanish NRCS + return SpanishNrcs; + case VTID("7"): // Swedish NRCS + case VTID("H"): // (fallback) + return SwedishNrcs; + case VTID("="): // Swiss NRCS + return SwissNrcs; + case VTID("&4"): // DEC Cyrillic + return DecCyrillic; + case VTID("&5"): // Russian NRCS + return RussianNrcs; + case VTID("\"?"): // DEC Greek + return DecGreek; + case VTID("\">"): // Greek NRCS + return GreekNrcs; + case VTID("\"4"): // DEC Hebrew + return DecHebrew; + case VTID("%="): // Hebrew NRCS + return HebrewNrcs; + case VTID("%0"): // DEC Turkish + return DecTurkish; + case VTID("%2"): // Turkish NRCS + return TurkishNrcs; + case VTID("%5"): // DEC Supplemental + return DecSupplemental; + case VTID("%6"): // Portuguese NRCS + return PortugueseNrcs; + default: + return {}; + } +} + +const std::wstring_view TerminalOutput::_LookupTranslationTable96(const VTID charset) const +{ + // Note that the DRCS set can be designated with either a 94 or 96 sequence, + // regardless of the actual size of the set. This isn't strictly correct, + // but there is existing software that depends on this behavior. + if (charset == _drcsId) + { + return _drcsTranslationTable; + } + switch (charset) + { + case VTID("A"): // ISO Latin-1 Supplemental + case VTID("<"): // (UPSS when assigned to Latin-1) + return Latin1; + case VTID("B"): // ISO Latin-2 Supplemental + return Latin2; + case VTID("L"): // ISO Latin-Cyrillic Supplemental + return LatinCyrillic; + case VTID("F"): // ISO Latin-Greek Supplemental + return LatinGreek; + case VTID("H"): // ISO Latin-Hebrew Supplemental + return LatinHebrew; + case VTID("M"): // ISO Latin-5 Supplemental + return Latin5; + default: + return {}; + } +} + bool TerminalOutput::_SetTranslationTable(const size_t gsetNumber, const std::wstring_view translationTable) { _gsetTranslationTables.at(gsetNumber) = translationTable; // We need to reapply the locking shifts in case the underlying G-sets have changed. return LockingShift(_glSetNumber) && LockingShiftRight(_grSetNumber); } + +void TerminalOutput::_ReplaceDrcsTable(const std::wstring_view oldTable, const std::wstring_view newTable) +{ + if (newTable.data() != oldTable.data()) + { + for (size_t gsetNumber = 0; gsetNumber < 4; gsetNumber++) + { + // Get the current translation table for this G-set. + auto gsetTable = _gsetTranslationTables.at(gsetNumber); + // If it's already a DRCS, replace it with a default charset. + if (Drcs94 == gsetTable || Drcs96 == gsetTable) + { + gsetTable = gsetNumber < 2 ? (std::wstring_view)Ascii : (std::wstring_view)Latin1; + } + // If it matches the old table, replace it with the new table. + if (gsetTable.data() == oldTable.data()) + { + gsetTable = newTable; + } + // Update the G-set entry with the new translation table. + _gsetTranslationTables.at(gsetNumber) = gsetTable; + } + // Reapply the locking shifts in case the underlying G-sets have changed. + LockingShift(_glSetNumber); + LockingShiftRight(_grSetNumber); + } +} diff --git a/src/terminal/adapter/terminalOutput.hpp b/src/terminal/adapter/terminalOutput.hpp index e82e067a7..aabc12657 100644 --- a/src/terminal/adapter/terminalOutput.hpp +++ b/src/terminal/adapter/terminalOutput.hpp @@ -28,6 +28,8 @@ namespace Microsoft::Console::VirtualTerminal wchar_t TranslateKey(const wchar_t wch) const noexcept; bool Designate94Charset(const size_t gsetNumber, const VTID charset); bool Designate96Charset(const size_t gsetNumber, const VTID charset); + void SetDrcs94Designation(const VTID charset); + void SetDrcs96Designation(const VTID charset); bool LockingShift(const size_t gsetNumber); bool LockingShiftRight(const size_t gsetNumber); bool SingleShift(const size_t gsetNumber); @@ -35,7 +37,10 @@ namespace Microsoft::Console::VirtualTerminal void EnableGrTranslation(boolean enabled); private: + const std::wstring_view _LookupTranslationTable94(const VTID charset) const; + const std::wstring_view _LookupTranslationTable96(const VTID charset) const; bool _SetTranslationTable(const size_t gsetNumber, const std::wstring_view translationTable); + void _ReplaceDrcsTable(const std::wstring_view oldTable, const std::wstring_view newTable); std::array _gsetTranslationTables; size_t _glSetNumber = 0; @@ -44,5 +49,7 @@ namespace Microsoft::Console::VirtualTerminal std::wstring_view _grTranslationTable; mutable std::wstring_view _ssTranslationTable; boolean _grTranslationEnabled = false; + VTID _drcsId = 0; + std::wstring_view _drcsTranslationTable; }; } diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index ce120ea3b..13f9476ae 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -567,6 +567,19 @@ public: return TRUE; } + bool PrivateUpdateSoftFont(const gsl::span /*bitPattern*/, + const SIZE cellSize, + const size_t /*centeringHint*/) noexcept override + { + Log::Comment(L"PrivateUpdateSoftFont MOCK called..."); + + Log::Comment(NoThrowString().Format(L"Cell size: %dx%d", cellSize.cx, cellSize.cy)); + VERIFY_ARE_EQUAL(_expectedCellSize.cx, cellSize.cx); + VERIFY_ARE_EQUAL(_expectedCellSize.cy, cellSize.cy); + + return TRUE; + } + void PrepData() { PrepData(CursorDirection::UP); // if called like this, the cursor direction doesn't matter. @@ -810,6 +823,8 @@ public: bool _privateSetDefaultBackgroundResult = false; COLORREF _expectedDefaultBackgroundColorValue = INVALID_COLOR; + SIZE _expectedCellSize = {}; + private: HANDLE _hCon; }; @@ -2353,6 +2368,229 @@ public: VERIFY_IS_FALSE(_pDispatch.get()->SetColorTableEntry(15, testColor)); } + TEST_METHOD(SoftFontSizeDetection) + { + using CellMatrix = DispatchTypes::DrcsCellMatrix; + using FontSet = DispatchTypes::DrcsFontSet; + using FontUsage = DispatchTypes::DrcsFontUsage; + + const auto decdld = [=](const auto cmw, const auto cmh, const auto ss, const auto u, const std::wstring_view data = {}) { + const auto ec = DispatchTypes::DrcsEraseControl::AllChars; + const auto css = DispatchTypes::DrcsCharsetSize::Size94; + const auto cellMatrix = static_cast(cmw); + const auto stringHandler = _pDispatch.get()->DownloadDRCS(0, 0, ec, cellMatrix, ss, u, cmh, css); + if (stringHandler) + { + stringHandler(L'B'); // Charset identifier + for (auto ch : data) + { + stringHandler(ch); + } + stringHandler(L'\033'); // String terminator + } + return stringHandler != nullptr; + }; + + // Matrix sizes at 80x24 should always use a 10x10 cell size (VT2xx). + Log::Comment(L"Matrix 5x10 for 80x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size5x10, 0, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Matrix 6x10 for 80x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size6x10, 0, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Matrix 7x10 for 80x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size7x10, 0, FontSet::Size80x24, FontUsage::Text)); + + // At 132x24 the cell size is typically 6x10 (VT240), but could be 10x10 (VT220) + Log::Comment(L"Matrix 5x10 for 132x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size5x10, 0, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Matrix 6x10 for 132x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size6x10, 0, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Matrix 7x10 for 132x24 font set with text usage (VT220 only)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size7x10, 0, FontSet::Size132x24, FontUsage::Text)); + + // Full cell usage is invalid for all matrix sizes except 6x10 at 132x24. + Log::Comment(L"Matrix 5x10 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size5x10, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 6x10 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size6x10, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 7x10 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size7x10, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 5x10 for 132x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size5x10, 0, FontSet::Size132x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 6x10 for 132x24 font set with full cell usage"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size6x10, 0, FontSet::Size132x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 7x10 for 132x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size7x10, 0, FontSet::Size132x24, FontUsage::FullCell)); + + // Matrix size 1 is always invalid. + Log::Comment(L"Matrix 1 for 80x24 font set with text usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Matrix 1 for 132x24 font set with text usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Matrix 1 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 1 for 132x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size132x24, FontUsage::FullCell)); + + // The height parameter has no effect when a matrix size is used. + Log::Comment(L"Matrix 7x10 with unused height parameter"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size7x10, 20, FontSet::Size80x24, FontUsage::Text)); + + // Full cell fonts with explicit dimensions are accepted as their given cell size. + Log::Comment(L"Explicit 13x17 for 80x24 font set with full cell usage"); + _testGetSet->_expectedCellSize = { 13, 17 }; + VERIFY_IS_TRUE(decdld(13, 17, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Explicit 9x25 for 132x24 font set with full cell usage"); + _testGetSet->_expectedCellSize = { 9, 25 }; + VERIFY_IS_TRUE(decdld(9, 25, FontSet::Size132x24, FontUsage::FullCell)); + + // Cell sizes outside the maximum supported range (16x32) are invalid. + Log::Comment(L"Explicit 18x38 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(18, 38, FontSet::Size80x24, FontUsage::FullCell)); + + // Text fonts with explicit dimensions are interpreted as their closest matching device. + Log::Comment(L"Explicit 12x12 for 80x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 15, 12 }; + VERIFY_IS_TRUE(decdld(12, 12, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 9x20 for 80x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 10, 20 }; + VERIFY_IS_TRUE(decdld(9, 20, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 10x30 for 80x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 12, 30 }; + VERIFY_IS_TRUE(decdld(10, 30, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 8x16 for 80x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 16 }; + VERIFY_IS_TRUE(decdld(8, 16, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 7x12 for 132x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 9, 12 }; + VERIFY_IS_TRUE(decdld(7, 12, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Explicit 5x20 for 132x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 6, 20 }; + VERIFY_IS_TRUE(decdld(5, 20, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Explicit 6x30 for 132x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 7, 30 }; + VERIFY_IS_TRUE(decdld(6, 30, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Explicit 5x16 for 132x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 16 }; + VERIFY_IS_TRUE(decdld(5, 16, FontSet::Size132x24, FontUsage::Text)); + + // Font sets with more than 24 lines must be VT420/VT5xx. + Log::Comment(L"80x36 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x36, FontUsage::Text)); + Log::Comment(L"80x48 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x48, FontUsage::Text)); + Log::Comment(L"132x36 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x36, FontUsage::Text)); + Log::Comment(L"132x48 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x48, FontUsage::Text)); + Log::Comment(L"80x36 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x36, FontUsage::FullCell)); + Log::Comment(L"80x48 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x48, FontUsage::FullCell)); + Log::Comment(L"132x36 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x36, FontUsage::FullCell)); + Log::Comment(L"132x48 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x48, FontUsage::FullCell)); + + // Without an explicit size, the cell size is estimated from the number of sixels + // used in the character bitmaps. But note that sixel heights are always a multiple + // of 6, so will often be larger than the cell size for which they were intended. + Log::Comment(L"8x12 bitmap for 80x24 font set with text usage (VT2xx)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + const auto bitmapOf8x12 = L"????????/????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf8x12)); + Log::Comment(L"12x12 bitmap for 80x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 15, 12 }; + const auto bitmapOf12x12 = L"????????????/????????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf12x12)); + Log::Comment(L"9x24 bitmap for 80x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 10, 20 }; + const auto bitmapOf9x24 = L"?????????/?????????/?????????/?????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf9x24)); + Log::Comment(L"10x30 bitmap for 80x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 12, 30 }; + const auto bitmapOf10x30 = L"??????????/??????????/??????????/??????????/??????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf10x30)); + Log::Comment(L"8x18 bitmap for 80x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 16 }; + const auto bitmapOf8x18 = L"????????/????????/????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf8x18)); + + Log::Comment(L"5x12 bitmap for 132x24 font set with text usage (VT240)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + const auto bitmapOf5x12 = L"?????/?????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf5x12)); + Log::Comment(L"7x12 bitmap for 132x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 9, 12 }; + const auto bitmapOf7x12 = L"???????/???????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf7x12)); + Log::Comment(L"5x24 bitmap for 132x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 6, 20 }; + const auto bitmapOf5x24 = L"?????/?????/?????/?????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf5x24)); + Log::Comment(L"6x30 bitmap for 132x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 7, 30 }; + const auto bitmapOf6x30 = L"??????/??????/??????/??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf6x30)); + Log::Comment(L"5x18 bitmap for 132x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 16 }; + const auto bitmapOf5x18 = L"?????/?????/?????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf5x18)); + + Log::Comment(L"15x12 bitmap for 80x24 font set with full cell usage (VT320)"); + _testGetSet->_expectedCellSize = { 15, 12 }; + const auto bitmapOf15x12 = L"???????????????/???????????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf15x12)); + Log::Comment(L"10x24 bitmap for 80x24 font set with full cell usage (VT340)"); + _testGetSet->_expectedCellSize = { 10, 20 }; + const auto bitmapOf10x24 = L"??????????/??????????/??????????/??????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf10x24)); + Log::Comment(L"12x30 bitmap for 80x24 font set with full cell usage (VT382)"); + _testGetSet->_expectedCellSize = { 12, 30 }; + const auto bitmapOf12x30 = L"????????????/????????????/????????????/????????????/????????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf12x30)); + Log::Comment(L"10x18 bitmap for 80x24 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 16 }; + const auto bitmapOf10x18 = L"??????????/??????????/??????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf10x18)); + + Log::Comment(L"6x12 bitmap for 132x24 font set with full cell usage (VT240)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + const auto bitmapOf6x12 = L"??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x12)); + Log::Comment(L"9x12 bitmap for 132x24 font set with full cell usage (VT320)"); + _testGetSet->_expectedCellSize = { 9, 12 }; + const auto bitmapOf9x12 = L"?????????/?????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf9x12)); + Log::Comment(L"6x24 bitmap for 132x24 font set with full cell usage (VT340)"); + _testGetSet->_expectedCellSize = { 6, 20 }; + const auto bitmapOf6x24 = L"??????/??????/??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x24)); + Log::Comment(L"7x30 bitmap for 132x24 font set with full cell usage (VT382)"); + _testGetSet->_expectedCellSize = { 7, 30 }; + const auto bitmapOf7x30 = L"???????/???????/???????/???????/???????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf7x30)); + Log::Comment(L"6x18 bitmap for 132x24 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 16 }; + const auto bitmapOf6x18 = L"??????/??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x18)); + } + private: TestGetSet* _testGetSet; // non-ownership pointer std::unique_ptr _pDispatch; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 0ecd5ea4d..953fce415 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -645,10 +645,27 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete // - parameters - set of numeric parameters collected while parsing the sequence. // Return Value: // - the data string handler function or nullptr if the sequence is not supported -IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(const VTID /*id*/, const VTParameters /*parameters*/) noexcept +IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(const VTID id, const VTParameters parameters) { StringHandler handler = nullptr; + switch (id) + { + case DcsActionCodes::DECDLD_DownloadDRCS: + handler = _dispatch->DownloadDRCS(parameters.at(0), + parameters.at(1), + parameters.at(2), + parameters.at(3), + parameters.at(4), + parameters.at(5), + parameters.at(6), + parameters.at(7)); + break; + default: + handler = nullptr; + break; + } + _ClearLastChar(); return handler; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 1f79795a9..8ef831160 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -39,7 +39,7 @@ namespace Microsoft::Console::VirtualTerminal bool ActionCsiDispatch(const VTID id, const VTParameters parameters) override; - StringHandler ActionDcsDispatch(const VTID id, const VTParameters parameters) noexcept override; + StringHandler ActionDcsDispatch(const VTID id, const VTParameters parameters) override; bool ActionClear() noexcept override; @@ -144,6 +144,11 @@ namespace Microsoft::Console::VirtualTerminal DECSCPP_SetColumnsPerPage = VTID("$|"), }; + enum DcsActionCodes : uint64_t + { + DECDLD_DownloadDRCS = VTID("{"), + }; + enum Vt52ActionCodes : uint64_t { CursorUp = VTID("A"), From 9f2d40614bde8f80dbcc11f369dc33e77f902652 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 9 Aug 2021 10:21:59 -0500 Subject: [PATCH 50/90] Allow `ThrottledFunc` to work on different types of dispatcher (#10187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### ⚠️ targets #10051 ## Summary of the Pull Request This updates our `ThrottledFunc`s to take a dispatcher parameter. This means that we can use the `Windows::UI::Core::CoreDispatcher` in the `TermControl`, where there's always a `CoreDispatcher`, and use a `Windows::System::DispatcherQueue` in `ControlCore`/`ControlInteractivity`. When running in-proc, these are always the _same thing_. However, out-of-proc, the core needs a dispatcher queue that's not tied to a UI thread (because the content proces _doesn't have a UI thread!_). This lets us get rid of the output event, because we don't need to bubble that event out to the `TermControl` to let it throttle that update anymore. ## References * Tear-out: #1256 * Megathread: #5000 * Project: https://github.com/microsoft/terminal/projects/5 ## PR Checklist * [x] This is a part of #1256 * [x] I work here * [n/a] Tests added/passed * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments Fortunately, `winrt::resume_foreground` works the same on both a `CoreDispatcher` and a `DispatcherQueue`, so this wasn't too hard! ## Validation Steps Performed This was validated in `dev/migrie/oop/the-whole-thing` (or `dev/migrie/oop/connection-factory`, I forget which), and I made sure that it worked both in-proc and x-proc. Not only that, _it wasn't any slower_!This reverts commit 04b751faa70680bf0296063deacec4657c6ff9d6. --- src/cascadia/TerminalApp/AppLogic.cpp | 2 +- src/cascadia/TerminalControl/ControlCore.cpp | 106 +++++++++++++++--- src/cascadia/TerminalControl/ControlCore.h | 23 +++- src/cascadia/TerminalControl/TermControl.cpp | 83 ++++---------- src/cascadia/TerminalControl/TermControl.h | 9 +- .../UnitTests_Control/ControlCoreTests.cpp | 25 +++-- .../ControlInteractivityTests.cpp | 11 +- src/cascadia/UnitTests_Control/pch.h | 2 + src/cascadia/WinRTUtils/inc/ThrottledFunc.h | 8 +- 9 files changed, 169 insertions(+), 100 deletions(-) diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index a9ac10d8f..c14a951de 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -203,7 +203,7 @@ namespace winrt::TerminalApp::implementation _isElevated = _isUserAdmin(); _root = winrt::make_self(); - _reloadSettings = std::make_shared>(_root->Dispatcher(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() { + _reloadSettings = std::make_shared>(winrt::Windows::System::DispatcherQueue::GetForCurrentThread(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() { if (auto self{ weakSelf.get() }) { self->_ReloadSettings(); diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index bf106322a..2cc3fef16 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -23,6 +23,16 @@ using namespace winrt::Windows::Graphics::Display; using namespace winrt::Windows::System; using namespace winrt::Windows::ApplicationModel::DataTransfer; +// The minimum delay between updates to the scroll bar's values. +// The updates are throttled to limit power usage. +constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8); + +// The minimum delay between updating the TSF input control. +constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100); + +// The minimum delay between updating the locations of regex patterns +constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500); + namespace winrt::Microsoft::Terminal::Control::implementation { // Helper static function to ensure that all ambiguous-width glyphs are reported as narrow. @@ -94,6 +104,61 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto pfnTerminalTaskbarProgressChanged = std::bind(&ControlCore::_terminalTaskbarProgressChanged, this); _terminal->TaskbarProgressChangedCallback(pfnTerminalTaskbarProgressChanged); + // Get our dispatcher. If we're hosted in-proc with XAML, this will get + // us the same dispatcher as TermControl::Dispatcher(). If we're out of + // proc, this'll return null. We'll need to instead make a new + // DispatcherQueue (on a new thread), so we can use that for throttled + // functions. + _dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + if (!_dispatcher) + { + auto controller{ winrt::Windows::System::DispatcherQueueController::CreateOnDedicatedThread() }; + _dispatcher = controller.DispatcherQueue(); + } + + // A few different events should be throttled, so they don't fire absolutely all the time: + // * _tsfTryRedrawCanvas: When the cursor position moves, we need to + // inform TSF, so it can move the canvas for the composition. We + // throttle this so that we're not hopping across the process boundary + // every time that the cursor moves. + // * _updatePatternLocations: When there's new output, or we scroll the + // viewport, we should re-check if there are any visible hyperlinks. + // But we don't really need to do this every single time text is + // output, we can limit this update to once every 500ms. + // * _updateScrollBar: Same idea as the TSF update - we don't _really_ + // need to hop across the process boundary every time text is output. + // We can throttle this to once every 8ms, which will get us out of + // the way of the main output & rendering threads. + _tsfTryRedrawCanvas = std::make_shared>( + _dispatcher, + TsfRedrawInterval, + [weakThis = get_weak()]() { + if (auto core{ weakThis.get() }; !core->_IsClosing()) + { + core->_CursorPositionChangedHandlers(*core, nullptr); + } + }); + + _updatePatternLocations = std::make_shared>( + _dispatcher, + UpdatePatternLocationsInterval, + [weakThis = get_weak()]() { + if (auto core{ weakThis.get() }; !core->_IsClosing()) + { + core->UpdatePatternLocations(); + } + }); + + _updateScrollBar = std::make_shared>( + _dispatcher, + ScrollBarUpdateInterval, + [weakThis = get_weak()](const auto& update) { + if (auto core{ weakThis.get() }; !core->_IsClosing()) + { + core->_ScrollPositionChangedHandlers(*core, update); + } + }); + UpdateSettings(settings); } @@ -1103,15 +1168,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation // TODO GH#9617: refine locking around pattern tree _terminal->ClearPatternTree(); - _ScrollPositionChangedHandlers(*this, - winrt::make(viewTop, - viewHeight, - bufferSize)); + // Start the throttled update of our scrollbar. + auto update{ winrt::make(viewTop, + viewHeight, + bufferSize) }; + if (!_inUnitTests) + { + _updateScrollBar->Run(update); + } + else + { + _ScrollPositionChangedHandlers(*this, update); + } + + // Additionally, start the throttled update of where our links are. + _updatePatternLocations->Run(); } void ControlCore::_terminalCursorPositionChanged() { - _CursorPositionChangedHandlers(*this, nullptr); + // When the buffer's cursor moves, start the throttled func to + // eventually dispatch a CursorPositionChanged event. + _tsfTryRedrawCanvas->Run(); } void ControlCore::_terminalTaskbarProgressChanged() @@ -1221,8 +1299,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::Close() { - if (!_closing.exchange(true)) + if (!_IsClosing()) { + _closing = true; + // Stop accepting new output and state changes before we disconnect everything. _connection.TerminalOutput(_connectionOutputEventToken); _connectionStateChangedRevoker.revoke(); @@ -1400,18 +1480,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _terminal->Write(hstr); - // NOTE: We're raising an event here to inform the TermControl that - // output has been received, so it can queue up a throttled - // UpdatePatternLocations call. In the future, we should have the - // _updatePatternLocations ThrottledFunc internal to this class, and - // run on this object's dispatcher queue. - // - // We're not doing that quite yet, because the Core will eventually - // be out-of-proc from the UI thread, and won't be able to just use - // the UI thread as the dispatcher queue thread. - // - // See TODO: https://github.com/microsoft/terminal/projects/5#card-50760282 - _ReceivedOutputHandlers(*this, nullptr); + // Start the throttled update of where our hyperlinks are. + _updatePatternLocations->Run(); } } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 855aa66d3..3bb321f0b 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -168,7 +168,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation private: bool _initializedTerminal{ false }; - std::atomic _closing{ false }; + bool _closing{ false }; TerminalConnection::ITerminalConnection _connection{ nullptr }; event_token _connectionOutputEventToken; @@ -206,6 +206,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation double _panelHeight{ 0 }; double _compositionScale{ 0 }; + winrt::Windows::System::DispatcherQueue _dispatcher{ nullptr }; + std::shared_ptr> _tsfTryRedrawCanvas; + std::shared_ptr> _updatePatternLocations; + std::shared_ptr> _updateScrollBar; + winrt::fire_and_forget _asyncCloseConnection(); void _setFontSize(int fontSize); @@ -239,8 +244,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _connectionOutputHandler(const hstring& hstr); void _updateHoveredCell(const std::optional terminalPosition); + inline bool _IsClosing() const noexcept + { +#ifndef NDEBUG + if (_dispatcher) + { + // _closing isn't atomic and may only be accessed from the main thread. + // + // Though, the unit tests don't actually run in TAEF's main + // thread, so we don't care when we're running in tests. + assert(_inUnitTests || _dispatcher.HasThreadAccess()); + } +#endif + return _closing; + } + friend class ControlUnitTests::ControlCoreTests; friend class ControlUnitTests::ControlInteractivityTests; + bool _inUnitTests{ false }; }; } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index e1ea1d7a0..0d20606d1 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -33,7 +33,8 @@ using namespace winrt::Windows::ApplicationModel::DataTransfer; constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8); // The minimum delay between updating the TSF input control. -constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100); +// This is already throttled primarily in the ControlCore, with a timeout of 100ms. We're adding another smaller one here, as the (potentially x-proc) call will come in off the UI thread +constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(8); // The minimum delay between updating the locations of regex patterns constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500); @@ -64,10 +65,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _interactivity = winrt::make(settings, connection); _core = _interactivity.Core(); - // Use a manual revoker on the output event, so we can immediately stop - // worrying about it on destruction. - _coreOutputEventToken = _core.ReceivedOutput({ this, &TermControl::_coreReceivedOutput }); - // These events might all be triggered by the connection, but that // should be drained and closed before we complete destruction. So these // are safe. @@ -104,37 +101,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation } }); - // Many of these ThrottledFunc's should be inside ControlCore. However, - // currently they depend on the Dispatcher() of the UI thread, which the - // Core eventually won't have access to. When we get to - // https://github.com/microsoft/terminal/projects/5#card-50760282 - // then we'll move the applicable ones. - // - // These four throttled functions are triggered by terminal output and interact with the UI. + // Get our dispatcher. This will get us the same dispatcher as + // TermControl::Dispatcher(). + auto dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + + // These three throttled functions are triggered by terminal output and interact with the UI. // Since Close() is the point after which we are removed from the UI, but before the // destructor has run, we MUST check control->_IsClosing() before actually doing anything. - _tsfTryRedrawCanvas = std::make_shared>( - Dispatcher(), - TsfRedrawInterval, - [weakThis = get_weak()]() { - if (auto control{ weakThis.get() }; !control->_IsClosing()) - { - control->TSFInputControl().TryRedrawCanvas(); - } - }); - - _updatePatternLocations = std::make_shared>( - Dispatcher(), - UpdatePatternLocationsInterval, - [weakThis = get_weak()]() { - if (auto control{ weakThis.get() }; !control->_IsClosing()) - { - control->_core.UpdatePatternLocations(); - } - }); - _playWarningBell = std::make_shared( - Dispatcher(), + dispatcher, TerminalWarningBellInterval, [weakThis = get_weak()]() { if (auto control{ weakThis.get() }; !control->_IsClosing()) @@ -144,7 +119,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation }); _updateScrollBar = std::make_shared>( - Dispatcher(), + dispatcher, ScrollBarUpdateInterval, [weakThis = get_weak()](const auto& update) { if (auto control{ weakThis.get() }; !control->_IsClosing()) @@ -540,7 +515,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { // create a custom automation peer with this code pattern: // (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers) - if (const auto& interactivityAutoPeer = _interactivity.OnCreateAutomationPeer()) + if (const auto& interactivityAutoPeer{ _interactivity.OnCreateAutomationPeer() }) { _automationPeer = winrt::make(this, interactivityAutoPeer); return _automationPeer; @@ -1276,23 +1251,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation CATCH_LOG(); } - void TermControl::_coreReceivedOutput(const IInspectable& /*sender*/, - const IInspectable& /*args*/) - { - // Queue up a throttled UpdatePatternLocations call. In the future, we - // should have the _updatePatternLocations ThrottledFunc internal to - // ControlCore, and run on that object's dispatcher queue. - // - // We're not doing that quite yet, because the Core will eventually - // be out-of-proc from the UI thread, and won't be able to just use - // the UI thread as the dispatcher queue thread. - // - // THIS IS CALLED ON EVERY STRING OF TEXT OUTPUT TO THE TERMINAL. Think - // twice before adding anything here. - - _updatePatternLocations->Run(); - } - // Method Description: // - Reset the font size of the terminal to its default size. // Arguments: @@ -1330,8 +1288,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _updateScrollBar->ModifyPending([](auto& update) { update.newValue.reset(); }); - - _updatePatternLocations->Run(); } // Method Description: @@ -1665,7 +1621,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation update.newValue = args.ViewTop(); _updateScrollBar->Run(update); - _updatePatternLocations->Run(); } // Method Description: @@ -1673,10 +1628,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation // to be where the current cursor position is. // Arguments: // - N/A - void TermControl::_CursorPositionChanged(const IInspectable& /*sender*/, - const IInspectable& /*args*/) + winrt::fire_and_forget TermControl::_CursorPositionChanged(const IInspectable& /*sender*/, + const IInspectable& /*args*/) { - _tsfTryRedrawCanvas->Run(); + // Prior to GH#10187, this fired a trailing throttled func to update the + // TSF canvas only every 100ms. Now, the throttling occurs on the + // ControlCore side. If we're told to update the cursor position, we can + // just go ahead and do it. + // This can come in off the COM thread - hop back to the UI thread. + auto weakThis{ get_weak() }; + co_await resume_foreground(Dispatcher()); + if (auto control{ weakThis.get() }; !control->_IsClosing()) + { + control->TSFInputControl().TryRedrawCanvas(); + } } hstring TermControl::Title() @@ -1730,8 +1695,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _closing = true; - _core.ReceivedOutput(_coreOutputEventToken); _RestorePointerCursorHandlers(*this, nullptr); + // Disconnect the TSF input control so it doesn't receive EditContext events. TSFInputControl().Close(); _autoScrollTimer.Stop(); diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 6c9aa6757..e16ecbaae 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -153,8 +153,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool _focused{ false }; bool _initializedTerminal{ false }; - std::shared_ptr> _tsfTryRedrawCanvas; - std::shared_ptr> _updatePatternLocations; std::shared_ptr _playWarningBell; struct ScrollBarUpdate @@ -164,7 +162,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation double newMinimum; double newViewportSize; }; + std::shared_ptr> _updateScrollBar; + bool _isInternalScrollBarUpdate; // Auto scroll occurs when user, while selecting, drags cursor outside @@ -181,8 +181,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::optional _cursorTimer; std::optional _blinkTimer; - event_token _coreOutputEventToken; - winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker; inline bool _IsClosing() const noexcept @@ -233,7 +231,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _TerminalTabColorChanged(const std::optional color); void _ScrollPositionChanged(const IInspectable& sender, const Control::ScrollPositionChangedArgs& args); - void _CursorPositionChanged(const IInspectable& sender, const IInspectable& args); + winrt::fire_and_forget _CursorPositionChanged(const IInspectable& sender, const IInspectable& args); bool _CapturePointer(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); bool _ReleasePointerCapture(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); @@ -265,7 +263,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation const int fontHeight, const bool isInitialChange); winrt::fire_and_forget _coreTransparencyChanged(IInspectable sender, Control::TransparencyChangedEventArgs args); - void _coreReceivedOutput(const IInspectable& sender, const IInspectable& args); void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); void _coreWarningBell(const IInspectable& sender, const IInspectable& args); }; diff --git a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp index 96617b513..e57dd8c23 100644 --- a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp @@ -56,6 +56,16 @@ namespace ControlUnitTests return { settings, conn }; } + + winrt::com_ptr createCore(Control::IControlSettings settings, + TerminalConnection::ITerminalConnection conn) + { + Log::Comment(L"Create ControlCore object"); + + auto core = winrt::make_self(settings, conn); + core->_inUnitTests = true; + return core; + } }; void ControlCoreTests::ComPtrSettings() @@ -71,8 +81,7 @@ namespace ControlUnitTests { auto [settings, conn] = _createSettingsAndConnection(); - Log::Comment(L"Create ControlCore object"); - auto core = winrt::make_self(*settings, *conn); + auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); } @@ -80,8 +89,7 @@ namespace ControlUnitTests { auto [settings, conn] = _createSettingsAndConnection(); - Log::Comment(L"Create ControlCore object"); - auto core = winrt::make_self(*settings, *conn); + auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); VERIFY_IS_FALSE(core->_initializedTerminal); @@ -99,8 +107,7 @@ namespace ControlUnitTests settings->UseAcrylic(true); settings->TintOpacity(0.5f); - Log::Comment(L"Create ControlCore object"); - auto core = winrt::make_self(*settings, *conn); + auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); // A callback to make sure that we're raising TransparencyChanged events @@ -167,8 +174,7 @@ namespace ControlUnitTests { auto [settings, conn] = _createSettingsAndConnection(); - Log::Comment(L"Create ControlCore object"); - auto core = winrt::make_self(*settings, *conn); + auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); Log::Comment(L"Close the Core, like a TermControl would"); @@ -190,8 +196,7 @@ namespace ControlUnitTests // that you don't default to Cascadia* settings->FontFace(L"Impact"); - Log::Comment(L"Create ControlCore object"); - auto core = winrt::make_self(*settings, *conn); + auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); VERIFY_ARE_EQUAL(L"Impact", std::wstring_view{ core->_actualFont.GetFaceName() }); diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp index 14801722e..a94ed8778 100644 --- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -73,6 +73,7 @@ namespace ControlUnitTests auto interactivity = winrt::make_self(settings, conn); VERIFY_IS_NOT_NULL(interactivity); auto core = interactivity->_core; + core->_inUnitTests = true; VERIFY_IS_NOT_NULL(core); return { core, interactivity }; @@ -163,6 +164,10 @@ namespace ControlUnitTests void ControlInteractivityTests::TestScrollWithMouse() { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD_PROPERTIES() + WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{}; auto [settings, conn] = _createSettingsAndConnection(); @@ -243,7 +248,7 @@ namespace ControlUnitTests buttonState); Log::Comment(NoThrowString().Format(L"internal scrollbar pos:%f", interactivity->_internalScrollbarPosition)); } - Log::Comment(L"Scrolling up more should do nothing"); + Log::Comment(L"Scrolling down more should do nothing"); expectedTop = 21; interactivity->MouseWheel(modifiers, -WHEEL_DELTA, @@ -257,6 +262,10 @@ namespace ControlUnitTests void ControlInteractivityTests::CreateSubsequentSelectionWithDragging() { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD_PROPERTIES() + // This is a test for GH#9725 WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{}; diff --git a/src/cascadia/UnitTests_Control/pch.h b/src/cascadia/UnitTests_Control/pch.h index 7568822f2..a013f7462 100644 --- a/src/cascadia/UnitTests_Control/pch.h +++ b/src/cascadia/UnitTests_Control/pch.h @@ -47,6 +47,8 @@ Licensed under the MIT license. // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" +#include "ThrottledFunc.h" + // Common includes for most tests: #include "../../inc/argb.h" #include "../../inc/conattrs.hpp" diff --git a/src/cascadia/WinRTUtils/inc/ThrottledFunc.h b/src/cascadia/WinRTUtils/inc/ThrottledFunc.h index d5ff0a0e4..40482faf1 100644 --- a/src/cascadia/WinRTUtils/inc/ThrottledFunc.h +++ b/src/cascadia/WinRTUtils/inc/ThrottledFunc.h @@ -24,7 +24,7 @@ public: // // After `func` was invoked the state is reset and this cycle is repeated again. ThrottledFunc( - winrt::Windows::UI::Core::CoreDispatcher dispatcher, + winrt::Windows::System::DispatcherQueue dispatcher, filetime_duration delay, function func) : _dispatcher{ std::move(dispatcher) }, @@ -81,7 +81,7 @@ private: { if constexpr (leading) { - _dispatcher.RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [weakSelf = this->weak_from_this()]() { + _dispatcher.TryEnqueue(winrt::Windows::System::DispatcherQueuePriority::Normal, [weakSelf = this->weak_from_this()]() { if (auto self{ weakSelf.lock() }) { try @@ -108,7 +108,7 @@ private: } else { - _dispatcher.RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [weakSelf = this->weak_from_this()]() { + _dispatcher.TryEnqueue(winrt::Windows::System::DispatcherQueuePriority::Normal, [weakSelf = this->weak_from_this()]() { if (auto self{ weakSelf.lock() }) { try @@ -129,7 +129,7 @@ private: } FILETIME _delay; - winrt::Windows::UI::Core::CoreDispatcher _dispatcher; + winrt::Windows::System::DispatcherQueue _dispatcher; function _func; wil::unique_threadpool_timer _timer; From fdffa24a711e66472c6f3a5f151c6859bc290c42 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Mon, 9 Aug 2021 10:29:04 -0700 Subject: [PATCH 51/90] Update SUI tooltips from 'checked' to 'enabled' (#10885) Updates the Settings UI tooltips to use "enabled" and "disabled" instead of "checked" and "unchecked" respectively. Closes #10814 --- .../Resources/en-US/Resources.resw | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 2dec26d0e..26eefd38b 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -232,7 +232,7 @@ Header for a control to toggle if the app should always show the tabs (similar to a website browser). - When unchecked, the tab bar will appear when a new tab is created. + When disabled, the tab bar will appear when a new tab is created. A description for what the "always show tabs" setting does. Presented near "Globals_AlwaysShowTabs.Header". @@ -272,7 +272,7 @@ Header for a control to toggle the "force full repaint" setting. When enabled, the app renders new content between screen frames. - When unchecked, the terminal will render only the updates to the screen between frames. + When disabled, the terminal will render only the updates to the screen between frames. A description for what the "force full repaint" setting does. Presented near "Globals_ForceFullRepaint.Header". @@ -340,7 +340,7 @@ Header for a control to toggle whether the title bar should be shown or not. Changing this setting requires the user to relaunch the app. - When unchecked, the title bar will appear above the tabs. + When disabled, the title bar will appear above the tabs. A description for what the "show titlebar" setting does. Presented near "Globals_ShowTitlebar.Header". @@ -348,7 +348,7 @@ Header for a control to toggle whether the terminal's title is shown as the application title, or not. - When unchecked, the title bar will be 'Windows Terminal'. + When disabled, the title bar will be 'Windows Terminal'. A description for what the "show title in titlebar" setting does. Presented near "Globals_ShowTitleInTitlebar.Header".{Locked="Windows"} @@ -356,7 +356,7 @@ Header for a control to toggle whether the terminal snaps the window to the character grid when resizing, or not. - When unchecked, the window will resize smoothly. + When disabled, the window will resize smoothly. A description for what the "snap to grid on resize" setting does. Presented near "Globals_SnapToGridOnResize.Header". @@ -364,7 +364,7 @@ Header for a control to toggle whether the terminal should use software to render content instead of the hardware. - When checked, the terminal will use the software renderer (a.k.a. WARP) instead of the hardware one. + When enabled, the terminal will use the software renderer (a.k.a. WARP) instead of the hardware one. A description for what the "software rendering" setting does. Presented near "Globals_SoftwareRendering.Header". @@ -372,7 +372,7 @@ Header for a control to toggle whether the app should launch when the user's machine starts up, or not. - When checked, this enables the launch of Windows Terminal at machine startup. + When enabled, this enables the launch of Windows Terminal at machine startup. A description for what the "start on user login" setting does. Presented near "Globals_StartOnUserLogin.Header". @@ -480,7 +480,7 @@ Header for a control to toggle whether the app treats ctrl+alt as the AltGr (also known as the Alt Graph) modifier key found on keyboards. {Locked="AltGr"} - By default Windows treats Ctrl+Alt as an alias for AltGr. When unchecked, this behavior will be disabled. + By default Windows treats Ctrl+Alt as an alias for AltGr. When disabled, this behavior will be disabled. A description for what the "AltGr aliasing" setting does. Presented near "Profile_AltGrAliasing.Header". @@ -700,7 +700,7 @@ Header for a control to toggle whether the profile is shown in a dropdown menu, or not. - If checked, the profile will not appear in the list of profiles. This can be used to hide default profiles and dynamically generated profiles, while leaving them in your settings file. + If enabled, the profile will not appear in the list of profiles. This can be used to hide default profiles and dynamically generated profiles, while leaving them in your settings file. A description for what the "hidden" setting does. Presented near "Profile_Hidden". @@ -736,7 +736,7 @@ Header for a control to toggle classic CRT display effects, which gives the terminal a retro look. - When checked, enables retro terminal effects such as glowing text and scan lines. + When enabled, enables retro terminal effects such as glowing text and scan lines. A description for what the "retro terminal effects" setting does. Presented near "Profile_RetroTerminalEffect". @@ -772,7 +772,7 @@ A supplementary setting to the "starting directory" setting. "Parent" refers to the parent process of the current process. - If checked, this profile will spawn in the directory from which Windows Terminal was launched. + If enabled, this profile will spawn in the directory from which Windows Terminal was launched. A description for what the supplementary "use parent process directory" setting does. Presented near "Profile_StartingDirectoryUseParentCheckbox". @@ -808,7 +808,7 @@ A supplementary setting to the "background image" setting. When enabled, the OS desktop wallpaper is used as the background image. Presented near "Profile_BackgroundImage". - When checked, use the desktop wallpaper image as the background image for the terminal. + When enabled, use the desktop wallpaper image as the background image for the terminal. A description for what the supplementary "use desktop image" setting does. Presented near "Profile_UseDesktopImage". @@ -1111,7 +1111,7 @@ A supplementary setting to the "font face" setting. Toggling this control updates the font face control to show all of the fonts installed. - If checked, show all installed fonts in the list above. Otherwise, only show the list of monospace fonts. + If enabled, show all installed fonts in the list above. Otherwise, only show the list of monospace fonts. A description for what the supplementary "show all fonts" setting does. Presented near "Profile_FontFaceShowAllFonts". From cd4aabda84d4cc33ed2f4401a37a154bf18953b7 Mon Sep 17 00:00:00 2001 From: Don-Vito Date: Mon, 9 Aug 2021 21:22:08 +0300 Subject: [PATCH 52/90] Prevent redraw upon resize if new size is equal to old (#10895) ## Summary of the Pull Request Do not invoke terminal resize logic if view port dimensions didn't change ## PR Checklist * [x] Closes #10857 * [x] CLA signed. * [ ] Tests added/passed * [ ] Documentation updated. * [ ] Schema updated. * [ ] I've discussed this with core contributors already. ## Detailed Description of the Pull Request / Additional comments Short-circuit `ControlCore::_doResizeUnderLock` if the dimensions of the required view port are equal to the dimensions of the current view port --- src/cascadia/TerminalControl/ControlCore.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 2cc3fef16..ac7bf1b07 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -804,6 +804,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } + // Convert our new dimensions to characters + const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, + { static_cast(size.cx), static_cast(size.cy) }); + const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels); + const auto currentVP = _terminal->GetViewport(); + + // Don't actually resize if viewport dimensions didn't change + if (vp.Height() == currentVP.Height() && vp.Width() == currentVP.Width()) + { + return; + } + _terminal->ClearSelection(); // Tell the dx engine that our window is now the new size. @@ -812,11 +824,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Invalidate everything _renderer->TriggerRedrawAll(); - // Convert our new dimensions to characters - const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, - { static_cast(size.cx), static_cast(size.cy) }); - const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels); - // If this function succeeds with S_FALSE, then the terminal didn't // actually change size. No need to notify the connection of this no-op. const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() }); From 7acec306a65d18320401bf05c63896849732d16e Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 9 Aug 2021 13:27:20 -0500 Subject: [PATCH 53/90] Account for the window frame when calculating initial position (#10902) ## Summary of the Pull Request Turns out, we'd only ever use the non-client size to calculate the size of the window, but not the actual position. As we learned in #10676, the nonclient area extends a few pixels past the visible borders of the window. ## PR Checklist * [x] Closes #10583 * [x] I work here * [ ] Tests added/passed * [n/a] Requires documentation to be updated ## Validation Steps Performed * [x] Works with the `IslandWindow` * [x] Works with the `NonClientIslandWindow` --- src/cascadia/WindowsTerminal/AppHost.cpp | 9 ++++-- src/cascadia/WindowsTerminal/IslandWindow.cpp | 29 ++++++++++++++--- src/cascadia/WindowsTerminal/IslandWindow.h | 1 + .../WindowsTerminal/NonClientIslandWindow.cpp | 31 +++++++++++++++---- .../WindowsTerminal/NonClientIslandWindow.h | 1 + 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 092fe8eae..bb1b0889e 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -410,6 +410,7 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, LaunchMode // Get the size of a window we'd need to host that client rect. This will // add the titlebar space. const til::size nonClientSize = _window->GetTotalNonClientExclusiveSize(dpix); + const til::rectangle nonClientFrame = _window->GetNonClientFrame(dpix); adjustedWidth = islandWidth + nonClientSize.width(); adjustedHeight = islandHeight + nonClientSize.height(); @@ -425,14 +426,18 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, LaunchMode const til::size desktopDimensions{ gsl::narrow(nearestMonitorInfo.rcWork.right - nearestMonitorInfo.rcWork.left), gsl::narrow(nearestMonitorInfo.rcWork.bottom - nearestMonitorInfo.rcWork.top) }; - til::point origin{ (proposedRect.left), + // GH#10583 - Adjust the position of the rectangle to account for the size + // of the invisible borders on the left/right. We DON'T want to adjust this + // for the top here - the IslandWindow includes the titlebar in + // nonClientFrame.top, so adjusting for that would actually place the + // titlebar _off_ the monitor. + til::point origin{ (proposedRect.left + nonClientFrame.left()), (proposedRect.top) }; if (_logic.IsQuakeWindow()) { // If we just use rcWork by itself, we'll fail to account for the invisible // space reserved for the resize handles. So retrieve that size here. - const til::size ncSize{ _window->GetTotalNonClientExclusiveSize(dpix) }; const til::size availableSpace = desktopDimensions + nonClientSize; origin = til::point{ diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 50fd2d156..5d8f823de 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -616,12 +616,20 @@ void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) } // Method Description: -// - Gets the difference between window and client area size. +// - Get the dimensions of our non-client area, as a rect where each component +// represents that side. +// - The .left will be a negative number, to represent that the actual side of +// the non-client area is outside the border of our window. It's roughly 8px ( +// * DPI scaling) to the left of the visible border. +// - The .right component will be positive, indicating that the nonclient border +// is in the positive-x direction from the edge of our client area. +// - This will also include our titlebar! It's in the nonclient area for us. // Arguments: -// - dpi: dpi of a monitor on which the window is placed -// Return Value -// - The size difference -SIZE IslandWindow::GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept +// - dpi: the scaling that we should use to calculate the border sizes. +// Return Value: +// - a RECT whose components represent the margins of the nonclient area, +// relative to the client area. +RECT IslandWindow::GetNonClientFrame(const UINT dpi) const noexcept { const auto windowStyle = static_cast(GetWindowLong(_window.get(), GWL_STYLE)); RECT islandFrame{}; @@ -630,7 +638,18 @@ SIZE IslandWindow::GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept // the error and go on. We'll use whatever the control proposed as the // size of our window, which will be at least close. LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi)); + return islandFrame; +} +// Method Description: +// - Gets the difference between window and client area size. +// Arguments: +// - dpi: dpi of a monitor on which the window is placed +// Return Value +// - The size difference +SIZE IslandWindow::GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept +{ + const auto islandFrame{ GetNonClientFrame(dpi) }; return { islandFrame.right - islandFrame.left, islandFrame.bottom - islandFrame.top diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 8e4a0ec4d..b0699864d 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -24,6 +24,7 @@ public: virtual void OnAppInitialized(); virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); virtual void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); + virtual RECT GetNonClientFrame(const UINT dpi) const noexcept; virtual SIZE GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept; virtual void Initialize(); diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index a1f5e6801..9dc8307ed 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -629,14 +629,21 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept return DefWindowProc(GetHandle(), WM_SETCURSOR, wParam, lParam); } - // Method Description: -// - Gets the difference between window and client area size. +// - Get the dimensions of our non-client area, as a rect where each component +// represents that side. +// - The .left will be a negative number, to represent that the actual side of +// the non-client area is outside the border of our window. It's roughly 8px ( +// * DPI scaling) to the left of the visible border. +// - The .right component will be positive, indicating that the nonclient border +// is in the positive-x direction from the edge of our client area. +// - This DOES NOT include our titlebar! It's in the client area for us. // Arguments: -// - dpi: dpi of a monitor on which the window is placed -// Return Value -// - The size difference -SIZE NonClientIslandWindow::GetTotalNonClientExclusiveSize(UINT dpi) const noexcept +// - dpi: the scaling that we should use to calculate the border sizes. +// Return Value: +// - a RECT whose components represent the margins of the nonclient area, +// relative to the client area. +RECT NonClientIslandWindow::GetNonClientFrame(UINT dpi) const noexcept { const auto windowStyle = static_cast(GetWindowLong(_window.get(), GWL_STYLE)); RECT islandFrame{}; @@ -647,6 +654,18 @@ SIZE NonClientIslandWindow::GetTotalNonClientExclusiveSize(UINT dpi) const noexc LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi)); islandFrame.top = -topBorderVisibleHeight; + return islandFrame; +} + +// Method Description: +// - Gets the difference between window and client area size. +// Arguments: +// - dpi: dpi of a monitor on which the window is placed +// Return Value +// - The size difference +SIZE NonClientIslandWindow::GetTotalNonClientExclusiveSize(UINT dpi) const noexcept +{ + const auto islandFrame{ GetNonClientFrame(dpi) }; // If we have a titlebar, this is being called after we've initialized, and // we can just ask that titlebar how big it wants to be. diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 10a39b4fc..02e59cc7a 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -37,6 +37,7 @@ public: [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; + virtual RECT GetNonClientFrame(UINT dpi) const noexcept override; virtual SIZE GetTotalNonClientExclusiveSize(UINT dpi) const noexcept override; void Initialize() override; From c55888f88d6b59ae88afb50a806df0dec8fb3a3b Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 9 Aug 2021 13:28:06 -0500 Subject: [PATCH 54/90] Make the TerminalApi exception handler less garrulous (#10901) ## Summary of the Pull Request Apparently the exception handler in TerminalApi is far too talkative. We're apparently throwing in `TerminalApi::CursorLineFeed` way too often, and that's caused an internal bug to be filed on us. This represents making the event less talkative, but doesn't actually fix the bug. It's just easier to get the OS bug cleared out quick this way. ## References * MSFT:33310649 ## PR Checklist * [x] Fixes the **A** portion of #10882, which closes MSFT:33310649 * [x] I work here * [n/a] Tests added/passed * [n/a] Requires documentation to be updated --- src/cascadia/TerminalCore/TerminalApi.cpp | 34 +++++++++++------------ src/inc/til.h | 11 +++++++- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 1fc36389d..83c898e47 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -16,7 +16,7 @@ try _WriteBuffer(stringView); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::ExecuteChar(wchar_t wch) noexcept try @@ -24,7 +24,7 @@ try _WriteBuffer({ &wch, 1 }); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() TextAttribute Terminal::GetTextAttributes() const noexcept { @@ -54,7 +54,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() COORD Terminal::GetCursorPosition() noexcept { @@ -75,7 +75,7 @@ try _buffer->GetCursor().SetColor(color); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Moves the cursor down one line, and possibly also to the leftmost column. @@ -101,7 +101,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - deletes count characters starting from the cursor's current position @@ -150,7 +150,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Inserts count spaces starting from the cursor's current position, moving over the existing text @@ -205,7 +205,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::EraseCharacters(const size_t numChars) noexcept try @@ -218,7 +218,7 @@ try _buffer->Write(eraseIter, absoluteCursorPos); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method description: // - erases a line of text, either from @@ -264,7 +264,7 @@ try _buffer->Write(eraseIter, startPos, false); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method description: // - erases text in the buffer in two ways depending on erase type @@ -348,7 +348,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::WarningBell() noexcept try @@ -356,7 +356,7 @@ try _pfnWarningBell(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::SetWindowTitle(std::wstring_view title) noexcept try @@ -368,7 +368,7 @@ try } return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Updates the value in the colortable at index tableIndex to the new color @@ -387,7 +387,7 @@ try _buffer->GetRenderTarget().TriggerRedrawAll(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Sets the cursor style to the given style. @@ -457,7 +457,7 @@ try _buffer->GetRenderTarget().TriggerRedrawAll(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Updates the default background color from a COLORREF, format 0x00BBGGRR. @@ -475,7 +475,7 @@ try _buffer->GetRenderTarget().TriggerRedrawAll(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() til::color Terminal::GetDefaultBackground() const noexcept { @@ -509,7 +509,7 @@ try _buffer->GetRenderTarget().TriggerRedrawAll(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::EnableVT200MouseMode(const bool enabled) noexcept { @@ -591,7 +591,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Updates the buffer's current text attributes to start a hyperlink diff --git a/src/inc/til.h b/src/inc/til.h index 509f02db9..3202a5959 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -89,7 +89,9 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } \ } while (0, 0) -// Due to a bug (DevDiv 441931), Warning 4297 (function marked noexcept throws exception) is detected even when the throwing code is unreachable, such as the end of scope after a return, in function-level catch. +// Due to a bug (DevDiv 441931), Warning 4297 (function marked noexcept throws +// exception) is detected even when the throwing code is unreachable, such as +// the end of scope after a return, in function-level catch. #define CATCH_LOG_RETURN_FALSE() \ catch (...) \ { \ @@ -98,6 +100,13 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return false; \ } +// This is like the above, but doesn't log any messages. This is for GH#10882. +#define CATCH_RETURN_FALSE() \ + catch (...) \ + { \ + return false; \ + } + // MultiByteToWideChar has a bug in it where it can return 0 and then not set last error. // WIL has a fit if the last error is 0 when a bool false is returned. // This macro doesn't have a fit. It just reports E_UNEXPECTED instead. From a14b6f89f686bda084fd45104806182b780d6586 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 10 Aug 2021 06:16:17 -0500 Subject: [PATCH 55/90] Combine progress states in the tab, taskbar (#10755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request ![background-progress-000](https://user-images.githubusercontent.com/18356694/126653006-3ad2fdae-67ae-4cdb-aa46-25d09217e365.gif) This PR causes the Terminal to combine taskbar states at the tab and window level, according to the [MSDN docs for `SetProgressState`](https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate#how-the-taskbar-button-chooses-the-progress-indicator-for-a-group). This allows the Terminal's taskbar icon to continue showing progress information, even if you're in a pane/tab that _doesn't_ have progress state. This is helpful for cases where the user may be running a build in one tab, and working on something else in another. ## References * [`SetProgressState`](https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate#how-the-taskbar-button-chooses-the-progress-indicator-for-a-group) * Progress mega: #6700 ## PR Checklist * [x] Closes #10090 * [x] I work here * [ ] Tests added/passed * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments This also fixes a related bug where transitioning from the "error" or "warning" state directly to the "indeterminate" state would cause the taskbar icon to get stuck in a bad state. ## Validation Steps Performed
progress.cmd ```cmd @echo off setlocal enabledelayedexpansion set _type=3 if (%1) == () ( set _type=3 ) else ( set _type=%1 ) if (%_type%) == (0) ( --- src/cascadia/TerminalApp/AppLogic.cpp | 21 +----- src/cascadia/TerminalApp/AppLogic.h | 3 +- src/cascadia/TerminalApp/AppLogic.idl | 3 +- src/cascadia/TerminalApp/Pane.cpp | 23 ++++++ src/cascadia/TerminalApp/Pane.h | 3 + src/cascadia/TerminalApp/TaskbarState.cpp | 45 +++++++++++ src/cascadia/TerminalApp/TaskbarState.h | 34 +++++++++ src/cascadia/TerminalApp/TaskbarState.idl | 15 ++++ .../TerminalApp/TerminalAppLib.vcxproj | 7 ++ src/cascadia/TerminalApp/TerminalPage.cpp | 42 ++++++----- src/cascadia/TerminalApp/TerminalPage.h | 3 +- src/cascadia/TerminalApp/TerminalPage.idl | 4 +- src/cascadia/TerminalApp/TerminalTab.cpp | 75 ++++++++++++------- src/cascadia/TerminalApp/TerminalTab.h | 1 + src/cascadia/WindowsTerminal/AppHost.cpp | 9 ++- src/cascadia/WindowsTerminal/IslandWindow.cpp | 7 +- 16 files changed, 218 insertions(+), 77 deletions(-) create mode 100644 src/cascadia/TerminalApp/TaskbarState.cpp create mode 100644 src/cascadia/TerminalApp/TaskbarState.h create mode 100644 src/cascadia/TerminalApp/TaskbarState.idl diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index c14a951de..420630774 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -1129,28 +1129,11 @@ namespace winrt::TerminalApp::implementation } } - // Method Description: - // - Gets the taskbar state value from the last active control - // Return Value: - // - The taskbar state of the last active control - uint64_t AppLogic::GetLastActiveControlTaskbarState() + winrt::TerminalApp::TaskbarState AppLogic::TaskbarState() { if (_root) { - return _root->GetLastActiveControlTaskbarState(); - } - return {}; - } - - // Method Description: - // - Gets the taskbar progress value from the last active control - // Return Value: - // - The taskbar progress of the last active control - uint64_t AppLogic::GetLastActiveControlTaskbarProgress() - { - if (_root) - { - return _root->GetLastActiveControlTaskbarProgress(); + return _root->TaskbarState(); } return {}; } diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 6d2e36d9e..4131d28f1 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -90,8 +90,7 @@ namespace winrt::TerminalApp::implementation void WindowCloseButtonClicked(); - uint64_t GetLastActiveControlTaskbarState(); - uint64_t GetLastActiveControlTaskbarProgress(); + winrt::TerminalApp::TaskbarState TaskbarState(); winrt::Windows::Foundation::IAsyncOperation ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index bb334d26f..0b8891038 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -68,8 +68,7 @@ namespace TerminalApp void TitlebarClicked(); void WindowCloseButtonClicked(); - UInt64 GetLastActiveControlTaskbarState(); - UInt64 GetLastActiveControlTaskbarProgress(); + TaskbarState TaskbarState{ get; }; FindTargetWindowResult FindTargetWindow(String[] args); diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 59dacf6b9..45db24870 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -2548,6 +2548,29 @@ bool Pane::ContainsReadOnly() const return _IsLeaf() ? _control.ReadOnly() : (_firstChild->ContainsReadOnly() || _secondChild->ContainsReadOnly()); } +// Method Description: +// - If we're a parent, place the taskbar state for all our leaves into the +// provided vector. +// - If we're a leaf, place our own state into the vector. +// Arguments: +// - states: a vector that will receive all the states of all leaves in the tree +// Return Value: +// - +void Pane::CollectTaskbarStates(std::vector& states) +{ + if (_IsLeaf()) + { + auto tbState{ winrt::make(_control.TaskbarState(), + _control.TaskbarProgress()) }; + states.push_back(tbState); + } + else + { + _firstChild->CollectTaskbarStates(states); + _secondChild->CollectTaskbarStates(states); + } +} + DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate>); DEFINE_EVENT(Pane, LostFocus, _LostFocusHandlers, winrt::delegate>); DEFINE_EVENT(Pane, PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index eed685d32..34d696340 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -21,6 +21,7 @@ #pragma once #include "../../cascadia/inc/cppwinrt_utils.h" +#include "TaskbarState.h" // fwdecl unittest classes namespace TerminalAppLocalTests @@ -92,6 +93,8 @@ public: bool ContainsReadOnly() const; + void CollectTaskbarStates(std::vector& states); + WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler); DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate>); DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate>); diff --git a/src/cascadia/TerminalApp/TaskbarState.cpp b/src/cascadia/TerminalApp/TaskbarState.cpp new file mode 100644 index 000000000..183460556 --- /dev/null +++ b/src/cascadia/TerminalApp/TaskbarState.cpp @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TaskbarState.h" +#include "TaskbarState.g.cpp" + +namespace winrt::TerminalApp::implementation +{ + // Default to unset, 0%. + TaskbarState::TaskbarState() : + TaskbarState(0, 0){}; + + TaskbarState::TaskbarState(const uint64_t dispatchTypesState, const uint64_t progressParam) : + _State{ dispatchTypesState }, + _Progress{ progressParam } {} + + uint64_t TaskbarState::Priority() const + { + // This seemingly nonsensical ordering is from + // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate#how-the-taskbar-button-chooses-the-progress-indicator-for-a-group + switch (_State) + { + case 0: // Clear = 0, + return 5; + case 1: // Set = 1, + return 3; + case 2: // Error = 2, + return 1; + case 3: // Indeterminate = 3, + return 4; + case 4: // Paused = 4 + return 2; + } + // Here, return 6, to definitely be greater than all the other valid values. + // This should never really happen. + return 6; + } + + int TaskbarState::ComparePriority(const winrt::TerminalApp::TaskbarState& lhs, const winrt::TerminalApp::TaskbarState& rhs) + { + return lhs.Priority() < rhs.Priority(); + } + +} diff --git a/src/cascadia/TerminalApp/TaskbarState.h b/src/cascadia/TerminalApp/TaskbarState.h new file mode 100644 index 000000000..e36b4440c --- /dev/null +++ b/src/cascadia/TerminalApp/TaskbarState.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once +#include "inc/cppwinrt_utils.h" +#include "TaskbarState.g.h" + +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class TabTests; +}; + +namespace winrt::TerminalApp::implementation +{ + struct TaskbarState : TaskbarStateT + { + public: + TaskbarState(); + TaskbarState(const uint64_t dispatchTypesState, const uint64_t progress); + + static int ComparePriority(const winrt::TerminalApp::TaskbarState& lhs, const winrt::TerminalApp::TaskbarState& rhs); + + uint64_t Priority() const; + + WINRT_PROPERTY(uint64_t, State, 0); + WINRT_PROPERTY(uint64_t, Progress, 0); + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(TaskbarState); +} diff --git a/src/cascadia/TerminalApp/TaskbarState.idl b/src/cascadia/TerminalApp/TaskbarState.idl new file mode 100644 index 000000000..159242a17 --- /dev/null +++ b/src/cascadia/TerminalApp/TaskbarState.idl @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace TerminalApp +{ + [default_interface] runtimeclass TaskbarState + { + TaskbarState(); + TaskbarState(UInt64 dispatchTypesState, UInt64 progress); + + UInt64 State{ get; }; + UInt64 Progress{ get; }; + UInt64 Priority { get; }; + } +} diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index 25cd7cfa3..4d9b5d3dc 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -90,6 +90,9 @@ TabBase.idl + + TaskbarState.idl + TerminalTab.idl @@ -166,6 +169,9 @@ TabBase.idl + + TaskbarState.idl + TerminalTab.idl @@ -258,6 +264,7 @@ + TerminalPage.xaml diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 3bbf27eab..4d49779ad 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -2077,29 +2077,35 @@ namespace winrt::TerminalApp::implementation } // Method Description: - // - Gets the taskbar state value from the last active control + // - Get the combined taskbar state for the page. This is the combination of + // all the states of all the tabs, which are themselves a combination of + // all their panes. Taskbar states are given a priority based on the rules + // in: + // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate + // under "How the Taskbar Button Chooses the Progress Indicator for a Group" + // Arguments: + // - // Return Value: - // - The taskbar state of the last active control - uint64_t TerminalPage::GetLastActiveControlTaskbarState() + // - A TaskbarState object representing the combined taskbar state and + // progress percentage of all our tabs. + winrt::TerminalApp::TaskbarState TerminalPage::TaskbarState() const { - if (auto control{ _GetActiveControl() }) - { - return control.TaskbarState(); - } - return {}; - } + auto state{ winrt::make() }; - // Method Description: - // - Gets the taskbar progress value from the last active control - // Return Value: - // - The taskbar progress of the last active control - uint64_t TerminalPage::GetLastActiveControlTaskbarProgress() - { - if (auto control{ _GetActiveControl() }) + for (const auto& tab : _tabs) { - return control.TaskbarProgress(); + if (auto tabImpl{ _GetTerminalTabImpl(tab) }) + { + auto tabState{ tabImpl->GetCombinedTaskbarState() }; + // lowest priority wins + if (tabState.Priority() < state.Priority()) + { + state = tabState; + } + } } - return {}; + + return state; } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index bb1a6f13e..6ec7bd7a0 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -85,8 +85,7 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::IDialogPresenter DialogPresenter() const; void DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter); - uint64_t GetLastActiveControlTaskbarState(); - uint64_t GetLastActiveControlTaskbarProgress(); + winrt::TerminalApp::TaskbarState TaskbarState() const; void ShowKeyboardServiceWarning(); winrt::hstring KeyboardServiceDisabledText(); diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 0d0557822..7d4c580e9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import "TaskbarState.idl"; namespace TerminalApp { @@ -42,8 +43,7 @@ namespace TerminalApp void ShowKeyboardServiceWarning(); String KeyboardServiceDisabledText { get; }; - UInt64 GetLastActiveControlTaskbarState(); - UInt64 GetLastActiveControlTaskbarProgress(); + TaskbarState TaskbarState{ get; }; event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler LastTabClosed; diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index ad4595a1b..48cf05d33 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -177,10 +177,9 @@ namespace winrt::TerminalApp::implementation { lastFocusedControl.Focus(_focusState); - // Update our own progress state, and fire an event signaling + // Update our own progress state. This will fire an event signaling // that our taskbar progress changed. _UpdateProgressState(); - _TaskbarProgressChangedHandlers(lastFocusedControl, nullptr); } // When we gain focus, remove the bell indicator if it is active if (_tabStatus.BellIndicator()) @@ -675,6 +674,26 @@ namespace winrt::TerminalApp::implementation }); } + // Method Description: + // - Get the combined taskbar state for the tab. This is the combination of + // all the states of all our panes. Taskbar states are given a priority + // based on the rules in: + // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate + // under "How the Taskbar Button Chooses the Progress Indicator for a + // Group" + // Arguments: + // - + // Return Value: + // - A TaskbarState object representing the combined taskbar state and + // progress percentage of all our panes. + winrt::TerminalApp::TaskbarState TerminalTab::GetCombinedTaskbarState() const + { + std::vector states; + _rootPane->CollectTaskbarStates(states); + return states.empty() ? winrt::make() : + *std::min_element(states.begin(), states.end(), TerminalApp::implementation::TaskbarState::ComparePriority); + } + // Method Description: // - This should be called on the UI thread. If you don't, then it might // silently do nothing. @@ -690,37 +709,39 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::_UpdateProgressState() { - if (const auto& activeControl{ GetActiveTerminalControl() }) - { - const auto taskbarState = activeControl.TaskbarState(); - // The progress of the control changed, but not necessarily the progress of the tab. - // Set the tab's progress ring to the active pane's progress - if (taskbarState > 0) - { - if (taskbarState == 3) - { - // 3 is the indeterminate state, set the progress ring as such - _tabStatus.IsProgressRingIndeterminate(true); - } - else - { - // any non-indeterminate state has a value, set the progress ring as such - _tabStatus.IsProgressRingIndeterminate(false); + const auto state{ GetCombinedTaskbarState() }; - const auto progressValue = gsl::narrow(activeControl.TaskbarProgress()); - _tabStatus.ProgressValue(progressValue); - } - // Hide the tab icon (the progress ring is placed over it) - HideIcon(true); - _tabStatus.IsProgressRingActive(true); + const auto taskbarState = state.State(); + // The progress of the control changed, but not necessarily the progress of the tab. + // Set the tab's progress ring to the active pane's progress + if (taskbarState > 0) + { + if (taskbarState == 3) + { + // 3 is the indeterminate state, set the progress ring as such + _tabStatus.IsProgressRingIndeterminate(true); } else { - // Show the tab icon - HideIcon(false); - _tabStatus.IsProgressRingActive(false); + // any non-indeterminate state has a value, set the progress ring as such + _tabStatus.IsProgressRingIndeterminate(false); + + const auto progressValue = gsl::narrow(state.Progress()); + _tabStatus.ProgressValue(progressValue); } + // Hide the tab icon (the progress ring is placed over it) + HideIcon(true); + _tabStatus.IsProgressRingActive(true); } + else + { + // Show the tab icon + HideIcon(false); + _tabStatus.IsProgressRingActive(false); + } + + // fire an event signaling that our taskbar progress changed. + _TaskbarProgressChangedHandlers(nullptr, nullptr); } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index f3cee7555..6a9c7808c 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -83,6 +83,7 @@ namespace winrt::TerminalApp::implementation void TogglePaneReadOnly(); std::shared_ptr GetActivePane() const; + winrt::TerminalApp::TaskbarState GetCombinedTaskbarState() const; winrt::TerminalApp::TerminalTabStatus TabStatus() { diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index bb1b0889e..5f821d190 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -126,13 +126,14 @@ bool AppHost::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, cons // Arguments: // - sender: not used // - args: not used -void AppHost::SetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::Foundation::IInspectable& /*args*/) +void AppHost::SetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*args*/) { if (_logic) { - const auto state = gsl::narrow_cast(_logic.GetLastActiveControlTaskbarState()); - const auto progress = gsl::narrow_cast(_logic.GetLastActiveControlTaskbarProgress()); - _window->SetTaskbarProgress(state, progress); + const auto state = _logic.TaskbarState(); + _window->SetTaskbarProgress(gsl::narrow_cast(state.State()), + gsl::narrow_cast(state.Progress())); } } diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 5d8f823de..5cfffa4fc 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -776,7 +776,12 @@ void IslandWindow::SetTaskbarProgress(const size_t state, const size_t progress) _taskbar->SetProgressValue(_window.get(), progress, 100); break; case 3: - // sets the progress indicator to an indeterminate state + // sets the progress indicator to an indeterminate state. + // FIRST, set the progress to "no progress". That'll clear out any + // progress value from the previous state. Otherwise, a transition + // from (error,x%) or (warning,x%) to indeterminate will leave the + // progress value unchanged, and not show the spinner. + _taskbar->SetProgressState(_window.get(), TBPF_NOPROGRESS); _taskbar->SetProgressState(_window.get(), TBPF_INDETERMINATE); break; case 4: From ebf41dd6b2e3d968262b4c53bc506c107c82bf0c Mon Sep 17 00:00:00 2001 From: Floris Westerman Date: Tue, 10 Aug 2021 21:53:07 +0200 Subject: [PATCH 56/90] Adding/fixing Alt+Space handling (#10799) ## Summary of the Pull Request This PR implements/solves #7125. Concretely: two requests regarding alt+space were posted there: 1. Disabling the alt+space menu when the keychord explicitly unbound - and forwarding the keystroke to the terminal 2. Disabling the alt+space menu when the keychord is bound to an action ## References Not that I know ## PR Checklist * [x] Closes #7125 * [x] CLA signed. * [x] Tests added/passed * [x] Documentation updated. N/A * [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. The issue was marked Help-Wanted. I am happy to change the implementation to better fit your (planned) architecture. ## Detailed Description of the Pull Request / Additional comments While researching the solution, I noticed that the XAML system was always opening the system menu after Alt+Space, even when explicitly setting the event to be handled according to the documentation. The only solution I could find was to hook into the "XAML bypass" already in place for F7 KeyDown, and Alt KeyUp keystrokes. This bypass sends the keystroke to the AppHost immediately. This bypass method will "fall back" to the normal XAML routing when the keystroke is not handled. The implemented behaviour is as follows: - Default: same as normal; system menu is working since the bypass does not handle the keystroke - Alt+Space explicitly unbound: bypass passes the keystroke to the terminal and marks it as handled - Alt+Space bound to command: bypass invokes the command and marks it as handled Concretely, added a method to the KeyBindings and ActionMap interfaces to check whether a keychord is explicitly unbound. The implementation for `_GetActionByKeyChordInternal` already distinguishes between explicitly unbound and lack of binding, however this distinction is not carried over to the public methods. I decided not to change this existing method, to avoid breaking other stuff and to make the API more explicit. Furthermore, there were some checks against Alt+Space further down in the code, preventing this keystroke from being entered in the terminal. Since the check for this keystroke is now done at a "higher" level, I thought I could safely remove these checks as otherwise the keystroke could never be sent to the terminal itself. Please correct me if I'm wrong. Note that when alt+space is bound to an action that opens the command pallette (such as tab search), then a second press of the key combination does still open the system menu. This is because at that point, the "bypass" is cancelled (called "not a good implementation" in #4031). I don't think this can easily be solved for now, but this is a very minor bug/inconvenience. ## Validation Steps Performed Added tests for the new method. Performed manual checking: * [x] Default configuration still opens system menu like normal * [x] Binding alt+space to an action performs the action and does not show the system menu * [x] Explicitly unbinding alt+space no longer shows the system menu and sends the keystroke to the terminal. I was unable to run the debug tap (it crashed my instance - same thing happening on preview and release builds) to check for sure, but behaviour was identical to native linux terminals. --- .../KeyBindingsTests.cpp | 27 ++++++++++++++- src/cascadia/TerminalApp/AppKeyBindings.cpp | 5 +++ src/cascadia/TerminalApp/AppKeyBindings.h | 1 + src/cascadia/TerminalControl/IKeyBindings.idl | 1 + src/cascadia/TerminalControl/TermControl.cpp | 33 ++++++++++++------- src/cascadia/TerminalCore/Terminal.cpp | 15 --------- .../TerminalSettingsModel/ActionMap.cpp | 26 +++++++++++++++ .../TerminalSettingsModel/ActionMap.h | 1 + .../TerminalSettingsModel/ActionMap.idl | 2 ++ .../UnitTests_TerminalCore/InputTest.cpp | 11 ------- src/cascadia/WindowsTerminal/main.cpp | 16 +++++++++ 11 files changed, 100 insertions(+), 38 deletions(-) diff --git a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp index a5479f6a7..e5c354d25 100644 --- a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp @@ -40,7 +40,7 @@ namespace SettingsModelLocalTests TEST_METHOD(ManyKeysSameAction); TEST_METHOD(LayerKeybindings); TEST_METHOD(UnbindKeybindings); - + TEST_METHOD(TestExplicitUnbind); TEST_METHOD(TestArbitraryArgs); TEST_METHOD(TestSplitPaneArgs); @@ -232,6 +232,31 @@ namespace SettingsModelLocalTests VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); } + void KeyBindingsTests::TestExplicitUnbind() + { + const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" }; + const std::string bindings1String{ R"([ { "command": "unbound", "keys": ["ctrl+c"] } ])" }; + const std::string bindings2String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" }; + + const auto bindings0Json = VerifyParseSucceeded(bindings0String); + const auto bindings1Json = VerifyParseSucceeded(bindings1String); + const auto bindings2Json = VerifyParseSucceeded(bindings2String); + + const KeyChord keyChord{ VirtualKeyModifiers::Control, static_cast('C'), 0 }; + + auto actionMap = winrt::make_self(); + VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord)); + + actionMap->LayerJson(bindings0Json); + VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord)); + + actionMap->LayerJson(bindings1Json); + VERIFY_IS_TRUE(actionMap->IsKeyChordExplicitlyUnbound(keyChord)); + + actionMap->LayerJson(bindings2Json); + VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord)); + } + void KeyBindingsTests::TestArbitraryArgs() { const std::string bindings0String{ R"([ diff --git a/src/cascadia/TerminalApp/AppKeyBindings.cpp b/src/cascadia/TerminalApp/AppKeyBindings.cpp index 71a3a03bb..3f921bc55 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindings.cpp @@ -21,6 +21,11 @@ namespace winrt::TerminalApp::implementation return false; } + bool AppKeyBindings::IsKeyChordExplicitlyUnbound(const KeyChord& kc) + { + return _actionMap.IsKeyChordExplicitlyUnbound(kc); + } + void AppKeyBindings::SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch) { _dispatch = dispatch; diff --git a/src/cascadia/TerminalApp/AppKeyBindings.h b/src/cascadia/TerminalApp/AppKeyBindings.h index 5004edeca..f4f6c20ba 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.h +++ b/src/cascadia/TerminalApp/AppKeyBindings.h @@ -20,6 +20,7 @@ namespace winrt::TerminalApp::implementation AppKeyBindings() = default; bool TryKeyChord(winrt::Microsoft::Terminal::Control::KeyChord const& kc); + bool IsKeyChordExplicitlyUnbound(winrt::Microsoft::Terminal::Control::KeyChord const& kc); void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch); void SetActionMap(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap); diff --git a/src/cascadia/TerminalControl/IKeyBindings.idl b/src/cascadia/TerminalControl/IKeyBindings.idl index 2b3901559..83d665faa 100644 --- a/src/cascadia/TerminalControl/IKeyBindings.idl +++ b/src/cascadia/TerminalControl/IKeyBindings.idl @@ -9,5 +9,6 @@ namespace Microsoft.Terminal.Control interface IKeyBindings { Boolean TryKeyChord(KeyChord kc); + Boolean IsKeyChordExplicitlyUnbound(KeyChord kc); } } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 0d20606d1..238d75c90 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -761,28 +761,39 @@ namespace winrt::Microsoft::Terminal::Control::implementation (void)_TrySendKeyEvent(VK_MENU, scanCode, modifiers, false); handled = true; } - else if (vkey == VK_F7 && down) + else if ((vkey == VK_F7 || vkey == VK_SPACE) && down) { // Manually generate an F7 event into the key bindings or terminal. // This is required as part of GH#638. + // Or do so for alt+space; only send to terminal when explicitly unbound + // That is part of #GH7125 auto bindings{ _settings.KeyBindings() }; + bool isUnbound = false; + const KeyChord kc = { + modifiers.IsCtrlPressed(), + modifiers.IsAltPressed(), + modifiers.IsShiftPressed(), + modifiers.IsWinPressed(), + gsl::narrow_cast(vkey), + 0 + }; if (bindings) { - handled = bindings.TryKeyChord({ - modifiers.IsCtrlPressed(), - modifiers.IsAltPressed(), - modifiers.IsShiftPressed(), - modifiers.IsWinPressed(), - VK_F7, - 0, - }); + handled = bindings.TryKeyChord(kc); + + if (!handled) + { + isUnbound = bindings.IsKeyChordExplicitlyUnbound(kc); + } } - if (!handled) + const bool sendToTerminal = vkey == VK_F7 || (vkey == VK_SPACE && isUnbound); + + if (!handled && sendToTerminal) { // _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason. - (void)_TrySendKeyEvent(VK_F7, scanCode, modifiers, true); + (void)_TrySendKeyEvent(gsl::narrow_cast(vkey), scanCode, modifiers, true); // GH#6438: Note that we're _not_ sending the key up here - that'll // get passed through XAML to our KeyUp handler normally. handled = true; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 68e8424ed..53ca70878 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -594,14 +594,6 @@ bool Terminal::SendKeyEvent(const WORD vkey, const auto isAltOnlyPressed = states.IsAltPressed() && !states.IsCtrlPressed(); - // DON'T manually handle Alt+Space - the system will use this to bring up - // the system menu for restore, min/maximize, size, move, close. - // (This doesn't apply to Ctrl+Alt+Space.) - if (isAltOnlyPressed && vkey == VK_SPACE) - { - return false; - } - // By default Windows treats Ctrl+Alt as an alias for AltGr. // When the altGrAliasing setting is set to false, this behaviour should be disabled. // @@ -672,13 +664,6 @@ bool Terminal::SendMouseEvent(const COORD viewportPos, const unsigned int uiButt // - false otherwise. bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) { - // DON'T manually handle Alt+Space - the system will use this to bring up - // the system menu for restore, min/maximize, size, move, close. - if (ch == L' ' && states.IsAltPressed() && !states.IsCtrlPressed()) - { - return false; - } - auto vkey = _TakeVirtualKeyFromLastKeyEvent(scanCode); if (vkey == 0 && scanCode != 0) { diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index b0740f8d3..75a1213ad 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -677,6 +677,32 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } } + // Method Description: + // - Determines whether the given key chord is explicitly unbound + // Arguments: + // - keys: the key chord to check + // Return value: + // - true if the keychord is explicitly unbound + // - false if either the keychord is bound, or not bound at all + bool ActionMap::IsKeyChordExplicitlyUnbound(Control::KeyChord const& keys) const + { + const auto modifiers = keys.Modifiers(); + + // The "keys" given to us can contain both a Vkey, as well as a ScanCode. + // For instance our UI code fills out a KeyChord with all available information. + // But our _KeyMap only contains KeyChords that contain _either_ a Vkey or ScanCode. + // Due to this we'll have to call _GetActionByKeyChordInternal twice. + if (auto vkey = keys.Vkey()) + { + // We use the fact that the ..Internal call returns nullptr for explicitly unbound + // key chords, and nullopt for keychord that are not bound - it allows us to distinguish + // between unbound and lack of binding. + return nullptr == _GetActionByKeyChordInternal({ modifiers, vkey, 0 }); + } + + return nullptr == _GetActionByKeyChordInternal({ modifiers, 0, keys.ScanCode() }); + } + // Method Description: // - Retrieves the assigned command that can be invoked with the given key chord // Arguments: diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index c81bd14c3..a79c2c6e8 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -61,6 +61,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // queries Model::Command GetActionByKeyChord(Control::KeyChord const& keys) const; + bool IsKeyChordExplicitlyUnbound(Control::KeyChord const& keys) const; Control::KeyChord GetKeyBindingForAction(ShortcutAction const& action) const; Control::KeyChord GetKeyBindingForAction(ShortcutAction const& action, IActionArgs const& actionArgs) const; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.idl b/src/cascadia/TerminalSettingsModel/ActionMap.idl index 0a9c4d07c..806baa17a 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.idl +++ b/src/cascadia/TerminalSettingsModel/ActionMap.idl @@ -8,6 +8,8 @@ namespace Microsoft.Terminal.Settings.Model // This interface ensures that no changes are made to ActionMap interface IActionMapView { + Boolean IsKeyChordExplicitlyUnbound(Microsoft.Terminal.Control.KeyChord keys); + Command GetActionByKeyChord(Microsoft.Terminal.Control.KeyChord keys); Microsoft.Terminal.Control.KeyChord GetKeyBindingForAction(ShortcutAction action); diff --git a/src/cascadia/UnitTests_TerminalCore/InputTest.cpp b/src/cascadia/UnitTests_TerminalCore/InputTest.cpp index 131e410d0..34dd9118b 100644 --- a/src/cascadia/UnitTests_TerminalCore/InputTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/InputTest.cpp @@ -29,7 +29,6 @@ namespace TerminalCoreUnitTests }; TEST_METHOD(AltShiftKey); - TEST_METHOD(AltSpace); TEST_METHOD(InvalidKeyEvent); void _VerifyExpectedInput(std::wstring& actualInput) @@ -57,16 +56,6 @@ namespace TerminalCoreUnitTests VERIFY_IS_TRUE(term.SendCharEvent(L'A', 0, ControlKeyStates::LeftAltPressed | ControlKeyStates::ShiftPressed)); } - void InputTest::AltSpace() - { - // Make sure we don't handle Alt+Space. The system will use this to - // bring up the system menu for restore, min/maximize, size, move, - // close - VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed, true)); - VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed, false)); - VERIFY_IS_FALSE(term.SendCharEvent(L' ', 0, ControlKeyStates::LeftAltPressed)); - } - void InputTest::InvalidKeyEvent() { // Certain applications like AutoHotKey and its keyboard remapping feature, diff --git a/src/cascadia/WindowsTerminal/main.cpp b/src/cascadia/WindowsTerminal/main.cpp index 0bf491de7..7fb2523a5 100644 --- a/src/cascadia/WindowsTerminal/main.cpp +++ b/src/cascadia/WindowsTerminal/main.cpp @@ -82,6 +82,10 @@ static bool _messageIsAltKeyup(const MSG& message) { return (message.message == WM_KEYUP || message.message == WM_SYSKEYUP) && message.wParam == VK_MENU; } +static bool _messageIsAltSpaceKeypress(const MSG& message) +{ + return message.message == WM_SYSKEYDOWN && message.wParam == VK_SPACE; +} int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) { @@ -171,6 +175,18 @@ int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) } } + // GH#7125 = System XAML will show a system dialog on Alt Space. We might want to + // explicitly prevent that - for example when an action is bound to it. So similar to + // above, we steal the event and hand it off to the host. When the host does not process + // it, we will still dispatch like normal. + if (_messageIsAltSpaceKeypress(message)) + { + if (host.OnDirectKeyEvent(VK_SPACE, LOBYTE(HIWORD(message.lParam)), true)) + { + continue; + } + } + TranslateMessage(&message); DispatchMessage(&message); } From 121fb739fd50966af3cb17e52bdf756d82bd384a Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 11 Aug 2021 10:13:38 -0500 Subject: [PATCH 57/90] Initialize the padding for the Control UIA provider (#10874) ## Summary of the Pull Request This was missed in #10051. We need to make sure that the UIA provider can immediately know about the padding in the control, not just after the settings reload. ## PR Checklist * [x] Closes #9955.e * [x] Additionally, this just closes #9955. The only remaining box in there never repro'd, so probably wasn't even root caused by #9820. I think we can close that issue for now, and reactivate if something else was broken. * [x] I work here * [ ] Tests added/passed * [n/a] Requires documentation to be updated ## Validation Steps Performed Checked before/after in Accessibility Insights. Before the row rectangles were the full width of the control initially. Now they're properly padded. --- src/cascadia/TerminalControl/TermControl.cpp | 8 +++++++- .../TerminalControl/TermControlAutomationPeer.cpp | 3 ++- src/cascadia/TerminalControl/TermControlAutomationPeer.h | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 238d75c90..8d477fadf 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -517,7 +517,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation // (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers) if (const auto& interactivityAutoPeer{ _interactivity.OnCreateAutomationPeer() }) { - _automationPeer = winrt::make(this, interactivityAutoPeer); + auto margins{ SwapChainPanel().Margin() }; + + Core::Padding padding{ margins.Left, + margins.Top, + margins.Right, + margins.Bottom }; + _automationPeer = winrt::make(this, padding, interactivityAutoPeer); return _automationPeer; } } diff --git a/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp b/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp index 730064476..3cfae1cce 100644 --- a/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp +++ b/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp @@ -31,13 +31,14 @@ namespace XamlAutomation namespace winrt::Microsoft::Terminal::Control::implementation { TermControlAutomationPeer::TermControlAutomationPeer(TermControl* owner, + const Core::Padding padding, Control::InteractivityAutomationPeer impl) : TermControlAutomationPeerT(*owner), // pass owner to FrameworkElementAutomationPeer _termControl{ owner }, _contentAutomationPeer{ impl } { UpdateControlBounds(); - + SetControlPadding(padding); // Listen for UIA signalling events from the implementation. We need to // be the one to actually raise these automation events, so they go // through the UI tree correctly. diff --git a/src/cascadia/TerminalControl/TermControlAutomationPeer.h b/src/cascadia/TerminalControl/TermControlAutomationPeer.h index 0b81ebafe..e5a2cc7ff 100644 --- a/src/cascadia/TerminalControl/TermControlAutomationPeer.h +++ b/src/cascadia/TerminalControl/TermControlAutomationPeer.h @@ -43,6 +43,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { public: TermControlAutomationPeer(Microsoft::Terminal::Control::implementation::TermControl* owner, + const Core::Padding padding, Control::InteractivityAutomationPeer implementation); void UpdateControlBounds(); From 42bf605e1cab2d37f5a20835b348cb62a11561d0 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 11 Aug 2021 17:18:56 +0200 Subject: [PATCH 58/90] Use STL for ActionMap members (#10916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit My first approach to solve #10875 failed. This PR contains the most useful change as a separate commit. ## PR Checklist * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * quake mode keybinding works ✔️ * command palette still works ✔️ --- .github/actions/spelling/expect/expect.txt | 1 + .../TerminalSettingsModel/ActionMap.cpp | 47 +++++++++---------- .../TerminalSettingsModel/ActionMap.h | 7 ++- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 05ccc895a..7af541cec 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -514,6 +514,7 @@ DECAWM DECCKM DECCOLM DECDHL +decdld DECDLD DECDWL DECEKBD diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 75a1213ad..9b18354c0 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -47,12 +47,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return hashedAction ^ hashedArgs; } - ActionMap::ActionMap() : - _NestedCommands{ single_threaded_map() }, - _IterableCommands{ single_threaded_vector() } - { - } - // Method Description: // - Retrieves the Command in the current layer, if it's valid // - We internally store invalid commands as full commands. @@ -194,7 +188,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { // Update NameMap with our parents. // Starting with this means we're doing a top-down approach. - FAIL_FAST_IF(_parents.size() > 1); + assert(_parents.size() <= 1); for (const auto& parent : _parents) { parent->_PopulateNameMapWithSpecialCommands(nameMap); @@ -272,7 +266,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation }); // Now, add the accumulated actions from our parents - FAIL_FAST_IF(_parents.size() > 1); + assert(_parents.size() <= 1); for (const auto& parent : _parents) { const auto parentActions{ parent->_GetCumulativeActions() }; @@ -358,7 +352,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } // Update keyBindingsMap and unboundKeys with our parents - FAIL_FAST_IF(_parents.size() > 1); + assert(_parents.size() <= 1); for (const auto& parent : _parents) { parent->_PopulateKeyBindingMapWithStandardCommands(keyBindingsMap, unboundKeys); @@ -369,35 +363,38 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { auto actionMap{ make_self() }; - // copy _KeyMap (KeyChord --> ID) + // KeyChord --> ID actionMap->_KeyMap = _KeyMap; - // copy _ActionMap (ID --> Command) + // ID --> Command + actionMap->_ActionMap.reserve(_ActionMap.size()); for (const auto& [actionID, cmd] : _ActionMap) { - actionMap->_ActionMap.emplace(actionID, *(get_self(cmd)->Copy())); + actionMap->_ActionMap.emplace(actionID, *winrt::get_self(cmd)->Copy()); } - // copy _MaskingActions (ID --> Command) + // ID --> Command + actionMap->_MaskingActions.reserve(_MaskingActions.size()); for (const auto& [actionID, cmd] : _MaskingActions) { - actionMap->_MaskingActions.emplace(actionID, *(get_self(cmd)->Copy())); + actionMap->_MaskingActions.emplace(actionID, *winrt::get_self(cmd)->Copy()); } - // copy _NestedCommands (Name --> Command) + // Name --> Command + actionMap->_NestedCommands.reserve(_NestedCommands.size()); for (const auto& [name, cmd] : _NestedCommands) { - actionMap->_NestedCommands.Insert(name, *(get_self(cmd)->Copy())); + actionMap->_NestedCommands.emplace(name, *winrt::get_self(cmd)->Copy()); } - // copy _IterableCommands + actionMap->_IterableCommands.reserve(_IterableCommands.size()); for (const auto& cmd : _IterableCommands) { - actionMap->_IterableCommands.Append(*(get_self(cmd)->Copy())); + actionMap->_IterableCommands.emplace_back(*winrt::get_self(cmd)->Copy()); } - // repeat this for each of our parents - FAIL_FAST_IF(_parents.size() > 1); + assert(_parents.size() <= 1); + actionMap->_parents.reserve(_parents.size()); for (const auto& parent : _parents) { actionMap->_parents.emplace_back(parent->Copy()); @@ -431,7 +428,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto name{ cmd.Name() }; if (!name.empty()) { - _NestedCommands.Insert(name, cmd); + _NestedCommands.emplace(name, cmd); } return; } @@ -439,7 +436,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Handle iterable commands if (cmdImpl->IterateOn() != ExpandCommandType::None) { - _IterableCommands.Append(cmd); + _IterableCommands.emplace_back(cmd); return; } @@ -582,7 +579,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } // Handle a collision with NestedCommands - _NestedCommands.TryRemove(newName); + _NestedCommands.erase(newName); } // Method Description: @@ -750,7 +747,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // the command was not bound in this layer, // ask my parents - FAIL_FAST_IF(_parents.size() > 1); + assert(_parents.size() <= 1); for (const auto& parent : _parents) { const auto& inheritedCmd{ parent->_GetActionByKeyChordInternal(keys) }; @@ -800,7 +797,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } // Check our parents - FAIL_FAST_IF(_parents.size() > 1); + assert(_parents.size() <= 1); for (const auto& parent : _parents) { if (const auto& keys{ parent->GetKeyBindingForAction(myAction, myArgs) }) diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index a79c2c6e8..d98d5fce5 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -50,8 +50,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct ActionMap : ActionMapT, IInheritable { - ActionMap(); - // views Windows::Foundation::Collections::IMapView AvailableActions(); Windows::Foundation::Collections::IMapView NameMap(); @@ -96,8 +94,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::Foundation::Collections::IMap _NameMapCache{ nullptr }; Windows::Foundation::Collections::IMap _GlobalHotkeysCache{ nullptr }; Windows::Foundation::Collections::IMap _KeyBindingMapCache{ nullptr }; - Windows::Foundation::Collections::IMap _NestedCommands{ nullptr }; - Windows::Foundation::Collections::IVector _IterableCommands{ nullptr }; + + std::unordered_map _NestedCommands; + std::vector _IterableCommands; std::unordered_map _KeyMap; std::unordered_map _ActionMap; From 9c858cd5b82ccc2e07917b2905aa03679ae2a01e Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 11 Aug 2021 10:20:15 -0500 Subject: [PATCH 59/90] Add logging, test for #10875 (#10907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request This isn't a fix for #10875, but it is logging that help identify the root cause here. The logging may additionally be helpful for some of the other issues we're seeing elsewhere in the repo, namely #10340. @lhecker is actually working on the fix for #10875, so hopefully this test will help validate. ## References * Regressed in #10666. * logging for #8888 ## PR Checklist * [x] Closes nothing * [x] I work here * [x] Tests added, and they absolutely fail, but they're localtests, so ¯\\\_(ツ)_/¯ * [n/a] Requires documentation to be updated ## details While I was here, I noticed that `KeyBindingsTests::KeyChords` has been broken for some time now. So I fixed that too. --- .../KeyBindingsTests.cpp | 50 +++++++++++++++++-- src/cascadia/Remoting/Monarch.cpp | 27 ++++++++++ src/cascadia/WindowsTerminal/AppHost.cpp | 12 ++++- src/cascadia/WindowsTerminal/IslandWindow.cpp | 32 ++++++++---- src/cascadia/WindowsTerminal/IslandWindow.h | 2 +- 5 files changed, 106 insertions(+), 17 deletions(-) diff --git a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp index e5c354d25..4154cf3d4 100644 --- a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp @@ -40,7 +40,11 @@ namespace SettingsModelLocalTests TEST_METHOD(ManyKeysSameAction); TEST_METHOD(LayerKeybindings); TEST_METHOD(UnbindKeybindings); + + TEST_METHOD(LayerScancodeKeybindings); + TEST_METHOD(TestExplicitUnbind); + TEST_METHOD(TestArbitraryArgs); TEST_METHOD(TestSplitPaneArgs); @@ -95,23 +99,34 @@ namespace SettingsModelLocalTests VirtualKeyModifiers::Control | VirtualKeyModifiers::Menu | VirtualKeyModifiers::Shift | VirtualKeyModifiers::Windows, 255, 0, - L"ctrl+shift+alt+win+vk(255)", + L"win+ctrl+alt+shift+vk(255)", }, testCase{ - VirtualKeyModifiers::Windows, + VirtualKeyModifiers::Control | VirtualKeyModifiers::Menu | VirtualKeyModifiers::Shift | VirtualKeyModifiers::Windows, 0, 123, - L"ctrl+shift+alt+win+sc(123)", + L"win+ctrl+alt+shift+sc(123)", }, }; + // Use the KeyChordHash and KeyChordEquality to compare if two + // KeyChord's are the same. If you try to just directly compare them + // with VERIFY_ARE_EQUAL, that will always fail. It'll revert to the + // default winrt equality operator, which will compare if they point to + // literally the same object (they won't). + implementation::KeyChordHash hash; + implementation::KeyChordEquality equals; for (const auto& tc : testCases) { + Log::Comment(NoThrowString().Format(L"Testing case:\"%s\"", tc.expected.data())); + KeyChord expectedKeyChord{ tc.modifiers, tc.vkey, tc.scanCode }; const auto actualString = KeyChordSerialization::ToString(expectedKeyChord); VERIFY_ARE_EQUAL(tc.expected, actualString); + const auto actualKeyChord = KeyChordSerialization::FromString(actualString); - VERIFY_ARE_EQUAL(expectedKeyChord, actualKeyChord); + VERIFY_ARE_EQUAL(hash(expectedKeyChord), hash(actualKeyChord)); + VERIFY_IS_TRUE(equals(expectedKeyChord, actualKeyChord)); } } @@ -747,4 +762,31 @@ namespace SettingsModelLocalTests VerifyKeyChordEquality({ VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, static_cast('P'), 0 }, kbd); } } + + void KeyBindingsTests::LayerScancodeKeybindings() + { + Log::Comment(L"Layering a keybinding with a character literal on top of" + L" an equivalent sc() key should replace it."); + + // Wrap the first one in `R"!(...)!"` because it has `()` internally. + const std::string bindings0String{ R"!([ { "command": "quakeMode", "keys":"win+sc(41)" } ])!" }; + const std::string bindings1String{ R"([ { "keys": "win+`", "command": { "action": "globalSummon", "monitor": "any" } } ])" }; + const std::string bindings2String{ R"([ { "keys": "ctrl+shift+`", "command": { "action": "quakeMode" } } ])" }; + + const auto bindings0Json = VerifyParseSucceeded(bindings0String); + const auto bindings1Json = VerifyParseSucceeded(bindings1String); + const auto bindings2Json = VerifyParseSucceeded(bindings2String); + + auto actionMap = winrt::make_self(); + VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); + + actionMap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); + + actionMap->LayerJson(bindings1Json); + VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size(), L"Layering the second action should replace the first one."); + + actionMap->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(2u, actionMap->_KeyMap.size()); + } } diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index f65422d3f..a5444e3d4 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -201,6 +201,12 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _clearOldMruEntries(id); } + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_lookupPeasantIdForName", + TraceLoggingWideString(std::wstring{ name }.c_str(), "name", "the name we're looking for"), + TraceLoggingUInt64(result, "peasantID", "the ID of the peasant with that name"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); return result; } @@ -750,6 +756,27 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { targetPeasant.Summon(args.SummonBehavior()); args.FoundMatch(true); + + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SummonWindow_Success", + TraceLoggingWideString(searchedForName.c_str(), "searchedForName", "The name of the window we tried to summon"), + TraceLoggingUInt64(windowId, "peasantID", "The id of the window we tried to summon"), + TraceLoggingBoolean(args.OnCurrentDesktop(), "OnCurrentDesktop", "true iff the window needs to be on the current virtual desktop"), + TraceLoggingBoolean(args.SummonBehavior().MoveToCurrentDesktop(), "MoveToCurrentDesktop", "if true, move the window to the current virtual desktop"), + TraceLoggingBoolean(args.SummonBehavior().ToggleVisibility(), "ToggleVisibility", "true if we should toggle the visibility of the window"), + TraceLoggingUInt32(args.SummonBehavior().DropdownDuration(), "DropdownDuration", "the duration to dropdown the window"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + else + { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SummonWindow_NoPeasant", + TraceLoggingWideString(searchedForName.c_str(), "searchedForName", "The name of the window we tried to summon"), + TraceLoggingUInt64(windowId, "peasantID", "The id of the window we tried to summon"), + TraceLoggingBoolean(args.OnCurrentDesktop(), "OnCurrentDesktop", "true iff the window needs to be on the current virtual desktop"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } } catch (...) diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 5f821d190..a89cc3756 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -692,7 +692,17 @@ winrt::fire_and_forget AppHost::_setupGlobalHotkeys() { if (auto summonArgs = cmd.ActionAndArgs().Args().try_as()) { - _window->RegisterHotKey(gsl::narrow_cast(_hotkeys.size()), keyChord); + int index = gsl::narrow_cast(_hotkeys.size()); + const bool succeeded = _window->RegisterHotKey(index, keyChord); + + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_setupGlobalHotkey", + TraceLoggingDescription("Emitted when setting a single hotkey"), + TraceLoggingInt64(index, "index", "the index of the hotkey to add"), + TraceLoggingWideString(cmd.Name().c_str(), "name", "the name of the command"), + TraceLoggingBoolean(succeeded, "succeeded", "true if we succeeded"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); _hotkeys.emplace_back(summonArgs); } } diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 5cfffa4fc..92dc184c5 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -1070,8 +1070,9 @@ void IslandWindow::UnregisterHotKey(const int index) noexcept { TraceLoggingWrite( g_hWindowsTerminalProvider, - "UnregisterAllHotKeys", + "UnregisterHotKey", TraceLoggingDescription("Emitted when clearing previously set hotkeys"), + TraceLoggingInt64(index, "index", "the index of the hotkey to remove"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); @@ -1088,15 +1089,8 @@ void IslandWindow::UnregisterHotKey(const int index) noexcept // - hotkey: The key-combination to register. // Return Value: // - -void IslandWindow::RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept +bool IslandWindow::RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept { - TraceLoggingWrite( - g_hWindowsTerminalProvider, - "RegisterHotKey", - TraceLoggingDescription("Emitted when setting hotkeys"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - auto vkey = hotkey.Vkey(); if (!vkey) { @@ -1104,7 +1098,7 @@ void IslandWindow::RegisterHotKey(const int index, const winrt::Microsoft::Termi } if (!vkey) { - return; + return false; } auto hotkeyFlags = MOD_NOREPEAT; @@ -1118,7 +1112,23 @@ void IslandWindow::RegisterHotKey(const int index, const winrt::Microsoft::Termi // TODO GH#8888: We should display a warning of some kind if this fails. // This can fail if something else already bound this hotkey. - LOG_IF_WIN32_BOOL_FALSE(::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey)); + const auto result = ::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey); + LOG_IF_WIN32_BOOL_FALSE(result); + + TraceLoggingWrite(g_hWindowsTerminalProvider, + "RegisterHotKey", + TraceLoggingDescription("Emitted when setting hotkeys"), + TraceLoggingInt64(index, "index", "the index of the hotkey to add"), + TraceLoggingUInt64(vkey, "vkey", "the key"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_WIN), "win", "is WIN in the modifiers"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_ALT), "alt", "is ALT in the modifiers"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_CONTROL), "control", "is CONTROL in the modifiers"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_SHIFT), "shift", "is SHIFT in the modifiers"), + TraceLoggingBool(result, "succeeded", "true if we succeeded"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + return result; } // Method Description: diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index b0699864d..bea360862 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -40,7 +40,7 @@ public: void SetTaskbarProgress(const size_t state, const size_t progress); void UnregisterHotKey(const int index) noexcept; - void RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept; + bool RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept; winrt::fire_and_forget SummonWindow(winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior args); From d465a47bc5bdd6eca2620a67144d139226d1d617 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 12 Aug 2021 01:09:25 +0200 Subject: [PATCH 60/90] Fix layering of sc() keybindings with vk() ones (#10917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quake mode keybinding is bound to a scancode. This made it impossible to override it with a vkey-based one like "win+\`". This commit fixes the issue by making sure that a `KeyChord` always has a vkey, and leveraging this fact inside ActionMap, which now ignores the scan-code. ## PR Checklist * [x] Closes #10875 * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * quake mode and other keybinding still work ✔️ * Repro settings from #10875 work correctly ✔️ --- .github/actions/spelling/expect/expect.txt | 2 + .../KeyBindingsTests.cpp | 21 +++--- src/cascadia/TerminalControl/KeyChord.cpp | 12 +++- .../TerminalSettingsModel/ActionMap.cpp | 48 +++++--------- .../TerminalSettingsModel/ActionMap.h | 64 +++++++++++++++++-- src/cascadia/WindowsTerminal/IslandWindow.cpp | 11 +--- tools/OpenConsole.psm1 | 11 +--- tools/tests.xml | 4 +- 8 files changed, 99 insertions(+), 74 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 7af541cec..44f9b0d60 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -788,6 +788,7 @@ flyout fmodern fmtarg fmtid +FNV FOLDERID FONTCHANGE fontdlg @@ -1469,6 +1470,7 @@ Mul multiline munged munges +murmurhash mutex mutexes muxes diff --git a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp index 4154cf3d4..cbdf82fda 100644 --- a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp @@ -109,24 +109,23 @@ namespace SettingsModelLocalTests }, }; - // Use the KeyChordHash and KeyChordEquality to compare if two - // KeyChord's are the same. If you try to just directly compare them - // with VERIFY_ARE_EQUAL, that will always fail. It'll revert to the - // default winrt equality operator, which will compare if they point to - // literally the same object (they won't). - implementation::KeyChordHash hash; - implementation::KeyChordEquality equals; for (const auto& tc : testCases) { Log::Comment(NoThrowString().Format(L"Testing case:\"%s\"", tc.expected.data())); - KeyChord expectedKeyChord{ tc.modifiers, tc.vkey, tc.scanCode }; - const auto actualString = KeyChordSerialization::ToString(expectedKeyChord); + const auto actualString = KeyChordSerialization::ToString({ tc.modifiers, tc.vkey, tc.scanCode }); VERIFY_ARE_EQUAL(tc.expected, actualString); + auto expectedVkey = tc.vkey; + if (!expectedVkey) + { + expectedVkey = MapVirtualKeyW(tc.scanCode, MAPVK_VSC_TO_VK_EX); + } + const auto actualKeyChord = KeyChordSerialization::FromString(actualString); - VERIFY_ARE_EQUAL(hash(expectedKeyChord), hash(actualKeyChord)); - VERIFY_IS_TRUE(equals(expectedKeyChord, actualKeyChord)); + VERIFY_ARE_EQUAL(tc.modifiers, actualKeyChord.Modifiers()); + VERIFY_ARE_EQUAL(expectedVkey, actualKeyChord.Vkey()); + VERIFY_ARE_EQUAL(tc.scanCode, actualKeyChord.ScanCode()); } } diff --git a/src/cascadia/TerminalControl/KeyChord.cpp b/src/cascadia/TerminalControl/KeyChord.cpp index 4dd61d6fa..04a3af358 100644 --- a/src/cascadia/TerminalControl/KeyChord.cpp +++ b/src/cascadia/TerminalControl/KeyChord.cpp @@ -21,9 +21,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } KeyChord::KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey, int32_t scanCode) noexcept : - _modifiers{ modifiersFromBooleans(ctrl, alt, shift, win) }, - _vkey{ vkey }, - _scanCode{ scanCode } + KeyChord(modifiersFromBooleans(ctrl, alt, shift, win), vkey, scanCode) { } @@ -32,6 +30,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation _vkey{ vkey }, _scanCode{ scanCode } { + // ActionMap needs to identify KeyChords which should "layer" (overwrite) each other. + // For instance win+sc(41) and win+` both specify the same KeyChord on an US keyboard layout + // from the perspective of a user. Either of the two should correctly overwrite the other. + // We can help ActionMap with this by ensuring that Vkey() is always valid. + if (!_vkey) + { + _vkey = MapVirtualKeyW(scanCode, MAPVK_VSC_TO_VK_EX); + } } VirtualKeyModifiers KeyChord::Modifiers() noexcept diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 9b18354c0..a8f1de792 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -302,7 +302,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { // populate _KeyBindingMapCache std::unordered_map keyBindingsMap; - std::unordered_set unboundKeys; + std::unordered_set unboundKeys; _PopulateKeyBindingMapWithStandardCommands(keyBindingsMap, unboundKeys); _KeyBindingMapCache = single_threaded_map(std::move(keyBindingsMap)); @@ -317,7 +317,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Arguments: // - keyBindingsMap: the keyBindingsMap we're populating. This maps the key chord of a command to the command itself. // - unboundKeys: a set of keys that are explicitly unbound - void ActionMap::_PopulateKeyBindingMapWithStandardCommands(std::unordered_map& keyBindingsMap, std::unordered_set& unboundKeys) const + void ActionMap::_PopulateKeyBindingMapWithStandardCommands(std::unordered_map& keyBindingsMap, std::unordered_set& unboundKeys) const { // Update KeyBindingsMap with our current layer for (const auto& [keys, actionID] : _KeyMap) @@ -683,21 +683,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // - false if either the keychord is bound, or not bound at all bool ActionMap::IsKeyChordExplicitlyUnbound(Control::KeyChord const& keys) const { - const auto modifiers = keys.Modifiers(); - - // The "keys" given to us can contain both a Vkey, as well as a ScanCode. - // For instance our UI code fills out a KeyChord with all available information. - // But our _KeyMap only contains KeyChords that contain _either_ a Vkey or ScanCode. - // Due to this we'll have to call _GetActionByKeyChordInternal twice. - if (auto vkey = keys.Vkey()) - { - // We use the fact that the ..Internal call returns nullptr for explicitly unbound - // key chords, and nullopt for keychord that are not bound - it allows us to distinguish - // between unbound and lack of binding. - return nullptr == _GetActionByKeyChordInternal({ modifiers, vkey, 0 }); - } - - return nullptr == _GetActionByKeyChordInternal({ modifiers, 0, keys.ScanCode() }); + // We use the fact that the ..Internal call returns nullptr for explicitly unbound + // key chords, and nullopt for keychord that are not bound - it allows us to distinguish + // between unbound and lack of binding. + return _GetActionByKeyChordInternal(keys) == nullptr; } // Method Description: @@ -709,21 +698,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // - nullptr if the key chord doesn't exist Model::Command ActionMap::GetActionByKeyChord(Control::KeyChord const& keys) const { - const auto modifiers = keys.Modifiers(); - - // The "keys" given to us can contain both a Vkey, as well as a ScanCode. - // For instance our UI code fills out a KeyChord with all available information. - // But our _KeyMap only contains KeyChords that contain _either_ a Vkey or ScanCode. - // Due to this we'll have to call _GetActionByKeyChordInternal twice. - if (auto vkey = keys.Vkey()) - { - if (auto command = _GetActionByKeyChordInternal({ modifiers, vkey, 0 })) - { - return *command; - } - } - - return _GetActionByKeyChordInternal({ modifiers, 0, keys.ScanCode() }).value_or(nullptr); + return _GetActionByKeyChordInternal(keys).value_or(nullptr); } // Method Description: @@ -735,8 +710,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // - the command with the given key chord // - nullptr if the key chord is explicitly unbound // - nullopt if it was not bound in this layer - std::optional ActionMap::_GetActionByKeyChordInternal(Control::KeyChord const& keys) const + std::optional ActionMap::_GetActionByKeyChordInternal(const ActionMapKeyChord keys) const { + // KeyChord's constructor ensures that Modifiers() & Vkey() is a valid value at a minimum. + // This allows ActionMap to identify KeyChords which should "layer" (overwrite) each other. + // For instance win+sc(41) and win+` both specify the same KeyChord on an US keyboard layout + // from the perspective of a user. Either of the two should correctly overwrite the other. + // As such we need to pretend as if ScanCode doesn't exist. + assert(keys.vkey != 0); + // Check the current layer if (const auto actionIDPair = _KeyMap.find(keys); actionIDPair != _KeyMap.end()) { diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index d98d5fce5..51e96963a 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -28,6 +28,60 @@ namespace SettingsModelLocalTests class TerminalSettingsTests; } +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + union ActionMapKeyChord + { + uint16_t value = 0; + struct + { + uint8_t modifiers; + uint8_t vkey; + }; + + constexpr ActionMapKeyChord() = default; + ActionMapKeyChord(const Control::KeyChord& keys) : + modifiers(gsl::narrow_cast(keys.Modifiers())), + vkey(gsl::narrow_cast(keys.Vkey())) + { + } + + constexpr bool operator==(ActionMapKeyChord other) const noexcept + { + return value == other.value; + } + }; +} + +template<> +struct std::hash +{ + constexpr size_t operator()(winrt::Microsoft::Terminal::Settings::Model::implementation::ActionMapKeyChord keys) const noexcept + { + // I didn't like how std::hash uses the byte-wise FNV1a for integers. + // So I built my own std::hash with murmurhash3. +#if SIZE_MAX == UINT32_MAX + size_t h = keys.value; + h ^= h >> 16; + h *= UINT32_C(0x85ebca6b); + h ^= h >> 13; + h *= UINT32_C(0xc2b2ae35); + h ^= h >> 16; + return h; +#elif SIZE_MAX == UINT64_MAX + size_t h = keys.value; + h ^= h >> 33; + h *= UINT64_C(0xff51afd7ed558ccd); + h ^= h >> 33; + h *= UINT64_C(0xc4ceb9fe1a85ec53); + h ^= h >> 33; + return h; +#else + return std::hash{}(keys.value); +#endif + } +}; + namespace winrt::Microsoft::Terminal::Settings::Model::implementation { using InternalActionID = size_t; @@ -36,7 +90,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { std::size_t operator()(const Control::KeyChord& key) const { - return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(key.Modifiers(), key.Vkey(), key.ScanCode()); + return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(key.Modifiers(), key.Vkey()); } }; @@ -44,7 +98,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { bool operator()(const Control::KeyChord& lhs, const Control::KeyChord& rhs) const { - return lhs.Modifiers() == rhs.Modifiers() && lhs.Vkey() == rhs.Vkey() && lhs.ScanCode() == rhs.ScanCode(); + return lhs.Modifiers() == rhs.Modifiers() && lhs.Vkey() == rhs.Vkey(); } }; @@ -78,12 +132,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation private: std::optional _GetActionByID(const InternalActionID actionID) const; - std::optional _GetActionByKeyChordInternal(const Control::KeyChord& keys) const; + std::optional _GetActionByKeyChordInternal(const ActionMapKeyChord keys) const; void _PopulateAvailableActionsWithStandardCommands(std::unordered_map& availableActions, std::unordered_set& visitedActionIDs) const; void _PopulateNameMapWithSpecialCommands(std::unordered_map& nameMap) const; void _PopulateNameMapWithStandardCommands(std::unordered_map& nameMap) const; - void _PopulateKeyBindingMapWithStandardCommands(std::unordered_map& keyBindingsMap, std::unordered_set& unboundKeys) const; + void _PopulateKeyBindingMapWithStandardCommands(std::unordered_map& keyBindingsMap, std::unordered_set& unboundKeys) const; std::vector _GetCumulativeActions() const noexcept; void _TryUpdateActionMap(const Model::Command& cmd, Model::Command& oldCmd, Model::Command& consolidatedCmd); @@ -97,7 +151,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation std::unordered_map _NestedCommands; std::vector _IterableCommands; - std::unordered_map _KeyMap; + std::unordered_map _KeyMap; std::unordered_map _ActionMap; // Masking Actions: diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 92dc184c5..731dfa168 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -1091,16 +1091,7 @@ void IslandWindow::UnregisterHotKey(const int index) noexcept // - bool IslandWindow::RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept { - auto vkey = hotkey.Vkey(); - if (!vkey) - { - vkey = MapVirtualKeyW(hotkey.ScanCode(), MAPVK_VSC_TO_VK); - } - if (!vkey) - { - return false; - } - + const auto vkey = hotkey.Vkey(); auto hotkeyFlags = MOD_NOREPEAT; { const auto modifiers = hotkey.Modifiers(); diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index 84b85c2dc..bad0afb0d 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -193,11 +193,9 @@ function Invoke-OpenConsoleTests() return } $OpenConsolePlatform = $Platform - $TestHostAppPath = "$root\bin\$OpenConsolePlatform\$Configuration\TestHostApp" if ($Platform -eq 'x86') { $OpenConsolePlatform = 'Win32' - $TestHostAppPath = "$root\$Configuration\TestHostApp" } $OpenConsolePath = "$env:OpenConsoleroot\bin\$OpenConsolePlatform\$Configuration\OpenConsole.exe" $TaefExePath = "$root\packages\Microsoft.Taef.10.60.210621002\build\Binaries\$Platform\te.exe" @@ -236,14 +234,7 @@ function Invoke-OpenConsoleTests() { if ($t.type -eq "unit") { - if ($t.runInHostApp -eq "true") - { - & $TaefExePath "$TestHostAppPath\$($t.binary)" $TaefArgs - } - else - { - & $TaefExePath "$BinDir\$($t.binary)" $TaefArgs - } + & $TaefExePath "$BinDir\$($t.binary)" $TaefArgs } elseif ($t.type -eq "ft") { diff --git a/tools/tests.xml b/tools/tests.xml index fd6444662..c6085b0b4 100644 --- a/tools/tests.xml +++ b/tools/tests.xml @@ -4,8 +4,8 @@ - - + + From 9eb9bc9235b9704d9dd12ccac6d82b79055fe4d6 Mon Sep 17 00:00:00 2001 From: Schuyler Rosefield Date: Thu, 12 Aug 2021 12:41:17 -0400 Subject: [PATCH 61/90] Move Pane to Tab (GH7075) (#10780) ## Summary of the Pull Request Add functionality to move a pane to another tab. If the tab index is greater than the number of current tabs a new tab will be created with the pane as its root. Similarly, if the last pane on a tab is moved to another tab, the original tab will be closed. This is largely complete, but I know that I'm messing around with things that I am unfamiliar with, and would like to avoid footguns where possible. ## References #4587 ## PR Checklist * [x] Closes #7075 * [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA * [ ] Tests added/passed * [x] 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 Things done: - Moving a pane to a new tab appears to work. Moving a pane to an existing tab mostly works. Moving a pane back to its original tab appears to work. - Set up {Attach,Detach}Pane methods to add or remove a pane from a pane. Detach is slightly different than Close in that we want to persist the tree structure and terminal controls. - Add `Detached` event on a pane that can be subscribed to to remove other event handlers if desired. - Added simple WalkTree abstraction for one-off recursion use cases that calls a provided function on each pane in order (and optionally terminates early). - Fixed an in-prod bug with closing panes. Specifically, if you have a tree (1; 2 3) and close the 1 pane, then 3 will lose its borders because of these lines clearing the border on both children https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalApp/Pane.cpp#L1197-L1201 . To do: - Right now I have `TerminalTab` as a friend class of `Pane` so I can access some extra properties in my `WalkTree` callbacks, but there is probably a better choice for the abstraction boundary. Next Steps: - In a future PR Drag & Drop handlers could be added that utilize the Attach/Detach infrastructure to provide a better UI. - Similarly once this is working, it should be possible to convert an entire tab into a pane on an existing tab (Tab::DetachRoot on original tab followed by Tab::AttachPane on the target tab). - Its been 10 years, I just really want to use concepts already. ## Validation Steps Performed Manual testing by creating pane(s), and moving them between tabs and creating new tabs and destroying tabs by moving the last remaining pane. --- .github/actions/spelling/allow/apis.txt | 1 + doc/cascadia/profiles.schema.json | 25 +- .../CommandlineTest.cpp | 35 +- .../LocalTests_TerminalApp/TabTests.cpp | 20 +- .../TerminalApp/AppActionHandlers.cpp | 20 +- .../TerminalApp/AppCommandlineArgs.cpp | 63 +++- src/cascadia/TerminalApp/AppCommandlineArgs.h | 5 +- src/cascadia/TerminalApp/Pane.cpp | 131 ++++++-- src/cascadia/TerminalApp/Pane.h | 41 ++- .../Resources/en-US/Resources.resw | 18 +- src/cascadia/TerminalApp/TabManagement.cpp | 122 ++++--- src/cascadia/TerminalApp/TerminalPage.cpp | 75 ++++- src/cascadia/TerminalApp/TerminalPage.h | 8 +- src/cascadia/TerminalApp/TerminalTab.cpp | 306 +++++++++++++++--- src/cascadia/TerminalApp/TerminalTab.h | 30 +- .../TerminalSettingsModel/ActionAndArgs.cpp | 2 + .../TerminalSettingsModel/ActionArgs.cpp | 14 +- .../TerminalSettingsModel/ActionArgs.h | 67 +++- .../TerminalSettingsModel/ActionArgs.idl | 10 +- .../AllShortcutActions.h | 2 + .../Resources/en-US/Resources.resw | 15 +- .../TerminalSettingsModel/defaults.json | 19 +- 22 files changed, 808 insertions(+), 221 deletions(-) diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 85121e065..18023d517 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -79,6 +79,7 @@ localtime lround LSHIFT memicmp +mptt mov msappx MULTIPLEUSE diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 2b078d00d..2c736dd2f 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -239,6 +239,7 @@ "identifyWindows", "moveFocus", "movePane", + "swapPane", "moveTab", "newTab", "newWindow", @@ -493,6 +494,23 @@ "type": "integer", "default": 0, "description": "Which tab to switch to, with the first being 0" + } + } + } + ], + "required": [ "index" ] + }, + "MovePaneAction": { + "description": "Arguments corresponding to a Move Pane Action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "movePane" }, + "index": { + "type": "integer", + "default": 0, + "description": "Which tab to move the pane to, with the first being 0" } } } @@ -516,13 +534,13 @@ ], "required": [ "direction" ] }, - "MovePaneAction": { - "description": "Arguments corresponding to a Move Pane Action", + "SwapPaneAction": { + "description": "Arguments corresponding to a Swap Pane Action", "allOf": [ { "$ref": "#/definitions/ShortcutAction" }, { "properties": { - "action": { "type": "string", "pattern": "movePane" }, + "action": { "type": "string", "pattern": "swapPane" }, "direction": { "$ref": "#/definitions/FocusDirection", "default": "left", @@ -972,6 +990,7 @@ { "$ref": "#/definitions/SwitchToTabAction" }, { "$ref": "#/definitions/MoveFocusAction" }, { "$ref": "#/definitions/MovePaneAction" }, + { "$ref": "#/definitions/SwapPaneAction" }, { "$ref": "#/definitions/ResizePaneAction" }, { "$ref": "#/definitions/SendInputAction" }, { "$ref": "#/definitions/SplitPaneAction" }, diff --git a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp index c5d853d32..8384690c4 100644 --- a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp +++ b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp @@ -56,7 +56,7 @@ namespace TerminalAppLocalTests TEST_METHOD(ParseComboCommandlineIntoArgs); TEST_METHOD(ParseFocusTabArgs); TEST_METHOD(ParseMoveFocusArgs); - TEST_METHOD(ParseMovePaneArgs); + TEST_METHOD(ParseSwapPaneArgs); TEST_METHOD(ParseArgumentsWithParsingTerminators); TEST_METHOD(ParseFocusPaneArgs); @@ -1208,14 +1208,9 @@ namespace TerminalAppLocalTests } } - void CommandlineTest::ParseMovePaneArgs() + void CommandlineTest::ParseSwapPaneArgs() { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:useShortForm", L"{false, true}") - END_TEST_METHOD_PROPERTIES() - - INIT_TEST_PROPERTY(bool, useShortForm, L"If true, use `mp` instead of `move-pane`"); - const wchar_t* subcommand = useShortForm ? L"mp" : L"move-pane"; + const wchar_t* subcommand = L"swap-pane"; { AppCommandlineArgs appArgs{}; @@ -1236,9 +1231,9 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction()); } @@ -1253,9 +1248,9 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction()); } @@ -1270,9 +1265,9 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Up, myArgs.Direction()); } @@ -1287,9 +1282,9 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Down, myArgs.Direction()); } @@ -1311,16 +1306,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); auto actionAndArgs = appArgs._startupActions.at(1); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - auto myArgs = actionAndArgs.Args().try_as(); + auto myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction()); actionAndArgs = appArgs._startupActions.at(2); - VERIFY_ARE_EQUAL(ShortcutAction::MovePane, actionAndArgs.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); VERIFY_IS_NOT_NULL(actionAndArgs.Args()); - myArgs = actionAndArgs.Args().try_as(); + myArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(myArgs); VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction()); } diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 9d7653dc8..7ae93e093 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -82,7 +82,7 @@ namespace TerminalAppLocalTests TEST_METHOD(MoveFocusFromZoomedPane); TEST_METHOD(CloseZoomedPane); - TEST_METHOD(MovePanes); + TEST_METHOD(SwapPanes); TEST_METHOD(NextMRUTab); TEST_METHOD(VerifyCommandPaletteTabSwitcherOrder); @@ -821,7 +821,7 @@ namespace TerminalAppLocalTests VERIFY_SUCCEEDED(result); } - void TabTests::MovePanes() + void TabTests::SwapPanes() { auto page = _commonSetup(); @@ -914,10 +914,10 @@ namespace TerminalAppLocalTests // ------------------- TestOnUIThread([&]() { // Set up action - MovePaneArgs args{ FocusDirection::Left }; + SwapPaneArgs args{ FocusDirection::Left }; ActionEventArgs eventArgs{ args }; - page->_HandleMovePane(nullptr, eventArgs); + page->_HandleSwapPane(nullptr, eventArgs); }); Sleep(250); @@ -945,10 +945,10 @@ namespace TerminalAppLocalTests // ------------------- TestOnUIThread([&]() { // Set up action - MovePaneArgs args{ FocusDirection::Up }; + SwapPaneArgs args{ FocusDirection::Up }; ActionEventArgs eventArgs{ args }; - page->_HandleMovePane(nullptr, eventArgs); + page->_HandleSwapPane(nullptr, eventArgs); }); Sleep(250); @@ -976,10 +976,10 @@ namespace TerminalAppLocalTests // ------------------- TestOnUIThread([&]() { // Set up action - MovePaneArgs args{ FocusDirection::Right }; + SwapPaneArgs args{ FocusDirection::Right }; ActionEventArgs eventArgs{ args }; - page->_HandleMovePane(nullptr, eventArgs); + page->_HandleSwapPane(nullptr, eventArgs); }); Sleep(250); @@ -1007,10 +1007,10 @@ namespace TerminalAppLocalTests // ------------------- TestOnUIThread([&]() { // Set up action - MovePaneArgs args{ FocusDirection::Down }; + SwapPaneArgs args{ FocusDirection::Down }; ActionEventArgs eventArgs{ args }; - page->_HandleMovePane(nullptr, eventArgs); + page->_HandleSwapPane(nullptr, eventArgs); }); Sleep(250); diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index f88017e1e..3960b6c38 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -143,6 +143,20 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleMovePane(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (args == nullptr) + { + args.Handled(false); + } + else if (const auto& realArgs = args.ActionArgs().try_as()) + { + auto moved = _MovePane(realArgs.TabIndex()); + args.Handled(moved); + } + } + void TerminalPage::_HandleSplitPane(const IInspectable& /*sender*/, const ActionEventArgs& args) { @@ -323,10 +337,10 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleMovePane(const IInspectable& /*sender*/, + void TerminalPage::_HandleSwapPane(const IInspectable& /*sender*/, const ActionEventArgs& args) { - if (const auto& realArgs = args.ActionArgs().try_as()) + if (const auto& realArgs = args.ActionArgs().try_as()) { if (realArgs.Direction() == FocusDirection::None) { @@ -335,7 +349,7 @@ namespace winrt::TerminalApp::implementation } else { - _MovePane(realArgs.Direction()); + _SwapPane(realArgs.Direction()); args.Handled(true); } } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index c820fa09b..5e7581d8f 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -193,6 +193,7 @@ void AppCommandlineArgs::_buildParser() _buildFocusTabParser(); _buildMoveFocusParser(); _buildMovePaneParser(); + _buildSwapPaneParser(); _buildFocusPaneParser(); } @@ -297,6 +298,43 @@ void AppCommandlineArgs::_buildSplitPaneParser() setupSubcommand(_newPaneCommand); setupSubcommand(_newPaneShort); } +// Method Description: +// - Adds the `move-pane` subcommand and related options to the commandline parser. +// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane` +// Arguments: +// - +// Return Value: +// - +void AppCommandlineArgs::_buildMovePaneParser() +{ + _movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc")); + _movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc")); + + auto setupSubcommand = [this](auto* subcommand) { + subcommand->add_option("-t,--tab", + _movePaneTabIndex, + RS_A(L"CmdMovePaneTabArgDesc")); + + // When ParseCommand is called, if this subcommand was provided, this + // callback function will be triggered on the same thread. We can be sure + // that `this` will still be safe - this function just lets us know this + // command was parsed. + subcommand->callback([&, this]() { + // Build the action from the values we've parsed on the commandline. + ActionAndArgs movePaneAction{}; + + if (_movePaneTabIndex >= 0) + { + movePaneAction.Action(ShortcutAction::MovePane); + MovePaneArgs args{ static_cast(_movePaneTabIndex) }; + movePaneAction.Args(args); + _startupActions.push_back(movePaneAction); + } + }); + }; + setupSubcommand(_movePaneCommand); + setupSubcommand(_movePaneShort); +} // Method Description: // - Adds the `focus-tab` subcommand and related options to the commandline parser. @@ -400,16 +438,14 @@ void AppCommandlineArgs::_buildMoveFocusParser() } // Method Description: -// - Adds the `move-pane` subcommand and related options to the commandline parser. -// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane` +// - Adds the `swap-pane` subcommand and related options to the commandline parser. // Arguments: // - // Return Value: // - -void AppCommandlineArgs::_buildMovePaneParser() +void AppCommandlineArgs::_buildSwapPaneParser() { - _movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc")); - _movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc")); + _swapPaneCommand = _app.add_subcommand("swap-pane", RS_A(L"CmdSwapPaneDesc")); auto setupSubcommand = [this](auto* subcommand) { std::map map = { @@ -420,8 +456,8 @@ void AppCommandlineArgs::_buildMovePaneParser() }; auto* directionOpt = subcommand->add_option("direction", - _movePaneDirection, - RS_A(L"CmdMovePaneDirectionArgDesc")); + _swapPaneDirection, + RS_A(L"CmdSwapPaneDirectionArgDesc")); directionOpt->transform(CLI::CheckedTransformer(map, CLI::ignore_case)); directionOpt->required(); @@ -430,12 +466,12 @@ void AppCommandlineArgs::_buildMovePaneParser() // that `this` will still be safe - this function just lets us know this // command was parsed. subcommand->callback([&, this]() { - if (_movePaneDirection != FocusDirection::None) + if (_swapPaneDirection != FocusDirection::None) { - MovePaneArgs args{ _movePaneDirection }; + SwapPaneArgs args{ _swapPaneDirection }; ActionAndArgs actionAndArgs{}; - actionAndArgs.Action(ShortcutAction::MovePane); + actionAndArgs.Action(ShortcutAction::SwapPane); actionAndArgs.Args(args); _startupActions.push_back(std::move(actionAndArgs)); @@ -443,8 +479,7 @@ void AppCommandlineArgs::_buildMovePaneParser() }); }; - setupSubcommand(_movePaneCommand); - setupSubcommand(_movePaneShort); + setupSubcommand(_swapPaneCommand); } // Method Description: @@ -625,6 +660,7 @@ bool AppCommandlineArgs::_noCommandsProvided() *_moveFocusShort || *_movePaneCommand || *_movePaneShort || + *_swapPaneCommand || *_focusPaneCommand || *_focusPaneShort || *_newPaneShort.subcommand || @@ -653,12 +689,13 @@ void AppCommandlineArgs::_resetStateToDefault() _splitPaneSize = 0.5f; _splitDuplicate = false; + _movePaneTabIndex = -1; _focusTabIndex = -1; _focusNextTab = false; _focusPrevTab = false; _moveFocusDirection = FocusDirection::None; - _movePaneDirection = FocusDirection::None; + _swapPaneDirection = FocusDirection::None; _focusPaneTarget = -1; diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 81dc564ba..598c3b8ed 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -84,6 +84,7 @@ private: CLI::App* _moveFocusShort; CLI::App* _movePaneCommand; CLI::App* _movePaneShort; + CLI::App* _swapPaneCommand; CLI::App* _focusPaneCommand; CLI::App* _focusPaneShort; @@ -97,7 +98,7 @@ private: bool _suppressApplicationTitle{ false }; winrt::Microsoft::Terminal::Settings::Model::FocusDirection _moveFocusDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; - winrt::Microsoft::Terminal::Settings::Model::FocusDirection _movePaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; + winrt::Microsoft::Terminal::Settings::Model::FocusDirection _swapPaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; // _commandline will contain the command line with which we'll be spawning a new terminal std::vector _commandline; @@ -107,6 +108,7 @@ private: bool _splitDuplicate{ false }; float _splitPaneSize{ 0.5f }; + int _movePaneTabIndex{ -1 }; int _focusTabIndex{ -1 }; bool _focusNextTab{ false }; bool _focusPrevTab{ false }; @@ -132,6 +134,7 @@ private: void _buildFocusTabParser(); void _buildMoveFocusParser(); void _buildMovePaneParser(); + void _buildSwapPaneParser(); void _buildFocusPaneParser(); bool _noCommandsProvided(); void _resetStateToDefault(); diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 45db24870..439f41313 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -220,7 +220,7 @@ bool Pane::ResizePane(const ResizeDirection& direction) // direction, we'll return false. This will indicate to our parent that they // should try and move the focus themselves. In this way, the focus can move // up and down the tree to the correct pane. -// - This method is _very_ similar to MovePane. Both are trying to find the +// - This method is _very_ similar to SwapPane. Both are trying to find the // right pane to move (focus) in a direction. // Arguments: // - direction: The direction to move the focus in. @@ -595,7 +595,7 @@ Pane::FocusNeighborSearch Pane::_FindFocusAndNeighbor(const FocusDirection& dire // - direction: The direction to move the focused pane in. // Return Value: // - true if we or a child handled this pane move request. -bool Pane::MovePane(const FocusDirection& direction) +bool Pane::SwapPane(const FocusDirection& direction) { // If we're a leaf, do nothing. We can't possibly swap anything. if (_IsLeaf()) @@ -614,7 +614,7 @@ bool Pane::MovePane(const FocusDirection& direction) // and its neighbor must necessarily be contained within the same child. if (!DirectionMatchesSplit(direction, _splitState)) { - return _firstChild->MovePane(direction) || _secondChild->MovePane(direction); + return _firstChild->SwapPane(direction) || _secondChild->SwapPane(direction); } // Since the direction is the same as our split, it is possible that we must @@ -1011,6 +1011,85 @@ void Pane::UpdateSettings(const TerminalSettingsCreateResult& settings, const GU } } +// Method Description: +// - Attempts to add the provided pane as a split of the current pane. +// Arguments: +// - pane: the new pane to add +// - splitType: How the pane should be attached +// Return Value: +// - the new reference to the child created from the current pane. +std::shared_ptr Pane::AttachPane(std::shared_ptr pane, SplitState splitType) +{ + // Splice the new pane into the tree + const auto [first, _] = _Split(splitType, .5, pane); + + // If the new pane has a child that was the focus, re-focus it + // to steal focus from the currently focused pane. + if (pane->_HasFocusedChild()) + { + pane->WalkTree([](auto p) { + if (p->_lastActive) + { + p->_FocusFirstChild(); + return true; + } + return false; + }); + } + + return first; +} + +// Method Description: +// - Attempts to find the parent of the target pane, +// if found remove the pane from the tree and return it. +// - If the removed pane was (or contained the focus) the first sibling will +// gain focus. +// Arguments: +// - pane: the pane to detach +// Return Value: +// - The removed pane, if found. +std::shared_ptr Pane::DetachPane(std::shared_ptr pane) +{ + // We can't remove a pane if we only have a reference to a leaf, even if we + // are the pane. + if (_IsLeaf()) + { + return nullptr; + } + + // Check if either of our children matches the search + const auto isFirstChild = _firstChild == pane; + const auto isSecondChild = _secondChild == pane; + + if (isFirstChild || isSecondChild) + { + // Keep a reference to the child we are removing + auto detached = isFirstChild ? _firstChild : _secondChild; + // Remove the child from the tree, replace the current node with the + // other child. + _CloseChild(isFirstChild); + + detached->_borders = Borders::None; + detached->_UpdateBorders(); + + // Trigger the detached event on each child + detached->WalkTree([](auto pane) { + pane->_PaneDetachedHandlers(pane); + return false; + }); + + return detached; + } + + if (const auto detached = _firstChild->DetachPane(pane)) + { + return detached; + } + + return _secondChild->DetachPane(pane); +} + // Method Description: // - Closes one of our children. In doing so, takes the control from the other // child, and makes this pane a leaf node again. @@ -1073,12 +1152,10 @@ void Pane::_CloseChild(const bool closeFirst) // them. _lastActive = _firstChild->_lastActive || _secondChild->_lastActive; - // Remove all the ui elements of our children. This'll make sure we can - // re-attach the TermControl to our Grid. - _firstChild->_root.Children().Clear(); - _secondChild->_root.Children().Clear(); - _firstChild->_border.Child(nullptr); - _secondChild->_border.Child(nullptr); + // 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); // Reset our UI: _root.Children().Clear(); @@ -1125,17 +1202,8 @@ void Pane::_CloseChild(const bool closeFirst) } else { - // Determine which border flag we gave to the child when we first split - // it, so that we can take just that flag away from them. - Borders clearBorderFlag = Borders::None; - if (_splitState == SplitState::Horizontal) - { - clearBorderFlag = closeFirst ? Borders::Top : Borders::Bottom; - } - else if (_splitState == SplitState::Vertical) - { - clearBorderFlag = closeFirst ? Borders::Left : Borders::Right; - } + // Find what borders need to persist after we close the child + auto remainingBorders = _GetCommonBorders(); // First stash away references to the old panes and their tokens const auto oldFirstToken = _firstClosedToken; @@ -1192,13 +1260,9 @@ void Pane::_CloseChild(const bool closeFirst) _root.Children().Append(_firstChild->GetRootElement()); _root.Children().Append(_secondChild->GetRootElement()); - // Take the flag away from the children that they inherited from their - // parent, and update their borders to visually match - WI_ClearAllFlags(_firstChild->_borders, clearBorderFlag); - WI_ClearAllFlags(_secondChild->_borders, clearBorderFlag); - _UpdateBorders(); - _firstChild->_UpdateBorders(); - _secondChild->_UpdateBorders(); + // 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) @@ -1757,7 +1821,8 @@ std::pair, std::shared_ptr> Pane::Split(SplitState s return { nullptr, nullptr }; } - return _Split(splitType, splitSize, profile, control); + auto newPane = std::make_shared(profile, control); + return _Split(splitType, splitSize, newPane); } // Method Description: @@ -1827,14 +1892,13 @@ SplitState Pane::_convertAutomaticSplitState(const SplitState& splitType) const // creates a new Pane to host the control, registers event handlers. // Arguments: // - splitType: what type of split we should create. -// - profile: The profile GUID to associate with the newly created pane. -// - control: A TermControl to use in the new pane. +// - splitSize: what fraction of the pane the new pane should get +// - newPane: the pane to add as a child // Return Value: // - The two newly created Panes std::pair, std::shared_ptr> Pane::_Split(SplitState splitType, const float splitSize, - const GUID& profile, - const TermControl& control) + std::shared_ptr newPane) { if (splitType == SplitState::None) { @@ -1874,7 +1938,7 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState _firstChild->_connectionState = std::exchange(_connectionState, ConnectionState::NotConnected); _profile = std::nullopt; _control = { nullptr }; - _secondChild = std::make_shared(profile, control); + _secondChild = newPane; _CreateRowColDefinitions(); @@ -2574,3 +2638,4 @@ void Pane::CollectTaskbarStates(std::vector& s DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate>); 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 34d696340..be51dfcad 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -29,6 +29,11 @@ namespace TerminalAppLocalTests class TabTests; }; +namespace winrt::TerminalApp::implementation +{ + struct TerminalTab; +} + enum class Borders : int { None = 0x0, @@ -63,7 +68,7 @@ public: void Relayout(); bool ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); - bool MovePane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool SwapPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool SwapPanes(std::shared_ptr first, std::shared_ptr second); std::pair, std::shared_ptr> Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, @@ -81,6 +86,10 @@ public: void Shutdown(); void Close(); + std::shared_ptr AttachPane(std::shared_ptr pane, + winrt::Microsoft::Terminal::Settings::Model::SplitState splitType); + std::shared_ptr DetachPane(std::shared_ptr pane); + int GetLeafPaneCount() const noexcept; void Maximize(std::shared_ptr zoomedPane); @@ -93,12 +102,38 @@ public: bool ContainsReadOnly() const; + // Method Description: + // - A helper method for ad-hoc recursion on a pane tree. Walks the pane + // tree, calling a predicate on each pane in a depth-first pattern. + // - If the predicate returns true, recursion is stopped early. + // Arguments: + // - f: The function to be applied to each pane. + // Return Value: + // - true if the predicate returned true on any pane. + template + //requires std::predicate> + bool WalkTree(F f) + { + if (f(shared_from_this())) + { + return true; + } + + 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>); DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate>); DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); + DECLARE_EVENT(Detached, _PaneDetachedHandlers, winrt::delegate>); private: struct PanePoint; @@ -143,8 +178,7 @@ private: std::pair, std::shared_ptr> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, const float splitSize, - const GUID& profile, - const winrt::Microsoft::Terminal::Control::TermControl& control); + std::shared_ptr newPane); void _CreateRowColDefinitions(); void _ApplySplitDefinitions(); @@ -274,5 +308,6 @@ private: void _AssignChildNode(std::unique_ptr& nodeField, const LayoutSizeNode* const newNode); }; + friend struct winrt::TerminalApp::implementation::TerminalTab; friend class ::TerminalAppLocalTests::TabTests; }; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 5f325b1a6..a64a239df 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -282,6 +282,16 @@ Move focus the tab at the given index + + Move focused pane to the tab at the given index + + + Move focused pane to another tab + + + An alias for the "move-pane" subcommand. + {Locked="\"move-pane\""} + Specify the size as a percentage of the parent pane. Valid values are between (0,1), exclusive. @@ -359,14 +369,10 @@ The direction to move focus in - + Swap the focused pane with the adjacent pane in the specified direction - - An alias for the "move-pane" subcommand. - {Locked="\"move-pane\""} - - + The direction to move the focused pane in diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index dd9e9dc96..62d67d260 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -95,45 +95,12 @@ namespace winrt::TerminalApp::implementation CATCH_RETURN(); // Method Description: - // - Creates a new tab with the given settings. If the tab bar is not being - // currently displayed, it will be shown. + // - Sets up state, event handlers, etc on a tab object that was just made. // Arguments: - // - profileGuid: ID to use to lookup profile settings for this connection - // - settings: the TerminalSettings object to use to create the TerminalControl with. - // - existingConnection: optionally receives a connection from the outside world instead of attempting to create one - void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, const TerminalSettingsCreateResult& settings, TerminalConnection::ITerminalConnection existingConnection) + // - newTabImpl: the uninitialized tab. + void TerminalPage::_InitializeTab(winrt::com_ptr newTabImpl) { - // Initialize the new tab - // Create a connection based on the values in our settings object if we weren't given one. - auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profileGuid, settings.DefaultSettings()); - - // If we had an `existingConnection`, then this is an inbound handoff from somewhere else. - // We need to tell it about our size information so it can match the dimensions of what - // we are about to present. - if (existingConnection) - { - connection.Resize(settings.DefaultSettings().InitialRows(), settings.DefaultSettings().InitialCols()); - } - - TerminalConnection::ITerminalConnection debugConnection{ nullptr }; - if (_settings.GlobalSettings().DebugFeaturesEnabled()) - { - const CoreWindow window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - if (bothAltsPressed) - { - std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); - } - } - - // Give term control a child of the settings so that any overrides go in the child - // This way, when we do a settings reload we just update the parent and the overrides remain - auto term = _InitControl(settings, connection); - - auto newTabImpl = winrt::make_self(profileGuid, term); + newTabImpl->Initialize(); // Add the new tab to the list of our tabs. _tabs.Append(*newTabImpl); @@ -146,7 +113,7 @@ namespace winrt::TerminalApp::implementation _UpdateTabIndices(); // Hookup our event handlers to the new terminal - _RegisterTerminalEvents(term, *newTabImpl); + _RegisterTabEvents(*newTabImpl); // Don't capture a strong ref to the tab. If the tab is removed as this // is called, we don't really care anymore about handling the event. @@ -208,10 +175,13 @@ namespace winrt::TerminalApp::implementation _tabView.TabItems().Append(tabViewItem); // Set this tab's icon to the icon from the user's profile - const auto profile = _settings.FindProfile(profileGuid); - if (profile != nullptr && !profile.Icon().empty()) + if (const auto profileGuid = newTabImpl->GetFocusedProfile()) { - newTabImpl->UpdateIcon(profile.Icon()); + const auto profile = _settings.FindProfile(profileGuid.value()); + if (profile != nullptr && !profile.Icon().empty()) + { + newTabImpl->UpdateIcon(profile.Icon()); + } } tabViewItem.PointerReleased({ this, &TerminalPage::_OnTabClick }); @@ -244,19 +214,73 @@ namespace winrt::TerminalApp::implementation } }); - if (debugConnection) // this will only be set if global debugging is on and tap is active - { - auto newControl = _InitControl(settings, debugConnection); - _RegisterTerminalEvents(newControl, *newTabImpl); - // Split (auto) with the debug tap. - newTabImpl->SplitPane(SplitState::Automatic, 0.5f, profileGuid, newControl); - } - // This kicks off TabView::SelectionChanged, in response to which // we'll attach the terminal's Xaml control to the Xaml root. _tabView.SelectedItem(tabViewItem); } + // Method Description: + // - Create a new tab using a specified pane as the root. + // Arguments: + // - pane: The pane to use as the root. + void TerminalPage::_CreateNewTabFromPane(std::shared_ptr pane) + { + auto newTabImpl = winrt::make_self(pane); + _InitializeTab(newTabImpl); + } + + // Method Description: + // - Creates a new tab with the given settings. If the tab bar is not being + // currently displayed, it will be shown. + // Arguments: + // - profileGuid: ID to use to lookup profile settings for this connection + // - settings: the TerminalSettings object to use to create the TerminalControl with. + // - existingConnection: optionally receives a connection from the outside world instead of attempting to create one + void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, const TerminalSettingsCreateResult& settings, TerminalConnection::ITerminalConnection existingConnection) + { + // Initialize the new tab + // Create a connection based on the values in our settings object if we weren't given one. + auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profileGuid, settings.DefaultSettings()); + + // If we had an `existingConnection`, then this is an inbound handoff from somewhere else. + // We need to tell it about our size information so it can match the dimensions of what + // we are about to present. + if (existingConnection) + { + connection.Resize(settings.DefaultSettings().InitialRows(), settings.DefaultSettings().InitialCols()); + } + + TerminalConnection::ITerminalConnection debugConnection{ nullptr }; + if (_settings.GlobalSettings().DebugFeaturesEnabled()) + { + const CoreWindow window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + if (bothAltsPressed) + { + std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); + } + } + + // Give term control a child of the settings so that any overrides go in the child + // This way, when we do a settings reload we just update the parent and the overrides remain + auto term = _InitControl(settings, connection); + + auto newTabImpl = winrt::make_self(profileGuid, term); + _RegisterTerminalEvents(term); + _InitializeTab(newTabImpl); + + if (debugConnection) // this will only be set if global debugging is on and tap is active + { + auto newControl = _InitControl(settings, debugConnection); + _RegisterTerminalEvents(newControl); + // Split (auto) with the debug tap. + newTabImpl->SplitPane(SplitState::Automatic, 0.5f, profileGuid, newControl); + } + } + // Method Description: // - Get the icon of the currently focused terminal control, and set its // tab's icon to that icon. diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 4d49779ad..67f30992c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -980,11 +980,9 @@ namespace winrt::TerminalApp::implementation // handle. This includes: // * the Copy and Paste events, for setting and retrieving clipboard data // on the right thread - // * the TitleChanged event, for changing the text of the tab // Arguments: // - term: The newly created TermControl to connect the events for - // - hostingTab: The Tab that's hosting this TermControl instance - void TerminalPage::_RegisterTerminalEvents(TermControl term, TerminalTab& hostingTab) + void TerminalPage::_RegisterTerminalEvents(TermControl term) { term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler }); @@ -999,10 +997,20 @@ namespace winrt::TerminalApp::implementation term.HidePointerCursor({ get_weak(), &TerminalPage::_HidePointerCursorHandler }); term.RestorePointerCursor({ get_weak(), &TerminalPage::_RestorePointerCursorHandler }); + // Add an event handler for when the terminal or tab wants to set a + // progress indicator on the taskbar + term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); + } - // Bind Tab events to the TermControl and the Tab's Pane - hostingTab.Initialize(term); - + // Method Description: + // - Connects event handlers to the TerminalTab for events that we want to + // handle. This includes: + // * the TitleChanged event, for changing the text of the tab + // * the Color{Selected,Cleared} events to change the color of a tab. + // Arguments: + // - hostingTab: The Tab that's hosting this TermControl instance + void TerminalPage::_RegisterTabEvents(TerminalTab& hostingTab) + { auto weakTab{ hostingTab.get_weak() }; auto weakThis{ get_weak() }; // PropertyChanged is the generic mechanism by which the Tab @@ -1054,7 +1062,6 @@ namespace winrt::TerminalApp::implementation // Add an event handler for when the terminal or tab wants to set a // progress indicator on the taskbar hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); - term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); // TODO GH#3327: Once we support colorizing the NewTab button based on // the color of the tab, we'll want to make sure to call @@ -1115,17 +1122,17 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Attempt to swap the positions of the focused pane with another pane. - // See Pane::MovePane for details. + // See Pane::SwapPane for details. // Arguments: // - direction: The direction to move the focused pane in. // Return Value: // - - void TerminalPage::_MovePane(const FocusDirection& direction) + void TerminalPage::_SwapPane(const FocusDirection& direction) { if (const auto terminalTab{ _GetFocusedTabImpl() }) { _UnZoomIfNeeded(); - terminalTab->MovePane(direction); + terminalTab->SwapPane(direction); } } @@ -1188,6 +1195,52 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - Moves the currently active pane on the currently active tab to the + // specified tab. If the tab index is greater than the number of + // tabs, then a new tab will be created for the pane. Similarly, if a pane + // is the last remaining pane on a tab, that tab will be closed upon moving. + // Arguments: + // - tabIdx: The target tab index. + bool TerminalPage::_MovePane(const uint32_t tabIdx) + { + auto focusedTab{ _GetFocusedTabImpl() }; + + if (!focusedTab) + { + return false; + } + + // If we are trying to move from the current tab to the current tab do nothing. + if (_GetFocusedTabIndex() == tabIdx) + { + return false; + } + + // Moving the pane from the current tab might close it, so get the next + // tab before its index changes. + if (_tabs.Size() > tabIdx) + { + auto targetTab = _GetTerminalTabImpl(_tabs.GetAt(tabIdx)); + // if the selected tab is not a host of terminals (e.g. settings) + // don't attempt to add a pane to it. + if (!targetTab) + { + return false; + } + auto pane = focusedTab->DetachPane(); + targetTab->AttachPane(pane); + _SetFocusedTab(*targetTab); + } + else + { + auto pane = focusedTab->DetachPane(); + _CreateNewTabFromPane(pane); + } + + return true; + } + // Method Description: // - Split the focused pane either horizontally or vertically, and place the // given TermControl into the newly created pane. @@ -1306,7 +1359,7 @@ namespace winrt::TerminalApp::implementation auto newControl = _InitControl(controlSettings, controlConnection); // Hookup our event handlers to the new terminal - _RegisterTerminalEvents(newControl, tab); + _RegisterTerminalEvents(newControl); _UnZoomIfNeeded(); diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 6ec7bd7a0..c7996bb1b 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -188,6 +188,7 @@ namespace winrt::TerminalApp::implementation void _CreateNewTabFlyout(); void _OpenNewTabDropdown(); HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + void _CreateNewTabFromPane(std::shared_ptr pane); void _CreateNewTabFromSettings(GUID profileGuid, const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(GUID profileGuid, Microsoft::Terminal::Settings::Model::TerminalSettings settings); @@ -225,7 +226,9 @@ namespace winrt::TerminalApp::implementation void _RemoveTab(const winrt::TerminalApp::TabBase& tab); winrt::fire_and_forget _RemoveTabs(const std::vector tabs); - void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term, TerminalTab& hostingTab); + void _InitializeTab(winrt::com_ptr newTabImpl); + void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); + void _RegisterTabEvents(TerminalTab& hostingTab); void _DismissTabContextMenus(); void _FocusCurrentTab(const bool focusAlways); @@ -236,7 +239,8 @@ namespace winrt::TerminalApp::implementation void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); bool _SelectTab(uint32_t tabIndex); bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); - void _MovePane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + void _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _MovePane(const uint32_t tabIdx); winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); std::optional _GetFocusedTabIndex() const noexcept; diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 48cf05d33..7896cd851 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -30,14 +30,61 @@ namespace winrt::TerminalApp::implementation _rootPane = std::make_shared(profile, control, true); _rootPane->Id(_nextPaneId); + _activePane = _rootPane; _mruPanes.insert(_mruPanes.begin(), _nextPaneId); ++_nextPaneId; - _rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) { + _Setup(); + } + + TerminalTab::TerminalTab(std::shared_ptr rootPane) + { + _rootPane = rootPane; + _activePane = nullptr; + + auto firstId = _nextPaneId; + + _rootPane->WalkTree([&](std::shared_ptr pane) { + // update the IDs on each pane + if (pane->_IsLeaf()) + { + pane->Id(_nextPaneId); + _nextPaneId++; + } + // Try to find the pane marked active (if it exists) + if (pane->_lastActive) + { + _activePane = pane; + } + + return false; + }); + + // In case none of the panes were already marked as the focus, just + // focus the first one. + if (_activePane == nullptr) + { + _rootPane->FocusPane(firstId); + _activePane = _rootPane->GetActivePane(); + } + // Set the active control + _mruPanes.insert(_mruPanes.begin(), _activePane->Id().value()); + + _Setup(); + } + + // Method Description: + // - Shared setup for the constructors. Assumed that _rootPane has been set. + // Arguments: + // - + // Return Value: + // - + void TerminalTab::_Setup() + { + _rootClosedToken = _rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) { _ClosedHandlers(nullptr, nullptr); }); - _activePane = _rootPane; Content(_rootPane->GetRootElement()); _MakeTabViewItem(); @@ -144,19 +191,31 @@ namespace winrt::TerminalApp::implementation // that was last focused. TermControl TerminalTab::GetActiveTerminalControl() const { - return _activePane->GetTerminalControl(); + if (_activePane) + { + return _activePane->GetTerminalControl(); + } + return nullptr; } // Method Description: // - Called after construction of a Tab object to bind event handlers to its - // associated Pane and TermControl object + // associated Pane and TermControl objects // Arguments: - // - control: reference to the TermControl object to bind event to + // - // Return Value: // - - void TerminalTab::Initialize(const TermControl& control) + void TerminalTab::Initialize() { - _BindEventHandlers(control); + _rootPane->WalkTree([&](std::shared_ptr pane) { + // Attach event handlers to each new pane + _AttachEventHandlersToPane(pane); + if (auto control = pane->GetTerminalControl()) + { + _AttachEventHandlersToControl(pane->Id().value(), control); + } + return false; + }); } // Method Description: @@ -203,19 +262,6 @@ namespace winrt::TerminalApp::implementation return _activePane->GetFocusedProfile(); } - // Method Description: - // - Called after construction of a Tab object to bind event handlers to its - // associated Pane and TermControl object - // Arguments: - // - control: reference to the TermControl object to bind event to - // Return Value: - // - - void TerminalTab::_BindEventHandlers(const TermControl& control) noexcept - { - _AttachEventHandlersToPane(_rootPane); - _AttachEventHandlersToControl(control); - } - // Method Description: // - Attempts to update the settings of this tab's tree of panes. // Arguments: @@ -425,10 +471,10 @@ namespace winrt::TerminalApp::implementation ++_nextPaneId; } _activePane = first; - _AttachEventHandlersToControl(control); // Add a event handlers to the new panes' GotFocus event. When the pane // gains focus, we'll mark it as the new active pane. + _AttachEventHandlersToControl(second->Id().value(), control); _AttachEventHandlersToPane(first); _AttachEventHandlersToPane(second); @@ -439,11 +485,121 @@ namespace winrt::TerminalApp::implementation _UpdateActivePane(second); } + // Method Description: + // - Removes the currently active pane from this tab. If that was the only + // remaining pane, then the entire tab is closed as well. + // Arguments: + // - + // Return Value: + // - The removed pane. + std::shared_ptr TerminalTab::DetachPane() + { + // if we only have one pane, remove it entirely + // and close this tab + if (_rootPane == _activePane) + { + return DetachRoot(); + } + + // Attempt to remove the active pane from the tree + if (const auto pane = _rootPane->DetachPane(_activePane)) + { + // Just make sure that the remaining pane is marked active + _UpdateActivePane(_rootPane->GetActivePane()); + + return pane; + } + + return nullptr; + } + + // Method Description: + // - Closes this tab and returns the root pane to be used elsewhere. + // Arguments: + // - + // Return Value: + // - The root pane. + std::shared_ptr TerminalTab::DetachRoot() + { + // remove the closed event handler since we are closing the tab + // manually. + _rootPane->Closed(_rootClosedToken); + auto p = _rootPane; + p->WalkTree([](auto pane) { + pane->_PaneDetachedHandlers(pane); + return false; + }); + + // Clean up references and close the tab + _rootPane = nullptr; + _activePane = nullptr; + Content(nullptr); + _ClosedHandlers(nullptr, nullptr); + + return p; + } + + // Method Description: + // - Add an arbitrary pane to this tab. This will be added as a split on the + // currently active pane. + // Arguments: + // - pane: The pane to add. + // Return Value: + // - + void TerminalTab::AttachPane(std::shared_ptr pane) + { + // Add the new event handlers to the new pane(s) + // and update their ids. + pane->WalkTree([&](auto p) { + _AttachEventHandlersToPane(p); + if (p->_IsLeaf()) + { + p->Id(_nextPaneId); + _nextPaneId++; + } + if (auto control = p->GetTerminalControl()) + { + _AttachEventHandlersToControl(p->Id().value(), control); + } + return false; + }); + + // pass the old id to the new child + const auto previousId = _activePane->Id(); + + // Add the new pane as an automatic split on the active pane. + auto first = _activePane->AttachPane(pane, SplitState::Automatic); + + // under current assumptions this condition should always be true. + if (previousId) + { + first->Id(previousId.value()); + } + else + { + first->Id(_nextPaneId); + ++_nextPaneId; + } + + // Update with event handlers on the new child. + _activePane = first; + _AttachEventHandlersToPane(first); + + // Make sure that we have the right pane set as the active pane + pane->WalkTree([&](auto p) { + if (p->_lastActive) + { + _UpdateActivePane(p); + return true; + } + return false; + }); + } + // Method Description: // - Find the currently active pane, and then switch the split direction of // its parent. E.g. switch from Horizontal to Vertical. // Return Value: - // - void TerminalTab::ToggleSplitOrientation() { @@ -520,7 +676,7 @@ namespace winrt::TerminalApp::implementation // - direction: The direction to move the pane in. // Return Value: // - - void TerminalTab::MovePane(const FocusDirection& direction) + void TerminalTab::SwapPane(const FocusDirection& direction) { if (direction == FocusDirection::Previous) { @@ -537,7 +693,7 @@ namespace winrt::TerminalApp::implementation { // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. - _rootPane->MovePane(direction); + _rootPane->SwapPane(direction); } } @@ -550,7 +706,10 @@ namespace winrt::TerminalApp::implementation // - Prepares this tab for being removed from the UI hierarchy by shutting down all active connections. void TerminalTab::Shutdown() { - _rootPane->Shutdown(); + if (_rootPane) + { + _rootPane->Shutdown(); + } } // Method Description: @@ -595,6 +754,34 @@ namespace winrt::TerminalApp::implementation _headerControl.BeginRename(); } + // Method Description: + // - Removes any event handlers set by the tab on the given pane's control. + // The pane's ID is the most stable identifier for a given control, because + // the control itself doesn't have a particular ID and its pointer is + // unstable since it is moved when panes split. + // Arguments: + // - paneId: The ID of the pane that contains the given control. + // - control: the control to remove events from. + // Return Value: + // - + void TerminalTab::_DetachEventHandlersFromControl(const uint32_t paneId, const TermControl& control) + { + auto it = _controlEvents.find(paneId); + if (it != _controlEvents.end()) + { + auto& events = it->second; + + control.TitleChanged(events.titleToken); + control.FontSizeChanged(events.fontToken); + control.TabColorChanged(events.colorToken); + control.SetTaskbarProgress(events.taskbarToken); + control.ReadOnlyChanged(events.readOnlyToken); + control.FocusFollowMouseRequested(events.focusToken); + + _controlEvents.erase(paneId); + } + } + // Method Description: // - Register any event handlers that we may need with the given TermControl. // This should be called on each and every TermControl that we add to the tree @@ -602,15 +789,17 @@ namespace winrt::TerminalApp::implementation // * notify us when the control's title changed, so we can update our own // title (if necessary) // Arguments: + // - paneId: the ID of the pane that this control belongs to. // - control: the TermControl to add events to. // Return Value: // - - void TerminalTab::_AttachEventHandlersToControl(const TermControl& control) + void TerminalTab::_AttachEventHandlersToControl(const uint32_t paneId, const TermControl& control) { auto weakThis{ get_weak() }; auto dispatcher = TabViewItem().Dispatcher(); + ControlEventTokens events{}; - control.TitleChanged([weakThis](auto&&, auto&&) { + events.titleToken = control.TitleChanged([weakThis](auto&&, auto&&) { // Check if Tab's lifetime has expired if (auto tab{ weakThis.get() }) { @@ -625,16 +814,16 @@ namespace winrt::TerminalApp::implementation // On the latter event, we tell the root pane to resize itself so that its descendants // (including ourself) can properly snap to character grids. In future, we may also // want to do that on regular font changes. - control.FontSizeChanged([this](const int /* fontWidth */, - const int /* fontHeight */, - const bool isInitialChange) { + events.fontToken = control.FontSizeChanged([this](const int /* fontWidth */, + const int /* fontHeight */, + const bool isInitialChange) { if (isInitialChange) { _rootPane->Relayout(); } }); - control.TabColorChanged([weakThis](auto&&, auto&&) { + events.colorToken = control.TabColorChanged([weakThis](auto&&, auto&&) { if (auto tab{ weakThis.get() }) { // The control's tabColor changed, but it is not necessarily the @@ -644,7 +833,7 @@ namespace winrt::TerminalApp::implementation } }); - control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto &&) -> winrt::fire_and_forget { + events.taskbarToken = control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto &&) -> winrt::fire_and_forget { co_await winrt::resume_foreground(dispatcher); // Check if Tab's lifetime has expired if (auto tab{ weakThis.get() }) @@ -653,14 +842,14 @@ namespace winrt::TerminalApp::implementation } }); - control.ReadOnlyChanged([weakThis](auto&&, auto&&) { + events.readOnlyToken = control.ReadOnlyChanged([weakThis](auto&&, auto&&) { if (auto tab{ weakThis.get() }) { tab->_RecalculateAndApplyReadOnly(); } }); - control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) { + events.focusToken = control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) { if (const auto tab{ weakThis.get() }) { if (tab->_focusState != FocusState::Unfocused) @@ -672,6 +861,8 @@ namespace winrt::TerminalApp::implementation } } }); + + _controlEvents[paneId] = events; } // Method Description: @@ -689,7 +880,10 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::TaskbarState TerminalTab::GetCombinedTaskbarState() const { std::vector states; - _rootPane->CollectTaskbarStates(states); + if (_rootPane) + { + _rootPane->CollectTaskbarStates(states); + } return states.empty() ? winrt::make() : *std::min_element(states.begin(), states.end(), TerminalApp::implementation::TaskbarState::ComparePriority); } @@ -798,7 +992,7 @@ namespace winrt::TerminalApp::implementation auto weakThis{ get_weak() }; std::weak_ptr weakPane{ pane }; - pane->GotFocus([weakThis](std::shared_ptr sender) { + auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr sender) { // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; @@ -818,7 +1012,7 @@ namespace winrt::TerminalApp::implementation } }); - pane->LostFocus([weakThis](std::shared_ptr /*sender*/) { + auto lostFocusToken = pane->LostFocus([weakThis](std::shared_ptr /*sender*/) { // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; @@ -832,7 +1026,7 @@ namespace winrt::TerminalApp::implementation // Add a Closed event handler to the Pane. If the pane closes out from // underneath us, and it's zoomed, we want to be able to make sure to // update our state accordingly to un-zoom that pane. See GH#7252. - pane->Closed([weakThis, weakPane](auto&& /*s*/, auto && /*e*/) -> winrt::fire_and_forget { + auto closedToken = pane->Closed([weakThis, weakPane](auto&& /*s*/, auto && /*e*/) -> winrt::fire_and_forget { if (auto tab{ weakThis.get() }) { if (tab->_zoomedPane) @@ -857,7 +1051,7 @@ namespace winrt::TerminalApp::implementation }); // Add a PaneRaiseBell event handler to the Pane - pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) { + auto bellToken = pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) { if (auto tab{ weakThis.get() }) { if (visual) @@ -879,6 +1073,40 @@ namespace winrt::TerminalApp::implementation } } }); + + // box the event token so that we can give a reference to it in the + // event handler. + auto detachedToken = std::make_shared(); + // Add a Detached event handler to the Pane to clean up tab state + // and other event handlers when a pane is removed from this tab. + *detachedToken = pane->Detached([weakThis, weakPane, gotFocusToken, lostFocusToken, closedToken, bellToken, detachedToken](std::shared_ptr /*sender*/) { + // Make sure we do this at most once + if (auto pane{ weakPane.lock() }) + { + pane->Detached(*detachedToken); + pane->GotFocus(gotFocusToken); + pane->LostFocus(lostFocusToken); + pane->Closed(closedToken); + pane->PaneRaiseBell(bellToken); + + if (auto tab{ weakThis.get() }) + { + if (auto control = pane->GetTerminalControl()) + { + tab->_DetachEventHandlersFromControl(pane->Id().value(), control); + } + + for (auto i = tab->_mruPanes.begin(); i != tab->_mruPanes.end(); ++i) + { + if (*i == pane->Id()) + { + tab->_mruPanes.erase(i); + break; + } + } + } + } + }); } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 6a9c7808c..bafa93951 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -22,9 +22,10 @@ namespace winrt::TerminalApp::implementation { public: TerminalTab(const GUID& profile, const winrt::Microsoft::Terminal::Control::TermControl& control); + TerminalTab(std::shared_ptr rootPane); // Called after construction to perform the necessary setup, which relies on weak_ptr - void Initialize(const winrt::Microsoft::Terminal::Control::TermControl& control); + void Initialize(); winrt::Microsoft::Terminal::Control::TermControl GetActiveTerminalControl() const; std::optional GetFocusedProfile() const noexcept; @@ -33,6 +34,10 @@ namespace winrt::TerminalApp::implementation winrt::fire_and_forget Scroll(const int delta); + std::shared_ptr DetachRoot(); + std::shared_ptr DetachPane(); + void AttachPane(std::shared_ptr pane); + void SplitPane(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, const float splitSize, const GUID& profile, @@ -54,7 +59,7 @@ namespace winrt::TerminalApp::implementation void ResizeContent(const winrt::Windows::Foundation::Size& newSize); void ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); - void MovePane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + void SwapPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool FocusPane(const uint32_t id); void UpdateSettings(const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, const GUID& profile); @@ -102,6 +107,7 @@ namespace winrt::TerminalApp::implementation std::shared_ptr _rootPane{ nullptr }; std::shared_ptr _activePane{ nullptr }; std::shared_ptr _zoomedPane{ nullptr }; + winrt::hstring _lastIconPath{}; winrt::TerminalApp::ColorPickupFlyout _tabColorPickup{}; std::optional _themeTabColor{}; @@ -109,6 +115,19 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::TabHeaderControl _headerControl{}; winrt::TerminalApp::TerminalTabStatus _tabStatus{}; + struct ControlEventTokens + { + winrt::event_token titleToken; + winrt::event_token fontToken; + winrt::event_token colorToken; + winrt::event_token taskbarToken; + winrt::event_token readOnlyToken; + winrt::event_token focusToken; + }; + std::unordered_map _controlEvents; + + winrt::event_token _rootClosedToken{}; + std::vector _mruPanes; uint32_t _nextPaneId{ 0 }; @@ -121,6 +140,8 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::ShortcutActionDispatch _dispatch; + void _Setup(); + std::optional _bellIndicatorTimer; void _BellIndicatorTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); @@ -133,9 +154,8 @@ namespace winrt::TerminalApp::implementation void _RefreshVisualState(); - void _BindEventHandlers(const winrt::Microsoft::Terminal::Control::TermControl& control) noexcept; - - void _AttachEventHandlersToControl(const winrt::Microsoft::Terminal::Control::TermControl& control); + void _DetachEventHandlersFromControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control); + void _AttachEventHandlersToControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control); void _AttachEventHandlersToPane(std::shared_ptr pane); void _UpdateActivePane(std::shared_ptr pane); diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 0bd4b174a..acef483be 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -21,6 +21,7 @@ static constexpr std::string_view ExecuteCommandlineKey{ "wt" }; static constexpr std::string_view FindKey{ "find" }; static constexpr std::string_view MoveFocusKey{ "moveFocus" }; static constexpr std::string_view MovePaneKey{ "movePane" }; +static constexpr std::string_view SwapPaneKey{ "swapPane" }; static constexpr std::string_view NewTabKey{ "newTab" }; static constexpr std::string_view NextTabKey{ "nextTab" }; static constexpr std::string_view OpenNewTabDropdownKey{ "openNewTabDropdown" }; @@ -322,6 +323,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::Invalid, L"" }, { ShortcutAction::MoveFocus, RS_(L"MoveFocusCommandKey") }, { ShortcutAction::MovePane, RS_(L"MovePaneCommandKey") }, + { ShortcutAction::SwapPane, RS_(L"SwapPaneCommandKey") }, { ShortcutAction::NewTab, RS_(L"NewTabCommandKey") }, { ShortcutAction::NextTab, RS_(L"NextTabCommandKey") }, { ShortcutAction::OpenNewTabDropdown, RS_(L"OpenNewTabDropdownCommandKey") }, diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 9baaaf240..d422bf2e2 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -13,6 +13,7 @@ #include "ResizePaneArgs.g.cpp" #include "MoveFocusArgs.g.cpp" #include "MovePaneArgs.g.cpp" +#include "SwapPaneArgs.g.cpp" #include "AdjustFontSizeArgs.g.cpp" #include "SendInputArgs.g.cpp" #include "SplitPaneArgs.g.cpp" @@ -229,6 +230,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation }; } + winrt::hstring MovePaneArgs::GenerateName() const + { + return winrt::hstring{ + fmt::format(L"{}, tab index:{}", RS_(L"MovePaneCommandKey"), TabIndex()) + }; + } + winrt::hstring SwitchToTabArgs::GenerateName() const { if (TabIndex() == UINT32_MAX) @@ -291,7 +299,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation }; } - winrt::hstring MovePaneArgs::GenerateName() const + winrt::hstring SwapPaneArgs::GenerateName() const { winrt::hstring directionString; switch (Direction()) @@ -309,10 +317,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation directionString = RS_(L"DirectionDown"); break; case FocusDirection::Previous: - return RS_(L"MovePaneToLastUsedPane"); + return RS_(L"SwapPaneToLastUsedPane"); } return winrt::hstring{ - fmt::format(std::wstring_view(RS_(L"MovePaneWithArgCommandKey")), + fmt::format(std::wstring_view(RS_(L"SwapPaneWithArgCommandKey")), directionString) }; } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 4861ea805..1d67ef809 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -13,6 +13,7 @@ #include "ResizePaneArgs.g.h" #include "MoveFocusArgs.g.h" #include "MovePaneArgs.g.h" +#include "SwapPaneArgs.g.h" #include "AdjustFontSizeArgs.g.h" #include "SendInputArgs.g.h" #include "SplitPaneArgs.g.h" @@ -286,6 +287,57 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } }; + struct MovePaneArgs : public MovePaneArgsT + { + MovePaneArgs() = default; + MovePaneArgs(uint32_t& tabIndex) : + _TabIndex{ tabIndex } {}; + ACTION_ARG(uint32_t, TabIndex, 0); + + static constexpr std::string_view TabIndexKey{ "index" }; + + public: + hstring GenerateName() const; + + bool Equals(const IActionArgs& other) + { + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_TabIndex == _TabIndex; + } + return false; + }; + static FromJsonResult FromJson(const Json::Value& json) + { + // LOAD BEARING: Not using make_self here _will_ break you in the future! + auto args = winrt::make_self(); + JsonUtils::GetValueForKey(json, TabIndexKey, args->_TabIndex); + return { *args, {} }; + } + static Json::Value ToJson(const IActionArgs& val) + { + if (!val) + { + return {}; + } + Json::Value json{ Json::ValueType::objectValue }; + const auto args{ get_self(val) }; + JsonUtils::SetValueForKey(json, TabIndexKey, args->_TabIndex); + return json; + } + IActionArgs Copy() const + { + auto copy{ winrt::make_self() }; + copy->_TabIndex = _TabIndex; + return *copy; + } + size_t Hash() const + { + return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(TabIndex()); + } + }; + struct SwitchToTabArgs : public SwitchToTabArgsT { SwitchToTabArgs() = default; @@ -452,10 +504,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } }; - struct MovePaneArgs : public MovePaneArgsT + struct SwapPaneArgs : public SwapPaneArgsT { - MovePaneArgs() = default; - MovePaneArgs(Model::FocusDirection direction) : + SwapPaneArgs() = default; + SwapPaneArgs(Model::FocusDirection direction) : _Direction{ direction } {}; ACTION_ARG(Model::FocusDirection, Direction, FocusDirection::None); @@ -467,7 +519,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation bool Equals(const IActionArgs& other) { - auto otherAsUs = other.try_as(); + auto otherAsUs = other.try_as(); if (otherAsUs) { return otherAsUs->_Direction == _Direction; @@ -477,7 +529,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static FromJsonResult FromJson(const Json::Value& json) { // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); + auto args = winrt::make_self(); JsonUtils::GetValueForKey(json, DirectionKey, args->_Direction); if (args->Direction() == FocusDirection::None) { @@ -495,13 +547,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return {}; } Json::Value json{ Json::ValueType::objectValue }; - const auto args{ get_self(val) }; + const auto args{ get_self(val) }; JsonUtils::SetValueForKey(json, DirectionKey, args->_Direction); return json; } IActionArgs Copy() const { - auto copy{ winrt::make_self() }; + auto copy{ winrt::make_self() }; copy->_Direction = _Direction; return *copy; } @@ -1708,6 +1760,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(NewTabArgs); BASIC_FACTORY(MoveFocusArgs); BASIC_FACTORY(MovePaneArgs); + BASIC_FACTORY(SwapPaneArgs); BASIC_FACTORY(SplitPaneArgs); BASIC_FACTORY(SetColorSchemeArgs); BASIC_FACTORY(ExecuteCommandlineArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 9e418a335..25e765f64 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -141,6 +141,12 @@ namespace Microsoft.Terminal.Settings.Model NewTerminalArgs TerminalArgs { get; }; }; + [default_interface] runtimeclass MovePaneArgs : IActionArgs + { + MovePaneArgs(UInt32 tabIndex); + UInt32 TabIndex; + }; + [default_interface] runtimeclass SwitchToTabArgs : IActionArgs { SwitchToTabArgs(UInt32 tabIndex); @@ -158,9 +164,9 @@ namespace Microsoft.Terminal.Settings.Model FocusDirection FocusDirection { get; }; }; - [default_interface] runtimeclass MovePaneArgs : IActionArgs + [default_interface] runtimeclass SwapPaneArgs : IActionArgs { - MovePaneArgs(FocusDirection direction); + SwapPaneArgs(FocusDirection direction); FocusDirection Direction { get; }; }; diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index 71c74fccb..7d0d0a728 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -50,6 +50,7 @@ ON_ALL_ACTIONS(ResizePane) \ ON_ALL_ACTIONS(MoveFocus) \ ON_ALL_ACTIONS(MovePane) \ + ON_ALL_ACTIONS(SwapPane) \ ON_ALL_ACTIONS(Find) \ ON_ALL_ACTIONS(ToggleShaderEffects) \ ON_ALL_ACTIONS(ToggleFocusMode) \ @@ -90,6 +91,7 @@ ON_ALL_ACTIONS_WITH_ARGS(GlobalSummon) \ ON_ALL_ACTIONS_WITH_ARGS(MoveFocus) \ ON_ALL_ACTIONS_WITH_ARGS(MovePane) \ + ON_ALL_ACTIONS_WITH_ARGS(SwapPane) \ ON_ALL_ACTIONS_WITH_ARGS(MoveTab) \ ON_ALL_ACTIONS_WITH_ARGS(NewTab) \ ON_ALL_ACTIONS_WITH_ARGS(NewWindow) \ diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index ad8762ed3..4ea614c25 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -246,15 +246,15 @@ Move focus to the last used pane - - Move pane + + Swap pane - - Move pane {0} + + Swap pane {0} {0} will be replaced with one of the four directions "DirectionLeft", "DirectionRight", "DirectionUp", "DirectionDown" - - Move pane to the last used pane + + Swap panes with the last used pane New tab @@ -356,6 +356,9 @@ Split pane horizontally + + Move pane + Split pane diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 6d8cdceb7..8cfb25cd3 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -344,14 +344,23 @@ { "command": { "action": "moveFocus", "direction": "right" }, "keys": "alt+right" }, { "command": { "action": "moveFocus", "direction": "up" }, "keys": "alt+up" }, { "command": { "action": "moveFocus", "direction": "previous" }, "keys": "ctrl+alt+left"}, - { "command": { "action": "movePane", "direction": "down" } }, - { "command": { "action": "movePane", "direction": "left" } }, - { "command": { "action": "movePane", "direction": "right" } }, - { "command": { "action": "movePane", "direction": "up" } }, - { "command": { "action": "movePane", "direction": "previous"} }, + { "command": { "action": "swapPane", "direction": "down" } }, + { "command": { "action": "swapPane", "direction": "left" } }, + { "command": { "action": "swapPane", "direction": "right" } }, + { "command": { "action": "swapPane", "direction": "up" } }, + { "command": { "action": "swapPane", "direction": "previous"} }, { "command": "togglePaneZoom" }, { "command": "toggleSplitOrientation" }, { "command": "toggleReadOnlyMode" }, + { "command": { "action": "movePane", "index": 0 } }, + { "command": { "action": "movePane", "index": 1 } }, + { "command": { "action": "movePane", "index": 2 } }, + { "command": { "action": "movePane", "index": 3 } }, + { "command": { "action": "movePane", "index": 4 } }, + { "command": { "action": "movePane", "index": 5 } }, + { "command": { "action": "movePane", "index": 6 } }, + { "command": { "action": "movePane", "index": 7 } }, + { "command": { "action": "movePane", "index": 8 } }, // Clipboard Integration { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+shift+c" }, From f1dc649135b1b798fdf121d69c67bfa27e8206bf Mon Sep 17 00:00:00 2001 From: Don-Vito Date: Thu, 12 Aug 2021 19:47:16 +0300 Subject: [PATCH 62/90] Fix WriteUTF8FileAtomic to preserve symlinks (#10908) WriteUTF8FileAtomic overrides the content of the file "atomically" by creating a temp file and then renaming it to the original path. The problem arises when the original path is symbolic link, as the link itself gets overridden by a file (rather than the link target). This PR introduces a special handling of the symlinks: if the path as a symlink we resolve the path and use: 1. target's directory to create a temp-file in 2. target itself to be replaced with the tempfile. Symlink resolution is problematic when the target path does not exist, as there is no good utility that resolves such link (canonical() fails). In this corner case we skip the "atomic" approach of renaming the file and write the link target directly. Closes #10787 --- .github/actions/spelling/expect/expect.txt | 1 + .../TerminalSettingsModel/FileUtils.cpp | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 44f9b0d60..1ee31b9e0 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -2269,6 +2269,7 @@ SWMR SWP swprintf SYMED +symlink SYNCPAINT sys syscalls diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.cpp b/src/cascadia/TerminalSettingsModel/FileUtils.cpp index b952f6a3d..81fbb6cee 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.cpp +++ b/src/cascadia/TerminalSettingsModel/FileUtils.cpp @@ -123,7 +123,27 @@ namespace Microsoft::Terminal::Settings::Model void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content) { - auto tmpPath = path; + // GH#10787: rename() will replace symbolic links themselves and not the path they point at. + // It's thus important that we first resolve them before generating temporary path. + std::error_code ec; + const auto resolvedPath = std::filesystem::is_symlink(path) ? std::filesystem::canonical(path, ec) : path; + if (ec) + { + if (ec.value() != ERROR_FILE_NOT_FOUND) + { + THROW_WIN32_MSG(ec.value(), "failed to compute canonical path"); + } + + // The original file is a symbolic link, but the target doesn't exist. + // Consider two fall-backs: + // * resolve the link manually, which might be less accurate and more prone to race conditions + // * write to the file directly, which lets the system resolve the symbolic link but leaves the write non-atomic + // The latter is chosen, as this is an edge case and our 'atomic' writes are only best-effort. + WriteUTF8File(path, content); + return; + } + + auto tmpPath = resolvedPath; tmpPath += L".tmp"; // Writing to a file isn't atomic, but... @@ -132,6 +152,6 @@ namespace Microsoft::Terminal::Settings::Model // renaming one is (supposed to be) atomic. // Wait... "supposed to be"!? Well it's technically not always atomic, // but it's pretty darn close to it, so... better than nothing. - std::filesystem::rename(tmpPath, path); + std::filesystem::rename(tmpPath, resolvedPath); } } From d3f98590515ed8282d31258a75251320ad1954ae Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 12 Aug 2021 19:54:59 +0200 Subject: [PATCH 63/90] Improve WriteCharsLegacy performance by increasing local buffer size (#10921) Improve WriteCharsLegacy performance by increasing LocalBuffer size, allowing longer runs of characters to be submitted to the remaining parts of conhost. References #10563 -- vtebench tracking issue ## Validation Steps Performed * Ran `cat big.txt`, vtebench and termbench and noted ~5% performance improvements --- src/host/_stream.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index 917501dba..e00238e41 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -28,8 +28,6 @@ using Microsoft::Console::VirtualTerminal::StateMachine; // Used by WriteCharsLegacy. #define IS_GLYPH_CHAR(wch) (((wch) >= L' ') && ((wch) != 0x007F)) -constexpr unsigned int LOCAL_BUFFER_SIZE = 100; - // Routine Description: // - This routine updates the cursor position. Its input is the non-special // cased new location of the cursor. For example, if the cursor were being @@ -339,7 +337,6 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; COORD CursorPosition = cursor.GetPosition(); NTSTATUS Status = STATUS_SUCCESS; SHORT XPosition; - WCHAR LocalBuffer[LOCAL_BUFFER_SIZE]; size_t TempNumSpaces = 0; const bool fUnprocessed = WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT); const bool fWrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); @@ -360,6 +357,9 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; coordScreenBufferSize.X = textBuffer.GetLineWidth(CursorPosition.Y); } + static constexpr unsigned int LOCAL_BUFFER_SIZE = 1024; + WCHAR LocalBuffer[LOCAL_BUFFER_SIZE]; + while (*pcb < BufferSize) { // correct for delayed EOL From a0edb12cd6a2b3f3d074b606a85eb4587915b7e2 Mon Sep 17 00:00:00 2001 From: Leon Liang Date: Thu, 12 Aug 2021 12:54:39 -0700 Subject: [PATCH 64/90] Add Minimize to Tray and Tray Icon (#10368) A brief summary of the behavior of the tray icon: - There will only ever be one tray icon representing all windows. - Left-Click on a Tray Icon brings up the MRU window. - Right-Click on a Tray Icon brings up a Context Menu: ``` Focus Terminal ---------------- Windows --> Window ID 1 - Named Window Named Window Again ``` - Focus Terminal will bring up the MRU window. - Clicking on any of the Window "names" in the submenu will summon the window. ## Settings Changes Two new global settings are introduced: `alwaysShowTrayIcon` and `minimizeToTray`. Here's a chart explaining the behavior with the two settings. | | `alwaysShowTrayIcon:true` | `alwaysShowTrayIcon:false` | |----------------------|------------------------------------------------------------------|------------------------------------------------------------------| | `minimizeToTray:true` | tray icon is always shown. minimize button will hide the window. | tray icon is always shown. minimize button will hide the window. | | `minimizeToTray:false` | tray icon is always shown. | tray icon is not shown ever. | Closes #5727 ## References [Spec for Minimize to Tray](https://github.com/microsoft/terminal/blob/main/doc/specs/%23653%20-%20Quake%20Mode/%23653%20-%20Quake%20Mode.md#minimize-to-tray) Docs PR - MicrosoftDocs/terminal#352 #10448 - My list of TODOs --- .github/actions/spelling/allow/apis.txt | 7 + .github/actions/spelling/expect/expect.txt | 2 + doc/cascadia/profiles.schema.json | 10 + src/cascadia/Remoting/Monarch.cpp | 87 ++++++- src/cascadia/Remoting/Monarch.h | 7 +- src/cascadia/Remoting/Monarch.idl | 6 + src/cascadia/Remoting/Peasant.cpp | 39 ++- src/cascadia/Remoting/Peasant.h | 4 + src/cascadia/Remoting/Peasant.idl | 4 + .../Remoting/SummonWindowSelectionArgs.h | 2 + src/cascadia/Remoting/WindowManager.cpp | 52 ++++ src/cascadia/Remoting/WindowManager.h | 9 + src/cascadia/Remoting/WindowManager.idl | 7 + src/cascadia/TerminalApp/AppLogic.cpp | 35 +++ src/cascadia/TerminalApp/AppLogic.h | 3 + src/cascadia/TerminalApp/AppLogic.idl | 3 + .../Resources/en-US/Resources.resw | 8 + .../GlobalAppSettings.cpp | 10 + .../TerminalSettingsModel/GlobalAppSettings.h | 2 + .../GlobalAppSettings.idl | 2 + .../Resources/en-US/Resources.resw | 3 + .../TerminalSettingsModel/defaults.json | 2 + .../UnitTests_Remoting/RemotingTests.cpp | 4 + src/cascadia/WindowsTerminal/AppHost.cpp | 182 +++++++++----- src/cascadia/WindowsTerminal/AppHost.h | 15 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 39 ++- src/cascadia/WindowsTerminal/IslandWindow.h | 10 +- src/cascadia/WindowsTerminal/TrayIcon.cpp | 237 ++++++++++++++++++ src/cascadia/WindowsTerminal/TrayIcon.h | 37 +++ .../WindowsTerminal/WindowsTerminal.vcxproj | 4 +- src/cascadia/WindowsTerminal/icon.cpp | 12 +- src/cascadia/WindowsTerminal/icon.h | 2 +- src/cascadia/WindowsTerminal/main.cpp | 9 + src/features.xml | 7 + 34 files changed, 775 insertions(+), 87 deletions(-) create mode 100644 src/cascadia/WindowsTerminal/TrayIcon.cpp create mode 100644 src/cascadia/WindowsTerminal/TrayIcon.h diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 18023d517..696f9da66 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -2,11 +2,13 @@ ACCEPTFILES ACCESSDENIED alignas alignof +APPLYTOSUBMENUS bitfield bitfields BUILDBRANCH BUILDMSG BUILDNUMBER +BYPOSITION charconv CLASSNOTAVAILABLE cmdletbinding @@ -78,6 +80,9 @@ llu localtime lround LSHIFT +MENUCOMMAND +MENUDATA +MENUINFO memicmp mptt mov @@ -94,6 +99,7 @@ NOCHANGEDIR NOPROGRESS NOREDIRECTIONBITMAP NOREPEAT +NOTIFYBYPOS NOTIFYICON NOTIFYICONDATA ntprivapi @@ -141,6 +147,7 @@ Stubless Subheader Subpage syscall +TASKBARCREATED TBPF THEMECHANGED tlg diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 1ee31b9e0..8ed94f9d4 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -2801,6 +2801,7 @@ xes xff XFile XFORM +xIcon XManifest XMath XMFLOAT @@ -2833,6 +2834,7 @@ YCast YCENTER YCount YDPI +yIcon yml YOffset YPosition diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 2c736dd2f..72ca7293e 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1201,6 +1201,16 @@ "minimum": 0, "type": [ "integer", "string" ], "deprecated": true + }, + "minimizeToTray": { + "default": "false", + "description": "When set to true, minimizing a Terminal window will no longer appear in the taskbar. Instead, a Terminal icon will appear in the system tray through which the user can access their windows.", + "type": "boolean" + }, + "alwaysShowTrayIcon": { + "default": "false", + "description": "When set to true, the Terminal's tray icon will always be shown in the system tray.", + "type": "boolean" }, "actions": { "description": "Properties are specific to each custom action.", diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index a5444e3d4..05f987661 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -28,8 +28,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation CATCH_LOG(); } - // This is a private constructor to be used in unit tests, where we don't - // want each Monarch to necessarily use the current PID. + // This constructor is intended to be used in unit tests, + // but we need to make it public in order to use make_self + // in the tests. It's not exposed through the idl though + // so it's not _truly_ fully public which should be acceptable. Monarch::Monarch(const uint64_t testPID) : _ourPID{ testPID } { @@ -78,6 +80,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation peasant.IdentifyWindowsRequested({ this, &Monarch::_identifyWindows }); peasant.RenameRequested({ this, &Monarch::_renameRequested }); + peasant.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequestedHandlers(*this, nullptr); }); + peasant.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequestedHandlers(*this, nullptr); }); + _peasants[newPeasantsId] = peasant; TraceLoggingWrite(g_hRemotingProvider, @@ -738,20 +743,30 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation try { args.FoundMatch(false); + + // If a WindowID is provided from the args, use that first. uint64_t windowId = 0; - // If no name was provided, then just summon the MRU window. - if (searchedForName.empty()) + if (args.WindowID()) { - // Use the value of the `desktop` arg to determine if we should - // limit to the current desktop (desktop:onCurrent) or not - // (desktop:any or desktop:toCurrent) - windowId = _getMostRecentPeasantID(args.OnCurrentDesktop(), false); + windowId = args.WindowID().Value(); } else { - // Try to find a peasant that currently has this name - windowId = _lookupPeasantIdForName(searchedForName); + // If no name was provided, then just summon the MRU window. + if (searchedForName.empty()) + { + // Use the value of the `desktop` arg to determine if we should + // limit to the current desktop (desktop:onCurrent) or not + // (desktop:any or desktop:toCurrent) + windowId = _getMostRecentPeasantID(args.OnCurrentDesktop(), false); + } + else + { + // Try to find a peasant that currently has this name + windowId = _lookupPeasantIdForName(searchedForName); + } } + if (auto targetPeasant{ _getPeasant(windowId) }) { targetPeasant.Summon(args.SummonBehavior()); @@ -789,4 +804,56 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } } + + // Method Description: + // - This method creates a map of peasant IDs to peasant names + // while removing dead peasants. + // Arguments: + // - + // Return Value: + // - A map of peasant IDs to their names. + Windows::Foundation::Collections::IMapView Monarch::GetPeasantNames() + { + auto names = winrt::single_threaded_map(); + + std::vector peasantsToErase{}; + for (const auto& [id, p] : _peasants) + { + try + { + names.Insert(id, p.WindowName()); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + peasantsToErase.push_back(id); + } + } + + // Remove the dead peasants we came across while iterating. + for (const auto& id : peasantsToErase) + { + _peasants.erase(id); + _clearOldMruEntries(id); + } + + return names.GetView(); + } + + void Monarch::SummonAllWindows() + { + auto callback = [](auto&& p, auto&& /*id*/) { + SummonWindowBehavior args{}; + args.ToggleVisibility(false); + p.Summon(args); + }; + auto onError = [](auto&& id) { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SummonAll_Failed", + TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not summon"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + }; + _forAllPeasantsIgnoringTheDead(callback, onError); + } } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 9dc8fc77e..2aa30dab4 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -41,6 +41,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation struct Monarch : public MonarchT { Monarch(); + Monarch(const uint64_t testPID); ~Monarch(); uint64_t GetPID(); @@ -51,10 +52,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void HandleActivatePeasant(const winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs& args); void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); + void SummonAllWindows(); + Windows::Foundation::Collections::IMapView GetPeasantNames(); + TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: - Monarch(const uint64_t testPID); uint64_t _ourPID; uint64_t _nextPeasantID{ 1 }; diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index d87d780b3..f4e8bd8b4 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -28,6 +28,7 @@ namespace Microsoft.Terminal.Remoting Boolean FoundMatch; SummonWindowBehavior SummonBehavior; + Windows.Foundation.IReference WindowID; } @@ -40,6 +41,11 @@ namespace Microsoft.Terminal.Remoting void HandleActivatePeasant(WindowActivatedArgs args); void SummonWindow(SummonWindowSelectionArgs args); + void SummonAllWindows(); + Windows.Foundation.Collections.IMapView GetPeasantNames { get; }; + event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; + event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; + event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; } diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp index 58a71acba..a9787f597 100644 --- a/src/cascadia/Remoting/Peasant.cpp +++ b/src/cascadia/Remoting/Peasant.cpp @@ -20,8 +20,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { } - // This is a private constructor to be used in unit tests, where we don't - // want each Peasant to necessarily use the current PID. + // This constructor is intended to be used in unit tests, + // but we need to make it public in order to use make_self + // in the tests. It's not exposed through the idl though + // so it's not _truly_ fully public which should be acceptable. Peasant::Peasant(const uint64_t testPID) : _ourPID{ testPID } { @@ -31,6 +33,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { _id = id; } + uint64_t Peasant::GetID() { return _id; @@ -222,4 +225,36 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } + + void Peasant::RequestShowTrayIcon() + { + try + { + _ShowTrayIconRequestedHandlers(*this, nullptr); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + TraceLoggingWrite(g_hRemotingProvider, + "Peasant_RequestShowTrayIcon", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + + void Peasant::RequestHideTrayIcon() + { + try + { + _HideTrayIconRequestedHandlers(*this, nullptr); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + TraceLoggingWrite(g_hRemotingProvider, + "Peasant_RequestHideTrayIcon", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } } diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h index 7b5b44e81..6093cd835 100644 --- a/src/cascadia/Remoting/Peasant.h +++ b/src/cascadia/Remoting/Peasant.h @@ -28,6 +28,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void RequestIdentifyWindows(); void DisplayWindowId(); void RequestRename(const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args); + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs(); @@ -40,6 +42,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(DisplayWindowIdRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(RenameRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RenameRequestArgs); TYPED_EVENT(SummonRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: Peasant(const uint64_t testPID); diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index bc36e3f30..454d748cf 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -64,6 +64,8 @@ namespace Microsoft.Terminal.Remoting void RequestIdentifyWindows(); // Tells us to raise a IdentifyWindowsRequested void RequestRename(RenameRequestArgs args); // Tells us to raise a RenameRequested void Summon(SummonWindowBehavior behavior); + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); event Windows.Foundation.TypedEventHandler WindowActivated; event Windows.Foundation.TypedEventHandler ExecuteCommandlineRequested; @@ -71,6 +73,8 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler DisplayWindowIdRequested; event Windows.Foundation.TypedEventHandler RenameRequested; event Windows.Foundation.TypedEventHandler SummonRequested; + event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; + event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; [default_interface] runtimeclass Peasant : IPeasant diff --git a/src/cascadia/Remoting/SummonWindowSelectionArgs.h b/src/cascadia/Remoting/SummonWindowSelectionArgs.h index 2696925ef..efbf72a3b 100644 --- a/src/cascadia/Remoting/SummonWindowSelectionArgs.h +++ b/src/cascadia/Remoting/SummonWindowSelectionArgs.h @@ -34,6 +34,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation WINRT_PROPERTY(bool, FoundMatch, false); WINRT_PROPERTY(bool, OnCurrentDesktop, false); WINRT_PROPERTY(SummonWindowBehavior, SummonBehavior); + + WINRT_PROPERTY(Windows::Foundation::IReference, WindowID); }; } diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 88e0adbf0..551bd0930 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -254,6 +254,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // window, and when the current monarch dies. _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); + _monarch.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequestedHandlers(*this, nullptr); }); + _monarch.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequestedHandlers(*this, nullptr); }); _BecameMonarchHandlers(*this, nullptr); } @@ -509,4 +511,54 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _monarch.SummonWindow(args); } + void WindowManager::SummonAllWindows() + { + if constexpr (Feature_TrayIcon::IsEnabled()) + { + _monarch.SummonAllWindows(); + } + } + + Windows::Foundation::Collections::IMapView WindowManager::GetPeasantNames() + { + // We should only get called when we're the monarch since the monarch + // is the only one that knows about all peasants. + return _monarch.GetPeasantNames(); + } + + // Method Description: + // - Ask the monarch to show a tray icon. + // Arguments: + // - + // Return Value: + // - + void WindowManager::RequestShowTrayIcon() + { + _peasant.RequestShowTrayIcon(); + } + + // Method Description: + // - Ask the monarch to hide its tray icon. + // Arguments: + // - + // Return Value: + // - + void WindowManager::RequestHideTrayIcon() + { + _peasant.RequestHideTrayIcon(); + } + + bool WindowManager::DoesQuakeWindowExist() + { + const auto names = GetPeasantNames(); + for (const auto [id, name] : names) + { + if (name == QuakeWindowName) + { + return true; + } + } + return false; + } + } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 0b87075f9..8169f3737 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -40,8 +40,17 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation bool IsMonarch(); void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); + void SummonAllWindows(); + Windows::Foundation::Collections::IMapView GetPeasantNames(); + + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); + bool DoesQuakeWindowExist(); + TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: bool _shouldCreateWindow{ false }; diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index 547b96f9a..ca1f9f747 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -12,7 +12,14 @@ namespace Microsoft.Terminal.Remoting IPeasant CurrentWindow(); Boolean IsMonarch { get; }; void SummonWindow(SummonWindowSelectionArgs args); + void SummonAllWindows(); + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); + Boolean DoesQuakeWindowExist(); + Windows.Foundation.Collections.IMapView GetPeasantNames(); event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler BecameMonarch; + event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; + event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; } diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 420630774..106da0fbd 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -1442,4 +1442,39 @@ namespace winrt::TerminalApp::implementation return _root->IsQuakeWindow(); } + bool AppLogic::GetMinimizeToTray() + { + if constexpr (Feature_TrayIcon::IsEnabled()) + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + return _settings.GlobalSettings().MinimizeToTray(); + } + else + { + return false; + } + } + + bool AppLogic::GetAlwaysShowTrayIcon() + { + if constexpr (Feature_TrayIcon::IsEnabled()) + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + return _settings.GlobalSettings().AlwaysShowTrayIcon(); + } + else + { + return false; + } + } } diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 4131d28f1..dbaf301c3 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -92,6 +92,9 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::TaskbarState TaskbarState(); + bool GetMinimizeToTray(); + bool GetAlwaysShowTrayIcon(); + winrt::Windows::Foundation::IAsyncOperation ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog); Windows::Foundation::Collections::IMapView GlobalHotkeys(); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index 0b8891038..1f7675ad4 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -70,6 +70,9 @@ namespace TerminalApp TaskbarState TaskbarState{ get; }; + Boolean GetMinimizeToTray(); + Boolean GetAlwaysShowTrayIcon(); + FindTargetWindowResult FindTargetWindow(String[] args); Windows.Foundation.Collections.IMapView GlobalHotkeys(); diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index a64a239df..f9e8cbf12 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -653,6 +653,14 @@ Command Palette + + Focus Terminal + This is displayed as a label for the context menu item that focuses the terminal. + + + Windows + This is displayed as a label for the context menu item that holds the submenu of available windows. + Open in Windows Terminal (Dev) {Locked} The dev build will never be seen in multiple languages diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 466031b05..aeca8499d 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -48,6 +48,8 @@ static constexpr std::string_view StartupActionsKey{ "startupActions" }; static constexpr std::string_view FocusFollowMouseKey{ "focusFollowMouse" }; static constexpr std::string_view WindowingBehaviorKey{ "windowingBehavior" }; static constexpr std::string_view TrimBlockSelectionKey{ "trimBlockSelection" }; +static constexpr std::string_view AlwaysShowTrayIconKey{ "alwaysShowTrayIcon" }; +static constexpr std::string_view MinimizeToTrayKey{ "minimizeToTray" }; static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" }; @@ -129,6 +131,8 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_WindowingBehavior = _WindowingBehavior; globals->_TrimBlockSelection = _TrimBlockSelection; globals->_DetectURLs = _DetectURLs; + globals->_MinimizeToTray = _MinimizeToTray; + globals->_AlwaysShowTrayIcon = _AlwaysShowTrayIcon; globals->_UnparsedDefaultProfile = _UnparsedDefaultProfile; globals->_validDefaultProfile = _validDefaultProfile; @@ -319,6 +323,10 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, DetectURLsKey, _DetectURLs); + JsonUtils::GetValueForKey(json, MinimizeToTrayKey, _MinimizeToTray); + + JsonUtils::GetValueForKey(json, AlwaysShowTrayIconKey, _AlwaysShowTrayIcon); + // This is a helper lambda to get the keybindings and commands out of both // and array of objects. We'll use this twice, once on the legacy // `keybindings` key, and again on the newer `bindings` key. @@ -414,6 +422,8 @@ Json::Value GlobalAppSettings::ToJson() const JsonUtils::SetValueForKey(json, WindowingBehaviorKey, _WindowingBehavior); JsonUtils::SetValueForKey(json, TrimBlockSelectionKey, _TrimBlockSelection); JsonUtils::SetValueForKey(json, DetectURLsKey, _DetectURLs); + JsonUtils::SetValueForKey(json, MinimizeToTrayKey, _MinimizeToTray); + JsonUtils::SetValueForKey(json, AlwaysShowTrayIconKey, _AlwaysShowTrayIcon); // clang-format on json[JsonKey(ActionsKey)] = _actionMap->ToJson(); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index 7fa8c7878..9a1536a10 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -98,6 +98,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::GlobalAppSettings, Model::WindowingMode, WindowingBehavior, Model::WindowingMode::UseNew); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, TrimBlockSelection, false); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, DetectURLs, true); + INHERITABLE_SETTING(Model::GlobalAppSettings, bool, MinimizeToTray, false); + INHERITABLE_SETTING(Model::GlobalAppSettings, bool, AlwaysShowTrayIcon, false); private: guid _defaultProfile; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 1fac7f66b..e9d1fccad 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -73,6 +73,8 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(WindowingMode, WindowingBehavior); INHERITABLE_SETTING(Boolean, TrimBlockSelection); INHERITABLE_SETTING(Boolean, DetectURLs); + INHERITABLE_SETTING(Boolean, MinimizeToTray); + INHERITABLE_SETTING(Boolean, AlwaysShowTrayIcon); Windows.Foundation.Collections.IMapView ColorSchemes(); void AddColorScheme(ColorScheme scheme); diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 4ea614c25..7f30440e6 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -436,4 +436,7 @@ Windows Console Host Name describing the usage of the classic windows console as the terminal UI. (`conhost.exe`) + + Minimize current window to tray + diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 8cfb25cd3..c87895d9b 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -29,6 +29,8 @@ "disableAnimations": false, "startupActions": "", "focusFollowMouse": false, + "minimizeToTray": false, + "alwaysShowTrayIcon": false, "profiles": [ diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 28b502927..d7d21b0db 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -71,12 +71,16 @@ namespace RemotingUnitTests Remoting::WindowActivatedArgs GetLastActivatedArgs() { throw winrt::hresult_error{}; } void RequestRename(const Remoting::RenameRequestArgs& /*args*/) { throw winrt::hresult_error{}; } void Summon(const Remoting::SummonWindowBehavior& /*args*/) { throw winrt::hresult_error{}; }; + void RequestShowTrayIcon() { throw winrt::hresult_error{}; }; + void RequestHideTrayIcon() { throw winrt::hresult_error{}; }; TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, Remoting::WindowActivatedArgs); TYPED_EVENT(ExecuteCommandlineRequested, winrt::Windows::Foundation::IInspectable, Remoting::CommandlineArgs); TYPED_EVENT(IdentifyWindowsRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(DisplayWindowIdRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(RenameRequested, winrt::Windows::Foundation::IInspectable, Remoting::RenameRequestArgs); TYPED_EVENT(SummonRequested, winrt::Windows::Foundation::IInspectable, Remoting::SummonWindowBehavior); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); }; class RemotingTests diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index a89cc3756..356d1d5c4 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -11,8 +11,6 @@ #include "VirtualDesktopUtils.h" #include "icon.h" -#include - using namespace winrt::Windows::UI; using namespace winrt::Windows::UI::Composition; using namespace winrt::Windows::UI::Xaml; @@ -65,6 +63,8 @@ AppHost::AppHost() noexcept : // Update our own internal state tracking if we're in quake mode or not. _IsQuakeWindowChanged(nullptr, nullptr); + _window->SetMinimizeToTrayBehavior(_logic.GetMinimizeToTray()); + // Tell the window to callback to us when it's about to handle a WM_CREATE auto pfn = std::bind(&AppHost::_HandleCreateWindow, this, @@ -80,15 +80,9 @@ AppHost::AppHost() noexcept : _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); _window->WindowActivated({ this, &AppHost::_WindowActivated }); _window->HotkeyPressed({ this, &AppHost::_GlobalHotkeyPressed }); - _window->NotifyTrayIconPressed({ this, &AppHost::_HandleTrayIconPressed }); _window->SetAlwaysOnTop(_logic.GetInitialAlwaysOnTop()); _window->MakeWindow(); - if (_window->IsQuakeWindow()) - { - _UpdateTrayIcon(); - } - _windowManager.BecameMonarch({ this, &AppHost::_BecomeMonarch }); if (_windowManager.IsMonarch()) { @@ -98,13 +92,12 @@ AppHost::AppHost() noexcept : AppHost::~AppHost() { - // destruction order is important for proper teardown here - if (_trayIconData) + if (_window->IsQuakeWindow()) { - Shell_NotifyIcon(NIM_DELETE, &_trayIconData.value()); - _trayIconData.reset(); + _windowManager.RequestHideTrayIcon(); } + // destruction order is important for proper teardown here _window = nullptr; _app.Close(); _app = nullptr; @@ -662,6 +655,20 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s const winrt::Windows::Foundation::IInspectable& /*args*/) { _setupGlobalHotkeys(); + + // The monarch is just going to be THE listener for inbound connections. + _listenForInboundConnections(); + + if (_windowManager.DoesQuakeWindowExist() || + _window->IsQuakeWindow() || + (_logic.GetAlwaysShowTrayIcon() || _logic.GetMinimizeToTray())) + { + _CreateTrayIcon(); + } + + // These events are coming from peasants that become or un-become quake windows. + _windowManager.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequested(); }); + _windowManager.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequested(); }); } void AppHost::_listenForInboundConnections() @@ -947,24 +954,50 @@ void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspecta const winrt::Windows::Foundation::IInspectable& /*args*/) { _setupGlobalHotkeys(); + + // If we're monarch, we need to check some conditions to show the tray icon. + // If there's a Quake window somewhere, we'll want to keep the tray icon. + // There's two settings - MinimizeToTray and AlwaysShowTrayIcon. If either + // one of them are true, we want to make sure there's a tray icon. + // If both are false, we want to remove our icon from the tray. + // When we remove our icon from the tray, we'll also want to re-summon + // any hidden windows, but right now we're not keeping track of who's hidden, + // so just summon them all. Tracking the work to do a "summon all minimized" in + // GH#10448 + if (_windowManager.IsMonarch()) + { + if (!_windowManager.DoesQuakeWindowExist()) + { + if (!_trayIcon && (_logic.GetMinimizeToTray() || _logic.GetAlwaysShowTrayIcon())) + { + _CreateTrayIcon(); + } + else if (_trayIcon && !_logic.GetMinimizeToTray() && !_logic.GetAlwaysShowTrayIcon()) + { + _windowManager.SummonAllWindows(); + _DestroyTrayIcon(); + } + } + } + + _window->SetMinimizeToTrayBehavior(_logic.GetMinimizeToTray()); } void AppHost::_IsQuakeWindowChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { - if (_window->IsQuakeWindow() && !_logic.IsQuakeWindow()) + // We want the quake window to be accessible through the tray icon. + // This means if there's a quake window _somewhere_, we want the tray icon + // to show regardless of the tray icon settings. + // This also means we'll need to destroy the tray icon if it was created + // specifically for the quake window. If not, it should not be destroyed. + if (!_window->IsQuakeWindow() && _logic.IsQuakeWindow()) { - // If we're exiting quake mode, we should make our - // tray icon disappear. - if (_trayIconData) - { - Shell_NotifyIcon(NIM_DELETE, &_trayIconData.value()); - _trayIconData.reset(); - } + _ShowTrayIconRequested(); } - else if (!_window->IsQuakeWindow() && _logic.IsQuakeWindow()) + else if (_window->IsQuakeWindow() && !_logic.IsQuakeWindow()) { - _UpdateTrayIcon(); + _HideTrayIconRequested(); } _window->IsQuakeWindow(_logic.IsQuakeWindow()); @@ -981,55 +1014,84 @@ void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspecta _HandleSummon(sender, summonArgs); } -void AppHost::_HandleTrayIconPressed() +// Method Description: +// - Creates a Tray Icon and hooks up its handlers +// Arguments: +// - +// Return Value: +// - +void AppHost::_CreateTrayIcon() { - // Currently scoping "minimize to tray" to only - // the quake window. - if (_logic.IsQuakeWindow()) + if constexpr (Feature_TrayIcon::IsEnabled()) { - const Remoting::SummonWindowBehavior summonArgs{}; - summonArgs.DropdownDuration(200); - _window->SummonWindow(summonArgs); + _trayIcon = std::make_unique(_window->GetHandle()); + + // Hookup the handlers, save the tokens for revoking if settings change. + _ReAddTrayIconToken = _window->NotifyReAddTrayIcon([this]() { _trayIcon->ReAddTrayIcon(); }); + _TrayIconPressedToken = _window->NotifyTrayIconPressed([this]() { _trayIcon->TrayIconPressed(); }); + _ShowTrayContextMenuToken = _window->NotifyShowTrayContextMenu([this](til::point coord) { _trayIcon->ShowTrayContextMenu(coord, _windowManager.GetPeasantNames()); }); + _TrayMenuItemSelectedToken = _window->NotifyTrayMenuItemSelected([this](HMENU hm, UINT idx) { _trayIcon->TrayMenuItemSelected(hm, idx); }); + _trayIcon->SummonWindowRequested([this](auto& args) { _windowManager.SummonWindow(args); }); } } // Method Description: -// - Creates and adds an icon to the notification tray. +// - Deletes our tray icon if we have one. // Arguments: -// - +// - // Return Value: // - -void AppHost::_UpdateTrayIcon() +void AppHost::_DestroyTrayIcon() { - if (!_trayIconData && _window->GetHandle()) + if constexpr (Feature_TrayIcon::IsEnabled()) { - NOTIFYICONDATA nid{}; + _window->NotifyReAddTrayIcon(_ReAddTrayIconToken); + _window->NotifyTrayIconPressed(_TrayIconPressedToken); + _window->NotifyShowTrayContextMenu(_ShowTrayContextMenuToken); + _window->NotifyTrayMenuItemSelected(_TrayMenuItemSelectedToken); - // This HWND will receive the callbacks sent by the tray icon. - nid.hWnd = _window->GetHandle(); - - // App-defined identifier of the icon. The HWND and ID are used - // to identify which icon to operate on when calling Shell_NotifyIcon. - // Multiple icons can be associated with one HWND, but here we're only - // going to be showing one so the ID doesn't really matter. - nid.uID = 1; - - nid.uCallbackMessage = CM_NOTIFY_FROM_TRAY; - - ScopedResourceLoader cascadiaLoader{ L"Resources" }; - - nid.hIcon = static_cast(GetActiveAppIconHandle(ICON_SMALL)); - StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), cascadiaLoader.GetLocalizedString(L"AppName").c_str()); - nid.uFlags = NIF_MESSAGE | NIF_SHOWTIP | NIF_TIP | NIF_ICON; - Shell_NotifyIcon(NIM_ADD, &nid); - - // For whatever reason, the NIM_ADD call doesn't seem to set the version - // properly, resulting in us being unable to receive the expected notification - // events. We actually have to make a separate NIM_SETVERSION call for it to - // work properly. - nid.uVersion = NOTIFYICON_VERSION_4; - Shell_NotifyIcon(NIM_SETVERSION, &nid); - - _trayIconData = nid; + _trayIcon->RemoveIconFromTray(); + _trayIcon = nullptr; + } +} + +winrt::fire_and_forget AppHost::_ShowTrayIconRequested() +{ + if constexpr (Feature_TrayIcon::IsEnabled()) + { + co_await winrt::resume_background(); + if (_windowManager.IsMonarch()) + { + if (!_trayIcon) + { + _CreateTrayIcon(); + } + } + else + { + _windowManager.RequestShowTrayIcon(); + } + } +} + +winrt::fire_and_forget AppHost::_HideTrayIconRequested() +{ + if constexpr (Feature_TrayIcon::IsEnabled()) + { + co_await winrt::resume_background(); + if (_windowManager.IsMonarch()) + { + // Destroy it only if our settings allow it + if (_trayIcon && + !_logic.GetAlwaysShowTrayIcon() && + !_logic.GetMinimizeToTray()) + { + _DestroyTrayIcon(); + } + } + else + { + _windowManager.RequestHideTrayIcon(); + } } } diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 5f6d10ca5..89a4c96d9 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -2,8 +2,8 @@ // Licensed under the MIT license. #include "pch.h" - #include "NonClientIslandWindow.h" +#include "TrayIcon.h" class AppHost { @@ -84,8 +84,13 @@ private: void _SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); - void _UpdateTrayIcon(); - void _HandleTrayIconPressed(); - - std::optional _trayIconData; + void _CreateTrayIcon(); + void _DestroyTrayIcon(); + winrt::fire_and_forget _ShowTrayIconRequested(); + winrt::fire_and_forget _HideTrayIconRequested(); + std::unique_ptr _trayIcon; + winrt::event_token _ReAddTrayIconToken; + winrt::event_token _TrayIconPressedToken; + winrt::event_token _ShowTrayContextMenuToken; + winrt::event_token _TrayMenuItemSelectedToken; }; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 731dfa168..616f80036 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -6,6 +6,7 @@ #include "../types/inc/Viewport.hpp" #include "resource.h" #include "icon.h" +#include "TrayIcon.h" extern "C" IMAGE_DOS_HEADER __ImageBase; @@ -22,6 +23,8 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; #define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS" +const UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); + IslandWindow::IslandWindow() noexcept : _interopWindowHandle{ nullptr }, _rootGrid{ nullptr }, @@ -453,7 +456,6 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize { if (wparam == SIZE_MINIMIZED && _isQuakeWindow) { - _NotifyWindowHiddenHandlers(); ShowWindow(GetHandle(), SW_HIDE); return 0; } @@ -573,9 +575,30 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize _NotifyTrayIconPressedHandlers(); return 0; } + case WM_CONTEXTMENU: + { + const til::point eventPoint{ GET_X_LPARAM(wparam), GET_Y_LPARAM(wparam) }; + _NotifyShowTrayContextMenuHandlers(eventPoint); + return 0; + } } break; } + case WM_MENUCOMMAND: + { + _NotifyTrayMenuItemSelectedHandlers((HMENU)lparam, (UINT)wparam); + return 0; + } + default: + // We'll want to receive this message when explorer.exe restarts + // so that we can re-add our icon to the tray. + // This unfortunately isn't a switch case because we register the + // message at runtime. + if (message == WM_TASKBARCREATED) + { + _NotifyReAddTrayIconHandlers(); + return 0; + } } // TODO: handle messages here... @@ -600,6 +623,10 @@ void IslandWindow::OnResize(const UINT width, const UINT height) void IslandWindow::OnMinimize() { // TODO GH#1989 Stop rendering island content when the app is minimized. + if (_minimizeToTray) + { + HideWindow(); + } } // Method Description: @@ -1603,5 +1630,15 @@ til::rectangle IslandWindow::_getQuakeModeSize(HMONITOR hmon) return til::rectangle{ origin, dimensions }; } +void IslandWindow::HideWindow() +{ + ShowWindow(GetHandle(), SW_HIDE); +} + +void IslandWindow::SetMinimizeToTrayBehavior(bool minimizeToTray) noexcept +{ + _minimizeToTray = minimizeToTray; +} + DEFINE_EVENT(IslandWindow, DragRegionClicked, _DragRegionClickedHandlers, winrt::delegate<>); DEFINE_EVENT(IslandWindow, WindowCloseButtonClicked, _windowCloseButtonClickedHandler, winrt::delegate<>); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index bea360862..1f78c8e1a 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -47,13 +47,19 @@ public: bool IsQuakeWindow() const noexcept; void IsQuakeWindow(bool isQuakeWindow) noexcept; + void HideWindow(); + + void SetMinimizeToTrayBehavior(bool minimizeToTray) noexcept; + DECLARE_EVENT(DragRegionClicked, _DragRegionClickedHandlers, winrt::delegate<>); DECLARE_EVENT(WindowCloseButtonClicked, _windowCloseButtonClickedHandler, winrt::delegate<>); WINRT_CALLBACK(MouseScrolled, winrt::delegate); WINRT_CALLBACK(WindowActivated, winrt::delegate); WINRT_CALLBACK(HotkeyPressed, winrt::delegate); WINRT_CALLBACK(NotifyTrayIconPressed, winrt::delegate); - WINRT_CALLBACK(NotifyWindowHidden, winrt::delegate); + WINRT_CALLBACK(NotifyShowTrayContextMenu, winrt::delegate); + WINRT_CALLBACK(NotifyTrayMenuItemSelected, winrt::delegate); + WINRT_CALLBACK(NotifyReAddTrayIcon, winrt::delegate); protected: void ForceResize() @@ -116,6 +122,8 @@ protected: void _summonWindowRoutineBody(winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior args); + bool _minimizeToTray{ false }; + private: // This minimum width allows for width the tabs fit static constexpr long minimumWidth = 460L; diff --git a/src/cascadia/WindowsTerminal/TrayIcon.cpp b/src/cascadia/WindowsTerminal/TrayIcon.cpp new file mode 100644 index 000000000..83bc04343 --- /dev/null +++ b/src/cascadia/WindowsTerminal/TrayIcon.cpp @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "icon.h" +#include "TrayIcon.h" +#include "CustomWindowMessages.h" + +#include +#include + +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Microsoft::Terminal; + +TrayIcon::TrayIcon(const HWND owningHwnd) : + _owningHwnd{ owningHwnd } +{ + CreateTrayIcon(); +} + +TrayIcon::~TrayIcon() +{ + RemoveIconFromTray(); +} + +// Method Description: +// - Creates and adds an icon to the notification tray. +// If an icon already exists, update the HWND associated +// to the icon with this window's HWND. +// Arguments: +// - +// Return Value: +// - +void TrayIcon::CreateTrayIcon() +{ + NOTIFYICONDATA nid{}; + nid.cbSize = sizeof(NOTIFYICONDATA); + + // This HWND will receive the callbacks sent by the tray icon. + nid.hWnd = _owningHwnd; + + // App-defined identifier of the icon. The HWND and ID are used + // to identify which icon to operate on when calling Shell_NotifyIcon. + // Multiple icons can be associated with one HWND, but here we're only + // going to be showing one so the ID doesn't really matter. + nid.uID = 1; + + nid.uCallbackMessage = CM_NOTIFY_FROM_TRAY; + + // AppName happens to be in CascadiaPackage's Resources. + ScopedResourceLoader loader{ L"Resources" }; + const auto appNameLoc = loader.GetLocalizedString(L"AppName"); + + nid.hIcon = static_cast(GetActiveAppIconHandle(true)); + StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), appNameLoc.c_str()); + nid.uFlags = NIF_MESSAGE | NIF_SHOWTIP | NIF_TIP | NIF_ICON; + Shell_NotifyIcon(NIM_ADD, &nid); + + // For whatever reason, the NIM_ADD call doesn't seem to set the version + // properly, resulting in us being unable to receive the expected notification + // events. We actually have to make a separate NIM_SETVERSION call for it to + // work properly. + nid.uVersion = NOTIFYICON_VERSION_4; + Shell_NotifyIcon(NIM_SETVERSION, &nid); + + _trayIconData = nid; +} + +// Method Description: +// - This creates our context menu and displays it at the given +// screen coordinates. +// Arguments: +// - coord: The coordinates where we should be showing the context menu. +// - peasants: The map of all peasants that should be available in the context menu. +// Return Value: +// - +void TrayIcon::ShowTrayContextMenu(const til::point coord, + IMapView peasants) +{ + if (const auto hMenu = _CreateTrayContextMenu(peasants)) + { + // We'll need to set our window to the foreground before calling + // TrackPopupMenuEx or else the menu won't dismiss when clicking away. + SetForegroundWindow(_owningHwnd); + + // User can select menu items with the left and right buttons. + UINT uFlags = TPM_RIGHTBUTTON; + + // Nonzero if drop-down menus are right-aligned with the corresponding menu-bar item + // 0 if the menus are left-aligned. + if (GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0) + { + uFlags |= TPM_RIGHTALIGN; + } + else + { + uFlags |= TPM_LEFTALIGN; + } + + TrackPopupMenuEx(hMenu, uFlags, gsl::narrow_cast(coord.x()), gsl::narrow_cast(coord.y()), _owningHwnd, NULL); + } +} + +// Method Description: +// - This creates the context menu for our tray icon. +// Arguments: +// - peasants: A map of all peasants' ID to their window name. +// Return Value: +// - The handle to the newly created context menu. +HMENU TrayIcon::_CreateTrayContextMenu(IMapView peasants) +{ + auto hMenu = CreatePopupMenu(); + if (hMenu) + { + MENUINFO mi{}; + mi.cbSize = sizeof(MENUINFO); + mi.fMask = MIM_STYLE | MIM_APPLYTOSUBMENUS | MIM_MENUDATA; + mi.dwStyle = MNS_NOTIFYBYPOS; + mi.dwMenuData = NULL; + SetMenuInfo(hMenu, &mi); + + // Focus Current Terminal Window + AppendMenu(hMenu, MF_STRING, gsl::narrow(TrayMenuItemAction::FocusTerminal), RS_(L"TrayIconFocusTerminal").c_str()); + AppendMenu(hMenu, MF_SEPARATOR, 0, L""); + + // Submenu for Windows + if (auto submenu = CreatePopupMenu()) + { + const auto locWindow = RS_(L"WindowIdLabel"); + const auto locUnnamed = RS_(L"UnnamedWindowName"); + for (const auto [id, name] : peasants) + { + winrt::hstring displayText = name; + if (name.empty()) + { + displayText = fmt::format(L"{} {} - <{}>", locWindow, id, locUnnamed); + } + + AppendMenu(submenu, MF_STRING, gsl::narrow(id), displayText.c_str()); + } + + MENUINFO submenuInfo{}; + submenuInfo.cbSize = sizeof(MENUINFO); + submenuInfo.fMask = MIM_MENUDATA; + submenuInfo.dwStyle = MNS_NOTIFYBYPOS; + submenuInfo.dwMenuData = (UINT_PTR)TrayMenuItemAction::SummonWindow; + SetMenuInfo(submenu, &submenuInfo); + + AppendMenu(hMenu, MF_POPUP, (UINT_PTR)submenu, RS_(L"TrayIconWindowSubmenu").c_str()); + } + } + return hMenu; +} + +// Method Description: +// - This is the handler for when one of the menu items are selected within +// the tray icon's context menu. +// Arguments: +// - menu: The handle to the menu that holds the menu item that was selected. +// - menuItemIndex: The index of the menu item within the given menu. +// Return Value: +// - +void TrayIcon::TrayMenuItemSelected(const HMENU menu, const UINT menuItemIndex) +{ + // Check the menu's data for a specific action. + MENUINFO mi{}; + mi.cbSize = sizeof(MENUINFO); + mi.fMask = MIM_MENUDATA; + GetMenuInfo(menu, &mi); + if (mi.dwMenuData) + { + if (gsl::narrow(mi.dwMenuData) == TrayMenuItemAction::SummonWindow) + { + winrt::Microsoft::Terminal::Remoting::SummonWindowSelectionArgs args{}; + args.WindowID(GetMenuItemID(menu, menuItemIndex)); + args.SummonBehavior().ToggleVisibility(false); + args.SummonBehavior().MoveToCurrentDesktop(false); + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + _SummonWindowRequestedHandlers(args); + return; + } + } + + // Now check the menu item itself for an action. + const auto action = gsl::narrow(GetMenuItemID(menu, menuItemIndex)); + switch (action) + { + case TrayMenuItemAction::FocusTerminal: + { + winrt::Microsoft::Terminal::Remoting::SummonWindowSelectionArgs args{}; + args.SummonBehavior().ToggleVisibility(false); + args.SummonBehavior().MoveToCurrentDesktop(false); + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + _SummonWindowRequestedHandlers(args); + break; + } + } +} + +// Method Description: +// - This is the handler for when the tray icon itself is left-clicked. +// Arguments: +// - +// Return Value: +// - +void TrayIcon::TrayIconPressed() +{ + // No name in the args means summon the mru window. + winrt::Microsoft::Terminal::Remoting::SummonWindowSelectionArgs args{}; + args.SummonBehavior().MoveToCurrentDesktop(false); + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + args.SummonBehavior().ToggleVisibility(false); + _SummonWindowRequestedHandlers(args); +} + +// Method Description: +// - Re-add a tray icon using our currently saved tray icon data. +// Arguments: +// - +// Return Value: +// - +void TrayIcon::ReAddTrayIcon() +{ + Shell_NotifyIcon(NIM_ADD, &_trayIconData); + Shell_NotifyIcon(NIM_SETVERSION, &_trayIconData); +} + +// Method Description: +// - Deletes our tray icon. +// Arguments: +// - +// Return Value: +// - +void TrayIcon::RemoveIconFromTray() +{ + Shell_NotifyIcon(NIM_DELETE, &_trayIconData); +} diff --git a/src/cascadia/WindowsTerminal/TrayIcon.h b/src/cascadia/WindowsTerminal/TrayIcon.h new file mode 100644 index 000000000..aa58d17f1 --- /dev/null +++ b/src/cascadia/WindowsTerminal/TrayIcon.h @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +// This enumerates all the possible actions +// that our tray icon context menu could do. +enum class TrayMenuItemAction +{ + FocusTerminal, // Focus the MRU terminal. + SummonWindow +}; + +class TrayIcon +{ +public: + TrayIcon() = delete; + TrayIcon(const HWND owningHwnd); + ~TrayIcon(); + + void CreateTrayIcon(); + void RemoveIconFromTray(); + void ReAddTrayIcon(); + + void TrayIconPressed(); + void ShowTrayContextMenu(const til::point coord, winrt::Windows::Foundation::Collections::IMapView peasants); + void TrayMenuItemSelected(const HMENU menu, const UINT menuItemIndex); + + WINRT_CALLBACK(SummonWindowRequested, winrt::delegate); + +private: + HMENU _CreateTrayContextMenu(winrt::Windows::Foundation::Collections::IMapView peasants); + + HWND _owningHwnd; + NOTIFYICONDATA _trayIconData; +}; diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj index 16ff12257..b20f6fd07 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -47,6 +47,7 @@ + @@ -57,6 +58,7 @@ + @@ -175,4 +177,4 @@ - + \ No newline at end of file diff --git a/src/cascadia/WindowsTerminal/icon.cpp b/src/cascadia/WindowsTerminal/icon.cpp index 4706894ad..687644aee 100644 --- a/src/cascadia/WindowsTerminal/icon.cpp +++ b/src/cascadia/WindowsTerminal/icon.cpp @@ -27,12 +27,14 @@ static int _GetActiveAppIconResource() return iconResource; } -HANDLE GetActiveAppIconHandle(int size) +// There's only two possible sizes - ICON_SMALL and ICON_BIG. +// So, use true for smallIcon if you want small and false for big. +HANDLE GetActiveAppIconHandle(bool smallIcon) { auto iconResource{ MAKEINTRESOURCEW(_GetActiveAppIconResource()) }; - const auto smXIcon = size == ICON_SMALL ? SM_CXSMICON : SM_CXICON; - const auto smYIcon = size == ICON_SMALL ? SM_CYSMICON : SM_CYICON; + const auto smXIcon = smallIcon ? SM_CXSMICON : SM_CXICON; + const auto smYIcon = smallIcon ? SM_CYSMICON : SM_CYICON; // These handles are loaded with LR_SHARED, so they are safe to "leak". HANDLE hIcon{ LoadImageW(wil::GetModuleInstanceHandle(), iconResource, IMAGE_ICON, GetSystemMetrics(smXIcon), GetSystemMetrics(smYIcon), LR_SHARED) }; @@ -43,11 +45,11 @@ HANDLE GetActiveAppIconHandle(int size) void UpdateWindowIconForActiveMetrics(HWND window) { - if (auto smallIcon = GetActiveAppIconHandle(ICON_SMALL)) + if (auto smallIcon = GetActiveAppIconHandle(true)) { SendMessageW(window, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); } - if (auto largeIcon = GetActiveAppIconHandle(ICON_BIG)) + if (auto largeIcon = GetActiveAppIconHandle(false)) { SendMessageW(window, WM_SETICON, ICON_BIG, reinterpret_cast(largeIcon)); } diff --git a/src/cascadia/WindowsTerminal/icon.h b/src/cascadia/WindowsTerminal/icon.h index ed4fb5587..84be4e9c1 100644 --- a/src/cascadia/WindowsTerminal/icon.h +++ b/src/cascadia/WindowsTerminal/icon.h @@ -3,5 +3,5 @@ #pragma once -HANDLE GetActiveAppIconHandle(const int size); +HANDLE GetActiveAppIconHandle(bool smallIcon); void UpdateWindowIconForActiveMetrics(HWND window); diff --git a/src/cascadia/WindowsTerminal/main.cpp b/src/cascadia/WindowsTerminal/main.cpp index 7fb2523a5..ddac884fa 100644 --- a/src/cascadia/WindowsTerminal/main.cpp +++ b/src/cascadia/WindowsTerminal/main.cpp @@ -22,6 +22,15 @@ TRACELOGGING_DEFINE_PROVIDER( (0x56c06166, 0x2e2e, 0x5f4d, 0x7f, 0xf3, 0x74, 0xf4, 0xb7, 0x8c, 0x87, 0xd6), TraceLoggingOptionMicrosoftTelemetry()); +// !! BODGY !! +// Manually use the resources from TerminalApp as our resources. +// The WindowsTerminal project doesn't actually build a Resources.resw file, but +// we still need to be able to localize strings for the tray icon menu. Anything +// you want localized for WindowsTerminal.exe should be stuck in +// ...\TerminalApp\Resources\en-US\Resources.resw +#include +UTILS_DEFINE_LIBRARY_RESOURCE_SCOPE(L"TerminalApp/Resources"); + // Routine Description: // - Takes an image architecture and locates a string resource that maps to that architecture. // Arguments: diff --git a/src/features.xml b/src/features.xml index 42ff2e5f9..c50399f4a 100644 --- a/src/features.xml +++ b/src/features.xml @@ -64,6 +64,13 @@ + + Feature_TrayIcon + Controls whether the Tray Icon and related settings (aka. MinimizeToTray and AlwaysShowTrayIcon) are enabled + AlwaysEnabled + + + Feature_ShowProfileDefaultsInSettings Whether to show the "defaults" page in the Terminal settings UI From 70560a789c67dce3673fb369a90fb4c42f16bbbe Mon Sep 17 00:00:00 2001 From: Don-Vito Date: Fri, 13 Aug 2021 01:36:10 +0300 Subject: [PATCH 65/90] Change settings content frame transition to drill in (#10934) ## PR Checklist * [x] Closes #10632 * [x] CLA signed. * [ ] Tests added/passed * [ ] Documentation updated. * [ ] Schema updated. * [ ] I've discussed this with core contributors already. --- src/cascadia/TerminalSettingsEditor/MainPage.xaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.xaml b/src/cascadia/TerminalSettingsEditor/MainPage.xaml index 83ad8ad90..9576102a1 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.xaml +++ b/src/cascadia/TerminalSettingsEditor/MainPage.xaml @@ -117,7 +117,17 @@ + Grid.Row="0"> + + + + + + + + + + Date: Fri, 13 Aug 2021 10:56:34 -0700 Subject: [PATCH 66/90] Prevent deadlock in UIA Move API (#10937) Fixes a bug where interacting with Windows Terminal when using Narrator causes Windows Terminal to hang. `UiaTextRangeBase::Move()` locks, but later calls `UiaTextRangeBase::ExpandToEnclosingUnit()` which attempts to lock again. The workaround for this is to introduce a `_expandToEnclosingUnit()` that _does not_ lock the console. Then, `Move()` calls this new method, thus only allowing one lock to be established at a time. This bug is observed to be in v1.11.2221.0 and _not_ in v1.9.1942.0. --- src/types/UiaTextRangeBase.cpp | 146 ++++++++++++++++++--------------- src/types/UiaTextRangeBase.hpp | 2 + 2 files changed, 80 insertions(+), 68 deletions(-) diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 69f531c09..02e8abdb6 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -262,61 +262,73 @@ IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexc try { - const auto& buffer = _pData->GetTextBuffer(); - const auto bufferSize = _getBufferSize(); - const auto bufferEnd = bufferSize.EndExclusive(); - - if (unit == TextUnit_Character) - { - _start = buffer.GetGlyphStart(_start); - _end = buffer.GetGlyphEnd(_start); - } - else if (unit <= TextUnit_Word) - { - // expand to word - _start = buffer.GetWordStart(_start, _wordDelimiters, true); - _end = buffer.GetWordEnd(_start, _wordDelimiters, true); - - // GetWordEnd may return the actual end of the TextBuffer. - // If so, just set it to this value of bufferEnd - if (!bufferSize.IsInBounds(_end)) - { - _end = bufferEnd; - } - } - else if (unit <= TextUnit_Line) - { - if (_start == bufferEnd) - { - // Special case: if we are at the bufferEnd, - // move _start back one, instead of _end forward - _start.X = 0; - _start.Y = base::ClampSub(_start.Y, 1); - _end = bufferEnd; - } - else - { - // expand to line - _start.X = 0; - _end.X = 0; - _end.Y = base::ClampAdd(_start.Y, 1); - } - } - else - { - // TODO GH#6986: properly handle "end of buffer" as last character - // instead of last cell - // expand to document - _start = bufferSize.Origin(); - _end = bufferSize.EndExclusive(); - } - + _expandToEnclosingUnit(unit); UiaTracing::TextRange::ExpandToEnclosingUnit(unit, *this); return S_OK; } CATCH_RETURN(); } +// Method Description: +// - Moves _start and _end endpoints to encompass the enclosing text unit. +// (i.e. word --> enclosing word, line --> enclosing line) +// - IMPORTANT: this does _not_ lock the console +// Arguments: +// - attributeId - the UIA text attribute identifier we're expanding by +// Return Value: +// - +void UiaTextRangeBase::_expandToEnclosingUnit(TextUnit unit) +{ + const auto& buffer = _pData->GetTextBuffer(); + const auto bufferSize = _getBufferSize(); + const auto bufferEnd = bufferSize.EndExclusive(); + + if (unit == TextUnit_Character) + { + _start = buffer.GetGlyphStart(_start); + _end = buffer.GetGlyphEnd(_start); + } + else if (unit <= TextUnit_Word) + { + // expand to word + _start = buffer.GetWordStart(_start, _wordDelimiters, true); + _end = buffer.GetWordEnd(_start, _wordDelimiters, true); + + // GetWordEnd may return the actual end of the TextBuffer. + // If so, just set it to this value of bufferEnd + if (!bufferSize.IsInBounds(_end)) + { + _end = bufferEnd; + } + } + else if (unit <= TextUnit_Line) + { + if (_start == bufferEnd) + { + // Special case: if we are at the bufferEnd, + // move _start back one, instead of _end forward + _start.X = 0; + _start.Y = base::ClampSub(_start.Y, 1); + _end = bufferEnd; + } + else + { + // expand to line + _start.X = 0; + _end.X = 0; + _end.Y = base::ClampAdd(_start.Y, 1); + } + } + else + { + // TODO GH#6986: properly handle "end of buffer" as last character + // instead of last cell + // expand to document + _start = bufferSize.Origin(); + _end = bufferSize.EndExclusive(); + } +} + // Method Description: // - Verify that the given attribute has the desired formatting saved in the attributeId and val // Arguments: @@ -994,6 +1006,7 @@ std::wstring UiaTextRangeBase::_getTextValue(std::optional maxLeng IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, _In_ int count, _Out_ int* pRetVal) noexcept +try { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = 0; @@ -1011,26 +1024,22 @@ IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, constexpr auto endpoint = TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start; constexpr auto preventBufferEnd = true; const auto wasDegenerate = IsDegenerate(); - try + if (unit == TextUnit::TextUnit_Character) { - if (unit == TextUnit::TextUnit_Character) - { - _moveEndpointByUnitCharacter(count, endpoint, pRetVal, preventBufferEnd); - } - else if (unit <= TextUnit::TextUnit_Word) - { - _moveEndpointByUnitWord(count, endpoint, pRetVal, preventBufferEnd); - } - else if (unit <= TextUnit::TextUnit_Line) - { - _moveEndpointByUnitLine(count, endpoint, pRetVal, preventBufferEnd); - } - else if (unit <= TextUnit::TextUnit_Document) - { - _moveEndpointByUnitDocument(count, endpoint, pRetVal, preventBufferEnd); - } + _moveEndpointByUnitCharacter(count, endpoint, pRetVal, preventBufferEnd); + } + else if (unit <= TextUnit::TextUnit_Word) + { + _moveEndpointByUnitWord(count, endpoint, pRetVal, preventBufferEnd); + } + else if (unit <= TextUnit::TextUnit_Line) + { + _moveEndpointByUnitLine(count, endpoint, pRetVal, preventBufferEnd); + } + else if (unit <= TextUnit::TextUnit_Document) + { + _moveEndpointByUnitDocument(count, endpoint, pRetVal, preventBufferEnd); } - CATCH_RETURN(); // If we actually moved... if (*pRetVal != 0) @@ -1044,13 +1053,14 @@ IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, else { // then just expand to get our _end - ExpandToEnclosingUnit(unit); + _expandToEnclosingUnit(unit); } } UiaTracing::TextRange::Move(unit, count, *pRetVal, *this); return S_OK; } +CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoint endpoint, _In_ TextUnit unit, diff --git a/src/types/UiaTextRangeBase.hpp b/src/types/UiaTextRangeBase.hpp index 47b9778a0..02bf0461b 100644 --- a/src/types/UiaTextRangeBase.hpp +++ b/src/types/UiaTextRangeBase.hpp @@ -153,6 +153,8 @@ namespace Microsoft::Console::Types void _getBoundingRect(const til::rectangle textRect, _Inout_ std::vector& coords) const; + void _expandToEnclosingUnit(TextUnit unit); + void _moveEndpointByUnitCharacter(_In_ const int moveCount, _In_ const TextPatternRangeEndpoint endpoint, From 5d36e5d2dfb1815637b79b87af17e24725904695 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 16 Aug 2021 15:32:05 +0200 Subject: [PATCH 67/90] Hide profiles by default if they aren't new (#10910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let's say a user doesn't know that they need to write `"hidden": true` in order to prevent a profile from showing up (and a settings UI doesn't exist). Naturally they would open settings.json and try to remove the profile object. This section of code recognizes if a profile was seen before and marks it as `"hidden": true` by default and thus ensures the behavior the user expects: Profiles won't show up again after they've been removed from settings.json. ## References #8324 - Application State ## PR Checklist * [x] Closes #8270 * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * settings.json/state.json are created if they don't exist ✔️ * Removing any profile from settings.json doesn't cause it to appear again ✔️ * Hitting save in SUI creates profiles with `"hidden": true` ✔️ * Removing a default profile and hitting save in SUI works ❌ An empty object is added instead. --- src/cascadia/TerminalControl/TermControl.h | 7 ++- .../CascadiaSettingsSerialization.cpp | 49 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index e16ecbaae..e2ee89b82 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -185,8 +185,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation inline bool _IsClosing() const noexcept { +#ifndef NDEBUG // _closing isn't atomic and may only be accessed from the main thread. - assert(Dispatcher().HasThreadAccess()); + if (const auto dispatcher = Dispatcher()) + { + assert(dispatcher.HasThreadAccess()); + } +#endif return _closing; } diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index bcefe5b77..9d6e5c1f8 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -155,15 +155,14 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: const auto hardcodedDefaultGuid = resultPtr->GlobalSettings().DefaultProfile(); std::optional fileData = _ReadUserSettings(); - const bool foundFile = fileData.has_value(); // Make sure the file isn't totally empty. If it is, we'll treat the file // like it doesn't exist at all. - const bool fileHasData = foundFile && !fileData.value().empty(); + const bool fileHasData = fileData && !fileData->empty(); bool needToWriteFile = false; if (fileHasData) { - resultPtr->_ParseJsonString(fileData.value(), false); + resultPtr->_ParseJsonString(*fileData, false); } // Load profiles from dynamic profile generators. _userSettings should be @@ -204,6 +203,35 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: _CatchRethrowSerializationExceptionWithLocationInfo(resultPtr->_userSettingsString); } + // Let's say a user doesn't know that they need to write `"hidden": true` in + // order to prevent a profile from showing up (and a settings UI doesn't exist). + // Naturally they would open settings.json and try to remove the profile object. + // This section of code recognizes if a profile was seen before and marks it as + // `"hidden": true` by default and thus ensures the behavior the user expects: + // Profiles won't show up again after they've been removed from settings.json. + { + const auto state = winrt::get_self(ApplicationState::SharedInstance()); + auto generatedProfiles = state->GeneratedProfiles(); + bool generatedProfilesChanged = false; + + for (auto profile : resultPtr->_allProfiles) + { + if (generatedProfiles.emplace(profile.Guid()).second) + { + generatedProfilesChanged = true; + } + else if (profile.Origin() != OriginTag::User) + { + profile.Hidden(true); + } + } + + if (generatedProfilesChanged) + { + state->GeneratedProfiles(generatedProfiles); + } + } + // After layering the user settings, check if there are any new profiles // that need to be inserted into their user settings file. needToWriteFile = resultPtr->_AppendDynamicProfilesToUserSettings() || needToWriteFile; @@ -352,7 +380,6 @@ void CascadiaSettings::_LoadDynamicProfiles() } } - const GUID nullGuid{ 0 }; for (auto& generator : _profileGenerators) { const std::wstring generatorNamespace{ generator->GetNamespace() }; @@ -711,7 +738,7 @@ bool CascadiaSettings::_AppendDynamicProfilesToUserSettings() wbuilder.settings_["indentation"] = " "; wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons - auto isInJsonObj = [](const auto& profile, const auto& json) { + static const auto isInJsonObj = [](const auto& profile, const auto& json) { for (auto profileJson : _GetProfilesJsonObject(json)) { if (profileJson.isObject()) @@ -745,8 +772,16 @@ bool CascadiaSettings::_AppendDynamicProfilesToUserSettings() for (const auto& profile : _allProfiles) { - // Skip profiles that are in the user settings or the default settings. - if (isInJsonObj(profile, _userSettings) || isInJsonObj(profile, _defaultSettings)) + // Skip profiles that are: + // * hidden + // Because when a user manually removes profiles from settings.json, + // we mark them as hidden in LoadAll(). Adding those profiles right + // back into settings.json would feel confusing, while the + // profile that was just erased is added right back. + // * in the user settings or the default settings + // Because we don't want to add profiles which are already + // in the settings.json (explicitly or implicitly). + if (profile.Hidden() || isInJsonObj(profile, _userSettings) || isInJsonObj(profile, _defaultSettings)) { continue; } From 29be8564f6ab4c34b46abd8099adbd02286574d1 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 16 Aug 2021 08:41:17 -0500 Subject: [PATCH 68/90] Manually dismiss popups when the window moves, or the SUI scrolls (#10922) ## Summary of the Pull Request BODGY! This solution was suggested in https://github.com/microsoft/microsoft-ui-xaml/issues/4554#issuecomment-887815332. When the window moves, or when a ScrollViewer scrolls, dismiss any popups that are visible. This happens automagically when an app is a real XAML app, but it doesn't work for XAML Islands. ## References * upstream at https://github.com/microsoft/microsoft-ui-xaml/issues/4554 ## PR Checklist * [x] Closes #9320 * [x] I work here * [ ] Tests added/passed * [ ] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments Unfortunately, we've got a bunch of scroll viewers in our SUI. So I did something bodgyx2 to make our life a little easier. `DismissAllPopups` can be used to dismiss all popups for a particular UI element. However, we've got a bunch of pages with scroll viewers that may or may not have popups in them. Rather than define the same exact body for all their `ViewChanging` events, the `HasScrollViewer` struct will just do it for you! Inside the `HasScrollViewer` stuct, we can't get at the `XamlRoot()` that our subclass implements. I mean, _we_ can, but when XAML does it's codegen, _XAML_ won't be able to figure it out. Fortunately for us, we don't need to! The sender is a UIElement, so we can just get _their_ `XamlRoot()`. So, you can fix this for any SUI page with just a simple ```diff - + ``` ```diff - struct AddProfile : AddProfileT + struct AddProfile : public HasScrollViewer, AddProfileT ``` ## Validation Steps Performed * the window doesn't close when you move it * the popups _do_ close when you move the window * the popups close when you scroll any SUI page --- src/cascadia/TerminalSettingsEditor/Actions.h | 2 +- .../TerminalSettingsEditor/Actions.xaml | 2 +- .../TerminalSettingsEditor/AddProfile.h | 2 +- .../TerminalSettingsEditor/AddProfile.xaml | 2 +- .../TerminalSettingsEditor/ColorSchemes.h | 2 +- .../TerminalSettingsEditor/ColorSchemes.xaml | 2 +- .../TerminalSettingsEditor/GlobalAppearance.h | 2 +- .../GlobalAppearance.xaml | 2 +- .../TerminalSettingsEditor/Interaction.h | 2 +- .../TerminalSettingsEditor/Interaction.xaml | 2 +- src/cascadia/TerminalSettingsEditor/Launch.h | 2 +- .../TerminalSettingsEditor/Launch.xaml | 2 +- .../TerminalSettingsEditor/Profiles.h | 2 +- .../TerminalSettingsEditor/Profiles.xaml | 6 +- .../TerminalSettingsEditor/ReadOnlyActions.h | 2 +- .../ReadOnlyActions.xaml | 2 +- .../TerminalSettingsEditor/Rendering.h | 2 +- .../TerminalSettingsEditor/Rendering.xaml | 2 +- src/cascadia/TerminalSettingsEditor/Utils.h | 38 +++ src/cascadia/WindowsTerminal/AppHost.cpp | 27 ++ src/cascadia/WindowsTerminal/AppHost.h | 1 + src/cascadia/WindowsTerminal/IslandWindow.cpp | 5 + src/cascadia/WindowsTerminal/IslandWindow.h | 273 +++++++++--------- src/cascadia/WindowsTerminal/pch.h | 2 + 24 files changed, 231 insertions(+), 155 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Actions.h b/src/cascadia/TerminalSettingsEditor/Actions.h index 9c4d37cf6..97ae3d58b 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.h +++ b/src/cascadia/TerminalSettingsEditor/Actions.h @@ -107,7 +107,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation WINRT_PROPERTY(Model::CascadiaSettings, Settings, nullptr) }; - struct Actions : ActionsT + struct Actions : public HasScrollViewer, ActionsT { public: Actions(); diff --git a/src/cascadia/TerminalSettingsEditor/Actions.xaml b/src/cascadia/TerminalSettingsEditor/Actions.xaml index 11c1f65e4..35a59cc8f 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Actions.xaml @@ -374,7 +374,7 @@ - + diff --git a/src/cascadia/TerminalSettingsEditor/AddProfile.h b/src/cascadia/TerminalSettingsEditor/AddProfile.h index 39b78ffe9..5eaedbbad 100644 --- a/src/cascadia/TerminalSettingsEditor/AddProfile.h +++ b/src/cascadia/TerminalSettingsEditor/AddProfile.h @@ -43,7 +43,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation WINRT_CALLBACK(AddNew, AddNewArgs); }; - struct AddProfile : AddProfileT + struct AddProfile : public HasScrollViewer, AddProfileT { public: AddProfile(); diff --git a/src/cascadia/TerminalSettingsEditor/AddProfile.xaml b/src/cascadia/TerminalSettingsEditor/AddProfile.xaml index 5cff69b87..6229512c6 100644 --- a/src/cascadia/TerminalSettingsEditor/AddProfile.xaml +++ b/src/cascadia/TerminalSettingsEditor/AddProfile.xaml @@ -21,7 +21,7 @@ - + - - + + + diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index c6875b08f..7b328dc4d 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -994,14 +994,6 @@ Rename Text label for a button that can be used to begin the renaming process. - - This profile cannot be deleted because it is included by default. - Disclaimer presented next to the delete button when it is disabled. Some profiles are included in the app by default and cannot be deleted. - - - This profile cannot be deleted because it is automatically generated. - Disclaimer presented next to the delete button when it is disabled. Some profiles are automatically generated by the app and cannot be deleted. - This color scheme cannot be deleted or renamed because it is included by default. Disclaimer presented next to the delete button when it is disabled. diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index 03b05a766..052026fb9 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -225,20 +225,20 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::CreateNew return nullptr; } - winrt::hstring newName{}; - for (uint32_t candidateIndex = 0; candidateIndex < _allProfiles.Size() + 1; candidateIndex++) + std::wstring newName; + for (uint32_t candidateIndex = 0, count = _allProfiles.Size() + 1; candidateIndex < count; candidateIndex++) { // There is a theoretical unsigned integer wraparound, which is OK - newName = fmt::format(L"Profile {}", _allProfiles.Size() + 1 + candidateIndex); + newName = fmt::format(L"Profile {}", count + candidateIndex); if (std::none_of(begin(_allProfiles), end(_allProfiles), [&](auto&& profile) { return profile.Name() == newName; })) { break; } } - auto newProfile{ _userDefaultProfileSettings->CreateChild() }; - newProfile->Name(newName); + const auto newProfile = _CreateNewProfile(newName); _allProfiles.Append(*newProfile); + _activeProfiles.Append(*newProfile); return *newProfile; } @@ -258,26 +258,10 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate { THROW_HR_IF_NULL(E_INVALIDARG, source); - winrt::com_ptr duplicated; - if (_userDefaultProfileSettings) - { - duplicated = _userDefaultProfileSettings->CreateChild(); - } - else - { - duplicated = winrt::make_self(); - } - _allProfiles.Append(*duplicated); - - if (!source.Hidden()) - { - _activeProfiles.Append(*duplicated); - } - - winrt::hstring newName{ fmt::format(L"{} ({})", source.Name(), RS_(L"CopySuffix")) }; + auto newName = fmt::format(L"{} ({})", source.Name(), RS_(L"CopySuffix")); // Check if this name already exists and if so, append a number - for (uint32_t candidateIndex = 0; candidateIndex < _allProfiles.Size() + 1; ++candidateIndex) + for (uint32_t candidateIndex = 0, count = _allProfiles.Size() + 1; candidateIndex < count; ++candidateIndex) { if (std::none_of(begin(_allProfiles), end(_allProfiles), [&](auto&& profile) { return profile.Name() == newName; })) { @@ -286,13 +270,14 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate // There is a theoretical unsigned integer wraparound, which is OK newName = fmt::format(L"{} ({} {})", source.Name(), RS_(L"CopySuffix"), candidateIndex + 2); } - duplicated->Name(winrt::hstring(newName)); - const auto isProfilesDefaultsOrigin = [](const auto& profile) -> bool { + const auto duplicated = _CreateNewProfile(newName); + + static constexpr auto isProfilesDefaultsOrigin = [](const auto& profile) -> bool { return profile && profile.Origin() != OriginTag::ProfilesDefaults; }; - const auto isProfilesDefaultsOriginSub = [=](const auto& sub) -> bool { + static constexpr auto isProfilesDefaultsOriginSub = [](const auto& sub) -> bool { return sub && isProfilesDefaultsOrigin(sub.SourceProfile()); }; @@ -308,7 +293,9 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate target.settingName(source.settingName()); \ } - DUPLICATE_SETTING_MACRO(Hidden); + // If the source is hidden and the Settings UI creates a + // copy of it we don't want the copy to be hidden as well. + // --> Don't do DUPLICATE_SETTING_MACRO(Hidden); DUPLICATE_SETTING_MACRO(Icon); DUPLICATE_SETTING_MACRO(CloseOnExit); DUPLICATE_SETTING_MACRO(TabTitle); @@ -388,6 +375,8 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate duplicated->ConnectionType(source.ConnectionType()); } + _allProfiles.Append(*duplicated); + _activeProfiles.Append(*duplicated); return *duplicated; } @@ -421,6 +410,32 @@ winrt::hstring CascadiaSettings::GetSerializationErrorMessage() return _deserializationErrorMessage; } +// As used by CreateNewProfile and DuplicateProfile this function +// creates a new Profile instance with a random UUID and a given name. +winrt::com_ptr CascadiaSettings::_CreateNewProfile(const std::wstring_view& name) const +{ + winrt::com_ptr profile; + + if (_userDefaultProfileSettings) + { + profile = _userDefaultProfileSettings->CreateChild(); + } + else + { + profile = winrt::make_self(); + } + + // Technically there's Utils::CreateV5Uuid which we could use, but I wanted + // truly globally unique UUIDs for profiles created through the settings UI. + GUID guid{}; + LOG_IF_FAILED(CoCreateGuid(&guid)); + + profile->Guid(guid); + profile->Name(winrt::hstring{ name }); + + return profile; +} + // Method Description: // - Attempts to validate this settings structure. If there are critical errors // found, they'll be thrown as a SettingsLoadError. Non-critical errors, such diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 5a5735831..ee5782277 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -124,6 +124,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Json::Value _defaultSettings; winrt::com_ptr _userDefaultProfileSettings{ nullptr }; + winrt::com_ptr _CreateNewProfile(const std::wstring_view& name) const; + void _LayerOrCreateProfile(const Json::Value& profileJson); winrt::com_ptr _FindMatchingProfile(const Json::Value& profileJson); std::optional _FindMatchingProfileIndex(const Json::Value& profileJson); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 9d6e5c1f8..36ca2a316 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -214,15 +214,18 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: auto generatedProfiles = state->GeneratedProfiles(); bool generatedProfilesChanged = false; - for (auto profile : resultPtr->_allProfiles) + for (const auto& profile : resultPtr->_allProfiles) { - if (generatedProfiles.emplace(profile.Guid()).second) + const auto profileImpl = winrt::get_self(profile); + + if (generatedProfiles.emplace(profileImpl->Guid()).second) { generatedProfilesChanged = true; } - else if (profile.Origin() != OriginTag::User) + else if (profileImpl->Origin() != OriginTag::User) { - profile.Hidden(true); + profileImpl->Deleted(true); + profileImpl->Hidden(true); } } @@ -351,7 +354,7 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: // tag these profiles as in-box for (const auto& profile : resultPtr->AllProfiles()) { - const auto& profileImpl{ winrt::get_self(profile) }; + const auto profileImpl{ winrt::get_self(profile) }; profileImpl->Origin(OriginTag::InBox); } @@ -781,7 +784,7 @@ bool CascadiaSettings::_AppendDynamicProfilesToUserSettings() // * in the user settings or the default settings // Because we don't want to add profiles which are already // in the settings.json (explicitly or implicitly). - if (profile.Hidden() || isInJsonObj(profile, _userSettings) || isInJsonObj(profile, _defaultSettings)) + if (profile.Deleted() || isInJsonObj(profile, _userSettings) || isInJsonObj(profile, _defaultSettings)) { continue; } @@ -1250,8 +1253,11 @@ Json::Value CascadiaSettings::ToJson() const Json::Value profilesList{ Json::ValueType::arrayValue }; for (const auto& entry : _allProfiles) { - const auto prof{ winrt::get_self(entry) }; - profilesList.append(prof->ToJson()); + if (!entry.Deleted()) + { + const auto prof{ winrt::get_self(entry) }; + profilesList.append(prof->ToJson()); + } } profiles[JsonKey(ProfilesListKey)] = profilesList; json[JsonKey(ProfilesKey)] = profiles; diff --git a/src/cascadia/TerminalSettingsModel/Profile.cpp b/src/cascadia/TerminalSettingsModel/Profile.cpp index d90a65884..338de21ac 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.cpp +++ b/src/cascadia/TerminalSettingsModel/Profile.cpp @@ -84,6 +84,7 @@ winrt::com_ptr Profile::CopySettings(winrt::com_ptr source) { auto profile{ winrt::make_self() }; + profile->_Deleted = source->_Deleted; profile->_Guid = source->_Guid; profile->_Name = source->_Name; profile->_Source = source->_Source; diff --git a/src/cascadia/TerminalSettingsModel/Profile.h b/src/cascadia/TerminalSettingsModel/Profile.h index 155dcec4c..c84dadefc 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.h +++ b/src/cascadia/TerminalSettingsModel/Profile.h @@ -106,6 +106,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _FinalizeInheritance() override; + WINRT_PROPERTY(bool, Deleted, false); WINRT_PROPERTY(OriginTag, Origin, OriginTag::None); INHERITABLE_SETTING(Model::Profile, guid, Guid, _GenerateGuidForProfile(Name(), Source())); diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 9572fdc17..5ad1ae933 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -46,6 +46,8 @@ namespace Microsoft.Terminal.Settings.Model void CreateUnfocusedAppearance(); void DeleteUnfocusedAppearance(); + // True if the user explicitly removed this Profile from settings.json. + Boolean Deleted { get; }; OriginTag Origin { get; }; INHERITABLE_PROFILE_SETTING(String, Name); From 77121049833261dc573583c07c38a6b255d6e7c7 Mon Sep 17 00:00:00 2001 From: Steffen Date: Tue, 24 Aug 2021 01:48:13 +0200 Subject: [PATCH 88/90] Refactor `u8u16` and `u16u8` conversion functions (#10966) * Perform the handling of partial code points in the `u8u16` and `u16u8` conversion functions without preparation in a preliminary buffer. * Simplify partials handling in `u8u16` (perf). * Declare the parameters for the incoming data as referenced string_views. * Simplify templatization. * Simplify exception handling. We complete the partial codepoint in the 4-bytes long cache and convert it separately. This makes the cache ready for capturing the next partials before the remaining string is converted. This way, we neither need to copy the whole string into a buffer which contains complete codepoints, nor do we need to allocate an unnecessarily long buffer which exists for the life time of the state class instance. Finding and capturing of partials is performed in a more linear code using the evaluation of the length of a code point. The parameters for the incoming data are now explicitely declared to be referenced string_views. `CATCH_RETURN` is used to improve the readability of the code. ## Validation Steps Performed * manually tested * unit tests passed Closes #10946 Co-authored-by: Leonard Hecker --- .github/actions/spelling/allow/names.txt | 2 + src/inc/til/u8u16convert.h | 483 +++++++++-------------- 2 files changed, 178 insertions(+), 307 deletions(-) diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt index b4fe3abae..27ba53635 100644 --- a/.github/actions/spelling/allow/names.txt +++ b/.github/actions/spelling/allow/names.txt @@ -31,6 +31,7 @@ Kourosh kowalczyk leonmsft Lepilleur +lhecker lukesampson Manandhar mbadolato @@ -66,6 +67,7 @@ sonpham stakx thereses Walisch +Wellons Wirt Wojciech zadjii diff --git a/src/inc/til/u8u16convert.h b/src/inc/til/u8u16convert.h index af6e4f9b7..226ae5ba0 100644 --- a/src/inc/til/u8u16convert.h +++ b/src/inc/til/u8u16convert.h @@ -16,249 +16,36 @@ Based on the results the decision was made to keep using the platform functions MultiByteToWideChar and WideCharToMultiByte. Author(s): -- Steffen Illhardt (german-one) 2020 +- Steffen Illhardt (german-one), Leonard Hecker (lhecker) 2020-2021 --*/ #pragma once namespace til // Terminal Implementation Library. Also: "Today I Learned" { - template - class u8u16state final + // state structure for maintenance of UTF-8 partials + struct u8state { - public: - u8u16state() noexcept : - _buffer{}, - _utfPartials{} + char partials[4]; + uint8_t have{}; + uint8_t want{}; + + constexpr void reset() noexcept { + *this = {}; } - - // Method Description: - // - Takes a UTF-8 string and populates it with *complete* UTF-8 codepoints. - // If it receives an incomplete codepoint, it will cache it until it can be completed. - // Arguments: - // - in - UTF-8 string_view potentially containing partial code points - // - out - on return, populated with complete codepoints at the string end - // Return Value: - // - S_OK - the resulting string doesn't end with a partial - // - S_FALSE - the resulting string contains the previously cached partials only - // - E_OUTOFMEMORY - the method failed to allocate memory for the resulting string - // - E_ABORT - the resulting string length would exceed the max_size and thus, the processing was aborted - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value, HRESULT>::type - operator()(const std::basic_string_view in, std::basic_string_view& out) noexcept - { - try - { - size_t capacity{}; - RETURN_HR_IF(E_ABORT, !base::CheckAdd(in.length(), _partialsLen).AssignIfValid(&capacity)); - - _buffer.clear(); - - // If we were previously called with a huge buffer we have an equally large _buffer. - // We shouldn't just keep this huge buffer around, if no one needs it anymore. - if (_buffer.capacity() > 16 * 1024 && (_buffer.capacity() >> 1) > capacity) - { - _buffer.shrink_to_fit(); - } - - _buffer.reserve(capacity); - - // copy UTF-8 code units that were remaining from the previous call (if any) - if (_partialsLen != 0u) - { - _buffer.assign(_utfPartials.cbegin(), _utfPartials.cbegin() + _partialsLen); - _partialsLen = 0u; - } - - if (in.empty()) - { - out = _buffer; - if (_buffer.empty()) - { - return S_OK; - } - - return S_FALSE; // the partial is populated - } - - _buffer.append(in); - size_t remainingLength{ _buffer.length() }; - - auto backIter = _buffer.end(); - // If the last byte in the string was a byte belonging to a UTF-8 multi-byte character - if ((*(backIter - 1) & _Utf8BitMasks::MaskAsciiByte) > _Utf8BitMasks::IsAsciiByte) - { - // Check only up to 3 last bytes, if no Lead Byte was found then the byte before must be the Lead Byte and no partials are in the string - const size_t stopLen{ std::min(_buffer.length(), gsl::narrow_cast(3u)) }; - for (size_t sequenceLen{ 1u }; sequenceLen <= stopLen; ++sequenceLen) - { - --backIter; - // If Lead Byte found - if ((*backIter & _Utf8BitMasks::MaskContinuationByte) > _Utf8BitMasks::IsContinuationByte) - { - // If the Lead Byte indicates that the last bytes in the string is a partial UTF-8 code point then cache them: - // Use the bitmask at index `sequenceLen`. Compare the result with the operand having the same index. If they - // are not equal then the sequence has to be cached because it is a partial code point. Otherwise the - // sequence is a complete UTF-8 code point and the whole string is ready for the conversion into a UTF-16 string. - if ((*backIter & _cmpMasks.at(sequenceLen)) != _cmpOperands.at(sequenceLen)) - { - std::move(backIter, _buffer.end(), _utfPartials.begin()); - remainingLength -= sequenceLen; - _partialsLen = sequenceLen; - } - - break; - } - } - } - - // populate the part of the string that contains complete code points only - out = { _buffer.data(), remainingLength }; - - return S_OK; - } - catch (std::length_error&) - { - return E_ABORT; - } - catch (std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (...) - { - return E_UNEXPECTED; - } - } - - // Method Description: - // - Takes a UTF-16 string and populates it with *complete* UTF-16 codepoints. - // If it receives an incomplete codepoint, it will cache it until it can be completed. - // Arguments: - // - in - UTF-16 string_view potentially containing partial code points - // - out - on return, populated with complete codepoints at the string end - // Return Value: - // - S_OK - the resulting string doesn't end with a partial - // - S_FALSE - the resulting string contains the previously cached partials only - // - E_OUTOFMEMORY - the method failed to allocate memory for the resulting string - // - E_ABORT - the resulting string length would exceed the max_size and thus, the processing was aborted - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value, HRESULT>::type - operator()(const std::basic_string_view in, std::basic_string_view& out) noexcept - { - try - { - size_t remainingLength{ in.length() }; - size_t capacity{}; - - RETURN_HR_IF(E_ABORT, !base::CheckAdd(remainingLength, _partialsLen).AssignIfValid(&capacity)); - - _buffer.clear(); - _buffer.reserve(capacity); - - // copy UTF-8 code units that were remaining from the previous call (if any) - if (_partialsLen != 0u) - { - _buffer.push_back(_utfPartials.front()); - _partialsLen = 0u; - } - - if (in.empty()) - { - out = _buffer; - if (_buffer.empty()) - { - return S_OK; - } - - return S_FALSE; // the high surrogate is populated - } - - // cache the last value in the string if it is in the range of high surrogates - if (in.back() >= 0xD800u && in.back() <= 0xDBFFu) - { - _utfPartials.front() = in.back(); - --remainingLength; - _partialsLen = 1u; - } - else - { - _partialsLen = 0u; - } - - // populate the part of the string that contains complete code points only - _buffer.append(in, 0u, remainingLength); - out = _buffer; - - return S_OK; - } - catch (std::length_error&) - { - return E_ABORT; - } - catch (std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (...) - { - return E_UNEXPECTED; - } - } - - // Method Description: - // - Discard cached partials. - // Arguments: - // - none - // Return Value: - // - void - void reset() noexcept - { - _partialsLen = 0u; - } - - private: - enum _Utf8BitMasks : BYTE - { - IsAsciiByte = 0b0'0000000, // Any byte representing an ASCII character has the MSB set to 0 - MaskAsciiByte = 0b1'0000000, // Bit mask to be used in a bitwise AND operation to find out whether or not a byte match the IsAsciiByte pattern - IsContinuationByte = 0b10'000000, // Continuation bytes of any UTF-8 non-ASCII character have the MSB set to 1 and the adjacent bit set to 0 - MaskContinuationByte = 0b11'000000, // Bit mask to be used in a bitwise AND operation to find out whether or not a byte match the IsContinuationByte pattern - IsLeadByteTwoByteSequence = 0b110'00000, // A lead byte that indicates a UTF-8 non-ASCII character consisting of two bytes has the two highest bits set to 1 and the adjacent bit set to 0 - MaskLeadByteTwoByteSequence = 0b111'00000, // Bit mask to be used in a bitwise AND operation to find out whether or not a lead byte match the IsLeadByteTwoByteSequence pattern - IsLeadByteThreeByteSequence = 0b1110'0000, // A lead byte that indicates a UTF-8 non-ASCII character consisting of three bytes has the three highest bits set to 1 and the adjacent bit set to 0 - MaskLeadByteThreeByteSequence = 0b1111'0000, // Bit mask to be used in a bitwise AND operation to find out whether or not a lead byte match the IsLeadByteThreeByteSequence pattern - IsLeadByteFourByteSequence = 0b11110'000, // A lead byte that indicates a UTF-8 non-ASCII character consisting of four bytes has the four highest bits set to 1 and the adjacent bit set to 0 - MaskLeadByteFourByteSequence = 0b11111'000 // Bit mask to be used in a bitwise AND operation to find out whether or not a lead byte match the IsLeadByteFourByteSequence pattern - }; - - // array of bitmasks - constexpr static std::array _cmpMasks{ - 0, // unused - _Utf8BitMasks::MaskContinuationByte, - _Utf8BitMasks::MaskLeadByteTwoByteSequence, - _Utf8BitMasks::MaskLeadByteThreeByteSequence, - }; - - // array of values for the comparisons - constexpr static std::array _cmpOperands{ - 0, // unused - _Utf8BitMasks::IsAsciiByte, // intentionally conflicts with MaskContinuationByte - _Utf8BitMasks::IsLeadByteTwoByteSequence, - _Utf8BitMasks::IsLeadByteThreeByteSequence, - }; - - std::basic_string _buffer; // buffer to which the populated string_view refers - std::array _utfPartials; // buffer for code units of a partial code point that have to be cached - size_t _partialsLen{}; // number of cached code units }; - // make clear what incoming string type the state is for - typedef u8u16state u8state; - typedef u8u16state u16state; + // state structure for maintenance of UTF-16 partials + struct u16state + { + wchar_t partials[2]{}; + + constexpr void reset() noexcept + { + *this = {}; + } + }; // Routine Description: // - Takes a UTF-8 string and performs the conversion to UTF-16. NOTE: The function relies on getting complete UTF-8 characters at the string boundaries. @@ -269,62 +56,120 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // - S_OK - the conversion succeeded // - E_OUTOFMEMORY - the function failed to allocate memory for the resulting string // - E_ABORT - the resulting string length would exceed the upper boundary of an int and thus, the conversion was aborted before the conversion has been completed - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value && std::is_same::value, HRESULT>::type - u8u16(const inT in, outT& out) noexcept + // - E_UNEXPECTED - the underlying conversion function failed + // - HRESULT value converted from a caught exception + template + [[nodiscard]] HRESULT u8u16(const std::string_view& in, outT& out) noexcept { try { out.clear(); - - if (in.empty()) - { - return S_OK; - } + RETURN_HR_IF(S_OK, in.empty()); int lengthRequired{}; // The worst ratio of UTF-8 code units to UTF-16 code units is 1 to 1 if UTF-8 consists of ASCII only. RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(in.length()).AssignIfValid(&lengthRequired)); out.resize(in.length()); // avoid to call MultiByteToWideChar twice only to get the required size - const int lengthOut = MultiByteToWideChar(gsl::narrow_cast(CP_UTF8), 0ul, in.data(), lengthRequired, out.data(), lengthRequired); + const int lengthOut = MultiByteToWideChar(CP_UTF8, 0ul, in.data(), lengthRequired, out.data(), lengthRequired); out.resize(gsl::narrow_cast(lengthOut)); return lengthOut == 0 ? E_UNEXPECTED : S_OK; } - catch (std::length_error&) - { - return E_ABORT; - } - catch (std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (...) - { - return E_UNEXPECTED; - } + CATCH_RETURN(); } +#pragma warning(push) +#pragma warning(disable : 26429 26446 26459 26481 26482) // use not_null, subscript operator, use span, pointer arithmetic, dynamic array indexing // Routine Description: // - Takes a UTF-8 string, complements and/or caches partials, and performs the conversion to UTF-16. // Arguments: // - in - UTF-8 string to be converted // - out - reference to the resulting UTF-16 string - // - state - reference to a til::u8state class holding the status of the current partials handling + // - state - reference to a til::u8state holding the status of the current partials handling // Return Value: // - S_OK - the conversion succeeded // - E_OUTOFMEMORY - the function failed to allocate memory for the resulting string // - E_ABORT - the resulting string length would exceed the upper boundary of an int and thus, the conversion was aborted before the conversion has been completed - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value && std::is_same::value, HRESULT>::type - u8u16(const inT in, outT& out, u8state& state) noexcept + // - E_UNEXPECTED - the underlying conversion function failed + // - HRESULT value converted from a caught exception + template + [[nodiscard]] HRESULT u8u16(const std::string_view& in, outT& out, u8state& state) noexcept { - std::string_view sv{}; - RETURN_IF_FAILED(state(std::string_view{ in }, sv)); - return til::u8u16(sv, out); + try + { + out.clear(); + RETURN_HR_IF(S_OK, in.empty()); + + int capa16{}; + // The worst ratio of UTF-8 code units to UTF-16 code units is 1 to 1 if UTF-8 consists of ASCII only. + RETURN_HR_IF(E_ABORT, !base::CheckAdd(in.length(), state.have).AssignIfValid(&capa16)); + + out.resize(gsl::narrow_cast(capa16)); + auto len8{ gsl::narrow_cast(in.length()) }; + int len16{}; + auto cursor8{ in.data() }; + if (state.have) + { + const auto copyable{ std::min(state.want, len8) }; + std::move(cursor8, cursor8 + copyable, &state.partials[state.have]); + state.have += gsl::narrow_cast(copyable); + state.want -= gsl::narrow_cast(copyable); + if (state.want) // we still didn't get enough data to complete the code point, however this is not an error + { + out.clear(); + return S_OK; + } + + len16 = MultiByteToWideChar(CP_UTF8, 0UL, &state.partials[0], gsl::narrow_cast(state.have), out.data(), capa16); + RETURN_HR_IF(E_UNEXPECTED, !len16); + + capa16 -= len16; + len8 -= copyable; + cursor8 += copyable; + // state.want is already zero at this point + state.have = 0; + } + + if (len8) + { + auto backIter{ cursor8 + len8 - 1 }; + int sequenceLen{ 1 }; + + // skip UTF8 continuation bytes + while (backIter != cursor8 && (*backIter & 0b11'000000) == 0b10'000000) + { + --backIter; + ++sequenceLen; + } + + // credits go to Christopher Wellons for this algorithm to determine the length of a UTF-8 code point + // it is released into the Public Domain. https://github.com/skeeto/branchless-utf8 + static constexpr uint8_t lengths[]{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 3, 3, 4, 0 }; + const auto codePointLen{ lengths[gsl::narrow_cast(*backIter) >> 3] }; + + if (codePointLen > sequenceLen) + { + std::move(backIter, backIter + sequenceLen, &state.partials[0]); + len8 -= sequenceLen; + state.have = gsl::narrow_cast(sequenceLen); + state.want = gsl::narrow_cast(codePointLen - sequenceLen); + } + } + + if (len8) + { + const auto convLen{ MultiByteToWideChar(CP_UTF8, 0UL, cursor8, len8, out.data() + len16, capa16) }; + RETURN_HR_IF(E_UNEXPECTED, !convLen); + + len16 += convLen; + } + + out.resize(gsl::narrow_cast(len16)); + return S_OK; + } + CATCH_RETURN(); } +#pragma warning(pop) // Routine Description: // - Takes a UTF-16 string and performs the conversion to UTF-8. NOTE: The function relies on getting complete UTF-16 characters at the string boundaries. @@ -335,19 +180,15 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // - S_OK - the conversion succeeded // - E_OUTOFMEMORY - the function failed to allocate memory for the resulting string // - E_ABORT - the resulting string length would exceed the upper boundary of an int and thus, the conversion was aborted before the conversion has been completed - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value && std::is_same::value, HRESULT>::type - u16u8(const inT in, outT& out) noexcept + // - E_UNEXPECTED - the underlying conversion function failed + // - HRESULT value converted from a caught exception + template + [[nodiscard]] HRESULT u16u8(const std::wstring_view& in, outT& out) noexcept { try { out.clear(); - - if (in.empty()) - { - return S_OK; - } + RETURN_HR_IF(S_OK, in.empty()); int lengthIn{}; int lengthRequired{}; @@ -356,25 +197,16 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Thus, the worst ratio of UTF-16 code units to UTF-8 code units is 1 to 3. RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(in.length()).AssignIfValid(&lengthIn) || !base::CheckMul(lengthIn, 3).AssignIfValid(&lengthRequired)); out.resize(gsl::narrow_cast(lengthRequired)); // avoid to call WideCharToMultiByte twice only to get the required size - const int lengthOut = WideCharToMultiByte(gsl::narrow_cast(CP_UTF8), 0ul, in.data(), lengthIn, out.data(), lengthRequired, nullptr, nullptr); + const int lengthOut = WideCharToMultiByte(CP_UTF8, 0ul, in.data(), lengthIn, out.data(), lengthRequired, nullptr, nullptr); out.resize(gsl::narrow_cast(lengthOut)); return lengthOut == 0 ? E_UNEXPECTED : S_OK; } - catch (std::length_error&) - { - return E_ABORT; - } - catch (std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (...) - { - return E_UNEXPECTED; - } + CATCH_RETURN(); } +#pragma warning(push) +#pragma warning(disable : 26429 26446 26459 26481) // use not_null, subscript operator, use span, pointer arithmetic // Routine Description: // - Takes a UTF-16 string, complements and/or caches partials, and performs the conversion to UTF-8. // Arguments: @@ -385,15 +217,60 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // - S_OK - the conversion succeeded without any change of the represented code points // - E_OUTOFMEMORY - the function failed to allocate memory for the resulting string // - E_ABORT - the resulting string length would exceed the upper boundary of an int and thus, the conversion was aborted before the conversion has been completed - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value && std::is_same::value, HRESULT>::type - u16u8(const inT in, outT& out, u16state& state) noexcept + // - E_UNEXPECTED - the underlying conversion function failed + // - HRESULT value converted from a caught exception + template + [[nodiscard]] HRESULT u16u8(const std::wstring_view& in, outT& out, u16state& state) noexcept { - std::wstring_view sv{}; - RETURN_IF_FAILED(state(std::wstring_view{ in }, sv)); - return u16u8(sv, out); + try + { + out.clear(); + RETURN_HR_IF(S_OK, in.empty()); + + int len16{}; + int capa8{}; + // The worst ratio of UTF-16 code units to UTF-8 code units is 1 to 3. + RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(in.length()).AssignIfValid(&len16) || !base::CheckAdd(len16, gsl::narrow_cast(state.partials[0]) != 0).AssignIfValid(&capa8) || !base::CheckMul(capa8, 3).AssignIfValid(&capa8)); + + out.resize(gsl::narrow_cast(capa8)); + int len8{}; + auto cursor16{ in.data() }; + if (state.partials[0]) + { + state.partials[1] = *cursor16; + len8 = WideCharToMultiByte(CP_UTF8, 0UL, &state.partials[0], 2, out.data(), capa8, nullptr, nullptr); + RETURN_HR_IF(E_UNEXPECTED, !len8); + + state.reset(); + capa8 -= len8; + --len16; + ++cursor16; + } + + if (len16) + { + const auto back = *(cursor16 + len16 - 1); + if (back >= 0xD800 && back <= 0xDBFF) // cache the last value in the string if it is in the range of high surrogates + { + state.partials[0] = back; + --len16; + } + } + + if (len16) + { + const auto convLen{ WideCharToMultiByte(CP_UTF8, 0UL, cursor16, len16, out.data() + len8, capa8, nullptr, nullptr) }; + RETURN_HR_IF(E_UNEXPECTED, !convLen); + + len8 += convLen; + } + + out.resize(gsl::narrow_cast(len8)); + return S_OK; + } + CATCH_RETURN(); } +#pragma warning(pop) // Routine Description: // - Takes a UTF-8 string and performs the conversion to UTF-16. NOTE: The function relies on getting complete UTF-8 characters at the string boundaries. @@ -402,12 +279,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Return Value: // - the resulting UTF-16 string // - NOTE: Throws HRESULT errors that the non-throwing sibling returns - template - typename std::enable_if::value, std::wstring>::type - u8u16(const inT in) + inline std::wstring u8u16(const std::string_view& in) { std::wstring out{}; - THROW_IF_FAILED(u8u16(std::string_view{ in }, out)); + THROW_IF_FAILED(u8u16(in, out)); return out; } @@ -419,12 +294,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Return Value: // - the resulting UTF-16 string // - NOTE: Throws HRESULT errors that the non-throwing sibling returns - template - typename std::enable_if::value, std::wstring>::type - u8u16(const inT in, u8state& state) + inline std::wstring u8u16(const std::string_view& in, u8state& state) { std::wstring out{}; - THROW_IF_FAILED(u8u16(std::string_view{ in }, out, state)); + THROW_IF_FAILED(u8u16(in, out, state)); return out; } @@ -435,12 +308,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Return Value: // - the resulting UTF-8 string // - NOTE: Throws HRESULT errors that the non-throwing sibling returns - template - typename std::enable_if::value, std::string>::type - u16u8(const inT in) + inline std::string u16u8(const std::wstring_view& in) { std::string out{}; - THROW_IF_FAILED(u16u8(std::wstring_view{ in }, out)); + THROW_IF_FAILED(u16u8(in, out)); return out; } @@ -452,12 +323,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Return Value: // - the resulting UTF-8 string // - NOTE: Throws HRESULT errors that the non-throwing sibling returns - template - typename std::enable_if::value, std::string>::type - u16u8(const inT in, u16state& state) + inline std::string u16u8(const std::wstring_view& in, u16state& state) { std::string out{}; - THROW_IF_FAILED(u16u8(std::wstring_view{ in }, out, state)); + THROW_IF_FAILED(u16u8(in, out, state)); return out; } } From 23a19c58181b489b10a2049232f23b033dbdab8d Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 24 Aug 2021 04:49:45 -0500 Subject: [PATCH 89/90] Only focus the active pane once initialization is complete (#10978) ## Summary of the Pull Request Since the days immemorial of the Terminal, the TermControl has auto-focused itself when it finalizes its layout. This has led to the problem that `wt ; sp ; sp ; sp...` ends up focusing one of these panes at random. This PR fixes this issue by getting rid of the auto-focusing. Panes now manually get focused when created. We manually focus the active pane when a commandline is dispatched. since we're internally tracking "active" separate from "focused", this ends up working as you'd hope. ## References ## PR Checklist * [x] Closes #6586 * [x] I work here * [ ] Tests added/passed * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments I also had to turn the cursor off by default. Most `TermControl`s would never get the `LostFocus` event, so their cursors would get left `On`, and that's not right. ## Validation Steps Performed I've run the following things a bunch of times to make sure they work: * `wtd sp ; sp ; sp` * `wtd sp ; sp ; sp ; fp -t 0` * `newTab` * `splitPane` * use the command palette to do the above as well Where the result used to be random (cases 1 & 2), the result is exactly what you'd expect now. It doesn't work at all for ``` wtd sp ; sp ; sp ; mf left ``` Presumably because we can't `move-focus` directionally during startup. However, that doesn't work _today_ either, so it's not making it worse. Just highlights that single scenario doesn't work right. --- src/cascadia/TerminalApp/TerminalPage.cpp | 13 +++++++++++++ src/cascadia/TerminalControl/TermControl.cpp | 11 ++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 12cec3594..cba5ea4a9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -457,6 +457,11 @@ namespace winrt::TerminalApp::implementation co_return; } } + + // GH#6586: now that we're done processing all startup commands, + // focus the active control. This will work as expected for both + // commandline invocations and for `wt` action invocations. + _GetActiveControl().Focus(FocusState::Programmatic); } if (initial) { @@ -1414,6 +1419,14 @@ namespace winrt::TerminalApp::implementation _UnZoomIfNeeded(); tab.SplitPane(realSplitType, splitSize, profile, newControl); + + // After GH#6586, the control will no longer focus itself + // automatically when it's finished being laid out. Manually focus + // the control here instead. + if (_startupState == StartupState::Initialized) + { + _GetActiveControl().Focus(FocusState::Programmatic); + } } CATCH_LOG(); } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index aaa562c34..e36fadd6a 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -681,8 +681,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation DispatcherTimer cursorTimer; cursorTimer.Interval(std::chrono::milliseconds(blinkTime)); cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick }); - cursorTimer.Start(); _cursorTimer.emplace(std::move(cursorTimer)); + // As of GH#6586, don't start the cursor timer immediately, and + // don't show the cursor initially. We'll show the cursor and start + // the timer when the control is first focused. cursorTimer.Start(); + _core.CursorOn(false); } else { @@ -711,12 +714,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Now that the renderer is set up, update the appearance for initialization _UpdateAppearanceFromUIThread(_settings); - // Focus the control here. If we do it during control initialization, then - // focus won't actually get passed to us. I believe this is because - // we're not technically a part of the UI tree yet, so focusing us - // becomes a no-op. - this->Focus(FocusState::Programmatic); - _initializedTerminal = true; // MSFT 33353327: If the AutomationPeer was created before we were done initializing, From f9a844dbdacff8b7b6b0a1221de7634668fa8ca7 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 24 Aug 2021 08:10:36 -0500 Subject: [PATCH 90/90] Lookup WSL distros in the registry (#10967) This PR converts the WSL distro generator to use the registry to lookup WSL distros instead of trying to parse the results of `wsl.exe`. `wsl.exe` sometimes takes a very long time to launch the WSL service, which means that on the first launch of the Terminal, WSL distros can sometimes be missing entirely! ## References * Also related is #6160, but I feel that deserves a separate PR for warning when the default profile is a dynamic profile who's source indicated it was gone. ## PR Checklist * [x] Closes #9905 * [x] Closes #7199 * [x] I work here * [ ] Tests added/passed * [ ] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments This is maybe a little BODGY, but hey we get tons of reports of this root cause. ## Validation Steps Performed Ran it locally, it did well. Ran a `wsl --shutdown`, then booted the terminal - seemed to do well. I never was able to repro the slowness myself, but I'd suspect this'll fix it. --- .github/actions/spelling/allow/microsoft.txt | 1 + .../WslDistroGenerator.cpp | 189 +++++++++++++++++- 2 files changed, 189 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index ca1b1cadb..87d7a3d8c 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -25,6 +25,7 @@ DWINRT enablewttlogging Intelli LKG +Lxss mfcribbon microsoft microsoftonline diff --git a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp index 7172bdcc7..799602941 100644 --- a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp @@ -15,6 +15,13 @@ static constexpr std::wstring_view DockerDistributionPrefix{ L"docker-desktop" }; +// The WSL entries are structured as such: +// HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss +// ⌞ {distroGuid} +// ⌞ DistributionName: {the name} +static constexpr wchar_t RegKeyLxss[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss"; +static constexpr wchar_t RegKeyDistroName[] = L"DistributionName"; + using namespace ::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::Terminal::Settings::Model; @@ -35,7 +42,7 @@ std::wstring_view WslDistroGenerator::GetNamespace() // - // Return Value: // - a vector with all distros for all the installed WSL distros -std::vector WslDistroGenerator::GenerateProfiles() +static std::vector legacyGenerate() { std::vector profiles; @@ -136,3 +143,183 @@ std::vector WslDistroGenerator::GenerateProfiles() return profiles; } + +// Function Description: +// - Create a list of Profiles for each distro listed in names. +// - Skips distros that are utility distros for docker (see GH#3556) +// Arguments: +// - names: a list of distro names to turn into profiles +// Return Value: +// - the list of profiles we've generated. +static std::vector namesToProfiles(const std::vector& names) +{ + std::vector profiles; + for (const auto& distName : names) + { + if (til::starts_with(distName, DockerDistributionPrefix)) + { + // Docker for Windows creates some utility distributions to handle Docker commands. + // Pursuant to GH#3556, because they are _not_ user-facing we want to hide them. + continue; + } + + auto WSLDistro{ CreateDefaultProfile(distName) }; + + WSLDistro.Commandline(L"wsl.exe -d " + distName); + WSLDistro.DefaultAppearance().ColorSchemeName(L"Campbell"); + WSLDistro.StartingDirectory(DEFAULT_STARTING_DIRECTORY); + WSLDistro.Icon(L"ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png"); + profiles.emplace_back(WSLDistro); + } + return profiles; +} + +// Function Description: +// - Open the reg key the root of the WSL data, in HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss +// Arguments: +// - +// Return Value: +// - the HKEY if it exists and we can read it, else nullptr +static wil::unique_hkey openWslRegKey() +{ + HKEY hKey{ nullptr }; + if (RegOpenKeyEx(HKEY_CURRENT_USER, RegKeyLxss, 0, KEY_READ, &hKey) == ERROR_SUCCESS) + { + return wil::unique_hkey{ hKey }; + } + return nullptr; +} + +// Function Description: +// - Open the reg key for a single distro, underneath the root WSL key. +// Arguments: +// - wslRootKey: the HKEY for the Lxss node. +// - guid: the string representation of the GUID for the distro to inspect +// Return Value: +// - the HKEY if it exists and we can read it, else nullptr +static wil::unique_hkey openDistroKey(const wil::unique_hkey& wslRootKey, const std::wstring& guid) +{ + HKEY hKey{ nullptr }; + if (RegOpenKeyEx(wslRootKey.get(), guid.c_str(), 0, KEY_READ, &hKey) == ERROR_SUCCESS) + { + return wil::unique_hkey{ hKey }; + } + return nullptr; +} + +// Function Description: +// - Get the list of all the guids of all the WSL distros from the registry. If +// we fail to open or read the root reg key, we'll return false. +// Places the guids of all the distros into the "guidStrings" param. +// Arguments: +// - wslRootKey: the HKEY for the Lxss node. +// - names: a vector that receives all the guids of the installed distros. +// Return Value: +// - false if we failed to enumerate all the WSL distros +static bool getWslGuids(const wil::unique_hkey& wslRootKey, + std::vector& guidStrings) +{ + if (!wslRootKey) + { + return false; + } + + wchar_t buffer[39]; // a {GUID} is 38 chars long + for (DWORD i = 0;; i++) + { + DWORD length = 39; + const auto result = RegEnumKeyEx(wslRootKey.get(), i, &buffer[0], &length, nullptr, nullptr, nullptr, nullptr); + if (result == ERROR_NO_MORE_ITEMS) + { + break; + } + + if (result == ERROR_SUCCESS && + length == 38 && + buffer[0] == L'{' && + buffer[37] == L'}') + { + guidStrings.emplace_back(&buffer[0], length); + } + } + + return true; +} + +// Function Description: +// - Get the list of all the names of all the WSL distros from the registry. If +// we fail to open any regkey for the GUID of a distro, we'll just skip it. +// Places the names of all the distros into the "names" param. +// Arguments: +// - wslRootKey: the HKEY for the Lxss node. +// - guidStrings: A list of all the GUIDs of the installed distros +// - names: a vector that receives all the names of the installed distros. +// Return Value: +// - false if the root key was invalid, else true. +static bool getWslNames(const wil::unique_hkey& wslRootKey, + const std::vector& guidStrings, + std::vector& names) +{ + if (!wslRootKey) + { + return false; + } + for (const auto& guid : guidStrings) + { + wil::unique_hkey distroKey{ openDistroKey(wslRootKey, guid) }; + if (!distroKey) + { + continue; + } + + std::wstring buffer; + auto result = wil::AdaptFixedSizeToAllocatedResult(buffer, [&](PWSTR value, size_t valueLength, size_t* valueLengthNeededWithNull) -> HRESULT { + auto length = static_cast(valueLength); + const auto status = RegQueryValueExW(distroKey.get(), RegKeyDistroName, 0, nullptr, reinterpret_cast(value), &length); + // length will receive the number of bytes - convert to a number of + // wchar_t's. AdaptFixedSizeToAllocatedResult will resize buffer to + // valueLengthNeededWithNull + *valueLengthNeededWithNull = (length / sizeof(wchar_t)); + // If you add one for another trailing null, then there'll actually + // be _two_ trailing nulls in the buffer. + return status == ERROR_MORE_DATA ? S_OK : HRESULT_FROM_WIN32(status); + }); + + if (result != S_OK) + { + continue; + } + names.emplace_back(std::move(buffer)); + } + return true; +} + +// Method Description: +// - Generate a list of profiles for each on the installed WSL distros. This +// will first try to read the installed distros from the registry. If that +// fails, we'll fall back to the legacy way of launching WSL.exe to read the +// distros from the commandline. Reading the registry is slightly more stable +// (see GH#7199, GH#9905), but it is certainly BODGY +// Arguments: +// - +// Return Value: +// - A list of WSL profiles. +std::vector WslDistroGenerator::GenerateProfiles() +{ + wil::unique_hkey wslRootKey{ openWslRegKey() }; + if (wslRootKey) + { + std::vector guidStrings{}; + if (getWslGuids(wslRootKey, guidStrings)) + { + std::vector names{}; + names.reserve(guidStrings.size()); + if (getWslNames(wslRootKey, guidStrings, names)) + { + return namesToProfiles(names); + } + } + } + + return legacyGenerate(); +}