/* -*- Mode: C; c-file-style: "gnu"; tab-width: 8 -*- */
/* GTK - The GIMP Toolkit
* gtkfilechoosernativewin32.c: Win32 Native File selector dialog
* Copyright (C) 2015, Red Hat, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see .
*/
#include "config.h"
/* Vista or newer */
#define _WIN32_WINNT 0x0600
#define WINVER _WIN32_WINNT
#define NTDDI_VERSION NTDDI_VISTA
#define COBJMACROS
#include "gtkfilechoosernativeprivate.h"
#include "gtknativedialogprivate.h"
#include "gtkprivate.h"
#include "gtkfilechooserdialog.h"
#include "gtkfilechooserprivate.h"
#include "gtkfilechooserwidget.h"
#include "gtkfilechooserwidgetprivate.h"
#include "gtkfilechooserutils.h"
#include "gtkfilechooserembed.h"
#include "gtkfilesystem.h"
#include "gtksizerequest.h"
#include "gtktypebuiltins.h"
#include "gtkintl.h"
#include "gtksettings.h"
#include "gtktogglebutton.h"
#include "gtkstylecontext.h"
#include "gtkheaderbar.h"
#include "gtklabel.h"
#include "gtkfilechooserentry.h"
#include "gtkfilefilterprivate.h"
#include "win32/gdkwin32.h"
#include
#include
typedef struct {
GtkFileChooserNative *self;
IFileDialogEvents *events;
HWND parent;
gboolean skip_response;
gboolean save;
gboolean folder;
gboolean modal;
gboolean overwrite_confirmation;
gboolean select_multiple;
gboolean show_hidden;
char *accept_label;
char *cancel_label;
char *title;
GSList *shortcut_uris;
GFile *current_folder;
GFile *current_file;
char *current_name;
COMDLG_FILTERSPEC *filters;
GSList *files;
int response;
} FilechooserWin32ThreadData;
static void
g_warning_hr (const char *msg, HRESULT hr)
{
char *errmsg;
errmsg = g_win32_error_message (hr);
g_warning ("%s: %s", msg, errmsg);
g_free (errmsg);
}
/* {3CAFD12E-82AE-4184-8309-848C0104B4DC} */
static const GUID myIID_IFileDialogEvents =
{ 0x3cafd12e, 0x82ae, 0x4184, { 0x83, 0x9, 0x84, 0x8c, 0x1, 0x4, 0xb4, 0xdc } };
/* Protects access to dialog_hwnd, do_close and ref_count */
G_LOCK_DEFINE_STATIC(FileDialogEvents);
typedef struct {
IFileDialogEvents iFileDialogEvents;
int ref_count;
gboolean enable_owner;
gboolean got_hwnd;
HWND dialog_hwnd;
gboolean do_close; /* Set if hide was called before dialog_hwnd was set */
FilechooserWin32ThreadData *data;
} FileDialogEvents;
static ULONG STDMETHODCALLTYPE
ifiledialogevents_AddRef (IFileDialogEvents *self)
{
FileDialogEvents *events = (FileDialogEvents *)self;
ULONG ref_count;
G_LOCK (FileDialogEvents);
ref_count = ++events->ref_count;
G_UNLOCK (FileDialogEvents);
return ref_count;
}
static ULONG STDMETHODCALLTYPE
ifiledialogevents_Release (IFileDialogEvents *self)
{
FileDialogEvents *events = (FileDialogEvents *)self;
int ref_count;
G_LOCK (FileDialogEvents);
ref_count = --events->ref_count;
G_UNLOCK (FileDialogEvents);
if (ref_count == 0)
g_free (self);
return ref_count;
}
static HRESULT STDMETHODCALLTYPE
ifiledialogevents_QueryInterface (IFileDialogEvents *self,
REFIID riid,
LPVOID *ppvObject)
{
if (IsEqualIID (riid, &IID_IUnknown) ||
IsEqualIID (riid, &myIID_IFileDialogEvents))
{
*ppvObject = self;
IUnknown_AddRef ((IUnknown *)self);
return NOERROR;
}
else
{
*ppvObject = NULL;
return E_NOINTERFACE;
}
}
static HRESULT STDMETHODCALLTYPE
ifiledialogevents_OnFileOk (IFileDialogEvents *self,
IFileDialog *pfd)
{
return S_OK;
}
static HRESULT STDMETHODCALLTYPE
ifiledialogevents_OnFolderChanging (IFileDialogEvents *self,
IFileDialog *pfd,
IShellItem *psiFolder)
{
return S_OK;
}
static HRESULT STDMETHODCALLTYPE
ifiledialogevents_OnFolderChange (IFileDialogEvents *self,
IFileDialog *pfd)
{
FileDialogEvents *events = (FileDialogEvents *)self;
IOleWindow *olew = NULL;
HWND dialog_hwnd;
HRESULT hr;
if (!events->got_hwnd)
{
events->got_hwnd = TRUE;
hr = IFileDialog_QueryInterface (pfd, &IID_IOleWindow, (LPVOID *) &olew);
if (SUCCEEDED (hr))
{
hr = IOleWindow_GetWindow (olew, &dialog_hwnd);
if (SUCCEEDED (hr))
{
G_LOCK (FileDialogEvents);
events->dialog_hwnd = dialog_hwnd;
if (events->do_close)
SendMessage (events->dialog_hwnd, WM_CLOSE, 0, 0);
G_UNLOCK (FileDialogEvents);
}
else
g_warning_hr ("Can't get HWND", hr);
hr = IOleWindow_Release (olew);
if (FAILED (hr))
g_warning_hr ("Can't unref IOleWindow", hr);
}
else
g_warning_hr ("Can't get IOleWindow", hr);
if (events->enable_owner && events->dialog_hwnd)
{
HWND owner = GetWindow (events->dialog_hwnd, GW_OWNER);
if (owner)
EnableWindow (owner, TRUE);
}
}
return S_OK;
}
static HRESULT STDMETHODCALLTYPE
ifiledialogevents_OnSelectionChange (IFileDialogEvents * self,
IFileDialog *pfd)
{
return S_OK;
}
static HRESULT STDMETHODCALLTYPE
ifiledialogevents_OnShareViolation (IFileDialogEvents * self,
IFileDialog *pfd,
IShellItem *psi,
FDE_SHAREVIOLATION_RESPONSE *pResponse)
{
return E_NOTIMPL;
}
static HRESULT STDMETHODCALLTYPE
ifiledialogevents_OnTypeChange (IFileDialogEvents * self,
IFileDialog *pfd)
{
FileDialogEvents *events = (FileDialogEvents *) self;
UINT fileType;
HRESULT hr = IFileDialog_GetFileTypeIndex (pfd, &fileType);
if (FAILED (hr))
{
g_warning_hr ("Can't get current file type", hr);
return S_OK;
}
fileType--; // fileTypeIndex starts at 1
GSList *filters = gtk_file_chooser_list_filters (GTK_FILE_CHOOSER (events->data->self));
events->data->self->current_filter = g_slist_nth_data (filters, fileType);
g_slist_free (filters);
g_object_notify (G_OBJECT (events->data->self), "filter");
return S_OK;
}
static HRESULT STDMETHODCALLTYPE
ifiledialogevents_OnOverwrite (IFileDialogEvents * self,
IFileDialog *pfd,
IShellItem *psi,
FDE_OVERWRITE_RESPONSE *pResponse)
{
return E_NOTIMPL;
}
static IFileDialogEventsVtbl ifde_vtbl = {
ifiledialogevents_QueryInterface,
ifiledialogevents_AddRef,
ifiledialogevents_Release,
ifiledialogevents_OnFileOk,
ifiledialogevents_OnFolderChanging,
ifiledialogevents_OnFolderChange,
ifiledialogevents_OnSelectionChange,
ifiledialogevents_OnShareViolation,
ifiledialogevents_OnTypeChange,
ifiledialogevents_OnOverwrite
};
static void
file_dialog_events_send_close (IFileDialogEvents *self)
{
FileDialogEvents *events = (FileDialogEvents *)self;
G_LOCK (FileDialogEvents);
if (events->dialog_hwnd)
SendMessage (events->dialog_hwnd, WM_CLOSE, 0, 0);
else
events->do_close = TRUE;
G_UNLOCK (FileDialogEvents);
}
static IFileDialogEvents *
file_dialog_events_new (gboolean enable_owner, FilechooserWin32ThreadData *data)
{
FileDialogEvents *events;
events = g_new0 (FileDialogEvents, 1);
events->iFileDialogEvents.lpVtbl = &ifde_vtbl;
events->ref_count = 1;
events->enable_owner = enable_owner;
events->data = data;
return &events->iFileDialogEvents;
}
static void
filechooser_win32_thread_data_free (FilechooserWin32ThreadData *data)
{
int i;
if (data->filters)
{
for (i = 0; data->filters[i].pszName != NULL; i++)
{
g_free ((char *)data->filters[i].pszName);
g_free ((char *)data->filters[i].pszSpec);
}
g_free (data->filters);
}
if (data->events)
IFileDialogEvents_Release (data->events);
g_clear_object (&data->current_folder);
g_clear_object (&data->current_file);
g_free (data->current_name);
g_slist_free_full (data->shortcut_uris, g_free);
g_slist_free_full (data->files, g_object_unref);
if (data->self)
g_object_unref (data->self);
g_free (data->accept_label);
g_free (data->cancel_label);
g_free (data->title);
g_free (data);
}
static gboolean
filechooser_win32_thread_done (gpointer _data)
{
FilechooserWin32ThreadData *data = _data;
GtkFileChooserNative *self = data->self;
self->mode_data = NULL;
if (!data->skip_response)
{
g_slist_free_full (self->custom_files, g_object_unref);
self->custom_files = data->files;
data->files = NULL;
_gtk_native_dialog_emit_response (GTK_NATIVE_DIALOG (data->self),
data->response);
}
filechooser_win32_thread_data_free (data);
return FALSE;
}
static GFile *
get_file_for_shell_item (IShellItem *item)
{
HRESULT hr;
PWSTR pathw = NULL;
char *path;
GFile *file;
hr = IShellItem_GetDisplayName (item, SIGDN_FILESYSPATH, &pathw);
if (SUCCEEDED (hr))
{
path = g_utf16_to_utf8 (pathw, -1, NULL, NULL, NULL);
CoTaskMemFree (pathw);
if (path != NULL)
{
file = g_file_new_for_path (path);
g_free (path);
return file;
}
}
/* TODO: also support URLs through SIGDN_URL, but Windows URLS are not
* RFC 3986 compliant and we'd need to convert them first.
*/
return NULL;
}
static void
data_add_shell_item (FilechooserWin32ThreadData *data,
IShellItem *item)
{
GFile *file;
file = get_file_for_shell_item (item);
if (file != NULL)
{
data->files = g_slist_prepend (data->files, file);
data->response = GTK_RESPONSE_ACCEPT;
}
}
static IShellItem *
get_shell_item_for_uri (const char *uri)
{
IShellItem *item;
HRESULT hr;
gunichar2 *uri_w = g_utf8_to_utf16 (uri, -1, NULL, NULL, NULL);
hr = SHCreateItemFromParsingName(uri_w, 0, &IID_IShellItem, (LPVOID *) &item);
if (SUCCEEDED (hr))
return item;
else
g_warning_hr ("Can't create shell item from shortcut", hr);
return NULL;
}
static IShellItem *
get_shell_item_for_file (GFile *file)
{
char *uri;
IShellItem *item;
uri = g_file_get_uri (file);
item = get_shell_item_for_uri (uri);
g_free (uri);
return item;
}
static gpointer
filechooser_win32_thread (gpointer _data)
{
FilechooserWin32ThreadData *data = _data;
HRESULT hr;
IFileDialog *pfd = NULL;
IFileDialog2 *pfd2 = NULL;
DWORD flags;
DWORD cookie;
GSList *l;
CoInitializeEx (NULL, COINIT_APARTMENTTHREADED);
if (data->save && !data->folder)
hr = CoCreateInstance (&CLSID_FileSaveDialog,
NULL, CLSCTX_INPROC_SERVER,
&IID_IFileSaveDialog, (LPVOID *) &pfd);
else
hr = CoCreateInstance (&CLSID_FileOpenDialog,
NULL, CLSCTX_INPROC_SERVER,
&IID_IFileOpenDialog, (LPVOID *) &pfd);
if (FAILED (hr))
g_error ("Can't create FileOpenDialog: %s", g_win32_error_message (hr));
hr = IFileDialog_GetOptions (pfd, &flags);
if (FAILED (hr))
g_error ("Can't get FileDialog options: %s", g_win32_error_message (hr));
flags |= FOS_FORCEFILESYSTEM;
if (data->folder)
flags |= FOS_PICKFOLDERS;
if (data->folder && data->save)
flags &= ~(FOS_FILEMUSTEXIST);
if (data->select_multiple)
flags |= FOS_ALLOWMULTISELECT;
if (data->show_hidden)
flags |= FOS_FORCESHOWHIDDEN;
if (data->overwrite_confirmation)
flags |= FOS_OVERWRITEPROMPT;
else
flags &= ~(FOS_OVERWRITEPROMPT);
hr = IFileDialog_SetOptions (pfd, flags);
if (FAILED (hr))
g_error ("Can't set FileDialog options: %s", g_win32_error_message (hr));
if (data->title)
{
gunichar2 *label = g_utf8_to_utf16 (data->title, -1,
NULL, NULL, NULL);
IFileDialog_SetTitle (pfd, label);
g_free (label);
}
if (data->accept_label)
{
gunichar2 *label = g_utf8_to_utf16 (data->accept_label, -1,
NULL, NULL, NULL);
IFileDialog_SetOkButtonLabel (pfd, label);
g_free (label);
}
if (data->cancel_label)
{
gunichar2 *label = g_utf8_to_utf16 (data->cancel_label, -1,
NULL, NULL, NULL);
hr = IFileDialog_QueryInterface (pfd, &IID_IFileDialog2, (LPVOID *) &pfd2);
if (SUCCEEDED (hr))
{
IFileDialog2_SetCancelButtonLabel (pfd2, label);
IFileDialog2_Release (pfd2);
}
g_free (label);
}
for (l = data->shortcut_uris; l != NULL; l = l->next)
{
IShellItem *item = get_shell_item_for_uri (l->data);
if (item)
{
hr = IFileDialog_AddPlace (pfd, item, FDAP_BOTTOM);
if (FAILED (hr))
g_warning_hr ("Can't add dialog shortcut", hr);
IShellItem_Release (item);
}
}
if (data->current_file)
{
IFileSaveDialog *pfsd;
hr = IFileDialog_QueryInterface (pfd, &IID_IFileSaveDialog, (LPVOID *) &pfsd);
if (SUCCEEDED (hr))
{
IShellItem *item = get_shell_item_for_file (data->current_file);
if (item)
{
hr = IFileSaveDialog_SetSaveAsItem (pfsd, item);
if (FAILED (hr))
g_warning_hr ("Can't set save as item", hr);
IShellItem_Release (item);
}
IFileSaveDialog_Release (pfsd);
}
}
if (data->current_folder)
{
IShellItem *item = get_shell_item_for_file (data->current_folder);
if (item)
{
hr = IFileDialog_SetFolder (pfd, item);
if (FAILED (hr))
g_warning_hr ("Can't set folder", hr);
IShellItem_Release (item);
}
}
if (data->current_name)
{
gunichar2 *name = g_utf8_to_utf16 (data->current_name, -1, NULL, NULL, NULL);
hr = IFileDialog_SetFileName (pfd, name);
if (FAILED (hr))
g_warning_hr ("Can't set file name", hr);
g_free (name);
}
if (data->filters)
{
int n;
for (n = 0; data->filters[n].pszName != NULL; n++)
{}
hr = IFileDialog_SetFileTypes (pfd, n, data->filters);
if (FAILED (hr))
g_warning_hr ("Can't set file types", hr);
if (data->self->current_filter)
{
GSList *filters = gtk_file_chooser_list_filters (GTK_FILE_CHOOSER (data->self));
gint current_filter_index = g_slist_index (filters, data->self->current_filter);
g_slist_free (filters);
if (current_filter_index >= 0)
hr = IFileDialog_SetFileTypeIndex (pfd, current_filter_index + 1);
else
hr = IFileDialog_SetFileTypeIndex (pfd, 1);
}
else
{
hr = IFileDialog_SetFileTypeIndex (pfd, 1);
}
if (FAILED (hr))
g_warning_hr ("Can't set current file type", hr);
}
data->response = GTK_RESPONSE_CANCEL;
hr = IFileDialog_Advise (pfd, data->events, &cookie);
if (FAILED (hr))
g_error ("Can't Advise FileDialog: %s", g_win32_error_message (hr));
hr = IFileDialog_Show (pfd, data->parent);
if (SUCCEEDED (hr))
{
IFileOpenDialog *pfod = NULL;
hr = IFileDialog_QueryInterface (pfd,&IID_IFileOpenDialog, (LPVOID *) &pfod);
if (SUCCEEDED (hr))
{
IShellItemArray *res;
DWORD i, count;
hr = IFileOpenDialog_GetResults (pfod, &res);
if (FAILED (hr))
g_error ("Can't get FileOpenDialog results: %s", g_win32_error_message (hr));
hr = IShellItemArray_GetCount (res, &count);
if (FAILED (hr))
g_error ("Can't get FileOpenDialog count: %s", g_win32_error_message (hr));
for (i = 0; i < count; i++)
{
IShellItem *item;
hr = IShellItemArray_GetItemAt (res, i, &item);
if (FAILED (hr))
g_error ("Can't get item at %lu: %s", i, g_win32_error_message (hr));
data_add_shell_item (data, item);
IShellItem_Release (item);
}
IShellItemArray_Release (res);
IFileOpenDialog_Release (pfod);
}
else
{
IShellItem *item;
hr = IFileDialog_GetResult (pfd, &item);
if (FAILED (hr))
g_error ("Can't get FileDialog result: %s", g_win32_error_message (hr));
data_add_shell_item (data, item);
IShellItem_Release (item);
}
}
hr = IFileDialog_Unadvise (pfd, cookie);
if (FAILED (hr))
g_error ("Can't Unadvise FileDialog: %s", g_win32_error_message (hr));
IFileDialog_Release ((IUnknown *)pfd);
g_main_context_invoke (NULL,
filechooser_win32_thread_done,
data);
return NULL;
}
static gboolean
file_filter_to_win32 (GtkFileFilter *filter,
COMDLG_FILTERSPEC *spec)
{
const char *name;
char **patterns;
char *pattern_list;
patterns = _gtk_file_filter_get_as_patterns (filter);
if (patterns == NULL)
return FALSE;
pattern_list = g_strjoinv (";", patterns);
g_strfreev (patterns);
name = gtk_file_filter_get_name (filter);
if (name == NULL)
name = pattern_list;
spec->pszName = g_utf8_to_utf16 (name, -1, NULL, NULL, NULL);
spec->pszSpec = g_utf8_to_utf16 (pattern_list, -1, NULL, NULL, NULL);
g_free (pattern_list);
return TRUE;
}
static char *
translate_mnemonics (const char *src)
{
GString *s;
const char *p;
char c;
if (src == NULL)
return NULL;
s = g_string_sized_new (strlen (src));
for (p = src; *p; p++)
{
c = *p;
switch (c)
{
case '_':
/* __ is _ escaped */
if (*(p+1) == '_')
{
g_string_append_c (s, '_');
p++;
}
else
g_string_append_c (s, '&');
break;
case '&':
/* Win32 needs ampersands escaped */
g_string_append (s, "&&");
default:
g_string_append_c (s, c);
}
}
return g_string_free (s, FALSE);
}
gboolean
gtk_file_chooser_native_win32_show (GtkFileChooserNative *self)
{
GThread *thread;
FilechooserWin32ThreadData *data;
GtkWindow *transient_for;
GtkFileChooserAction action;
guint update_preview_signal;
GSList *filters, *l;
int n_filters, i;
if (gtk_file_chooser_get_extra_widget (GTK_FILE_CHOOSER (self)) != NULL)
return FALSE;
update_preview_signal = g_signal_lookup ("update-preview", GTK_TYPE_FILE_CHOOSER);
if (g_signal_has_handler_pending (self, update_preview_signal, 0, TRUE))
return FALSE;
data = g_new0 (FilechooserWin32ThreadData, 1);
filters = gtk_file_chooser_list_filters (GTK_FILE_CHOOSER (self));
n_filters = g_slist_length (filters);
if (n_filters > 0)
{
data->filters = g_new0 (COMDLG_FILTERSPEC, n_filters + 1);
for (l = filters, i = 0; l != NULL; l = l->next, i++)
{
if (!file_filter_to_win32 (l->data, &data->filters[i]))
{
filechooser_win32_thread_data_free (data);
return FALSE;
}
}
self->current_filter = gtk_file_chooser_get_filter (GTK_FILE_CHOOSER (self));
}
else
{
self->current_filter = NULL;
}
self->mode_data = data;
data->self = g_object_ref (self);
data->shortcut_uris =
gtk_file_chooser_list_shortcut_folder_uris (GTK_FILE_CHOOSER (self->dialog));
data->accept_label = translate_mnemonics (self->accept_label);
data->cancel_label = translate_mnemonics (self->cancel_label);
action = gtk_file_chooser_get_action (GTK_FILE_CHOOSER (self->dialog));
if (action == GTK_FILE_CHOOSER_ACTION_SAVE ||
action == GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER)
data->save = TRUE;
if (action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER ||
action == GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER)
data->folder = TRUE;
if ((action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER ||
action == GTK_FILE_CHOOSER_ACTION_OPEN) &&
gtk_file_chooser_get_select_multiple (GTK_FILE_CHOOSER (self->dialog)))
data->select_multiple = TRUE;
if (gtk_file_chooser_get_do_overwrite_confirmation (GTK_FILE_CHOOSER (self->dialog)))
data->overwrite_confirmation = TRUE;
if (gtk_file_chooser_get_show_hidden (GTK_FILE_CHOOSER (self->dialog)))
data->show_hidden = TRUE;
transient_for = gtk_native_dialog_get_transient_for (GTK_NATIVE_DIALOG (self));
if (transient_for)
{
gtk_widget_realize (GTK_WIDGET (transient_for));
data->parent = gdk_win32_surface_get_handle (gtk_widget_get_surface (GTK_WIDGET (transient_for)));
if (gtk_native_dialog_get_modal (GTK_NATIVE_DIALOG (self)))
data->modal = TRUE;
}
data->title =
g_strdup (gtk_native_dialog_get_title (GTK_NATIVE_DIALOG (self)));
if (self->current_file)
data->current_file = g_object_ref (self->current_file);
else
{
if (self->current_folder)
data->current_folder = g_object_ref (self->current_folder);
if (action == GTK_FILE_CHOOSER_ACTION_SAVE ||
action == GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER)
data->current_name = g_strdup (self->current_name);
}
data->events = file_dialog_events_new (!data->modal, data);
thread = g_thread_new ("win32 filechooser", filechooser_win32_thread, data);
if (thread == NULL)
{
filechooser_win32_thread_data_free (data);
return FALSE;
}
return TRUE;
}
void
gtk_file_chooser_native_win32_hide (GtkFileChooserNative *self)
{
FilechooserWin32ThreadData *data = self->mode_data;
/* This is always set while dialog visible */
g_assert (data != NULL);
data->skip_response = TRUE;
file_dialog_events_send_close (data->events);
}