// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "icon.h" #include "NotificationIcon.h" #include "CustomWindowMessages.h" #include #include using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Microsoft::Terminal; NotificationIcon::NotificationIcon(const HWND owningHwnd) : _owningHwnd{ owningHwnd } { CreateNotificationIcon(); } NotificationIcon::~NotificationIcon() { RemoveIconFromNotificationArea(); } void NotificationIcon::_CreateWindow() { WNDCLASSW wc{}; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.hInstance = wil::GetModuleInstanceHandle(); wc.lpszClassName = L"NOTIFICATION_ICON_HOSTING_WINDOW_CLASS"; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = DefWindowProcW; wc.hIcon = static_cast(GetActiveAppIconHandle(true)); RegisterClass(&wc); _notificationIconHwnd = wil::unique_hwnd(CreateWindowW(wc.lpszClassName, wc.lpszClassName, WS_DISABLED, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND_MESSAGE, nullptr, wc.hInstance, nullptr)); WINRT_VERIFY(_notificationIconHwnd); } // Method Description: // - Creates and adds an icon to the notification area. // If an icon already exists, update the HWND associated // to the icon with this window's HWND. // Arguments: // - // Return Value: // - void NotificationIcon::CreateNotificationIcon() { if (!_notificationIconHwnd) { // Creating a disabled, non visible window just so we can set it // as the foreground window when showing the context menu. // This is done so that the context menu can be dismissed // when clicking outside of it. _CreateWindow(); } NOTIFYICONDATA nid{}; nid.cbSize = sizeof(NOTIFYICONDATA); // This HWND will receive the callbacks sent by the notification icon. nid.hWnd = _owningHwnd; // App-defined identifier of the icon. The HWND and ID are used // to identify which icon to operate on when calling Shell_NotifyIcon. // Multiple icons can be associated with one HWND, but here we're only // going to be showing one so the ID doesn't really matter. nid.uID = 1; nid.uCallbackMessage = CM_NOTIFY_FROM_NOTIFICATION_AREA; // AppName happens to be in CascadiaPackage's Resources. ScopedResourceLoader loader{ L"Resources" }; const auto appNameLoc = loader.GetLocalizedString(L"AppName"); nid.hIcon = static_cast(GetActiveAppIconHandle(true)); StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), appNameLoc.c_str()); nid.uFlags = NIF_MESSAGE | NIF_SHOWTIP | NIF_TIP | NIF_ICON; Shell_NotifyIcon(NIM_ADD, &nid); // For whatever reason, the NIM_ADD call doesn't seem to set the version // properly, resulting in us being unable to receive the expected notification // events. We actually have to make a separate NIM_SETVERSION call for it to // work properly. nid.uVersion = NOTIFYICON_VERSION_4; Shell_NotifyIcon(NIM_SETVERSION, &nid); _notificationIconData = nid; } // Method Description: // - This creates our context menu and displays it at the given // screen coordinates. // Arguments: // - coord: The coordinates where we should be showing the context menu. // - peasants: The map of all peasants that should be available in the context menu. // Return Value: // - void NotificationIcon::ShowContextMenu(const til::point& coord, const IVectorView& peasants) { if (const auto hMenu = _CreateContextMenu(peasants)) { // We'll need to set our window to the foreground before calling // TrackPopupMenuEx or else the menu won't dismiss when clicking away. SetForegroundWindow(_notificationIconHwnd.get()); // User can select menu items with the left and right buttons. UINT uFlags = TPM_RIGHTBUTTON; // Nonzero if drop-down menus are right-aligned with the corresponding menu-bar item // 0 if the menus are left-aligned. if (GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0) { uFlags |= TPM_RIGHTALIGN; } else { uFlags |= TPM_LEFTALIGN; } TrackPopupMenuEx(hMenu, uFlags, gsl::narrow_cast(coord.x()), gsl::narrow_cast(coord.y()), _owningHwnd, NULL); } } // Method Description: // - This creates the context menu for our notification icon. // Arguments: // - peasants: A map of all peasants' ID to their window name. // Return Value: // - The handle to the newly created context menu. HMENU NotificationIcon::_CreateContextMenu(const IVectorView& peasants) { auto hMenu = CreatePopupMenu(); if (hMenu) { MENUINFO mi{}; mi.cbSize = sizeof(MENUINFO); mi.fMask = MIM_STYLE | MIM_APPLYTOSUBMENUS | MIM_MENUDATA; mi.dwStyle = MNS_NOTIFYBYPOS; mi.dwMenuData = NULL; SetMenuInfo(hMenu, &mi); // Focus Current Terminal Window AppendMenu(hMenu, MF_STRING, gsl::narrow(NotificationIconMenuItemAction::FocusTerminal), RS_(L"NotificationIconFocusTerminal").c_str()); AppendMenu(hMenu, MF_SEPARATOR, 0, L""); // Submenu for Windows if (auto submenu = CreatePopupMenu()) { for (const auto& p : peasants) { std::wstringstream displayText; displayText << L"#" << p.Id; if (!p.TabTitle.empty()) { displayText << L": " << std::wstring_view{ p.TabTitle }; } if (!p.Name.empty()) { displayText << L" [" << std::wstring_view{ p.Name } << L"]"; } AppendMenu(submenu, MF_STRING, gsl::narrow(p.Id), displayText.str().c_str()); } MENUINFO submenuInfo{}; submenuInfo.cbSize = sizeof(MENUINFO); submenuInfo.fMask = MIM_MENUDATA; submenuInfo.dwStyle = MNS_NOTIFYBYPOS; submenuInfo.dwMenuData = (UINT_PTR)NotificationIconMenuItemAction::SummonWindow; SetMenuInfo(submenu, &submenuInfo); AppendMenu(hMenu, MF_POPUP, (UINT_PTR)submenu, RS_(L"NotificationIconWindowSubmenu").c_str()); } } return hMenu; } // Method Description: // - This is the handler for when one of the menu items are selected within // the notification icon's context menu. // Arguments: // - menu: The handle to the menu that holds the menu item that was selected. // - menuItemIndex: The index of the menu item within the given menu. // Return Value: // - void NotificationIcon::MenuItemSelected(const HMENU menu, const UINT menuItemIndex) { // Check the menu's data for a specific action. MENUINFO mi{}; mi.cbSize = sizeof(MENUINFO); mi.fMask = MIM_MENUDATA; GetMenuInfo(menu, &mi); if (mi.dwMenuData) { if (gsl::narrow(mi.dwMenuData) == NotificationIconMenuItemAction::SummonWindow) { winrt::Microsoft::Terminal::Remoting::SummonWindowSelectionArgs args{}; args.WindowID(GetMenuItemID(menu, menuItemIndex)); args.SummonBehavior().ToggleVisibility(false); args.SummonBehavior().MoveToCurrentDesktop(false); args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); _SummonWindowRequestedHandlers(args); return; } } // Now check the menu item itself for an action. const auto action = gsl::narrow(GetMenuItemID(menu, menuItemIndex)); switch (action) { case NotificationIconMenuItemAction::FocusTerminal: { winrt::Microsoft::Terminal::Remoting::SummonWindowSelectionArgs args{}; args.SummonBehavior().ToggleVisibility(false); args.SummonBehavior().MoveToCurrentDesktop(false); args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); _SummonWindowRequestedHandlers(args); break; } } } // Method Description: // - This is the handler for when the notification icon itself is left-clicked. // Arguments: // - // Return Value: // - void NotificationIcon::NotificationIconPressed() { // No name in the args means summon the mru window. winrt::Microsoft::Terminal::Remoting::SummonWindowSelectionArgs args{}; args.SummonBehavior().MoveToCurrentDesktop(false); args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); args.SummonBehavior().ToggleVisibility(false); _SummonWindowRequestedHandlers(args); } // Method Description: // - Re-add a notification icon using our currently saved notification icon data. // Arguments: // - // Return Value: // - void NotificationIcon::ReAddNotificationIcon() { Shell_NotifyIcon(NIM_ADD, &_notificationIconData); Shell_NotifyIcon(NIM_SETVERSION, &_notificationIconData); } // Method Description: // - Deletes our notification icon. // Arguments: // - // Return Value: // - void NotificationIcon::RemoveIconFromNotificationArea() { Shell_NotifyIcon(NIM_DELETE, &_notificationIconData); }