diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index 87d7a3d8c5..c82254bbbe 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -25,6 +25,7 @@ DWINRT enablewttlogging Intelli LKG +LOCKFILE Lxss mfcribbon microsoft diff --git a/src/cascadia/Remoting/GetWindowLayoutArgs.cpp b/src/cascadia/Remoting/GetWindowLayoutArgs.cpp new file mode 100644 index 0000000000..f2cc01df40 --- /dev/null +++ b/src/cascadia/Remoting/GetWindowLayoutArgs.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "pch.h" +#include "GetWindowLayoutArgs.h" +#include "GetWindowLayoutArgs.g.cpp" diff --git a/src/cascadia/Remoting/GetWindowLayoutArgs.h b/src/cascadia/Remoting/GetWindowLayoutArgs.h new file mode 100644 index 0000000000..06706f60ba --- /dev/null +++ b/src/cascadia/Remoting/GetWindowLayoutArgs.h @@ -0,0 +1,32 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- GetWindowLayoutArgs.h + +Abstract: +- This is a helper class for getting the window layout from a peasant. + Depending on if we are running on the monarch or on a peasant we might need + to switch what thread we are executing on. This gives us the option of + either returning the json result synchronously, or as a promise. +--*/ + +#pragma once + +#include "GetWindowLayoutArgs.g.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::Remoting::implementation +{ + struct GetWindowLayoutArgs : public GetWindowLayoutArgsT + { + WINRT_PROPERTY(winrt::hstring, WindowLayoutJson, L""); + WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncOperation, WindowLayoutJsonAsync, nullptr) + }; +} + +namespace winrt::Microsoft::Terminal::Remoting::factory_implementation +{ + BASIC_FACTORY(GetWindowLayoutArgs); +} diff --git a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj index a2d6e8ae64..517c2f56fc 100644 --- a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj +++ b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj @@ -12,7 +12,6 @@ - @@ -36,6 +35,12 @@ Peasant.idl + + Peasant.idl + + + Monarch.idl + @@ -71,6 +76,12 @@ Peasant.idl + + Peasant.idl + + + Monarch.idl + Create @@ -128,6 +139,5 @@ - - + \ No newline at end of file diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index c16eb52e9b..eed247ab41 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -6,6 +6,7 @@ #include "Monarch.h" #include "CommandlineArgs.h" #include "FindTargetWindowArgs.h" +#include "QuitAllRequestedArgs.h" #include "ProposeCommandlineResult.h" #include "Monarch.g.cpp" @@ -135,12 +136,18 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // - used // Return Value: // - - void Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) + winrt::fire_and_forget Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*args*/) { // Let the process hosting the monarch run any needed logic before // closing all windows. - _QuitAllRequestedHandlers(*this, nullptr); + auto args = winrt::make_self(); + _QuitAllRequestedHandlers(*this, *args); + + if (const auto action = args->BeforeQuitAllAction()) + { + co_await action; + } _quitting.store(true); // Tell all peasants to exit. @@ -994,4 +1001,28 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _forEachPeasant(func, onError); } + + // Method Description: + // - Ask all peasants to return their window layout as json + // Arguments: + // - + // Return Value: + // - The collection of window layouts from each peasant. + Windows::Foundation::Collections::IVector Monarch::GetAllWindowLayouts() + { + std::vector vec; + auto callback = [&](const auto& /*id*/, const auto& p) { + vec.emplace_back(p.GetWindowLayout()); + }; + auto onError = [](auto&& id) { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_GetAllWindowLayouts_Failed", + TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not get a window layout from"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + }; + _forEachPeasant(callback, onError); + + return winrt::single_threaded_vector(std::move(vec)); + } } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 7ec9044998..b965d1d2a9 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -59,13 +59,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void SummonAllWindows(); bool DoesQuakeWindowExist(); Windows::Foundation::Collections::IVectorView GetPeasantInfos(); + Windows::Foundation::Collections::IVector GetAllWindowLayouts(); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideNotificationIconRequested, 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(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); private: uint64_t _ourPID; @@ -103,8 +104,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void _renameRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args); - void _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); + winrt::fire_and_forget _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& args); // Method Description: // - Helper for doing something on each and every peasant. @@ -177,6 +178,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } _clearOldMruEntries(peasantsToErase); + + // A peasant died, let the app host know that the number of + // windows has changed. + _WindowClosedHandlers(nullptr, nullptr); } } diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index 4eb77695a2..f60b3997a6 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -31,6 +31,12 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.IReference WindowID; } + [default_interface] runtimeclass QuitAllRequestedArgs + { + QuitAllRequestedArgs(); + Windows.Foundation.IAsyncAction BeforeQuitAllAction; + } + struct PeasantInfo { UInt64 Id; @@ -52,12 +58,13 @@ namespace Microsoft.Terminal.Remoting void SummonAllWindows(); Boolean DoesQuakeWindowExist(); Windows.Foundation.Collections.IVectorView GetPeasantInfos { get; }; + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; - event Windows.Foundation.TypedEventHandler QuitAllRequested; + event Windows.Foundation.TypedEventHandler QuitAllRequested; }; } diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp index a8cb749d64..46fd7ce2e4 100644 --- a/src/cascadia/Remoting/Peasant.cpp +++ b/src/cascadia/Remoting/Peasant.cpp @@ -5,6 +5,7 @@ #include "Peasant.h" #include "CommandlineArgs.h" #include "SummonWindowBehavior.h" +#include "GetWindowLayoutArgs.h" #include "Peasant.g.cpp" #include "../../types/inc/utils.hpp" @@ -289,4 +290,24 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } + + // Method Description: + // - Request and return the window layout from the current TerminalPage + // Arguments: + // - + // Return Value: + // - the window layout as a json string + hstring Peasant::GetWindowLayout() + { + auto args = winrt::make_self(); + _GetWindowLayoutRequestedHandlers(nullptr, *args); + if (const auto op = args->WindowLayoutJsonAsync()) + { + // This will fail if called on the UI thread, so the monarch should + // never set WindowLayoutJsonAsync. + auto str = op.get(); + return str; + } + return args->WindowLayoutJson(); + } } diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h index f6f884491f..fdb20d9428 100644 --- a/src/cascadia/Remoting/Peasant.h +++ b/src/cascadia/Remoting/Peasant.h @@ -36,6 +36,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs(); winrt::Microsoft::Terminal::Remoting::CommandlineArgs InitialArgs(); + + winrt::hstring GetWindowLayout(); + WINRT_PROPERTY(winrt::hstring, WindowName); WINRT_PROPERTY(winrt::hstring, ActiveTabTitle); @@ -49,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); private: Peasant(const uint64_t testPID); diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index 80e24cb2c4..ec87c85188 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -30,6 +30,11 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.DateTime ActivatedTime { get; }; }; + [default_interface] runtimeclass GetWindowLayoutArgs { + GetWindowLayoutArgs(); + String WindowLayoutJson; + Windows.Foundation.IAsyncOperation WindowLayoutJsonAsync; + } enum MonitorBehavior { @@ -69,6 +74,7 @@ namespace Microsoft.Terminal.Remoting void RequestHideNotificationIcon(); void RequestQuitAll(); void Quit(); + String GetWindowLayout(); event Windows.Foundation.TypedEventHandler WindowActivated; event Windows.Foundation.TypedEventHandler ExecuteCommandlineRequested; @@ -78,6 +84,7 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler SummonRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; + event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; event Windows.Foundation.TypedEventHandler QuitAllRequested; event Windows.Foundation.TypedEventHandler QuitRequested; }; diff --git a/src/cascadia/Remoting/QuitAllRequestedArgs.cpp b/src/cascadia/Remoting/QuitAllRequestedArgs.cpp new file mode 100644 index 0000000000..ed5c39dcf4 --- /dev/null +++ b/src/cascadia/Remoting/QuitAllRequestedArgs.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "pch.h" +#include "QuitAllRequestedArgs.h" +#include "QuitAllRequestedArgs.g.cpp" diff --git a/src/cascadia/Remoting/QuitAllRequestedArgs.h b/src/cascadia/Remoting/QuitAllRequestedArgs.h new file mode 100644 index 0000000000..8c9c26fd28 --- /dev/null +++ b/src/cascadia/Remoting/QuitAllRequestedArgs.h @@ -0,0 +1,30 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- QuitAllRequestedArgs.h + +Abstract: +- This is a helper class for allowing the monarch to run code before telling all + peasants to quit. This way the monarch can raise an event and get back a future + to wait for before continuing. +--*/ + +#pragma once + +#include "QuitAllRequestedArgs.g.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::Remoting::implementation +{ + struct QuitAllRequestedArgs : public QuitAllRequestedArgsT + { + WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncAction, BeforeQuitAllAction, nullptr) + }; +} + +namespace winrt::Microsoft::Terminal::Remoting::factory_implementation +{ + BASIC_FACTORY(QuitAllRequestedArgs); +} diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 9a71fcef09..4cafee145a 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -271,7 +271,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); _monarch.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequestedHandlers(*this, nullptr); }); _monarch.HideNotificationIconRequested([this](auto&&, auto&&) { _HideNotificationIconRequestedHandlers(*this, nullptr); }); - _monarch.QuitAllRequested([this](auto&&, auto&&) { _QuitAllRequestedHandlers(*this, nullptr); }); + _monarch.QuitAllRequested({ get_weak(), &WindowManager::_QuitAllRequestedHandlers }); _BecameMonarchHandlers(*this, nullptr); } @@ -318,6 +318,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } + _peasant.GetWindowLayoutRequested({ get_weak(), &WindowManager::_GetWindowLayoutRequestedHandlers }); + TraceLoggingWrite(g_hRemotingProvider, "WindowManager_CreateOurPeasant", TraceLoggingUInt64(_peasant.GetID(), "peasantID", "The ID of our new peasant"), @@ -610,4 +612,17 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { winrt::get_self(_peasant)->ActiveTabTitle(title); } + + Windows::Foundation::Collections::IVector WindowManager::GetAllWindowLayouts() + { + if (_monarch) + { + try + { + return _monarch.GetAllWindowLayouts(); + } + CATCH_LOG() + } + return nullptr; + } } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 3d2eaf6c74..379038750e 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -50,6 +50,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::fire_and_forget RequestQuitAll(); bool DoesQuakeWindowExist(); void UpdateActiveTabTitle(winrt::hstring title); + Windows::Foundation::Collections::IVector GetAllWindowLayouts(); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); @@ -57,7 +58,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation 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::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); private: bool _shouldCreateWindow{ false }; diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index cf15fbb426..2fdfd7e343 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -16,6 +16,8 @@ namespace Microsoft.Terminal.Remoting void SummonAllWindows(); void RequestShowNotificationIcon(); void RequestHideNotificationIcon(); + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); + UInt64 GetNumberOfPeasants(); void RequestQuitAll(); void UpdateActiveTabTitle(String title); @@ -25,8 +27,9 @@ namespace Microsoft.Terminal.Remoting 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 QuitAllRequested; }; } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 1ef4c0c8ab..d99726b397 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -78,7 +78,7 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleCloseWindow(const IInspectable& /*sender*/, const ActionEventArgs& args) { - CloseWindow(false); + _CloseRequestedHandlers(nullptr, nullptr); args.Handled(true); } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 7f2f919a45..3baad1aa04 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -187,6 +187,10 @@ void AppCommandlineArgs::_buildParser() _windowTarget, RS_A(L"CmdWindowTargetArgDesc")); + _app.add_option("-s,--saved", + _loadPersistedLayoutIdx, + RS_A(L"CmdSavedLayoutArgDesc")); + // Subcommands _buildNewTabParser(); _buildSplitPaneParser(); @@ -700,6 +704,7 @@ void AppCommandlineArgs::_resetStateToDefault() _swapPaneDirection = FocusDirection::None; _focusPaneTarget = -1; + _loadPersistedLayoutIdx = -1; // DON'T clear _launchMode here! This will get called once for every // subcommand, so we don't want `wt -F new-tab ; split-pane` clearing out @@ -915,6 +920,12 @@ void AppCommandlineArgs::ValidateStartupCommands() } } } +std::optional AppCommandlineArgs::GetPersistedLayoutIdx() const noexcept +{ + return _loadPersistedLayoutIdx >= 0 ? + std::optional{ static_cast(_loadPersistedLayoutIdx) } : + std::nullopt; +} std::optional AppCommandlineArgs::GetLaunchMode() const noexcept { diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 598c3b8ed9..de076ec99c 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -39,6 +39,7 @@ public: const std::string& GetExitMessage(); bool ShouldExitEarly() const noexcept; + std::optional GetPersistedLayoutIdx() const noexcept; std::optional GetLaunchMode() const noexcept; int ParseArgs(const winrt::Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); @@ -123,6 +124,7 @@ private: std::string _exitMessage; bool _shouldExitEarly{ false }; + int _loadPersistedLayoutIdx{}; std::string _windowTarget{}; // Are you adding more args or attributes here? If they are not reset in _resetStateToDefault, make sure to reset them in FullResetState diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 08032f1c87..72f25b83e1 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -600,13 +600,11 @@ namespace winrt::TerminalApp::implementation winrt::Windows::Foundation::Size proposedSize{}; const float scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); - if (_root->ShouldUsePersistedLayout(_settings)) + if (const auto layout = _root->LoadPersistedLayout(_settings)) { - const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialSize()) + if (layout.InitialSize()) { - proposedSize = layouts.GetAt(0).InitialSize().Value(); + proposedSize = layout.InitialSize().Value(); // The size is saved as a non-scaled real pixel size, // so we need to scale it appropriately. proposedSize.Height = proposedSize.Height * scale; @@ -704,13 +702,11 @@ namespace winrt::TerminalApp::implementation auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; - if (_root->ShouldUsePersistedLayout(_settings)) + if (const auto layout = _root->LoadPersistedLayout(_settings)) { - const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialPosition()) + if (layout.InitialPosition()) { - initialPosition = layouts.GetAt(0).InitialPosition().Value(); + initialPosition = layout.InitialPosition().Value(); } } @@ -1151,10 +1147,22 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - - void AppLogic::WindowCloseButtonClicked() + void AppLogic::CloseWindow(LaunchPosition pos) { if (_root) { + // If persisted layout is enabled and we are the last window closing + // we should save our state. + if (_root->ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(pos); + const auto state = ApplicationState::SharedInstance(); + state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); + } + } + _root->CloseWindow(false); } } @@ -1168,6 +1176,16 @@ namespace winrt::TerminalApp::implementation return {}; } + bool AppLogic::HasCommandlineArguments() const noexcept + { + return _hasCommandLineArguments; + } + + bool AppLogic::HasSettingsStartupActions() const noexcept + { + return _hasSettingsStartupActions; + } + // Method Description: // - Sets the initial commandline to process on startup, and attempts to // parse it. Commands will be parsed into a list of ShortcutActions that @@ -1191,6 +1209,10 @@ namespace winrt::TerminalApp::implementation // then it contains only the executable name and no other arguments. _hasCommandLineArguments = args.size() > 1; _appArgs.ValidateStartupCommands(); + if (const auto idx = _appArgs.GetPersistedLayoutIdx()) + { + _root->SetPersistedLayoutIdx(idx.value()); + } _root->SetStartupActions(_appArgs.GetStartupActions()); // Check if we were started as a COM server for inbound connections of console sessions @@ -1428,6 +1450,40 @@ namespace winrt::TerminalApp::implementation return _settings.GlobalSettings().ActionMap().GlobalHotkeys(); } + bool AppLogic::ShouldUsePersistedLayout() + { + return _root != nullptr ? _root->ShouldUsePersistedLayout(_settings) : false; + } + + void AppLogic::SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts) + { + std::vector converted; + converted.reserve(layouts.Size()); + + for (const auto& json : layouts) + { + if (json != L"") + { + converted.emplace_back(WindowLayout::FromJson(json)); + } + } + + ApplicationState::SharedInstance().PersistedWindowLayouts(winrt::single_threaded_vector(std::move(converted))); + } + + hstring AppLogic::GetWindowLayoutJson(LaunchPosition position) + { + if (_root != nullptr) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(position); + return WindowLayout::ToJson(layout); + } + } + return L""; + } + void AppLogic::IdentifyWindow() { if (_root) @@ -1459,8 +1515,17 @@ namespace winrt::TerminalApp::implementation } } + void AppLogic::SetPersistedLayoutIdx(const uint32_t idx) + { + if (_root) + { + _root->SetPersistedLayoutIdx(idx); + } + } + void AppLogic::SetNumberOfOpenWindows(const uint64_t num) { + _numOpenWindows = num; if (_root) { _root->SetNumberOfOpenWindows(num); diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 08dc770072..17bd61aaa9 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -55,6 +55,8 @@ namespace winrt::TerminalApp::implementation void Quit(); + bool HasCommandlineArguments() const noexcept; + bool HasSettingsStartupActions() const noexcept; int32_t SetStartupCommandline(array_view actions); int32_t ExecuteCommandline(array_view actions, const winrt::hstring& cwd); TerminalApp::FindTargetWindowResult FindTargetWindow(array_view actions); @@ -65,12 +67,16 @@ namespace winrt::TerminalApp::implementation bool Fullscreen() const; bool AlwaysOnTop() const; + bool ShouldUsePersistedLayout(); + hstring GetWindowLayoutJson(Microsoft::Terminal::Settings::Model::LaunchPosition position); + void SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts); void IdentifyWindow(); void RenameFailed(); winrt::hstring WindowName(); void WindowName(const winrt::hstring& name); uint64_t WindowId(); void WindowId(const uint64_t& id); + void SetPersistedLayoutIdx(const uint32_t idx); void SetNumberOfOpenWindows(const uint64_t num); bool IsQuakeWindow() const noexcept; @@ -91,7 +97,7 @@ namespace winrt::TerminalApp::implementation void TitlebarClicked(); bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); - void WindowCloseButtonClicked(); + void CloseWindow(Microsoft::Terminal::Settings::Model::LaunchPosition position); winrt::TerminalApp::TaskbarState TaskbarState(); @@ -123,6 +129,8 @@ namespace winrt::TerminalApp::implementation HRESULT _settingsLoadedResult = S_OK; bool _loadedInitialSettings = false; + uint64_t _numOpenWindows{ 0 }; + std::shared_mutex _dialogLock; ::TerminalApp::AppCommandlineArgs _appArgs; @@ -175,6 +183,7 @@ namespace winrt::TerminalApp::implementation FORWARDED_TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs, _root, RenameWindowRequested); FORWARDED_TYPED_EVENT(IsQuakeWindowChanged, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IsQuakeWindowChanged); FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested); + FORWARDED_TYPED_EVENT(CloseRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, CloseRequested); FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu); FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index a600891c49..cfe93321aa 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -34,6 +34,8 @@ namespace TerminalApp void RunAsUwp(); Boolean IsElevated(); + Boolean HasCommandlineArguments(); + Boolean HasSettingsStartupActions(); Int32 SetStartupCommandline(String[] commands); Int32 ExecuteCommandline(String[] commands, String cwd); String ParseCommandlineMessage { get; }; @@ -55,6 +57,7 @@ namespace TerminalApp void IdentifyWindow(); String WindowName; UInt64 WindowId; + void SetPersistedLayoutIdx(UInt32 idx); void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); @@ -69,10 +72,14 @@ namespace TerminalApp Boolean GetInitialAlwaysOnTop(); Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension); void TitlebarClicked(); - void WindowCloseButtonClicked(); + void CloseWindow(Microsoft.Terminal.Settings.Model.LaunchPosition position); TaskbarState TaskbarState{ get; }; + Boolean ShouldUsePersistedLayout(); + String GetWindowLayoutJson(Microsoft.Terminal.Settings.Model.LaunchPosition position); + void SaveWindowLayoutJsons(Windows.Foundation.Collections.IVector layouts); + Boolean GetMinimizeToNotificationArea(); Boolean GetAlwaysShowNotificationIcon(); Boolean GetShowTitleInTitlebar(); @@ -99,6 +106,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler SettingsChanged; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler CloseRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; event Windows.Foundation.TypedEventHandler QuitRequested; } diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 0c5705a7bb..310d7ca47e 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -106,7 +106,12 @@ NewTerminalArgs Pane::GetTerminalArgsForPane() const if (controlSettings.AppliedColorScheme()) { auto name = controlSettings.AppliedColorScheme().Name(); - args.ColorScheme(name); + // Only save the color scheme if it is different than the profile color + // scheme to not override any other profile appearance choices. + if (_profile.DefaultAppearance().ColorSchemeName() != name) + { + args.ColorScheme(name); + } } return args; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 421816f584..272fa9df6e 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -381,6 +381,9 @@ Launch the window in focus mode + + This parameter is an internal implementation detail and should not be used. + Specify a terminal window to run the given commandline in. "0" always refers to the current window. diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index f57e3fe9bf..e5d1cdc46b 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -501,7 +501,9 @@ namespace winrt::TerminalApp::implementation { // If we are supposed to save state, make sure we clear it out // if the user manually closed all tabs. - if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings)) + // Do this only if we are the last window; the monarch will notice + // we are missing and remove us that way otherwise. + if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1) { auto state = ApplicationState::SharedInstance(); state.PersistedWindowLayouts(nullptr); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 74606ab4f9..25140362de 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -298,10 +298,37 @@ namespace winrt::TerminalApp::implementation // - true if the ApplicationState should be used. bool TerminalPage::ShouldUsePersistedLayout(CascadiaSettings& settings) const { - // If the setting is enabled, and we are the only window. + // GH#5000 Until there is a separate state file for elevated sessions we should just not + // save at all while in an elevated window. return Feature_PersistedWindowLayout::IsEnabled() && - settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout && - _numOpenWindows == 1; + !IsElevated() && + settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout; + } + + // Method Description; + // - Checks if the current window is configured to load a particular layout + // Arguments: + // - settings: The settings to use as this may be called before the page is + // fully initialized. + // Return Value: + // - non-null if there is a particular saved layout to use + std::optional TerminalPage::LoadPersistedLayoutIdx(CascadiaSettings& settings) const + { + return ShouldUsePersistedLayout(settings) ? _loadFromPersistedLayoutIdx : std::nullopt; + } + + WindowLayout TerminalPage::LoadPersistedLayout(CascadiaSettings& settings) const + { + if (const auto idx = LoadPersistedLayoutIdx(settings)) + { + const auto i = idx.value(); + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (layouts && layouts.Size() > i) + { + return layouts.GetAt(i); + } + } + return nullptr; } winrt::fire_and_forget TerminalPage::NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e) @@ -387,30 +414,13 @@ namespace winrt::TerminalApp::implementation { _startupState = StartupState::InStartup; - // If the user selected to save their tab layout, we are the first - // window opened, and wt was not run with any other arguments, then - // we should use the saved settings. - auto firstActionIsDefault = [](ActionAndArgs action) { - if (action.Action() != ShortcutAction::NewTab) - { - return false; - } - - // If no commands were given, we will have default args - if (const auto args = action.Args().try_as()) - { - NewTerminalArgs defaultArgs{}; - return args.TerminalArgs() == nullptr || args.TerminalArgs().Equals(defaultArgs); - } - - return false; - }; - if (ShouldUsePersistedLayout(_settings) && _startupActions.Size() == 1 && firstActionIsDefault(_startupActions.GetAt(0))) + // If we are provided with an index, the cases where we have + // commandline args and startup actions are already handled. + if (const auto layout = LoadPersistedLayout(_settings)) { - auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).TabLayout() && layouts.GetAt(0).TabLayout().Size() > 0) + if (layout.TabLayout().Size() > 0) { - _startupActions = layouts.GetAt(0).TabLayout(); + _startupActions = layout.TabLayout(); } } @@ -1289,12 +1299,19 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Saves the window position and tab layout to the application state + // - This does not create the InitialPosition field, that needs to be + // added externally. // Arguments: // - // Return Value: - // - - void TerminalPage::PersistWindowLayout() + // - the window layout + WindowLayout TerminalPage::GetWindowLayout() { + if (_startupState != StartupState::Initialized) + { + return nullptr; + } + std::vector actions; for (auto tab : _tabs) @@ -1302,7 +1319,7 @@ namespace winrt::TerminalApp::implementation if (auto terminalTab = _GetTerminalTabImpl(tab)) { auto tabActions = terminalTab->BuildStartupActions(); - actions.insert(actions.end(), tabActions.begin(), tabActions.end()); + actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); } else if (tab.try_as()) { @@ -1311,7 +1328,7 @@ namespace winrt::TerminalApp::implementation OpenSettingsArgs args{ SettingsTarget::SettingsUI }; action.Args(args); - actions.push_back(action); + actions.emplace_back(std::move(action)); } } @@ -1324,7 +1341,18 @@ namespace winrt::TerminalApp::implementation SwitchToTabArgs switchToTabArgs{ idx.value() }; action.Args(switchToTabArgs); - actions.push_back(action); + actions.emplace_back(std::move(action)); + } + + // If the user set a custom name, save it + if (_WindowName != L"") + { + ActionAndArgs action; + action.Action(ShortcutAction::RenameWindow); + RenameWindowArgs args{ _WindowName }; + action.Args(args); + + actions.emplace_back(std::move(action)); } WindowLayout layout{}; @@ -1337,33 +1365,7 @@ namespace winrt::TerminalApp::implementation layout.InitialSize(windowSize); - if (_hostingHwnd) - { - // Get the position of the current window. This includes the - // non-client already. - RECT window{}; - GetWindowRect(_hostingHwnd.value(), &window); - - // We want to remove the non-client area so calculate that. - // We don't have access to the (NonClient)IslandWindow directly so - // just replicate the logic. - const auto windowStyle = static_cast(GetWindowLong(_hostingHwnd.value(), GWL_STYLE)); - - auto dpi = GetDpiForWindow(_hostingHwnd.value()); - RECT nonClientArea{}; - LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&nonClientArea, windowStyle, false, 0, dpi)); - - // The nonClientArea adjustment is negative, so subtract that out. - // This way we save the user-visible location of the terminal. - LaunchPosition pos{}; - pos.X = window.left - nonClientArea.left; - pos.Y = window.top; - - layout.InitialPosition(pos); - } - - auto state = ApplicationState::SharedInstance(); - state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); + return layout; } // Method Description: @@ -1392,8 +1394,9 @@ namespace winrt::TerminalApp::implementation if (ShouldUsePersistedLayout(_settings)) { - PersistWindowLayout(); - // don't delete the ApplicationState when all of the tabs are removed. + // Don't delete the ApplicationState when all of the tabs are removed. + // If there is still a monarch living they will get the event that + // a window closed and trigger a new save without this window. _maintainStateOnTabClose = true; } @@ -3106,6 +3109,11 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::SetPersistedLayoutIdx(const uint32_t idx) + { + _loadFromPersistedLayoutIdx = idx; + } + void TerminalPage::SetNumberOfOpenWindows(const uint64_t num) { _numOpenWindows = num; diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index a9ff78959a..8f776a2d7b 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -59,6 +59,9 @@ namespace winrt::TerminalApp::implementation void Create(); bool ShouldUsePersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + std::optional LoadPersistedLayoutIdx(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + winrt::Microsoft::Terminal::Settings::Model::WindowLayout LoadPersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout(); winrt::fire_and_forget NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e); @@ -82,7 +85,6 @@ namespace winrt::TerminalApp::implementation bool AlwaysOnTop() const; void SetStartupActions(std::vector& actions); - void PersistWindowLayout(); void SetInboundListener(bool isEmbedding); static std::vector ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); @@ -111,6 +113,7 @@ namespace winrt::TerminalApp::implementation void WindowId(const uint64_t& value); void SetNumberOfOpenWindows(const uint64_t value); + void SetPersistedLayoutIdx(const uint32_t value); winrt::hstring WindowIdForDisplay() const noexcept; winrt::hstring WindowNameForDisplay() const noexcept; @@ -133,6 +136,7 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs); TYPED_EVENT(IsQuakeWindowChanged, IInspectable, IInspectable); TYPED_EVENT(SummonWindowRequested, IInspectable, IInspectable); + TYPED_EVENT(CloseRequested, IInspectable, IInspectable); TYPED_EVENT(OpenSystemMenu, IInspectable, IInspectable); TYPED_EVENT(QuitRequested, IInspectable, IInspectable); @@ -166,6 +170,7 @@ namespace winrt::TerminalApp::implementation bool _isAlwaysOnTop{ false }; winrt::hstring _WindowName{}; uint64_t _WindowId{ 0 }; + std::optional _loadFromPersistedLayoutIdx{}; uint64_t _numOpenWindows{ 0 }; bool _maintainStateOnTabClose{ false }; diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index dd2777add6..979483d0ad 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -33,7 +33,6 @@ namespace TerminalApp UInt64 WindowId; String WindowNameForDisplay { get; }; String WindowIdForDisplay { get; }; - void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); @@ -58,6 +57,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RenameWindowRequested; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler CloseRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; } } diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 53c75eb6f9..afb6378104 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -450,12 +450,25 @@ namespace winrt::TerminalApp::implementation // 1 for the child after the first split. auto state = _rootPane->BuildStartupActions(0, 1); - ActionAndArgs newTabAction{}; - newTabAction.Action(ShortcutAction::NewTab); - NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() }; - newTabAction.Args(newTabArgs); + { + ActionAndArgs newTabAction{}; + newTabAction.Action(ShortcutAction::NewTab); + NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() }; + newTabAction.Args(newTabArgs); - state.args.emplace(state.args.begin(), std::move(newTabAction)); + state.args.emplace(state.args.begin(), std::move(newTabAction)); + } + + if (_runtimeTabColor) + { + ActionAndArgs setColorAction{}; + setColorAction.Action(ShortcutAction::SetTabColor); + + SetTabColorArgs setColorArgs{ _runtimeTabColor.value() }; + setColorAction.Args(setColorArgs); + + state.args.emplace_back(std::move(setColorAction)); + } // If we only have one arg, we only have 1 pane so we don't need any // special focus logic diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index a21ae08e3b..bf0e85ee83 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -304,8 +304,8 @@ An option to choose from for the "First window preference" setting. Open the default profile. - Open tabs from a previous session - An option to choose from for the "First window preference" setting. Reopen the layout from the last session. + Open windows from a previous session + An option to choose from for the "First window preference" setting. Reopen the layouts from the last session. Launch mode diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 94f7cebea7..74315c2ab4 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -864,6 +864,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct SetTabColorArgs : public SetTabColorArgsT { SetTabColorArgs() = default; + SetTabColorArgs(Windows::UI::Color tabColor) : + _TabColor{ tabColor } {} ACTION_ARG(Windows::Foundation::IReference, TabColor, nullptr); static constexpr std::string_view ColorKey{ "color" }; @@ -1582,6 +1584,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct RenameWindowArgs : public RenameWindowArgsT { RenameWindowArgs() = default; + RenameWindowArgs(winrt::hstring name) : + _Name{ name } {}; ACTION_ARG(winrt::hstring, Name); static constexpr std::string_view NameKey{ "name" }; @@ -1869,9 +1873,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(NewTabArgs); BASIC_FACTORY(MoveFocusArgs); BASIC_FACTORY(MovePaneArgs); + BASIC_FACTORY(SetTabColorArgs); BASIC_FACTORY(SwapPaneArgs); BASIC_FACTORY(SplitPaneArgs); BASIC_FACTORY(SetColorSchemeArgs); + BASIC_FACTORY(RenameWindowArgs); BASIC_FACTORY(ExecuteCommandlineArgs); BASIC_FACTORY(CloseOtherTabsArgs); BASIC_FACTORY(CloseTabsAfterArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 21b1b4b0bc..2ea94e84d0 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -213,6 +213,7 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass SetTabColorArgs : IActionArgs { + SetTabColorArgs(Windows.UI.Color tabColor); Windows.Foundation.IReference TabColor { get; }; }; @@ -294,6 +295,7 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass RenameWindowArgs : IActionArgs { + RenameWindowArgs(String name); String Name { get; }; }; diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp index 5a00ba2b41..acd876884b 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp @@ -60,6 +60,31 @@ using namespace ::Microsoft::Terminal::Settings::Model; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { + winrt::hstring WindowLayout::ToJson(const Model::WindowLayout& layout) + { + JsonUtils::ConversionTrait trait; + auto json = trait.ToJson(layout); + + Json::StreamWriterBuilder wbuilder; + const auto content = Json::writeString(wbuilder, json); + return hstring{ til::u8u16(content) }; + } + + Model::WindowLayout WindowLayout::FromJson(const hstring& str) + { + auto data = til::u16u8(str); + std::string errs; + std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + + Json::Value root; + if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) + { + throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); + } + JsonUtils::ConversionTrait trait; + return trait.FromJson(root); + } + // Returns the application-global ApplicationState object. Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance() { @@ -108,6 +133,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { \ auto state = _state.lock(); \ state->name.emplace(value); \ + state->name##Changed = true; \ } \ \ _throttler(); \ @@ -115,34 +141,50 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN + Json::Value ApplicationState::_getRoot(const locked_hfile& file) const noexcept + { + Json::Value root; + try + { + const auto data = ReadUTF8FileLocked(file); + if (data.empty()) + { + return root; + } + + std::string errs; + std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + + if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) + { + throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); + } + } + CATCH_LOG() + + return root; + } + // Deserializes the state.json at _path into this ApplicationState. // * ANY errors during app state will result in the creation of a new empty state. // * ANY errors during runtime will result in changes being partially ignored. void ApplicationState::_read() const noexcept try { - const auto data = ReadUTF8FileIfExists(_path).value_or(std::string{}); - if (data.empty()) - { - return; - } - - std::string errs; - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; - - Json::Value root; - if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) - { - throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); - } - auto state = _state.lock(); + const auto file = OpenFileReadSharedLocked(_path); + + auto root = _getRoot(file); // GetValueForKey() comes in two variants: // * take a std::optional reference // * return std::optional by value // At the time of writing the former version skips missing fields in the json, // but we want to explicitly clear state fields that were removed from state.json. -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey>(root, key); +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ + if (!state->name##Changed) \ + { \ + state->name = JsonUtils::GetValueForKey>(root, key); \ + } MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN } @@ -152,21 +194,29 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // * Errors are only logged. // * _state->_writeScheduled is set to false, signaling our // setters that _synchronize() needs to be called again. - void ApplicationState::_write() const noexcept + void ApplicationState::_write() noexcept try { - Json::Value root{ Json::objectValue }; - + // re-read the state so that we can only update the properties that were changed. + Json::Value root{}; { - auto state = _state.lock_shared(); -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); + auto state = _state.lock(); + const auto file = OpenFileRWExclusiveLocked(_path); + root = _getRoot(file); + +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ + if (state->name##Changed) \ + { \ + JsonUtils::SetValueForKey(root, key, state->name); \ + state->name##Changed = false; \ + } MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN - } - Json::StreamWriterBuilder wbuilder; - const auto content = Json::writeString(wbuilder, root); - WriteUTF8FileAtomic(_path, content); + Json::StreamWriterBuilder wbuilder; + const auto content = Json::writeString(wbuilder, root); + WriteUTF8FileLocked(file, content); + } } CATCH_LOG() } diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index 71c6a576ee..9b4f40f28f 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -18,6 +18,7 @@ Abstract: #include #include #include +#include "FileUtils.h" #include // This macro generates all getters and setters for ApplicationState. @@ -33,6 +34,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct WindowLayout : WindowLayoutT { + static winrt::hstring ToJson(const Model::WindowLayout& layout); + static Model::WindowLayout FromJson(const winrt::hstring& json); + WINRT_PROPERTY(Windows::Foundation::Collections::IVector, TabLayout, nullptr); WINRT_PROPERTY(winrt::Windows::Foundation::IReference, InitialPosition, nullptr); WINRT_PROPERTY(winrt::Windows::Foundation::IReference, InitialSize, nullptr); @@ -63,12 +67,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation private: struct state_t { -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) std::optional name{ __VA_ARGS__ }; +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ + std::optional name{ __VA_ARGS__ }; \ + bool name##Changed = false; + MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN }; - void _write() const noexcept; + Json::Value _getRoot(const winrt::Microsoft::Terminal::Settings::Model::locked_hfile& file) const noexcept; + void _write() noexcept; void _read() const noexcept; std::filesystem::path _path; diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.idl b/src/cascadia/TerminalSettingsModel/ApplicationState.idl index 91231a112a..119a379793 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.idl +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.idl @@ -15,6 +15,9 @@ namespace Microsoft.Terminal.Settings.Model { WindowLayout(); + static String ToJson(WindowLayout layout); + static WindowLayout FromJson(String json); + Windows.Foundation.Collections.IVector TabLayout; Windows.Foundation.IReference InitialPosition; Windows.Foundation.IReference InitialSize; diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.cpp b/src/cascadia/TerminalSettingsModel/FileUtils.cpp index cf273d5a55..599d29a554 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.cpp +++ b/src/cascadia/TerminalSettingsModel/FileUtils.cpp @@ -39,6 +39,83 @@ namespace winrt::Microsoft::Terminal::Settings::Model return baseSettingsPath; } + locked_hfile OpenFileReadSharedLocked(const std::filesystem::path& path) + { + wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) }; + THROW_LAST_ERROR_IF(!file); + // just lock the entire file + OVERLAPPED sOverlapped; + sOverlapped.Offset = 0; + sOverlapped.OffsetHigh = 0; + // Shared lock + THROW_LAST_ERROR_IF(!LockFileEx(file.get(), + 0, // lock shared, wait to return until lock is obtained + 0, // reserved, does nothing + INT_MAX, // lock INT_MAX bytes + 0, // higher-order bytes, if our state file is greater than 2GB I guess this will be a problem + &sOverlapped)); + return { std::move(file), sOverlapped }; + } + + locked_hfile OpenFileRWExclusiveLocked(const std::filesystem::path& path) + { + wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; + THROW_LAST_ERROR_IF(!file); + // just lock the entire file + OVERLAPPED sOverlapped; + sOverlapped.Offset = 0; + sOverlapped.OffsetHigh = 0; + // Shared lock + THROW_LAST_ERROR_IF(!LockFileEx(file.get(), + LOCKFILE_EXCLUSIVE_LOCK, // lock exclusive, wait to return until lock is obtained + 0, // reserved, does nothing + INT_MAX, // lock INT_MAX bytes + 0, // higher-order bytes, if our state file is greater than 2GB I guess this will be a problem + &sOverlapped)); + return { std::move(file), sOverlapped }; + } + + std::string ReadUTF8FileLocked(const locked_hfile& file) + { + const auto fileSize = GetFileSize(file.get(), nullptr); + THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE); + + // By making our buffer just slightly larger we can detect if + // the file size changed and we've failed to read the full file. + std::string buffer(static_cast(fileSize) + 1, '\0'); + DWORD bytesRead = 0; + THROW_IF_WIN32_BOOL_FALSE(ReadFile(file.get(), buffer.data(), gsl::narrow(buffer.size()), &bytesRead, nullptr)); + + // As mentioned before our buffer was allocated oversized. + buffer.resize(bytesRead); + + if (til::starts_with(buffer, Utf8Bom)) + { + // Yeah this memmove()s the entire content. + // But I don't really want to deal with UTF8 BOMs any more than necessary, + // as basically not a single editor writes a BOM for UTF8. + buffer.erase(0, Utf8Bom.size()); + } + + return buffer; + } + + void WriteUTF8FileLocked(const locked_hfile& file, const std::string_view& content) + { + // truncate the file because we want to overwrite it + SetFilePointer(file.get(), 0, nullptr, FILE_BEGIN); + THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(file.get())); + + const auto fileSize = gsl::narrow(content.size()); + DWORD bytesWritten = 0; + THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), fileSize, &bytesWritten, nullptr)); + + if (bytesWritten != fileSize) + { + THROW_WIN32_MSG(ERROR_WRITE_FAULT, "failed to write whole file"); + } + } + // Tries to read a file somewhat atomically without locking it. // Strips the UTF8 BOM if it exists. std::string ReadUTF8File(const std::filesystem::path& path) diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.h b/src/cascadia/TerminalSettingsModel/FileUtils.h index c003228c37..187051e7cc 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.h +++ b/src/cascadia/TerminalSettingsModel/FileUtils.h @@ -1,9 +1,39 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +#pragma once + namespace winrt::Microsoft::Terminal::Settings::Model { + // I couldn't find a wil helper for this so I made it myself + class locked_hfile + { + public: + wil::unique_hfile file; + OVERLAPPED lockedRegion; + + ~locked_hfile() + { + if (file) + { + // Need to unlock the file before it is closed + UnlockFileEx(file.get(), 0, INT_MAX, 0, &lockedRegion); + } + } + + HANDLE get() const noexcept + { + return file.get(); + } + }; + std::filesystem::path GetBaseSettingsPath(); + + locked_hfile OpenFileReadSharedLocked(const std::filesystem::path& path); + locked_hfile OpenFileRWExclusiveLocked(const std::filesystem::path& path); + std::string ReadUTF8FileLocked(const locked_hfile& file); + void WriteUTF8FileLocked(const locked_hfile& file, const std::string_view& content); + std::string ReadUTF8File(const std::filesystem::path& path); std::optional ReadUTF8FileIfExists(const std::filesystem::path& path); void WriteUTF8File(const std::filesystem::path& path, const std::string_view& content); diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 0a534e5a0a..2112c58a6b 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -76,6 +76,7 @@ namespace RemotingUnitTests void Summon(const Remoting::SummonWindowBehavior& /*args*/) { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void RequestShowNotificationIcon() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void RequestHideNotificationIcon() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; + winrt::hstring GetWindowLayout() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void RequestQuitAll() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void Quit() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, Remoting::WindowActivatedArgs); @@ -88,6 +89,7 @@ namespace RemotingUnitTests TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, Remoting::GetWindowLayoutArgs); }; class RemotingTests diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index e9d9b2416c..c2b8dcda2a 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -20,6 +20,7 @@ using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace ::Microsoft::Console; using namespace ::Microsoft::Console::Types; +using namespace std::chrono_literals; // This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx // "If the high-order bit is 1, the key is down; otherwise, it is up." @@ -29,7 +30,8 @@ AppHost::AppHost() noexcept : _app{}, _windowManager{}, _logic{ nullptr }, // don't make one, we're going to take a ref on app's - _window{ nullptr } + _window{ nullptr }, + _getWindowLayoutThrottler{} // this will get set if we become the monarch { _logic = _app.Logic(); // get a ref to app's logic @@ -84,6 +86,12 @@ AppHost::AppHost() noexcept : _window->SetAlwaysOnTop(_logic.GetInitialAlwaysOnTop()); _window->MakeWindow(); + _windowManager.GetWindowLayoutRequested([this](auto&&, const winrt::Microsoft::Terminal::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()); + }); + _windowManager.BecameMonarch({ this, &AppHost::_BecomeMonarch }); if (_windowManager.IsMonarch()) { @@ -220,7 +228,47 @@ void AppHost::_HandleCommandlineArgs() // is created. if (_windowManager.IsMonarch()) { - _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); + const auto numPeasants = _windowManager.GetNumberOfPeasants(); + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (_logic.ShouldUsePersistedLayout() && layouts && layouts.Size() > 0) + { + uint32_t startIdx = 0; + // We want to create a window for every saved layout. + // If we are the only window, and no commandline arguments were provided + // then we should just use the current window to load the first layout. + // 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 && !_logic.HasCommandlineArguments() && !_logic.HasSettingsStartupActions()) + { + _logic.SetPersistedLayoutIdx(startIdx); + startIdx += 1; + } + + // 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); + + STARTUPINFO si; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + wil::unique_process_information pi; + + LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr, + newWindowArgs.data(), + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + false, // bInheritHandles + DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT, // doCreationFlags + nullptr, // lpEnvironment + nullptr, // lpStartingDirectory + &si, // lpStartupInfo + &pi // lpProcessInformation + )); + } + } + _logic.SetNumberOfOpenWindows(numPeasants); } _logic.WindowName(peasant.WindowName()); _logic.WindowId(peasant.GetID()); @@ -257,7 +305,16 @@ void AppHost::Initialize() // Register the 'X' button of the window for a warning experience of multiple // tabs opened, this is consistent with Alt+F4 closing - _window->WindowCloseButtonClicked([this]() { _logic.WindowCloseButtonClicked(); }); + _window->WindowCloseButtonClicked([this]() { + const auto pos = _GetWindowLaunchPosition(); + _logic.CloseWindow(pos); + }); + // If the user requests a close in another way handle the same as if the 'X' + // was clicked. + _logic.CloseRequested([this](auto&&, auto&&) { + const auto pos = _GetWindowLaunchPosition(); + _logic.CloseWindow(pos); + }); // Add an event handler to plumb clicks in the titlebar area down to the // application layer. @@ -347,6 +404,24 @@ void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*se _window->Close(); } +LaunchPosition AppHost::_GetWindowLaunchPosition() +{ + // Get the position of the current window. This includes the + // non-client already. + const auto window = _window->GetWindowRect(); + + const auto dpi = _window->GetCurrentDpi(); + const auto nonClientArea = _window->GetNonClientFrame(dpi); + + // The nonClientArea adjustment is negative, so subtract that out. + // This way we save the user-visible location of the terminal. + LaunchPosition pos{}; + pos.X = window.left - nonClientArea.left; + pos.Y = window.top; + + return pos; +} + // Method Description: // - Resize the window we're about to create to the appropriate dimensions, as // specified in the settings. This will be called during the handling of @@ -634,6 +709,31 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send _logic.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; + + // Use the main thread since we are accessing controls. + co_await winrt::resume_foreground(_logic.GetRoot().Dispatcher()); + const auto pos = _GetWindowLaunchPosition(); + const auto layoutJson = _logic.GetWindowLayoutJson(pos); + + // 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 @@ -687,8 +787,13 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s // and subscribe for updates if there are any changes to that number. _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); - _windowManager.WindowCreated([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); - _windowManager.WindowClosed([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); + _windowManager.WindowCreated([this](auto&&, auto&&) { + _getWindowLayoutThrottler.value()(); + _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); + _windowManager.WindowClosed([this](auto&&, auto&&) { + _getWindowLayoutThrottler.value()(); + _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); + }); // These events are coming from peasants that become or un-become quake windows. _windowManager.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequested(); }); @@ -696,6 +801,48 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s // If the monarch receives a QuitAll event it will signal this event to be // ran before each peasant is closed. _windowManager.QuitAllRequested({ 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 (_logic.ShouldUsePersistedLayout()) + { + const auto layoutJsons = _windowManager.GetAllWindowLayouts(); + _logic.SaveWindowLayoutJsons(layoutJsons); + } + + 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 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()) + { + _getWindowLayoutThrottler.value()(); + } } void AppHost::_listenForInboundConnections() @@ -1046,10 +1193,18 @@ void AppHost::_RequestQuitAll(const winrt::Windows::Foundation::IInspectable&, } void AppHost::_QuitAllRequested(const winrt::Windows::Foundation::IInspectable&, - const winrt::Windows::Foundation::IInspectable&) + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args) { - // TODO: GH#9800: For now, nothing needs to be done before the monarch closes all windows. - // Later when we have state saving that should go here. + // 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()); } void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index db8a0bf1b3..d51ba227a8 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -4,6 +4,7 @@ #include "pch.h" #include "NonClientIslandWindow.h" #include "NotificationIcon.h" +#include class AppHost { @@ -31,7 +32,12 @@ private: bool _shouldCreateWindow{ false }; bool _useNonClientArea{ false }; + std::optional> _getWindowLayoutThrottler; + winrt::Windows::Foundation::IAsyncAction _SaveWindowLayouts(); + winrt::fire_and_forget _SaveWindowLayoutsRepeat(); + void _HandleCommandlineArgs(); + winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition(); void _HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); void _UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable& sender, @@ -53,6 +59,8 @@ private: void _DispatchCommandline(winrt::Windows::Foundation::IInspectable sender, winrt::Microsoft::Terminal::Remoting::CommandlineArgs args); + winrt::Windows::Foundation::IAsyncOperation _GetWindowLayoutAsync(); + void _FindTargetWindow(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args); @@ -95,7 +103,7 @@ private: const winrt::Windows::Foundation::IInspectable& args); void _QuitAllRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args); void _CreateNotificationIcon(); void _DestroyNotificationIcon(); diff --git a/src/cascadia/WindowsTerminal/BaseWindow.h b/src/cascadia/WindowsTerminal/BaseWindow.h index 581b386172..167b309bb7 100644 --- a/src/cascadia/WindowsTerminal/BaseWindow.h +++ b/src/cascadia/WindowsTerminal/BaseWindow.h @@ -133,6 +133,11 @@ public: return _window.get(); } + UINT GetCurrentDpi() const noexcept + { + return ::GetDpiForWindow(_window.get()); + } + float GetCurrentDpiScale() const noexcept { const auto dpi = ::GetDpiForWindow(_window.get());