From d08271e73447b817da6aeb1042961d711350e6a5 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 28 Apr 2021 17:13:28 -0500 Subject: [PATCH] Add `globalSummon` action (#9854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for two new actions: * `globalSummon`, which can be used to activate a window using a _global_ (READ: OS-level) hotkey. - accepts an optional `name` argument. When provided, this will attempt to summon with the given name. When omitted, we'll try to summon the most recent window. * `quakeMode` which is `globalSummon` for the `_quake` window. These actions are stored in the actions array, but are read by the `WindowsTerminal` level and bound to the OS in `IslandWindow`. The monarch registers for these keybindings with the OS. When one is pressed, the monarch will recieve a `WM_HOTKEY` message. It'll use that to look up the corresponding action args. It'll use those to try and summon the right window. ## References * #8888: Quake mode megathread * #9274: Spec (**guys seriously i just need one more ✔️**) * #9785: The start of granting "\_quake" super powers ## PR Checklist * [x] Closes #653 - I'm gonna say this closes it for now, though we have _many_ follow-ups in #8888 * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * Validated that it works with `win` keys * Validated that it works without `win` keys * Validated that it hot-reloads * Validated that it moves to the new monarch * Validated that you can bind both `globalSummon` and `quakeMode` at the same time and do different things * Validated that you can bind `globalSummon` with a name and it creates that name if it doesn't already exist --- .github/actions/spelling/dictionary/apis.txt | 1 + .../Microsoft.Terminal.RemotingLib.vcxproj | 6 + src/cascadia/Remoting/Monarch.cpp | 49 ++- src/cascadia/Remoting/Monarch.h | 1 + src/cascadia/Remoting/Monarch.idl | 12 + src/cascadia/Remoting/Peasant.cpp | 19 + src/cascadia/Remoting/Peasant.h | 3 + src/cascadia/Remoting/Peasant.idl | 7 +- .../Remoting/SummonWindowSelectionArgs.cpp | 5 + .../Remoting/SummonWindowSelectionArgs.h | 40 +++ src/cascadia/Remoting/WindowManager.cpp | 16 + src/cascadia/Remoting/WindowManager.h | 3 + src/cascadia/Remoting/WindowManager.idl | 3 + .../TerminalApp/AppActionHandlers.cpp | 19 + src/cascadia/TerminalApp/AppLogic.cpp | 9 +- src/cascadia/TerminalApp/AppLogic.h | 3 + src/cascadia/TerminalApp/AppLogic.idl | 3 + src/cascadia/TerminalApp/TerminalPage.cpp | 3 +- .../TerminalSettingsModel/ActionAndArgs.cpp | 8 + .../TerminalSettingsModel/ActionArgs.cpp | 16 + .../TerminalSettingsModel/ActionArgs.h | 45 +++ .../TerminalSettingsModel/ActionArgs.idl | 5 + .../AllShortcutActions.h | 4 +- .../GlobalAppSettings.cpp | 1 + .../TerminalSettingsModel/KeyMapping.cpp | 28 ++ .../TerminalSettingsModel/KeyMapping.h | 2 + .../TerminalSettingsModel/KeyMapping.idl | 2 + .../Resources/en-US/Resources.resw | 62 ++-- .../UnitTests_Remoting/RemotingTests.cpp | 339 +++++++++++++++++- src/cascadia/WindowsTerminal/AppHost.cpp | 133 +++++++ src/cascadia/WindowsTerminal/AppHost.h | 15 + src/cascadia/WindowsTerminal/IslandWindow.cpp | 70 +++- src/cascadia/WindowsTerminal/IslandWindow.h | 4 + src/cascadia/WindowsTerminal/pch.h | 4 +- src/cascadia/inc/WindowingBehavior.h | 2 + 35 files changed, 903 insertions(+), 39 deletions(-) create mode 100644 src/cascadia/Remoting/SummonWindowSelectionArgs.cpp create mode 100644 src/cascadia/Remoting/SummonWindowSelectionArgs.h diff --git a/.github/actions/spelling/dictionary/apis.txt b/.github/actions/spelling/dictionary/apis.txt index 68137d69296..f5812a01b07 100644 --- a/.github/actions/spelling/dictionary/apis.txt +++ b/.github/actions/spelling/dictionary/apis.txt @@ -77,6 +77,7 @@ NOASYNC NOCHANGEDIR NOPROGRESS NOREDIRECTIONBITMAP +NOREPEAT ntprivapi oaidl ocidl diff --git a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj index 519a9c69acc..34f70956ae8 100644 --- a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj +++ b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj @@ -25,6 +25,9 @@ Monarch.idl + + Monarch.idl + Peasant.idl @@ -54,6 +57,9 @@ Monarch.idl + + Monarch.idl + Peasant.idl diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index 6945777815a..66cbe6ad21b 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -375,7 +375,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation continue; } - if (peasant.WindowName() == L"_quake") + if (peasant.WindowName() == QuakeWindowName) { // The _quake window should never be treated as the MRU window. // Skip it if we see it. Users can still target it with `wt -w @@ -686,4 +686,51 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); } } + + // Method Description: + // - Attempt to summon a window. `args` contains information about which + // window we should try to summon: + // * if a WindowName is provided, we'll try to find a window with exactly + // that name, and fail if there isn't one. + // - Calls Peasant::Summon on the matching peasant (which might be an RPC call) + // - This should only ever be called by the WindowManager in the monarch + // process itself. The monarch is the one registering for global hotkeys, + // so it's the one calling this method. + // Arguments: + // - args: contains information about the window that should be summoned. + // Return Value: + // - + // - Sets args.FoundMatch when a window matching args is found successfully. + void Monarch::SummonWindow(const Remoting::SummonWindowSelectionArgs& args) + { + const auto searchedForName{ args.WindowName() }; + try + { + args.FoundMatch(false); + uint64_t windowId = 0; + // If no name was provided, then just summon the MRU window. + if (searchedForName.empty()) + { + windowId = _getMostRecentPeasantID(true); + } + else + { + // Try to find a peasant that currently has this name + windowId = _lookupPeasantIdForName(searchedForName); + } + if (auto targetPeasant{ _getPeasant(windowId) }) + { + targetPeasant.Summon(); + args.FoundMatch(true); + } + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SummonWindow_Failed", + TraceLoggingWideString(searchedForName.c_str(), "searchedForName", "The name of the window we tried to summon"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + } + } } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 7996934fd0e..e7f2da0da62 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -49,6 +49,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::Microsoft::Terminal::Remoting::ProposeCommandlineResult ProposeCommandline(const winrt::Microsoft::Terminal::Remoting::CommandlineArgs& args); void HandleActivatePeasant(const winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs& args); + void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index 836cb7ee9d4..ca62cdc0440 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -18,6 +18,17 @@ namespace Microsoft.Terminal.Remoting Boolean ShouldCreateWindow { get; }; // If you name this `CreateWindow`, the compiler will explode } + [default_interface] runtimeclass SummonWindowSelectionArgs { + SummonWindowSelectionArgs(); + SummonWindowSelectionArgs(String windowName); + String WindowName; + // TODO GH#8888 Other options: + // * CurrentDesktop + // * CurrentMonitor + + Boolean FoundMatch; + } + [default_interface] runtimeclass Monarch { Monarch(); @@ -25,6 +36,7 @@ namespace Microsoft.Terminal.Remoting UInt64 AddPeasant(IPeasant peasant); ProposeCommandlineResult ProposeCommandline(CommandlineArgs args); void HandleActivatePeasant(WindowActivatedArgs args); + void SummonWindow(SummonWindowSelectionArgs args); event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; }; diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp index c47cf1e98f2..990d1d591b0 100644 --- a/src/cascadia/Remoting/Peasant.cpp +++ b/src/cascadia/Remoting/Peasant.cpp @@ -117,6 +117,25 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation return _lastActivatedArgs; } + // Method Description: + // - Summon this peasant to become the active window. Currently, it just + // causes the peasant to become the active window wherever the window + // already was. + // - Will raise a SummonRequested event to ask the hosting window to handle for us. + // Arguments: + // - + // Return Value: + // - + void Peasant::Summon() + { + _SummonRequestedHandlers(*this, nullptr); + + TraceLoggingWrite(g_hRemotingProvider, + "Peasant_Summon", + TraceLoggingUInt64(GetID(), "peasantID", "Our ID"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + } + // Method Description: // - Tell this window to display it's window ID. We'll raise a // DisplayWindowIdRequested event, which will get handled in the AppHost, diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h index 67785c95bd1..72c672c2e44 100644 --- a/src/cascadia/Remoting/Peasant.h +++ b/src/cascadia/Remoting/Peasant.h @@ -23,6 +23,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation bool ExecuteCommandline(const winrt::Microsoft::Terminal::Remoting::CommandlineArgs& args); void ActivateWindow(const winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs& args); + + void Summon(); void RequestIdentifyWindows(); void DisplayWindowId(); void RequestRename(const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args); @@ -37,6 +39,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation 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, winrt::Microsoft::Terminal::Remoting::RenameRequestArgs); + TYPED_EVENT(SummonRequested, 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 583a494a6b9..6ff404b73fe 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -40,17 +40,20 @@ namespace Microsoft.Terminal.Remoting Boolean ExecuteCommandline(CommandlineArgs args); void ActivateWindow(WindowActivatedArgs args); WindowActivatedArgs GetLastActivatedArgs(); - String WindowName { get; }; - void RequestIdentifyWindows(); // Tells us to raise a IdentifyWindowsRequested + void DisplayWindowId(); // Tells us to display its own ID (which causes a DisplayWindowIdRequested to be raised) + String WindowName { get; }; + void RequestIdentifyWindows(); // Tells us to raise a IdentifyWindowsRequested void RequestRename(RenameRequestArgs args); // Tells us to raise a RenameRequested + void Summon(); event Windows.Foundation.TypedEventHandler WindowActivated; event Windows.Foundation.TypedEventHandler ExecuteCommandlineRequested; event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler DisplayWindowIdRequested; event Windows.Foundation.TypedEventHandler RenameRequested; + event Windows.Foundation.TypedEventHandler SummonRequested; }; [default_interface] runtimeclass Peasant : IPeasant diff --git a/src/cascadia/Remoting/SummonWindowSelectionArgs.cpp b/src/cascadia/Remoting/SummonWindowSelectionArgs.cpp new file mode 100644 index 00000000000..6db4cad3304 --- /dev/null +++ b/src/cascadia/Remoting/SummonWindowSelectionArgs.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "pch.h" +#include "SummonWindowSelectionArgs.h" +#include "SummonWindowSelectionArgs.g.cpp" diff --git a/src/cascadia/Remoting/SummonWindowSelectionArgs.h b/src/cascadia/Remoting/SummonWindowSelectionArgs.h new file mode 100644 index 00000000000..4aec6488f41 --- /dev/null +++ b/src/cascadia/Remoting/SummonWindowSelectionArgs.h @@ -0,0 +1,40 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- SummonWindowSelectionArgs.h + +Abstract: +- This is a helper class for determining which window a should be summoned when + a global hotkey is pressed. Parameters from a GlobalSummon action will be + filled in here. The Monarch will use these to find the window that matches + these args, and Summon() that Peasant. +- When the monarch finds a match, it will set FoundMatch to true. If it doesn't, + then the Monarch window might need to create a new window matching these args + instead. +--*/ + +#pragma once + +#include "SummonWindowSelectionArgs.g.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::Remoting::implementation +{ + struct SummonWindowSelectionArgs : public SummonWindowSelectionArgsT + { + public: + SummonWindowSelectionArgs() = default; + SummonWindowSelectionArgs(winrt::hstring name) : + _WindowName{ name } {}; + + WINRT_PROPERTY(winrt::hstring, WindowName); + WINRT_PROPERTY(bool, FoundMatch, false); + }; +} + +namespace winrt::Microsoft::Terminal::Remoting::factory_implementation +{ + BASIC_FACTORY(SummonWindowSelectionArgs); +} diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index f76bfd47821..f96262dad0a 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -247,6 +247,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // window, and when the current monarch dies. _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); + + _BecameMonarchHandlers(*this, nullptr); } bool WindowManager::_areWeTheKing() @@ -478,4 +480,18 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { _FindTargetWindowRequestedHandlers(sender, args); } + + bool WindowManager::IsMonarch() + { + return _isKing; + } + + void WindowManager::SummonWindow(const Remoting::SummonWindowSelectionArgs& args) + { + // We should only ever get called when we are the monarch, because only + // the monarch ever registers for the global hotkey. So the monarch is + // the only window that will be calling this. + _monarch.SummonWindow(args); + } + } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 8e5678369b2..85451884b90 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -37,8 +37,11 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation bool ShouldCreateWindow(); winrt::Microsoft::Terminal::Remoting::Peasant CurrentWindow(); + bool IsMonarch(); + void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); + TYPED_EVENT(BecameMonarch, 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 3099f1128b5..547b96f9aa5 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -10,6 +10,9 @@ namespace Microsoft.Terminal.Remoting void ProposeCommandline(CommandlineArgs args); Boolean ShouldCreateWindow { get; }; IPeasant CurrentWindow(); + Boolean IsMonarch { get; }; + void SummonWindow(SummonWindowSelectionArgs args); event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; + event Windows.Foundation.TypedEventHandler BecameMonarch; }; } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 681aaeadd49..04c037f27ed 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -771,4 +771,23 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } + + void TerminalPage::_HandleGlobalSummon(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + // Manually return false. These shouldn't ever get here, except for when + // we fail to register for the global hotkey. In that case, returning + // false here will let the underlying terminal still process the key, as + // if it wasn't bound at all. + args.Handled(false); + } + void TerminalPage::_HandleQuakeMode(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + // Manually return false. These shouldn't ever get here, except for when + // we fail to register for the global hotkey. In that case, returning + // false here will let the underlying terminal still process the key, as + // if it wasn't bound at all. + args.Handled(false); + } } diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 3cc669badab..e8229157b7d 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -997,7 +997,7 @@ namespace winrt::TerminalApp::implementation CATCH_LOG(); // Method Description: - // - Reloads the settings from the profile.json. + // - Reloads the settings from the settings.json file. void AppLogic::_ReloadSettings() { // Attempt to load our settings. @@ -1027,6 +1027,8 @@ namespace winrt::TerminalApp::implementation _ApplyStartupTaskStateChange(); Jumplist::UpdateJumplist(_settings); + + _SettingsChangedHandlers(*this, nullptr); } // Method Description: @@ -1409,6 +1411,11 @@ namespace winrt::TerminalApp::implementation return _root ? _root->AlwaysOnTop() : false; } + Windows::Foundation::Collections::IMap AppLogic::GlobalHotkeys() + { + return _settings.GlobalSettings().KeyMap().GlobalHotkeys(); + } + void AppLogic::IdentifyWindow() { if (_root) diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 54c1a206944..951185f473d 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -90,8 +90,11 @@ namespace winrt::TerminalApp::implementation winrt::Windows::Foundation::IAsyncOperation ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog); + Windows::Foundation::Collections::IMap GlobalHotkeys(); + // -------------------------------- WinRT Events --------------------------------- TYPED_EVENT(RequestedThemeChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::ElementTheme); + TYPED_EVENT(SettingsChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: bool _isUwp{ false }; diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index 6bbd2a5d1e4..bbad80254d8 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -71,6 +71,8 @@ namespace TerminalApp FindTargetWindowResult FindTargetWindow(String[] args); + Windows.Foundation.Collections.IMap GlobalHotkeys(); + // See IDialogPresenter and TerminalPage's DialogPresenter for more // information. Windows.Foundation.IAsyncOperation ShowDialog(Windows.UI.Xaml.Controls.ContentDialog dialog); @@ -86,6 +88,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler SetTaskbarProgress; event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler RenameWindowRequested; + event Windows.Foundation.TypedEventHandler SettingsChanged; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; } } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 9094d7cc5db..62c0f8e2dd4 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -16,6 +16,7 @@ #include "DebugTapConnection.h" #include "SettingsTab.h" #include "RenameWindowRequestedArgs.g.cpp" +#include "../inc/WindowingBehavior.h" using namespace winrt; using namespace winrt::Windows::Foundation::Collections; @@ -42,8 +43,6 @@ namespace winrt using IInspectable = Windows::Foundation::IInspectable; } -static constexpr std::wstring_view QuakeWindowName{ L"_quake" }; - namespace winrt::TerminalApp::implementation { TerminalPage::TerminalPage() : diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 7cdfc96796b..aa7fc7c7d0b 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -57,6 +57,8 @@ static constexpr std::string_view IdentifyWindowKey{ "identifyWindow" }; static constexpr std::string_view IdentifyWindowsKey{ "identifyWindows" }; static constexpr std::string_view RenameWindowKey{ "renameWindow" }; static constexpr std::string_view OpenWindowRenamerKey{ "openWindowRenamer" }; +static constexpr std::string_view GlobalSummonKey{ "globalSummon" }; +static constexpr std::string_view QuakeModeKey{ "quakeMode" }; static constexpr std::string_view ActionKey{ "action" }; @@ -127,6 +129,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { IdentifyWindowsKey, ShortcutAction::IdentifyWindows }, { RenameWindowKey, ShortcutAction::RenameWindow }, { OpenWindowRenamerKey, ShortcutAction::OpenWindowRenamer }, + { GlobalSummonKey, ShortcutAction::GlobalSummon }, + { QuakeModeKey, ShortcutAction::QuakeMode }, }; using ParseResult = std::tuple>; @@ -162,6 +166,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::PrevTab, PrevTabArgs::FromJson }, { ShortcutAction::NextTab, NextTabArgs::FromJson }, { ShortcutAction::RenameWindow, RenameWindowArgs::FromJson }, + { ShortcutAction::GlobalSummon, GlobalSummonArgs::FromJson }, + { ShortcutAction::QuakeMode, GlobalSummonArgs::QuakeModeFromJson }, { ShortcutAction::Invalid, nullptr }, }; @@ -337,6 +343,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::IdentifyWindows, RS_(L"IdentifyWindowsCommandKey") }, { ShortcutAction::RenameWindow, RS_(L"ResetWindowNameCommandKey") }, { ShortcutAction::OpenWindowRenamer, RS_(L"OpenWindowRenamerCommandKey") }, + { ShortcutAction::GlobalSummon, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::QuakeMode, RS_(L"QuakeModeCommandKey") }, }; }(); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index aebc2f4ecdd..9565d3d8132 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -29,6 +29,7 @@ #include "PrevTabArgs.g.cpp" #include "NextTabArgs.g.cpp" #include "RenameWindowArgs.g.cpp" +#include "GlobalSummonArgs.g.cpp" #include @@ -582,4 +583,19 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } return RS_(L"ResetWindowNameCommandKey"); } + + winrt::hstring GlobalSummonArgs::GenerateName() const + { + std::wstringstream ss; + ss << std::wstring_view(RS_(L"GlobalSummonCommandKey")); + + // "Summon the Terminal window" + // "Summon the Terminal window, name:\"{_Name}\"" + if (!_Name.empty()) + { + ss << L", name: "; + ss << std::wstring_view(_Name); + } + return winrt::hstring{ ss.str() }; + } } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 68e77ef99f4..68132691604 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -31,10 +31,12 @@ #include "PrevTabArgs.g.h" #include "NextTabArgs.g.h" #include "RenameWindowArgs.g.h" +#include "GlobalSummonArgs.g.h" #include "../../cascadia/inc/cppwinrt_utils.h" #include "JsonUtils.h" #include "TerminalWarnings.h" +#include "../inc/WindowingBehavior.h" #include "TerminalSettingsSerializationHelpers.h" @@ -1037,6 +1039,49 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return *copy; } }; + + struct GlobalSummonArgs : public GlobalSummonArgsT + { + GlobalSummonArgs() = default; + WINRT_PROPERTY(winrt::hstring, Name, L""); + + static constexpr std::string_view NameKey{ "name" }; + + public: + hstring GenerateName() const; + + bool Equals(const IActionArgs& other) + { + if (auto otherAsUs = other.try_as(); otherAsUs) + { + return otherAsUs->_Name == _Name; + } + 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, NameKey, args->_Name); + return { *args, {} }; + } + IActionArgs Copy() const + { + auto copy{ winrt::make_self() }; + copy->_Name = _Name; + return *copy; + } + // SPECIAL! This deserializer creates a GlobalSummonArgs with the + // default values for quakeMode + static FromJsonResult QuakeModeFromJson(const Json::Value& /*json*/) + { + // LOAD BEARING: Not using make_self here _will_ break you in the future! + auto args = winrt::make_self(); + args->_Name = QuakeWindowName; + return { *args, {} }; + } + }; + } namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index cb666d9b826..ca29300fe52 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -252,4 +252,9 @@ namespace Microsoft.Terminal.Settings.Model { String Name { get; }; }; + + [default_interface] runtimeclass GlobalSummonArgs : IActionArgs + { + String Name { get; }; + }; } diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index cf32e5f36ec..bc710a756b9 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -72,4 +72,6 @@ ON_ALL_ACTIONS(IdentifyWindow) \ ON_ALL_ACTIONS(IdentifyWindows) \ ON_ALL_ACTIONS(RenameWindow) \ - ON_ALL_ACTIONS(OpenWindowRenamer) + ON_ALL_ACTIONS(OpenWindowRenamer) \ + ON_ALL_ACTIONS(GlobalSummon) \ + ON_ALL_ACTIONS(QuakeMode) diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index d9d1427d37a..cc98e3d4a14 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -7,6 +7,7 @@ #include "../../inc/DefaultSettings.h" #include "JsonUtils.h" #include "TerminalSettingsSerializationHelpers.h" +#include "KeyChordSerialization.h" #include "GlobalAppSettings.g.cpp" diff --git a/src/cascadia/TerminalSettingsModel/KeyMapping.cpp b/src/cascadia/TerminalSettingsModel/KeyMapping.cpp index 880a6b34d4a..3bd93cfe59f 100644 --- a/src/cascadia/TerminalSettingsModel/KeyMapping.cpp +++ b/src/cascadia/TerminalSettingsModel/KeyMapping.cpp @@ -141,4 +141,32 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return keyModifiers; } + + // Method Description: + // - Build a map of all the globalSummon actions. + // - quakeMode actions are included in this, but expanded to the equivalent + // set of GlobalSummonArgs + // - This is only ever called in two scenarios: + // - on becoming the monarch (which only happens once per window) + // - when the settings reload (and the cache would inevitably be dirty) + // So it's perfectly reasonable to not cache these results. + // Arguments: + // - + // Return Value: + // - a map of KeyChord -> ActionAndArgs containing all globally bindable actions. + Windows::Foundation::Collections::IMap KeyMapping::GlobalHotkeys() + { + std::unordered_map justGlobals; + + for (const auto& [k, v] : _keyShortcuts) + { + if (v.Action() == ShortcutAction::GlobalSummon || v.Action() == ShortcutAction::QuakeMode) + { + justGlobals[k] = v; + } + } + + return winrt::single_threaded_map(std::move(justGlobals)); + } + } diff --git a/src/cascadia/TerminalSettingsModel/KeyMapping.h b/src/cascadia/TerminalSettingsModel/KeyMapping.h index ce5c811e389..eea978b59a6 100644 --- a/src/cascadia/TerminalSettingsModel/KeyMapping.h +++ b/src/cascadia/TerminalSettingsModel/KeyMapping.h @@ -69,6 +69,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation std::vector LayerJson(const Json::Value& json); Json::Value ToJson(); + Windows::Foundation::Collections::IMap GlobalHotkeys(); + private: std::unordered_map _keyShortcuts; std::vector> _keyShortcutsByInsertionOrder; diff --git a/src/cascadia/TerminalSettingsModel/KeyMapping.idl b/src/cascadia/TerminalSettingsModel/KeyMapping.idl index 540c74dea50..1b6886acded 100644 --- a/src/cascadia/TerminalSettingsModel/KeyMapping.idl +++ b/src/cascadia/TerminalSettingsModel/KeyMapping.idl @@ -35,5 +35,7 @@ namespace Microsoft.Terminal.Settings.Model Microsoft.Terminal.Control.KeyChord GetKeyBindingForAction(ShortcutAction action); Microsoft.Terminal.Control.KeyChord GetKeyBindingForActionWithArgs(ActionAndArgs actionAndArgs); + + Windows.Foundation.Collections.IMap GlobalHotkeys(); } } diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 2f1ed50adf5..8f8aece2a2f 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -1,17 +1,17 @@  - @@ -391,6 +391,12 @@ Rename window... + + Summon the Terminal window + + + Summon Quake window + Microsoft Corporation Paired with `InboxWindowsConsoleName`, this is the application author... which is us: Microsoft. @@ -399,4 +405,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/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 1e3bd4f2309..694dcef8fb0 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -60,11 +60,13 @@ namespace RemotingUnitTests Remoting::CommandlineArgs InitialArgs() { throw winrt::hresult_error{}; } Remoting::WindowActivatedArgs GetLastActivatedArgs() { throw winrt::hresult_error{}; } void RequestRename(const Remoting::RenameRequestArgs& /*args*/) { throw winrt::hresult_error{}; } + void Summon() { 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, winrt::Windows::Foundation::IInspectable); }; class RemotingTests @@ -107,6 +109,11 @@ namespace RemotingUnitTests TEST_METHOD(TestRenameSameNameAsAnother); TEST_METHOD(TestRenameSameNameAsADeadPeasant); + TEST_METHOD(TestSummonMostRecentWindow); + TEST_METHOD(TestSummonNamedWindow); + TEST_METHOD(TestSummonNamedDeadWindow); + TEST_METHOD(TestSummonMostRecentDeadWindow); + TEST_CLASS_SETUP(ClassSetup) { return true; @@ -468,7 +475,6 @@ namespace RemotingUnitTests VERIFY_ARE_EQUAL(false, (bool)result.Id()); } } - void RemotingTests::ProposeCommandlineCurrentWindow() { Log::Comment(L"Test proposing a commandline for the current window (ID=0)"); @@ -549,7 +555,6 @@ namespace RemotingUnitTests VERIFY_ARE_EQUAL(false, (bool)result.Id()); } } - void RemotingTests::ProposeCommandlineNonExistentWindow() { Log::Comment(L"Test proposing a commandline for an ID that doesn't have a current peasant"); @@ -1610,4 +1615,334 @@ namespace RemotingUnitTests VERIFY_ARE_EQUAL(p1->GetID(), m0->_lookupPeasantIdForName(L"two")); } + void RemotingTests::TestSummonMostRecentWindow() + { + Log::Comment(L"Attempt to summon the most recent window"); + + const winrt::guid guid1{ Utils::GuidFromString(L"{11111111-1111-1111-1111-111111111111}") }; + + const auto monarch0PID = 12345u; + const auto peasant1PID = 23456u; + const auto peasant2PID = 34567u; + + com_ptr m0; + m0.attach(new Remoting::implementation::Monarch(monarch0PID)); + + com_ptr p1; + p1.attach(new Remoting::implementation::Peasant(peasant1PID)); + + com_ptr p2; + p2.attach(new Remoting::implementation::Peasant(peasant2PID)); + + VERIFY_IS_NOT_NULL(m0); + VERIFY_IS_NOT_NULL(p1); + VERIFY_IS_NOT_NULL(p2); + p1->WindowName(L"one"); + p2->WindowName(L"two"); + + VERIFY_ARE_EQUAL(0, p1->GetID()); + VERIFY_ARE_EQUAL(0, p2->GetID()); + + m0->AddPeasant(*p1); + m0->AddPeasant(*p2); + + VERIFY_ARE_EQUAL(1, p1->GetID()); + VERIFY_ARE_EQUAL(2, p2->GetID()); + + VERIFY_ARE_EQUAL(2u, m0->_peasants.size()); + + bool p1ExpectedToBeSummoned = false; + bool p2ExpectedToBeSummoned = false; + + p1->SummonRequested([&](auto&&, auto&&) { + Log::Comment(L"p1 summoned"); + VERIFY_IS_TRUE(p1ExpectedToBeSummoned); + }); + p2->SummonRequested([&](auto&&, auto&&) { + Log::Comment(L"p2 summoned"); + VERIFY_IS_TRUE(p2ExpectedToBeSummoned); + }); + + { + Log::Comment(L"Activate the first peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p1->GetID(), + guid1, + winrt::clock().now() }; + p1->ActivateWindow(activatedArgs); + } + { + Log::Comment(L"Activate the second peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p2->GetID(), + guid1, + winrt::clock().now() }; + p2->ActivateWindow(activatedArgs); + } + + p2ExpectedToBeSummoned = true; + Remoting::SummonWindowSelectionArgs args; + // Without setting the WindowName, SummonWindowSelectionArgs defaults to + // the MRU window + Log::Comment(L"Summon the MRU window, which is window two"); + m0->SummonWindow(args); + VERIFY_IS_TRUE(args.FoundMatch()); + + { + Log::Comment(L"Activate the first peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p1->GetID(), + guid1, + winrt::clock().now() }; + p1->ActivateWindow(activatedArgs); + } + + Log::Comment(L"Now that one is the MRU, summon it"); + p2ExpectedToBeSummoned = false; + p1ExpectedToBeSummoned = true; + args.FoundMatch(false); + m0->SummonWindow(args); + VERIFY_IS_TRUE(args.FoundMatch()); + } + + void RemotingTests::TestSummonNamedWindow() + { + Log::Comment(L"Attempt to summon a window by name. When there isn't a " + L"window with that name, set FoundMatch to false, so the " + L"caller can handle that case."); + + const auto monarch0PID = 12345u; + const auto peasant1PID = 23456u; + const auto peasant2PID = 34567u; + + com_ptr m0; + m0.attach(new Remoting::implementation::Monarch(monarch0PID)); + + com_ptr p1; + p1.attach(new Remoting::implementation::Peasant(peasant1PID)); + + com_ptr p2; + p2.attach(new Remoting::implementation::Peasant(peasant2PID)); + + VERIFY_IS_NOT_NULL(m0); + VERIFY_IS_NOT_NULL(p1); + VERIFY_IS_NOT_NULL(p2); + p1->WindowName(L"one"); + p2->WindowName(L"two"); + + VERIFY_ARE_EQUAL(0, p1->GetID()); + VERIFY_ARE_EQUAL(0, p2->GetID()); + + m0->AddPeasant(*p1); + m0->AddPeasant(*p2); + + VERIFY_ARE_EQUAL(1, p1->GetID()); + VERIFY_ARE_EQUAL(2, p2->GetID()); + + VERIFY_ARE_EQUAL(2u, m0->_peasants.size()); + + bool p1ExpectedToBeSummoned = false; + bool p2ExpectedToBeSummoned = false; + + p1->SummonRequested([&](auto&&, auto&&) { + Log::Comment(L"p1 summoned"); + VERIFY_IS_TRUE(p1ExpectedToBeSummoned); + }); + p2->SummonRequested([&](auto&&, auto&&) { + Log::Comment(L"p2 summoned"); + VERIFY_IS_TRUE(p2ExpectedToBeSummoned); + }); + + Remoting::SummonWindowSelectionArgs args; + + Log::Comment(L"Summon window two by name"); + p2ExpectedToBeSummoned = true; + args.WindowName(L"two"); + m0->SummonWindow(args); + VERIFY_IS_TRUE(args.FoundMatch()); + + Log::Comment(L"Summon window one by name"); + p2ExpectedToBeSummoned = false; + p1ExpectedToBeSummoned = true; + args.FoundMatch(false); + args.WindowName(L"one"); + m0->SummonWindow(args); + VERIFY_IS_TRUE(args.FoundMatch()); + + Log::Comment(L"Fail to summon window three by name"); + p1ExpectedToBeSummoned = false; + args.FoundMatch(false); + args.WindowName(L"three"); + m0->SummonWindow(args); + VERIFY_IS_FALSE(args.FoundMatch()); + } + + void RemotingTests::TestSummonNamedDeadWindow() + { + Log::Comment(L"Attempt to summon a dead window by name. This will fail, but not crash."); + + const auto monarch0PID = 12345u; + const auto peasant1PID = 23456u; + const auto peasant2PID = 34567u; + + com_ptr m0; + m0.attach(new Remoting::implementation::Monarch(monarch0PID)); + + com_ptr p1; + p1.attach(new Remoting::implementation::Peasant(peasant1PID)); + + com_ptr p2; + p2.attach(new Remoting::implementation::Peasant(peasant2PID)); + + VERIFY_IS_NOT_NULL(m0); + VERIFY_IS_NOT_NULL(p1); + VERIFY_IS_NOT_NULL(p2); + p1->WindowName(L"one"); + p2->WindowName(L"two"); + + VERIFY_ARE_EQUAL(0, p1->GetID()); + VERIFY_ARE_EQUAL(0, p2->GetID()); + + m0->AddPeasant(*p1); + m0->AddPeasant(*p2); + + VERIFY_ARE_EQUAL(1, p1->GetID()); + VERIFY_ARE_EQUAL(2, p2->GetID()); + + VERIFY_ARE_EQUAL(2u, m0->_peasants.size()); + + bool p1ExpectedToBeSummoned = false; + bool p2ExpectedToBeSummoned = false; + + p1->SummonRequested([&](auto&&, auto&&) { + Log::Comment(L"p1 summoned"); + VERIFY_IS_TRUE(p1ExpectedToBeSummoned); + }); + p2->SummonRequested([&](auto&&, auto&&) { + Log::Comment(L"p2 summoned"); + VERIFY_IS_TRUE(p2ExpectedToBeSummoned); + }); + + Remoting::SummonWindowSelectionArgs args; + + Log::Comment(L"Summon window two by name"); + p2ExpectedToBeSummoned = true; + args.WindowName(L"two"); + m0->SummonWindow(args); + VERIFY_IS_TRUE(args.FoundMatch()); + + Log::Comment(L"Summon window one by name"); + p2ExpectedToBeSummoned = false; + p1ExpectedToBeSummoned = true; + args.FoundMatch(false); + args.WindowName(L"one"); + m0->SummonWindow(args); + VERIFY_IS_TRUE(args.FoundMatch()); + + Log::Comment(L"Kill peasant one."); + RemotingTests::_killPeasant(m0, p1->GetID()); + + Log::Comment(L"Fail to summon window one by name"); + p1ExpectedToBeSummoned = false; + args.FoundMatch(false); + args.WindowName(L"one"); + m0->SummonWindow(args); + VERIFY_IS_FALSE(args.FoundMatch()); + } + + void RemotingTests::TestSummonMostRecentDeadWindow() + { + Log::Comment(L"Attempt to summon the MRU window, when the MRU window " + L"has died. This will fall back to the next MRU window."); + + const winrt::guid guid1{ Utils::GuidFromString(L"{11111111-1111-1111-1111-111111111111}") }; + + const auto monarch0PID = 12345u; + const auto peasant1PID = 23456u; + const auto peasant2PID = 34567u; + + com_ptr m0; + m0.attach(new Remoting::implementation::Monarch(monarch0PID)); + + com_ptr p1; + p1.attach(new Remoting::implementation::Peasant(peasant1PID)); + + com_ptr p2; + p2.attach(new Remoting::implementation::Peasant(peasant2PID)); + + VERIFY_IS_NOT_NULL(m0); + VERIFY_IS_NOT_NULL(p1); + VERIFY_IS_NOT_NULL(p2); + p1->WindowName(L"one"); + p2->WindowName(L"two"); + + VERIFY_ARE_EQUAL(0, p1->GetID()); + VERIFY_ARE_EQUAL(0, p2->GetID()); + + m0->AddPeasant(*p1); + m0->AddPeasant(*p2); + + VERIFY_ARE_EQUAL(1, p1->GetID()); + VERIFY_ARE_EQUAL(2, p2->GetID()); + + VERIFY_ARE_EQUAL(2u, m0->_peasants.size()); + + bool p1ExpectedToBeSummoned = false; + bool p2ExpectedToBeSummoned = false; + + p1->SummonRequested([&](auto&&, auto&&) { + Log::Comment(L"p1 summoned"); + VERIFY_IS_TRUE(p1ExpectedToBeSummoned); + }); + p2->SummonRequested([&](auto&&, auto&&) { + Log::Comment(L"p2 summoned"); + VERIFY_IS_TRUE(p2ExpectedToBeSummoned); + }); + + { + Log::Comment(L"Activate the first peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p1->GetID(), + guid1, + winrt::clock().now() }; + p1->ActivateWindow(activatedArgs); + } + { + Log::Comment(L"Activate the second peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p2->GetID(), + guid1, + winrt::clock().now() }; + p2->ActivateWindow(activatedArgs); + } + + p2ExpectedToBeSummoned = true; + Remoting::SummonWindowSelectionArgs args; + // Without setting the WindowName, SummonWindowSelectionArgs defaults to + // the MRU window + Log::Comment(L"Summon the MRU window, which is window two"); + m0->SummonWindow(args); + VERIFY_IS_TRUE(args.FoundMatch()); + + { + Log::Comment(L"Activate the first peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p1->GetID(), + guid1, + winrt::clock().now() }; + p1->ActivateWindow(activatedArgs); + } + + Log::Comment(L"Now that one is the MRU, summon it"); + p2ExpectedToBeSummoned = false; + p1ExpectedToBeSummoned = true; + args.FoundMatch(false); + m0->SummonWindow(args); + VERIFY_IS_TRUE(args.FoundMatch()); + + Log::Comment(L"Kill peasant one."); + RemotingTests::_killPeasant(m0, p1->GetID()); + + Log::Comment(L"We now expect to summon two, since the MRU peasant (one) is actually dead."); + p2ExpectedToBeSummoned = true; + p1ExpectedToBeSummoned = false; + args.FoundMatch(false); + m0->SummonWindow(args); + VERIFY_IS_TRUE(args.FoundMatch()); + } + } diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index b25e96ece22..f4054fbb7d4 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -6,6 +6,7 @@ #include "../types/inc/Viewport.hpp" #include "../types/inc/utils.hpp" #include "../types/inc/User32Utils.hpp" +#include "../WinRTUtils/inc/WtExeUtils.h" #include "resource.h" using namespace winrt::Windows::UI; @@ -74,8 +75,15 @@ AppHost::AppHost() noexcept : std::placeholders::_2)); _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); _window->WindowActivated({ this, &AppHost::_WindowActivated }); + _window->HotkeyPressed({ this, &AppHost::_GlobalHotkeyPressed }); _window->SetAlwaysOnTop(_logic.GetInitialAlwaysOnTop()); _window->MakeWindow(); + + _windowManager.BecameMonarch({ this, &AppHost::_BecomeMonarch }); + if (_windowManager.IsMonarch()) + { + _BecomeMonarch(nullptr, nullptr); + } } AppHost::~AppHost() @@ -197,6 +205,7 @@ void AppHost::_HandleCommandlineArgs() // commandline (in the future), it'll trigger this callback, that we'll // use to send the actions to the app. peasant.ExecuteCommandlineRequested({ this, &AppHost::_DispatchCommandline }); + peasant.SummonRequested({ this, &AppHost::_HandleSummon }); peasant.DisplayWindowIdRequested({ this, &AppHost::_DisplayWindowId }); @@ -254,6 +263,7 @@ void AppHost::Initialize() _logic.SetTaskbarProgress({ this, &AppHost::SetTaskbarProgress }); _logic.IdentifyWindowsRequested({ this, &AppHost::_IdentifyWindowsRequested }); _logic.RenameWindowRequested({ this, &AppHost::_RenameWindowRequested }); + _logic.SettingsChanged({ this, &AppHost::_HandleSettingsChanged }); _logic.IsQuakeWindowChanged({ this, &AppHost::_IsQuakeWindowChanged }); _window->UpdateTitle(_logic.Title()); @@ -578,6 +588,8 @@ bool AppHost::HasWindow() void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable /*sender*/, Remoting::CommandlineArgs args) { + // Summon the window whenever we dispatch a commandline to it. This will + // make it obvious when a new tab/pane is created in a window. _window->SummonWindow(); _logic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory()); } @@ -619,6 +631,121 @@ winrt::fire_and_forget AppHost::_WindowActivated() } } +void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*args*/) +{ + _setupGlobalHotkeys(); +} + +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); + + // 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) + { + if (k != nullptr) + { + _hotkeys.push_back(k); + } + } + + _window->SetGlobalHotkeys(_hotkeys); +} + +// Method Description: +// - Called whenever a registered hotkey is pressed. We'll look up the +// GlobalSummonArgs for the specified hotkey, then dispatch a call to the +// Monarch with the selection information. +// - If the monarch finds a match for the window name (or no name was provided), +// it'll set FoundMatch=true. +// - If FoundMatch is false, and a name was provided, then we should create a +// new window with the given name. +// Arguments: +// - hotkeyIndex: the index of the entry in _hotkeys that was pressed. +// Return Value: +// - +void AppHost::_GlobalHotkeyPressed(const long hotkeyIndex) +{ + if (hotkeyIndex < 0 || static_cast(hotkeyIndex) > _hotkeys.size()) + { + return; + } + // Lookup the matching keychord + Control::KeyChord kc = _hotkeys.at(hotkeyIndex); + // Get the stored ActionAndArgs for that chord + if (const auto& actionAndArgs{ _hotkeyActions.Lookup(kc) }) + { + if (const auto& summonArgs{ actionAndArgs.Args().try_as() }) + { + Remoting::SummonWindowSelectionArgs args{ summonArgs.Name() }; + + _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); + } + } + } +} + +// Method Description: +// - Called when the monarch failed to summon a window for a given set of +// SummonWindowSelectionArgs. In this case, we should create the specified +// window ourselves. +// - This is to support the scenario like `globalSummon(Name="_quake")` being +// used to summon the window if it already exists, or create it if it doesn't. +// Arguments: +// - args: Contains information on how we should name the window +// Return Value: +// - +winrt::fire_and_forget AppHost::_createNewTerminalWindow(Settings::Model::GlobalSummonArgs args) +{ + // Hop to the BG thread + co_await winrt::resume_background(); + + // This will get us the correct exe for dev/preview/release. If you + // don't stick this in a local, it'll get mangled by ShellExecute. I + // have no idea why. + const auto exePath{ GetWtExePath() }; + + // If we weren't given a name, then just use new to force the window to be + // unnamed. + winrt::hstring cmdline{ + fmt::format(L"-w {}", + args.Name().empty() ? L"new" : + args.Name()) + }; + + SHELLEXECUTEINFOW seInfo{ 0 }; + seInfo.cbSize = sizeof(seInfo); + seInfo.fMask = SEE_MASK_NOASYNC; + seInfo.lpVerb = L"open"; + seInfo.lpFile = exePath.c_str(); + seInfo.lpParameters = cmdline.c_str(); + seInfo.nShow = SW_SHOWNORMAL; + LOG_IF_WIN32_BOOL_FALSE(ShellExecuteExW(&seInfo)); + + co_return; +} + +void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*args*/) +{ + _window->SummonWindow(); +} + GUID AppHost::_CurrentDesktopGuid() { GUID currentDesktopGuid{ 0 }; @@ -698,6 +825,12 @@ winrt::fire_and_forget AppHost::_RenameWindowRequested(const winrt::Windows::Fou } } +void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*args*/) +{ + _setupGlobalHotkeys(); +} + void AppHost::_IsQuakeWindowChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 3c32af7190a..9288ee560df 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -28,6 +28,9 @@ class AppHost bool _shouldCreateWindow{ false }; winrt::Microsoft::Terminal::Remoting::WindowManager _windowManager{ nullptr }; + std::vector _hotkeys{}; + winrt::Windows::Foundation::Collections::IMap _hotkeyActions{ nullptr }; + void _HandleCommandlineArgs(); void _HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); @@ -51,6 +54,13 @@ class AppHost void _FindTargetWindow(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args); + + void _BecomeMonarch(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& args); + void _GlobalHotkeyPressed(const long hotkeyIndex); + void _HandleSummon(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& args); + winrt::fire_and_forget _IdentifyWindowsRequested(const winrt::Windows::Foundation::IInspectable sender, const winrt::Windows::Foundation::IInspectable args); void _DisplayWindowId(const winrt::Windows::Foundation::IInspectable& sender, @@ -60,6 +70,11 @@ class AppHost GUID _CurrentDesktopGuid(); + winrt::fire_and_forget _setupGlobalHotkeys(); + winrt::fire_and_forget _createNewTerminalWindow(winrt::Microsoft::Terminal::Settings::Model::GlobalSummonArgs args); + void _HandleSettingsChanged(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& args); + void _IsQuakeWindowChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); }; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 9a7d681908c..7a6d6f4bea9 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -15,6 +15,7 @@ using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Hosting; using namespace winrt::Windows::Foundation::Numerics; using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Control; using namespace ::Microsoft::Console::Types; #define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS" @@ -386,6 +387,11 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize { switch (message) { + case WM_HOTKEY: + { + _HotkeyPressedHandlers(static_cast(wparam)); + return 0; + } case WM_GETMINMAXINFO: { _OnGetMinMaxInfo(wparam, lparam); @@ -402,8 +408,9 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize { // send focus to the child window SetFocus(_interopWindowHandle); - return 0; // eat the message + return 0; } + break; } case WM_ACTIVATE: { @@ -935,6 +942,63 @@ 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 +// Return Value: +// - +void IslandWindow::UnsetHotkeys(const std::vector& hotkeyList) +{ + 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)); + + for (size_t i = 0; i < hotkeyList.size(); i++) + { + LOG_IF_WIN32_BOOL_FALSE(UnregisterHotKey(_window.get(), static_cast(i))); + } +} + +// Method Description: +// - Call RegisterHotKey once for each entry in hotkeyList, 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. +// Arguments: +// - hotkeyList: a list of hotkeys to bind +// Return Value: +// - +void IslandWindow::SetGlobalHotkeys(const std::vector& hotkeyList) +{ + TraceLoggingWrite(g_hWindowsTerminalProvider, + "SetGlobalHotkeys", + TraceLoggingDescription("Emitted when setting hotkeys"), + TraceLoggingInt64(hotkeyList.size(), "numHotkeys", "The number of hotkeys to set"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + int index = 0; + for (const auto& hotkey : hotkeyList) + { + const auto modifiers = hotkey.Modifiers(); + const auto hotkeyFlags = MOD_NOREPEAT | + (WI_IsFlagSet(modifiers, KeyModifiers::Windows) ? MOD_WIN : 0) | + (WI_IsFlagSet(modifiers, KeyModifiers::Alt) ? MOD_ALT : 0) | + (WI_IsFlagSet(modifiers, KeyModifiers::Ctrl) ? MOD_CONTROL : 0) | + (WI_IsFlagSet(modifiers, KeyModifiers::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++; + } +} + // Method Description: // - Force activate this window. This method will bring us to the foreground and // activate us. If the window is minimized, it will restore the window. If the @@ -969,6 +1033,10 @@ winrt::fire_and_forget IslandWindow::SummonWindow() }); LOG_IF_WIN32_BOOL_FALSE(BringWindowToTop(_window.get())); LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_SHOW)); + + // Activate the window too. This will force us to the virtual desktop this + // window is on, if it's on another virtual desktop. + LOG_LAST_ERROR_IF_NULL(SetActiveWindow(_window.get())); } bool IslandWindow::IsQuakeWindow() const noexcept diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index d20bd115556..29897f1112f 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -38,6 +38,9 @@ class IslandWindow : void FlashTaskbar(); void SetTaskbarProgress(const size_t state, const size_t progress); + void UnsetHotkeys(const std::vector& hotkeyList); + void SetGlobalHotkeys(const std::vector& hotkeyList); + winrt::fire_and_forget SummonWindow(); bool IsQuakeWindow() const noexcept; @@ -47,6 +50,7 @@ class IslandWindow : DECLARE_EVENT(WindowCloseButtonClicked, _windowCloseButtonClickedHandler, winrt::delegate<>); WINRT_CALLBACK(MouseScrolled, winrt::delegate); WINRT_CALLBACK(WindowActivated, winrt::delegate); + WINRT_CALLBACK(HotkeyPressed, winrt::delegate); protected: void ForceResize() diff --git a/src/cascadia/WindowsTerminal/pch.h b/src/cascadia/WindowsTerminal/pch.h index 8009d455e1a..a8befdf6106 100644 --- a/src/cascadia/WindowsTerminal/pch.h +++ b/src/cascadia/WindowsTerminal/pch.h @@ -56,12 +56,14 @@ Module Name: #include // Additional headers for various xaml features. We need: -// * Core so we can resume_foreground +// * Core so we can resume_foreground with CoreDispatcher // * Controls for grid // * Media for ScaleTransform +// * ApplicationModel for finding the path to wt.exe #include #include #include +#include #include #include diff --git a/src/cascadia/inc/WindowingBehavior.h b/src/cascadia/inc/WindowingBehavior.h index bce36f7a693..04260893191 100644 --- a/src/cascadia/inc/WindowingBehavior.h +++ b/src/cascadia/inc/WindowingBehavior.h @@ -9,3 +9,5 @@ constexpr int32_t WindowingBehaviorUseNew{ -1 }; constexpr int32_t WindowingBehaviorUseExisting{ -2 }; constexpr int32_t WindowingBehaviorUseAnyExisting{ -3 }; constexpr int32_t WindowingBehaviorUseName{ -4 }; + +static constexpr std::wstring_view QuakeWindowName{ L"_quake" };