From 8d41ecff3e0233589a45af50ad3c122b09db5334 Mon Sep 17 00:00:00 2001 From: Sergey Bugaev Date: Mon, 5 Aug 2024 17:29:56 +0300 Subject: [PATCH 1/2] Add gtk_show_uri_win32 () This is a new internal utility to open/show/launch/execute URIs and files on Windows, using Windows's native ShellExecuteEx () and SHOpenWithDialog () APIs. The advantages this has over using the win32 implementation of g_app_info_launch_default_for_uri (): * the implementation here is fairly simple; * it doesn't involve trying to grok the registry for app / file type registrations (at least not inside GLib/GTK side, the implementations of ShellExecuteEx/SHOpenWithDialog presumably do that internally); * it doesn't require convoluted formatting / escaping of invocation command lines that GWin32AppInfo / gspawn-win32 has to do otherwise (again, presumably the Windows libraries implement this internally); * it's certain to end up opening the file/URI the same way other apps in the system would; * it can/will open the native system UI for picking an app in case there are multiple options (or when so requested programmatically with the always-ask flag), or if there is no app installed that can handle the URI scheme / file type; * it lets us pass the parent window handle, much like the portal APIs; presumably Windows would use this for positioning the picking UI, or placing the launched app's window; * presumably, this will properly elevate privileges with a User Access Control (UAC) prompt if the app being launched requires administrator access; this presumably is impossible with the wspawn* APIs that gspawn-win32 uses; * this has a much better chance to work properly with the win32 app isolation (AppContainer) technology. Signed-off-by: Sergey Bugaev --- gtk/gtkshowwin32.c | 185 +++++++++++++++++++++++++++++++++++++++++++++ gtk/gtkshowwin32.h | 39 ++++++++++ gtk/meson.build | 1 + 3 files changed, 225 insertions(+) create mode 100644 gtk/gtkshowwin32.c create mode 100644 gtk/gtkshowwin32.h diff --git a/gtk/gtkshowwin32.c b/gtk/gtkshowwin32.c new file mode 100644 index 0000000000..c515056879 --- /dev/null +++ b/gtk/gtkshowwin32.c @@ -0,0 +1,185 @@ +/* + * GTK - The GIMP Toolkit + * Copyright (C) 2024 Sergey Bugaev + * All rights reserved. + * + * This Library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This Library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#include "config.h" + +#include "gtkshowwin32.h" +#include +#include + +#include +#include + +typedef struct { + gunichar2 *uri_or_path; + gboolean always_ask; +} ShowData; + +static void +show_data_free (ShowData *data) +{ + g_free (data->uri_or_path); + g_slice_free (ShowData, data); +} + +static void +show_uri_win32_in_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + ShowData *data = task_data; + GdkSurface *parent_surface; + HWND parent_hwnd; + HMONITOR monitor; + HRESULT hr; + + if (source_object) + { + parent_surface = gtk_native_get_surface (GTK_NATIVE (source_object)); + parent_hwnd = GDK_SURFACE_HWND (parent_surface); + monitor = MonitorFromWindow (parent_hwnd, MONITOR_DEFAULTTONULL); + } + else + { + parent_hwnd = 0; + monitor = (HMONITOR) NULL; + } + + if (!data->always_ask) + { + BOOL res; + SHELLEXECUTEINFOW shex_info; + + /* Attempt to initialize COM, in the off chance that there + * are ShellExecute hooks. */ + hr = CoInitializeEx (NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + + memset (&shex_info, 0, sizeof (shex_info)); + + shex_info.cbSize = sizeof (shex_info); + shex_info.fMask = SEE_MASK_NOASYNC | SEE_MASK_HMONITOR; + shex_info.hwnd = parent_hwnd; + shex_info.lpVerb = NULL; + + shex_info.lpFile = data->uri_or_path; + shex_info.lpParameters = NULL; + shex_info.lpDirectory = NULL; + shex_info.nShow = SW_SHOWNORMAL; + shex_info.hMonitor = monitor; + + /* Us passing the monitor derived from the parent window shouldn't + * break any custom window positioning logic in the app being + * launched, since the passed monitor is only used as a fallback + * for apps that use CW_USEDEFAULT. */ + + res = ShellExecuteExW (&shex_info); + + /* Un-initialize COM if we have successfully initialized it earlier. */ + if (SUCCEEDED (hr)) + CoUninitialize (); + + if (res) + g_task_return_boolean (task, TRUE); + else + { + int errsv = GetLastError (); + gchar *emsg = g_win32_error_message (errsv); + + g_task_return_new_error (task, G_IO_ERROR, + g_io_error_from_win32_error (errsv), + "%s", emsg); + g_free (emsg); + } + } + else + { + OPENASINFO openas_info; + + memset (&openas_info, 0, sizeof (openas_info)); + + openas_info.pcszFile = data->uri_or_path; + openas_info.pcszClass = NULL; + openas_info.oaifInFlags = OAIF_ALLOW_REGISTRATION | OAIF_REGISTER_EXT | OAIF_EXEC; + + hr = SHOpenWithDialog (parent_hwnd, &openas_info); + + if (SUCCEEDED (hr)) + g_task_return_boolean (task, TRUE); + else + g_task_return_new_error (task, G_IO_ERROR, + G_IO_ERROR_FAILED, + "Failed to display Open With dialog: 0x%lx", hr); + } +} + +void +gtk_show_uri_win32 (GtkWindow *parent, + const char *uri_or_path, + gboolean always_ask, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShowData *show_data; + GTask *task; + GError *error = NULL; + char *owned_path = NULL; + + g_return_if_fail (uri_or_path != NULL); + + task = g_task_new (parent, cancellable, callback, user_data); + g_task_set_source_tag (task, gtk_show_uri_win32); + + /* ShellExecute doesn't quite like file:// URLs, so convert + * those to file paths now. */ + if (g_str_has_prefix (uri_or_path, "file://")) + { + GFile *file = g_file_new_for_uri (uri_or_path); + uri_or_path = owned_path = g_file_get_path (file); + g_object_unref (file); + } + + show_data = g_slice_new (ShowData); + show_data->always_ask = always_ask; + /* Note: uri_or_path is UTF-8 encoded here. */ + show_data->uri_or_path = g_utf8_to_utf16 (uri_or_path, -1, NULL, NULL, &error); + g_clear_pointer(&owned_path, g_free); + if (G_UNLIKELY (error)) + { + g_task_return_error (task, error); + g_slice_free (ShowData, show_data); + g_object_unref (task); + return; + } + + g_task_set_task_data (task, show_data, (GDestroyNotify) show_data_free); + g_task_run_in_thread (task, show_uri_win32_in_thread); + g_object_unref (task); +} + +gboolean +gtk_show_uri_win32_finish (GtkWindow *parent, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, parent), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/gtk/gtkshowwin32.h b/gtk/gtkshowwin32.h new file mode 100644 index 0000000000..ddef7d4854 --- /dev/null +++ b/gtk/gtkshowwin32.h @@ -0,0 +1,39 @@ +/* + * GTK - The GIMP Toolkit + * Copyright (C) 2024 Sergey Bugaev + * All rights reserved. + * + * This Library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This Library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#pragma once + +#include + +typedef struct _GtkWindow GtkWindow; + +G_BEGIN_DECLS + +void gtk_show_uri_win32 (GtkWindow *parent, + const char *uri_or_path, + gboolean always_ask, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean gtk_show_uri_win32_finish (GtkWindow *parent, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/gtk/meson.build b/gtk/meson.build index ea1c871f92..5d2f2859fe 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -663,6 +663,7 @@ if os_win32 'gtkimcontextime.c', 'gtkfilechoosernativewin32.c', 'gtkwin32.c', + 'gtkshowwin32.c', ] endif From b33b679b13ec766e7c54ce0dd1f8be9a9a6da3dd Mon Sep 17 00:00:00 2001 From: Sergey Bugaev Date: Mon, 5 Aug 2024 17:50:32 +0300 Subject: [PATCH 2/2] Hook up gtk_show_uri_full () to gtk_show_uri_win32 () On Windows, always use gtk_show_uri_win32 () instead of going through GAppInfo. Hook up gtk_file_launcher_launch () to gtk_show_uri_win32 () as well, always extracting the file path (and not a URI) and propagating the always-ask flag. Signed-off-by: Sergey Bugaev --- gtk/deprecated/gtkshow.c | 35 +++++++++++++++++++++++++++++++++++ gtk/gtkfilelauncher.c | 14 +++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/gtk/deprecated/gtkshow.c b/gtk/deprecated/gtkshow.c index 6a160c7ee3..cfed84ced4 100644 --- a/gtk/deprecated/gtkshow.c +++ b/gtk/deprecated/gtkshow.c @@ -27,8 +27,13 @@ #include "gtkalertdialog.h" #include +#ifdef G_OS_WIN32 +#include "gtkshowwin32.h" +#endif + G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#ifndef G_OS_WIN32 typedef struct { GtkWindow *parent; char *handle; @@ -86,6 +91,24 @@ window_handle_exported (GtkWindow *window, data); } +#else /* G_OS_WIN32 */ +static void +show_win32_done (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GTask *task = user_data; + GError *error = NULL; + + if (gtk_show_uri_win32_finish (GTK_WINDOW (source), result, &error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, error); + + g_object_unref (task); +} +#endif + /** * gtk_show_uri_full: * @parent: (nullable): parent window @@ -114,6 +137,7 @@ gtk_show_uri_full (GtkWindow *parent, GAsyncReadyCallback callback, gpointer user_data) { +#ifndef G_OS_WIN32 GtkShowUriData *data; GdkAppLaunchContext *context; GdkDisplay *display; @@ -138,6 +162,17 @@ gtk_show_uri_full (GtkWindow *parent, if (!parent || !gtk_window_export_handle (parent, window_handle_exported, data)) window_handle_exported (parent, NULL, data); + +#else /* G_OS_WIN32 */ + GTask *task; + + g_return_if_fail (parent == NULL || GTK_IS_WINDOW (parent)); + g_return_if_fail (uri != NULL); + + task = g_task_new (parent, cancellable, callback, user_data); + g_task_set_source_tag (task, gtk_show_uri_full); + gtk_show_uri_win32 (parent, uri, FALSE, cancellable, show_win32_done, task); +#endif } /** diff --git a/gtk/gtkfilelauncher.c b/gtk/gtkfilelauncher.c index 955eadf054..a36fff9d2a 100644 --- a/gtk/gtkfilelauncher.c +++ b/gtk/gtkfilelauncher.c @@ -26,6 +26,10 @@ #include "deprecated/gtkshow.h" #include +#ifdef G_OS_WIN32 +#include "gtkshowwin32.h" +#endif + /** * GtkFileLauncher: * @@ -432,7 +436,11 @@ show_uri_done (GObject *source, GTask *task = G_TASK (data); GError *error = NULL; +#ifndef G_OS_WIN32 if (!gtk_show_uri_full_finish (parent, result, &error)) +#else + if (!gtk_show_uri_win32_finish (parent, result, &error)) +#endif { if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_task_return_new_error (task, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_CANCELLED, "Cancelled by user"); @@ -503,7 +511,6 @@ gtk_file_launcher_launch (GtkFileLauncher *self, gtk_openuri_portal_open_async (self->file, FALSE, flags, parent, cancellable, open_done, task); } else -#endif { char *uri = g_file_get_uri (self->file); @@ -513,6 +520,11 @@ G_GNUC_END_IGNORE_DEPRECATIONS g_free (uri); } +#else /* G_OS_WIN32 */ + char *path = g_file_get_path (self->file); + gtk_show_uri_win32 (parent, path, self->always_ask, cancellable, show_uri_done, task); + g_free (path); +#endif } /**