diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 01cf71dbca9..770f23f697d 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -441,6 +441,38 @@ } ] }, + "ScrollUpAction": { + "description": "Arguments for a scrollUp action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "scrollUp" }, + "rowsToScroll": { + "type": ["integer", "null"], + "default": null, + "description": "Scroll up rowsToScroll lines. If no value is provided, use the system-level defaults." + } + } + } + ] + }, + "ScrollDownAction": { + "description": "Arguments for a scrollDown action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "scrollDown" }, + "rowsToScroll": { + "type": ["integer", "null"], + "default": null, + "description": "Scroll down rowsToScroll lines. If no value is provided, use the system-level defaults." + } + } + } + ] + }, "Keybinding": { "additionalProperties": false, "properties": { @@ -462,6 +494,8 @@ { "$ref": "#/definitions/WtAction" }, { "$ref": "#/definitions/CloseOtherTabsAction" }, { "$ref": "#/definitions/CloseTabsAfterAction" }, + { "$ref": "#/definitions/ScrollUpAction" }, + { "$ref": "#/definitions/ScrollDownAction" }, { "type": "null" } ] }, diff --git a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp index 5ff3f2facad..78ac3ada249 100644 --- a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp @@ -46,6 +46,8 @@ namespace SettingsModelLocalTests TEST_METHOD(TestSetTabColorArgs); + TEST_METHOD(TestScrollArgs); + TEST_CLASS_SETUP(ClassSetup) { InitializeJsonReader(); @@ -452,4 +454,89 @@ namespace SettingsModelLocalTests VERIFY_IS_FALSE(realArgs.SingleLine()); } } + + void KeyBindingsTests::TestScrollArgs() + { + const std::string bindings0String{ R"([ + { "keys": ["up"], "command": "scrollUp" }, + { "keys": ["down"], "command": "scrollDown" }, + { "keys": ["ctrl+up"], "command": { "action": "scrollUp" } }, + { "keys": ["ctrl+down"], "command": { "action": "scrollDown" } }, + { "keys": ["ctrl+shift+up"], "command": { "action": "scrollUp", "rowsToScroll": 10 } }, + { "keys": ["ctrl+shift+down"], "command": { "action": "scrollDown", "rowsToScroll": 10 } } + ])" }; + + const auto bindings0Json = VerifyParseSucceeded(bindings0String); + + auto keymap = winrt::make_self(); + VERIFY_IS_NOT_NULL(keymap); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(6u, keymap->_keyShortcuts.size()); + + { + KeyChord kc{ false, false, false, static_cast(VK_UP) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NULL(realArgs.RowsToScroll()); + } + { + KeyChord kc{ false, false, false, static_cast(VK_DOWN) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NULL(realArgs.RowsToScroll()); + } + { + KeyChord kc{ true, false, false, static_cast(VK_UP) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NULL(realArgs.RowsToScroll()); + } + { + KeyChord kc{ true, false, false, static_cast(VK_DOWN) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NULL(realArgs.RowsToScroll()); + } + { + KeyChord kc{ true, false, true, static_cast(VK_UP) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // 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) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.RowsToScroll()); + VERIFY_ARE_EQUAL(10u, realArgs.RowsToScroll().Value()); + } + { + const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "scrollDown", "rowsToScroll": -1 } }])" }; + const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString); + auto invalidKeyMap = winrt::make_self(); + VERIFY_IS_NOT_NULL(invalidKeyMap); + VERIFY_ARE_EQUAL(0u, invalidKeyMap->_keyShortcuts.size()); + VERIFY_THROWS(invalidKeyMap->LayerJson(bindingsInvalidJson);, std::exception); + } + } } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index fc5dd1582c7..d71f00fc120 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -65,15 +65,23 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleScrollUp(const IInspectable& /*sender*/, const ActionEventArgs& args) { - _Scroll(-1); - args.Handled(true); + const auto& realArgs = args.ActionArgs().try_as(); + if (realArgs) + { + _Scroll(ScrollUp, realArgs.RowsToScroll()); + args.Handled(true); + } } void TerminalPage::_HandleScrollDown(const IInspectable& /*sender*/, const ActionEventArgs& args) { - _Scroll(1); - args.Handled(true); + const auto& realArgs = args.ActionArgs().try_as(); + if (realArgs) + { + _Scroll(ScrollDown, realArgs.RowsToScroll()); + args.Handled(true); + } } void TerminalPage::_HandleNextTab(const IInspectable& /*sender*/, @@ -143,14 +151,14 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleScrollUpPage(const IInspectable& /*sender*/, const ActionEventArgs& args) { - _ScrollPage(-1); + _ScrollPage(ScrollUp); args.Handled(true); } void TerminalPage::_HandleScrollDownPage(const IInspectable& /*sender*/, const ActionEventArgs& args) { - _ScrollPage(1); + _ScrollPage(ScrollDown); args.Handled(true); } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 2b9be96ef29..4330bbfb8c5 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -89,6 +89,10 @@ namespace winrt::TerminalApp::implementation _RefreshUIForSettingsReload(); } + // Upon settings update we reload the system settings for scrolling as well. + // TODO: consider reloading this value periodically. + _systemRowsToScroll = _ReadSystemRowsToScroll(); + auto weakThis{ get_weak() }; co_await winrt::resume_foreground(Dispatcher()); if (auto page{ weakThis.get() }) @@ -1413,16 +1417,30 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Move the viewport of the terminal of the currently focused tab up or - // down a number of lines. Negative values of `delta` will move the - // view up, and positive values will move the viewport down. + // down a number of lines. // Arguments: - // - delta: a number of lines to move the viewport relative to the current viewport. - void TerminalPage::_Scroll(int delta) + // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down + // - rowsToScroll: a number of lines to move the viewport. If not provided we will use a system default. + void TerminalPage::_Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll) { if (auto index{ _GetFocusedTabIndex() }) { auto focusedTab{ _GetStrongTabImpl(*index) }; - focusedTab->Scroll(delta); + uint32_t realRowsToScroll; + if (rowsToScroll == nullptr) + { + // The magic value of WHEEL_PAGESCROLL indicates that we need to scroll the entire page + realRowsToScroll = _systemRowsToScroll == WHEEL_PAGESCROLL ? + focusedTab->GetActiveTerminalControl().GetViewHeight() : + _systemRowsToScroll; + } + else + { + // use the custom value specified in the command + realRowsToScroll = rowsToScroll.Value(); + } + auto scrollDelta = _ComputeScrollDelta(scrollDirection, realRowsToScroll); + focusedTab->Scroll(scrollDelta); } } @@ -1540,12 +1558,9 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Move the viewport of the terminal of the currently focused tab up or // down a page. The page length will be dependent on the terminal view height. - // Negative values of `delta` will move the view up by one page, and positive values - // will move the viewport down by one page. // Arguments: - // - delta: The direction to move the view relative to the current viewport(it - // is clamped between -1 and 1) - void TerminalPage::_ScrollPage(int delta) + // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down + void TerminalPage::_ScrollPage(ScrollDirection scrollDirection) { auto indexOpt = _GetFocusedTabIndex(); // Do nothing if for some reason, there's no tab in focus. We don't want to crash. @@ -1554,11 +1569,11 @@ namespace winrt::TerminalApp::implementation return; } - delta = std::clamp(delta, -1, 1); const auto control = _GetActiveControl(); const auto termHeight = control.GetViewHeight(); auto focusedTab{ _GetStrongTabImpl(*indexOpt) }; - focusedTab->Scroll(termHeight * delta); + auto scrollDelta = _ComputeScrollDelta(scrollDirection, termHeight); + focusedTab->Scroll(scrollDelta); } // Method Description: @@ -2552,6 +2567,39 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - Computes the delta for scrolling the tab's viewport. + // Arguments: + // - scrollDirection - direction (up / down) to scroll + // - rowsToScroll - the number of rows to scroll + // Return Value: + // - delta - Signed delta, where a negative value means scrolling up. + int TerminalPage::_ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll) + { + return scrollDirection == ScrollUp ? -1 * rowsToScroll : rowsToScroll; + } + + // Method Description: + // - Reads system settings for scrolling (based on the step of the mouse scroll). + // Upon failure fallbacks to default. + // Return Value: + // - The number of rows to scroll or a magic value of WHEEL_PAGESCROLL + // indicating that we need to scroll an entire view height + uint32_t TerminalPage::_ReadSystemRowsToScroll() + { + uint32_t systemRowsToScroll; + if (!SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &systemRowsToScroll, 0)) + { + LOG_LAST_ERROR(); + + // If SystemParametersInfoW fails, which it shouldn't, fall back to + // Windows' default value. + return DefaultRowsToScroll; + } + + return systemRowsToScroll; + } + // Method Description: // - Bumps the tab in its in-order index up to the top of the mru list. // Arguments: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 2001eaaf1d4..e651fc6c58e 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -12,6 +12,8 @@ #include "AppCommandlineArgs.h" +static constexpr uint32_t DefaultRowsToScroll{ 3 }; + // fwdecl unittest classes namespace TerminalAppLocalTests { @@ -28,6 +30,12 @@ namespace winrt::TerminalApp::implementation Initialized = 2 }; + enum ScrollDirection : int + { + ScrollUp = 0, + ScrollDown = 1 + }; + struct TerminalPage : TerminalPageT { public: @@ -102,6 +110,8 @@ namespace winrt::TerminalApp::implementation std::optional _rearrangeFrom; std::optional _rearrangeTo; + uint32_t _systemRowsToScroll{ DefaultRowsToScroll }; + // use a weak reference to prevent circular dependency with AppLogic winrt::weak_ref _dialogPresenter; @@ -165,10 +175,10 @@ namespace winrt::TerminalApp::implementation // Todo: add more event implementations here // MSFT:20641986: Add keybindings for New Window - void _Scroll(int delta); + void _Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll); void _SplitPane(const Microsoft::Terminal::Settings::Model::SplitState splitType, const Microsoft::Terminal::Settings::Model::SplitType splitMode = Microsoft::Terminal::Settings::Model::SplitType::Manual, const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr); void _ResizePane(const Microsoft::Terminal::Settings::Model::Direction& direction); - void _ScrollPage(int delta); + void _ScrollPage(ScrollDirection scrollDirection); void _SetAcceleratorForMenuItem(Windows::UI::Xaml::Controls::MenuFlyoutItem& menuItem, const winrt::Microsoft::Terminal::TerminalControl::KeyChord& keyChord); winrt::fire_and_forget _CopyToClipboardHandler(const IInspectable sender, const winrt::Microsoft::Terminal::TerminalControl::CopyToClipboardEventArgs copiedData); @@ -206,6 +216,9 @@ namespace winrt::TerminalApp::implementation void _UnZoomIfNeeded(); + static int _ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll); + static uint32_t _ReadSystemRowsToScroll(); + void _UpdateTabSwitcherCommands(const bool mru); void _UpdateMRUTab(const uint32_t index); diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 14dedc0a24f..66a58270ccc 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -129,6 +129,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::SetTabColor, SetTabColorArgs::FromJson }, { ShortcutAction::SplitPane, SplitPaneArgs::FromJson }, { ShortcutAction::SwitchToTab, SwitchToTabArgs::FromJson }, + { ShortcutAction::ScrollUp, ScrollUpArgs::FromJson }, + { ShortcutAction::ScrollDown, ScrollDownArgs::FromJson }, { ShortcutAction::Invalid, nullptr }, }; diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index cc9b97d83ab..4271ba8099f 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -362,4 +362,28 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } return RS_(L"CloseTabsAfterDefaultCommandKey"); } + + winrt::hstring ScrollUpArgs::GenerateName() const + { + if (_RowsToScroll) + { + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"ScrollUpSeveralRowsCommandKey")), + _RowsToScroll.Value()) + }; + } + return RS_(L"ScrollUpCommandKey"); + } + + winrt::hstring ScrollDownArgs::GenerateName() const + { + if (_RowsToScroll) + { + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"ScrollDownSeveralRowsCommandKey")), + _RowsToScroll.Value()) + }; + } + return RS_(L"ScrollDownCommandKey"); + } } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index c70e438373e..8562f587063 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -22,6 +22,8 @@ #include "ExecuteCommandlineArgs.g.h" #include "CloseOtherTabsArgs.g.h" #include "CloseTabsAfterArgs.g.h" +#include "ScrollUpArgs.g.h" +#include "ScrollDownArgs.g.h" #include "../../cascadia/inc/cppwinrt_utils.h" #include "JsonUtils.h" @@ -670,6 +672,74 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return *copy; } }; + + struct ScrollUpArgs : public ScrollUpArgsT + { + ScrollUpArgs() = default; + GETSET_PROPERTY(Windows::Foundation::IReference, RowsToScroll, nullptr); + + static constexpr std::string_view RowsToScrollKey{ "rowsToScroll" }; + + public: + hstring GenerateName() const; + + bool Equals(const IActionArgs& other) + { + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_RowsToScroll == _RowsToScroll; + } + 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, RowsToScrollKey, args->_RowsToScroll); + return { *args, {} }; + } + IActionArgs Copy() const + { + auto copy{ winrt::make_self() }; + copy->_RowsToScroll = _RowsToScroll; + return *copy; + } + }; + + struct ScrollDownArgs : public ScrollDownArgsT + { + ScrollDownArgs() = default; + GETSET_PROPERTY(Windows::Foundation::IReference, RowsToScroll, nullptr); + + static constexpr std::string_view RowsToScrollKey{ "rowsToScroll" }; + + public: + hstring GenerateName() const; + + bool Equals(const IActionArgs& other) + { + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_RowsToScroll == _RowsToScroll; + } + 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, RowsToScrollKey, args->_RowsToScroll); + return { *args, {} }; + } + IActionArgs Copy() const + { + auto copy{ winrt::make_self() }; + copy->_RowsToScroll = _RowsToScroll; + return *copy; + } + }; } namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 7a4d7eb0bf6..08fccbddae8 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -155,4 +155,14 @@ namespace Microsoft.Terminal.Settings.Model CloseTabsAfterArgs(UInt32 tabIndex); Windows.Foundation.IReference Index { get; }; }; + + [default_interface] runtimeclass ScrollUpArgs : IActionArgs + { + Windows.Foundation.IReference RowsToScroll { get; }; + }; + + [default_interface] runtimeclass ScrollDownArgs : IActionArgs + { + Windows.Foundation.IReference RowsToScroll { get; }; + }; } diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index bf902d54a6d..2bccbc967f0 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -267,13 +267,21 @@ {0} will be replaced with one of the four directions "DirectionLeft", "DirectionRight", "DirectionUp", or "DirectionDown" - Scroll down one line + Scroll down + + + Scroll down {0} line(s) + {0} will be replaced with the number of lines to scroll" Scroll down one page - Scroll up one line + Scroll up + + + Scroll up {0} line(s) + {0} will be replaced with the number of lines to scroll" Scroll up one page @@ -324,4 +332,4 @@ Toggle retro terminal effect - \ No newline at end of file +