From 7f58a6f408ce7ab485d1d9977c4bf1b16fb82876 Mon Sep 17 00:00:00 2001 From: deepend-tildeclub Date: Mon, 6 Jul 2026 08:52:32 -0600 Subject: [PATCH] Use UIA to target Win taskbar minimize workaround --- src/fe-gtk/fe-gtk.vcxproj | 2 +- src/fe-gtk/maingui.c | 236 ++++++++++++++++++++++++++++++++++++++ src/fe-gtk/meson.build | 2 + 3 files changed, 239 insertions(+), 1 deletion(-) diff --git a/src/fe-gtk/fe-gtk.vcxproj b/src/fe-gtk/fe-gtk.vcxproj index a53f9314..cad076b5 100644 --- a/src/fe-gtk/fe-gtk.vcxproj +++ b/src/fe-gtk/fe-gtk.vcxproj @@ -31,7 +31,7 @@ $(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories) - $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;%(AdditionalDependencies) + $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;ole32.lib;oleaut32.lib;%(AdditionalDependencies) mainCRTStartup diff --git a/src/fe-gtk/maingui.c b/src/fe-gtk/maingui.c index fa4973c2..d7af2788 100644 --- a/src/fe-gtk/maingui.c +++ b/src/fe-gtk/maingui.c @@ -65,6 +65,7 @@ #ifdef G_OS_WIN32 #include #include +#include #include static void @@ -5004,6 +5005,234 @@ mg_tabwindow_de_cb (GtkWidget *widget, GdkEvent *event, gpointer user_data) } #ifdef G_OS_WIN32 +/* GTK3's win32 backend breaks the shell's minimize-on-taskbar-button-click: + * when the taskbar button of the active window is clicked, the window only + * loses activation but stays on screen + * (https://gitlab.gnome.org/GNOME/gtk/-/issues/3749). + * + * Activation state alone cannot identify that gesture: clicking empty + * taskbar space, the Start button, the tray or another application's + * button also deactivates this window while the pointer is over the + * taskbar, and in each case (including the failed minimize) activation + * ends up outside this window. So when activation is lost with the + * pointer over the taskbar, hit-test the click point with UI Automation: + * only a taskbar button whose accessible name refers to this window's + * title is treated as a minimize request, and the minimize is then + * performed manually after a short delay (which also lets a working + * shell minimize win, should GTK ever be fixed). + */ +#define MG_WIN32_TASKBAR_MINIMIZE_DELAY_MS 200 +#define MG_WIN32_UIA_BUTTON_CONTROL_TYPE 50000 /* UIA_ButtonControlTypeId */ +#define MG_WIN32_UIA_MAX_ANCESTORS 8 + +/* CLSID_CUIAutomation / IID_IUIAutomation, defined locally so no + * uuid import library is needed */ +static const CLSID mg_win32_clsid_cuiautomation = +{ 0xff48dba4, 0x60ef, 0x4201, { 0xaa, 0x87, 0x54, 0x10, 0x3e, 0xef, 0x59, 0x4e } }; +static const IID mg_win32_iid_iuiautomation = +{ 0x30cbe57d, 0xd9d0, 0x452a, { 0xab, 0x13, 0x7a, 0xc5, 0xac, 0x48, 0x25, 0xee } }; + +static guint mg_win32_taskbar_minimize_source_id = 0; +static HWND mg_win32_taskbar_minimize_hwnd = NULL; +static POINT mg_win32_taskbar_minimize_point; + +static gboolean +mg_win32_hwnd_is_taskbar (HWND hwnd) +{ + wchar_t class_name[32]; + HWND root; + + if (!hwnd) + return FALSE; + + root = GetAncestor (hwnd, GA_ROOT); + if (!root) + root = hwnd; + + if (GetClassNameW (root, class_name, G_N_ELEMENTS (class_name)) == 0) + return FALSE; + + return lstrcmpW (class_name, L"Shell_TrayWnd") == 0 + || lstrcmpW (class_name, L"Shell_SecondaryTrayWnd") == 0; +} + +/* The main thread is COM-initialized for the process lifetime (see main () + * in src/common/zoitechat.c), so the instance can be created lazily and + * kept until exit. */ +static IUIAutomation * +mg_win32_get_uia (void) +{ + static IUIAutomation *uia = NULL; + static gboolean failed = FALSE; + + if (uia == NULL && !failed) + { + if (FAILED (CoCreateInstance (&mg_win32_clsid_cuiautomation, NULL, + CLSCTX_INPROC_SERVER, + &mg_win32_iid_iuiautomation, + (void **)&uia))) + { + uia = NULL; + failed = TRUE; + } + } + + return uia; +} + +/* A taskbar button's accessible name refers to a window title if one + * string equals the other or extends it at a separator: the button of a + * single window carries the title, on Win11 with a suffix + * (" - 1 running window"), and either string may briefly trail + * the other while the shell catches up with a title change (e.g. the + * " (52)" user count appearing). A button grouping several windows is + * named after the application instead and deliberately does not match: + * clicking a group opens its thumbnails rather than minimizing. */ +static gboolean +mg_win32_button_name_matches_title (const wchar_t *name, const wchar_t *title) +{ + size_t name_len, title_len, common; + wchar_t next; + + if (!name || !title) + return FALSE; + + name_len = wcslen (name); + title_len = wcslen (title); + common = MIN (name_len, title_len); + + if (common == 0 || wcsncmp (name, title, common) != 0) + return FALSE; + + if (name_len == title_len) + return TRUE; + + next = (name_len > title_len) ? name[common] : title[common]; + return next == L' ' || next == L':' || next == L'-'; +} + +/* Was the click at pt on this window's own taskbar button? Hit-test the + * point and search the element and a few ancestors for a button whose + * name refers to this window. Empty taskbar space has no button ancestor, + * and other buttons (Start, tray icons, other applications' windows) + * fail the name check. */ +static gboolean +mg_win32_taskbar_click_on_own_button (IUIAutomation *uia, HWND hwnd, POINT pt) +{ + IUIAutomationTreeWalker *walker = NULL; + IUIAutomationElement *element = NULL; + wchar_t title[512]; + gboolean match = FALSE; + int depth; + + if (GetWindowTextW (hwnd, title, G_N_ELEMENTS (title)) == 0) + return FALSE; + + if (FAILED (uia->lpVtbl->ElementFromPoint (uia, pt, &element)) + || element == NULL) + return FALSE; + + if (FAILED (uia->lpVtbl->get_ControlViewWalker (uia, &walker))) + walker = NULL; + + for (depth = 0; element != NULL && depth < MG_WIN32_UIA_MAX_ANCESTORS; depth++) + { + IUIAutomationElement *parent = NULL; + CONTROLTYPEID control_type = 0; + + if (SUCCEEDED (element->lpVtbl->get_CurrentControlType (element, &control_type)) + && control_type == MG_WIN32_UIA_BUTTON_CONTROL_TYPE) + { + BSTR name = NULL; + + if (SUCCEEDED (element->lpVtbl->get_CurrentName (element, &name)) + && name != NULL) + { + match = mg_win32_button_name_matches_title (name, title); + SysFreeString (name); + } + break; + } + + if (walker == NULL + || FAILED (walker->lpVtbl->GetParentElement (walker, element, &parent))) + parent = NULL; + + element->lpVtbl->Release (element); + element = parent; + } + + if (element) + element->lpVtbl->Release (element); + if (walker) + walker->lpVtbl->Release (walker); + + return match; +} + +static gboolean +mg_win32_taskbar_minimize_cb (gpointer userdata) +{ + HWND hwnd = mg_win32_taskbar_minimize_hwnd; + POINT click_pos = mg_win32_taskbar_minimize_point; + IUIAutomation *uia; + + mg_win32_taskbar_minimize_source_id = 0; + mg_win32_taskbar_minimize_hwnd = NULL; + + /* Already minimized or gone: the shell's own minimize worked */ + if (!hwnd || !IsWindow (hwnd) || !IsWindowVisible (hwnd) || IsIconic (hwnd)) + return G_SOURCE_REMOVE; + + /* Button still held: a taskbar-button drag, not a click */ + if (GetAsyncKeyState (VK_LBUTTON) & 0x8000) + return G_SOURCE_REMOVE; + + uia = mg_win32_get_uia (); + if (uia != NULL) + { + if (!mg_win32_taskbar_click_on_own_button (uia, hwnd, click_pos)) + return G_SOURCE_REMOVE; + } + else + { + /* No UI Automation available: fall back to the activation + * signature - a failed shell minimize can leave this window + * as the foreground window. */ + HWND foreground = GetForegroundWindow (); + + if (foreground != NULL && foreground != hwnd) + return G_SOURCE_REMOVE; + } + + PostMessage (hwnd, WM_SYSCOMMAND, SC_MINIMIZE, 0); + return G_SOURCE_REMOVE; +} + +static void +mg_win32_taskbar_click_check (HWND hwnd) +{ + POINT cursor_pos; + + if (!IsWindowVisible (hwnd) || IsIconic (hwnd)) + return; + + if (!GetCursorPos (&cursor_pos)) + return; + + if (!mg_win32_hwnd_is_taskbar (WindowFromPoint (cursor_pos))) + return; + + if (mg_win32_taskbar_minimize_source_id) + g_source_remove (mg_win32_taskbar_minimize_source_id); + + mg_win32_taskbar_minimize_hwnd = hwnd; + mg_win32_taskbar_minimize_point = cursor_pos; + mg_win32_taskbar_minimize_source_id = + g_timeout_add (MG_WIN32_TASKBAR_MINIMIZE_DELAY_MS, + mg_win32_taskbar_minimize_cb, NULL); +} + static GdkFilterReturn mg_win32_filter (GdkXEvent *xevent, GdkEvent *event, gpointer data) { @@ -5012,6 +5241,13 @@ mg_win32_filter (GdkXEvent *xevent, GdkEvent *event, gpointer data) if (!msg) return GDK_FILTER_CONTINUE; + if (msg->message == WM_ACTIVATE) + { + if (LOWORD (msg->wParam) == WA_INACTIVE) + mg_win32_taskbar_click_check (msg->hwnd); + return GDK_FILTER_CONTINUE; + } + if (msg->message == WM_TIMECHANGE) { _tzset(); diff --git a/src/fe-gtk/meson.build b/src/fe-gtk/meson.build index 1454ba82..2ac1a06d 100644 --- a/src/fe-gtk/meson.build +++ b/src/fe-gtk/meson.build @@ -91,6 +91,8 @@ zoitechat_gtk_ldflags = [] if host_machine.system() == 'windows' zoitechat_gtk_sources += 'notifications/notification-windows.c' zoitechat_gtk_deps += cc.find_library('dwmapi', required: true) + zoitechat_gtk_deps += cc.find_library('ole32', required: true) # taskbar minimize (UI Automation) + zoitechat_gtk_deps += cc.find_library('oleaut32', required: true) # taskbar minimize (UI Automation) zoitechat_theme_deps += cc.find_library('dwmapi', required: true) else