diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 0843ec910e6..c55d71ad4f6 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -159,6 +159,7 @@ rcx REGCLS RETURNCMD rfind +RLO ROOTOWNER roundf RSHIFT diff --git a/.vscode/settings.json b/.vscode/settings.json index 604f91797d9..bb2f304e54a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,7 @@ "C_Cpp.loggingLevel": "None", "files.associations": { "xstring": "cpp", - "*.idl": "cpp", + "*.idl": "midl3", "array": "cpp", "future": "cpp", "istream": "cpp", @@ -106,4 +106,4 @@ "**/packages/**": true, "**/Generated Files/**": true } -} \ No newline at end of file +} diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 64b5903dc39..2c62802115a 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1822,7 +1822,7 @@ "name": { "type": "string", "description": "The name of the theme. This will be displayed in the settings UI.", - "not": { + "not": { "enum": [ "light", "dark", "system" ] } }, @@ -2092,6 +2092,11 @@ "description": "When set to true, the terminal will focus the pane on mouse hover.", "type": "boolean" }, + "compatibility.isolatedMode": { + "default": false, + "description": "When set to true, Terminal windows will not be able to interact with each other (including global hotkeys, tab drag/drop, running commandlines in existing windows, etc.). This is a compatibility escape hatch for users who are running into certain windowing issues.", + "type": "boolean" + }, "copyFormatting": { "default": true, "description": "When set to `true`, the color and font formatting of selected text is also copied to your clipboard. When set to `false`, only plain text is copied to your clipboard. An array of specific formats can also be used. Supported array values include `html` and `rtf`. Plain text is always copied.", diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index a3dcbb45f4e..a940cc915f3 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -98,6 +98,26 @@ namespace TerminalAppLocalTests } } } + void _logCommands(winrt::Windows::Foundation::Collections::IVector commands, const int indentation = 1) + { + if (indentation == 1) + { + Log::Comment((commands.Size() == 0) ? L"Commands:\n " : L"Commands:"); + } + for (const auto& cmd : commands) + { + Log::Comment(fmt::format(L"{0:>{1}}* {2}", + L"", + indentation, + cmd.Name()) + .c_str()); + + if (cmd.HasNestedCommands()) + { + _logCommandNames(cmd.NestedCommands(), indentation + 2); + } + } + } }; void SettingsTests::TestIterateCommands() @@ -164,14 +184,15 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); { - auto command = expandedCommands.Lookup(L"iterable command profile0"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"iterable command profile0", command.Name()); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -189,7 +210,8 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile1"); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"iterable command profile1", command.Name()); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -207,7 +229,8 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile2"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"iterable command profile2", command.Name()); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -287,14 +310,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); { - auto command = expandedCommands.Lookup(L"Split pane, profile: profile0"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"Split pane, profile: profile0", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -312,7 +337,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"Split pane, profile: profile1"); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"Split pane, profile: profile1", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -330,7 +357,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"Split pane, profile: profile2"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"Split pane, profile: profile2", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -412,14 +441,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); { - auto command = expandedCommands.Lookup(L"iterable command profile0"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"iterable command profile0", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -437,7 +468,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile1\""); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"iterable command profile1\"", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -455,7 +488,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile2"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"iterable command profile2", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -527,14 +562,15 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto rootCommand = expandedCommands.Lookup(L"Connect to ssh..."); + auto rootCommand = expandedCommands.GetAt(0); VERIFY_IS_NOT_NULL(rootCommand); + VERIFY_ARE_EQUAL(L"Connect to ssh...", rootCommand.Name()); auto rootActionAndArgs = rootCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(rootActionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::Invalid, rootActionAndArgs.Action()); @@ -621,14 +657,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto grandparentCommand = expandedCommands.Lookup(L"grandparent"); + auto grandparentCommand = expandedCommands.GetAt(0); VERIFY_IS_NOT_NULL(grandparentCommand); + VERIFY_ARE_EQUAL(L"grandparent", grandparentCommand.Name()); + auto grandparentActionAndArgs = grandparentCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(grandparentActionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::Invalid, grandparentActionAndArgs.Action()); @@ -744,17 +782,22 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); - for (auto name : std::vector({ L"profile0", L"profile1", L"profile2" })) + const std::vector profileNames{ L"profile0", L"profile1", L"profile2" }; + for (auto i = 0u; i < profileNames.size(); i++) { - winrt::hstring commandName{ name + L"..." }; - auto command = expandedCommands.Lookup(commandName); + const auto& name{ profileNames[i] }; + winrt::hstring commandName{ profileNames[i] + L"..." }; + + auto command = expandedCommands.GetAt(i); + VERIFY_ARE_EQUAL(commandName, command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -880,14 +923,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto rootCommand = expandedCommands.Lookup(L"New Tab With Profile..."); + auto rootCommand = expandedCommands.GetAt(0); VERIFY_IS_NOT_NULL(rootCommand); + VERIFY_ARE_EQUAL(L"New Tab With Profile...", rootCommand.Name()); + auto rootActionAndArgs = rootCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(rootActionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::Invalid, rootActionAndArgs.Action()); @@ -982,13 +1027,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto rootCommand = expandedCommands.Lookup(L"New Pane..."); + auto rootCommand = expandedCommands.GetAt(0); + VERIFY_IS_NOT_NULL(rootCommand); + VERIFY_ARE_EQUAL(L"New Pane...", rootCommand.Name()); + VERIFY_IS_NOT_NULL(rootCommand); auto rootActionAndArgs = rootCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(rootActionAndArgs); @@ -1205,8 +1253,8 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${scheme.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); @@ -1215,7 +1263,9 @@ namespace TerminalAppLocalTests // just easy tests to write. { - auto command = expandedCommands.Lookup(L"iterable command Campbell"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"iterable command Campbell", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -1233,7 +1283,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command Campbell PowerShell"); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"iterable command Campbell PowerShell", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -1251,7 +1303,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command Vintage"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"iterable command Vintage", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index 8d69e2768e9..10cbbf78faf 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -10,6 +10,7 @@ #include "ProposeCommandlineResult.h" #include "Monarch.g.cpp" +#include "WindowRequestedArgs.g.cpp" #include "../../types/inc/utils.hpp" using namespace winrt; @@ -658,6 +659,13 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + if (targetWindow == WindowingBehaviorUseNone) + { + // In this case, the targetWindow was UseNone, which means that we + // want to make a message box, but otherwise not make a Terminal + // window. + return winrt::make(false); + } // 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. @@ -687,6 +695,11 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation case WindowingBehaviorUseName: windowID = _lookupPeasantIdForName(targetWindowName); break; + case WindowingBehaviorUseNone: + // This should be impossible. The if statement above should have + // prevented WindowingBehaviorUseNone from falling in here. + // Explode, because this is a programming error. + THROW_HR(E_UNEXPECTED); default: windowID = ::base::saturated_cast(targetWindow); break; @@ -724,6 +737,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation result->WindowName(targetWindowName); result->ShouldCreateWindow(true); + _RequestNewWindowHandlers(*this, *winrt::make_self(*result, args)); + // If this fails, it'll be logged in the following // TraceLoggingWrite statement, with succeeded=false } @@ -759,6 +774,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation auto result{ winrt::make_self(true) }; result->Id(windowID); result->WindowName(targetWindowName); + + _RequestNewWindowHandlers(*this, *winrt::make_self(*result, args)); + return *result; } } @@ -773,6 +791,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // In this case, no usable ID was provided. Return { true, nullopt } auto result = winrt::make_self(true); result->WindowName(targetWindowName); + + _RequestNewWindowHandlers(*this, *winrt::make_self(*result, args)); + return *result; } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index a9eac206daf..d347fc73daa 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 "WindowActivatedArgs.h" +#include "WindowRequestedArgs.g.h" #include // We sure different GUIDs here depending on whether we're running a Release, @@ -38,6 +39,26 @@ namespace RemotingUnitTests namespace winrt::Microsoft::Terminal::Remoting::implementation { + struct WindowRequestedArgs : public WindowRequestedArgsT + { + public: + WindowRequestedArgs(const Remoting::ProposeCommandlineResult& windowInfo, const Remoting::CommandlineArgs& command) : + _Id{ windowInfo.Id() ? windowInfo.Id().Value() : 0 }, // We'll use 0 as a sentinel, since no window will ever get to have that ID + _WindowName{ windowInfo.WindowName() }, + _args{ command.Commandline() }, + _CurrentDirectory{ command.CurrentDirectory() } {}; + + void Commandline(const winrt::array_view& value) { _args = { value.begin(), value.end() }; }; + winrt::com_array Commandline() { return winrt::com_array{ _args.begin(), _args.end() }; } + + WINRT_PROPERTY(uint64_t, Id); + WINRT_PROPERTY(winrt::hstring, WindowName); + WINRT_PROPERTY(winrt::hstring, CurrentDirectory); + + private: + winrt::com_array _args; + }; + struct Monarch : public MonarchT { Monarch(); @@ -67,6 +88,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); + TYPED_EVENT(RequestNewWindow, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs); + private: uint64_t _ourPID; @@ -191,4 +214,5 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation namespace winrt::Microsoft::Terminal::Remoting::factory_implementation { BASIC_FACTORY(Monarch); + BASIC_FACTORY(WindowRequestedArgs); } diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index 2ef09807080..c046a82d3b5 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -18,6 +18,16 @@ namespace Microsoft.Terminal.Remoting Boolean ShouldCreateWindow { get; }; // If you name this `CreateWindow`, the compiler will explode } + [default_interface] runtimeclass WindowRequestedArgs { + WindowRequestedArgs(ProposeCommandlineResult windowInfo, CommandlineArgs command); + + UInt64 Id { get; }; + String WindowName { get; }; + + String[] Commandline { get; }; + String CurrentDirectory { get; }; + } + [default_interface] runtimeclass SummonWindowSelectionArgs { SummonWindowSelectionArgs(); SummonWindowSelectionArgs(String windowName); @@ -66,6 +76,8 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; event Windows.Foundation.TypedEventHandler QuitAllRequested; + + event Windows.Foundation.TypedEventHandler RequestNewWindow; }; runtimeclass Monarch : [default] IMonarch diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index ec87c85188d..4a94f1e2255 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -43,7 +43,6 @@ namespace Microsoft.Terminal.Remoting ToMouse, }; - [default_interface] runtimeclass SummonWindowBehavior { SummonWindowBehavior(); Boolean MoveToCurrentDesktop; diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 8c0b8d5a900..d448c1cd26e 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -2,10 +2,13 @@ // Licensed under the MIT license. #include "pch.h" + #include "WindowManager.h" + +#include "../inc/WindowingBehavior.h" #include "MonarchFactory.h" + #include "CommandlineArgs.h" -#include "../inc/WindowingBehavior.h" #include "FindTargetWindowArgs.h" #include "ProposeCommandlineResult.h" @@ -21,32 +24,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { WindowManager::WindowManager() { - _monarchWaitInterrupt.create(); - - // Register with COM as a server for the Monarch class - _registerAsMonarch(); - // Instantiate an instance of the Monarch. This may or may not be in-proc! - auto foundMonarch = false; - while (!foundMonarch) - { - try - { - _createMonarchAndCallbacks(); - // _createMonarchAndCallbacks will initialize _isKing - foundMonarch = true; - } - catch (...) - { - // If we fail to find the monarch, - // stay in this jail until we do. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ExceptionInCtor", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } } - WindowManager::~WindowManager() { // IMPORTANT! Tear down the registration as soon as we exit. If we're not a @@ -55,32 +33,178 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // monarch! CoRevokeClassObject(_registrationHostClass); _registrationHostClass = 0; - SignalClose(); - _monarchWaitInterrupt.SetEvent(); + } - // A thread is joinable once it's been started. Basically this just - // makes sure that the thread isn't just default-constructed. - if (_electionThread.joinable()) - { - _electionThread.join(); - } + void WindowManager::_createMonarch() + { + // Heads up! This only works because we're using + // "metadata-based-marshalling" for our WinRT types. That means the OS is + // using the .winmd file we generate to figure out the proxy/stub + // definitions for our types automatically. This only works in the following + // cases: + // + // * If we're running unpackaged: the .winmd must be a sibling of the .exe + // * If we're running packaged: the .winmd must be in the package root + _monarch = try_create_instance(Monarch_clsid, + CLSCTX_LOCAL_SERVER); + } + + // Check if we became the king, and if we are, wire up callbacks. + void WindowManager::_createCallbacks() + { + assert(_monarch); + // Here, we're the king! + // + // This is where you should do any additional setup that might need to be + // done when we become the king. This will be called both for the first + // window, and when the current monarch dies. + + _monarch.WindowCreated({ get_weak(), &WindowManager::_WindowCreatedHandlers }); + _monarch.WindowClosed({ get_weak(), &WindowManager::_WindowClosedHandlers }); + _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); + _monarch.QuitAllRequested({ get_weak(), &WindowManager::_QuitAllRequestedHandlers }); + + _monarch.RequestNewWindow({ get_weak(), &WindowManager::_raiseRequestNewWindow }); + } + + void WindowManager::_registerAsMonarch() + { + winrt::check_hresult(CoRegisterClassObject(Monarch_clsid, + winrt::make<::MonarchFactory>().get(), + CLSCTX_LOCAL_SERVER, + REGCLS_MULTIPLEUSE, + &_registrationHostClass)); } - void WindowManager::SignalClose() + void WindowManager::_raiseFindTargetWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args) { + _FindTargetWindowRequestedHandlers(sender, args); + } + void WindowManager::_raiseRequestNewWindow(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args) + { + _RequestNewWindowHandlers(sender, args); + } + + Remoting::ProposeCommandlineResult WindowManager::ProposeCommandline(const Remoting::CommandlineArgs& args, const bool isolatedMode) + { + if (!isolatedMode) + { + // _createMonarch always attempts to connect an existing monarch. In + // isolated mode, we don't want to do that. + _createMonarch(); + } + if (_monarch) { - try + // We connected to a monarch instance, not us though. This won't hit + // in isolated mode. + + // Send the commandline over to the monarch process + if (_proposeToMonarch(args)) { - _monarch.SignalClose(_peasant.GetID()); + // If that succeeded, then we don't need to make a new window. + // Our job is done. Either the monarch is going to run the + // commandline in an existing window, or a new one, but either way, + // this process doesn't need to make a new window. + + return winrt::make(false); + } + // Otherwise, we'll try to handle this ourselves. + } + + // Theoretically, this condition is always true here: + // + // if (_monarch == nullptr) + // + // If we do still have a _monarch at this point, then we must have + // successfully proposed to it in _proposeToMonarch, so we can't get + // here with a monarch. + { + // No preexisting instance. + + // Raise an event, to ask how to handle this commandline. We can't ask + // the app ourselves - we exist isolated from that knowledge (and + // dependency hell). The WindowManager will raise this up to the app + // host, which will then ask the AppLogic, who will then parse the + // commandline and determine the provided ID of the window. + auto findWindowArgs{ winrt::make_self(args) }; + + // This is handled by some handler in-proc + _FindTargetWindowRequestedHandlers(*this, *findWindowArgs); + + // After the event was handled, ResultTargetWindow() will be filled with + // the parsed result. + const auto targetWindow = findWindowArgs->ResultTargetWindow(); + const auto targetWindowName = findWindowArgs->ResultTargetWindowName(); + + if (targetWindow == WindowingBehaviorUseNone) + { + // This commandline doesn't deserve a window. Don't make a monarch + // either. + return winrt::make(false); + } + else + { + // This commandline _does_ want a window, which means we do want + // to create a window, and a monarch. + // + // Congrats! This is now THE PROCESS. It's the only one that's + // getting any windows. + + // In isolated mode, we don't want to register as the monarch, + // we just want to make a local one. So we'll skip this step. + // The condition below it will handle making the unregistered + // local monarch. + + if (!isolatedMode) + { + _registerAsMonarch(); + _createMonarch(); + } + else + { + TraceLoggingWrite(g_hRemotingProvider, + "WindowManager_IntentionallyIsolated", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + + if (!_monarch) + { + // Something catastrophically bad happened here OR we were + // intentionally in isolated mode. We don't want to just + // exit immediately. Instead, we'll just instantiate a local + // Monarch instance, without registering it. We're firmly in + // the realm of undefined behavior, but better to have some + // window than not. + _monarch = winrt::make(); + TraceLoggingWrite(g_hRemotingProvider, + "WindowManager_FailedToCoCreate", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + _createCallbacks(); + + // So, we wanted a new peasant. Cool! + // + // We need to fill in args.ResultTargetWindow, + // args.ResultTargetWindowName so that we can create the new + // window with those values. Otherwise, the very first window + // won't obey the given name / ID. + // + // So let's just ask the monarch (ourselves) to get those values. + return _monarch.ProposeCommandline(args); } - CATCH_LOG() } } - void WindowManager::_proposeToMonarch(const Remoting::CommandlineArgs& args, - std::optional& givenID, - winrt::hstring& givenName) + // Method Description: + // - Helper attempting to call to the monarch multiple times. If the monarch + // fails to respond, or we encounter any sort of error, we'll try again + // until we find one, or decisively determine there isn't one. + bool WindowManager::_proposeToMonarch(const Remoting::CommandlineArgs& args) { // these two errors are Win32 errors, convert them to HRESULTS so we can actually compare below. static constexpr auto RPC_SERVER_UNAVAILABLE_HR = HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE); @@ -114,10 +238,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // dies between now and the inspection of // `result.ShouldCreateWindow` below, we don't want to explode // (since _proposeToMonarch is not try/caught). - auto outOfProcResult = _monarch.ProposeCommandline(args); - result = winrt::make(outOfProcResult); - proposedCommandline = true; + _monarch.ProposeCommandline(args); + return true; } catch (...) { @@ -154,560 +277,75 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - _monarch = winrt::make(); - _createCallbacks(); + // Set the monarch to null, so that we'll create a new one + // (or just generally check if we need to even make a window + // for this commandline.) + _monarch = nullptr; + return false; } else { // We failed to ask the monarch. It must have died. Try and - // find the real monarch. Don't perform an election, that - // assumes we have a peasant, which we don't yet. - _createMonarchAndCallbacks(); - // _createMonarchAndCallbacks will initialize _isKing - } - if (_isKing) - { - // We became the king. We don't need to ProposeCommandline to ourself, we're just - // going to do it. - // - // Return early, because there's nothing else for us to do here. + // find another monarch. + _createMonarch(); + if (!_monarch) + { + // We failed to create a monarch. That means there + // aren't any other windows, and we can become the monarch. + return false; + } + // Go back around the loop. TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_proposeToMonarch_becameKing", + "WindowManager_proposeToMonarch_tryAgain", TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - // In WindowManager::ProposeCommandline, had we been the - // king originally, we would have started by setting - // this to true. We became the monarch here, so set it - // here as well. - _shouldCreateWindow = true; - return; } - - // Here, we created the new monarch, it wasn't us, so we're - // gonna go through the while loop again and ask the new - // king. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_proposeToMonarch_tryAgain", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } - - // Here, the monarch (not us) has replied to the message. Get the - // valuables out of the response: - _shouldCreateWindow = result.ShouldCreateWindow(); - if (result.Id()) - { - givenID = result.Id().Value(); - } - givenName = result.WindowName(); - - // TraceLogging doesn't have a good solution for logging an - // optional. So we have to repeat the calls here: - if (givenID) - { - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingUInt64(givenID.value(), "Id", "The ID we should assign our peasant"), - TraceLoggingWideString(givenName.c_str(), "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - else - { - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingPointer(nullptr, "Id", "No ID provided"), - TraceLoggingWideString(givenName.c_str(), "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } - void WindowManager::ProposeCommandline(const Remoting::CommandlineArgs& args) - { - // If we're the king, we _definitely_ want to process the arguments, we were - // launched with them! - // - // Otherwise, the King will tell us if we should make a new window - _shouldCreateWindow = _isKing; - std::optional givenID; - winrt::hstring givenName{}; - if (!_isKing) - { - _proposeToMonarch(args, givenID, givenName); - } - - // During _proposeToMonarch, it's possible that we found that the king was dead, and we're the new king. Cool! Do this now. - if (_isKing) - { - // We're the monarch, we don't need to propose anything. We're just - // going to do it. - // - // However, we _do_ need to ask what our name should be. It's - // possible someone started the _first_ wt with something like `wt - // -w king` as the commandline - we want to make sure we set our - // name to "king". - // - // The FindTargetWindow event is the WindowManager's way of saying - // "I do not know how to figure out how to turn this list of args - // into a window ID/name. Whoever's listening to this event does, so - // I'll ask them". It's a convoluted way of hooking the - // WindowManager up to AppLogic without actually telling it anything - // about TerminalApp (or even WindowsTerminal) - auto findWindowArgs{ winrt::make_self(args) }; - _raiseFindTargetWindowRequested(nullptr, *findWindowArgs); - - const auto responseId = findWindowArgs->ResultTargetWindow(); - if (responseId > 0) - { - givenID = ::base::saturated_cast(responseId); - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline_AsMonarch", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingUInt64(givenID.value(), "Id", "The ID we should assign our peasant"), - TraceLoggingWideString(givenName.c_str(), "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - else if (responseId == WindowingBehaviorUseName) - { - givenName = findWindowArgs->ResultTargetWindowName(); - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline_AsMonarch", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingUInt64(0, "Id", "The ID we should assign our peasant"), - TraceLoggingWideString(givenName.c_str(), "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - else - { - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline_AsMonarch", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingUInt64(0, "Id", "The ID we should assign our peasant"), - TraceLoggingWideString(L"", "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } - - if (_shouldCreateWindow) - { - // If we should create a new window, then instantiate our Peasant - // instance, and tell that peasant to handle that commandline. - _createOurPeasant({ givenID }, givenName); - - // Spawn a thread to wait on the monarch, and handle the election - if (!_isKing) - { - _createPeasantThread(); - } - - // This right here will just tell us to stash the args away for the - // future. The AppHost hasn't yet set up the callbacks, and the rest - // of the app hasn't started at all. We'll note them and come back - // later. - _peasant.ExecuteCommandline(args); - } - // Otherwise, we'll do _nothing_. - } - - bool WindowManager::ShouldCreateWindow() - { - return _shouldCreateWindow; - } - - void WindowManager::_registerAsMonarch() - { - winrt::check_hresult(CoRegisterClassObject(Monarch_clsid, - winrt::make<::MonarchFactory>().get(), - CLSCTX_LOCAL_SERVER, - REGCLS_MULTIPLEUSE, - &_registrationHostClass)); - } - - void WindowManager::_createMonarch() - { - // Heads up! This only works because we're using - // "metadata-based-marshalling" for our WinRT types. That means the OS is - // using the .winmd file we generate to figure out the proxy/stub - // definitions for our types automatically. This only works in the following - // cases: - // - // * If we're running unpackaged: the .winmd must be a sibling of the .exe - // * If we're running packaged: the .winmd must be in the package root - _monarch = create_instance(Monarch_clsid, - CLSCTX_LOCAL_SERVER); - } - - // Tries to instantiate a monarch, tries again, and eventually either throws - // (so that the caller will try again) or falls back to the isolated - // monarch. - void WindowManager::_redundantCreateMonarch() - { - _createMonarch(); - - if (_monarch == nullptr) - { - // See MSFT:38540483, GH#12774 for details. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_NullMonarchTryAgain", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - // Here we're gonna just give it a quick second try.Probably not - // definitive, but might help. - _createMonarch(); - } - - if (_monarch == nullptr) - { - // See MSFT:38540483, GH#12774 for details. - if constexpr (Feature_IsolatedMonarchMode::IsEnabled()) - { - // Fall back to having a in proc monarch. Were now isolated from - // other windows. This is a pretty torn state, but at least we - // didn't just explode. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_NullMonarchIsolateMode", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - _monarch = winrt::make(); - } - else - { - // The monarch is null. We're hoping that we can find another, - // hopefully us. We're gonna go back around the loop again and - // see what happens. If this is really an infinite loop (where - // the OS won't even give us back US as the monarch), then I - // suppose we'll find out soon enough. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_NullMonarchTryAgain", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - winrt::hresult_error(E_UNEXPECTED, L"Did not expect the Monarch to ever be null"); - } - } - } - - // NOTE: This can throw! Callers include: - // - the constructor, who performs this in a loop until it successfully - // find a a monarch - // - the performElection method, which is called in the waitOnMonarch - // thread. All the calls in that thread are wrapped in try/catch's - // already. - // - _createOurPeasant, who might do this in a loop to establish us with the - // monarch. - void WindowManager::_createMonarchAndCallbacks() - { - _redundantCreateMonarch(); - // We're pretty confident that we have a Monarch here. - _createCallbacks(); - } - - // Check if we became the king, and if we are, wire up callbacks. - void WindowManager::_createCallbacks() - { - // Save the result of checking if we're the king. We want to avoid - // unnecessary calls back and forth if we can. - _isKing = _areWeTheKing(); - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ConnectedToMonarch", - TraceLoggingUInt64(_monarch.GetPID(), "monarchPID", "The PID of the new Monarch"), - TraceLoggingBoolean(_isKing, "isKing", "true if we are the new monarch"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - if (_peasant) - { - if (const auto& lastActivated{ _peasant.GetLastActivatedArgs() }) - { - // Inform the monarch of the time we were last activated - _monarch.HandleActivatePeasant(lastActivated); } } - if (!_isKing) - { - return; - } - // Here, we're the king! - // - // This is where you should do any additional setup that might need to be - // done when we become the king. This will be called both for the first - // window, and when the current monarch dies. - - _monarch.WindowCreated({ get_weak(), &WindowManager::_WindowCreatedHandlers }); - _monarch.WindowClosed({ get_weak(), &WindowManager::_WindowClosedHandlers }); - _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); - _monarch.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequestedHandlers(*this, nullptr); }); - _monarch.HideNotificationIconRequested([this](auto&&, auto&&) { _HideNotificationIconRequestedHandlers(*this, nullptr); }); - _monarch.QuitAllRequested({ get_weak(), &WindowManager::_QuitAllRequestedHandlers }); - - _BecameMonarchHandlers(*this, nullptr); - } - - bool WindowManager::_areWeTheKing() - { - const auto ourPID{ GetCurrentProcessId() }; - const auto kingPID{ _monarch.GetPID() }; - return (ourPID == kingPID); + // I don't think we can ever get here, but the compiler doesn't know + return false; } - Remoting::IPeasant WindowManager::_createOurPeasant(std::optional givenID, - const winrt::hstring& givenName) + Remoting::Peasant WindowManager::CreatePeasant(const Remoting::WindowRequestedArgs& args) { auto p = winrt::make_self(); - if (givenID) + // This will be false if the Id is 0, which is our sentinel for "no specific ID was requested" + if (const auto id = args.Id()) { - p->AssignID(givenID.value()); + p->AssignID(id); } // If the name wasn't specified, this will be an empty string. - p->WindowName(givenName); - _peasant = *p; + p->WindowName(args.WindowName()); - // Try to add us to the monarch. If that fails, try to find a monarch - // again, until we find one (we will eventually find us) - while (true) - { - try - { - _monarch.AddPeasant(_peasant); - break; - } - catch (...) - { - try - { - // Wrap this in its own try/catch, because this can throw. - _createMonarchAndCallbacks(); - } - catch (...) - { - } - } - } + p->ExecuteCommandline(*winrt::make_self(args.Commandline(), args.CurrentDirectory())); - _peasant.GetWindowLayoutRequested({ get_weak(), &WindowManager::_GetWindowLayoutRequestedHandlers }); + _monarch.AddPeasant(*p); + + p->GetWindowLayoutRequested({ get_weak(), &WindowManager::_GetWindowLayoutRequestedHandlers }); TraceLoggingWrite(g_hRemotingProvider, "WindowManager_CreateOurPeasant", - TraceLoggingUInt64(_peasant.GetID(), "peasantID", "The ID of our new peasant"), + TraceLoggingUInt64(p->GetID(), "peasantID", "The ID of our new peasant"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - // If the peasant asks us to quit we should not try to act in future elections. - _peasant.QuitRequested([weakThis{ get_weak() }](auto&&, auto&&) { - if (auto wm = weakThis.get()) - { - wm->_monarchWaitInterrupt.SetEvent(); - } - }); - - return _peasant; + return *p; } - // Method Description: - // - Attempt to connect to the monarch process. This might be us! - // - For the new monarch, add us to their list of peasants. - // Arguments: - // - - // Return Value: - // - true iff we're the new monarch process. - // NOTE: This can throw! - bool WindowManager::_performElection() - { - _createMonarchAndCallbacks(); - - // Tell the new monarch who we are. We might be that monarch! - _monarch.AddPeasant(_peasant); - - // This method is only called when a _new_ monarch is elected. So - // don't do anything here that needs to be done for all monarch - // windows. This should only be for work that's done when a window - // _becomes_ a monarch, after the death of the previous monarch. - return _isKing; - } - - void WindowManager::_createPeasantThread() - { - // If we catch an exception trying to get at the monarch ever, we can - // set the _monarchWaitInterrupt, and use that to trigger a new - // election. Though, we wouldn't be able to retry the function that - // caused the exception in the first place... - - _electionThread = std::thread([this] { - _waitOnMonarchThread(); - }); - } - - void WindowManager::_waitOnMonarchThread() + void WindowManager::SignalClose(const Remoting::Peasant& peasant) { - // This is the array of HANDLEs that we're going to wait on in - // WaitForMultipleObjects below. - // * waits[0] will be the handle to the monarch process. It gets - // signalled when the process exits / dies. - // * waits[1] is the handle to our _monarchWaitInterrupt event. Another - // thread can use that to manually break this loop. We'll do that when - // we're getting torn down. - HANDLE waits[2]; - waits[1] = _monarchWaitInterrupt.get(); - const auto peasantID = _peasant.GetID(); // safe: _peasant is in-proc. - - auto exitThreadRequested = false; - while (!exitThreadRequested) + if (_monarch) { - // At any point in all this, the current monarch might die. If it - // does, we'll go straight to a new election, in the "jail" - // try/catch below. Worst case, eventually, we'll become the new - // monarch. try { - // This might fail to even ask the monarch for its PID. - wil::unique_handle hMonarch{ OpenProcess(PROCESS_ALL_ACCESS, - FALSE, - static_cast(_monarch.GetPID())) }; - - // If we fail to open the monarch, then they don't exist - // anymore! Go straight to an election. - if (hMonarch.get() == nullptr) - { - const auto gle = GetLastError(); - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_FailedToOpenMonarch", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingUInt64(gle, "lastError", "The result of GetLastError"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - exitThreadRequested = _performElection(); - continue; - } - - waits[0] = hMonarch.get(); - auto waitResult = WaitForMultipleObjects(2, waits, FALSE, INFINITE); - - switch (waitResult) - { - case WAIT_OBJECT_0 + 0: // waits[0] was signaled, the handle to the monarch process - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_MonarchDied", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - // Connect to the new monarch, which might be us! - // If we become the monarch, then we'll return true and exit this thread. - exitThreadRequested = _performElection(); - break; - - case WAIT_OBJECT_0 + 1: // waits[1] was signaled, our manual interrupt - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_MonarchWaitInterrupted", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - exitThreadRequested = true; - break; - - case WAIT_TIMEOUT: - // This should be impossible. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_MonarchWaitTimeout", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - exitThreadRequested = true; - break; - - default: - { - // Returning any other value is invalid. Just die. - const auto gle = GetLastError(); - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_WaitFailed", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingUInt64(gle, "lastError", "The result of GetLastError"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - ExitProcess(0); - } - } - } - catch (...) - { - // Theoretically, if window[1] dies when we're trying to get - // its PID we'll get here. If we just try to do the election - // once here, it's possible we might elect window[2], but have - // it die before we add ourselves as a peasant. That - // _performElection call will throw, and we wouldn't catch it - // here, and we'd die. - - // Instead, we're going to have a resilient election process. - // We're going to keep trying an election, until one _doesn't_ - // throw an exception. That might mean burning through all the - // other dying monarchs until we find us as the monarch. But if - // this process is alive, then there's _someone_ in the line of - // succession. - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ExceptionInWaitThread", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - auto foundNewMonarch = false; - while (!foundNewMonarch) - { - try - { - exitThreadRequested = _performElection(); - // It doesn't matter if we're the monarch, or someone - // else is, but if we complete the election, then we've - // registered with a new one. We can escape this jail - // and re-enter society. - foundNewMonarch = true; - } - catch (...) - { - // If we fail to acknowledge the results of the election, - // stay in this jail until we do. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ExceptionInNestedWaitThread", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } + _monarch.SignalClose(peasant.GetID()); } + CATCH_LOG() } } - Remoting::Peasant WindowManager::CurrentWindow() - { - return _peasant; - } - - void WindowManager::_raiseFindTargetWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args) - { - _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 @@ -741,42 +379,16 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation return 0; } - // Method Description: - // - Ask the monarch to show a notification icon. - // Arguments: - // - - // Return Value: - // - - winrt::fire_and_forget WindowManager::RequestShowNotificationIcon() - { - co_await winrt::resume_background(); - _peasant.RequestShowNotificationIcon(); - } - - // Method Description: - // - Ask the monarch to hide its notification icon. - // Arguments: - // - - // Return Value: - // - - winrt::fire_and_forget WindowManager::RequestHideNotificationIcon() - { - auto strongThis{ get_strong() }; - co_await winrt::resume_background(); - _peasant.RequestHideNotificationIcon(); - } - // Method Description: // - Ask the monarch to quit all windows. // Arguments: // - // Return Value: // - - winrt::fire_and_forget WindowManager::RequestQuitAll() + winrt::fire_and_forget WindowManager::RequestQuitAll(Remoting::Peasant peasant) { - auto strongThis{ get_strong() }; co_await winrt::resume_background(); - _peasant.RequestQuitAll(); + peasant.RequestQuitAll(); } bool WindowManager::DoesQuakeWindowExist() @@ -784,9 +396,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation return _monarch.DoesQuakeWindowExist(); } - void WindowManager::UpdateActiveTabTitle(winrt::hstring title) + void WindowManager::UpdateActiveTabTitle(const winrt::hstring& title, const Remoting::Peasant& peasant) { - winrt::get_self(_peasant)->ActiveTabTitle(title); + winrt::get_self(peasant)->ActiveTabTitle(title); } Windows::Foundation::Collections::IVector WindowManager::GetAllWindowLayouts() diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 96c60710d79..bed5fc3ca1b 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -1,10 +1,8 @@ /*++ Copyright (c) Microsoft Corporation Licensed under the MIT license. - Class Name: - WindowManager.h - Abstract: - The Window Manager takes care of coordinating the monarch and peasant for this process. @@ -16,9 +14,7 @@ Class Name: - When the monarch needs to ask the TerminalApp about how to parse a commandline, it'll ask by raising an event that we'll bubble up to the AppHost. - --*/ - #pragma once #include "WindowManager.g.h" @@ -29,65 +25,46 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { struct WindowManager : public WindowManagerT { + public: WindowManager(); ~WindowManager(); + winrt::Microsoft::Terminal::Remoting::ProposeCommandlineResult ProposeCommandline(const winrt::Microsoft::Terminal::Remoting::CommandlineArgs& args, const bool isolatedMode); + Remoting::Peasant CreatePeasant(const Remoting::WindowRequestedArgs& args); - void ProposeCommandline(const winrt::Microsoft::Terminal::Remoting::CommandlineArgs& args); - bool ShouldCreateWindow(); - - winrt::Microsoft::Terminal::Remoting::Peasant CurrentWindow(); - bool IsMonarch(); + void SignalClose(const Remoting::Peasant& peasant); void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); - void SignalClose(); - void SummonAllWindows(); - uint64_t GetNumberOfPeasants(); Windows::Foundation::Collections::IVectorView GetPeasantInfos(); + uint64_t GetNumberOfPeasants(); - winrt::fire_and_forget RequestShowNotificationIcon(); - winrt::fire_and_forget RequestHideNotificationIcon(); - winrt::fire_and_forget RequestQuitAll(); - bool DoesQuakeWindowExist(); - void UpdateActiveTabTitle(winrt::hstring title); + static winrt::fire_and_forget RequestQuitAll(Remoting::Peasant peasant); + void UpdateActiveTabTitle(const winrt::hstring& title, const Remoting::Peasant& peasant); Windows::Foundation::Collections::IVector GetAllWindowLayouts(); + bool DoesQuakeWindowExist(); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); - TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); + TYPED_EVENT(RequestNewWindow, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs); + private: - bool _shouldCreateWindow{ false }; - bool _isKing{ false }; DWORD _registrationHostClass{ 0 }; winrt::Microsoft::Terminal::Remoting::IMonarch _monarch{ nullptr }; - winrt::Microsoft::Terminal::Remoting::Peasant _peasant{ nullptr }; - - wil::unique_event _monarchWaitInterrupt; - std::thread _electionThread; - void _registerAsMonarch(); void _createMonarch(); - void _redundantCreateMonarch(); - void _createMonarchAndCallbacks(); - void _createCallbacks(); - bool _areWeTheKing(); - winrt::Microsoft::Terminal::Remoting::IPeasant _createOurPeasant(std::optional givenID, - const winrt::hstring& givenName); + void _registerAsMonarch(); + + bool _proposeToMonarch(const Remoting::CommandlineArgs& args); - bool _performElection(); - void _createPeasantThread(); - void _waitOnMonarchThread(); + void _createCallbacks(); void _raiseFindTargetWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args); - - void _proposeToMonarch(const Remoting::CommandlineArgs& args, - std::optional& givenID, - winrt::hstring& givenName); + void _raiseRequestNewWindow(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); }; } diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index 2fdfd7e3432..963c19b6645 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -7,29 +7,33 @@ namespace Microsoft.Terminal.Remoting [default_interface] runtimeclass WindowManager { WindowManager(); - void ProposeCommandline(CommandlineArgs args); - void SignalClose(); - Boolean ShouldCreateWindow { get; }; - IPeasant CurrentWindow(); - Boolean IsMonarch { get; }; + + ProposeCommandlineResult ProposeCommandline(CommandlineArgs args, Boolean isolatedMode); + Peasant CreatePeasant(WindowRequestedArgs args); + + void SignalClose(Peasant p); + + void UpdateActiveTabTitle(String title, Peasant p); + static void RequestQuitAll(Peasant p); + void SummonWindow(SummonWindowSelectionArgs args); void SummonAllWindows(); - void RequestShowNotificationIcon(); - void RequestHideNotificationIcon(); + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); + Windows.Foundation.Collections.IVectorView GetPeasantInfos(); UInt64 GetNumberOfPeasants(); - void RequestQuitAll(); - void UpdateActiveTabTitle(String title); + Boolean DoesQuakeWindowExist(); - Windows.Foundation.Collections.IVectorView GetPeasantInfos(); + event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; - event Windows.Foundation.TypedEventHandler BecameMonarch; + event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; event Windows.Foundation.TypedEventHandler QuitAllRequested; event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; - event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; - event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; + + event Windows.Foundation.TypedEventHandler RequestNewWindow; + }; } diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index c8677eb7f82..1756ba03d60 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -6,6 +6,7 @@ #include "../inc/WindowingBehavior.h" #include "AppLogic.g.cpp" #include "FindTargetWindowResult.g.cpp" +#include "SettingsLoadEventArgs.h" #include #include @@ -585,7 +586,7 @@ namespace winrt::TerminalApp::implementation { if (!appArgs.GetExitMessage().empty()) { - return winrt::make(WindowingBehaviorUseNew); + return winrt::make(WindowingBehaviorUseNone); } const std::string parsedTarget{ appArgs.GetTargetWindow() }; @@ -653,18 +654,14 @@ namespace winrt::TerminalApp::implementation } } - // Any unsuccessful parse will be a new window. That new window will try - // to handle the commandline itself, and find that the commandline - // failed to parse. When that happens, the new window will display the - // message box. + // Any unsuccessful parse will result in _no_ window. We will indicate + // to the caller that they shouldn't make a window. They can still find + // the commandline failed to parse and choose to display the message + // box. // // This will also work for the case where the user specifies an invalid - // commandline in conjunction with `-w 0`. This function will determine - // that the commandline has a parse error, and indicate that we should - // 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 winrt::make(WindowingBehaviorUseNew); + // commandline in conjunction with `-w 0`. + return winrt::make(WindowingBehaviorUseNone); } Windows::Foundation::Collections::IMapView AppLogic::GlobalHotkeys() @@ -677,6 +674,26 @@ namespace winrt::TerminalApp::implementation return _settings.GlobalSettings().CurrentTheme(); } + bool AppLogic::IsolatedMode() + { + if (!_loadedInitialSettings) + { + ReloadSettings(); + } + return _settings.GlobalSettings().IsolatedMode(); + } + bool AppLogic::RequestsTrayIcon() + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + ReloadSettings(); + } + const auto& globals{ _settings.GlobalSettings() }; + return globals.AlwaysShowNotificationIcon() || + globals.MinimizeToNotificationArea(); + } + TerminalApp::TerminalWindow AppLogic::CreateNewWindow() { if (_settings == nullptr) @@ -725,4 +742,12 @@ namespace winrt::TerminalApp::implementation ApplicationState::SharedInstance().PersistedWindowLayouts(winrt::single_threaded_vector(std::move(converted))); } + + TerminalApp::ParseCommandlineResult AppLogic::GetParseCommandlineMessage(array_view args) + { + ::TerminalApp::AppCommandlineArgs _appArgs; + const auto r = _appArgs.ParseArgs(args); + return TerminalApp::ParseCommandlineResult{ winrt::to_hstring(_appArgs.GetExitMessage()), r }; + } + } diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 821ec0f5f10..165f0935cbf 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -64,9 +64,13 @@ namespace winrt::TerminalApp::implementation Windows::Foundation::Collections::IMapView GlobalHotkeys(); Microsoft::Terminal::Settings::Model::Theme Theme(); + bool IsolatedMode(); + bool RequestsTrayIcon(); TerminalApp::TerminalWindow CreateNewWindow(); + TerminalApp::ParseCommandlineResult GetParseCommandlineMessage(array_view args); + TYPED_EVENT(SettingsChanged, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::SettingsLoadEventArgs); private: diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index a5db001de67..c48517dcc9e 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -10,6 +10,12 @@ namespace TerminalApp String WindowName { get; }; }; + struct ParseCommandlineResult + { + String Message; + Int32 ExitCode; + }; + // See IDialogPresenter and TerminalPage's DialogPresenter for more // information. [default_interface] runtimeclass AppLogic @@ -34,13 +40,18 @@ namespace TerminalApp void ReloadSettings(); + // Selected settings to expose Microsoft.Terminal.Settings.Model.Theme Theme { get; }; + Boolean IsolatedMode { get; }; + Boolean RequestsTrayIcon { get; }; FindTargetWindowResult FindTargetWindow(String[] args); TerminalWindow CreateNewWindow(); - Windows.Foundation.Collections.IMapView GlobalHotkeys(); + ParseCommandlineResult GetParseCommandlineMessage(String[] args); + + IMapView GlobalHotkeys(); event Windows.Foundation.TypedEventHandler SettingsChanged; diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index ef4d9ab2911..74f04015c6b 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -33,9 +33,6 @@ static const int CombinedPaneBorderSize = 2 * PaneBorderSize; static const int AnimationDurationInMilliseconds = 200; static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(AnimationDurationInMilliseconds))); -winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_focusedBorderBrush = { nullptr }; -winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_unfocusedBorderBrush = { nullptr }; - Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFocused) : _control{ control }, _lastActive{ lastFocused }, @@ -83,7 +80,7 @@ Pane::Pane(std::shared_ptr first, // Use the unfocused border color as the pane background, so an actual color // appears behind panes as we animate them sliding in. - _root.Background(s_unfocusedBorderBrush); + _root.Background(_themeResources.unfocusedBorderBrush); _root.Children().Append(_borderFirst); _root.Children().Append(_borderSecond); @@ -1396,8 +1393,8 @@ void Pane::UpdateVisuals() { _UpdateBorders(); } - _borderFirst.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush); - _borderSecond.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush); + _borderFirst.BorderBrush(_lastActive ? _themeResources.focusedBorderBrush : _themeResources.unfocusedBorderBrush); + _borderSecond.BorderBrush(_lastActive ? _themeResources.focusedBorderBrush : _themeResources.unfocusedBorderBrush); } // Method Description: @@ -1849,7 +1846,7 @@ winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst) Controls::Grid dummyGrid; // GH#603 - we can safely add a BG here, as the control is gone right // away, to fill the space as the rest of the pane expands. - dummyGrid.Background(s_unfocusedBorderBrush); + dummyGrid.Background(_themeResources.unfocusedBorderBrush); // It should be the size of the closed pane. dummyGrid.Width(removedOriginalSize.Width); dummyGrid.Height(removedOriginalSize.Height); @@ -2127,7 +2124,7 @@ void Pane::_SetupEntranceAnimation() // * If we give the parent (us) root BG a color, then a transparent pane // will flash opaque during the animation, then back to transparent, which // looks bad. - _secondChild->_root.Background(s_unfocusedBorderBrush); + _secondChild->_root.Background(_themeResources.unfocusedBorderBrush); const auto [firstSize, secondSize] = _CalcChildrenSizes(::base::saturated_cast(totalSize)); @@ -3092,51 +3089,20 @@ float Pane::_ClampSplitPosition(const bool widthOrHeight, const float requestedV return std::clamp(requestedValue, minSplitPosition, maxSplitPosition); } -// Function Description: -// - Attempts to load some XAML resources that the Pane will need. This includes: -// * The Color we'll use for active Panes's borders - SystemAccentColor -// * The Brush we'll use for inactive Panes - TabViewBackground (to match the -// color of the titlebar) -// Arguments: -// - requestedTheme: this should be the currently active Theme for the app -// Return Value: -// - -void Pane::SetupResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) +// Method Description: +// - Update our stored brushes for the current theme. This will also recursively +// update all our children. +// - TerminalPage creates these brushes and ultimately owns them. Effectively, +// we're just storing a smart pointer to the page's brushes. +void Pane::UpdateResources(const PaneResources& resources) { - const auto res = Application::Current().Resources(); - const auto accentColorKey = winrt::box_value(L"SystemAccentColor"); - if (res.HasKey(accentColorKey)) - { - const auto colorFromResources = ThemeLookup(res, requestedTheme, accentColorKey); - // If SystemAccentColor is _not_ a Color for some reason, use - // Transparent as the color, so we don't do this process again on - // the next pane (by leaving s_focusedBorderBrush nullptr) - auto actualColor = winrt::unbox_value_or(colorFromResources, Colors::Black()); - s_focusedBorderBrush = SolidColorBrush(actualColor); - } - else - { - // DON'T use Transparent here - if it's "Transparent", then it won't - // be able to hittest for clicks, and then clicking on the border - // will eat focus. - s_focusedBorderBrush = SolidColorBrush{ Colors::Black() }; - } + _themeResources = resources; + UpdateVisuals(); - const auto unfocusedBorderBrushKey = winrt::box_value(L"UnfocusedBorderBrush"); - if (res.HasKey(unfocusedBorderBrushKey)) - { - // MAKE SURE TO USE ThemeLookup, so that we get the correct resource for - // the requestedTheme, not just the value from the resources (which - // might not respect the settings' requested theme) - auto obj = ThemeLookup(res, requestedTheme, unfocusedBorderBrushKey); - s_unfocusedBorderBrush = obj.try_as(); - } - else + if (!_IsLeaf()) { - // DON'T use Transparent here - if it's "Transparent", then it won't - // be able to hittest for clicks, and then clicking on the border - // will eat focus. - s_unfocusedBorderBrush = SolidColorBrush{ Colors::Black() }; + _firstChild->UpdateResources(resources); + _secondChild->UpdateResources(resources); } } diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 4719d1c4b47..c1a947d4218 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -51,6 +51,12 @@ enum class SplitState : int Vertical = 2 }; +struct PaneResources +{ + winrt::Windows::UI::Xaml::Media::SolidColorBrush focusedBorderBrush{ nullptr }; + winrt::Windows::UI::Xaml::Media::SolidColorBrush unfocusedBorderBrush{ nullptr }; +}; + class Pane : public std::enable_shared_from_this { public: @@ -136,7 +142,7 @@ class Pane : public std::enable_shared_from_this bool ContainsReadOnly() const; - static void SetupResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); + void UpdateResources(const PaneResources& resources); // Method Description: // - A helper method for ad-hoc recursion on a pane tree. Walks the pane @@ -217,8 +223,8 @@ class Pane : public std::enable_shared_from_this winrt::Windows::UI::Xaml::Controls::Grid _root{}; winrt::Windows::UI::Xaml::Controls::Border _borderFirst{}; winrt::Windows::UI::Xaml::Controls::Border _borderSecond{}; - static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_focusedBorderBrush; - static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_unfocusedBorderBrush; + + PaneResources _themeResources; #pragma region Properties that need to be transferred between child / parent panes upon splitting / closing std::shared_ptr _firstChild{ nullptr }; diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 96e7f240873..4d7571363bb 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -31,10 +31,12 @@ using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::System; using namespace winrt::Windows::System; +using namespace winrt::Windows::UI; using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::UI::Text; using namespace winrt::Windows::UI::Xaml::Controls; using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Media; using namespace ::TerminalApp; using namespace ::Microsoft::Console; using namespace ::Microsoft::Terminal::Core; @@ -101,38 +103,11 @@ namespace winrt::TerminalApp::implementation return S_OK; } - // Function Description: - // - Recursively check our commands to see if there's a keybinding for - // exactly their action. If there is, label that command with the text - // corresponding to that key chord. - // - Will recurse into nested commands as well. - // Arguments: - // - settings: The settings who's keybindings we should use to look up the key chords from - // - commands: The list of commands to label. - static void _recursiveUpdateCommandKeybindingLabels(CascadiaSettings settings, - IMapView commands) - { - for (const auto& nameAndCmd : commands) - { - const auto& command = nameAndCmd.Value(); - if (command.HasNestedCommands()) - { - _recursiveUpdateCommandKeybindingLabels(settings, command.NestedCommands()); - } - else - { - // If there's a keybinding that's bound to exactly this command, - // then get the keychord and display it as a - // part of the command in the UI. - // We specifically need to do this for nested commands. - const auto keyChord{ settings.ActionMap().GetKeyBindingForAction(command.ActionAndArgs().Action(), command.ActionAndArgs().Args()) }; - command.RegisterKey(keyChord); - } - } - } - + // INVARIANT: This needs to be called on OUR UI thread! void TerminalPage::SetSettings(CascadiaSettings settings, bool needRefreshUI) { + assert(Dispatcher().HasThreadAccess()); + _settings = settings; // Make sure to _UpdateCommandsForPalette before @@ -253,9 +228,6 @@ namespace winrt::TerminalApp::implementation // Hookup our event handlers to the ShortcutActionDispatch _RegisterActionCallbacks(); - // Hook up inbound connection event handler - ConptyConnection::NewConnection({ this, &TerminalPage::_OnNewConnection }); - //Event Bindings (Early) _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { if (auto page{ weakThis.get() }) @@ -544,6 +516,9 @@ namespace winrt::TerminalApp::implementation { _shouldStartInboundListener = false; + // Hook up inbound connection event handler + _newConnectionRevoker = ConptyConnection::NewConnection(winrt::auto_revoke, { this, &TerminalPage::_OnNewConnection }); + try { winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::StartInboundListener(); @@ -2946,50 +2921,6 @@ namespace winrt::TerminalApp::implementation } } - // This is a helper to aid in sorting commands by their `Name`s, alphabetically. - static bool _compareSchemeNames(const ColorScheme& lhs, const ColorScheme& rhs) - { - std::wstring leftName{ lhs.Name() }; - std::wstring rightName{ rhs.Name() }; - return leftName.compare(rightName) < 0; - } - - // Method Description: - // - Takes a mapping of names->commands and expands them - // Arguments: - // - - // Return Value: - // - - IMap TerminalPage::_ExpandCommands(IMapView commandsToExpand, - IVectorView profiles, - IMapView schemes) - { - auto warnings{ winrt::single_threaded_vector() }; - - std::vector sortedSchemes; - sortedSchemes.reserve(schemes.Size()); - - for (const auto& nameAndScheme : schemes) - { - sortedSchemes.push_back(nameAndScheme.Value()); - } - std::sort(sortedSchemes.begin(), - sortedSchemes.end(), - _compareSchemeNames); - - auto copyOfCommands = winrt::single_threaded_map(); - for (const auto& nameAndCommand : commandsToExpand) - { - copyOfCommands.Insert(nameAndCommand.Key(), nameAndCommand.Value()); - } - - Command::ExpandCommands(copyOfCommands, - profiles, - { sortedSchemes }, - warnings); - - return copyOfCommands; - } // Method Description: // - Repopulates the list of commands in the command palette with the // current commands in the settings. Also updates the keybinding labels to @@ -3000,20 +2931,9 @@ namespace winrt::TerminalApp::implementation // - void TerminalPage::_UpdateCommandsForPalette() { - auto copyOfCommands = _ExpandCommands(_settings.GlobalSettings().ActionMap().NameMap(), - _settings.ActiveProfiles().GetView(), - _settings.GlobalSettings().ColorSchemes()); - - _recursiveUpdateCommandKeybindingLabels(_settings, copyOfCommands.GetView()); - // Update the command palette when settings reload - auto commandsCollection = winrt::single_threaded_vector(); - for (const auto& nameAndCommand : copyOfCommands) - { - commandsCollection.Append(nameAndCommand.Value()); - } - - CommandPalette().SetCommands(commandsCollection); + const auto& expanded{ _settings.GlobalSettings().ActionMap().ExpandedCommands() }; + CommandPalette().SetCommands(expanded); } // Method Description: @@ -3424,6 +3344,8 @@ namespace winrt::TerminalApp::implementation HRESULT TerminalPage::_OnNewConnection(const ConptyConnection& connection) { + _newConnectionRevoker.revoke(); + // We need to be on the UI thread in order for _OpenNewTab to run successfully. // HasThreadAccess will return true if we're currently on a UI thread and false otherwise. // When we're on a COM thread, we'll need to dispatch the calls to the UI thread @@ -4198,17 +4120,14 @@ namespace winrt::TerminalApp::implementation auto requestedTheme{ theme.RequestedTheme() }; { - // Update the brushes that Pane's use... - Pane::SetupResources(requestedTheme); - // ... then trigger a visual update for all the pane borders to - // apply the new ones. + _updatePaneResources(requestedTheme); + for (const auto& tab : _tabs) { if (auto terminalTab{ _GetTerminalTabImpl(tab) }) { - terminalTab->GetRootPane()->WalkTree([&](auto&& pane) { - pane->UpdateVisuals(); - }); + // The root pane will propagate the theme change to all its children. + terminalTab->GetRootPane()->UpdateResources(_paneResources); } } } @@ -4315,6 +4234,54 @@ namespace winrt::TerminalApp::implementation } } + // Function Description: + // - Attempts to load some XAML resources that Panes will need. This includes: + // * The Color they'll use for active Panes's borders - SystemAccentColor + // * The Brush they'll use for inactive Panes - TabViewBackground (to match the + // color of the titlebar) + // Arguments: + // - requestedTheme: this should be the currently active Theme for the app + // Return Value: + // - + void TerminalPage::_updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) + { + const auto res = Application::Current().Resources(); + const auto accentColorKey = winrt::box_value(L"SystemAccentColor"); + if (res.HasKey(accentColorKey)) + { + const auto colorFromResources = ThemeLookup(res, requestedTheme, accentColorKey); + // If SystemAccentColor is _not_ a Color for some reason, use + // Transparent as the color, so we don't do this process again on + // the next pane (by leaving s_focusedBorderBrush nullptr) + auto actualColor = winrt::unbox_value_or(colorFromResources, Colors::Black()); + _paneResources.focusedBorderBrush = SolidColorBrush(actualColor); + } + else + { + // DON'T use Transparent here - if it's "Transparent", then it won't + // be able to hittest for clicks, and then clicking on the border + // will eat focus. + _paneResources.focusedBorderBrush = SolidColorBrush{ Colors::Black() }; + } + + const auto unfocusedBorderBrushKey = winrt::box_value(L"UnfocusedBorderBrush"); + if (res.HasKey(unfocusedBorderBrushKey)) + { + // MAKE SURE TO USE ThemeLookup, so that we get the correct resource for + // the requestedTheme, not just the value from the resources (which + // might not respect the settings' requested theme) + auto obj = ThemeLookup(res, requestedTheme, unfocusedBorderBrushKey); + _paneResources.unfocusedBorderBrush = obj.try_as(); + } + else + { + // DON'T use Transparent here - if it's "Transparent", then it won't + // be able to hittest for clicks, and then clicking on the border + // will eat focus. + _paneResources.unfocusedBorderBrush = SolidColorBrush{ Colors::Black() }; + } + } + void TerminalPage::WindowActivated(const bool activated) { // Stash if we're activated. Use that when we reload diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index f141d46025b..34382de8aed 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -224,6 +224,10 @@ namespace winrt::TerminalApp::implementation TerminalApp::WindowProperties _WindowProperties{ nullptr }; + PaneResources _paneResources; + + winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::NewConnection_revoker _newConnectionRevoker; + winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); void _ShowAboutDialog(); @@ -267,10 +271,6 @@ namespace winrt::TerminalApp::implementation void _UpdateCommandsForPalette(); void _SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance); - static winrt::Windows::Foundation::Collections::IMap _ExpandCommands(Windows::Foundation::Collections::IMapView commandsToExpand, - Windows::Foundation::Collections::IVectorView profiles, - Windows::Foundation::Collections::IMapView schemes); - void _DuplicateFocusedTab(); void _DuplicateTab(const TerminalTab& tab); @@ -453,6 +453,7 @@ namespace winrt::TerminalApp::implementation void _updateThemeColors(); void _updateTabCloseButton(const winrt::Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem); + void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); winrt::fire_and_forget _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); winrt::fire_and_forget _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index eb0ef7c78b9..7ab1837b194 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -123,11 +123,8 @@ namespace winrt::TerminalApp::implementation _initialLoadResult{ settingsLoadedResult }, _WindowProperties{ winrt::make_self() } { - // The TerminalPage has to be constructed during our construction, to - // make sure that there's a terminal page for callers of - // SetTitleBarContent - _root = winrt::make_self(*_WindowProperties); - _dialog = ContentDialog{}; + // The TerminalPage has to ABSOLUTELY NOT BE constructed during our + // construction. We can't do ANY xaml till Initialize() is called. // For your own sanity, it's better to do setup outside the ctor. // If you do any setup in the ctor that ends up throwing an exception, @@ -140,6 +137,10 @@ namespace winrt::TerminalApp::implementation // - Implements the IInitializeWithWindow interface from shobjidl_core. HRESULT TerminalWindow::Initialize(HWND hwnd) { + // Now that we know we can do XAML, build our page. + _root = winrt::make_self(*_WindowProperties); + _dialog = ContentDialog{}; + // Pass commandline args into the TerminalPage. If we were supposed to // load from a persisted layout, do that instead. @@ -251,8 +252,9 @@ namespace winrt::TerminalApp::implementation _root->SetStartupActions(_settingsStartupArgs); } - _root->SetSettings(_settings, false); - _root->Loaded({ this, &TerminalWindow::_OnLoaded }); + _root->SetSettings(_settings, false); // We're on our UI thread right now, so this is safe + _root->Loaded({ get_weak(), &TerminalWindow::_OnLoaded }); + _root->Initialized([this](auto&&, auto&&) { // GH#288 - When we finish initialization, if the user wanted us // launched _fullscreen_, toggle fullscreen mode. This will make sure @@ -744,32 +746,40 @@ namespace winrt::TerminalApp::implementation _RequestedThemeChangedHandlers(*this, Theme()); } + // This may be called on a background thread, or the main thread, but almost + // definitely not on OUR UI thread. winrt::fire_and_forget TerminalWindow::UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args) { _settings = args.NewSettings(); - // Update the settings in TerminalPage - _root->SetSettings(_settings, true); + const auto weakThis{ get_weak() }; co_await wil::resume_foreground(_root->Dispatcher()); + // Back on our UI thread... + if (auto logic{ weakThis.get() }) + { + // Update the settings in TerminalPage + // We're on our UI thread right now, so this is safe + _root->SetSettings(_settings, true); - // Bubble the notification up to the AppHost, now that we've updated our _settings. - _SettingsChangedHandlers(*this, args); + // Bubble the notification up to the AppHost, now that we've updated our _settings. + _SettingsChangedHandlers(*this, args); - if (FAILED(args.Result())) - { - const winrt::hstring titleKey = USES_RESOURCE(L"ReloadJsonParseErrorTitle"); - const winrt::hstring textKey = USES_RESOURCE(L"ReloadJsonParseErrorText"); - _ShowLoadErrorsDialog(titleKey, - textKey, - gsl::narrow_cast(args.Result()), - args.ExceptionText()); - co_return; - } - else if (args.Result() == S_FALSE) - { - _ShowLoadWarningsDialog(args.Warnings()); + if (FAILED(args.Result())) + { + const winrt::hstring titleKey = USES_RESOURCE(L"ReloadJsonParseErrorTitle"); + const winrt::hstring textKey = USES_RESOURCE(L"ReloadJsonParseErrorText"); + _ShowLoadErrorsDialog(titleKey, + textKey, + gsl::narrow_cast(args.Result()), + args.ExceptionText()); + co_return; + } + else if (args.Result() == S_FALSE) + { + _ShowLoadWarningsDialog(args.Warnings()); + } + _RefreshThemeRoutine(); } - _RefreshThemeRoutine(); } void TerminalWindow::_OpenSettingsUI() @@ -1136,6 +1146,22 @@ namespace winrt::TerminalApp::implementation return *_cachedLayout; } + void TerminalWindow::RequestExitFullscreen() + { + _root->SetFullscreen(false); + } + + bool TerminalWindow::AutoHideWindow() + { + return _settings.GlobalSettings().AutoHideWindow(); + } + + void TerminalWindow::UpdateSettingsHandler(const winrt::IInspectable& /*sender*/, + const winrt::TerminalApp::SettingsLoadEventArgs& args) + { + UpdateSettings(args); + } + void TerminalWindow::IdentifyWindow() { if (_root) @@ -1171,22 +1197,6 @@ namespace winrt::TerminalApp::implementation _WindowProperties->WindowId(id); } - void TerminalWindow::RequestExitFullscreen() - { - _root->SetFullscreen(false); - } - - bool TerminalWindow::AutoHideWindow() - { - return _settings.GlobalSettings().AutoHideWindow(); - } - - void TerminalWindow::UpdateSettingsHandler(const winrt::IInspectable& /*sender*/, - const winrt::TerminalApp::SettingsLoadEventArgs& arg) - { - UpdateSettings(arg); - } - bool TerminalWindow::ShouldImmediatelyHandoffToElevated() { return _root != nullptr ? _root->ShouldImmediatelyHandoffToElevated(_settings) : false; @@ -1238,12 +1248,7 @@ namespace winrt::TerminalApp::implementation void WindowProperties::WindowId(const uint64_t& value) { - if (_WindowId != value) - { - _WindowId = value; - _PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowId" }); - _PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowIdForDisplay" }); - } + _WindowId = value; } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index 1a446010761..757a966c4b6 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -9,6 +9,7 @@ #include "SettingsLoadEventArgs.h" #include "TerminalPage.h" +#include "SettingsLoadEventArgs.h" #include #include @@ -89,6 +90,7 @@ namespace winrt::TerminalApp::implementation bool AutoHideWindow(); hstring GetWindowLayoutJson(Microsoft::Terminal::Settings::Model::LaunchPosition position); + void IdentifyWindow(); void RenameFailed(); @@ -126,6 +128,7 @@ namespace winrt::TerminalApp::implementation bool GetMinimizeToNotificationArea(); bool GetAlwaysShowNotificationIcon(); + bool GetShowTitleInTitlebar(); winrt::Windows::Foundation::IAsyncOperation ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog); @@ -136,6 +139,7 @@ namespace winrt::TerminalApp::implementation void WindowName(const winrt::hstring& value); void WindowId(const uint64_t& value); + bool IsQuakeWindow() const noexcept { return _WindowProperties->IsQuakeWindow(); } TerminalApp::WindowProperties WindowProperties() { return *_WindowProperties; } diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index a3a5124a3d0..b2c78970d45 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -4,12 +4,14 @@ #include "pch.h" #include "AllShortcutActions.h" #include "ActionMap.h" +#include "Command.h" #include "AllShortcutActions.h" #include "ActionMap.g.cpp" using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::Terminal::Control; +using namespace winrt::Windows::Foundation::Collections; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { @@ -118,7 +120,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Method Description: // - Retrieves a map of actions that can be bound to a key - Windows::Foundation::Collections::IMapView ActionMap::AvailableActions() + IMapView ActionMap::AvailableActions() { if (!_AvailableActionsCache) { @@ -172,7 +174,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // - Retrieves a map of command names to the commands themselves // - These commands should not be modified directly because they may result in // an invalid state for the `ActionMap` - Windows::Foundation::Collections::IMapView ActionMap::NameMap() + IMapView ActionMap::NameMap() { if (!_NameMapCache) { @@ -283,7 +285,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return cumulativeActions; } - Windows::Foundation::Collections::IMapView ActionMap::GlobalHotkeys() + IMapView ActionMap::GlobalHotkeys() { if (!_GlobalHotkeysCache) { @@ -292,7 +294,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return _GlobalHotkeysCache.GetView(); } - Windows::Foundation::Collections::IMapView ActionMap::KeyBindings() + IMapView ActionMap::KeyBindings() { if (!_KeyBindingMapCache) { @@ -854,4 +856,79 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation cmd->ActionAndArgs(action); AddAction(*cmd); } + + void ActionMap::_recursiveUpdateCommandKeybindingLabels() + { + const auto& commands{ _ExpandedCommandsCache }; + + for (const auto& command : commands) + { + if (command.HasNestedCommands()) + { + _recursiveUpdateCommandKeybindingLabels(); + } + else + { + // If there's a keybinding that's bound to exactly this command, + // then get the keychord and display it as a + // part of the command in the UI. + // We specifically need to do this for nested commands. + const auto keyChord{ GetKeyBindingForAction(command.ActionAndArgs().Action(), + command.ActionAndArgs().Args()) }; + command.RegisterKey(keyChord); + } + } + } + + // This is a helper to aid in sorting commands by their `Name`s, alphabetically. + static bool _compareSchemeNames(const ColorScheme& lhs, const ColorScheme& rhs) + { + std::wstring leftName{ lhs.Name() }; + std::wstring rightName{ rhs.Name() }; + return leftName.compare(rightName) < 0; + } + + void ActionMap::ExpandCommands(const IVectorView& profiles, + const IMapView& schemes) + { + // TODO in review - It's a little weird to stash the expanded commands + // into a separate map. Is it possible to just replace the name map with + // the post-expanded commands? + // + // WHILE also making sure that upon re-saving the commands, we don't + // actually serialize the results of the expansion. I don't think it is. + + std::vector sortedSchemes; + sortedSchemes.reserve(schemes.Size()); + + for (const auto& nameAndScheme : schemes) + { + sortedSchemes.push_back(nameAndScheme.Value()); + } + std::sort(sortedSchemes.begin(), + sortedSchemes.end(), + _compareSchemeNames); + + auto copyOfCommands = winrt::single_threaded_map(); + + const auto& commandsToExpand{ NameMap() }; + for (auto nameAndCommand : commandsToExpand) + { + copyOfCommands.Insert(nameAndCommand.Key(), nameAndCommand.Value()); + } + + implementation::Command::ExpandCommands(copyOfCommands, + profiles, + winrt::param::vector_view{ sortedSchemes }); + + _ExpandedCommandsCache = winrt::single_threaded_vector(); + for (const auto& [_, command] : copyOfCommands) + { + _ExpandedCommandsCache.Append(command); + } + } + IVector ActionMap::ExpandedCommands() + { + return _ExpandedCommandsCache; + } } diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index 6dbd2b04c23..95ab9e9ab7f 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -75,6 +75,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void DeleteKeyBinding(const Control::KeyChord& keys); void RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action); + Windows::Foundation::Collections::IVector ExpandedCommands(); + void ExpandCommands(const Windows::Foundation::Collections::IVectorView& profiles, + const Windows::Foundation::Collections::IMapView& schemes); + private: std::optional _GetActionByID(const InternalActionID actionID) const; std::optional _GetActionByKeyChordInternal(const Control::KeyChord& keys) const; @@ -90,11 +94,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _TryUpdateName(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& consolidatedCmd); void _TryUpdateKeyChord(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& consolidatedCmd); + void _recursiveUpdateCommandKeybindingLabels(); + Windows::Foundation::Collections::IMap _AvailableActionsCache{ nullptr }; Windows::Foundation::Collections::IMap _NameMapCache{ nullptr }; Windows::Foundation::Collections::IMap _GlobalHotkeysCache{ nullptr }; Windows::Foundation::Collections::IMap _KeyBindingMapCache{ nullptr }; + Windows::Foundation::Collections::IVector _ExpandedCommandsCache{ nullptr }; + std::unordered_map _NestedCommands; std::vector _IterableCommands; std::unordered_map _KeyMap; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.idl b/src/cascadia/TerminalSettingsModel/ActionMap.idl index 806baa17a30..99df487263b 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.idl +++ b/src/cascadia/TerminalSettingsModel/ActionMap.idl @@ -20,6 +20,8 @@ namespace Microsoft.Terminal.Settings.Model Windows.Foundation.Collections.IMapView NameMap { get; }; Windows.Foundation.Collections.IMapView KeyBindings { get; }; Windows.Foundation.Collections.IMapView GlobalHotkeys { get; }; + + IVector ExpandedCommands { get; }; }; [default_interface] runtimeclass ActionMap : IActionMapView diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index 4a4fd5a057f..90c2dc55280 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -1214,3 +1214,8 @@ void CascadiaSettings::_validateThemeExists() } } } + +void CascadiaSettings::ExpandCommands() +{ + _globals->ExpandCommands(ActiveProfiles().GetView(), GlobalSettings().ColorSchemes()); +} diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 50ac292bd00..bfd21ee3390 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -143,6 +143,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Model::DefaultTerminal CurrentDefaultTerminal() noexcept; void CurrentDefaultTerminal(const Model::DefaultTerminal& terminal); + void ExpandCommands(); + private: static const std::filesystem::path& _settingsPath(); static const std::filesystem::path& _releaseSettingsPath(); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl index 5a1987dcf3e..58db9f20e87 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl @@ -53,5 +53,7 @@ namespace Microsoft.Terminal.Settings.Model static Boolean IsDefaultTerminalSet { get; }; IObservableVector DefaultTerminals { get; }; DefaultTerminal CurrentDefaultTerminal; + + void ExpandCommands(); } } diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 0c4cb2f16e0..5984053e9e7 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -1110,6 +1110,8 @@ CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) : _resolveDefaultProfile(); _resolveNewTabMenuProfiles(); _validateSettings(); + + ExpandCommands(); } // Method Description: diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index f9f52dcedaf..beea1edfed8 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -478,23 +478,22 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // appended to this vector. // Return Value: // - - void Command::ExpandCommands(IMap commands, + void Command::ExpandCommands(IMap& commands, IVectorView profiles, - IVectorView schemes, - IVector warnings) + IVectorView schemes) { std::vector commandsToRemove; std::vector commandsToAdd; // First, collect up all the commands that need replacing. - for (const auto& nameAndCmd : commands) + for (const auto& [name, command] : commands) { - auto cmd{ get_self(nameAndCmd.Value()) }; + auto cmd{ get_self(command) }; - auto newCommands = _expandCommand(cmd, profiles, schemes, warnings); + auto newCommands = _expandCommand(cmd, profiles, schemes); if (newCommands.size() > 0) { - commandsToRemove.push_back(nameAndCmd.Key()); + commandsToRemove.push_back(name); commandsToAdd.insert(commandsToAdd.end(), newCommands.begin(), newCommands.end()); } } @@ -529,21 +528,18 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Arguments: // - expandable: the Command to potentially turn into more commands // - profiles: A list of all the profiles that this command should be expanded on. - // - warnings: If there were any warnings during parsing, they'll be - // appended to this vector. // Return Value: // - and empty vector if the command wasn't expandable, otherwise a list of // the newly-created commands. std::vector Command::_expandCommand(Command* const expandable, IVectorView profiles, - IVectorView schemes, - IVector& warnings) + IVectorView schemes) { std::vector newCommands; if (expandable->HasNestedCommands()) { - ExpandCommands(expandable->_subcommands, profiles, schemes, warnings); + ExpandCommands(expandable->_subcommands, profiles, schemes); } if (expandable->_IterateOn == ExpandCommandType::None) @@ -564,18 +560,19 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto actualDataEnd = newJsonString.data() + newJsonString.size(); if (!reader->parse(actualDataStart, actualDataEnd, &newJsonValue, &errs)) { - warnings.Append(SettingsLoadWarnings::FailedToParseCommandJson); // If we encounter a re-parsing error, just stop processing the rest of the commands. return false; } // Pass the new json back though FromJson, to get the new expanded value. - std::vector newWarnings; - if (auto newCmd{ Command::FromJson(newJsonValue, newWarnings) }) + // FromJson requires that we pass in a vector to hang on to the + // warnings, but ultimately, we don't care about warnings during + // expansion. + std::vector unused; + if (auto newCmd{ Command::FromJson(newJsonValue, unused) }) { newCommands.push_back(*newCmd); } - std::for_each(newWarnings.begin(), newWarnings.end(), [warnings](auto& warn) { warnings.Append(warn); }); return true; }; diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index 4f4dcbe2048..4c2b17874c2 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -41,10 +41,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static winrt::com_ptr FromJson(const Json::Value& json, std::vector& warnings); - static void ExpandCommands(Windows::Foundation::Collections::IMap commands, + static void ExpandCommands(Windows::Foundation::Collections::IMap& commands, Windows::Foundation::Collections::IVectorView profiles, - Windows::Foundation::Collections::IVectorView schemes, - Windows::Foundation::Collections::IVector warnings); + Windows::Foundation::Collections::IVectorView schemes); static std::vector LayerJson(Windows::Foundation::Collections::IMap& commands, const Json::Value& json); @@ -83,8 +82,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static std::vector _expandCommand(Command* const expandable, Windows::Foundation::Collections::IVectorView profiles, - Windows::Foundation::Collections::IVectorView schemes, - Windows::Foundation::Collections::IVector& warnings); + Windows::Foundation::Collections::IVectorView schemes); friend class SettingsModelLocalTests::DeserializationTests; friend class SettingsModelLocalTests::CommandTests; }; diff --git a/src/cascadia/TerminalSettingsModel/Command.idl b/src/cascadia/TerminalSettingsModel/Command.idl index b749a79fb0a..38af6184bef 100644 --- a/src/cascadia/TerminalSettingsModel/Command.idl +++ b/src/cascadia/TerminalSettingsModel/Command.idl @@ -42,10 +42,5 @@ namespace Microsoft.Terminal.Settings.Model Boolean HasNestedCommands { get; }; Windows.Foundation.Collections.IMapView NestedCommands { get; }; - - static void ExpandCommands(Windows.Foundation.Collections.IMap commands, - Windows.Foundation.Collections.IVectorView profiles, - Windows.Foundation.Collections.IVectorView schemes, - Windows.Foundation.Collections.IVector warnings); } } diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 249bc060f7e..dff11656140 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -240,7 +240,13 @@ winrt::Windows::Foundation::Collections::IMapView& profiles, + const winrt::Windows::Foundation::Collections::IMapView& schemes) +{ + _actionMap->ExpandCommands(profiles, schemes); +} + bool GlobalAppSettings::ShouldUsePersistedLayout() const { - return FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout; + return FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout && !IsolatedMode(); } diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index 8319e561407..8ef1a8a2f72 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -64,6 +64,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Model::Theme CurrentTheme() noexcept; bool ShouldUsePersistedLayout() const; + void ExpandCommands(const Windows::Foundation::Collections::IVectorView& profiles, + const Windows::Foundation::Collections::IMapView& schemes); + INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, L""); #define GLOBAL_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \ diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index e986339665d..5a32d3eebf0 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -98,6 +98,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, ShowAdminShield); INHERITABLE_SETTING(IVector, NewTabMenu); INHERITABLE_SETTING(Boolean, EnableColorSelection); + INHERITABLE_SETTING(Boolean, IsolatedMode); Windows.Foundation.Collections.IMapView ColorSchemes(); void AddColorScheme(ColorScheme scheme); @@ -109,6 +110,7 @@ namespace Microsoft.Terminal.Settings.Model void AddTheme(Theme theme); INHERITABLE_SETTING(ThemePair, Theme); Theme CurrentTheme { get; }; + Boolean ShouldUsePersistedLayout(); } } diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 4a03b898982..798505de23d 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -18,52 +18,53 @@ Author(s): // Macro format (defaultArgs are optional): // (type, name, jsonKey, defaultArgs) -#define MTSM_GLOBAL_SETTINGS(X) \ - X(int32_t, InitialRows, "initialRows", 30) \ - X(int32_t, InitialCols, "initialCols", 80) \ - X(hstring, WordDelimiters, "wordDelimiters", DEFAULT_WORD_DELIMITERS) \ - X(bool, CopyOnSelect, "copyOnSelect", false) \ - X(bool, FocusFollowMouse, "focusFollowMouse", false) \ - X(bool, ForceFullRepaintRendering, "experimental.rendering.forceFullRepaint", false) \ - X(bool, SoftwareRendering, "experimental.rendering.software", false) \ - X(bool, UseBackgroundImageForWindow, "experimental.useBackgroundImageForWindow", false) \ - X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) \ - X(bool, ForceVTInput, "experimental.input.forceVT", false) \ - X(bool, TrimBlockSelection, "trimBlockSelection", true) \ - X(bool, DetectURLs, "experimental.detectURLs", true) \ - X(bool, AlwaysShowTabs, "alwaysShowTabs", true) \ - X(Model::NewTabPosition, NewTabPosition, "newTabPosition", Model::NewTabPosition::AfterLastTab) \ - X(bool, ShowTitleInTitlebar, "showTerminalTitleInTitlebar", true) \ - X(bool, ConfirmCloseAllTabs, "confirmCloseAllTabs", true) \ - X(Model::ThemePair, Theme, "theme") \ - X(hstring, Language, "language") \ - X(winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabWidthMode, "tabWidthMode", winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::Equal) \ - X(bool, UseAcrylicInTabRow, "useAcrylicInTabRow", false) \ - X(bool, ShowTabsInTitlebar, "showTabsInTitlebar", true) \ - X(bool, InputServiceWarning, "inputServiceWarning", true) \ - X(winrt::Microsoft::Terminal::Control::CopyFormat, CopyFormatting, "copyFormatting", 0) \ - X(bool, WarnAboutLargePaste, "largePasteWarning", true) \ - X(bool, WarnAboutMultiLinePaste, "multiLinePasteWarning", true) \ - X(Model::LaunchPosition, InitialPosition, "initialPosition", nullptr, nullptr) \ - X(bool, CenterOnLaunch, "centerOnLaunch", false) \ - X(Model::FirstWindowPreference, FirstWindowPreference, "firstWindowPreference", FirstWindowPreference::DefaultProfile) \ - X(Model::LaunchMode, LaunchMode, "launchMode", LaunchMode::DefaultMode) \ - X(bool, SnapToGridOnResize, "snapToGridOnResize", true) \ - X(bool, DebugFeaturesEnabled, "debugFeatures", debugFeaturesDefault) \ - X(bool, StartOnUserLogin, "startOnUserLogin", false) \ - X(bool, AlwaysOnTop, "alwaysOnTop", false) \ - X(bool, AutoHideWindow, "autoHideWindow", false) \ - X(Model::TabSwitcherMode, TabSwitcherMode, "tabSwitcherMode", Model::TabSwitcherMode::InOrder) \ - X(bool, DisableAnimations, "disableAnimations", false) \ - X(hstring, StartupActions, "startupActions", L"") \ - X(Model::WindowingMode, WindowingBehavior, "windowingBehavior", Model::WindowingMode::UseNew) \ - X(bool, MinimizeToNotificationArea, "minimizeToNotificationArea", false) \ - X(bool, AlwaysShowNotificationIcon, "alwaysShowNotificationIcon", false) \ - X(winrt::Windows::Foundation::Collections::IVector, DisabledProfileSources, "disabledProfileSources", nullptr) \ - X(bool, ShowAdminShield, "showAdminShield", true) \ - X(bool, TrimPaste, "trimPaste", true) \ - X(bool, EnableColorSelection, "experimental.enableColorSelection", false) \ - X(winrt::Windows::Foundation::Collections::IVector, NewTabMenu, "newTabMenu", winrt::single_threaded_vector({ Model::RemainingProfilesEntry{} })) +#define MTSM_GLOBAL_SETTINGS(X) \ + X(int32_t, InitialRows, "initialRows", 30) \ + X(int32_t, InitialCols, "initialCols", 80) \ + X(hstring, WordDelimiters, "wordDelimiters", DEFAULT_WORD_DELIMITERS) \ + X(bool, CopyOnSelect, "copyOnSelect", false) \ + X(bool, FocusFollowMouse, "focusFollowMouse", false) \ + X(bool, ForceFullRepaintRendering, "experimental.rendering.forceFullRepaint", false) \ + X(bool, SoftwareRendering, "experimental.rendering.software", false) \ + X(bool, UseBackgroundImageForWindow, "experimental.useBackgroundImageForWindow", false) \ + X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) \ + X(bool, ForceVTInput, "experimental.input.forceVT", false) \ + X(bool, TrimBlockSelection, "trimBlockSelection", true) \ + X(bool, DetectURLs, "experimental.detectURLs", true) \ + X(bool, AlwaysShowTabs, "alwaysShowTabs", true) \ + X(Model::NewTabPosition, NewTabPosition, "newTabPosition", Model::NewTabPosition::AfterLastTab) \ + X(bool, ShowTitleInTitlebar, "showTerminalTitleInTitlebar", true) \ + X(bool, ConfirmCloseAllTabs, "confirmCloseAllTabs", true) \ + X(Model::ThemePair, Theme, "theme") \ + X(hstring, Language, "language") \ + X(winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabWidthMode, "tabWidthMode", winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::Equal) \ + X(bool, UseAcrylicInTabRow, "useAcrylicInTabRow", false) \ + X(bool, ShowTabsInTitlebar, "showTabsInTitlebar", true) \ + X(bool, InputServiceWarning, "inputServiceWarning", true) \ + X(winrt::Microsoft::Terminal::Control::CopyFormat, CopyFormatting, "copyFormatting", 0) \ + X(bool, WarnAboutLargePaste, "largePasteWarning", true) \ + X(bool, WarnAboutMultiLinePaste, "multiLinePasteWarning", true) \ + X(Model::LaunchPosition, InitialPosition, "initialPosition", nullptr, nullptr) \ + X(bool, CenterOnLaunch, "centerOnLaunch", false) \ + X(Model::FirstWindowPreference, FirstWindowPreference, "firstWindowPreference", FirstWindowPreference::DefaultProfile) \ + X(Model::LaunchMode, LaunchMode, "launchMode", LaunchMode::DefaultMode) \ + X(bool, SnapToGridOnResize, "snapToGridOnResize", true) \ + X(bool, DebugFeaturesEnabled, "debugFeatures", debugFeaturesDefault) \ + X(bool, StartOnUserLogin, "startOnUserLogin", false) \ + X(bool, AlwaysOnTop, "alwaysOnTop", false) \ + X(bool, AutoHideWindow, "autoHideWindow", false) \ + X(Model::TabSwitcherMode, TabSwitcherMode, "tabSwitcherMode", Model::TabSwitcherMode::InOrder) \ + X(bool, DisableAnimations, "disableAnimations", false) \ + X(hstring, StartupActions, "startupActions", L"") \ + X(Model::WindowingMode, WindowingBehavior, "windowingBehavior", Model::WindowingMode::UseNew) \ + X(bool, MinimizeToNotificationArea, "minimizeToNotificationArea", false) \ + X(bool, AlwaysShowNotificationIcon, "alwaysShowNotificationIcon", false) \ + X(winrt::Windows::Foundation::Collections::IVector, DisabledProfileSources, "disabledProfileSources", nullptr) \ + X(bool, ShowAdminShield, "showAdminShield", true) \ + X(bool, TrimPaste, "trimPaste", true) \ + X(bool, EnableColorSelection, "experimental.enableColorSelection", false) \ + X(winrt::Windows::Foundation::Collections::IVector, NewTabMenu, "newTabMenu", winrt::single_threaded_vector({ Model::RemainingProfilesEntry{} })) \ + X(bool, IsolatedMode, "compatibility.isolatedMode", false) #define MTSM_PROFILE_SETTINGS(X) \ X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE) \ diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index f3fd19537eb..bc7a7c6664f 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -121,6 +121,7 @@ namespace RemotingUnitTests TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, Remoting::QuitAllRequestedArgs); + TYPED_EVENT(RequestNewWindow, winrt::Windows::Foundation::IInspectable, Remoting::WindowRequestedArgs); }; class RemotingTests diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 93e68dd0b65..5265e36a994 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -28,32 +28,17 @@ using namespace std::chrono_literals; // "If the high-order bit is 1, the key is down; otherwise, it is up." static constexpr short KeyPressed{ gsl::narrow_cast(0x8000) }; -AppHost::AppHost() noexcept : - _app{}, - _windowManager{}, - _appLogic{ nullptr }, // don't make one, we're going to take a ref on app's +AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + const Remoting::WindowManager& manager, + const Remoting::Peasant& peasant) noexcept : + _windowManager{ manager }, + _peasant{ peasant }, + _appLogic{ logic }, // don't make one, we're going to take a ref on app's _windowLogic{ nullptr }, - _window{ nullptr }, - _getWindowLayoutThrottler{} // this will get set if we become the monarch + _window{ nullptr } { - _appLogic = _app.Logic(); // get a ref to app's logic - - // Inform the WindowManager that it can use us to find the target window for - // a set of commandline args. This needs to be done before - // _HandleCommandlineArgs, because WE might end up being the monarch. That - // would mean we'd need to be responsible for looking that up. - _windowManager.FindTargetWindowRequested({ this, &AppHost::_FindTargetWindow }); - - // If there were commandline args to our process, try and process them here. - // Do this before AppLogic::Create, otherwise this will have no effect. - // - // This will send our commandline to the Monarch, to ask if we should make a - // new window or not. If not, exit immediately. _HandleCommandlineArgs(); - if (!_shouldCreateWindow) - { - return; - } // _HandleCommandlineArgs will create a _windowLogic _useNonClientArea = _windowLogic.GetShowTabsInTitlebar(); @@ -96,7 +81,7 @@ AppHost::AppHost() noexcept : _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); _window->WindowActivated({ this, &AppHost::_WindowActivated }); _window->WindowMoved({ this, &AppHost::_WindowMoved }); - _window->HotkeyPressed({ this, &AppHost::_GlobalHotkeyPressed }); + _window->ShouldExitFullscreen({ &_windowLogic, &winrt::TerminalApp::TerminalWindow::RequestExitFullscreen }); _window->SetAlwaysOnTop(_windowLogic.GetInitialAlwaysOnTop()); @@ -104,17 +89,12 @@ AppHost::AppHost() noexcept : _window->MakeWindow(); - _GetWindowLayoutRequestedToken = _windowManager.GetWindowLayoutRequested([this](auto&&, const winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs& args) { + _GetWindowLayoutRequestedToken = _peasant.GetWindowLayoutRequested([this](auto&&, + const Remoting::GetWindowLayoutArgs& args) { // The peasants are running on separate threads, so they'll need to // swap what context they are in to the ui thread to get the actual layout. args.WindowLayoutJsonAsync(_GetWindowLayoutAsync()); }); - - _revokers.BecameMonarch = _windowManager.BecameMonarch(winrt::auto_revoke, { this, &AppHost::_BecomeMonarch }); - if (_windowManager.IsMonarch()) - { - _BecomeMonarch(nullptr, nullptr); - } } AppHost::~AppHost() @@ -130,8 +110,6 @@ AppHost::~AppHost() _showHideWindowThrottler.reset(); _window = nullptr; - _app.Close(); - _app = nullptr; } bool AppHost::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) @@ -161,26 +139,17 @@ void AppHost::SetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& } } -void _buildArgsFromCommandline(std::vector& args) +void AppHost::s_DisplayMessageBox(const winrt::TerminalApp::ParseCommandlineResult& result) { - if (auto commandline{ GetCommandLineW() }) - { - auto argc = 0; - - // Get the argv, and turn them into a hstring array to pass to the app. - wil::unique_any argv{ CommandLineToArgvW(commandline, &argc) }; - if (argv) - { - for (auto& elem : wil::make_range(argv.get(), argc)) - { - args.emplace_back(elem); - } - } - } - if (args.empty()) - { - args.emplace_back(L"wt.exe"); - } + const auto displayHelp = result.ExitCode == 0; + const auto messageTitle = displayHelp ? IDS_HELP_DIALOG_TITLE : IDS_ERROR_DIALOG_TITLE; + const auto messageIcon = displayHelp ? MB_ICONWARNING : MB_ICONERROR; + // TODO:GH#4134: polish this dialog more, to make the text more + // like msiexec /? + MessageBoxW(nullptr, + result.Message.data(), + GetStringResource(messageTitle).data(), + MB_OK | messageIcon); } // Method Description: @@ -202,44 +171,24 @@ void _buildArgsFromCommandline(std::vector& args) // - void AppHost::_HandleCommandlineArgs() { - std::vector args; - _buildArgsFromCommandline(args); - auto cwd{ wil::GetCurrentDirectoryW() }; - - Remoting::CommandlineArgs eventArgs{ { args }, { cwd } }; - _windowManager.ProposeCommandline(eventArgs); - - _shouldCreateWindow = _windowManager.ShouldCreateWindow(); - if (!_shouldCreateWindow) - { - return; - } - // We did want to make a window, so let's instantiate it here. // We don't have XAML yet, but we do have other stuff. _windowLogic = _appLogic.CreateNewWindow(); - if (auto peasant{ _windowManager.CurrentWindow() }) + if (_peasant) { - if (auto args{ peasant.InitialArgs() }) + const auto& args{ _peasant.InitialArgs() }; + if (args) { const auto result = _windowLogic.SetStartupCommandline(args.Commandline()); const auto message = _windowLogic.ParseCommandlineMessage(); if (!message.empty()) { - const auto displayHelp = result == 0; - const auto messageTitle = displayHelp ? IDS_HELP_DIALOG_TITLE : IDS_ERROR_DIALOG_TITLE; - const auto messageIcon = displayHelp ? MB_ICONWARNING : MB_ICONERROR; - // TODO:GH#4134: polish this dialog more, to make the text more - // like msiexec /? - MessageBoxW(nullptr, - message.data(), - GetStringResource(messageTitle).data(), - MB_OK | messageIcon); + AppHost::s_DisplayMessageBox({ message, result }); if (_windowLogic.ShouldExitEarly()) { - ExitProcess(result); + ExitThread(result); } } } @@ -264,17 +213,19 @@ void AppHost::_HandleCommandlineArgs() // use to send the actions to the app. // // MORE EVENT HANDLERS, same rules as the ones above. - _revokers.peasantExecuteCommandlineRequested = peasant.ExecuteCommandlineRequested(winrt::auto_revoke, { this, &AppHost::_DispatchCommandline }); - _revokers.peasantSummonRequested = peasant.SummonRequested(winrt::auto_revoke, { this, &AppHost::_HandleSummon }); - _revokers.peasantDisplayWindowIdRequested = peasant.DisplayWindowIdRequested(winrt::auto_revoke, { this, &AppHost::_DisplayWindowId }); - _revokers.peasantQuitRequested = peasant.QuitRequested(winrt::auto_revoke, { this, &AppHost::_QuitRequested }); - - // We need this property to be set before we get the InitialSize/Position - // and BecameMonarch which normally sets it is only run after the window - // is created. - if (_windowManager.IsMonarch()) + _revokers.peasantExecuteCommandlineRequested = _peasant.ExecuteCommandlineRequested(winrt::auto_revoke, { this, &AppHost::_DispatchCommandline }); + _revokers.peasantSummonRequested = _peasant.SummonRequested(winrt::auto_revoke, { this, &AppHost::_HandleSummon }); + _revokers.peasantDisplayWindowIdRequested = _peasant.DisplayWindowIdRequested(winrt::auto_revoke, { this, &AppHost::_DisplayWindowId }); + _revokers.peasantQuitRequested = _peasant.QuitRequested(winrt::auto_revoke, { this, &AppHost::_QuitRequested }); + + // This is logic that almost seems like it belongs on the WindowEmperor. + // It probably does. However, it needs to muck with our own window so + // much, that there was no reasonable way of moving this. Moving it also + // seemed to reorder bits of init so much that everything broke. So + // we'll leave it here. + const auto numPeasants = _windowManager.GetNumberOfPeasants(); + if (numPeasants == 1) { - const auto numPeasants = _windowManager.GetNumberOfPeasants(); const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); if (_appLogic.ShouldUsePersistedLayout() && layouts && @@ -287,7 +238,8 @@ void AppHost::_HandleCommandlineArgs() // Otherwise create this window normally with its commandline, and create // a new window using the first saved layout information. // The 2nd+ layout will always get a new window. - if (numPeasants == 1 && !_windowLogic.HasCommandlineArguments() && !_appLogic.HasSettingsStartupActions()) + if (!_windowLogic.HasCommandlineArguments() && + !_appLogic.HasSettingsStartupActions()) { _windowLogic.SetPersistedLayoutIdx(startIdx); startIdx += 1; @@ -296,7 +248,7 @@ void AppHost::_HandleCommandlineArgs() // Create new windows for each of the other saved layouts. for (const auto size = layouts.Size(); startIdx < size; startIdx += 1) { - auto newWindowArgs = fmt::format(L"{0} -w new -s {1}", args[0], startIdx); + auto newWindowArgs = fmt::format(L"{0} -w new -s {1}", args.Commandline()[0], startIdx); STARTUPINFO si; memset(&si, 0, sizeof(si)); @@ -317,8 +269,9 @@ void AppHost::_HandleCommandlineArgs() } } } - _windowLogic.WindowName(peasant.WindowName()); - _windowLogic.WindowId(peasant.GetID()); + + _windowLogic.WindowName(_peasant.WindowName()); + _windowLogic.WindowId(_peasant.GetID()); } } @@ -335,10 +288,12 @@ void AppHost::_HandleCommandlineArgs() // - void AppHost::Initialize() { + // You aren't allowed to do ANY XAML before this line!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! _window->Initialize(); if (auto withWindow{ _windowLogic.try_as() }) { + // You aren't allowed to do anything with the TerminalPage before this line!!!!!!! withWindow->Initialize(_window->GetHandle()); } @@ -377,7 +332,8 @@ void AppHost::Initialize() _window->DragRegionClicked([this]() { _windowLogic.TitlebarClicked(); }); _window->WindowVisibilityChanged([this](bool showOrHide) { _windowLogic.WindowVisibilityChanged(showOrHide); }); - _window->UpdateSettingsRequested([this]() { _appLogic.ReloadSettings(); }); + + _window->UpdateSettingsRequested({ this, &AppHost::_requestUpdateSettings }); _revokers.RequestedThemeChanged = _windowLogic.RequestedThemeChanged(winrt::auto_revoke, { this, &AppHost::_UpdateTheme }); _revokers.FullscreenChanged = _windowLogic.FullscreenChanged(winrt::auto_revoke, { this, &AppHost::_FullscreenChanged }); @@ -397,7 +353,7 @@ void AppHost::Initialize() _window->AutomaticShutdownRequested([this]() { // Raised when the OS is beginning an update of the app. We will quit, // to save our state, before the OS manually kills us. - _windowManager.RequestQuitAll(); + Remoting::WindowManager::RequestQuitAll(_peasant); }); // Load bearing: make sure the PropertyChanged handler is added before we @@ -459,24 +415,6 @@ void AppHost::Initialize() // set that content as well. _window->SetContent(_windowLogic.GetRoot()); _window->OnAppInitialized(); - - // BODGY - // - // We've got a weird crash that happens terribly inconsistently, but pretty - // readily on migrie's laptop, only in Debug mode. Apparently, there's some - // weird ref-counting magic that goes on during teardown, and our - // Application doesn't get closed quite right, which can cause us to crash - // into the debugger. This of course, only happens on exit, and happens - // somewhere in the XamlHost.dll code. - // - // Crazily, if we _manually leak the Application_ here, then the crash - // doesn't happen. This doesn't matter, because we really want the - // Application to live for _the entire lifetime of the process_, so the only - // time when this object would actually need to get cleaned up is _during - // exit_. So we can safely leak this Application object, and have it just - // get cleaned up normally when our process exits. - auto a{ _app }; - ::winrt::detach_abi(a); } // Method Description: @@ -494,7 +432,7 @@ void AppHost::AppTitleChanged(const winrt::Windows::Foundation::IInspectable& /* { _window->UpdateTitle(newTitle); } - _windowManager.UpdateActiveTabTitle(newTitle); + _windowManager.UpdateActiveTabTitle(newTitle, _peasant); } // Method Description: @@ -506,23 +444,19 @@ void AppHost::AppTitleChanged(const winrt::Windows::Foundation::IInspectable& /* // - void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::TerminalApp::LastTabClosedEventArgs& args) { - if (_windowManager.IsMonarch() && _notificationIcon) - { - _DestroyNotificationIcon(); - } - else if (_window->IsQuakeWindow()) + // We don't want to try to save layouts if we are about to close. + _peasant.GetWindowLayoutRequested(_GetWindowLayoutRequestedToken); + + // If the user closes the last tab, in the last window, _by closing the tab_ + // (not by closing the whole window), we need to manually persist an empty + // window state here. That will cause the terminal to re-open with the usual + // settings (not the persisted state) + if (args.ClearPersistedState() && + _windowManager.GetNumberOfPeasants() == 1) { - _HideNotificationIconRequested(nullptr, nullptr); + _windowLogic.ClearPersistedWindowState(); } - // We don't want to try to save layouts if we are about to close. - _getWindowLayoutThrottler.reset(); - _windowManager.GetWindowLayoutRequested(_GetWindowLayoutRequestedToken); - // We also don't need to update any of our bookkeeping on how many - // windows are open. - _windowManager.WindowCreated(_WindowCreatedToken); - _windowManager.WindowClosed(_WindowClosedToken); - // If the user closes the last tab, in the last window, _by closing the tab_ // (not by closing the whole window), we need to manually persist an empty // window state here. That will cause the terminal to re-open with the usual @@ -536,7 +470,7 @@ void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*se // Remove ourself from the list of peasants so that we aren't included in // any future requests. This will also mean we block until any existing // event handler finishes. - _windowManager.SignalClose(); + _windowManager.SignalClose(_peasant); _window->Close(); } @@ -887,54 +821,6 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send _windowLogic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory()); } -// Method Description: -// - Asynchronously get the window layout from the current page. This is -// done async because we need to switch between the ui thread and the calling -// thread. -// - NB: The peasant calling this must not be running on the UI thread, otherwise -// they will crash since they just call .get on the async operation. -// Arguments: -// - -// Return Value: -// - The window layout as a json string. -winrt::Windows::Foundation::IAsyncOperation AppHost::_GetWindowLayoutAsync() -{ - winrt::apartment_context peasant_thread; - - winrt::hstring layoutJson = L""; - // Use the main thread since we are accessing controls. - co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher()); - try - { - const auto pos = _GetWindowLaunchPosition(); - layoutJson = _windowLogic.GetWindowLayoutJson(pos); - } - CATCH_LOG() - - // go back to give the result to the peasant. - co_await peasant_thread; - - co_return layoutJson; -} - -// Method Description: -// - Event handler for the WindowManager::FindTargetWindowRequested event. The -// manager will ask us how to figure out what the target window is for a set -// of commandline arguments. We'll take those arguments, and ask AppLogic to -// parse them for us. We'll then set ResultTargetWindow in the given args, so -// the sender can use that result. -// Arguments: -// - args: the bundle of a commandline and working directory to find the correct target window for. -// Return Value: -// - -void AppHost::_FindTargetWindow(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const Remoting::FindTargetWindowArgs& args) -{ - const auto targetWindow = _appLogic.FindTargetWindow(args.Args().Commandline()); - args.ResultTargetWindow(targetWindow.WindowId()); - args.ResultTargetWindowName(targetWindow.WindowName()); -} - winrt::fire_and_forget AppHost::_WindowActivated(bool activated) { _windowLogic.WindowActivated(activated); @@ -946,275 +832,49 @@ winrt::fire_and_forget AppHost::_WindowActivated(bool activated) co_await winrt::resume_background(); - if (auto peasant{ _windowManager.CurrentWindow() }) + if (_peasant) { 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(), + Remoting::WindowActivatedArgs args{ _peasant.GetID(), (uint64_t)_window->GetHandle(), currentDesktopGuid, winrt::clock().now() }; - peasant.ActivateWindow(args); - } -} - -void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) -{ - // MSFT:35726322 - // - // Although we're manually revoking the event handler now in the dtor before - // we null out the window, let's be extra careful and check JUST IN CASE. - if (_window == nullptr) - { - return; - } - - _setupGlobalHotkeys(); - - if (_windowManager.DoesQuakeWindowExist() || - _window->IsQuakeWindow() || - (_windowLogic.GetAlwaysShowNotificationIcon() || _windowLogic.GetMinimizeToNotificationArea())) - { - _CreateNotificationIcon(); - } - - _WindowCreatedToken = _windowManager.WindowCreated([this](auto&&, auto&&) { - if (_getWindowLayoutThrottler) - { - _getWindowLayoutThrottler.value()(); - } - }); - - _WindowClosedToken = _windowManager.WindowClosed([this](auto&&, auto&&) { - if (_getWindowLayoutThrottler) - { - _getWindowLayoutThrottler.value()(); - } - }); - - // These events are coming from peasants that become or un-become quake windows. - _revokers.ShowNotificationIconRequested = _windowManager.ShowNotificationIconRequested(winrt::auto_revoke, { this, &AppHost::_ShowNotificationIconRequested }); - _revokers.HideNotificationIconRequested = _windowManager.HideNotificationIconRequested(winrt::auto_revoke, { this, &AppHost::_HideNotificationIconRequested }); - // If the monarch receives a QuitAll event it will signal this event to be - // ran before each peasant is closed. - _revokers.QuitAllRequested = _windowManager.QuitAllRequested(winrt::auto_revoke, { this, &AppHost::_QuitAllRequested }); - - // The monarch should be monitoring if it should save the window layout. - if (!_getWindowLayoutThrottler.has_value()) - { - // We want at least some delay to prevent the first save from overwriting - // the data as we try load windows initially. - _getWindowLayoutThrottler.emplace(std::move(std::chrono::seconds(10)), std::move([this]() { _SaveWindowLayoutsRepeat(); })); - _getWindowLayoutThrottler.value()(); - } -} - -winrt::Windows::Foundation::IAsyncAction AppHost::_SaveWindowLayouts() -{ - // Make sure we run on a background thread to not block anything. - co_await winrt::resume_background(); - - if (_appLogic.ShouldUsePersistedLayout()) - { - try - { - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_SaveWindowLayouts_Collect", - TraceLoggingDescription("Logged when collecting window state"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - const auto layoutJsons = _windowManager.GetAllWindowLayouts(); - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_SaveWindowLayouts_Save", - TraceLoggingDescription("Logged when writing window state"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - _appLogic.SaveWindowLayoutJsons(layoutJsons); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_SaveWindowLayouts_Failed", - TraceLoggingDescription("An error occurred when collecting or writing window state"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } - - co_return; -} - -winrt::fire_and_forget AppHost::_SaveWindowLayoutsRepeat() -{ - // Make sure we run on a background thread to not block anything. - co_await winrt::resume_background(); - - co_await _SaveWindowLayouts(); - - // Don't need to save too frequently. - co_await winrt::resume_after(30s); - - // As long as we are supposed to keep saving, request another save. - // This will be delayed by the throttler so that at most one save happens - // per 10 seconds, if a save is requested by another source simultaneously. - if (_getWindowLayoutThrottler.has_value()) - { - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_requestGetLayout", - TraceLoggingDescription("Logged when triggering a throttled write of the window state"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - _getWindowLayoutThrottler.value()(); - } -} - -winrt::fire_and_forget AppHost::_setupGlobalHotkeys() -{ - // The hotkey MUST be registered on the main thread. It will fail otherwise! - co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher()); - - if (!_window) - { - // MSFT:36797001 There's a surprising number of hits of this callback - // getting triggered during teardown. As a best practice, we really - // should make sure _window exists before accessing it on any coroutine. - // We might be getting called back after the app already began getting - // cleaned up. - co_return; - } - // Unregister all previously registered hotkeys. - // - // RegisterHotKey(), will not unregister hotkeys automatically. - // If a hotkey with a given HWND and ID combination already exists - // then a duplicate one will be added, which we don't want. - // (Additionally we want to remove hotkeys that were removed from the settings.) - for (auto i = 0, count = gsl::narrow_cast(_hotkeys.size()); i < count; ++i) - { - _window->UnregisterHotKey(i); - } - - _hotkeys.clear(); - - // Re-register all current hotkeys. - for (const auto& [keyChord, cmd] : _appLogic.GlobalHotkeys()) - { - if (auto summonArgs = cmd.ActionAndArgs().Args().try_as()) - { - auto index = gsl::narrow_cast(_hotkeys.size()); - const auto succeeded = _window->RegisterHotKey(index, keyChord); - - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_setupGlobalHotkey", - TraceLoggingDescription("Emitted when setting a single hotkey"), - TraceLoggingInt64(index, "index", "the index of the hotkey to add"), - TraceLoggingWideString(cmd.Name().c_str(), "name", "the name of the command"), - TraceLoggingBoolean(succeeded, "succeeded", "true if we succeeded"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - _hotkeys.emplace_back(summonArgs); - } + _peasant.ActivateWindow(args); } } // 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. +// - Asynchronously get the window layout from the current page. This is +// done async because we need to switch between the ui thread and the calling +// thread. +// - NB: The peasant calling this must not be running on the UI thread, otherwise +// they will crash since they just call .get on the async operation. // Arguments: -// - hotkeyIndex: the index of the entry in _hotkeys that was pressed. -// Return Value: // - -void AppHost::_GlobalHotkeyPressed(const long hotkeyIndex) +// Return Value: +// - The window layout as a json string. +winrt::Windows::Foundation::IAsyncOperation AppHost::_GetWindowLayoutAsync() { - if (hotkeyIndex < 0 || static_cast(hotkeyIndex) > _hotkeys.size()) - { - return; - } - - const auto& summonArgs = til::at(_hotkeys, hotkeyIndex); - Remoting::SummonWindowSelectionArgs args{ summonArgs.Name() }; - - // desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false - // desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false - // desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true - args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent); - args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent); - args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility()); - args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration()); - - switch (summonArgs.Monitor()) - { - case Settings::Model::MonitorBehavior::Any: - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); - break; - case Settings::Model::MonitorBehavior::ToCurrent: - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToCurrent); - break; - case Settings::Model::MonitorBehavior::ToMouse: - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToMouse); - break; - } + winrt::apartment_context peasant_thread; - _windowManager.SummonWindow(args); - if (args.FoundMatch()) - { - // Excellent, the window was found. We have nothing else to do here. - } - else + winrt::hstring layoutJson = L""; + // Use the main thread since we are accessing controls. + co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher()); + try { - // We should make the window ourselves. - _createNewTerminalWindow(summonArgs); + const auto pos = _GetWindowLaunchPosition(); + layoutJson = _windowLogic.GetWindowLayoutJson(pos); } -} + CATCH_LOG() -// 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(); + // go back to give the result to the peasant. + co_await peasant_thread; - // 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; + co_return layoutJson; } // Method Description: @@ -1308,9 +968,9 @@ winrt::fire_and_forget AppHost::_IdentifyWindowsRequested(const winrt::Windows:: // make sure we're on the background thread, or this will silently fail co_await winrt::resume_background(); - if (auto peasant{ _windowManager.CurrentWindow() }) + if (_peasant) { - peasant.RequestIdentifyWindows(); + _peasant.RequestIdentifyWindows(); } } @@ -1336,11 +996,11 @@ winrt::fire_and_forget AppHost::_RenameWindowRequested(const winrt::Windows::Fou // Switch to the BG thread - anything x-proc must happen on a BG thread co_await winrt::resume_background(); - if (auto peasant{ _windowManager.CurrentWindow() }) + if (_peasant) { Remoting::RenameRequestArgs requestArgs{ args.ProposedName() }; - peasant.RequestRename(requestArgs); + _peasant.RequestRename(requestArgs); // Switch back to the UI thread. Setting the WindowName needs to happen // on the UI thread, because it'll raise a PropertyChanged event @@ -1407,32 +1067,7 @@ void AppHost::_updateTheme() void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::TerminalApp::SettingsLoadEventArgs& /*args*/) { - _setupGlobalHotkeys(); - - // If we're monarch, we need to check some conditions to show the notification icon. - // If there's a Quake window somewhere, we'll want to keep the notification icon. - // There's two settings - MinimizeToNotificationArea and AlwaysShowNotificationIcon. If either - // one of them are true, we want to make sure there's a notification icon. - // If both are false, we want to remove our icon from the notification area. - // When we remove our icon from the notification area, we'll also want to re-summon - // any hidden windows, but right now we're not keeping track of who's hidden, - // so just summon them all. Tracking the work to do a "summon all minimized" in - // GH#10448 - if (_windowManager.IsMonarch()) - { - if (!_windowManager.DoesQuakeWindowExist()) - { - if (!_notificationIcon && (_windowLogic.GetMinimizeToNotificationArea() || _windowLogic.GetAlwaysShowNotificationIcon())) - { - _CreateNotificationIcon(); - } - else if (_notificationIcon && !_windowLogic.GetMinimizeToNotificationArea() && !_windowLogic.GetAlwaysShowNotificationIcon()) - { - _windowManager.SummonAllWindows(); - _DestroyNotificationIcon(); - } - } - } + // We don't need to call in to windowLogic here - it has its own SettingsChanged handler _window->SetMinimizeToNotificationAreaBehavior(_windowLogic.GetMinimizeToNotificationArea()); _window->SetAutoHideWindow(_windowLogic.AutoHideWindow()); @@ -1442,23 +1077,10 @@ void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspecta void AppHost::_IsQuakeWindowChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { - // We want the quake window to be accessible through the notification icon. - // This means if there's a quake window _somewhere_, we want the notification icon - // to show regardless of the notification icon settings. - // This also means we'll need to destroy the notification icon if it was created - // specifically for the quake window. If not, it should not be destroyed. - if (!_window->IsQuakeWindow() && _windowLogic.IsQuakeWindow()) - { - _ShowNotificationIconRequested(nullptr, nullptr); - } - else if (_window->IsQuakeWindow() && !_windowLogic.IsQuakeWindow()) - { - _HideNotificationIconRequested(nullptr, nullptr); - } - _window->IsQuakeWindow(_windowLogic.IsQuakeWindow()); } +// Raised from our Peasant. We handle by propagating the call to our terminal window. winrt::fire_and_forget AppHost::_QuitRequested(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { @@ -1468,25 +1090,11 @@ winrt::fire_and_forget AppHost::_QuitRequested(const winrt::Windows::Foundation: _windowLogic.Quit(); } +// Raised from TerminalWindow. We handle by bubbling the request to the window manager. void AppHost::_RequestQuitAll(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { - _windowManager.RequestQuitAll(); -} - -void AppHost::_QuitAllRequested(const winrt::Windows::Foundation::IInspectable&, - const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args) -{ - // Make sure that the current timer is destroyed so that it doesn't attempt - // to run while we are in the middle of quitting. - if (_getWindowLayoutThrottler.has_value()) - { - _getWindowLayoutThrottler.reset(); - } - - // Tell the monarch to wait for the window layouts to save before - // everyone quits. - args.BeforeQuitAllAction(_SaveWindowLayouts()); + Remoting::WindowManager::RequestQuitAll(_peasant); } void AppHost::_ShowWindowChanged(const winrt::Windows::Foundation::IInspectable&, @@ -1540,76 +1148,6 @@ void AppHost::_SystemMenuChangeRequested(const winrt::Windows::Foundation::IInsp } } -// Method Description: -// - Creates a Notification Icon and hooks up its handlers -// Arguments: -// - -// Return Value: -// - -void AppHost::_CreateNotificationIcon() -{ - _notificationIcon = std::make_unique(_window->GetHandle()); - - // Hookup the handlers, save the tokens for revoking if settings change. - _ReAddNotificationIconToken = _window->NotifyReAddNotificationIcon([this]() { _notificationIcon->ReAddNotificationIcon(); }); - _NotificationIconPressedToken = _window->NotifyNotificationIconPressed([this]() { _notificationIcon->NotificationIconPressed(); }); - _ShowNotificationIconContextMenuToken = _window->NotifyShowNotificationIconContextMenu([this](til::point coord) { _notificationIcon->ShowContextMenu(coord, _windowManager.GetPeasantInfos()); }); - _NotificationIconMenuItemSelectedToken = _window->NotifyNotificationIconMenuItemSelected([this](HMENU hm, UINT idx) { _notificationIcon->MenuItemSelected(hm, idx); }); - _notificationIcon->SummonWindowRequested([this](auto& args) { _windowManager.SummonWindow(args); }); -} - -// Method Description: -// - Deletes our notification icon if we have one. -// Arguments: -// - -// Return Value: -// - -void AppHost::_DestroyNotificationIcon() -{ - _window->NotifyReAddNotificationIcon(_ReAddNotificationIconToken); - _window->NotifyNotificationIconPressed(_NotificationIconPressedToken); - _window->NotifyShowNotificationIconContextMenu(_ShowNotificationIconContextMenuToken); - _window->NotifyNotificationIconMenuItemSelected(_NotificationIconMenuItemSelectedToken); - - _notificationIcon->RemoveIconFromNotificationArea(); - _notificationIcon = nullptr; -} - -void AppHost::_ShowNotificationIconRequested(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) -{ - if (_windowManager.IsMonarch()) - { - if (!_notificationIcon) - { - _CreateNotificationIcon(); - } - } - else - { - _windowManager.RequestShowNotificationIcon(); - } -} - -void AppHost::_HideNotificationIconRequested(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) -{ - if (_windowManager.IsMonarch()) - { - // Destroy it only if our settings allow it - if (_notificationIcon && - !_windowLogic.GetAlwaysShowNotificationIcon() && - !_windowLogic.GetMinimizeToNotificationArea()) - { - _DestroyNotificationIcon(); - } - } - else - { - _windowManager.RequestHideNotificationIcon(); - } -} - // Method Description: // - BODGY workaround for GH#9320. When the window moves, dismiss all the popups // in the UI tree. Xaml Islands unfortunately doesn't do this for us, see @@ -1673,3 +1211,16 @@ void AppHost::_PropertyChangedHandler(const winrt::Windows::Foundation::IInspect } } } + +winrt::TerminalApp::TerminalWindow AppHost::Logic() +{ + return _windowLogic; +} + +// Bubble the update settings request up to the emperor. We're being called on +// the Window thread, but the Emperor needs to update the settings on the _main_ +// thread. +void AppHost::_requestUpdateSettings() +{ + _UpdateSettingsRequestedHandlers(); +} diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index abee5a9e668..ca970073fc8 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -9,7 +9,10 @@ class AppHost { public: - AppHost() noexcept; + AppHost(const winrt::TerminalApp::AppLogic& logic, + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + const winrt::Microsoft::Terminal::Remoting::WindowManager& manager, + const winrt::Microsoft::Terminal::Remoting::Peasant& peasant) noexcept; virtual ~AppHost(); void AppTitleChanged(const winrt::Windows::Foundation::IInspectable& sender, winrt::hstring newTitle); @@ -21,24 +24,27 @@ class AppHost bool HasWindow(); winrt::TerminalApp::TerminalWindow Logic(); + static void s_DisplayMessageBox(const winrt::TerminalApp::ParseCommandlineResult& message); + + WINRT_CALLBACK(UpdateSettingsRequested, winrt::delegate); + private: std::unique_ptr _window; - winrt::TerminalApp::App _app; + winrt::TerminalApp::AppLogic _appLogic; winrt::TerminalApp::TerminalWindow _windowLogic; + winrt::Microsoft::Terminal::Remoting::WindowManager _windowManager{ nullptr }; winrt::Microsoft::Terminal::Remoting::Peasant _peasant{ nullptr }; - std::vector _hotkeys; winrt::com_ptr _desktopManager{ nullptr }; bool _shouldCreateWindow{ false }; bool _useNonClientArea{ false }; - std::optional> _getWindowLayoutThrottler; std::shared_ptr> _showHideWindowThrottler; - winrt::Windows::Foundation::IAsyncAction _SaveWindowLayouts(); - winrt::fire_and_forget _SaveWindowLayoutsRepeat(); + + void _preInit(); void _HandleCommandlineArgs(); winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition(); @@ -67,12 +73,6 @@ class AppHost winrt::Windows::Foundation::IAsyncOperation _GetWindowLayoutAsync(); - 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::Microsoft::Terminal::Remoting::SummonWindowBehavior& args); @@ -87,9 +87,6 @@ class AppHost bool _LazyLoadDesktopManager(); - void _listenForInboundConnections(); - 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::TerminalApp::SettingsLoadEventArgs& args); @@ -119,13 +116,6 @@ class AppHost void _ShowWindowChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs& args); - void _CreateNotificationIcon(); - void _DestroyNotificationIcon(); - void _ShowNotificationIconRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); - void _HideNotificationIconRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); - void _updateTheme(); void _PropertyChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, @@ -133,14 +123,9 @@ class AppHost void _initialResizeAndRepositionWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); - std::unique_ptr _notificationIcon; - winrt::event_token _ReAddNotificationIconToken; - winrt::event_token _NotificationIconPressedToken; - winrt::event_token _ShowNotificationIconContextMenuToken; - winrt::event_token _NotificationIconMenuItemSelectedToken; + void _requestUpdateSettings(); + winrt::event_token _GetWindowLayoutRequestedToken; - winrt::event_token _WindowCreatedToken; - winrt::event_token _WindowClosedToken; // Helper struct. By putting these all into one struct, we can revoke them // all at once, by assigning _revokers to a fresh Revokers instance. That'll @@ -149,7 +134,6 @@ class AppHost struct Revokers { // Event handlers to revoke in ~AppHost, before calling App.Close - winrt::Microsoft::Terminal::Remoting::WindowManager::BecameMonarch_revoker BecameMonarch; winrt::Microsoft::Terminal::Remoting::Peasant::ExecuteCommandlineRequested_revoker peasantExecuteCommandlineRequested; winrt::Microsoft::Terminal::Remoting::Peasant::SummonRequested_revoker peasantSummonRequested; winrt::Microsoft::Terminal::Remoting::Peasant::DisplayWindowIdRequested_revoker peasantDisplayWindowIdRequested; @@ -174,8 +158,7 @@ class AppHost winrt::TerminalApp::TerminalWindow::ShowWindowChanged_revoker ShowWindowChanged; winrt::TerminalApp::TerminalWindow::PropertyChanged_revoker PropertyChanged; winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged; - winrt::Microsoft::Terminal::Remoting::WindowManager::ShowNotificationIconRequested_revoker ShowNotificationIconRequested; - winrt::Microsoft::Terminal::Remoting::WindowManager::HideNotificationIconRequested_revoker HideNotificationIconRequested; + winrt::Microsoft::Terminal::Remoting::WindowManager::QuitAllRequested_revoker QuitAllRequested; } _revokers{}; }; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index acc039ae447..978159b2803 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -27,8 +27,6 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; #define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS" #define IDM_SYSTEM_MENU_BEGIN 0x1000 -const UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); - IslandWindow::IslandWindow() noexcept : _interopWindowHandle{ nullptr }, _rootGrid{ nullptr }, @@ -418,11 +416,6 @@ 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); @@ -638,30 +631,6 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize } break; } - case CM_NOTIFY_FROM_NOTIFICATION_AREA: - { - switch (LOWORD(lparam)) - { - case NIN_SELECT: - case NIN_KEYSELECT: - { - _NotifyNotificationIconPressedHandlers(); - return 0; - } - case WM_CONTEXTMENU: - { - const til::point eventPoint{ GET_X_LPARAM(wparam), GET_Y_LPARAM(wparam) }; - _NotifyShowNotificationIconContextMenuHandlers(eventPoint); - return 0; - } - } - break; - } - case WM_MENUCOMMAND: - { - _NotifyNotificationIconMenuItemSelectedHandlers((HMENU)lparam, (UINT)wparam); - return 0; - } case WM_SYSCOMMAND: { // the low 4 bits contain additional information (that we don't care about) @@ -738,16 +707,6 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize _AutomaticShutdownRequestedHandlers(); return true; } - default: - // We'll want to receive this message when explorer.exe restarts - // so that we can re-add our icon to the notification area. - // This unfortunately isn't a switch case because we register the - // message at runtime. - if (message == WM_TASKBARCREATED) - { - _NotifyReAddNotificationIconHandlers(); - return 0; - } } // TODO: handle messages here... @@ -1264,65 +1223,6 @@ void IslandWindow::_SetIsFullscreen(const bool fullscreenEnabled) } } -// Method Description: -// - Call UnregisterHotKey once for each previously registered hotkey. -// Return Value: -// - -void IslandWindow::UnregisterHotKey(const int index) noexcept -{ - TraceLoggingWrite( - g_hWindowsTerminalProvider, - "UnregisterHotKey", - TraceLoggingDescription("Emitted when clearing previously set hotkeys"), - TraceLoggingInt64(index, "index", "the index of the hotkey to remove"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - LOG_IF_WIN32_BOOL_FALSE(::UnregisterHotKey(_window.get(), index)); -} - -// Method Description: -// - Call RegisterHotKey to attempt to register that keybinding as a global hotkey. -// - When these keys are pressed, we'll get a WM_HOTKEY message with the payload -// containing the index we registered here. -// - Call UnregisterHotKey() before registering your hotkeys. -// See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey#remarks -// Arguments: -// - hotkey: The key-combination to register. -// Return Value: -// - -bool IslandWindow::RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept -{ - const auto vkey = hotkey.Vkey(); - auto hotkeyFlags = MOD_NOREPEAT; - { - const auto modifiers = hotkey.Modifiers(); - WI_SetFlagIf(hotkeyFlags, MOD_WIN, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows)); - WI_SetFlagIf(hotkeyFlags, MOD_ALT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu)); - WI_SetFlagIf(hotkeyFlags, MOD_CONTROL, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control)); - WI_SetFlagIf(hotkeyFlags, MOD_SHIFT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift)); - } - - // TODO GH#8888: We should display a warning of some kind if this fails. - // This can fail if something else already bound this hotkey. - const auto result = ::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey); - - TraceLoggingWrite(g_hWindowsTerminalProvider, - "RegisterHotKey", - TraceLoggingDescription("Emitted when setting hotkeys"), - TraceLoggingInt64(index, "index", "the index of the hotkey to add"), - TraceLoggingUInt64(vkey, "vkey", "the key"), - TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_WIN), "win", "is WIN in the modifiers"), - TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_ALT), "alt", "is ALT in the modifiers"), - TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_CONTROL), "control", "is CONTROL in the modifiers"), - TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_SHIFT), "shift", "is SHIFT in the modifiers"), - TraceLoggingBool(result, "succeeded", "true if we succeeded"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - return result; -} - // Method Description: // - Summon the window, or possibly dismiss it. If toggleVisibility is true, // then we'll dismiss (minimize) the window if it's currently active. diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 12100cc78ba..eb412a0f445 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -3,7 +3,6 @@ #include "pch.h" #include "BaseWindow.h" -#include void SetWindowLongWHelper(const HWND hWnd, const int nIndex, const LONG dwNewLong) noexcept; @@ -51,9 +50,6 @@ class IslandWindow : void FlashTaskbar(); void SetTaskbarProgress(const size_t state, const size_t progress); - void UnregisterHotKey(const int index) noexcept; - bool RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept; - winrt::fire_and_forget SummonWindow(winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior args); bool IsQuakeWindow() const noexcept; @@ -74,7 +70,6 @@ class IslandWindow : WINRT_CALLBACK(WindowCloseButtonClicked, winrt::delegate<>); WINRT_CALLBACK(MouseScrolled, winrt::delegate); WINRT_CALLBACK(WindowActivated, winrt::delegate); - WINRT_CALLBACK(HotkeyPressed, winrt::delegate); WINRT_CALLBACK(NotifyNotificationIconPressed, winrt::delegate); WINRT_CALLBACK(NotifyWindowHidden, winrt::delegate); WINRT_CALLBACK(NotifyShowNotificationIconContextMenu, winrt::delegate); diff --git a/src/cascadia/WindowsTerminal/NotificationIcon.h b/src/cascadia/WindowsTerminal/NotificationIcon.h index cc71a88b41f..3277a1e845f 100644 --- a/src/cascadia/WindowsTerminal/NotificationIcon.h +++ b/src/cascadia/WindowsTerminal/NotificationIcon.h @@ -3,6 +3,7 @@ #include "pch.h" +#pragma once // This enumerates all the possible actions // that our notification icon context menu could do. enum class NotificationIconMenuItemAction diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp new file mode 100644 index 00000000000..75fe386d6c6 --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -0,0 +1,757 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "WindowEmperor.h" + +#include "../inc/WindowingBehavior.h" + +#include "../../types/inc/utils.hpp" + +#include "../WinRTUtils/inc/WtExeUtils.h" + +#include "resource.h" +#include "NotificationIcon.h" + +using namespace winrt; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Windows::Foundation; +using namespace ::Microsoft::Console; +using namespace std::chrono_literals; +using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; + +#define TERMINAL_MESSAGE_CLASS_NAME L"TERMINAL_MESSAGE_CLASS" +extern "C" IMAGE_DOS_HEADER __ImageBase; + +WindowEmperor::WindowEmperor() noexcept : + _app{} +{ + _manager.FindTargetWindowRequested([this](const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& findWindowArgs) { + { + const auto targetWindow = _app.Logic().FindTargetWindow(findWindowArgs.Args().Commandline()); + findWindowArgs.ResultTargetWindow(targetWindow.WindowId()); + findWindowArgs.ResultTargetWindowName(targetWindow.WindowName()); + } + }); + + _dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); +} + +WindowEmperor::~WindowEmperor() +{ + _app.Close(); + _app = nullptr; +} + +void _buildArgsFromCommandline(std::vector& args) +{ + if (auto commandline{ GetCommandLineW() }) + { + auto argc = 0; + + // Get the argv, and turn them into a hstring array to pass to the app. + wil::unique_any argv{ CommandLineToArgvW(commandline, &argc) }; + if (argv) + { + for (auto& elem : wil::make_range(argv.get(), argc)) + { + args.emplace_back(elem); + } + } + } + if (args.empty()) + { + args.emplace_back(L"wt.exe"); + } +} + +bool WindowEmperor::HandleCommandlineArgs() +{ + std::vector args; + _buildArgsFromCommandline(args); + auto cwd{ wil::GetCurrentDirectoryW() }; + + Remoting::CommandlineArgs eventArgs{ { args }, { cwd } }; + + const auto isolatedMode{ _app.Logic().IsolatedMode() }; + + const auto result = _manager.ProposeCommandline(eventArgs, isolatedMode); + + if (result.ShouldCreateWindow()) + { + _createNewWindowThread(Remoting::WindowRequestedArgs{ result, eventArgs }); + + _becomeMonarch(); + } + else + { + const auto res = _app.Logic().GetParseCommandlineMessage(eventArgs.Commandline()); + if (!res.Message.empty()) + { + AppHost::s_DisplayMessageBox(res); + ExitThread(res.ExitCode); + } + } + + return result.ShouldCreateWindow(); +} + +void WindowEmperor::WaitForWindows() +{ + MSG message{}; + while (GetMessageW(&message, nullptr, 0, 0)) + { + TranslateMessage(&message); + DispatchMessage(&message); + } +} + +void WindowEmperor::_createNewWindowThread(const Remoting::WindowRequestedArgs& args) +{ + Remoting::Peasant peasant{ _manager.CreatePeasant(args) }; + auto window{ std::make_shared(_app.Logic(), args, _manager, peasant) }; + std::weak_ptr weakThis{ weak_from_this() }; + + std::thread t([weakThis, window]() { + window->CreateHost(); + + if (auto self{ weakThis.lock() }) + { + self->_windowStartedHandler(window); + } + + window->RunMessagePump(); + + if (auto self{ weakThis.lock() }) + { + self->_windowExitedHandler(window->Peasant().GetID()); + } + }); + LOG_IF_FAILED(SetThreadDescription(t.native_handle(), L"Window Thread")); + + t.detach(); +} + +// Handler for a WindowThread's Started event, which it raises once the window +// thread starts and XAML is ready to go on that thread. Set up some callbacks +// now that we know this window is set up and ready to go. +// Q: Why isn't adding these callbacks just a part of _createNewWindowThread? +// A: Until the thread actually starts, the AppHost (and its Logic()) haven't +// been ctor'd or initialized, so trying to add callbacks immediately will A/V +void WindowEmperor::_windowStartedHandler(const std::shared_ptr& sender) +{ + // Add a callback to the window's logic to let us know when the window's + // quake mode state changes. We'll use this to check if we need to add + // or remove the notification icon. + sender->Logic().IsQuakeWindowChanged({ this, &WindowEmperor::_windowIsQuakeWindowChanged }); + sender->UpdateSettingsRequested({ this, &WindowEmperor::_windowRequestUpdateSettings }); + + // Summon the window to the foreground, since we might not _currently_ be in + // the foreground, but we should act like the new window is. + // + // TODO: GH#14957 - use AllowSetForeground from the original wt.exe instead + Remoting::SummonWindowSelectionArgs args{}; + args.OnCurrentDesktop(false); + args.WindowID(sender->Peasant().GetID()); + args.SummonBehavior().MoveToCurrentDesktop(false); + args.SummonBehavior().ToggleVisibility(false); + args.SummonBehavior().DropdownDuration(0); + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + _manager.SummonWindow(args); + + // Now that the window is ready to go, we can add it to our list of windows, + // because we know it will be well behaved. + // + // Be sure to only modify the list of windows under lock. + { + auto lockedWindows{ _windows.lock() }; + lockedWindows->push_back(sender); + } +} +void WindowEmperor::_windowExitedHandler(uint64_t senderID) +{ + auto lockedWindows{ _windows.lock() }; + + // find the window in _windows who's peasant's Id matches the peasant's Id + // and remove it + std::erase_if(*lockedWindows, + [&](const auto& w) { + return w->Peasant().GetID() == senderID; + }); + + if (lockedWindows->size() == 0) + { + _close(); + } +} +// Method Description: +// - Set up all sorts of handlers now that we've determined that we're a process +// that will end up hosting the windows. These include: +// - Setting up a message window to handle hotkeys and notification icon +// invokes. +// - Setting up the global hotkeys. +// - Setting up the notification icon. +// - Setting up callbacks for when the settings change. +// - Setting up callbacks for when the number of windows changes. +// - Setting up the throttled func for layout persistence. Arguments: +// - +void WindowEmperor::_becomeMonarch() +{ + // Add a callback to the window manager so that when the Monarch wants a new + // window made, they come to us + _manager.RequestNewWindow([this](auto&&, const Remoting::WindowRequestedArgs& args) { + _createNewWindowThread(args); + }); + + _createMessageWindow(); + + _setupGlobalHotkeys(); + + // When the settings change, we'll want to update our global hotkeys and our + // notification icon based on the new settings. + _app.Logic().SettingsChanged([this](auto&&, const TerminalApp::SettingsLoadEventArgs& args) { + if (SUCCEEDED(args.Result())) + { + _setupGlobalHotkeys(); + _checkWindowsForNotificationIcon(); + } + }); + + // On startup, immediately check if we need to show the notification icon. + _checkWindowsForNotificationIcon(); + + // Set the number of open windows (so we know if we are the last window) + // and subscribe for updates if there are any changes to that number. + + _revokers.WindowCreated = _manager.WindowCreated(winrt::auto_revoke, { this, &WindowEmperor::_numberOfWindowsChanged }); + _revokers.WindowClosed = _manager.WindowClosed(winrt::auto_revoke, { this, &WindowEmperor::_numberOfWindowsChanged }); + + // If the monarch receives a QuitAll event it will signal this event to be + // ran before each peasant is closed. + _revokers.QuitAllRequested = _manager.QuitAllRequested(winrt::auto_revoke, { this, &WindowEmperor::_quitAllRequested }); + + // The monarch should be monitoring if it should save the window layout. + // We want at least some delay to prevent the first save from overwriting + _getWindowLayoutThrottler.emplace(std::move(std::chrono::seconds(10)), std::move([this]() { _saveWindowLayoutsRepeat(); })); + _getWindowLayoutThrottler.value()(); + + // BODGY + // + // We've got a weird crash that happens terribly inconsistently, but pretty + // readily on migrie's laptop, only in Debug mode. Apparently, there's some + // weird ref-counting magic that goes on during teardown, and our + // Application doesn't get closed quite right, which can cause us to crash + // into the debugger. This of course, only happens on exit, and happens + // somewhere in the XamlHost.dll code. + // + // Crazily, if we _manually leak the Application_ here, then the crash + // doesn't happen. This doesn't matter, because we really want the + // Application to live for _the entire lifetime of the process_, so the only + // time when this object would actually need to get cleaned up is _during + // exit_. So we can safely leak this Application object, and have it just + // get cleaned up normally when our process exits. + auto a{ _app }; + ::winrt::detach_abi(a); +} + +// sender and args are always nullptr +void WindowEmperor::_numberOfWindowsChanged(const winrt::Windows::Foundation::IInspectable&, + const winrt::Windows::Foundation::IInspectable&) +{ + if (_getWindowLayoutThrottler) + { + _getWindowLayoutThrottler.value()(); + } + + // If we closed out the quake window, and don't otherwise need the tray + // icon, let's get rid of it. + _checkWindowsForNotificationIcon(); +} + +// Raised from our windowManager (on behalf of the monarch). We respond by +// giving the monarch an async function that the manager should wait on before +// completing the quit. +void WindowEmperor::_quitAllRequested(const winrt::Windows::Foundation::IInspectable&, + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args) +{ + // Make sure that the current timer is destroyed so that it doesn't attempt + // to run while we are in the middle of quitting. + if (_getWindowLayoutThrottler.has_value()) + { + _getWindowLayoutThrottler.reset(); + } + + // Tell the monarch to wait for the window layouts to save before + // everyone quits. + args.BeforeQuitAllAction(_saveWindowLayouts()); +} + +#pragma region LayoutPersistence + +winrt::Windows::Foundation::IAsyncAction WindowEmperor::_saveWindowLayouts() +{ + // Make sure we run on a background thread to not block anything. + co_await winrt::resume_background(); + + if (_app.Logic().ShouldUsePersistedLayout()) + { + try + { + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_SaveWindowLayouts_Collect", + TraceLoggingDescription("Logged when collecting window state"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + const auto layoutJsons = _manager.GetAllWindowLayouts(); + + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_SaveWindowLayouts_Save", + TraceLoggingDescription("Logged when writing window state"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + _app.Logic().SaveWindowLayoutJsons(layoutJsons); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_SaveWindowLayouts_Failed", + TraceLoggingDescription("An error occurred when collecting or writing window state"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + } + + co_return; +} + +winrt::fire_and_forget WindowEmperor::_saveWindowLayoutsRepeat() +{ + // Make sure we run on a background thread to not block anything. + co_await winrt::resume_background(); + + co_await _saveWindowLayouts(); + + // Don't need to save too frequently. + co_await winrt::resume_after(30s); + + // As long as we are supposed to keep saving, request another save. + // This will be delayed by the throttler so that at most one save happens + // per 10 seconds, if a save is requested by another source simultaneously. + if (_getWindowLayoutThrottler.has_value()) + { + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_requestGetLayout", + TraceLoggingDescription("Logged when triggering a throttled write of the window state"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + _getWindowLayoutThrottler.value()(); + } +} +#pragma endregion + +#pragma region WindowProc + +static WindowEmperor* GetThisFromHandle(HWND const window) noexcept +{ + const auto data = GetWindowLongPtr(window, GWLP_USERDATA); + return reinterpret_cast(data); +} +[[nodiscard]] LRESULT __stdcall WindowEmperor::_wndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept +{ + WINRT_ASSERT(window); + + if (WM_NCCREATE == message) + { + auto cs = reinterpret_cast(lparam); + WindowEmperor* that = static_cast(cs->lpCreateParams); + WINRT_ASSERT(that); + WINRT_ASSERT(!that->_window); + that->_window = wil::unique_hwnd(window); + SetWindowLongPtr(that->_window.get(), GWLP_USERDATA, reinterpret_cast(that)); + } + else if (WindowEmperor* that = GetThisFromHandle(window)) + { + return that->_messageHandler(message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} +void WindowEmperor::_createMessageWindow() +{ + WNDCLASS wc{}; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hInstance = reinterpret_cast(&__ImageBase); + wc.lpszClassName = TERMINAL_MESSAGE_CLASS_NAME; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = WindowEmperor::_wndProc; + wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(IDI_APPICON)); + RegisterClass(&wc); + WINRT_ASSERT(!_window); + + WINRT_VERIFY(CreateWindow(wc.lpszClassName, + L"Windows Terminal", + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + HWND_MESSAGE, + nullptr, + wc.hInstance, + this)); +} + +LRESULT WindowEmperor::_messageHandler(UINT const message, WPARAM const wParam, LPARAM const lParam) noexcept +{ + // use C++11 magic statics to make sure we only do this once. + // This won't change over the lifetime of the application + static const UINT WM_TASKBARCREATED = []() { return RegisterWindowMessageW(L"TaskbarCreated"); }(); + + switch (message) + { + case WM_HOTKEY: + { + _hotkeyPressed(static_cast(wParam)); + return 0; + } + case CM_NOTIFY_FROM_NOTIFICATION_AREA: + { + switch (LOWORD(lParam)) + { + case NIN_SELECT: + case NIN_KEYSELECT: + { + _notificationIcon->NotificationIconPressed(); + return 0; + } + case WM_CONTEXTMENU: + { + const til::point eventPoint{ GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam) }; + _notificationIcon->ShowContextMenu(eventPoint, _manager.GetPeasantInfos()); + return 0; + } + } + break; + } + case WM_MENUCOMMAND: + { + _notificationIcon->MenuItemSelected((HMENU)lParam, (UINT)wParam); + return 0; + } + default: + { + // We'll want to receive this message when explorer.exe restarts + // so that we can re-add our icon to the notification area. + // This unfortunately isn't a switch case because we register the + // message at runtime. + if (message == WM_TASKBARCREATED) + { + _notificationIcon->ReAddNotificationIcon(); + return 0; + } + } + } + return DefWindowProc(_window.get(), message, wParam, lParam); +} + +winrt::fire_and_forget WindowEmperor::_close() +{ + // Important! Switch back to the main thread for the emperor. That way, the + // quit will go to the emperor's message pump. + co_await wil::resume_foreground(_dispatcher); + PostQuitMessage(0); +} + +#pragma endregion +#pragma region GlobalHotkeys + +// 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: +// - +static winrt::fire_and_forget _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 WindowEmperor::_hotkeyPressed(const long hotkeyIndex) +{ + if (hotkeyIndex < 0 || static_cast(hotkeyIndex) > _hotkeys.size()) + { + return; + } + + const auto& summonArgs = til::at(_hotkeys, hotkeyIndex); + Remoting::SummonWindowSelectionArgs args{ summonArgs.Name() }; + + // desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false + // desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false + // desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true + args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent); + args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent); + args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility()); + args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration()); + + switch (summonArgs.Monitor()) + { + case Settings::Model::MonitorBehavior::Any: + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + break; + case Settings::Model::MonitorBehavior::ToCurrent: + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToCurrent); + break; + case Settings::Model::MonitorBehavior::ToMouse: + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToMouse); + break; + } + + _manager.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); + } +} + +bool WindowEmperor::_registerHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept +{ + const auto vkey = hotkey.Vkey(); + auto hotkeyFlags = MOD_NOREPEAT; + { + const auto modifiers = hotkey.Modifiers(); + WI_SetFlagIf(hotkeyFlags, MOD_WIN, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows)); + WI_SetFlagIf(hotkeyFlags, MOD_ALT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu)); + WI_SetFlagIf(hotkeyFlags, MOD_CONTROL, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control)); + WI_SetFlagIf(hotkeyFlags, MOD_SHIFT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift)); + } + + // TODO GH#8888: We should display a warning of some kind if this fails. + // This can fail if something else already bound this hotkey. + const auto result = ::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey); + LOG_LAST_ERROR_IF(!result); + TraceLoggingWrite(g_hWindowsTerminalProvider, + "RegisterHotKey", + TraceLoggingDescription("Emitted when setting hotkeys"), + TraceLoggingInt64(index, "index", "the index of the hotkey to add"), + TraceLoggingUInt64(vkey, "vkey", "the key"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_WIN), "win", "is WIN in the modifiers"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_ALT), "alt", "is ALT in the modifiers"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_CONTROL), "control", "is CONTROL in the modifiers"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_SHIFT), "shift", "is SHIFT in the modifiers"), + TraceLoggingBool(result, "succeeded", "true if we succeeded"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + return result; +} + +// Method Description: +// - Call UnregisterHotKey once for each previously registered hotkey. +// Return Value: +// - +void WindowEmperor::_unregisterHotKey(const int index) noexcept +{ + TraceLoggingWrite( + g_hWindowsTerminalProvider, + "UnregisterHotKey", + TraceLoggingDescription("Emitted when clearing previously set hotkeys"), + TraceLoggingInt64(index, "index", "the index of the hotkey to remove"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + LOG_IF_WIN32_BOOL_FALSE(::UnregisterHotKey(_window.get(), index)); +} + +winrt::fire_and_forget WindowEmperor::_setupGlobalHotkeys() +{ + // The hotkey MUST be registered on the main thread. It will fail otherwise! + co_await wil::resume_foreground(_dispatcher); + + if (!_window) + { + // MSFT:36797001 There's a surprising number of hits of this callback + // getting triggered during teardown. As a best practice, we really + // should make sure _window exists before accessing it on any coroutine. + // We might be getting called back after the app already began getting + // cleaned up. + co_return; + } + // Unregister all previously registered hotkeys. + // + // RegisterHotKey(), will not unregister hotkeys automatically. + // If a hotkey with a given HWND and ID combination already exists + // then a duplicate one will be added, which we don't want. + // (Additionally we want to remove hotkeys that were removed from the settings.) + for (auto i = 0, count = gsl::narrow_cast(_hotkeys.size()); i < count; ++i) + { + _unregisterHotKey(i); + } + + _hotkeys.clear(); + + // Re-register all current hotkeys. + for (const auto& [keyChord, cmd] : _app.Logic().GlobalHotkeys()) + { + if (auto summonArgs = cmd.ActionAndArgs().Args().try_as()) + { + auto index = gsl::narrow_cast(_hotkeys.size()); + const auto succeeded = _registerHotKey(index, keyChord); + + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_setupGlobalHotkey", + TraceLoggingDescription("Emitted when setting a single hotkey"), + TraceLoggingInt64(index, "index", "the index of the hotkey to add"), + TraceLoggingWideString(cmd.Name().c_str(), "name", "the name of the command"), + TraceLoggingBoolean(succeeded, "succeeded", "true if we succeeded"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + _hotkeys.emplace_back(summonArgs); + } + } +} + +#pragma endregion + +#pragma region NotificationIcon +// Method Description: +// - Creates a Notification Icon and hooks up its handlers +// Arguments: +// - +// Return Value: +// - +void WindowEmperor::_createNotificationIcon() +{ + _notificationIcon = std::make_unique(_window.get()); + _notificationIcon->SummonWindowRequested([this](auto& args) { _manager.SummonWindow(args); }); +} + +// Method Description: +// - Deletes our notification icon if we have one. +// Arguments: +// - +// Return Value: +// - +void WindowEmperor::_destroyNotificationIcon() +{ + _notificationIcon->RemoveIconFromNotificationArea(); + _notificationIcon = nullptr; +} + +void WindowEmperor::_checkWindowsForNotificationIcon() +{ + // We need to check some conditions to show the notification icon. + // + // * If there's a Quake window somewhere, we'll want to keep the + // notification icon. + // * There's two settings - MinimizeToNotificationArea and + // AlwaysShowNotificationIcon. If either one of them are true, we want to + // make sure there's a notification icon. + // + // If both are false, we want to remove our icon from the notification area. + // When we remove our icon from the notification area, we'll also want to + // re-summon any hidden windows, but right now we're not keeping track of + // who's hidden, so just summon them all. Tracking the work to do a "summon + // all minimized" in GH#10448 + // + // To avoid races between us thinking the settings updated, and the windows + // themselves getting the new settings, only ask the app logic for the + // RequestsTrayIcon setting value, and combine that with the result of each + // window (which won't change during a settings reload). + bool needsIcon = _app.Logic().RequestsTrayIcon(); + { + auto windows{ _windows.lock_shared() }; + for (const auto& _windowThread : *windows) + { + needsIcon |= _windowThread->Logic().IsQuakeWindow(); + } + } + + if (needsIcon) + { + _showNotificationIconRequested(); + } + else + { + _hideNotificationIconRequested(); + } +} + +void WindowEmperor::_showNotificationIconRequested() +{ + if (!_notificationIcon) + { + _createNotificationIcon(); + } +} + +void WindowEmperor::_hideNotificationIconRequested() +{ + // Destroy it only if our settings allow it + if (_notificationIcon) + { + // If we no longer want the tray icon, but we did have one, then quick + // re-summon all our windows, so they don't get lost when the icon + // disappears forever. + _manager.SummonAllWindows(); + + _destroyNotificationIcon(); + } +} +#pragma endregion + +// A callback to the window's logic to let us know when the window's +// quake mode state changes. We'll use this to check if we need to add +// or remove the notification icon. +winrt::fire_and_forget WindowEmperor::_windowIsQuakeWindowChanged(winrt::Windows::Foundation::IInspectable sender, + winrt::Windows::Foundation::IInspectable args) +{ + co_await wil::resume_foreground(this->_dispatcher); + _checkWindowsForNotificationIcon(); +} +winrt::fire_and_forget WindowEmperor::_windowRequestUpdateSettings() +{ + // We MUST be on the main thread to update the settings. We will crash when trying to enumerate fragment extensions otherwise. + co_await wil::resume_foreground(this->_dispatcher); + _app.Logic().ReloadSettings(); +} diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h new file mode 100644 index 00000000000..9ed30a41cea --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -0,0 +1,89 @@ +/*++ +Copyright (c) Microsoft Corporation Licensed under the MIT license. + +Class Name: +- WindowEmperor.h + +Abstract: +- The WindowEmperor is our class for managing the single Terminal process + with all our windows. It will be responsible for handling the commandline + arguments. It will initially try to find another terminal process to + communicate with. If it does, it'll hand off to the existing process. +- If it determines that it should create a window, it will set up a new thread + for that window, and a message loop on the main thread for handling global + state, such as hotkeys and the notification icon. + +--*/ + +#pragma once +#include "pch.h" + +#include "WindowThread.h" + +class WindowEmperor : public std::enable_shared_from_this +{ +public: + WindowEmperor() noexcept; + ~WindowEmperor(); + void WaitForWindows(); + + bool HandleCommandlineArgs(); + +private: + void _createNewWindowThread(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); + + [[nodiscard]] static LRESULT __stdcall _wndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; + LRESULT _messageHandler(UINT const message, WPARAM const wParam, LPARAM const lParam) noexcept; + wil::unique_hwnd _window; + + winrt::TerminalApp::App _app; + winrt::Windows::System::DispatcherQueue _dispatcher{ nullptr }; + winrt::Microsoft::Terminal::Remoting::WindowManager _manager; + + til::shared_mutex>> _windows; + + std::optional> _getWindowLayoutThrottler; + + winrt::event_token _WindowCreatedToken; + winrt::event_token _WindowClosedToken; + + std::vector _hotkeys; + + std::unique_ptr _notificationIcon; + + void _windowStartedHandler(const std::shared_ptr& sender); + void _windowExitedHandler(uint64_t senderID); + + void _becomeMonarch(); + void _numberOfWindowsChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&); + void _quitAllRequested(const winrt::Windows::Foundation::IInspectable&, + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs&); + + winrt::fire_and_forget _windowIsQuakeWindowChanged(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::Foundation::IInspectable args); + winrt::fire_and_forget _windowRequestUpdateSettings(); + + winrt::Windows::Foundation::IAsyncAction _saveWindowLayouts(); + winrt::fire_and_forget _saveWindowLayoutsRepeat(); + + void _createMessageWindow(); + + void _hotkeyPressed(const long hotkeyIndex); + bool _registerHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept; + void _unregisterHotKey(const int index) noexcept; + winrt::fire_and_forget _setupGlobalHotkeys(); + + winrt::fire_and_forget _close(); + + void _createNotificationIcon(); + void _destroyNotificationIcon(); + void _checkWindowsForNotificationIcon(); + void _showNotificationIconRequested(); + void _hideNotificationIconRequested(); + + struct Revokers + { + winrt::Microsoft::Terminal::Remoting::WindowManager::WindowCreated_revoker WindowCreated; + winrt::Microsoft::Terminal::Remoting::WindowManager::WindowClosed_revoker WindowClosed; + winrt::Microsoft::Terminal::Remoting::WindowManager::QuitAllRequested_revoker QuitAllRequested; + } _revokers{}; +}; diff --git a/src/cascadia/WindowsTerminal/WindowThread.cpp b/src/cascadia/WindowsTerminal/WindowThread.cpp new file mode 100644 index 00000000000..398a36e16dd --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowThread.cpp @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "WindowThread.h" + +WindowThread::WindowThread(winrt::TerminalApp::AppLogic logic, + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + winrt::Microsoft::Terminal::Remoting::WindowManager manager, + winrt::Microsoft::Terminal::Remoting::Peasant peasant) : + _peasant{ std::move(peasant) }, + _appLogic{ std::move(logic) }, + _args{ std::move(args) }, + _manager{ std::move(manager) } +{ + // DO NOT start the AppHost here in the ctor, as that will start XAML on the wrong thread! +} + +void WindowThread::CreateHost() +{ + // Start the AppHost HERE, on the actual thread we want XAML to run on + _host = std::make_unique<::AppHost>(_appLogic, + _args, + _manager, + _peasant); + _host->UpdateSettingsRequested([this]() { _UpdateSettingsRequestedHandlers(); }); + + winrt::init_apartment(winrt::apartment_type::single_threaded); + + // Initialize the xaml content. This must be called AFTER the + // WindowsXamlManager is initialized. + _host->Initialize(); +} + +int WindowThread::RunMessagePump() +{ + // Enter the main window loop. + const auto exitCode = _messagePump(); + // Here, the main window loop has exited. + + _host = nullptr; + // !! LOAD BEARING !! + // + // Make sure to finish pumping all the messages for our thread here. We + // may think we're all done, but we're not quite. XAML needs more time + // to pump the remaining events through, even at the point we're + // exiting. So do that now. If you don't, then the last tab to close + // will never actually destruct the last tab / TermControl / ControlCore + // / renderer. + { + MSG msg = {}; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + ::DispatchMessageW(&msg); + } + } + + return exitCode; +} + +winrt::TerminalApp::TerminalWindow WindowThread::Logic() +{ + return _host->Logic(); +} + +static bool _messageIsF7Keypress(const MSG& message) +{ + return (message.message == WM_KEYDOWN || message.message == WM_SYSKEYDOWN) && message.wParam == VK_F7; +} +static bool _messageIsAltKeyup(const MSG& message) +{ + return (message.message == WM_KEYUP || message.message == WM_SYSKEYUP) && message.wParam == VK_MENU; +} +static bool _messageIsAltSpaceKeypress(const MSG& message) +{ + return message.message == WM_SYSKEYDOWN && message.wParam == VK_SPACE; +} + +int WindowThread::_messagePump() +{ + MSG message{}; + + while (GetMessageW(&message, nullptr, 0, 0)) + { + // GH#638 (Pressing F7 brings up both the history AND a caret browsing message) + // The Xaml input stack doesn't allow an application to suppress the "caret browsing" + // dialog experience triggered when you press F7. Official recommendation from the Xaml + // team is to catch F7 before we hand it off. + // AppLogic contains an ad-hoc implementation of event bubbling for a runtime classes + // implementing a custom IF7Listener interface. + // If the recipient of IF7Listener::OnF7Pressed suggests that the F7 press has, in fact, + // been handled we can discard the message before we even translate it. + if (_messageIsF7Keypress(message)) + { + if (_host->OnDirectKeyEvent(VK_F7, LOBYTE(HIWORD(message.lParam)), true)) + { + // The application consumed the F7. Don't let Xaml get it. + continue; + } + } + + // GH#6421 - System XAML will never send an Alt KeyUp event. So, similar + // to how we'll steal the F7 KeyDown above, we'll steal the Alt KeyUp + // here, and plumb it through. + if (_messageIsAltKeyup(message)) + { + // Let's pass to the application + if (_host->OnDirectKeyEvent(VK_MENU, LOBYTE(HIWORD(message.lParam)), false)) + { + // The application consumed the Alt. Don't let Xaml get it. + continue; + } + } + + // GH#7125 = System XAML will show a system dialog on Alt Space. We want to + // explicitly prevent that because we handle that ourselves. So similar to + // above, we steal the event and hand it off to the host. + if (_messageIsAltSpaceKeypress(message)) + { + _host->OnDirectKeyEvent(VK_SPACE, LOBYTE(HIWORD(message.lParam)), true); + continue; + } + + TranslateMessage(&message); + DispatchMessage(&message); + } + return 0; +} +winrt::Microsoft::Terminal::Remoting::Peasant WindowThread::Peasant() +{ + return _peasant; +} diff --git a/src/cascadia/WindowsTerminal/WindowThread.h b/src/cascadia/WindowsTerminal/WindowThread.h new file mode 100644 index 00000000000..fecee558876 --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowThread.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#pragma once +#include "pch.h" +#include "AppHost.h" + +class WindowThread : public std::enable_shared_from_this +{ +public: + WindowThread(winrt::TerminalApp::AppLogic logic, + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + winrt::Microsoft::Terminal::Remoting::WindowManager manager, + winrt::Microsoft::Terminal::Remoting::Peasant peasant); + + winrt::TerminalApp::TerminalWindow Logic(); + void CreateHost(); + int RunMessagePump(); + + winrt::Microsoft::Terminal::Remoting::Peasant Peasant(); + + WINRT_CALLBACK(UpdateSettingsRequested, winrt::delegate); + +private: + winrt::Microsoft::Terminal::Remoting::Peasant _peasant{ nullptr }; + + winrt::TerminalApp::AppLogic _appLogic{ nullptr }; + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs _args{ nullptr }; + winrt::Microsoft::Terminal::Remoting::WindowManager _manager{ nullptr }; + + std::unique_ptr<::AppHost> _host{ nullptr }; + + int _messagePump(); +}; diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj index cd555c70499..85359d58274 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -56,6 +56,8 @@ + + @@ -67,6 +69,8 @@ + + diff --git a/src/cascadia/WindowsTerminal/main.cpp b/src/cascadia/WindowsTerminal/main.cpp index a54d37faddc..09cac10710c 100644 --- a/src/cascadia/WindowsTerminal/main.cpp +++ b/src/cascadia/WindowsTerminal/main.cpp @@ -2,7 +2,7 @@ // Licensed under the MIT license. #include "pch.h" -#include "AppHost.h" +#include "WindowEmperor.h" #include "resource.h" #include "../types/inc/User32Utils.hpp" #include @@ -83,19 +83,6 @@ static void EnsureNativeArchitecture() } } -static bool _messageIsF7Keypress(const MSG& message) -{ - return (message.message == WM_KEYDOWN || message.message == WM_SYSKEYDOWN) && message.wParam == VK_F7; -} -static bool _messageIsAltKeyup(const MSG& message) -{ - return (message.message == WM_KEYUP || message.message == WM_SYSKEYUP) && message.wParam == VK_MENU; -} -static bool _messageIsAltSpaceKeypress(const MSG& message) -{ - return message.message == WM_SYSKEYDOWN && message.wParam == VK_SPACE; -} - int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) { TraceLoggingRegister(g_hWindowsTerminalProvider); @@ -127,68 +114,9 @@ int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) // doing that, we can safely init as STA before any WinRT dispatches. winrt::init_apartment(winrt::apartment_type::single_threaded); - // Create the AppHost object, which will create both the window and the - // Terminal App. This MUST BE constructed before the Xaml manager as TermApp - // provides an implementation of Windows.UI.Xaml.Application. - AppHost host; - if (!host.HasWindow()) - { - // If we were told to not have a window, exit early. Make sure to use - // ExitProcess to die here. If you try just `return 0`, then - // the XAML app host will crash during teardown. ExitProcess avoids - // that. - ExitProcess(0); - } - - // Initialize the xaml content. This must be called AFTER the - // WindowsXamlManager is initialized. - host.Initialize(); - - MSG message; - - while (GetMessage(&message, nullptr, 0, 0)) + const auto emperor = std::make_shared<::WindowEmperor>(); + if (emperor->HandleCommandlineArgs()) { - // GH#638 (Pressing F7 brings up both the history AND a caret browsing message) - // The Xaml input stack doesn't allow an application to suppress the "caret browsing" - // dialog experience triggered when you press F7. Official recommendation from the Xaml - // team is to catch F7 before we hand it off. - // AppLogic contains an ad-hoc implementation of event bubbling for a runtime classes - // implementing a custom IF7Listener interface. - // If the recipient of IF7Listener::OnF7Pressed suggests that the F7 press has, in fact, - // been handled we can discard the message before we even translate it. - if (_messageIsF7Keypress(message)) - { - if (host.OnDirectKeyEvent(VK_F7, LOBYTE(HIWORD(message.lParam)), true)) - { - // The application consumed the F7. Don't let Xaml get it. - continue; - } - } - - // GH#6421 - System XAML will never send an Alt KeyUp event. So, similar - // to how we'll steal the F7 KeyDown above, we'll steal the Alt KeyUp - // here, and plumb it through. - if (_messageIsAltKeyup(message)) - { - // Let's pass to the application - if (host.OnDirectKeyEvent(VK_MENU, LOBYTE(HIWORD(message.lParam)), false)) - { - // The application consumed the Alt. Don't let Xaml get it. - continue; - } - } - - // GH#7125 = System XAML will show a system dialog on Alt Space. We want to - // explicitly prevent that because we handle that ourselves. So similar to - // above, we steal the event and hand it off to the host. - if (_messageIsAltSpaceKeypress(message)) - { - host.OnDirectKeyEvent(VK_SPACE, LOBYTE(HIWORD(message.lParam)), true); - continue; - } - - TranslateMessage(&message); - DispatchMessage(&message); + emperor->WaitForWindows(); } - return 0; } diff --git a/src/cascadia/WindowsTerminal/pch.h b/src/cascadia/WindowsTerminal/pch.h index 345731f39c8..eafe88dfb6c 100644 --- a/src/cascadia/WindowsTerminal/pch.h +++ b/src/cascadia/WindowsTerminal/pch.h @@ -87,7 +87,9 @@ TRACELOGGING_DECLARE_PROVIDER(g_hWindowsTerminalProvider); #include #include #include + #include "til.h" +#include "til/mutex.h" #include #include // must go after the CoreDispatcher type is defined diff --git a/src/cascadia/inc/WindowingBehavior.h b/src/cascadia/inc/WindowingBehavior.h index 04260893191..43515e7b468 100644 --- a/src/cascadia/inc/WindowingBehavior.h +++ b/src/cascadia/inc/WindowingBehavior.h @@ -9,5 +9,6 @@ constexpr int32_t WindowingBehaviorUseNew{ -1 }; constexpr int32_t WindowingBehaviorUseExisting{ -2 }; constexpr int32_t WindowingBehaviorUseAnyExisting{ -3 }; constexpr int32_t WindowingBehaviorUseName{ -4 }; +constexpr int32_t WindowingBehaviorUseNone{ -5 }; static constexpr std::wstring_view QuakeWindowName{ L"_quake" };