From aa7dee58d81817285208471074f1af598ebf0c98 Mon Sep 17 00:00:00 2001 From: Cecilio Pardo Date: Wed, 11 Sep 2024 15:44:28 +0200 Subject: [PATCH] Support GUI dialogs and message boxes better on MS-Windows * src/w32menu.c (TASKDIALOG_COMMON_BUTTON_FLAGS, TASKDIALOG_FLAGS) (PFTASKDIALOGCALLBACK, TASKDIALOG_BUTTON, TASKDIALOGCONFIG) (TDN_CREATED, TDM_ENABLE_BUTTON, TDF_ALLOW_DIALOG_CANCELLATION) (TD_INFORMATION_ICON) [!MINGW_W64]: Define. (TaskDialogIndirect_Proc): Declare function type. (TASK_DIALOG_MAX_BUTTONS): New macro. (task_dialog_callback): New callback function. (w32_popup_dialog): Add dialog implementation using TaskDialog. (globals_of_w32menu): Load TaskDialogIndirect from comctl32.dll. (Bug#20481) --- src/menu.c | 7 +- src/w32menu.c | 214 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 216 insertions(+), 5 deletions(-) diff --git a/src/menu.c b/src/menu.c index de4d0964e9c..6b4aaef1715 100644 --- a/src/menu.c +++ b/src/menu.c @@ -1594,9 +1594,10 @@ for instance using the window manager, then this produces a quit and Lisp_Object selection = FRAME_TERMINAL (f)->popup_dialog_hook (f, header, contents); #ifdef HAVE_NTGUI - /* NTGUI supports only simple dialogs with Yes/No choices. For - other dialogs, it returns the symbol 'unsupported--w32-dialog', - as a signal for the caller to fall back to the emulation code. */ + /* NTGUI on Windows versions before Vista supports only simple + dialogs with Yes/No choices. For other dialogs, it returns the + symbol 'unsupported--w32-dialog', as a signal for the caller to + fall back to the emulation code. */ if (!EQ (selection, Qunsupported__w32_dialog)) #endif return selection; diff --git a/src/w32menu.c b/src/w32menu.c index cea4f4892a4..c3d147841b6 100644 --- a/src/w32menu.c +++ b/src/w32menu.c @@ -52,6 +52,9 @@ along with GNU Emacs. If not, see . */ #include "w32common.h" /* for osinfo_cache */ +#include "commctrl.h" + +/* This only applies to OS versions prior to Vista. */ #undef HAVE_DIALOGS /* TODO: Implement native dialogs. */ #ifndef TRUE @@ -77,6 +80,66 @@ typedef int (WINAPI * MessageBoxW_Proc) ( IN const WCHAR *caption, IN UINT type); +#ifndef MINGW_W64 +/* mingw.org's MinGW doesn't have this in its header files. */ + typedef int TASKDIALOG_COMMON_BUTTON_FLAGS; + + typedef int TASKDIALOG_FLAGS; + + typedef HRESULT (CALLBACK *PFTASKDIALOGCALLBACK) ( + HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, LONG_PTR lpRefData); + + typedef struct _TASKDIALOG_BUTTON { + int nButtonID; + PCWSTR pszButtonText; + } TASKDIALOG_BUTTON; + + typedef struct _TASKDIALOGCONFIG { + UINT cbSize; + HWND hwndParent; + HINSTANCE hInstance; + TASKDIALOG_FLAGS dwFlags; + TASKDIALOG_COMMON_BUTTON_FLAGS dwCommonButtons; + PCWSTR pszWindowTitle; + union { + HICON hMainIcon; + PCWSTR pszMainIcon; + } DUMMYUNIONNAME; + PCWSTR pszMainInstruction; + PCWSTR pszContent; + UINT cButtons; + const TASKDIALOG_BUTTON *pButtons; + int nDefaultButton; + UINT cRadioButtons; + const TASKDIALOG_BUTTON *pRadioButtons; + int nDefaultRadioButton; + PCWSTR pszVerificationText; + PCWSTR pszExpandedInformation; + PCWSTR pszExpandedControlText; + PCWSTR pszCollapsedControlText; + union { + HICON hFooterIcon; + PCWSTR pszFooterIcon; + } DUMMYUNIONNAME2; + PCWSTR pszFooter; + PFTASKDIALOGCALLBACK pfCallback; + LONG_PTR lpCallbackData; + UINT cxWidth; + } TASKDIALOGCONFIG; + +# define TDN_CREATED 0 +# define TDM_ENABLE_BUTTON (WM_USER+111) +# define TDF_ALLOW_DIALOG_CANCELLATION 0x8 +# define TD_INFORMATION_ICON MAKEINTRESOURCEW (-3) + +#endif + +typedef HRESULT (WINAPI *TaskDialogIndirect_Proc) ( + IN const TASKDIALOGCONFIG *pTaskConfig, + OUT int *pnButton, + OUT int *pnRadioButton, + OUT BOOL *pfVerificationFlagChecked); + #ifdef NTGUI_UNICODE GetMenuItemInfoA_Proc get_menu_item_info = GetMenuItemInfoA; SetMenuItemInfoA_Proc set_menu_item_info = SetMenuItemInfoA; @@ -89,6 +152,8 @@ AppendMenuW_Proc unicode_append_menu = NULL; MessageBoxW_Proc unicode_message_box = NULL; #endif /* NTGUI_UNICODE */ +static TaskDialogIndirect_Proc task_dialog_indirect; + #ifdef HAVE_DIALOGS static Lisp_Object w32_dialog_show (struct frame *, Lisp_Object, Lisp_Object, char **); #else @@ -101,14 +166,155 @@ static int fill_in_menu (HMENU, widget_value *); void w32_free_menu_strings (HWND); +#define TASK_DIALOG_MAX_BUTTONS 10 + +static HRESULT CALLBACK +task_dialog_callback (HWND hwnd, UINT msg, WPARAM wParam, + LPARAM lParam, LONG_PTR callback_data) +{ + switch (msg) + { + case TDN_CREATED: + /* Disable all buttons with ID >= 2000 */ + for (int i = 0; i < TASK_DIALOG_MAX_BUTTONS; i++) + SendMessage (hwnd, TDM_ENABLE_BUTTON, 2000 + i, FALSE); + break; + } + return S_OK; +} + Lisp_Object w32_popup_dialog (struct frame *f, Lisp_Object header, Lisp_Object contents) { - check_window_system (f); -#ifndef HAVE_DIALOGS + if (task_dialog_indirect) + { + int wide_len; + CHECK_CONS (contents); + + /* Get the title as an UTF-16 string. */ + char *title = SSDATA (ENCODE_UTF_8 (XCAR (contents))); + wide_len = (sizeof (WCHAR) + * pMultiByteToWideChar (CP_UTF8, 0, title, -1, NULL, 0)); + WCHAR *title_w = alloca (wide_len); + pMultiByteToWideChar (CP_UTF8, 0, title, -1, title_w, wide_len); + + /* Prepare the arrays with the dialog's buttons and return values. */ + TASKDIALOG_BUTTON buttons[TASK_DIALOG_MAX_BUTTONS]; + Lisp_Object button_values[TASK_DIALOG_MAX_BUTTONS]; + int button_count = 0; + Lisp_Object b = XCDR (contents); + + while (!NILP (b)) + { + if (button_count >= TASK_DIALOG_MAX_BUTTONS) + { + /* We have too many buttons. We ignore the rest. */ + break; + } + + Lisp_Object item = XCAR (b); + + if (CONSP (item)) + { + /* A normal item (text . value) */ + Lisp_Object item_name = XCAR (item); + Lisp_Object item_value = XCDR (item); + + CHECK_STRING (item_name); + + item_name = ENCODE_UTF_8 (item_name); + wide_len = (sizeof (WCHAR) + * pMultiByteToWideChar (CP_UTF8, 0, SSDATA (item_name), + -1, NULL, 0)); + buttons[button_count].pszButtonText = alloca (wide_len); + pMultiByteToWideChar (CP_UTF8, 0, SSDATA (item_name), -1, + (LPWSTR) + buttons[button_count].pszButtonText, + wide_len); + buttons[button_count].nButtonID = 1000 + button_count; + button_values[button_count++] = item_value; + } + else if (NILP (item)) + { + /* A nil item means to put all following items on the + right. We ignore this. */ + } + else if (STRINGP (item)) + { + /* A string item means an unselectable button. We add a + button, and then need to disable it on the callback. We + use ids based on 2000 to mark these buttons. */ + Lisp_Object item_name = ENCODE_UTF_8 (item); + wide_len = (sizeof (WCHAR) + * pMultiByteToWideChar (CP_UTF8, 0, + SSDATA (item_name), + -1, NULL, 0)); + buttons[button_count].pszButtonText = alloca (wide_len); + pMultiByteToWideChar (CP_UTF8, 0, SSDATA (item_name), -1, + (LPWSTR) + buttons[button_count].pszButtonText, + wide_len); + buttons[button_count].nButtonID = 2000 + button_count; + button_values[button_count++] = Qnil; + } + else + { + error ("Incorrect dialog button specification"); + return Qnil; + } + + b = XCDR (b); + } + + int pressed_button = 0; + + TASKDIALOGCONFIG config = { 0 }; + config.hwndParent = FRAME_W32_WINDOW (f); + config.cbSize = sizeof (config); + config.hInstance = hinst; + config.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION; + config.pfCallback = task_dialog_callback; + config.pszWindowTitle = L"Question"; + if (!NILP (header)) + { + config.pszWindowTitle = L"Information"; + config.pszMainIcon = TD_INFORMATION_ICON; + } + + config.pszMainInstruction = title_w; + config.pButtons = buttons; + config.cButtons = button_count; + + if (!SUCCEEDED (task_dialog_indirect (&config, &pressed_button, + NULL, NULL))) + quit (); + + int button_index; + switch (pressed_button) + { + case IDOK: + /* This can only happen if no buttons were provided. The OK + button is automatically added by TaskDialogIndirect in that + case. */ + return Qt; + case IDCANCEL: + /* The user closed the dialog without using the buttons. */ + return quit (); + default: + /* One of the specified buttons. */ + button_index = pressed_button - 1000; + if (button_index >= 0 && button_index < button_count) + return button_values[button_index]; + return quit (); + } + } + + /* If we get here, TaskDialog is not supported. Use MessageBox/Menu. */ + +#ifndef HAVE_DIALOGS /* Handle simple Yes/No choices as MessageBox popups. */ if (is_simple_dialog (contents)) return simple_dialog_show (f, contents, header); @@ -1618,6 +1824,10 @@ syms_of_w32menu (void) void globals_of_w32menu (void) { + HMODULE comctrl32 = GetModuleHandle ("comctl32.dll"); + task_dialog_indirect = (TaskDialogIndirect_Proc) + get_proc_addr (comctrl32, "TaskDialogIndirect"); + #ifndef NTGUI_UNICODE /* See if Get/SetMenuItemInfo functions are available. */ HMODULE user32 = GetModuleHandle ("user32.dll");