diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 29d56325c51..43d3f31fd87 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -131,6 +131,7 @@ buflen buildsystems buildtransitive BValue +BYPOSITION Cacafire CALLCONV CANDRABINDU diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index c715b0e2a11..2402aa2f2f7 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1067,6 +1067,54 @@ namespace winrt::TerminalApp::implementation _newTabButton.Flyout(newTabFlyout); } + // Method Description: + // - Builds the list of active profiles and raises SystemMenuNewTabProfilesChanged + // so the hosting window can populate its system menu "New Tab" submenu. + void TerminalPage::PopulateSystemMenuNewTabProfiles() + { + const auto activeProfiles = _settings.ActiveProfiles(); + if (!activeProfiles || activeProfiles.Size() == 0) + { + return; + } + + const auto defaultProfileGuid = _settings.GlobalSettings().DefaultProfile(); + + auto profileItems = winrt::single_threaded_vector(); + + for (uint32_t i = 0; i < activeProfiles.Size(); i++) + { + const auto profile = activeProfiles.GetAt(i); + auto displayName = profile.Name(); + + if (profile.Guid() == defaultProfileGuid) + { + displayName = displayName + L" (default)"; + } + + NewTerminalArgs newTerminalArgs{ gsl::narrow_cast(i) }; + + auto handler = winrt::TerminalApp::SystemMenuNewTabProfileHandler( + [weakThis{ get_weak() }, newTerminalArgs]() { + if (auto page{ weakThis.get() }) + { + NewTabArgs newTabArgs{ newTerminalArgs }; + ActionAndArgs actionAndArgs{ ShortcutAction::NewTab, newTabArgs }; + page->_actionDispatch->DoAction(actionAndArgs); + } + }); + + auto item = winrt::make_self(); + item->DisplayName(displayName); + item->Handler(handler); + profileItems.Append(*item); + } + + auto args = winrt::make_self(); + args->Profiles(profileItems); + SystemMenuNewTabProfilesChanged.raise(*this, *args); + } + // Method Description: // - For a given list of tab menu entries, this method will create the corresponding // list of flyout items. This is a recursive method that calls itself when it comes @@ -3919,6 +3967,8 @@ namespace winrt::TerminalApp::implementation _UpdateTabWidthMode(); _CreateNewTabFlyout(); + PopulateSystemMenuNewTabProfiles(); + // Reload the current value of alwaysOnTop from the settings file. This // will let the user hot-reload this setting, but any runtime changes to // the alwaysOnTop setting will be lost. diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index b1d26ee69d3..4c6f3d7877b 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -12,6 +12,8 @@ #include "RenameWindowRequestedArgs.g.h" #include "RequestMoveContentArgs.g.h" #include "LaunchPositionRequest.g.h" +#include "SystemMenuNewTabProfileItem.g.h" +#include "SystemMenuNewTabProfilesArgs.g.h" #include "Toast.h" #include "WindowsPackageManagerFactory.h" @@ -77,6 +79,21 @@ namespace winrt::TerminalApp::implementation _TabIndex{ tabIndex } {}; }; + struct SystemMenuNewTabProfileItem : SystemMenuNewTabProfileItemT + { + SystemMenuNewTabProfileItem() = default; + + WINRT_PROPERTY(winrt::hstring, DisplayName, L""); + WINRT_PROPERTY(winrt::TerminalApp::SystemMenuNewTabProfileHandler, Handler, nullptr); + }; + + struct SystemMenuNewTabProfilesArgs : SystemMenuNewTabProfilesArgsT + { + SystemMenuNewTabProfilesArgs() = default; + + WINRT_PROPERTY(Windows::Foundation::Collections::IVector, Profiles, nullptr); + }; + struct LaunchPositionRequest : LaunchPositionRequestT { LaunchPositionRequest() = default; @@ -176,6 +193,8 @@ namespace winrt::TerminalApp::implementation uint32_t NumberOfTabs() const; + void PopulateSystemMenuNewTabProfiles(); + til::property_changed_event PropertyChanged; // -------------------------------- WinRT Events --------------------------------- @@ -204,6 +223,8 @@ namespace winrt::TerminalApp::implementation til::typed_event RequestLaunchPosition; + til::typed_event SystemMenuNewTabProfilesChanged; + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr); diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index ce15059864b..af2b51f305f 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -50,6 +50,19 @@ namespace TerminalApp Microsoft.Terminal.Settings.Model.LaunchPosition Position; } + delegate void SystemMenuNewTabProfileHandler(); + + [default_interface] runtimeclass SystemMenuNewTabProfileItem + { + String DisplayName { get; }; + SystemMenuNewTabProfileHandler Handler { get; }; + }; + + [default_interface] runtimeclass SystemMenuNewTabProfilesArgs + { + IVector Profiles { get; }; + }; + [default_interface] runtimeclass TerminalPage : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged, Microsoft.Terminal.UI.IDirectKeyListener { TerminalPage(WindowProperties properties, ContentManager manager); @@ -103,5 +116,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestReceiveContent; event Windows.Foundation.TypedEventHandler RequestLaunchPosition; + + event Windows.Foundation.TypedEventHandler SystemMenuNewTabProfilesChanged; } } diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index b2fa349e40c..b54fcbd0a4b 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -217,6 +217,8 @@ namespace winrt::TerminalApp::implementation SystemMenuItemHandler(this, &TerminalWindow::_OpenSettingsUI)); SystemMenuChangeRequested.raise(*this, *args); + _root->PopulateSystemMenuNewTabProfiles(); + TraceLoggingWrite( g_hTerminalAppProvider, "WindowCreated", diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index 2f1aad5a7aa..3b42a06a685 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -161,6 +161,8 @@ namespace winrt::TerminalApp::implementation til::typed_event SettingsChanged; til::typed_event WindowSizeChanged; + FORWARDED_TYPED_EVENT(SystemMenuNewTabProfilesChanged, Windows::Foundation::IInspectable, winrt::TerminalApp::SystemMenuNewTabProfilesArgs, _root, SystemMenuNewTabProfilesChanged); + private: // If you add controls here, but forget to null them either here or in // the ctor, you're going to have a bad time. It'll mysteriously fail to diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index b900522cbf2..f6993a3c7eb 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -129,6 +129,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler OpenSystemMenu; event Windows.Foundation.TypedEventHandler QuitRequested; event Windows.Foundation.TypedEventHandler SystemMenuChangeRequested; + event Windows.Foundation.TypedEventHandler SystemMenuNewTabProfilesChanged; event Windows.Foundation.TypedEventHandler ShowWindowChanged; event Windows.Foundation.TypedEventHandler WindowSizeChanged; diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 522e90c7d28..ad27b8be5cc 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -233,6 +233,7 @@ void AppHost::Initialize() _revokers.AlwaysOnTopChanged = _windowLogic.AlwaysOnTopChanged(winrt::auto_revoke, { this, &AppHost::_AlwaysOnTopChanged }); _revokers.RaiseVisualBell = _windowLogic.RaiseVisualBell(winrt::auto_revoke, { this, &AppHost::_RaiseVisualBell }); _revokers.SystemMenuChangeRequested = _windowLogic.SystemMenuChangeRequested(winrt::auto_revoke, { this, &AppHost::_SystemMenuChangeRequested }); + _revokers.SystemMenuNewTabProfilesChanged = _windowLogic.SystemMenuNewTabProfilesChanged(winrt::auto_revoke, { this, &AppHost::_SystemMenuNewTabProfilesChanged }); _revokers.ChangeMaximizeRequested = _windowLogic.ChangeMaximizeRequested(winrt::auto_revoke, { this, &AppHost::_ChangeMaximizeRequested }); _revokers.RequestLaunchPosition = _windowLogic.RequestLaunchPosition(winrt::auto_revoke, { this, &AppHost::_HandleRequestLaunchPosition }); @@ -1091,6 +1092,48 @@ void AppHost::_SystemMenuChangeRequested(const winrt::Windows::Foundation::IInsp } } +// Method Description: +// - Handles the SystemMenuNewTabProfilesChanged event from the TerminalPage. +// Rebuilds the "New Tab" submenu in the system menu (Alt+Space) with items +// driven by the app layer's action engine. +// Arguments: +// - args: The SystemMenuNewTabProfilesArgs containing the new list of +// profiles to show in the "New Tab" submenu. +// Return Value: +// - +void AppHost::_SystemMenuNewTabProfilesChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::TerminalApp::SystemMenuNewTabProfilesArgs& args) +{ + if (!_window) + { + return; + } + + _window->RemoveSystemSubMenu(L"New Tab"); + + if (!args || !args.Profiles() || args.Profiles().Size() == 0) + { + return; + } + + std::vector>> profileItems; + profileItems.reserve(args.Profiles().Size()); + + for (const auto& item : args.Profiles()) + { + auto handler = item.Handler(); + auto callback = winrt::delegate([handler]() { + if (handler) + { + handler(); + } + }); + + profileItems.emplace_back(item.DisplayName(), std::move(callback)); + } + + _window->AddSystemSubMenu(L"New Tab", profileItems); +} + // Method Description: // - BODGY workaround for GH#9320. When the window moves, dismiss all the popups // in the UI tree. Xaml Islands unfortunately doesn't do this for us, see diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 379f876b943..5c7a6aee4f5 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -131,6 +131,9 @@ class AppHost : public std::enable_shared_from_this void _HandleRequestLaunchPosition(const winrt::Windows::Foundation::IInspectable& sender, winrt::TerminalApp::LaunchPositionRequest args); + void _SystemMenuNewTabProfilesChanged(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::TerminalApp::SystemMenuNewTabProfilesArgs& args); + // Helper struct. By putting these all into one struct, we can revoke them // all at once, by assigning _revokers to a fresh Revokers instance. That'll // cause us to dtor the old one, which will immediately call revoke on all @@ -160,6 +163,7 @@ class AppHost : public std::enable_shared_from_this winrt::TerminalApp::TerminalWindow::PropertyChanged_revoker PropertyChanged; winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged; winrt::TerminalApp::TerminalWindow::WindowSizeChanged_revoker WindowSizeChanged; + winrt::TerminalApp::TerminalWindow::SystemMenuNewTabProfilesChanged_revoker SystemMenuNewTabProfilesChanged; } _revokers{}; // our IslandWindow is not a WinRT type. It can't make auto_revokers like diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index e627bd44957..b640d444ad5 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -1808,6 +1808,51 @@ void IslandWindow::AddToSystemMenu(const winrt::hstring& itemLabel, winrt::deleg _systemMenuNextItemId++; } +void IslandWindow::AddSystemSubMenu(const winrt::hstring& menuLabel, const std::vector>>& items) +{ + const auto systemMenu = GetSystemMenu(_window.get(), FALSE); + + HMENU subMenu = CreatePopupMenu(); + if (!subMenu) + { + LOG_LAST_ERROR(); + return; + } + + for (const auto& [label, callback] : items) + { + auto wID = _systemMenuNextItemId; + + MENUITEMINFOW item; + item.cbSize = sizeof(MENUITEMINFOW); + item.fMask = MIIM_STATE | MIIM_ID | MIIM_STRING; + item.fState = MF_ENABLED; + item.wID = wID; + item.dwTypeData = const_cast(label.c_str()); + item.cch = static_cast(label.size()); + + if (LOG_LAST_ERROR_IF(!InsertMenuItemW(subMenu, wID, FALSE, &item))) + { + continue; + } + _systemMenuItems.insert({ wID, { label, callback } }); + _systemMenuNextItemId++; + } + + MENUITEMINFOW subMenuItem; + subMenuItem.cbSize = sizeof(MENUITEMINFOW); + subMenuItem.fMask = MIIM_STRING | MIIM_SUBMENU; + subMenuItem.hSubMenu = subMenu; + subMenuItem.dwTypeData = const_cast(menuLabel.c_str()); + subMenuItem.cch = static_cast(menuLabel.size()); + + if (LOG_LAST_ERROR_IF(!InsertMenuItemW(systemMenu, _systemMenuNextItemId, FALSE, &subMenuItem))) + { + DestroyMenu(subMenu); + return; + } +} + void IslandWindow::RemoveFromSystemMenu(const winrt::hstring& itemLabel) { const auto systemMenu = GetSystemMenu(_window.get(), FALSE); @@ -1832,6 +1877,53 @@ void IslandWindow::RemoveFromSystemMenu(const winrt::hstring& itemLabel) _systemMenuItems.erase(it->first); } +// Method Description: +// - Removes a submenu from the system menu by its label. Also removes all +// child items from the _systemMenuItems tracking map. The submenu HMENU +// is destroyed automatically by DeleteMenu. +// Arguments: +// - menuLabel: The label of the submenu to remove. +void IslandWindow::RemoveSystemSubMenu(const winrt::hstring& menuLabel) +{ + const auto systemMenu = GetSystemMenu(_window.get(), FALSE); + const auto itemCount = GetMenuItemCount(systemMenu); + if (LOG_LAST_ERROR_IF(itemCount == -1)) + { + return; + } + + for (int i = 0; i < itemCount; i++) + { + wchar_t buffer[256]{}; + MENUITEMINFOW mii{}; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STRING | MIIM_SUBMENU; + mii.dwTypeData = buffer; + mii.cch = ARRAYSIZE(buffer); + + if (!GetMenuItemInfoW(systemMenu, static_cast(i), TRUE, &mii)) + { + continue; + } + + if (mii.hSubMenu && menuLabel == std::wstring_view{ buffer }) + { + const auto subItemCount = GetMenuItemCount(mii.hSubMenu); + for (int j = 0; j < subItemCount; j++) + { + const auto subItemId = GetMenuItemID(mii.hSubMenu, j); + if (subItemId != static_cast(-1)) + { + _systemMenuItems.erase(subItemId); + } + } + + DeleteMenu(systemMenu, static_cast(i), MF_BYPOSITION); + return; + } + } +} + void IslandWindow::_resetSystemMenu() { // GetSystemMenu(..., true) will revert the menu to the default state. diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 83afeaf44f5..effd1983b15 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -67,7 +67,9 @@ class IslandWindow : void OpenSystemMenu(const std::optional mouseX, const std::optional mouseY) const noexcept; void AddToSystemMenu(const winrt::hstring& itemLabel, winrt::delegate callback); + void AddSystemSubMenu(const winrt::hstring& menuLabel, const std::vector>>& items); void RemoveFromSystemMenu(const winrt::hstring& itemLabel); + void RemoveSystemSubMenu(const winrt::hstring& menuLabel); void UseDarkTheme(const bool v); virtual void UseMica(const bool newValue, const double titlebarOpacity);