Skip to content

Commit b0aeda5

Browse files
committed
Add New Tab Submenu to System Menu
1 parent c62bb19 commit b0aeda5

11 files changed

Lines changed: 233 additions & 0 deletions

File tree

.github/actions/spelling/expect/expect.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ buflen
131131
buildsystems
132132
buildtransitive
133133
BValue
134+
BYPOSITION
134135
Cacafire
135136
CALLCONV
136137
CANDRABINDU

src/cascadia/TerminalApp/TerminalPage.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,54 @@ namespace winrt::TerminalApp::implementation
10671067
_newTabButton.Flyout(newTabFlyout);
10681068
}
10691069

1070+
// Method Description:
1071+
// - Builds the list of active profiles and raises SystemMenuNewTabProfilesChanged
1072+
// so the hosting window can populate its system menu "New Tab" submenu.
1073+
void TerminalPage::PopulateSystemMenuNewTabProfiles()
1074+
{
1075+
const auto activeProfiles = _settings.ActiveProfiles();
1076+
if (!activeProfiles || activeProfiles.Size() == 0)
1077+
{
1078+
return;
1079+
}
1080+
1081+
const auto defaultProfileGuid = _settings.GlobalSettings().DefaultProfile();
1082+
1083+
auto profileItems = winrt::single_threaded_vector<winrt::TerminalApp::SystemMenuNewTabProfileItem>();
1084+
1085+
for (uint32_t i = 0; i < activeProfiles.Size(); i++)
1086+
{
1087+
const auto profile = activeProfiles.GetAt(i);
1088+
auto displayName = profile.Name();
1089+
1090+
if (profile.Guid() == defaultProfileGuid)
1091+
{
1092+
displayName = displayName + L" (default)";
1093+
}
1094+
1095+
NewTerminalArgs newTerminalArgs{ gsl::narrow_cast<int32_t>(i) };
1096+
1097+
auto handler = winrt::TerminalApp::SystemMenuNewTabProfileHandler(
1098+
[weakThis{ get_weak() }, newTerminalArgs]() {
1099+
if (auto page{ weakThis.get() })
1100+
{
1101+
NewTabArgs newTabArgs{ newTerminalArgs };
1102+
ActionAndArgs actionAndArgs{ ShortcutAction::NewTab, newTabArgs };
1103+
page->_actionDispatch->DoAction(actionAndArgs);
1104+
}
1105+
});
1106+
1107+
auto item = winrt::make_self<winrt::TerminalApp::implementation::SystemMenuNewTabProfileItem>();
1108+
item->DisplayName(displayName);
1109+
item->Handler(handler);
1110+
profileItems.Append(*item);
1111+
}
1112+
1113+
auto args = winrt::make_self<winrt::TerminalApp::implementation::SystemMenuNewTabProfilesArgs>();
1114+
args->Profiles(profileItems);
1115+
SystemMenuNewTabProfilesChanged.raise(*this, *args);
1116+
}
1117+
10701118
// Method Description:
10711119
// - For a given list of tab menu entries, this method will create the corresponding
10721120
// list of flyout items. This is a recursive method that calls itself when it comes
@@ -3919,6 +3967,8 @@ namespace winrt::TerminalApp::implementation
39193967
_UpdateTabWidthMode();
39203968
_CreateNewTabFlyout();
39213969

3970+
PopulateSystemMenuNewTabProfiles();
3971+
39223972
// Reload the current value of alwaysOnTop from the settings file. This
39233973
// will let the user hot-reload this setting, but any runtime changes to
39243974
// the alwaysOnTop setting will be lost.

src/cascadia/TerminalApp/TerminalPage.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#include "RenameWindowRequestedArgs.g.h"
1313
#include "RequestMoveContentArgs.g.h"
1414
#include "LaunchPositionRequest.g.h"
15+
#include "SystemMenuNewTabProfileItem.g.h"
16+
#include "SystemMenuNewTabProfilesArgs.g.h"
1517
#include "Toast.h"
1618

1719
#include "WindowsPackageManagerFactory.h"
@@ -77,6 +79,21 @@ namespace winrt::TerminalApp::implementation
7779
_TabIndex{ tabIndex } {};
7880
};
7981

