From ce4c9a9ad66ff8225ee82a984585eb2d7785a419 Mon Sep 17 00:00:00 2001 From: Tom Schoonjans Date: Fri, 30 Jun 2017 21:34:05 +0100 Subject: [PATCH] GtkFilechooserNative: add macOS support Based on the Win32 implementation, as well as the macOS file chooser from https://github.com/GNOME/gedit/blob/master/gedit/gedit-file-chooser-dialog-osx.[ch] Not fully tested yet, but working properly so far. TODO: filter support, extra widget (label), documentation... https://bugzilla.gnome.org/show_bug.cgi?id=784723 --- gtk/Makefile.am | 1 + gtk/gtkfilechoosernative.c | 12 + gtk/gtkfilechoosernativeprivate.h | 3 + gtk/gtkfilechoosernativequartz.c | 431 ++++++++++++++++++++++++++++++ 4 files changed, 447 insertions(+) create mode 100644 gtk/gtkfilechoosernativequartz.c diff --git a/gtk/Makefile.am b/gtk/Makefile.am index 45fb7c2257..f2de4d17cd 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -1042,6 +1042,7 @@ gtk_use_quartz_c_sources = \ gtkmountoperation-stub.c \ gtkapplication-quartz.c \ gtkapplication-quartz-menu.c \ + gtkfilechoosernativequartz.c \ gtkquartz.c gtk_use_stub_c_sources = \ gtkmountoperation-stub.c diff --git a/gtk/gtkfilechoosernative.c b/gtk/gtkfilechoosernative.c index a3e5636d52..7314fd67c8 100644 --- a/gtk/gtkfilechoosernative.c +++ b/gtk/gtkfilechoosernative.c @@ -192,6 +192,7 @@ enum { MODE_FALLBACK, MODE_WIN32, + MODE_QUARTZ, MODE_PORTAL, }; @@ -704,6 +705,7 @@ gtk_file_chooser_native_get_files (GtkFileChooser *chooser) { case MODE_PORTAL: case MODE_WIN32: + case MODE_QUARTZ: return g_slist_copy_deep (self->custom_files, (GCopyFunc)g_object_ref, NULL); case MODE_FALLBACK: @@ -724,6 +726,11 @@ gtk_file_chooser_native_show (GtkNativeDialog *native) self->mode = MODE_WIN32; #endif +#ifdef GDK_WINDOWING_QUARTZ + if (gtk_file_chooser_native_quartz_show (self)) + self->mode = MODE_QUARTZ; +#endif + if (self->mode == MODE_FALLBACK && gtk_file_chooser_native_portal_show (self)) self->mode = MODE_PORTAL; @@ -745,6 +752,11 @@ gtk_file_chooser_native_hide (GtkNativeDialog *native) case MODE_WIN32: #ifdef GDK_WINDOWING_WIN32 gtk_file_chooser_native_win32_hide (self); +#endif + break; + case MODE_QUARTZ: +#ifdef GDK_WINDOWING_QUARTZ + gtk_file_chooser_native_quartz_hide (self); #endif break; case MODE_PORTAL: diff --git a/gtk/gtkfilechoosernativeprivate.h b/gtk/gtkfilechoosernativeprivate.h index 283eda902d..7bbbed0241 100644 --- a/gtk/gtkfilechoosernativeprivate.h +++ b/gtk/gtkfilechoosernativeprivate.h @@ -57,6 +57,9 @@ struct _GtkFileChooserNative gboolean gtk_file_chooser_native_win32_show (GtkFileChooserNative *self); void gtk_file_chooser_native_win32_hide (GtkFileChooserNative *self); +gboolean gtk_file_chooser_native_quartz_show (GtkFileChooserNative *self); +void gtk_file_chooser_native_quartz_hide (GtkFileChooserNative *self); + gboolean gtk_file_chooser_native_portal_show (GtkFileChooserNative *self); void gtk_file_chooser_native_portal_hide (GtkFileChooserNative *self); diff --git a/gtk/gtkfilechoosernativequartz.c b/gtk/gtkfilechoosernativequartz.c new file mode 100644 index 0000000000..c422d72cf2 --- /dev/null +++ b/gtk/gtkfilechoosernativequartz.c @@ -0,0 +1,431 @@ +/* -*- Mode: C; c-file-style: "gnu"; tab-width: 8 -*- */ +/* GTK - The GIMP Toolkit + * gtkfilechoosernativequartz.c: Quartz Native File selector dialog + * Copyright (C) 2017, Tom Schoonjans + * + * 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" + +#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 "quartz/gdkquartz.h" + +typedef struct { + GtkFileChooserNative *self; + + NSSavePanel *panel; + NSWindow *parent; + gboolean skip_response; + gboolean save; + gboolean folder; + gboolean create_folders; + gboolean modal; + gboolean overwrite_confirmation; + gboolean select_multiple; + gboolean show_hidden; + gboolean running; + + char *accept_label; + char *cancel_label; + char *title; + + GSList *shortcut_uris; + + GFile *current_folder; + GFile *current_file; + char *current_name; + + NSArray *filters; + + GSList *files; + int response; +} FileChooserQuartzData; + +static GFile * +ns_url_to_g_file (NSURL *url) +{ + if (url == nil) + { + return NULL; + } + + return g_file_new_for_uri ([[url absoluteString] UTF8String]); +} + +static GSList * +chooser_get_files (FileChooserQuartzData *data) +{ + + GSList *ret = NULL; + + if (!data->save) + { + NSArray *urls; + gint i; + + urls = [(NSOpenPanel *)data->panel URLs]; + + for (i = 0; i < [urls count]; i++) + { + NSURL *url; + + url = (NSURL *)[urls objectAtIndex:i]; + ret = g_slist_prepend (ret, ns_url_to_g_file (url)); + } + } + else + { + GFile *file; + + file = ns_url_to_g_file ([data->panel URL]); + + if (file != NULL) + { + ret = g_slist_prepend (ret, file); + } + } + + return g_slist_reverse (ret); +} + +static void +chooser_set_current_folder (FileChooserQuartzData *data, + GFile *folder) +{ + + if (folder != NULL) + { + gchar *uri; + + uri = g_file_get_uri (folder); + [data->panel setDirectoryURL:[NSURL URLWithString:[NSString stringWithUTF8String:uri]]]; + g_free (uri); + } +} + +static void +chooser_set_current_name (FileChooserQuartzData *data, + const gchar *name) +{ + + if (name != NULL) + [data->panel setNameFieldStringValue:[NSString stringWithUTF8String:name]]; +} + + +static void +filechooser_quartz_data_free (FileChooserQuartzData *data) +{ + int i; + if (data->filters) + { + } + + 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_quartz_launch (FileChooserQuartzData *data) +{ + + // GTK_FILE_CHOOSER_ACTION_SAVE and GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER + if (data->save) + { + NSSavePanel *panel = [[NSSavePanel savePanel] retain]; + + /*if ([panel respondsToSelector:@selector(setShowsTagField:)]) + { + [(id)panel setShowsTagField:NO]; + } + */ + + if (!data->folder && !data->create_folders) + { + [panel setCanCreateDirectories:NO]; + } + else + { + [panel setCanCreateDirectories:YES]; + } + + data->panel = panel; + } + // GTK_FILE_CHOOSER_ACTION_OPEN and GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER + else + { + NSOpenPanel *panel = [[NSOpenPanel openPanel] retain]; + + if (data->select_multiple) + { + [panel setAllowsMultipleSelection:YES]; + } + if (data->folder) + { + [panel setCanChooseDirectories:YES]; + [panel setCanChooseFiles:NO]; + } + else + { + [panel setCanChooseDirectories:NO]; + [panel setCanChooseFiles:YES]; + } + + data->panel = panel; + } + + [data->panel setReleasedWhenClosed:YES]; + + if (data->show_hidden) + { + [data->panel setShowsHiddenFiles:YES]; + } + + if (data->accept_label) + [data->panel setPrompt:[NSString stringWithUTF8String:data->accept_label]]; + + if (data->title) + [data->panel setTitle:[NSString stringWithUTF8String:data->title]]; + + if (data->current_file) + { + GFile *folder; + gchar *name; + + folder = g_file_get_parent (data->current_file); + name = g_file_get_basename (data->current_file); + + chooser_set_current_folder (data, folder); + chooser_set_current_name (data, name); + + g_object_unref (folder); + g_free (name); + } + + if (data->current_folder) + { + chooser_set_current_folder (data, data->current_folder); + } + + if (data->current_name) + { + chooser_set_current_name (data, data->current_name); + } + + if (data->filters) + { + // TODO + } + + data->response = GTK_RESPONSE_CANCEL; + + + void (^handler)(NSInteger ret) = ^(NSInteger result) { + + if (result == NSFileHandlingPanelOKButton) + { + // get selected files and update data->files + data->response = GTK_RESPONSE_ACCEPT; + data->files = chooser_get_files (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); + } + // free data! + filechooser_quartz_data_free (data); + }; + + if (data->parent != NULL && data->modal) + { + [data->panel setLevel:NSModalPanelWindowLevel]; + [data->panel beginSheetModalForWindow:data->parent completionHandler:handler]; + } + else + { + [data->panel setLevel:NSModalPanelWindowLevel]; + [data->panel beginWithCompletionHandler:handler]; + } + + return TRUE; +} + +static gchar * +strip_mnemonic (const gchar *s) +{ + gchar *escaped; + gchar *ret = NULL; + + if (s == NULL) + return NULL; + + escaped = g_markup_escape_text (s, -1); + pango_parse_markup (escaped, -1, '_', NULL, &ret, NULL, NULL); + + if (ret != NULL) + { + return ret; + } + else + { + return g_strdup (s); + } +} + +gboolean +gtk_file_chooser_native_quartz_show (GtkFileChooserNative *self) +{ + FileChooserQuartzData *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 (FileChooserQuartzData, 1); + + // examine filters! TODO + + self->mode_data = data; + data->self = g_object_ref (self); + + data->create_folders = gtk_file_chooser_get_create_folders( GTK_FILE_CHOOSER (self)); + + // shortcut_folder_uris support seems difficult if not impossible + + // mnemonics are not supported on macOS, so remove the underscores + data->accept_label = strip_mnemonic (self->accept_label); + // cancel button is not present in macOS filechooser dialogs! + // data->cancel_label = strip_mnemonic (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; + + // overwrite confirmation appears to be always on + if (gtk_file_chooser_get_do_overwrite_confirmation (GTK_FILE_CHOOSER (self->dialog))) + data->overwrite_confirmation = TRUE; + + // showsHiddenFiles?? + 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_quartz_window_get_nswindow (gtk_widget_get_window (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); + } + + return filechooser_quartz_launch(data); +} + +void +gtk_file_chooser_native_quartz_hide (GtkFileChooserNative *self) +{ + FileChooserQuartzData *data = self->mode_data; + + /* This is always set while dialog visible */ + g_assert (data != NULL); + + data->skip_response = TRUE; + if (data->panel == NULL) + return; + + [data->panel orderBack:nil]; + [data->panel close]; + if (data->parent) + [data->parent orderFront:nil]; + data->panel = NULL; +} +