terminal/src/cascadia/WindowsTerminal/NotificationIcon.cpp
2021-09-28 09:15:21 -07:00

273 lines
9.9 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "icon.h"
#include "NotificationIcon.h"
#include "CustomWindowMessages.h"
#include <ScopedResourceLoader.h>
#include <LibraryResources.h>
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<HICON>(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:
// - <unused>
// Return Value:
// - <none>
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<HICON>(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:
// - <none>
void NotificationIcon::ShowContextMenu(const til::point& coord,
IMapView<uint64_t, winrt::hstring> 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<int>(coord.x()), gsl::narrow_cast<int>(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(IMapView<uint64_t, winrt::hstring> 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<UINT_PTR>(NotificationIconMenuItemAction::FocusTerminal), RS_(L"NotificationIconFocusTerminal").c_str());
AppendMenu(hMenu, MF_SEPARATOR, 0, L"");
// Submenu for Windows
if (auto submenu = CreatePopupMenu())
{
const auto locWindow = RS_(L"WindowIdLabel");
const auto locUnnamed = RS_(L"UnnamedWindowName");
for (const auto [id, name] : peasants)
{
winrt::hstring displayText = name;
if (name.empty())
{
displayText = fmt::format(L"{} {} - <{}>", locWindow, id, locUnnamed);
}
AppendMenu(submenu, MF_STRING, gsl::narrow<UINT_PTR>(id), displayText.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:
// - <none>
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<NotificationIconMenuItemAction>(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<NotificationIconMenuItemAction>(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:
// - <none>
// Return Value:
// - <none>
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:
// - <none>
// Return Value:
// - <none>
void NotificationIcon::ReAddNotificationIcon()
{
Shell_NotifyIcon(NIM_ADD, &_notificationIconData);
Shell_NotifyIcon(NIM_SETVERSION, &_notificationIconData);
}
// Method Description:
// - Deletes our notification icon.
// Arguments:
// - <none>
// Return Value:
// - <none>
void NotificationIcon::RemoveIconFromNotificationArea()
{
Shell_NotifyIcon(NIM_DELETE, &_notificationIconData);
}