Browse Source

Use a custom tooltip window for the tray icon

Built-in tray notification icon has a tip text length limit of 128
characters which is often limited for showing the connected profile name,
connected since time and IP addresses. If the profile name is long the IP
numbers could get truncated.

Fix by using a custom tooltip window and display it when mouse hovers over
the icon. As the status bar need not be at the bottom of the screen (could be
at right, left or top as well), the location of the window is chosen based
on the mouse co-ordinates that trigger the hover event.

In case of errors while setting up the tooltip window, fall back to the current
behaviour.

If the message is too long to include time and IP, truncate the profile name
part of the message.

v2: Do not use wParam in NIN_POPUOPEN message as it does not seem to work
    on Windows 11. Instead use GetCursorPos() for mouse location.

Fixes issue #666

Signed-off-by: Selva Nair <selva.nair@gmail.com>
pull/658/merge
Selva Nair 10 months ago
parent
commit
0c9ae87e0f
  1. 133
      tray.c

133
tray.c

@ -28,6 +28,7 @@
#include <shellapi.h> #include <shellapi.h>
#include <tchar.h> #include <tchar.h>
#include <time.h> #include <time.h>
#include <commctrl.h>
#include "tray.h" #include "tray.h"
#include "main.h" #include "main.h"
@ -47,6 +48,10 @@ int hmenu_size = 0; /* allocated size of hMenuConn array */
HBITMAP hbmpConnecting; HBITMAP hbmpConnecting;
NOTIFYICONDATA ni; NOTIFYICONDATA ni;
HWND traytip; /* handle of tooltip window for tray icon */
TOOLINFO ti; /* global tool info structure for tool tip of tray icon*/
WCHAR tip_msg[500]; /* global buffer for tray tip message */
extern options_t o; extern options_t o;
#define USE_NESTED_CONFIG_MENU ((o.config_menu_view == CONFIG_VIEW_AUTO && o.num_configs > 25) \ #define USE_NESTED_CONFIG_MENU ((o.config_menu_view == CONFIG_VIEW_AUTO && o.num_configs > 25) \
@ -351,6 +356,33 @@ RecreatePopupMenus(void)
CreatePopupMenus(); CreatePopupMenus();
} }
/*
* Position tool tip window so that it does not overlap with the mouse position
* and does not spill out of the screen. If mouse location overlaps, NIM_POPUPCLOSE
* and NIM_POPUPOPEN will trigger continuously.
* Account for the possibility that the taskbar could be at the bottom, right, top
* or left edge of the screen.
*
* On input (x, y) is the mouse position that triggered the display of tooltip
*/
static void
PositionTrayToolTip(LONG x, LONG y)
{
RECT r;
LONG cxmax = GetSystemMetrics(SM_CXSCREEN);
GetWindowRect(traytip, &r);
LONG w = r.right - r.left;
LONG h = r.bottom - r.top;
/* - vertically, position the bottom of the window 10 pixels above y or top of the window
* 10 pixels below y depending on whether we are closer to the bottom or top of the screen.
* - horizontally, try to centre around x adjusting for overflow to the right or left
*/
r.left = (x < w/2) ? 0 : ((x + w/2 < cxmax) ? x - w/2 : cxmax - w);
r.top = (y > h + 10) ? y - (h + 10) : y + 10;
SendMessageW(traytip, TTM_TRACKPOSITION, 0, MAKELONG(r.left, r.top));
SetWindowPos(traytip, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE);
}
/* /*
* Handle mouse clicks on tray icon * Handle mouse clicks on tray icon
*/ */
@ -359,7 +391,8 @@ OnNotifyTray(LPARAM lParam)
{ {
POINT pt; POINT pt;
switch (lParam) /* Use LOWORD(lParam) as HIWORD() contains the icon id if uVersion >= 4 */
switch (LOWORD(lParam))
{ {
case WM_RBUTTONUP: case WM_RBUTTONUP:
RecreatePopupMenus(); RecreatePopupMenus();
@ -401,6 +434,23 @@ OnNotifyTray(LPARAM lParam)
} }
break; break;
/* handle messages when mouse enters and leaves the icon -- we show the custom tooltip window */
case NIN_POPUPOPEN:
if (traytip)
{
GetCursorPos(&pt);
SendMessageW(traytip, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM) &ti);
PositionTrayToolTip(pt.x, pt.y); /* taking position from wParam do snot work on Win11! */
}
break;
case NIN_POPUPCLOSE:
if (traytip)
{
SendMessageW(traytip, TTM_TRACKACTIVATE, (WPARAM)FALSE, 0);
}
break;
case WM_OVPN_RESCAN: case WM_OVPN_RESCAN:
/* Rescan config folders and recreate popup menus */ /* Rescan config folders and recreate popup menus */
RecreatePopupMenus(); RecreatePopupMenus();
@ -416,6 +466,11 @@ RemoveTrayIcon()
Shell_NotifyIcon(NIM_DELETE, &ni); Shell_NotifyIcon(NIM_DELETE, &ni);
CLEAR(ni); CLEAR(ni);
} }
if (traytip)
{
DestroyWindow(traytip);
traytip = NULL;
}
} }
void void
@ -435,32 +490,59 @@ ShowTrayIcon()
ni.uCallbackMessage = WM_NOTIFYICONTRAY; ni.uCallbackMessage = WM_NOTIFYICONTRAY;
ni.hIcon = LoadLocalizedSmallIcon(ID_ICO_DISCONNECTED); ni.hIcon = LoadLocalizedSmallIcon(ID_ICO_DISCONNECTED);
_tcsncpy(ni.szTip, _T(PACKAGE_NAME), _countof(_T(PACKAGE_NAME))); _tcsncpy(ni.szTip, _T(PACKAGE_NAME), _countof(_T(PACKAGE_NAME)));
ni.uVersion = NOTIFYICON_VERSION_4;
Shell_NotifyIcon(NIM_ADD, &ni); Shell_NotifyIcon(NIM_ADD, &ni);
/* try to set version 4 and a custom tooltip window */
if (Shell_NotifyIcon(NIM_SETVERSION, &ni))
{
/* create a custom tooltip for the tray */
traytip = CreateWindowEx(0, TOOLTIPS_CLASS, NULL, WS_POPUP |TTS_ALWAYSTIP,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
o.hWnd, NULL, o.hInstance, NULL);
if (!traytip) /* revert the version back so that we can use legacy ni.szTip for tip text */
{
ni.uVersion = 0;
Shell_NotifyIcon(NIM_SETVERSION, &ni);
}
}
if (traytip)
{
LONG cx = GetSystemMetrics(SM_CXSCREEN)/4; /* max width of tray tooltip = 25% of screen */
ti.cbSize = sizeof(ti);
ti.uId = (UINT_PTR) traytip;
ti.uFlags = TTF_ABSOLUTE|TTF_TRACK|TTF_IDISHWND;
ti.hwnd = o.hWnd;
ti.lpszText = L" ";
SendMessage(traytip, TTM_ADDTOOL, 0, (LPARAM)&ti);
SendMessage(traytip, TTM_SETTITLE, TTI_NONE, (LPARAM) _T(PACKAGE_NAME));
SendMessage(traytip, TTM_SETMAXTIPWIDTH, 0, (LPARAM) cx);
}
} }
void void
SetTrayIcon(conn_state_t state) SetTrayIcon(conn_state_t state)
{ {
TCHAR msg[500];
TCHAR msg_connected[100]; TCHAR msg_connected[100];
TCHAR msg_connecting[100]; TCHAR msg_connecting[100];
BOOL first_conn; BOOL first_conn;
UINT icon_id; UINT icon_id;
connection_t *cc = NULL; /* a connected config */ connection_t *cc = NULL; /* a connected config */
_tcsncpy(msg, _T(PACKAGE_NAME), _countof(_T(PACKAGE_NAME)));
_tcsncpy(msg_connected, LoadLocalizedString(IDS_TIP_CONNECTED), _countof(msg_connected)); _tcsncpy(msg_connected, LoadLocalizedString(IDS_TIP_CONNECTED), _countof(msg_connected));
_tcsncpy(msg_connecting, LoadLocalizedString(IDS_TIP_CONNECTING), _countof(msg_connecting)); _tcsncpy(msg_connecting, LoadLocalizedString(IDS_TIP_CONNECTING), _countof(msg_connecting));
tip_msg[0] = L'\0';
first_conn = TRUE; first_conn = TRUE;
for (connection_t *c = o.chead; c; c = c->next) for (connection_t *c = o.chead; c; c = c->next)
{ {
if (c->state == connected) if (c->state == connected)
{ {
/* Append connection name to Icon Tip Msg */ /* Append connection name to Icon Tip Msg */
_tcsncat(msg, (first_conn ? msg_connected : _T(", ")), _countof(msg) - _tcslen(msg) - 1); _tcsncat(tip_msg, (first_conn ? msg_connected : _T(", ")), _countof(tip_msg) - _tcslen(tip_msg) - 1);
_tcsncat(msg, c->config_name, _countof(msg) - _tcslen(msg) - 1); _tcsncat(tip_msg, c->config_name, _countof(tip_msg) - _tcslen(tip_msg) - 1);
first_conn = FALSE; first_conn = FALSE;
cc = c; cc = c;
} }
@ -472,8 +554,8 @@ SetTrayIcon(conn_state_t state)
if (c->state == connecting || c->state == resuming || c->state == reconnecting) if (c->state == connecting || c->state == resuming || c->state == reconnecting)
{ {
/* Append connection name to Icon Tip Msg */ /* Append connection name to Icon Tip Msg */
_tcsncat(msg, (first_conn ? msg_connecting : _T(", ")), _countof(msg) - _tcslen(msg) - 1); _tcsncat(tip_msg, (first_conn ? msg_connecting : _T(", ")), _countof(tip_msg) - _tcslen(tip_msg) - 1);
_tcsncat(msg, c->config_name, _countof(msg) - _tcslen(msg) - 1); _tcsncat(tip_msg, c->config_name, _countof(tip_msg) - _tcslen(tip_msg) - 1);
first_conn = FALSE; first_conn = FALSE;
} }
} }
@ -482,16 +564,26 @@ SetTrayIcon(conn_state_t state)
{ {
/* Append "Connected since and assigned IP" to message */ /* Append "Connected since and assigned IP" to message */
TCHAR time[50]; TCHAR time[50];
WCHAR ip[64];
/* If there is not enough space in the message to add time and IP, truncate it.
* This works only if custom tooltip window is in use.
* Include about 50 characters for "Connected since:" and "Assigned IP:" prefixes.
*/
size_t max_msglen = _countof(tip_msg) - (_countof(time) + _countof(ip) + 50);
if (wcslen(tip_msg) > max_msglen && traytip)
{
wcsncpy_s(&tip_msg[max_msglen-1], 2, L"", _TRUNCATE);
}
LocalizedTime(cc->connected_since, time, _countof(time)); LocalizedTime(cc->connected_since, time, _countof(time));
_tcsncat(msg, LoadLocalizedString(IDS_TIP_CONNECTED_SINCE), _countof(msg) - _tcslen(msg) - 1); _tcsncat(tip_msg, LoadLocalizedString(IDS_TIP_CONNECTED_SINCE), _countof(tip_msg) - _tcslen(tip_msg) - 1);
_tcsncat(msg, time, _countof(msg) - _tcslen(msg) - 1); _tcsncat(tip_msg, time, _countof(tip_msg) - _tcslen(tip_msg) - 1);
/* concatenate ipv4 and ipv6 addresses into one string */ /* concatenate ipv4 and ipv6 addresses into one string */
WCHAR ip[64];
wcs_concat2(ip, _countof(ip), cc->ip, cc->ipv6, L", "); wcs_concat2(ip, _countof(ip), cc->ip, cc->ipv6, L", ");
WCHAR *assigned_ip = LoadLocalizedString(IDS_TIP_ASSIGNED_IP, ip); WCHAR *assigned_ip = LoadLocalizedString(IDS_TIP_ASSIGNED_IP, ip);
_tcsncat(msg, assigned_ip, _countof(msg) - _tcslen(msg) - 1); _tcsncat(tip_msg, assigned_ip, _countof(tip_msg) - _tcslen(tip_msg) - 1);
} }
icon_id = ID_ICO_CONNECTING; icon_id = ID_ICO_CONNECTING;
@ -508,9 +600,24 @@ SetTrayIcon(conn_state_t state)
ni.uID = 0; ni.uID = 0;
ni.hWnd = o.hWnd; ni.hWnd = o.hWnd;
ni.hIcon = LoadLocalizedSmallIcon(icon_id); ni.hIcon = LoadLocalizedSmallIcon(icon_id);
ni.uFlags = NIF_MESSAGE | NIF_TIP | NIF_ICON; ni.uFlags = NIF_MESSAGE | NIF_ICON;
ni.uCallbackMessage = WM_NOTIFYICONTRAY; ni.uCallbackMessage = WM_NOTIFYICONTRAY;
_tcsncpy(ni.szTip, msg, _countof(ni.szTip));
if (traytip)
{
/* Set msg as the tool tip text -- skip the leading "\n" */
WCHAR *msgp = tip_msg;
msgp += (msgp[0] == L'\n') ? 1 : 0;
/* tool tip window needs a non-empty text to show */
ti.lpszText = wcslen(msgp) ? msgp : L" ";
SendMessage(traytip, TTM_UPDATETIPTEXT, 0, (LPARAM) &ti);
}
else
{
wcsncpy_s(ni.szTip, _countof(ni.szTip), _T(PACKAGE_NAME), _TRUNCATE);
wcsncat_s(ni.szTip, _countof(ni.szTip), tip_msg, _TRUNCATE);
ni.uFlags |= NIF_TIP;
}
Shell_NotifyIcon(NIM_MODIFY, &ni); Shell_NotifyIcon(NIM_MODIFY, &ni);
} }

Loading…
Cancel
Save