82+
struct SystemMenuNewTabProfileItem : SystemMenuNewTabProfileItemT<SystemMenuNewTabProfileItem>
83+
{
84+
SystemMenuNewTabProfileItem() = default;
85+
86+
WINRT_PROPERTY(winrt::hstring, DisplayName, L"");
87+
WINRT_PROPERTY(winrt::TerminalApp::SystemMenuNewTabProfileHandler, Handler, nullptr);
88+
};
89+
90+
struct SystemMenuNewTabProfilesArgs : SystemMenuNewTabProfilesArgsT<SystemMenuNewTabProfilesArgs>
91+
{
92+
SystemMenuNewTabProfilesArgs() = default;
93+
94+
WINRT_PROPERTY(Windows::Foundation::Collections::IVector<winrt::TerminalApp::SystemMenuNewTabProfileItem>, Profiles, nullptr);
95+
};
96+
8097
struct LaunchPositionRequest : LaunchPositionRequestT<LaunchPositionRequest>
8198
{
8299
LaunchPositionRequest() = default;
@@ -176,6 +193,8 @@ namespace winrt::TerminalApp::implementation
176193

177194
uint32_t NumberOfTabs() const;
178195

196+
void PopulateSystemMenuNewTabProfiles();
197+
179198
til::property_changed_event PropertyChanged;
180199

181200
// -------------------------------- WinRT Events ---------------------------------
@@ -204,6 +223,8 @@ namespace winrt::TerminalApp::implementation
204223

205224
til::typed_event<IInspectable, winrt::TerminalApp::LaunchPositionRequest> RequestLaunchPosition;
206225

226+
til::typed_event<IInspectable, winrt::TerminalApp::SystemMenuNewTabProfilesArgs> SystemMenuNewTabProfilesChanged;
227+
207228
WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr);
208229
WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr);
209230

src/cascadia/TerminalApp/TerminalPage.idl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ namespace TerminalApp
5050
Microsoft.Terminal.Settings.Model.LaunchPosition Position;
5151
}
5252

53+
delegate void SystemMenuNewTabProfileHandler();
54+
55+
[default_interface] runtimeclass SystemMenuNewTabProfileItem
56+
{
57+
String DisplayName { get; };
58+
SystemMenuNewTabProfileHandler Handler { get; };
59+
};
60+
61+
[default_interface] runtimeclass SystemMenuNewTabProfilesArgs
62+
{
63+
IVector<SystemMenuNewTabProfileItem> Profiles { get; };
64+
};
65+
5366
[default_interface] runtimeclass TerminalPage : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged, Microsoft.Terminal.UI.IDirectKeyListener
5467
{
5568
TerminalPage(WindowProperties properties, ContentManager manager);
@@ -103,5 +116,7 @@ namespace TerminalApp
103116
event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent;
104117

105118
event Windows.Foundation.TypedEventHandler<Object, LaunchPositionRequest> RequestLaunchPosition;
119+
120+
event Windows.Foundation.TypedEventHandler<Object, SystemMenuNewTabProfilesArgs> SystemMenuNewTabProfilesChanged;
106121
}
107122
}

src/cascadia/TerminalApp/TerminalWindow.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ namespace winrt::TerminalApp::implementation
217217
SystemMenuItemHandler(this, &TerminalWindow::_OpenSettingsUI));
218218
SystemMenuChangeRequested.raise(*this, *args);
219219

220+
_root->PopulateSystemMenuNewTabProfiles();
221+
220222
TraceLoggingWrite(
221223
g_hTerminalAppProvider,
222224
"WindowCreated",

src/cascadia/TerminalApp/TerminalWindow.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ namespace winrt::TerminalApp::implementation
161161
til::typed_event<winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::SettingsLoadEventArgs> SettingsChanged;
162162
til::typed_event<winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs> WindowSizeChanged;
163163

164+
FORWARDED_TYPED_EVENT(SystemMenuNewTabProfilesChanged, Windows::Foundation::IInspectable, winrt::TerminalApp::SystemMenuNewTabProfilesArgs, _root, SystemMenuNewTabProfilesChanged);
165+
164166
private:
165167
// If you add controls here, but forget to null them either here or in
166168
// the ctor, you're going to have a bad time. It'll mysteriously fail to

src/cascadia/TerminalApp/TerminalWindow.idl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ namespace TerminalApp
129129
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
130130
event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
131131
event Windows.Foundation.TypedEventHandler<Object, TerminalApp.SystemMenuChangeArgs> SystemMenuChangeRequested;
132+
event Windows.Foundation.TypedEventHandler<Object, TerminalApp.SystemMenuNewTabProfilesArgs> SystemMenuNewTabProfilesChanged;
132133
event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Control.ShowWindowArgs> ShowWindowChanged;
133134
event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Control.WindowSizeChangedEventArgs> WindowSizeChanged;
134135

src/cascadia/WindowsTerminal/AppHost.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ void AppHost::Initialize()
233233
_revokers.AlwaysOnTopChanged = _windowLogic.AlwaysOnTopChanged(winrt::auto_revoke, { this, &AppHost::_AlwaysOnTopChanged });
234234
_revokers.RaiseVisualBell = _windowLogic.RaiseVisualBell(winrt::auto_revoke, { this, &AppHost::_RaiseVisualBell });
235235
_revokers.SystemMenuChangeRequested = _windowLogic.SystemMenuChangeRequested(winrt::auto_revoke, { this, &AppHost::_SystemMenuChangeRequested });
236+
_revokers.SystemMenuNewTabProfilesChanged = _windowLogic.SystemMenuNewTabProfilesChanged(winrt::auto_revoke, { this, &AppHost::_SystemMenuNewTabProfilesChanged });
236237
_revokers.ChangeMaximizeRequested = _windowLogic.ChangeMaximizeRequested(winrt::auto_revoke, { this, &AppHost::_ChangeMaximizeRequested });
237238
_revokers.RequestLaunchPosition = _windowLogic.RequestLaunchPosition(winrt::auto_revoke, { this, &AppHost::_HandleRequestLaunchPosition });
238239

@@ -1091,6 +1092,48 @@ void AppHost::_SystemMenuChangeRequested(const winrt::Windows::Foundation::IInsp
10911092
}
10921093
}
10931094

