From 13e9546babd5c729c80f46788b2d059dcab3487f Mon Sep 17 00:00:00 2001 From: Schuyler Rosefield Date: Wed, 8 Sep 2021 18:44:53 -0400 Subject: [PATCH] Persist window layout on window close (#10972) This commit adds initial support for saving window layout on application close. Done: - Add user setting for if tabs should be maintained. - Added events to track the number of open windows for the monarch, and then save if you are the last window closing. - Saves layout when the user explicitly hits the "Close Window" button. - If the user manually closed all of their tabs (through the tab x button or through closing all panes on the tab) then remove any saved state. - Saves in the ApplicationState file a list of actions the terminal can perform to restore its layout and the window size/position information. - This saves an action to focus the correct pane, but this won't actually work without #10978. Note that if you have a pane zoomed, it does still zoom the correct pane, but when you unzoom it will have a different pane selected. Todo: - multiple windows? Right now it can only handle loading/saving one window. - PR #11083 will save multiple windows. - This also sometimes runs into the existing bug where multiple tabs appear to be focused on opening. Next Steps: - The business logic of when the save is triggered can be adjusted as necessary. - Right now I am taking the pragmatic approach and just saving the state as an array of objects, but only ever populate it with 1, that way saving multiple windows in the future could be added without breaking schema compatibility. Selfishly I'm hoping that handling multiple windows could be spun off into another pr/feature for now. - One possible thing that can maybe be done is that the commandline can be augmented with a "--saved ##" attribute that would load from the nth saved state if it exists. e.g. if there are 3 saved windows, on first load it can spawn three wt --saved {0,1,2} that would reopen the windows? This way there also exists a way to load a copy of a previous window (if it is in the saved state). - Is the application state something that is planned to be public/user editable? In theory the user could since it is just json, but I don't know what it buys them over just modifying their settings and startupActions. Validation Steps Performed: - The happy path: open terminal -> set setting to true -> close terminal -> reopen and see tabs. Tested with powershell/cmd/wsl windows. - That closing all panes/tabs on their own will remove the saved session. - Open multiple windows, close windows and confirm that the last window closed saves its state. The generated file stores a sequence of actions that will be executed to restore the terminal to its saved form. References #8324 This is also one of the items on microsoft/terminal#5000 Closes #766 --- doc/cascadia/profiles.schema.json | 9 + src/cascadia/Remoting/Monarch.cpp | 41 +++ src/cascadia/Remoting/Monarch.h | 5 + src/cascadia/Remoting/Monarch.idl | 4 + src/cascadia/Remoting/WindowManager.cpp | 30 +- src/cascadia/Remoting/WindowManager.h | 4 + src/cascadia/Remoting/WindowManager.idl | 4 + src/cascadia/TerminalApp/AppLogic.cpp | 47 ++- src/cascadia/TerminalApp/AppLogic.h | 1 + src/cascadia/TerminalApp/AppLogic.idl | 1 + src/cascadia/TerminalApp/Pane.cpp | 148 ++++++++ src/cascadia/TerminalApp/Pane.h | 10 + src/cascadia/TerminalApp/TabManagement.cpp | 8 + src/cascadia/TerminalApp/TerminalPage.cpp | 134 ++++++++ src/cascadia/TerminalApp/TerminalPage.h | 15 +- src/cascadia/TerminalApp/TerminalPage.idl | 1 + src/cascadia/TerminalApp/TerminalTab.cpp | 44 +++ src/cascadia/TerminalApp/TerminalTab.h | 2 + .../TerminalSettingsEditor/Launch.cpp | 6 + src/cascadia/TerminalSettingsEditor/Launch.h | 3 + .../TerminalSettingsEditor/Launch.idl | 6 + .../TerminalSettingsEditor/Launch.xaml | 9 + .../Resources/en-US/Resources.resw | 16 + .../TerminalSettingsModel/ActionArgs.h | 21 +- .../ApplicationState.cpp | 49 ++- .../TerminalSettingsModel/ApplicationState.h | 21 +- .../ApplicationState.idl | 13 + .../TerminalSettingsModel/ColorScheme.idl | 1 + .../TerminalSettingsModel/EnumMappings.cpp | 1 + .../TerminalSettingsModel/EnumMappings.h | 1 + .../TerminalSettingsModel/EnumMappings.idl | 1 + .../GlobalAppSettings.cpp | 5 + .../TerminalSettingsModel/GlobalAppSettings.h | 1 + .../GlobalAppSettings.idl | 7 + .../TerminalSettingsModel/JsonUtils.h | 325 +++++++++++------- .../TerminalSettings.cpp | 2 + .../TerminalSettingsModel/TerminalSettings.h | 1 + .../TerminalSettings.idl | 2 + .../TerminalSettingsSerializationHelpers.h | 11 +- src/cascadia/WindowsTerminal/AppHost.cpp | 14 + src/features.xml | 9 + 41 files changed, 865 insertions(+), 168 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 6bba7b165de..15bc6260288 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1231,6 +1231,15 @@ "description": "When set to true, this enables the launch of Windows Terminal at startup. Setting this to false will disable the startup task entry. If the Windows Terminal startup task entry is disabled either by org policy or by user action this setting will have no effect.", "type": "boolean" }, + "firstWindowPreference": { + "default": "defaultProfile", + "description": "Defines what behavior the terminal takes when it starts. \"defaultProfile\" will have the terminal launch with one tab of the default profile, and \"persistedWindowLayout\" will cause the terminal to save its layout on close and reload it on open.", + "enum": [ + "defaultProfile", + "persistedWindowLayout" + ], + "type": "string" + }, "launchMode": { "default": "default", "description": "Defines whether the terminal will launch as maximized, full screen, or in a window. Setting this to \"focus\" is equivalent to launching the terminal in the \"default\" mode, but with the focus mode enabled. Similar, setting this to \"maximizedFocus\" will result in launching the terminal in a maximized window with the focus mode enabled.", diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index 7a7e3625c8e..d6a12c8c39d 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -91,6 +91,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingUInt64(newPeasantsId, "peasantID", "the ID of the new peasant"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + _WindowCreatedHandlers(nullptr, nullptr); return newPeasantsId; } catch (...) @@ -107,6 +109,45 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } + // Method Description: + // - Tells the monarch that a peasant is being closed. + // Arguments: + // - peasantId: the id of the peasant + // Return Value: + // - + void Monarch::SignalClose(const uint64_t peasantId) + { + _peasants.erase(peasantId); + _WindowClosedHandlers(nullptr, nullptr); + } + + // Method Description: + // - Counts the number of living peasants. + // Arguments: + // - + // Return Value: + // - the number of active peasants. + uint64_t Monarch::GetNumberOfPeasants() + { + auto num = 0; + auto callback = [&](const auto& /*id*/, const auto& p) { + // Check that the peasant is alive, and if so increment the count + p.GetID(); + num += 1; + }; + auto onError = [](const auto& id) { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_GetNumberOfPeasants_Failed", + TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not enumerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + }; + + _forEachPeasant(callback, onError); + + return num; + } + // Method Description: // - Event handler for the Peasant::WindowActivated event. Used as an // opportunity for us to update our internal stack of the "most recent diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 80ee9cd5721..6be69975eed 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -47,6 +47,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation uint64_t GetPID(); uint64_t AddPeasant(winrt::Microsoft::Terminal::Remoting::IPeasant peasant); + void SignalClose(const uint64_t peasantId); + + uint64_t GetNumberOfPeasants(); winrt::Microsoft::Terminal::Remoting::ProposeCommandlineResult ProposeCommandline(const winrt::Microsoft::Terminal::Remoting::CommandlineArgs& args); void HandleActivatePeasant(const winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs& args); @@ -59,6 +62,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideTrayIconRequested, 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); private: uint64_t _ourPID; diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index 21a1670ac99..eab7eaa92f3 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -43,9 +43,11 @@ namespace Microsoft.Terminal.Remoting UInt64 GetPID(); UInt64 AddPeasant(IPeasant peasant); + UInt64 GetNumberOfPeasants(); ProposeCommandlineResult ProposeCommandline(CommandlineArgs args); void HandleActivatePeasant(WindowActivatedArgs args); void SummonWindow(SummonWindowSelectionArgs args); + void SignalClose(UInt64 peasantId); void SummonAllWindows(); Boolean DoesQuakeWindowExist(); @@ -54,5 +56,7 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; event Windows.Foundation.TypedEventHandler HideTrayIconRequested; + event Windows.Foundation.TypedEventHandler WindowCreated; + event Windows.Foundation.TypedEventHandler WindowClosed; }; } diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index e0c4585c090..668178892b1 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -54,6 +54,7 @@ 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 @@ -64,6 +65,18 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } + void WindowManager::SignalClose() + { + if (_monarch) + { + try + { + _monarch.SignalClose(_peasant.GetID()); + } + CATCH_LOG() + } + } + void WindowManager::ProposeCommandline(const Remoting::CommandlineArgs& args) { // If we're the king, we _definitely_ want to process the arguments, we were @@ -250,9 +263,11 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // 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 + // 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.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequestedHandlers(*this, nullptr); }); _monarch.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequestedHandlers(*this, nullptr); }); @@ -526,6 +541,19 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation return _monarch.GetPeasantInfos(); } + uint64_t WindowManager::GetNumberOfPeasants() + { + if (_monarch) + { + try + { + return _monarch.GetNumberOfPeasants(); + } + CATCH_LOG() + } + return 0; + } + // Method Description: // - Ask the monarch to show a tray icon. // Arguments: diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 88a423b6d8d..8a4724bfb9f 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -39,8 +39,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::Microsoft::Terminal::Remoting::Peasant CurrentWindow(); bool IsMonarch(); void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); + void SignalClose(); void SummonAllWindows(); + uint64_t GetNumberOfPeasants(); Windows::Foundation::Collections::IVectorView GetPeasantInfos(); winrt::fire_and_forget RequestShowTrayIcon(); @@ -50,6 +52,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation 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(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index dc15f9b6ec4..7d287146b01 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -8,6 +8,7 @@ namespace Microsoft.Terminal.Remoting { WindowManager(); void ProposeCommandline(CommandlineArgs args); + void SignalClose(); Boolean ShouldCreateWindow { get; }; IPeasant CurrentWindow(); Boolean IsMonarch { get; }; @@ -15,11 +16,14 @@ namespace Microsoft.Terminal.Remoting void SummonAllWindows(); void RequestShowTrayIcon(); void RequestHideTrayIcon(); + UInt64 GetNumberOfPeasants(); 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 ShowTrayIconRequested; event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 22993369072..6680fd75bb7 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -596,12 +596,30 @@ namespace winrt::TerminalApp::implementation LoadSettings(); } - // Use the default profile to determine how big of a window we need. - const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, nullptr, nullptr) }; - - auto proposedSize = TermControl::GetProposedDimensions(settings.DefaultSettings(), dpi); + winrt::Windows::Foundation::Size proposedSize{}; const float scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); + if (_root->ShouldUsePersistedLayout(_settings)) + { + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + + if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialSize()) + { + proposedSize = layouts.GetAt(0).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; + proposedSize.Width = proposedSize.Width * scale; + } + } + + if (proposedSize.Width == 0 && proposedSize.Height == 0) + { + // Use the default profile to determine how big of a window we need. + const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, nullptr, nullptr) }; + + proposedSize = TermControl::GetProposedDimensions(settings.DefaultSettings(), dpi); + } // GH#2061 - If the global setting "Always show tab bar" is // set or if "Show tabs in title bar" is set, then we'll need to add @@ -683,7 +701,18 @@ namespace winrt::TerminalApp::implementation LoadSettings(); } - const auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; + auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; + + if (_root->ShouldUsePersistedLayout(_settings)) + { + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + + if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialPosition()) + { + initialPosition = layouts.GetAt(0).InitialPosition().Value(); + } + } + return { initialPosition.X ? initialPosition.X.Value() : defaultInitialX, initialPosition.Y ? initialPosition.Y.Value() : defaultInitialY @@ -1429,6 +1458,14 @@ namespace winrt::TerminalApp::implementation } } + void AppLogic::SetNumberOfOpenWindows(const uint64_t num) + { + if (_root) + { + _root->SetNumberOfOpenWindows(num); + } + } + void AppLogic::RenameFailed() { if (_root) diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 86d95c8b58d..3cfbd378614 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -69,6 +69,7 @@ namespace winrt::TerminalApp::implementation void WindowName(const winrt::hstring& name); uint64_t WindowId(); void WindowId(const uint64_t& id); + void SetNumberOfOpenWindows(const uint64_t num); bool IsQuakeWindow() const noexcept; Windows::Foundation::Size GetLaunchDimensions(uint32_t dpi); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index 028022b45b1..7f0867048a5 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -53,6 +53,7 @@ namespace TerminalApp void IdentifyWindow(); String WindowName; UInt64 WindowId; + void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 814869c4d40..0898f0613eb 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -71,6 +71,154 @@ Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFo }); } +// Method Description: +// - Extract the terminal settings from the current (leaf) pane's control +// to be used to create an equivalent control +// Arguments: +// - +// Return Value: +// - Arguments appropriate for a SplitPane or NewTab action +NewTerminalArgs Pane::GetTerminalArgsForPane() const +{ + // Leaves are the only things that have controls + assert(_IsLeaf()); + + NewTerminalArgs args{}; + auto controlSettings = _control.Settings().as(); + + args.Profile(controlSettings.ProfileName()); + args.StartingDirectory(controlSettings.StartingDirectory()); + args.TabTitle(controlSettings.StartingTitle()); + args.Commandline(controlSettings.Commandline()); + args.SuppressApplicationTitle(controlSettings.SuppressApplicationTitle()); + if (controlSettings.TabColor() || controlSettings.StartingTabColor()) + { + til::color c; + // StartingTabColor is prioritized over other colors + if (const auto color = controlSettings.StartingTabColor()) + { + c = til::color(color.Value()); + } + else + { + c = til::color(controlSettings.TabColor().Value()); + } + + args.TabColor(winrt::Windows::Foundation::IReference(c)); + } + + if (controlSettings.AppliedColorScheme()) + { + auto name = controlSettings.AppliedColorScheme().Name(); + args.ColorScheme(name); + } + + return args; +} + +// Method Description: +// - Serializes the state of this tab as a series of commands that can be +// executed to recreate it. +// - This will always result in the right-most child being the focus +// after the commands finish executing. +// Arguments: +// - currentId: the id to use for the current/first pane +// - nextId: the id to use for a new pane if we split +// Return Value: +// - The state from building the startup actions, includes a vector of commands, +// the original root pane, the id of the focused pane, and the number of panes +// created. +Pane::BuildStartupState Pane::BuildStartupActions(uint32_t currentId, uint32_t nextId) +{ + // if we are a leaf then all there is to do is defer to the parent. + if (_IsLeaf()) + { + if (_lastActive) + { + return { {}, shared_from_this(), currentId, 0 }; + } + + return { {}, shared_from_this(), std::nullopt, 0 }; + } + + auto buildSplitPane = [&](auto newPane) { + ActionAndArgs actionAndArgs; + actionAndArgs.Action(ShortcutAction::SplitPane); + const auto terminalArgs{ newPane->GetTerminalArgsForPane() }; + // When creating a pane the split size is the size of the new pane + // and not position. + SplitPaneArgs args{ SplitType::Manual, _splitState, 1. - _desiredSplitPosition, terminalArgs }; + actionAndArgs.Args(args); + + return actionAndArgs; + }; + + auto buildMoveFocus = [](auto direction) { + MoveFocusArgs args{ direction }; + + ActionAndArgs actionAndArgs{}; + actionAndArgs.Action(ShortcutAction::MoveFocus); + actionAndArgs.Args(args); + + return actionAndArgs; + }; + + // Handle simple case of a single split (a minor optimization for clarity) + // Here we just create the second child (by splitting) and return the first + // child for the parent to deal with. + if (_firstChild->_IsLeaf() && _secondChild->_IsLeaf()) + { + auto actionAndArgs = buildSplitPane(_secondChild); + std::optional focusedPaneId = std::nullopt; + if (_firstChild->_lastActive) + { + focusedPaneId = currentId; + } + else if (_secondChild->_lastActive) + { + focusedPaneId = nextId; + } + + return { { actionAndArgs }, _firstChild, focusedPaneId, 1 }; + } + + // We now need to execute the commands for each side of the tree + // We've done one split, so the first-most child will have currentId, and the + // one after it will be incremented. + auto firstState = _firstChild->BuildStartupActions(currentId, nextId + 1); + // the next id for the second branch depends on how many splits were in the + // first child. + auto secondState = _secondChild->BuildStartupActions(nextId, nextId + firstState.panesCreated + 1); + + std::vector actions{}; + actions.reserve(firstState.args.size() + secondState.args.size() + 3); + + // first we make our split + const auto newSplit = buildSplitPane(secondState.firstPane); + actions.emplace_back(std::move(newSplit)); + + if (firstState.args.size() > 0) + { + // Then move to the first child and execute any actions on the left branch + // then move back + actions.emplace_back(buildMoveFocus(FocusDirection::PreviousInOrder)); + actions.insert(actions.end(), std::make_move_iterator(std::begin(firstState.args)), std::make_move_iterator(std::end(firstState.args))); + actions.emplace_back(buildMoveFocus(FocusDirection::NextInOrder)); + } + + // And if there are any commands to run on the right branch do so + if (secondState.args.size() > 0) + { + actions.insert(actions.end(), std::make_move_iterator(secondState.args.begin()), std::make_move_iterator(secondState.args.end())); + } + + // if the tree is well-formed then f1.has_value and f2.has_value are + // mutually exclusive. + const auto focusedPaneId = firstState.focusedPaneId.has_value() ? firstState.focusedPaneId : secondState.focusedPaneId; + + return { actions, firstState.firstPane, focusedPaneId, firstState.panesCreated + secondState.panesCreated + 1 }; +} + // Method Description: // - Update the size of this pane. Resizes each of our columns so they have the // same relative sizes, given the newSize. diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 77a891b2692..35126c614ad 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -70,6 +70,16 @@ class Pane : public std::enable_shared_from_this void ClearActive(); void SetActive(); + struct BuildStartupState + { + std::vector args; + std::shared_ptr firstPane; + std::optional focusedPaneId; + uint32_t panesCreated; + }; + BuildStartupState BuildStartupActions(uint32_t currentId, uint32_t nextId); + winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs GetTerminalArgsForPane() const; + void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); void ResizeContent(const winrt::Windows::Foundation::Size& newSize); diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 50610264cdc..31c9fb312a6 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -499,6 +499,14 @@ namespace winrt::TerminalApp::implementation // To close the window here, we need to close the hosting window. if (_tabs.Size() == 0) { + // If we are supposed to save state, make sure we clear it out + // if the user manually closed all tabs. + if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings)) + { + auto state = ApplicationState::SharedInstance(); + state.PersistedWindowLayouts(nullptr); + } + _LastTabClosedHandlers(*this, nullptr); } else if (focusedTabIndex.has_value() && focusedTabIndex.value() == gsl::narrow_cast(tabIndex)) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 32955c9a5bc..d63684ed9a8 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -276,6 +276,21 @@ namespace winrt::TerminalApp::implementation CATCH_LOG(); } + // Method Description; + // - Checks if the current terminal window should load or save its layout information. + // Arguments: + // - settings: The settings to use as this may be called before the page is + // fully initialized. + // Return Value: + // - true if the ApplicationState should be used. + bool TerminalPage::ShouldUsePersistedLayout(CascadiaSettings& settings) const + { + // If the setting is enabled, and we are the only window. + return Feature_PersistedWindowLayout::IsEnabled() && + settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout && + _numOpenWindows == 1; + } + winrt::fire_and_forget TerminalPage::NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e) { Windows::Foundation::Collections::IVectorView items; @@ -358,6 +373,34 @@ namespace winrt::TerminalApp::implementation if (_startupState == StartupState::NotInitialized) { _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))) + { + auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (layouts && layouts.Size() > 0 && layouts.GetAt(0).TabLayout() && layouts.GetAt(0).TabLayout().Size() > 0) + { + _startupActions = layouts.GetAt(0).TabLayout(); + } + } + ProcessStartupActions(_startupActions, true); // If we were told that the COM server needs to be started to listen for incoming @@ -1195,6 +1238,85 @@ namespace winrt::TerminalApp::implementation return nullptr; } + // Method Description: + // - Saves the window position and tab layout to the application state + // Arguments: + // - + // Return Value: + // - + void TerminalPage::PersistWindowLayout() + { + std::vector actions; + + for (auto tab : _tabs) + { + if (auto terminalTab = _GetTerminalTabImpl(tab)) + { + auto tabActions = terminalTab->BuildStartupActions(); + actions.insert(actions.end(), tabActions.begin(), tabActions.end()); + } + else if (tab.try_as()) + { + ActionAndArgs action; + action.Action(ShortcutAction::OpenSettings); + OpenSettingsArgs args{ SettingsTarget::SettingsUI }; + action.Args(args); + + actions.push_back(action); + } + } + + // if the focused tab was not the last tab, restore that + auto idx = _GetFocusedTabIndex(); + if (idx && idx != _tabs.Size() - 1) + { + ActionAndArgs action; + action.Action(ShortcutAction::SwitchToTab); + SwitchToTabArgs switchToTabArgs{ idx.value() }; + action.Args(switchToTabArgs); + + actions.push_back(action); + } + + WindowLayout layout{}; + layout.TabLayout(winrt::single_threaded_vector(std::move(actions))); + + // Only save the content size because the tab size will be added on load. + const float contentWidth = ::base::saturated_cast(_tabContent.ActualWidth()); + const float contentHeight = ::base::saturated_cast(_tabContent.ActualHeight()); + const winrt::Windows::Foundation::Size windowSize{ contentWidth, contentHeight }; + + 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 })); + } + // Method Description: // - Close the terminal app. If there is more // than one tab opened, show a warning dialog. @@ -1214,6 +1336,13 @@ namespace winrt::TerminalApp::implementation } } + if (ShouldUsePersistedLayout(_settings)) + { + PersistWindowLayout(); + // don't delete the ApplicationState when all of the tabs are removed. + _maintainStateOnTabClose = true; + } + _RemoveAllTabs(); } @@ -2932,6 +3061,11 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::SetNumberOfOpenWindows(const uint64_t num) + { + _numOpenWindows = num; + } + // Method Description: // - Returns a label like "Window: 1234" for the ID of this window // Arguments: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index d598d095556..6451e5158a2 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -58,6 +58,8 @@ namespace winrt::TerminalApp::implementation void Create(); + bool ShouldUsePersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + winrt::fire_and_forget NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e); hstring Title(); @@ -79,6 +81,8 @@ 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); @@ -104,6 +108,9 @@ namespace winrt::TerminalApp::implementation winrt::fire_and_forget WindowName(const winrt::hstring& value); uint64_t WindowId() const noexcept; void WindowId(const uint64_t& value); + + void SetNumberOfOpenWindows(const uint64_t value); + winrt::hstring WindowIdForDisplay() const noexcept; winrt::hstring WindowNameForDisplay() const noexcept; bool IsQuakeWindow() const noexcept; @@ -155,10 +162,12 @@ namespace winrt::TerminalApp::implementation bool _isAlwaysOnTop{ false }; winrt::hstring _WindowName{}; uint64_t _WindowId{ 0 }; + uint64_t _numOpenWindows{ 0 }; - bool _rearranging; - std::optional _rearrangeFrom; - std::optional _rearrangeTo; + bool _maintainStateOnTabClose{ false }; + bool _rearranging{ false }; + std::optional _rearrangeFrom{}; + std::optional _rearrangeTo{}; bool _removing{ false }; uint32_t _systemRowsToScroll{ DefaultRowsToScroll }; diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 7d4c580e9f3..a57b1aada15 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -33,6 +33,7 @@ namespace TerminalApp UInt64 WindowId; String WindowNameForDisplay { get; }; String WindowIdForDisplay { get; }; + void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 99865cbbb9e..5245cb985ef 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -437,6 +437,50 @@ namespace winrt::TerminalApp::implementation control.ScrollViewport(::base::ClampAdd(currentOffset, delta)); } + // Method Description: + // - Serializes the state of this tab as a series of commands that can be + // executed to recreate it. + // Arguments: + // - + // Return Value: + // - A vector of commands + std::vector TerminalTab::BuildStartupActions() const + { + // Give initial ids (0 for the child created with this tab, + // 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); + + state.args.emplace(state.args.begin(), std::move(newTabAction)); + + // If we only have one arg, we only have 1 pane so we don't need any + // special focus logic + if (state.args.size() > 1 && state.focusedPaneId.has_value()) + { + ActionAndArgs focusPaneAction{}; + focusPaneAction.Action(ShortcutAction::FocusPane); + FocusPaneArgs focusArgs{ state.focusedPaneId.value() }; + focusPaneAction.Args(focusArgs); + + state.args.emplace_back(std::move(focusPaneAction)); + } + + if (_zoomedPane) + { + // we start without any panes zoomed so toggle zoom will enable zoom. + ActionAndArgs zoomPaneAction{}; + zoomPaneAction.Action(ShortcutAction::TogglePaneZoom); + + state.args.emplace_back(std::move(zoomPaneAction)); + } + + return state.args; + } + // Method Description: // - Split the focused pane in our tree of panes, and place the // given TermControl into the newly created pane. diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 2ca51f1c571..5bc0e62ed9b 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -85,6 +85,8 @@ namespace winrt::TerminalApp::implementation void EnterZoom(); void ExitZoom(); + std::vector BuildStartupActions() const; + int GetLeafPaneCount() const noexcept; void TogglePaneReadOnly(); diff --git a/src/cascadia/TerminalSettingsEditor/Launch.cpp b/src/cascadia/TerminalSettingsEditor/Launch.cpp index b881b4e0012..09215561de8 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.cpp +++ b/src/cascadia/TerminalSettingsEditor/Launch.cpp @@ -17,6 +17,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { InitializeComponent(); + INITIALIZE_BINDABLE_ENUM_SETTING(FirstWindowPreference, FirstWindowPreference, FirstWindowPreference, L"Globals_FirstWindowPreference", L"Content"); INITIALIZE_BINDABLE_ENUM_SETTING(LaunchMode, LaunchMode, LaunchMode, L"Globals_LaunchMode", L"Content"); INITIALIZE_BINDABLE_ENUM_SETTING(WindowingBehavior, WindowingMode, WindowingMode, L"Globals_WindowingBehavior", L"Content"); @@ -68,4 +69,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation return winrt::single_threaded_observable_vector(std::move(profiles)); } + + bool Launch::ShowFirstWindowPreference() const noexcept + { + return Feature_PersistedWindowLayout::IsEnabled(); + } } diff --git a/src/cascadia/TerminalSettingsEditor/Launch.h b/src/cascadia/TerminalSettingsEditor/Launch.h index 82fed6e419c..baf110c52e4 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.h +++ b/src/cascadia/TerminalSettingsEditor/Launch.h @@ -29,8 +29,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void CurrentDefaultProfile(const IInspectable& value); winrt::Windows::Foundation::Collections::IObservableVector DefaultProfiles() const; + bool ShowFirstWindowPreference() const noexcept; + WINRT_PROPERTY(Editor::LaunchPageNavigationState, State, nullptr); + GETSET_BINDABLE_ENUM_SETTING(FirstWindowPreference, Model::FirstWindowPreference, State().Settings().GlobalSettings, FirstWindowPreference); GETSET_BINDABLE_ENUM_SETTING(LaunchMode, Model::LaunchMode, State().Settings().GlobalSettings, LaunchMode); GETSET_BINDABLE_ENUM_SETTING(WindowingBehavior, Model::WindowingMode, State().Settings().GlobalSettings, WindowingBehavior); }; diff --git a/src/cascadia/TerminalSettingsEditor/Launch.idl b/src/cascadia/TerminalSettingsEditor/Launch.idl index a5b132eb238..1dfc9e86bb5 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.idl +++ b/src/cascadia/TerminalSettingsEditor/Launch.idl @@ -20,6 +20,12 @@ namespace Microsoft.Terminal.Settings.Editor // https://github.com/microsoft/microsoft-ui-xaml/issues/5395 IObservableVector DefaultProfiles { get; }; + + Boolean ShowFirstWindowPreference { get; }; + + IInspectable CurrentFirstWindowPreference; + IObservableVector FirstWindowPreferenceList { get; }; + IInspectable CurrentLaunchMode; IObservableVector LaunchModeList { get; }; diff --git a/src/cascadia/TerminalSettingsEditor/Launch.xaml b/src/cascadia/TerminalSettingsEditor/Launch.xaml index caac364f4d7..ece569affe0 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.xaml +++ b/src/cascadia/TerminalSettingsEditor/Launch.xaml @@ -133,6 +133,15 @@ + + + + + + The number of rows displayed in the window upon first load. Measured in characters. A description for what the "rows" setting does. Presented near "Globals_InitialRows.Header". + + When Terminal starts + Header for a control to select how the terminal should load its first window. + + + What should be shown when the first terminal is created. + + + + Open a tab with the default profile + 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. + Launch mode Header for a control to select what mode to launch the terminal in. diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 3ae8700393d..731ced23776 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -126,14 +126,19 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation bool Equals(const Model::NewTerminalArgs& other) { - return other.Commandline() == _Commandline && - other.StartingDirectory() == _StartingDirectory && - other.TabTitle() == _TabTitle && - other.TabColor() == _TabColor && - other.ProfileIndex() == _ProfileIndex && - other.Profile() == _Profile && - other.SuppressApplicationTitle() == _SuppressApplicationTitle && - other.ColorScheme() == _ColorScheme; + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_Commandline == _Commandline && + otherAsUs->_StartingDirectory == _StartingDirectory && + otherAsUs->_TabTitle == _TabTitle && + otherAsUs->_TabColor == _TabColor && + otherAsUs->_ProfileIndex == _ProfileIndex && + otherAsUs->_Profile == _Profile && + otherAsUs->_SuppressApplicationTitle == _SuppressApplicationTitle && + otherAsUs->_ColorScheme == _ColorScheme; + } + return false; }; static Model::NewTerminalArgs FromJson(const Json::Value& json) { diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp index 0984a5330cf..5a00ba2b410 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp @@ -5,54 +5,53 @@ #include "ApplicationState.h" #include "CascadiaSettings.h" #include "ApplicationState.g.cpp" - +#include "WindowLayout.g.cpp" +#include "ActionAndArgs.h" #include "JsonUtils.h" #include "FileUtils.h" -constexpr std::wstring_view stateFileName{ L"state.json" }; +static constexpr std::wstring_view stateFileName{ L"state.json" }; +static constexpr std::string_view TabLayoutKey{ "tabLayout" }; +static constexpr std::string_view InitialPositionKey{ "initialPosition" }; +static constexpr std::string_view InitialSizeKey{ "initialSize" }; namespace Microsoft::Terminal::Settings::Model::JsonUtils { - // This trait exists in order to serialize the std::unordered_set for GeneratedProfiles. - template - struct ConversionTrait> + using namespace winrt::Microsoft::Terminal::Settings::Model; + + template<> + struct ConversionTrait { - std::unordered_set FromJson(const Json::Value& json) const + WindowLayout FromJson(const Json::Value& json) { - ConversionTrait trait; - std::unordered_set val; - val.reserve(json.size()); + auto layout = winrt::make_self(); - for (const auto& element : json) - { - val.emplace(trait.FromJson(element)); - } + GetValueForKey(json, TabLayoutKey, layout->_TabLayout); + GetValueForKey(json, InitialPositionKey, layout->_InitialPosition); + GetValueForKey(json, InitialSizeKey, layout->_InitialSize); - return val; + return *layout; } - bool CanConvert(const Json::Value& json) const + bool CanConvert(const Json::Value& json) { - ConversionTrait trait; - return json.isArray() && std::all_of(json.begin(), json.end(), [trait](const auto& json) -> bool { return trait.CanConvert(json); }); + return json.isObject(); } - Json::Value ToJson(const std::unordered_set& val) + Json::Value ToJson(const WindowLayout& val) { - ConversionTrait trait; - Json::Value json{ Json::arrayValue }; + Json::Value json{ Json::objectValue }; - for (const auto& key : val) - { - json.append(trait.ToJson(key)); - } + SetValueForKey(json, TabLayoutKey, val.TabLayout()); + SetValueForKey(json, InitialPositionKey, val.InitialPosition()); + SetValueForKey(json, InitialSizeKey, val.InitialSize()); return json; } std::string TypeDescription() const { - return fmt::format("{}[]", ConversionTrait{}.TypeDescription()); + return "WindowLayout"; } }; } diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index 3a9a1e8d741..5edf21a7eda 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -13,20 +13,32 @@ Module Name: #pragma once #include "ApplicationState.g.h" +#include "WindowLayout.g.h" #include #include #include +#include // This macro generates all getters and setters for ApplicationState. // It provides X with the following arguments: // (type, function name, JSON key, ...variadic construction arguments) -#define MTSM_APPLICATION_STATE_FIELDS(X) \ - X(std::unordered_set, GeneratedProfiles, "generatedProfiles") \ - X(Windows::Foundation::Collections::IVector, RecentCommands, "recentCommands") - namespace winrt::Microsoft::Terminal::Settings::Model::implementation { +#define MTSM_APPLICATION_STATE_FIELDS(X) \ + X(std::unordered_set, GeneratedProfiles, "generatedProfiles") \ + X(Windows::Foundation::Collections::IVector, PersistedWindowLayouts, "persistedWindowLayouts") \ + X(Windows::Foundation::Collections::IVector, RecentCommands, "recentCommands") + + struct WindowLayout : WindowLayoutT + { + WINRT_PROPERTY(Windows::Foundation::Collections::IVector, TabLayout, nullptr); + WINRT_PROPERTY(winrt::Windows::Foundation::IReference, InitialPosition, nullptr); + WINRT_PROPERTY(winrt::Windows::Foundation::IReference, InitialSize, nullptr); + + friend ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait; + }; + struct ApplicationState : ApplicationStateT { static Microsoft::Terminal::Settings::Model::ApplicationState SharedInstance(); @@ -66,5 +78,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation { + BASIC_FACTORY(WindowLayout) BASIC_FACTORY(ApplicationState); } diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.idl b/src/cascadia/TerminalSettingsModel/ApplicationState.idl index 972b3e55e9a..366059d9816 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.idl +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.idl @@ -1,8 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import "Command.idl"; +import "GlobalAppSettings.idl"; namespace Microsoft.Terminal.Settings.Model { + runtimeclass WindowLayout + { + WindowLayout(); + + Windows.Foundation.Collections.IVector TabLayout; + Windows.Foundation.IReference InitialPosition; + Windows.Foundation.IReference InitialSize; + }; + [default_interface] runtimeclass ApplicationState { static ApplicationState SharedInstance(); @@ -10,6 +21,8 @@ namespace Microsoft.Terminal.Settings.Model String FilePath { get; }; + Windows.Foundation.Collections.IVector PersistedWindowLayouts { get; set; }; + Windows.Foundation.Collections.IVector RecentCommands { get; set; }; } } diff --git a/src/cascadia/TerminalSettingsModel/ColorScheme.idl b/src/cascadia/TerminalSettingsModel/ColorScheme.idl index bb812aaac0f..418fbb531f2 100644 --- a/src/cascadia/TerminalSettingsModel/ColorScheme.idl +++ b/src/cascadia/TerminalSettingsModel/ColorScheme.idl @@ -4,6 +4,7 @@ namespace Microsoft.Terminal.Settings.Model { [default_interface] runtimeclass ColorScheme : Windows.Foundation.IStringable { + ColorScheme(); ColorScheme(String name); String Name; diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp index e0c9d2b45d0..2eb69c64918 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp @@ -32,6 +32,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Global Settings DEFINE_ENUM_MAP(winrt::Windows::UI::Xaml::ElementTheme, ElementTheme); DEFINE_ENUM_MAP(winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabViewWidthMode); + DEFINE_ENUM_MAP(Model::FirstWindowPreference, FirstWindowPreference); DEFINE_ENUM_MAP(Model::LaunchMode, LaunchMode); DEFINE_ENUM_MAP(Model::TabSwitcherMode, TabSwitcherMode); DEFINE_ENUM_MAP(Microsoft::Terminal::Control::CopyFormat, CopyFormat); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.h b/src/cascadia/TerminalSettingsModel/EnumMappings.h index c597c6a774b..00f3b4ff124 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.h +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.h @@ -28,6 +28,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Global Settings static winrt::Windows::Foundation::Collections::IMap ElementTheme(); static winrt::Windows::Foundation::Collections::IMap TabViewWidthMode(); + static winrt::Windows::Foundation::Collections::IMap FirstWindowPreference(); static winrt::Windows::Foundation::Collections::IMap LaunchMode(); static winrt::Windows::Foundation::Collections::IMap TabSwitcherMode(); static winrt::Windows::Foundation::Collections::IMap CopyFormat(); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.idl b/src/cascadia/TerminalSettingsModel/EnumMappings.idl index 4a7bb593b7e..341f5699c25 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.idl +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.idl @@ -10,6 +10,7 @@ namespace Microsoft.Terminal.Settings.Model // Global Settings static Windows.Foundation.Collections.IMap ElementTheme { get; }; static Windows.Foundation.Collections.IMap TabViewWidthMode { get; }; + static Windows.Foundation.Collections.IMap FirstWindowPreference { get; }; static Windows.Foundation.Collections.IMap LaunchMode { get; }; static Windows.Foundation.Collections.IMap TabSwitcherMode { get; }; static Windows.Foundation.Collections.IMap CopyFormat { get; }; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 31760ec7a5f..6f1abf837fb 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -41,6 +41,7 @@ static constexpr std::string_view LaunchModeKey{ "launchMode" }; static constexpr std::string_view ConfirmCloseAllKey{ "confirmCloseAllTabs" }; static constexpr std::string_view SnapToGridOnResizeKey{ "snapToGridOnResize" }; static constexpr std::string_view EnableStartupTaskKey{ "startOnUserLogin" }; +static constexpr std::string_view FirstWindowPreferenceKey{ "firstWindowPreference" }; static constexpr std::string_view AlwaysOnTopKey{ "alwaysOnTop" }; static constexpr std::string_view LegacyUseTabSwitcherModeKey{ "useTabSwitcher" }; static constexpr std::string_view TabSwitcherModeKey{ "tabSwitcherMode" }; @@ -125,6 +126,7 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_ForceVTInput = _ForceVTInput; globals->_DebugFeaturesEnabled = _DebugFeaturesEnabled; globals->_StartOnUserLogin = _StartOnUserLogin; + globals->_FirstWindowPreference = _FirstWindowPreference; globals->_AlwaysOnTop = _AlwaysOnTop; globals->_TabSwitcherMode = _TabSwitcherMode; globals->_DisableAnimations = _DisableAnimations; @@ -285,6 +287,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, WarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste); + JsonUtils::GetValueForKey(json, FirstWindowPreferenceKey, _FirstWindowPreference); + JsonUtils::GetValueForKey(json, LaunchModeKey, _LaunchMode); JsonUtils::GetValueForKey(json, LanguageKey, _Language); @@ -408,6 +412,7 @@ Json::Value GlobalAppSettings::ToJson() const JsonUtils::SetValueForKey(json, CopyFormattingKey, _CopyFormatting); JsonUtils::SetValueForKey(json, WarnAboutLargePasteKey, _WarnAboutLargePaste); JsonUtils::SetValueForKey(json, WarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste); + JsonUtils::SetValueForKey(json, FirstWindowPreferenceKey, _FirstWindowPreference); JsonUtils::SetValueForKey(json, LaunchModeKey, _LaunchMode); JsonUtils::SetValueForKey(json, LanguageKey, _Language); JsonUtils::SetValueForKey(json, ThemeKey, _Theme); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index 2ac936e699b..ed06ee312d0 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -84,6 +84,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::GlobalAppSettings, bool, WarnAboutMultiLinePaste, true); INHERITABLE_SETTING(Model::GlobalAppSettings, Model::LaunchPosition, InitialPosition, nullptr, nullptr); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, CenterOnLaunch, false); + INHERITABLE_SETTING(Model::GlobalAppSettings, Model::FirstWindowPreference, FirstWindowPreference, FirstWindowPreference::DefaultProfile); INHERITABLE_SETTING(Model::GlobalAppSettings, Model::LaunchMode, LaunchMode, LaunchMode::DefaultMode); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, SnapToGridOnResize, true); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ForceFullRepaintRendering, false); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index a2145d3919d..94a317dade7 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -34,6 +34,12 @@ namespace Microsoft.Terminal.Settings.Model UseExisting, }; + enum FirstWindowPreference + { + DefaultProfile, + PersistedWindowLayout, + }; + [default_interface] runtimeclass GlobalAppSettings { Guid DefaultProfile; @@ -59,6 +65,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, WarnAboutMultiLinePaste); INHERITABLE_SETTING(LaunchPosition, InitialPosition); INHERITABLE_SETTING(Boolean, CenterOnLaunch); + INHERITABLE_SETTING(FirstWindowPreference, FirstWindowPreference); INHERITABLE_SETTING(LaunchMode, LaunchMode); INHERITABLE_SETTING(Boolean, SnapToGridOnResize); INHERITABLE_SETTING(Boolean, ForceFullRepaintRendering); diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index c5370cdbfbd..58124d84bcb 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -117,6 +117,127 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils std::string expectedType; }; + // Method Description: + // - Helper that will populate a reference with a value converted from a json object. + // Arguments: + // - json: the json object to convert + // - target: the value to populate with the converted result + // Return Value: + // - a boolean indicating whether the value existed (in this case, was non-null) + // + // GetValue, type-deduced, manual converter + template + bool GetValue(const Json::Value& json, T& target, Converter&& conv) + { + if (!conv.CanConvert(json)) + { + DeserializationError e{ json }; + e.expectedType = conv.TypeDescription(); + throw e; + } + + target = conv.FromJson(json); + return true; + } + + // GetValue, forced return type, manual converter + template + std::decay_t GetValue(const Json::Value& json, Converter&& conv) + { + std::decay_t local{}; + GetValue(json, local, std::forward(conv)); + return local; // returns zero-initialized or value + } + + // GetValueForKey, type-deduced, manual converter + template + bool GetValueForKey(const Json::Value& json, std::string_view key, T& target, Converter&& conv) + { + if (auto found{ json.find(&*key.cbegin(), (&*key.cbegin()) + key.size()) }) + { + try + { + return GetValue(*found, target, std::forward(conv)); + } + catch (DeserializationError& e) + { + e.SetKey(key); + throw; // rethrow now that it has a key + } + } + return false; + } + + // GetValueForKey, forced return type, manual converter + template + std::decay_t GetValueForKey(const Json::Value& json, std::string_view key, Converter&& conv) + { + std::decay_t local{}; + GetValueForKey(json, key, local, std::forward(conv)); + return local; // returns zero-initialized? + } + + // GetValue, type-deduced, with automatic converter + template + bool GetValue(const Json::Value& json, T& target) + { + return GetValue(json, target, ConversionTrait::type>{}); + } + + // GetValue, forced return type, with automatic converter + template + std::decay_t GetValue(const Json::Value& json) + { + std::decay_t local{}; + GetValue(json, local, ConversionTrait::type>{}); + return local; // returns zero-initialized or value + } + + // GetValueForKey, type-deduced, with automatic converter + template + bool GetValueForKey(const Json::Value& json, std::string_view key, T& target) + { + return GetValueForKey(json, key, target, ConversionTrait::type>{}); + } + + // GetValueForKey, forced return type, with automatic converter + template + std::decay_t GetValueForKey(const Json::Value& json, std::string_view key) + { + return GetValueForKey(json, key, ConversionTrait::type>{}); + } + + // Get multiple values for keys (json, k, &v, k, &v, k, &v, ...). + // Uses the default converter for each v. + // Careful: this can cause a template explosion. + constexpr void GetValuesForKeys(const Json::Value& /*json*/) {} + + template + void GetValuesForKeys(const Json::Value& json, std::string_view key1, T&& val1, Args&&... args) + { + GetValueForKey(json, key1, val1); + GetValuesForKeys(json, std::forward(args)...); + } + + // SetValueForKey, type-deduced, manual converter + template + void SetValueForKey(Json::Value& json, std::string_view key, const T& target, Converter&& conv) + { + // We don't want to write any empty optionals into JSON (right now). + if (OptionOracle::HasValue(target)) + { + // demand guarantees that it will return a value or throw an exception + *json.demand(&*key.cbegin(), (&*key.cbegin()) + key.size()) = conv.ToJson(target); + } + } + + // SetValueForKey, type-deduced, with automatic converter + template + void SetValueForKey(Json::Value& json, std::string_view key, const T& target) + { + SetValueForKey(json, key, target, ConversionTrait::type>{}); + } + template struct ConversionTrait { @@ -219,6 +340,49 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils }; template + struct ConversionTrait> + { + std::unordered_set FromJson(const Json::Value& json) const + { + ConversionTrait trait; + std::unordered_set val; + val.reserve(json.size()); + + for (const auto& element : json) + { + val.emplace(trait.FromJson(element)); + } + + return val; + } + + bool CanConvert(const Json::Value& json) const + { + ConversionTrait trait; + return json.isArray() && std::all_of(json.begin(), json.end(), [trait](const auto& json) mutable -> bool { return trait.CanConvert(json); }); + } + + Json::Value ToJson(const std::unordered_set& val) + { + ConversionTrait trait; + Json::Value json{ Json::arrayValue }; + + for (const auto& key : val) + { + json.append(trait.ToJson(key)); + } + + return json; + } + + std::string TypeDescription() const + { + return fmt::format("{}[]", ConversionTrait{}.TypeDescription()); + } + }; + + template + struct ConversionTrait> { std::unordered_map FromJson(const Json::Value& json) const @@ -595,6 +759,46 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils } }; +#ifdef WINRT_Windows_Foundation_H + template<> + struct ConversionTrait + { + winrt::Windows::Foundation::Size FromJson(const Json::Value& json) + { + winrt::Windows::Foundation::Size size{}; + GetValueForKey(json, std::string_view("width"), size.Width); + GetValueForKey(json, std::string_view("height"), size.Height); + + return size; + } + + bool CanConvert(const Json::Value& json) + { + if (!json.isObject()) + { + return false; + } + + return json.isMember("width") && json.isMember("height"); + } + + Json::Value ToJson(const winrt::Windows::Foundation::Size& val) + { + Json::Value json{ Json::objectValue }; + + SetValueForKey(json, std::string_view("width"), val.Width); + SetValueForKey(json, std::string_view("height"), val.Height); + + return json; + } + + std::string TypeDescription() const + { + return "size { width, height }"; + } + }; +#endif + #ifdef WINRT_Windows_UI_H template<> struct ConversionTrait @@ -852,127 +1056,6 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils return "any"; } }; - - // Method Description: - // - Helper that will populate a reference with a value converted from a json object. - // Arguments: - // - json: the json object to convert - // - target: the value to populate with the converted result - // Return Value: - // - a boolean indicating whether the value existed (in this case, was non-null) - // - // GetValue, type-deduced, manual converter - template - bool GetValue(const Json::Value& json, T& target, Converter&& conv) - { - if (!conv.CanConvert(json)) - { - DeserializationError e{ json }; - e.expectedType = conv.TypeDescription(); - throw e; - } - - target = conv.FromJson(json); - return true; - } - - // GetValue, forced return type, manual converter - template - std::decay_t GetValue(const Json::Value& json, Converter&& conv) - { - std::decay_t local{}; - GetValue(json, local, std::forward(conv)); - return local; // returns zero-initialized or value - } - - // GetValueForKey, type-deduced, manual converter - template - bool GetValueForKey(const Json::Value& json, std::string_view key, T& target, Converter&& conv) - { - if (auto found{ json.find(&*key.cbegin(), (&*key.cbegin()) + key.size()) }) - { - try - { - return GetValue(*found, target, std::forward(conv)); - } - catch (DeserializationError& e) - { - e.SetKey(key); - throw; // rethrow now that it has a key - } - } - return false; - } - - // GetValueForKey, forced return type, manual converter - template - std::decay_t GetValueForKey(const Json::Value& json, std::string_view key, Converter&& conv) - { - std::decay_t local{}; - GetValueForKey(json, key, local, std::forward(conv)); - return local; // returns zero-initialized? - } - - // GetValue, type-deduced, with automatic converter - template - bool GetValue(const Json::Value& json, T& target) - { - return GetValue(json, target, ConversionTrait::type>{}); - } - - // GetValue, forced return type, with automatic converter - template - std::decay_t GetValue(const Json::Value& json) - { - std::decay_t local{}; - GetValue(json, local, ConversionTrait::type>{}); - return local; // returns zero-initialized or value - } - - // GetValueForKey, type-deduced, with automatic converter - template - bool GetValueForKey(const Json::Value& json, std::string_view key, T& target) - { - return GetValueForKey(json, key, target, ConversionTrait::type>{}); - } - - // GetValueForKey, forced return type, with automatic converter - template - std::decay_t GetValueForKey(const Json::Value& json, std::string_view key) - { - return GetValueForKey(json, key, ConversionTrait::type>{}); - } - - // Get multiple values for keys (json, k, &v, k, &v, k, &v, ...). - // Uses the default converter for each v. - // Careful: this can cause a template explosion. - constexpr void GetValuesForKeys(const Json::Value& /*json*/) {} - - template - void GetValuesForKeys(const Json::Value& json, std::string_view key1, T&& val1, Args&&... args) - { - GetValueForKey(json, key1, val1); - GetValuesForKeys(json, std::forward(args)...); - } - - // SetValueForKey, type-deduced, manual converter - template - void SetValueForKey(Json::Value& json, std::string_view key, const T& target, Converter&& conv) - { - // We don't want to write any empty optionals into JSON (right now). - if (OptionOracle::HasValue(target)) - { - // demand guarantees that it will return a value or throw an exception - *json.demand(&*key.cbegin(), (&*key.cbegin()) + key.size()) = conv.ToJson(target); - } - } - - // SetValueForKey, type-deduced, with automatic converter - template - void SetValueForKey(Json::Value& json, std::string_view key, const T& target) - { - SetValueForKey(json, key, target, ConversionTrait::type>{}); - } }; #define JSON_ENUM_MAPPER(...) \ diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index de6b013472c..ff46d81b64c 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -340,6 +340,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // settings. if (scheme == nullptr) { + ClearAppliedColorScheme(); ClearDefaultForeground(); ClearDefaultBackground(); ClearSelectionBackground(); @@ -348,6 +349,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } else { + AppliedColorScheme(scheme); _DefaultForeground = til::color{ scheme.Foreground() }; _DefaultBackground = til::color{ scheme.Background() }; _SelectionBackground = til::color{ scheme.SelectionBackground() }; diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index 3744c10573a..8525668ab6e 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -127,6 +127,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, IFontAxesMap, FontAxes); INHERITABLE_SETTING(Model::TerminalSettings, IFontFeatureMap, FontFeatures); + INHERITABLE_SETTING(Model::TerminalSettings, Model::ColorScheme, AppliedColorScheme); INHERITABLE_SETTING(Model::TerminalSettings, hstring, BackgroundImage); INHERITABLE_SETTING(Model::TerminalSettings, double, BackgroundImageOpacity, 1.0); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl index c40aba55557..80231906e8f 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl @@ -33,5 +33,7 @@ namespace Microsoft.Terminal.Settings.Model void SetParent(TerminalSettings parent); TerminalSettings GetParent(); void ApplyColorScheme(ColorScheme scheme); + + ColorScheme AppliedColorScheme; }; } diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 674a42fbe7e..e186b7c2b74 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -209,6 +209,14 @@ JSON_ENUM_MAPPER(::winrt::Windows::UI::Xaml::ElementTheme) }; }; +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FirstWindowPreference) +{ + JSON_MAPPINGS(2) = { + pair_type{ "defaultProfile", ValueType::DefaultProfile }, + pair_type{ "persistedWindowLayout", ValueType::PersistedWindowLayout }, + }; +}; + JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::LaunchMode) { JSON_MAPPINGS(5) = { @@ -373,7 +381,8 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::SplitState) // Possible SplitType values JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::SplitType) { - JSON_MAPPINGS(1) = { + JSON_MAPPINGS(2) = { + pair_type{ "manual", ValueType::Manual }, pair_type{ "duplicate", ValueType::Duplicate }, }; }; diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index e5c52d4f14a..1af50212506 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -214,6 +214,13 @@ void AppHost::_HandleCommandlineArgs() peasant.DisplayWindowIdRequested({ this, &AppHost::_DisplayWindowId }); + // 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()) + { + _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); + } _logic.WindowName(peasant.WindowName()); _logic.WindowId(peasant.GetID()); } @@ -673,6 +680,13 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s _CreateTrayIcon(); } + // 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. + _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); + + _windowManager.WindowCreated([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); + _windowManager.WindowClosed([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); + // These events are coming from peasants that become or un-become quake windows. _windowManager.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequested(); }); _windowManager.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequested(); }); diff --git a/src/features.xml b/src/features.xml index c50399f4a19..14c1364ff8a 100644 --- a/src/features.xml +++ b/src/features.xml @@ -79,4 +79,13 @@ + + + Feature_PersistedWindowLayout + Whether to allow the user to enable persisted window layout saving and loading + 766 + AlwaysEnabled + + +