diff --git a/.github/actions/spelling/dictionary/apis.txt b/.github/actions/spelling/dictionary/apis.txt index 15ee1701f2c..dff45c936b4 100644 --- a/.github/actions/spelling/dictionary/apis.txt +++ b/.github/actions/spelling/dictionary/apis.txt @@ -44,6 +44,7 @@ IStorage IStringable ITab ITaskbar +IVirtual LCID llabs llu diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 13b8a1b31f1..322edd0a583 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -838,6 +838,16 @@ "type": "string" } ] + }, + "windowingBehavior": { + "default": "useNew", + "description": "Controls how new terminal instances attach to existing windows. \"useNew\" will always create a new window. \"useExisting\" will create new tabs in the most recently used window on this virtual desktop, and \"useAnyExisting\" will create tabs in the most recent window on any desktop.", + "enum": [ + "useNew", + "useExisting", + "useAnyExisting" + ], + "type": "string" } }, "required": [ diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index 1c3d1538906..628b8d01a37 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT license. #include "pch.h" +#include "../inc/WindowingBehavior.h" #include "Monarch.h" #include "CommandlineArgs.h" #include "FindTargetWindowArgs.h" @@ -20,6 +21,11 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation Monarch::Monarch() : _ourPID{ GetCurrentProcessId() } { + try + { + _desktopManager = winrt::create_instance(__uuidof(VirtualDesktopManager)); + } + CATCH_LOG(); } // This is a private constructor to be used in unit tests, where we don't @@ -39,7 +45,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } // Method Description: - // - Add the given peasant to the list of peasants we're tracking. This Peasant may have already been assigned an ID. If it hasn't, then give it an ID. + // - Add the given peasant to the list of peasants we're tracking. This + // Peasant may have already been assigned an ID. If it hasn't, then give + // it an ID. // Arguments: // - peasant: the new Peasant to track. // Return Value: @@ -106,7 +114,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } // Method Description: - // - Lookup a peasant by its ID. + // - Lookup a peasant by its ID. If the peasant has died, this will also + // remove the peasant from our list of peasants. // Arguments: // - peasantID: The ID Of the peasant to find // Return Value: @@ -130,63 +139,239 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation LOG_CAUGHT_EXCEPTION(); // Remove the peasant from the list of peasants _peasants.erase(peasantID); + + // Remove the peasant from the list of MRU windows. They're dead. + // They can't be the MRU anymore. + _clearOldMruEntries(peasantID); return nullptr; } } + // Method Description: + // - Handler for the `Peasant::WindowActivated` event. We'll make a in-proc + // copy of the WindowActivatedArgs from the peasant. That way, we won't + // need to worry about the origin process dying when working with the + // WindowActivatedArgs. + // - If the peasant process dies while we're making this copy, then we'll + // just log it and do nothing. We certainly don't want to track a dead + // peasant. + // - We'll pass that copy of the WindowActivatedArgs to + // _doHandleActivatePeasant, which will actually insert the + // WindowActivatedArgs into the list we're using to track the most recent + // peasants. + // Arguments: + // - args: the WindowActivatedArgs describing when and where the peasant was activated. + // Return Value: + // - void Monarch::HandleActivatePeasant(const Remoting::WindowActivatedArgs& args) { - // TODO:projects/5 Use a heap/priority queue per-desktop to track which - // peasant was the most recent per-desktop. When we want to get the most - // recent of all desktops (WindowingBehavior::UseExisting), then use the - // most recent of all desktops. - const auto oldLastActiveTime = _lastActivatedTime.time_since_epoch().count(); - const auto newLastActiveTime = args.ActivatedTime().time_since_epoch().count(); - - // For now, we'll just pay attention to whoever the most recent peasant - // was. We're not too worried about the mru peasant dying. Worst case - - // when the user executes a `wt -w 0`, we won't be able to find that - // peasant, and it'll open in a new window instead of the current one. - if (args.ActivatedTime() > _lastActivatedTime) + // Start by making a local copy of these args. It's easier for us if our + // tracking of these args is all in-proc. That way, the only thing that + // could fail due to the peasant dying is _this first copy_. + winrt::com_ptr localArgs{ nullptr }; + try + { + localArgs = winrt::make_self(args); + // This method will actually do the hard work + _doHandleActivatePeasant(localArgs); + } + catch (...) + { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_HandleActivatePeasant_Failed", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + } + } + + // Method Description: + // - Helper for removing a peasant from the list of MRU peasants. We want to + // do this both when the peasant dies, and also when the peasant is newly + // activated (so that we don't leave an old entry for it in the list). + // Arguments: + // - peasantID: The ID of the peasant to remove from the MRU list + // Return Value: + // - + void Monarch::_clearOldMruEntries(const uint64_t peasantID) + { + auto result = std::find_if(_mruPeasants.begin(), + _mruPeasants.end(), + [peasantID](auto&& other) { + return peasantID == other.PeasantID(); + }); + + if (result != std::end(_mruPeasants)) { - _mostRecentPeasant = args.PeasantID(); - _lastActivatedTime = args.ActivatedTime(); + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_RemovedPeasantFromDesktop", + TraceLoggingUInt64(peasantID, "peasantID", "The ID of the peasant"), + TraceLoggingGuid(result->DesktopID(), "desktopGuid", "The GUID of the previous desktop the window was on"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + + _mruPeasants.erase(result); } + } + + // Method Description: + // - Actually handle inserting the WindowActivatedArgs into our list of MRU windows. + // Arguments: + // - localArgs: an in-proc WindowActivatedArgs that we should add to our list of MRU windows. + // Return Value: + // - + void Monarch::_doHandleActivatePeasant(const winrt::com_ptr& localArgs) + { + const auto newLastActiveTime = localArgs->ActivatedTime().time_since_epoch().count(); + + // * Check all the current lists to look for this peasant. + // remove it from any where it exists. + _clearOldMruEntries(localArgs->PeasantID()); + + // * If the current desktop doesn't have a vector, add one. + const auto desktopGuid{ localArgs->DesktopID() }; + + // * Add this args list. By using lower_bound with insert, we can get it + // into exactly the right spot, without having to re-sort the whole + // array. + _mruPeasants.insert(std::lower_bound(_mruPeasants.begin(), + _mruPeasants.end(), + *localArgs, + [](const auto& first, const auto& second) { return first.ActivatedTime() > second.ActivatedTime(); }), + *localArgs); TraceLoggingWrite(g_hRemotingProvider, "Monarch_SetMostRecentPeasant", - TraceLoggingUInt64(args.PeasantID(), "peasantID", "the ID of the activated peasant"), - TraceLoggingInt64(oldLastActiveTime, "oldLastActiveTime", "The previous lastActiveTime"), - TraceLoggingInt64(newLastActiveTime, "newLastActiveTime", "The provided args.ActivatedTime()"), + TraceLoggingUInt64(localArgs->PeasantID(), "peasantID", "the ID of the activated peasant"), + TraceLoggingGuid(desktopGuid, "desktopGuid", "The GUID of the desktop the window is on"), + TraceLoggingInt64(newLastActiveTime, "newLastActiveTime", "The provided localArgs->ActivatedTime()"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); } - uint64_t Monarch::_getMostRecentPeasantID() + // Method Description: + // - Retrieves the ID of the MRU peasant window. If requested, will limit + // the search to windows that are on the current desktop. + // Arguments: + // - limitToCurrentDesktop: if true, only return the MRU peasant that's + // actually on the current desktop. + // Return Value: + // - the ID of the most recent peasant, otherwise 0 if we could not find one. + uint64_t Monarch::_getMostRecentPeasantID(const bool limitToCurrentDesktop) { - if (_mostRecentPeasant != 0) + if (_mruPeasants.empty()) { - return _mostRecentPeasant; + // We haven't yet been told the MRU peasant. Just use the first one. + // This is just gonna be a random one, but really shouldn't happen + // in practice. The WindowManager should set the MRU peasant + // immediately as soon as it creates the monarch/peasant for the + // first window. + if (_peasants.size() > 0) + { + try + { + return _peasants.begin()->second.GetID(); + } + catch (...) + { + // This shouldn't really happen. If we're the monarch, then the + // first peasant should also _be us_. So we should be able to + // get our own ID. + return 0; + } + } + + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_getMostRecentPeasantID_NoPeasants", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + return 0; } - // We haven't yet been told the MRU peasant. Just use the first one. - // This is just gonna be a random one, but really shouldn't happen - // in practice. The WindowManager should set the MRU peasant - // immediately as soon as it creates the monarch/peasant for the - // first window. - if (_peasants.size() > 0) + // Here, there's at least one MRU peasant. + // + // We're going to iterate over these peasants until we find one that both: + // 1. Is alive + // 2. Meets our selection criteria (do we care if it is on this desktop?) + // + // If the peasant is dead, then we'll remove it, and try the next one. + // Once we find one that's alive, we'll either: + // * check if we only want a peasant on the current desktop, and if so, + // check if this peasant is on the current desktop. + // - If it isn't on the current desktop, we'll loop again, on the + // following peasant. + // * If we don't care, then we'll just return that one. + // + // We're not just using an iterator because the contents of the list + // might change while we're iterating here (if the peasant is dead we'll + // remove it from the list). + int positionInList = 0; + while (_mruPeasants.cbegin() + positionInList < _mruPeasants.cend()) { - try + const auto mruWindowArgs{ *(_mruPeasants.begin() + positionInList) }; + const auto peasant{ _getPeasant(mruWindowArgs.PeasantID()) }; + if (!peasant) { - return _peasants.begin()->second.GetID(); + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_Collect_WasDead", + TraceLoggingUInt64(mruWindowArgs.PeasantID(), + "peasantID", + "We thought this peasant was the MRU one, but it was actually already dead."), + TraceLoggingGuid(mruWindowArgs.DesktopID(), "desktopGuid", "The GUID of the desktop the window is on"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + // We'll go through the loop again. We removed the current one + // at positionInList, so the next one in positionInList will be + // a new, different peasant. + continue; } - catch (...) + + if (limitToCurrentDesktop && _desktopManager) { - // This shouldn't really happen. If we're the monarch, then the - // first peasant should also _be us_. So we should be able to - // get our own ID. - return 0; + // Check if this peasant is actually on this desktop. We can't + // simply get the GUID of the current desktop. We have to ask if + // the HWND is on the current desktop. + BOOL onCurrentDesktop{ false }; + + // SUCCEEDED_LOG will log if it failed, and return true if it SUCCEEDED + if (SUCCEEDED_LOG(_desktopManager->IsWindowOnCurrentVirtualDesktop(reinterpret_cast(mruWindowArgs.Hwnd()), + &onCurrentDesktop)) && + onCurrentDesktop) + { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_Collect", + TraceLoggingUInt64(mruWindowArgs.PeasantID(), + "peasantID", + "the ID of the MRU peasant for a desktop"), + TraceLoggingGuid(mruWindowArgs.DesktopID(), + "desktopGuid", + "The GUID of the desktop the window is on"), + TraceLoggingBoolean(limitToCurrentDesktop, + "limitToCurrentDesktop", + "True if we should only search for a window on the current desktop"), + TraceLoggingBool(onCurrentDesktop, + "onCurrentDesktop", + "true if this window was in fact on the current desktop"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + return mruWindowArgs.PeasantID(); + } + // If this window wasn't on the current desktop, another one + // might be. We'll increment positionInList below, and try + // again. } + else + { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_getMostRecentPeasantID_Found", + TraceLoggingUInt64(mruWindowArgs.PeasantID(), "peasantID", "The ID of the MRU peasant"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + + return mruWindowArgs.PeasantID(); + } + positionInList++; } + + // Here, we've checked all the windows, and none of them was both alive + // and the most recent (on this desktop). Just return 0 - the caller + // will use this to create a new window. + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_getMostRecentPeasantID_NotFound", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + return 0; } @@ -216,16 +401,51 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // the parsed result. const auto targetWindow = findWindowArgs->ResultTargetWindow(); - // If there's a valid ID returned, then let's try and find the peasant that goes with it. - if (targetWindow >= 0) - { - uint64_t windowID = ::base::saturated_cast(targetWindow); + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_ProposeCommandline", + TraceLoggingInt64(targetWindow, "targetWindow", "The window ID the args specified"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); - if (windowID == 0) + // If there's a valid ID returned, then let's try and find the peasant + // that goes with it. Alternatively, if we were given a magic windowing + // constant, we can use that to look up an appropriate peasant. + if (targetWindow >= 0 || + targetWindow == WindowingBehaviorUseExisting || + targetWindow == WindowingBehaviorUseAnyExisting) + { + uint64_t windowID = 0; + switch (targetWindow) { - windowID = _getMostRecentPeasantID(); + case WindowingBehaviorUseCurrent: + case WindowingBehaviorUseExisting: + // TODO:projects/5 for now, just use the MRU window. Technically, + // UseExisting and UseCurrent are different. + // UseCurrent implies that we should try to do the WT_SESSION + // lookup to find the window that spawned this process (then + // fall back to sameDesktop if we can't find a match). For now, + // it's good enough to just try to find a match on this desktop. + windowID = _getMostRecentPeasantID(true); + break; + case WindowingBehaviorUseAnyExisting: + windowID = _getMostRecentPeasantID(false); + break; + default: + windowID = ::base::saturated_cast(targetWindow); + break; } + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_ProposeCommandline", + TraceLoggingInt64(windowID, + "windowID", + "The actual peasant ID we evaluated the window ID as"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + + // If_getMostRecentPeasantID returns 0 above, then we couldn't find + // a matching window for that style of windowing. _getPeasant will + // return nullptr, and we'll fall through to the "create a new + // window" branch below. + if (auto targetPeasant{ _getPeasant(windowID) }) { auto result{ winrt::make_self(false) }; @@ -251,9 +471,13 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingWrite(g_hRemotingProvider, "Monarch_ProposeCommandline_Existing", - TraceLoggingUInt64(windowID, "peasantID", "the ID of the peasant the commandline waws intended for"), + TraceLoggingUInt64(windowID, + "peasantID", + "the ID of the peasant the commandline waws intended for"), TraceLoggingBoolean(true, "foundMatch", "true if we found a peasant with that ID"), - TraceLoggingBoolean(!result->ShouldCreateWindow(), "succeeded", "true if we successfully dispatched the commandline to the peasant"), + TraceLoggingBoolean(!result->ShouldCreateWindow(), + "succeeded", + "true if we successfully dispatched the commandline to the peasant"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); return *result; } @@ -265,7 +489,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingWrite(g_hRemotingProvider, "Monarch_ProposeCommandline_Existing", - TraceLoggingUInt64(windowID, "peasantID", "the ID of the peasant the commandline waws intended for"), + TraceLoggingUInt64(windowID, + "peasantID", + "the ID of the peasant the commandline waws intended for"), TraceLoggingBoolean(false, "foundMatch", "true if we found a peasant with that ID"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); @@ -275,6 +501,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } + // If we get here, we couldn't find an existing window. Make a new one. TraceLoggingWrite(g_hRemotingProvider, "Monarch_ProposeCommandline_NewWindow", TraceLoggingInt64(targetWindow, "targetWindow", "The provided ID"), @@ -283,5 +510,4 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // In this case, no usable ID was provided. Return { true, nullopt } return winrt::make(true); } - } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 96456580e88..8c62e14866b 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -6,6 +6,7 @@ #include "Monarch.g.h" #include "Peasant.h" #include "../cascadia/inc/cppwinrt_utils.h" +#include "WindowActivatedArgs.h" // We sure different GUIDs here depending on whether we're running a Release, // Preview, or Dev build. This ensures that different installs don't @@ -30,12 +31,6 @@ constexpr GUID Monarch_clsid } }; -enum class WindowingBehavior : uint64_t -{ - UseNew = 0, - UseExisting = 1, -}; - namespace RemotingUnitTests { class RemotingTests; @@ -63,17 +58,20 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation uint64_t _nextPeasantID{ 1 }; uint64_t _thisPeasantID{ 0 }; - uint64_t _mostRecentPeasant{ 0 }; - winrt::Windows::Foundation::DateTime _lastActivatedTime{}; - WindowingBehavior _windowingBehavior{ WindowingBehavior::UseNew }; + winrt::com_ptr _desktopManager{ nullptr }; + std::unordered_map _peasants; + std::vector _mruPeasants; + winrt::Microsoft::Terminal::Remoting::IPeasant _getPeasant(uint64_t peasantID); - uint64_t _getMostRecentPeasantID(); + uint64_t _getMostRecentPeasantID(bool limitToCurrentDesktop); void _peasantWindowActivated(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs& args); + void _doHandleActivatePeasant(const winrt::com_ptr& args); + void _clearOldMruEntries(const uint64_t peasantID); friend class RemotingUnitTests::RemotingTests; }; diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index c77b5673e5c..8d092d3d423 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -16,7 +16,9 @@ namespace Microsoft.Terminal.Remoting runtimeclass WindowActivatedArgs { WindowActivatedArgs(UInt64 peasantID, Guid desktopID, Windows.Foundation.DateTime activatedTime); + WindowActivatedArgs(UInt64 peasantID, UInt64 hwnd, Guid desktopID, Windows.Foundation.DateTime activatedTime); UInt64 PeasantID { get; }; + UInt64 Hwnd { get; }; Guid DesktopID { get; }; Windows.Foundation.DateTime ActivatedTime { get; }; }; diff --git a/src/cascadia/Remoting/WindowActivatedArgs.h b/src/cascadia/Remoting/WindowActivatedArgs.h index de663383648..19ea914ef89 100644 --- a/src/cascadia/Remoting/WindowActivatedArgs.h +++ b/src/cascadia/Remoting/WindowActivatedArgs.h @@ -18,17 +18,40 @@ Class Name: namespace winrt::Microsoft::Terminal::Remoting::implementation { + struct CompareWindowActivatedArgs + { + bool operator()(const Remoting::WindowActivatedArgs& lhs, const Remoting::WindowActivatedArgs& rhs) const + { + return lhs.ActivatedTime() > rhs.ActivatedTime(); + } + }; struct WindowActivatedArgs : public WindowActivatedArgsT { GETSET_PROPERTY(uint64_t, PeasantID, 0); GETSET_PROPERTY(winrt::guid, DesktopID, {}); GETSET_PROPERTY(winrt::Windows::Foundation::DateTime, ActivatedTime, {}); + GETSET_PROPERTY(uint64_t, Hwnd, 0); public: - WindowActivatedArgs(uint64_t peasantID, winrt::guid desktopID, winrt::Windows::Foundation::DateTime timestamp) : + WindowActivatedArgs(uint64_t peasantID, + uint64_t hwnd, + winrt::guid desktopID, + winrt::Windows::Foundation::DateTime timestamp) : _PeasantID{ peasantID }, + _Hwnd{ hwnd }, _DesktopID{ desktopID }, _ActivatedTime{ timestamp } {}; + + WindowActivatedArgs(uint64_t peasantID, + winrt::guid desktopID, + winrt::Windows::Foundation::DateTime timestamp) : + WindowActivatedArgs(peasantID, 0, desktopID, timestamp){}; + + WindowActivatedArgs(const Remoting::WindowActivatedArgs& other) : + _PeasantID{ other.PeasantID() }, + _Hwnd{ other.Hwnd() }, + _DesktopID{ other.DesktopID() }, + _ActivatedTime{ other.ActivatedTime() } {}; }; } diff --git a/src/cascadia/Remoting/pch.h b/src/cascadia/Remoting/pch.h index 0fef7307ae5..f6d7a9de9ac 100644 --- a/src/cascadia/Remoting/pch.h +++ b/src/cascadia/Remoting/pch.h @@ -7,11 +7,17 @@ #pragma once +// Block minwindef.h min/max macros to prevent conflict +#define NOMINMAX + #define WIN32_LEAN_AND_MEAN #define NOMCX #define NOHELP #define NOCOMM +#include +#include + // Manually include til after we include Windows.Foundation to give it winrt superpowers #define BLOCK_TIL #include @@ -25,8 +31,6 @@ #include -#include - #include #include diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 2fb64c38c59..c6782252125 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -185,9 +185,9 @@ void AppCommandlineArgs::_buildParser() maximized->excludes(fullscreen); focus->excludes(fullscreen); - _app.add_option("-w,--window", - _windowTarget, - RS_A(L"CmdWindowTargetArgDesc")); + _app.add_option, int>("-w,--window", + _windowTarget, + RS_A(L"CmdWindowTargetArgDesc")); // Subcommands _buildNewTabParser(); @@ -854,10 +854,16 @@ void AppCommandlineArgs::FullResetState() _exitMessage = ""; _shouldExitEarly = false; - _windowTarget = -1; + _windowTarget = std::nullopt; } -int AppCommandlineArgs::GetTargetWindow() const noexcept +std::optional AppCommandlineArgs::GetTargetWindow() const noexcept { + // If the user provides _any_ negative number, then treat it as -1, for "use a new window". + if (_windowTarget.has_value() && *_windowTarget < 0) + { + return { -1 }; + } + return _windowTarget; } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index fa4c94ec2a1..a09bd36280b 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -44,7 +44,7 @@ class TerminalApp::AppCommandlineArgs final void DisableHelpInExitMessage(); void FullResetState(); - int GetTargetWindow() const noexcept; + std::optional GetTargetWindow() const noexcept; private: static const std::wregex _commandDelimiterRegex; @@ -106,7 +106,7 @@ class TerminalApp::AppCommandlineArgs final std::string _exitMessage; bool _shouldExitEarly{ false }; - int _windowTarget{ -1 }; + std::optional _windowTarget{ std::nullopt }; // Are you adding more args or attributes here? If they are not reset in _resetStateToDefault, make sure to reset them in FullResetState winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs _getNewTerminalArgs(NewTerminalSubcommand& subcommand); diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 6f2ec734b01..a7fc876f095 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "AppLogic.h" +#include "../inc/WindowingBehavior.h" #include "AppLogic.g.cpp" #include @@ -1197,7 +1198,11 @@ namespace winrt::TerminalApp::implementation // - args: an array of strings to process as a commandline. These args can contain spaces // Return Value: // - 0: We should handle the args "in the current window". - // - -1: We should handle the args in a new window + // - WindowingBehaviorUseNew: We should handle the args in a new window + // - WindowingBehaviorUseExisting: We should handle the args "in + // the current window ON THIS DESKTOP" + // - WindowingBehaviorUseAnyExisting: We should handle the args "in the current + // window ON ANY DESKTOP" // - anything else: We should handle the commandline in the window with the given ID. int32_t AppLogic::FindTargetWindow(array_view args) { @@ -1205,19 +1210,31 @@ namespace winrt::TerminalApp::implementation const auto result = appArgs.ParseArgs(args); if (result == 0) { - return appArgs.GetTargetWindow(); - - // TODO:projects/5 - // - // In the future, we'll want to use the windowingBehavior setting to - // determine what happens when a window ID wasn't manually provided. - // - // Maybe that'd be a special return value out of here, to tell the - // monarch to do something special: - // - // -1 -> create a new window - // -2 -> find the mru, this desktop - // -3 -> MRU, any desktop (is this not just 0?) + const auto parsedTarget = appArgs.GetTargetWindow(); + if (parsedTarget.has_value()) + { + // parsedTarget might be -1, if the user explicitly requested -1 + // (or any other negative number) on the commandline. So the set + // of possible values here is {-1, 0, ℤ+} + return *parsedTarget; + } + else + { + // If the user did not provide any value on the commandline, + // then lookup our windowing behavior to determine what to do + // now. + const auto windowingBehavior = _settings.GlobalSettings().WindowingBehavior(); + switch (windowingBehavior) + { + case WindowingMode::UseExisting: + return WindowingBehaviorUseExisting; + case WindowingMode::UseAnyExisting: + return WindowingBehaviorUseAnyExisting; + case WindowingMode::UseNew: + default: + return WindowingBehaviorUseNew; + } + } } // Any unsuccessful parse will be a new window. That new window will try @@ -1231,7 +1248,7 @@ namespace winrt::TerminalApp::implementation // create a new window. Then, in that new window, we'll try to set the // StartupActions, which will again fail, returning the correct error // message. - return -1; + return WindowingBehaviorUseNew; } // Method Description: diff --git a/src/cascadia/TerminalSettingsEditor/Launch.cpp b/src/cascadia/TerminalSettingsEditor/Launch.cpp index f1b42c9b0aa..b2bbaedd4bf 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.cpp +++ b/src/cascadia/TerminalSettingsEditor/Launch.cpp @@ -18,6 +18,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation InitializeComponent(); INITIALIZE_BINDABLE_ENUM_SETTING(LaunchMode, LaunchMode, LaunchMode, L"Globals_LaunchMode", L"Content"); + INITIALIZE_BINDABLE_ENUM_SETTING(WindowingBehavior, WindowingMode, WindowingMode, L"Globals_WindowingBehavior", L"Content"); } void Launch::OnNavigatedTo(const NavigationEventArgs& e) diff --git a/src/cascadia/TerminalSettingsEditor/Launch.h b/src/cascadia/TerminalSettingsEditor/Launch.h index 0cede6a9fbc..33fb30cb63c 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.h +++ b/src/cascadia/TerminalSettingsEditor/Launch.h @@ -31,6 +31,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation GETSET_PROPERTY(Editor::LaunchPageNavigationState, State, nullptr); GETSET_BINDABLE_ENUM_SETTING(LaunchMode, Model::LaunchMode, State().Settings().GlobalSettings, LaunchMode); + GETSET_BINDABLE_ENUM_SETTING(WindowingBehavior, Model::WindowingMode, State().Settings().GlobalSettings, WindowingBehavior); }; } diff --git a/src/cascadia/TerminalSettingsEditor/Launch.idl b/src/cascadia/TerminalSettingsEditor/Launch.idl index 8823a80fb86..e2a1382e0d3 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.idl +++ b/src/cascadia/TerminalSettingsEditor/Launch.idl @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "EnumEntry.idl"; +import "EnumEntry.idl"; namespace Microsoft.Terminal.Settings.Editor { @@ -18,6 +18,10 @@ namespace Microsoft.Terminal.Settings.Editor IInspectable CurrentDefaultProfile; IInspectable CurrentLaunchMode; - Windows.Foundation.Collections.IObservableVector LaunchModeList { get; }; + IObservableVector LaunchModeList { get; }; + + + IInspectable CurrentWindowingBehavior; + IObservableVector WindowingBehaviorList { get; }; } } diff --git a/src/cascadia/TerminalSettingsEditor/Launch.xaml b/src/cascadia/TerminalSettingsEditor/Launch.xaml index c9c2a090d43..ab0b60d1862 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.xaml +++ b/src/cascadia/TerminalSettingsEditor/Launch.xaml @@ -78,6 +78,13 @@ the MIT License. See LICENSE in the project root for license information. --> ItemsSource="{x:Bind LaunchModeList}" ItemTemplate="{StaticResource EnumRadioButtonTemplate}"/> + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 20fe3d4138a..155827a8381 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -232,6 +232,21 @@ Maximized + + New instance behavior + + + Controls how new terminal instances attach to existing windows. + + + Create a new window + + + Attach to the most recently used window + + + Attach to the most recently used window on this desktop + These settings may be useful for troubleshooting an issue, however they will impact your performance. diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp index 7058df611f9..f28e2a44c50 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp @@ -35,6 +35,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation DEFINE_ENUM_MAP(Model::LaunchMode, LaunchMode); DEFINE_ENUM_MAP(Model::TabSwitcherMode, TabSwitcherMode); DEFINE_ENUM_MAP(Microsoft::Terminal::TerminalControl::CopyFormat, CopyFormat); + DEFINE_ENUM_MAP(Model::WindowingMode, WindowingMode); // Profile Settings DEFINE_ENUM_MAP(Model::CloseOnExitMode, CloseOnExitMode); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.h b/src/cascadia/TerminalSettingsModel/EnumMappings.h index fd299107f77..69aba3f9014 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.h +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.h @@ -31,6 +31,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static winrt::Windows::Foundation::Collections::IMap LaunchMode(); static winrt::Windows::Foundation::Collections::IMap TabSwitcherMode(); static winrt::Windows::Foundation::Collections::IMap CopyFormat(); + static winrt::Windows::Foundation::Collections::IMap WindowingMode(); // Profile Settings static winrt::Windows::Foundation::Collections::IMap CloseOnExitMode(); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.idl b/src/cascadia/TerminalSettingsModel/EnumMappings.idl index 86ec268fcbf..d9ff512a7b3 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.idl +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.idl @@ -13,6 +13,7 @@ namespace Microsoft.Terminal.Settings.Model static Windows.Foundation.Collections.IMap LaunchMode { get; }; static Windows.Foundation.Collections.IMap TabSwitcherMode { get; }; static Windows.Foundation.Collections.IMap CopyFormat { get; }; + static Windows.Foundation.Collections.IMap WindowingMode { get; }; // Profile Settings static Windows.Foundation.Collections.IMap CloseOnExitMode { get; }; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 61846613bc0..fc0debc5c01 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -42,6 +42,7 @@ static constexpr std::string_view TabSwitcherModeKey{ "tabSwitcherMode" }; static constexpr std::string_view DisableAnimationsKey{ "disableAnimations" }; 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 DebugFeaturesKey{ "debugFeatures" }; @@ -118,6 +119,7 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_DisableAnimations = _DisableAnimations; globals->_StartupActions = _StartupActions; globals->_FocusFollowMouse = _FocusFollowMouse; + globals->_WindowingBehavior = _WindowingBehavior; globals->_UnparsedDefaultProfile = _UnparsedDefaultProfile; globals->_validDefaultProfile = _validDefaultProfile; @@ -308,6 +310,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, FocusFollowMouseKey, _FocusFollowMouse); + JsonUtils::GetValueForKey(json, WindowingBehaviorKey, _WindowingBehavior); + // 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. @@ -404,6 +408,7 @@ Json::Value GlobalAppSettings::ToJson() const JsonUtils::SetValueForKey(json, DisableAnimationsKey, _DisableAnimations); JsonUtils::SetValueForKey(json, StartupActionsKey, _StartupActions); JsonUtils::SetValueForKey(json, FocusFollowMouseKey, _FocusFollowMouse); + JsonUtils::SetValueForKey(json, WindowingBehaviorKey, _WindowingBehavior); // clang-format on // TODO GH#8100: keymap needs to be serialized here diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index bfa47d63718..506db9839dd 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -88,6 +88,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation GETSET_SETTING(bool, DisableAnimations, false); GETSET_SETTING(hstring, StartupActions, L""); GETSET_SETTING(bool, FocusFollowMouse, false); + GETSET_SETTING(Model::WindowingMode, WindowingBehavior, Model::WindowingMode::UseNew); private: guid _defaultProfile; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index e9ffca476db..8af520bc80a 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -33,6 +33,13 @@ namespace Microsoft.Terminal.Settings.Model Disabled, }; + enum WindowingMode + { + UseNew, + UseAnyExisting, + UseExisting, + }; + [default_interface] runtimeclass GlobalAppSettings { Guid DefaultProfile; Boolean HasUnparsedDefaultProfile(); @@ -150,5 +157,9 @@ namespace Microsoft.Terminal.Settings.Model Boolean HasFocusFollowMouse(); void ClearFocusFollowMouse(); Boolean FocusFollowMouse; + + Boolean HasWindowingBehavior(); + void ClearWindowingBehavior(); + WindowingMode WindowingBehavior; } } diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index c71e858d06b..416a284330b 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -439,3 +439,12 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FindMatchDirecti pair_type{ "prev", ValueType::Previous }, }; }; + +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::WindowingMode) +{ + JSON_MAPPINGS(3) = { + pair_type{ "useNew", ValueType::UseNew }, + pair_type{ "useAnyExisting", ValueType::UseAnyExisting }, + pair_type{ "useExisting", ValueType::UseExisting }, + }; +}; diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 557a7ab85fe..81856c5422f 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -79,6 +79,12 @@ namespace RemotingUnitTests TEST_METHOD(ProposeCommandlineNonExistentWindow); TEST_METHOD(ProposeCommandlineDeadWindow); + TEST_METHOD(MostRecentWindowSameDesktops); + TEST_METHOD(MostRecentWindowDifferentDesktops); + TEST_METHOD(MostRecentWindowMoveDesktops); + TEST_METHOD(GetMostRecentAnyDesktop); + TEST_METHOD(MostRecentIsDead); + TEST_CLASS_SETUP(ClassSetup) { return true; @@ -116,7 +122,7 @@ namespace RemotingUnitTests if (arguments.size() > 0) { const auto index = std::stoi(arguments.at(0).c_str()); - args.ResultTargetWindow(index); + args.ResultTargetWindow(index >= 0 ? index : -1); } } @@ -602,4 +608,374 @@ namespace RemotingUnitTests VERIFY_ARE_EQUAL(1u, result.Id().Value()); } } + + // TODO:projects/5 + // + // In order to test WindowingBehaviorUseExisting, we'll have to + // create our own IVirtualDesktopManager implementation that can be subbed + // in for testing. We can't _actually_ create HWNDs as a part of the test + // and move them to different desktops. Instead, we'll have to create a stub + // impl that can fake a result for IsWindowOnCurrentVirtualDesktop. + + void RemotingTests::MostRecentWindowSameDesktops() + { + Log::Comment(L"Make windows on the same desktop. Validate the contents of _mruPeasants are as expected."); + + const winrt::guid guid1{ Utils::GuidFromString(L"{11111111-1111-1111-1111-111111111111}") }; + const winrt::guid guid2{ Utils::GuidFromString(L"{22222222-2222-2222-2222-222222222222}") }; + + const auto monarch0PID = 12345u; + com_ptr m0; + m0.attach(new Remoting::implementation::Monarch(monarch0PID)); + VERIFY_IS_NOT_NULL(m0); + m0->FindTargetWindowRequested(&RemotingTests::_findTargetWindowHelper); + + Log::Comment(L"Add a peasant"); + const auto peasant1PID = 23456u; + com_ptr p1; + p1.attach(new Remoting::implementation::Peasant(peasant1PID)); + VERIFY_IS_NOT_NULL(p1); + m0->AddPeasant(*p1); + + Log::Comment(L"Add a second peasant"); + const auto peasant2PID = 34567u; + com_ptr p2; + p2.attach(new Remoting::implementation::Peasant(peasant2PID)); + VERIFY_IS_NOT_NULL(p2); + m0->AddPeasant(*p2); + + { + 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); + } + VERIFY_ARE_EQUAL(2u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[1].PeasantID()); + + { + Log::Comment(L"Activate the first peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p1->GetID(), + guid1, + winrt::clock().now() }; + p1->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(2u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[1].PeasantID()); + } + + void RemotingTests::MostRecentWindowDifferentDesktops() + { + Log::Comment(L"Make windows on different desktops. Validate the contents of _mruPeasants are as expected."); + + const winrt::guid guid1{ Utils::GuidFromString(L"{11111111-1111-1111-1111-111111111111}") }; + const winrt::guid guid2{ Utils::GuidFromString(L"{22222222-2222-2222-2222-222222222222}") }; + + const auto monarch0PID = 12345u; + com_ptr m0; + m0.attach(new Remoting::implementation::Monarch(monarch0PID)); + VERIFY_IS_NOT_NULL(m0); + m0->FindTargetWindowRequested(&RemotingTests::_findTargetWindowHelper); + + Log::Comment(L"Add a peasant"); + const auto peasant1PID = 23456u; + com_ptr p1; + p1.attach(new Remoting::implementation::Peasant(peasant1PID)); + VERIFY_IS_NOT_NULL(p1); + m0->AddPeasant(*p1); + + Log::Comment(L"Add a second peasant"); + const auto peasant2PID = 34567u; + com_ptr p2; + p2.attach(new Remoting::implementation::Peasant(peasant2PID)); + VERIFY_IS_NOT_NULL(p2); + m0->AddPeasant(*p2); + + { + 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, second desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p2->GetID(), + guid2, + winrt::clock().now() }; + p2->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(2u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[1].PeasantID()); + + Log::Comment(L"Add a third peasant"); + const auto peasant3PID = 45678u; + com_ptr p3; + p3.attach(new Remoting::implementation::Peasant(peasant3PID)); + VERIFY_IS_NOT_NULL(p3); + m0->AddPeasant(*p3); + { + Log::Comment(L"Activate the third peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p3->GetID(), + guid1, + winrt::clock().now() }; + p3->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(3u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p3->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[1].PeasantID()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[2].PeasantID()); + + { + Log::Comment(L"Activate the first peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p1->GetID(), + guid1, + winrt::clock().now() }; + p1->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(3u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p3->GetID(), m0->_mruPeasants[1].PeasantID()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[2].PeasantID()); + } + + void RemotingTests::MostRecentWindowMoveDesktops() + { + Log::Comment(L"Make windows on different desktops. Move one to another " + L"desktop. Validate the contents of _mruPeasants are as expected."); + + const winrt::guid guid1{ Utils::GuidFromString(L"{11111111-1111-1111-1111-111111111111}") }; + const winrt::guid guid2{ Utils::GuidFromString(L"{22222222-2222-2222-2222-222222222222}") }; + + const auto monarch0PID = 12345u; + com_ptr m0; + m0.attach(new Remoting::implementation::Monarch(monarch0PID)); + VERIFY_IS_NOT_NULL(m0); + m0->FindTargetWindowRequested(&RemotingTests::_findTargetWindowHelper); + + Log::Comment(L"Add a peasant"); + const auto peasant1PID = 23456u; + com_ptr p1; + p1.attach(new Remoting::implementation::Peasant(peasant1PID)); + VERIFY_IS_NOT_NULL(p1); + m0->AddPeasant(*p1); + + Log::Comment(L"Add a second peasant"); + const auto peasant2PID = 34567u; + com_ptr p2; + p2.attach(new Remoting::implementation::Peasant(peasant2PID)); + VERIFY_IS_NOT_NULL(p2); + m0->AddPeasant(*p2); + + { + 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, second desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p2->GetID(), + guid2, + winrt::clock().now() }; + p2->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(2u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[1].PeasantID()); + + Log::Comment(L"Add a third peasant"); + const auto peasant3PID = 45678u; + com_ptr p3; + p3.attach(new Remoting::implementation::Peasant(peasant3PID)); + VERIFY_IS_NOT_NULL(p3); + m0->AddPeasant(*p3); + { + Log::Comment(L"Activate the third peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p3->GetID(), + guid1, + winrt::clock().now() }; + p3->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(3u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p3->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[1].PeasantID()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[2].PeasantID()); + + { + Log::Comment(L"Activate the first peasant, second desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p1->GetID(), + guid2, + winrt::clock().now() }; + p1->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(3u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p3->GetID(), m0->_mruPeasants[1].PeasantID()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[2].PeasantID()); + + { + Log::Comment(L"Activate the third peasant, second desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p3->GetID(), + guid2, + winrt::clock().now() }; + p3->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(3u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p3->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[1].PeasantID()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[2].PeasantID()); + + { + Log::Comment(L"Activate the second peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p2->GetID(), + guid1, + winrt::clock().now() }; + p2->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(3u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p3->GetID(), m0->_mruPeasants[1].PeasantID()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[2].PeasantID()); + } + + void RemotingTests::GetMostRecentAnyDesktop() + { + Log::Comment(L"Make windows on different desktops. Confirm that " + L"getting the most recent of all windows works as expected."); + + const winrt::guid guid1{ Utils::GuidFromString(L"{11111111-1111-1111-1111-111111111111}") }; + const winrt::guid guid2{ Utils::GuidFromString(L"{22222222-2222-2222-2222-222222222222}") }; + + const auto monarch0PID = 12345u; + com_ptr m0; + m0.attach(new Remoting::implementation::Monarch(monarch0PID)); + VERIFY_IS_NOT_NULL(m0); + m0->FindTargetWindowRequested(&RemotingTests::_findTargetWindowHelper); + + Log::Comment(L"Add a peasant"); + const auto peasant1PID = 23456u; + com_ptr p1; + p1.attach(new Remoting::implementation::Peasant(peasant1PID)); + VERIFY_IS_NOT_NULL(p1); + m0->AddPeasant(*p1); + + Log::Comment(L"Add a second peasant"); + const auto peasant2PID = 34567u; + com_ptr p2; + p2.attach(new Remoting::implementation::Peasant(peasant2PID)); + VERIFY_IS_NOT_NULL(p2); + m0->AddPeasant(*p2); + + { + 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, second desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p2->GetID(), + guid2, + winrt::clock().now() }; + p2->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(p2->GetID(), m0->_getMostRecentPeasantID(false)); + + Log::Comment(L"Add a third peasant"); + const auto peasant3PID = 45678u; + com_ptr p3; + p3.attach(new Remoting::implementation::Peasant(peasant3PID)); + VERIFY_IS_NOT_NULL(p3); + m0->AddPeasant(*p3); + { + Log::Comment(L"Activate the third peasant, first desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p3->GetID(), + guid1, + winrt::clock().now() }; + p3->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(p3->GetID(), m0->_getMostRecentPeasantID(false)); + + { + Log::Comment(L"Activate the first peasant, second desktop"); + Remoting::WindowActivatedArgs activatedArgs{ p1->GetID(), + guid2, + winrt::clock().now() }; + p1->ActivateWindow(activatedArgs); + } + VERIFY_ARE_EQUAL(p1->GetID(), m0->_getMostRecentPeasantID(false)); + } + + void RemotingTests::MostRecentIsDead() + { + Log::Comment(L"Make two windows. Activate the first, then the second. " + L"Kill the second. The most recent should be the _first_ window."); + + const winrt::guid guid1{ Utils::GuidFromString(L"{11111111-1111-1111-1111-111111111111}") }; + const winrt::guid guid2{ Utils::GuidFromString(L"{22222222-2222-2222-2222-222222222222}") }; + + const auto monarch0PID = 12345u; + com_ptr m0; + m0.attach(new Remoting::implementation::Monarch(monarch0PID)); + VERIFY_IS_NOT_NULL(m0); + m0->FindTargetWindowRequested(&RemotingTests::_findTargetWindowHelper); + + Log::Comment(L"Add a peasant"); + const auto peasant1PID = 23456u; + com_ptr p1; + p1.attach(new Remoting::implementation::Peasant(peasant1PID)); + VERIFY_IS_NOT_NULL(p1); + m0->AddPeasant(*p1); + + Log::Comment(L"Add a second peasant"); + const auto peasant2PID = 34567u; + com_ptr p2; + p2.attach(new Remoting::implementation::Peasant(peasant2PID)); + VERIFY_IS_NOT_NULL(p2); + m0->AddPeasant(*p2); + + { + 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); + } + VERIFY_ARE_EQUAL(2u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p2->GetID(), m0->_mruPeasants[0].PeasantID()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[1].PeasantID()); + + Log::Comment(L"Kill peasant 2"); + RemotingTests::_killPeasant(m0, p2->GetID()); + Log::Comment(L"Peasant 1 should be the new MRU peasant"); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_getMostRecentPeasantID(false)); + + Log::Comment(L"Peasant 2 should not be in the monarch at all anymore"); + VERIFY_ARE_EQUAL(1u, m0->_peasants.size()); + VERIFY_ARE_EQUAL(1u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(1u, m0->_mruPeasants.size()); + VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[0].PeasantID()); + } + } diff --git a/src/cascadia/UnitTests_Remoting/pch.h b/src/cascadia/UnitTests_Remoting/pch.h index da79b2d4fd8..8239e461c15 100644 --- a/src/cascadia/UnitTests_Remoting/pch.h +++ b/src/cascadia/UnitTests_Remoting/pch.h @@ -5,6 +5,17 @@ Licensed under the MIT license. #pragma once +// Block minwindef.h min/max macros to prevent conflict +#define NOMINMAX + +#define WIN32_LEAN_AND_MEAN +#define NOMCX +#define NOHELP +#define NOCOMM + +#include +#include + // Manually include til after we include Windows.Foundation to give it winrt superpowers #define BLOCK_TIL // This includes support libraries from the CRT, STL, WIL, and GSL diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 492a21d1d27..24fb7cdc062 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -557,12 +557,30 @@ winrt::fire_and_forget AppHost::_WindowActivated() if (auto peasant{ _windowManager.CurrentWindow() }) { + const auto currentDesktopGuid{ _CurrentDesktopGuid() }; + // TODO: projects/5 - in the future, we'll want to actually get the // desktop GUID in IslandWindow, and bubble that up here, then down to // the Peasant. For now, we're just leaving space for it. Remoting::WindowActivatedArgs args{ peasant.GetID(), - winrt::guid{}, + (uint64_t)_window->GetHandle(), + currentDesktopGuid, winrt::clock().now() }; peasant.ActivateWindow(args); } } + +GUID AppHost::_CurrentDesktopGuid() +{ + GUID currentDesktopGuid{ 0 }; + try + { + const auto manager = winrt::create_instance(__uuidof(VirtualDesktopManager)); + if (manager) + { + LOG_IF_FAILED(manager->GetWindowDesktopId(_window->GetHandle(), ¤tDesktopGuid)); + } + } + CATCH_LOG(); + return currentDesktopGuid; +} diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index db32ad67106..c8782d61e31 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -51,4 +51,5 @@ class AppHost void _FindTargetWindow(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args); + GUID _CurrentDesktopGuid(); }; diff --git a/src/cascadia/inc/WindowingBehavior.h b/src/cascadia/inc/WindowingBehavior.h new file mode 100644 index 00000000000..0a6b55bd34e --- /dev/null +++ b/src/cascadia/inc/WindowingBehavior.h @@ -0,0 +1,10 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. +--*/ + +#pragma once +constexpr int64_t WindowingBehaviorUseCurrent{ 0 }; +constexpr int64_t WindowingBehaviorUseNew{ -1 }; +constexpr int64_t WindowingBehaviorUseExisting{ -2 }; +constexpr int64_t WindowingBehaviorUseAnyExisting{ -3 };