1095+
// Method Description:
1096+
// - Handles the SystemMenuNewTabProfilesChanged event from the TerminalPage.
1097+
// Rebuilds the "New Tab" submenu in the system menu (Alt+Space) with items
1098+
// driven by the app layer's action engine.
1099+
// Arguments:
1100+
// - args: The SystemMenuNewTabProfilesArgs containing the new list of
1101+
// profiles to show in the "New Tab" submenu.
1102+
// Return Value:
1103+
// - <none>
1104+
void AppHost::_SystemMenuNewTabProfilesChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::TerminalApp::SystemMenuNewTabProfilesArgs& args)
1105+
{
1106+
if (!_window)
1107+
{
1108+
return;
1109+
}
1110+
1111+
_window->RemoveSystemSubMenu(L"New Tab");
1112+
1113+
if (!args || !args.Profiles() || args.Profiles().Size() == 0)
1114+
{
1115+
return;
1116+
}
1117+
1118+
std::vector<std::pair<winrt::hstring, winrt::delegate<void()>>> profileItems;
1119+
profileItems.reserve(args.Profiles().Size());
1120+
1121+
for (const auto& item : args.Profiles())
1122+
{
1123+
auto handler = item.Handler();
1124+
auto callback = winrt::delegate<void()>([handler]() {
1125+
if (handler)
1126+
{
1127+
handler();
1128+
}
1129+
});
1130+
1131+
profileItems.emplace_back(item.DisplayName(), std::move(callback));
1132+
}
1133+
1134+
_window->AddSystemSubMenu(L"New Tab", profileItems);
1135+
}
1136+
10941137
// Method Description:
10951138
// - BODGY workaround for GH#9320. When the window moves, dismiss all the popups
10961139
// in the UI tree. Xaml Islands unfortunately doesn't do this for us, see

src/cascadia/WindowsTerminal/AppHost.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ class AppHost : public std::enable_shared_from_this<AppHost>
131131
void _HandleRequestLaunchPosition(const winrt::Windows::Foundation::IInspectable& sender,
132132
winrt::TerminalApp::LaunchPositionRequest args);
133133

134+
void _SystemMenuNewTabProfilesChanged(const winrt::Windows::Foundation::IInspectable& sender,
135+
const winrt::TerminalApp::SystemMenuNewTabProfilesArgs& args);
136+
134137
// Helper struct. By putting these all into one struct, we can revoke them
135138
// all at once, by assigning _revokers to a fresh Revokers instance. That'll
136139
// 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<AppHost>
160163
winrt::TerminalApp::TerminalWindow::PropertyChanged_revoker PropertyChanged;
161164
winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged;
162165
winrt::TerminalApp::TerminalWindow::WindowSizeChanged_revoker WindowSizeChanged;
166+
winrt::TerminalApp::TerminalWindow::SystemMenuNewTabProfilesChanged_revoker SystemMenuNewTabProfilesChanged;
163167
} _revokers{};
164168

165169
// our IslandWindow is not a WinRT type. It can't make auto_revokers like

src/cascadia/WindowsTerminal/IslandWindow.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,6 +1808,51 @@ void IslandWindow::AddToSystemMenu(const winrt::hstring& itemLabel, winrt::deleg
18081808
_systemMenuNextItemId++;
18091809
}
18101810

1811+
void IslandWindow::AddSystemSubMenu(const winrt::hstring& menuLabel, const std::vector<std::pair<winrt::hstring, winrt::delegate<void()>>>& items)
1812+
{
1813+
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
1814+
1815+
HMENU subMenu = CreatePopupMenu();
1816+
if (!subMenu)
1817+
{
1818+
LOG_LAST_ERROR();
1819+
return;
1820+
}
1821+
1822+
for (const auto& [label, callback] : items)
1823+
{
1824+
auto wID = _systemMenuNextItemId;
1825+
1826+
MENUITEMINFOW item;
1827+
item.cbSize = sizeof(MENUITEMINFOW);
1828+
item.fMask = MIIM_STATE | MIIM_ID | MIIM_STRING;
1829+
item.fState = MF_ENABLED;
1830+
item.wID = wID;
1831+
item.dwTypeData = const_cast<LPWSTR>(label.c_str());
1832+
item.cch = static_cast<UINT>(label.size());
1833+
1834+
if (LOG_LAST_ERROR_IF(!InsertMenuItemW(subMenu, wID, FALSE, &item)))
1835+
{
1836+
continue;
1837+
}
1838+
_systemMenuItems.insert({ wID, { label, callback } });
1839+
_systemMenuNextItemId++;
1840+
}
1841+
1842+
MENUITEMINFOW subMenuItem;
1843+
subMenuItem.cbSize = sizeof(MENUITEMINFOW);
1844+
subMenuItem.fMask = MIIM_STRING | MIIM_SUBMENU;
1845+
subMenuItem.hSubMenu = subMenu;
1846+
subMenuItem.dwTypeData = const_cast<LPWSTR>(menuLabel.c_str());
1847+
subMenuItem.cch = static_cast<UINT>(menuLabel.size());
1848+
1849+
if (LOG_LAST_ERROR_IF(!InsertMenuItemW(systemMenu, _systemMenuNextItemId, FALSE, &subMenuItem)))
1850+
{
1851+
DestroyMenu(subMenu);
1852+
return;
1853+
}
1854+
}
1855+
18111856
void IslandWindow::RemoveFromSystemMenu(const winrt::hstring& itemLabel)
18121857
{
18131858
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
@@ -1832,6 +1877,53 @@ void IslandWindow::RemoveFromSystemMenu(const winrt::hstring& itemLabel)
18321877
_systemMenuItems.erase(it->first);
18331878
}
18341879

1880+
// Method Description:
1881+
// - Removes a submenu from the system menu by its label. Also removes all
1882+
// child items from the _systemMenuItems tracking map. The submenu HMENU
1883+
// is destroyed automatically by DeleteMenu.
1884+
// Arguments:
1885+
// - menuLabel: The label of the submenu to remove.
1886+
void IslandWindow::RemoveSystemSubMenu(const winrt::hstring& menuLabel)
1887+
{
1888+
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
1889+
const auto itemCount = GetMenuItemCount(systemMenu);
1890+
if (LOG_LAST_ERROR_IF(itemCount == -1))
1891+
{
1892+
return;
1893+
}
1894+
1895+
for (int i = 0; i < itemCount; i++)
1896+
{
1897+
wchar_t buffer[256]{};
1898+
MENUITEMINFOW mii{};
1899+
mii.cbSize = sizeof(MENUITEMINFOW);
1900+
mii.fMask = MIIM_STRING | MIIM_SUBMENU;
1901+
mii.dwTypeData = buffer;
1902+
mii.cch = ARRAYSIZE(buffer);
1903+
1904+
if (!GetMenuItemInfoW(systemMenu, static_cast<UINT>(i), TRUE, &mii))
1905+
{
1906+
continue;
1907+
}
1908+
1909+
if (mii.hSubMenu && menuLabel == std::wstring_view{ buffer })
1910+
{
1911+
const auto subItemCount = GetMenuItemCount(mii.hSubMenu);
1912+
for (int j = 0; j < subItemCount; j++)
1913+
{
1914+
const auto subItemId = GetMenuItemID(mii.hSubMenu, j);
1915+
if (subItemId != static_cast<UINT>(-1))
1916+
{
1917+
_systemMenuItems.erase(subItemId);
1918+
}
1919+
}
1920+
1921+
DeleteMenu(systemMenu, static_cast<UINT>(i), MF_BYPOSITION);
1922+
return;
1923+
}
1924+
}
1925+
}
1926+
18351927
void IslandWindow::_resetSystemMenu()
18361928
{
18371929
// GetSystemMenu(..., true) will revert the menu to the default state.

0 commit comments

Comments
 (0)