/* -*- 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 "gtksizerequest.h" #include "gtktypebuiltins.h" #include "gtkintl.h" #include "gtksettings.h" #include "gtktogglebutton.h" #include "gtkstylecontext.h" #include "gtkheaderbar.h" #include "gtklabel.h" #include "gtknative.h" #include "gtkfilefilterprivate.h" #include "macos/gdkmacos.h" #include "macos/gdkmacossurface-private.h" typedef struct { GtkFileChooserNative *self; NSSavePanel *panel; NSWindow *parent; NSWindow *key_window; gboolean skip_response; gboolean save; gboolean folder; gboolean create_folders; gboolean modal; gboolean select_multiple; gboolean running; char *accept_label; char *cancel_label; char *title; char *message; GFile *current_folder; GFile *current_file; char *current_name; NSMutableArray *filters; NSMutableArray *filter_names; NSComboBox *filter_combo_box; GSList *files; int response; } FileChooserQuartzData; @interface FilterComboBox : NSObject { FileChooserQuartzData *data; } - (id) initWithData:(FileChooserQuartzData *) quartz_data; - (void)comboBoxSelectionDidChange:(NSNotification *)notification; @end @implementation FilterComboBox - (id) initWithData:(FileChooserQuartzData *) quartz_data { [super init]; data = quartz_data; return self; } - (void)comboBoxSelectionDidChange:(NSNotification *)notification { NSInteger selected_index = [data->filter_combo_box indexOfSelectedItem]; NSArray *filter = [data->filters objectAtIndex:selected_index]; // check for empty strings in filter -> indicates all filetypes should be allowed! if ([filter containsObject:@""]) [data->panel setAllowedFileTypes:nil]; else [data->panel setAllowedFileTypes:filter]; GListModel *filters = gtk_file_chooser_get_filters (GTK_FILE_CHOOSER (data->self)); data->self->current_filter = g_list_model_get_item (filters, selected_index); g_object_unref (data->self->current_filter); g_object_unref (filters); g_object_notify (G_OBJECT (data->self), "filter"); } @end 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; int 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) { char *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 char *name) { if (name != NULL) [data->panel setNameFieldStringValue:[NSString stringWithUTF8String:name]]; } static void filechooser_quartz_data_free (FileChooserQuartzData *data) { if (data->filters) { [data->filters release]; } if (data->filter_names) { [data->filter_names release]; } g_clear_object (&data->current_folder); g_clear_object (&data->current_file); g_free (data->current_name); 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->message); g_free (data); } @protocol CanSetAccessoryViewDisclosed - (void)setAccessoryViewDisclosed:(BOOL)val; @end static gboolean filechooser_quartz_launch (FileChooserQuartzData *data) { if (data->save) { if (data->folder) { NSOpenPanel *panel = [[NSOpenPanel openPanel] retain]; [panel setCanChooseDirectories:YES]; [panel setCanChooseFiles:NO]; [panel setCanCreateDirectories:YES]; data->panel = panel; } else { NSSavePanel *panel = [[NSSavePanel savePanel] retain]; if (data->create_folders) { [panel setCanCreateDirectories:YES]; } else { [panel setCanCreateDirectories:NO]; } data->panel = panel; } } 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->accept_label) [data->panel setPrompt:[NSString stringWithUTF8String:data->accept_label]]; if (data->title) [data->panel setTitle:[NSString stringWithUTF8String:data->title]]; if (data->message) [data->panel setMessage:[NSString stringWithUTF8String:data->message]]; if (data->current_file) { GFile *folder; char *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) { // when filters have been provided, a combobox needs to be added data->filter_combo_box = [[NSComboBox alloc] initWithFrame:NSMakeRect(0.0, 0.0, 200, 20)]; [data->filter_combo_box addItemsWithObjectValues:data->filter_names]; [data->filter_combo_box setEditable:NO]; [data->filter_combo_box setDelegate:[[FilterComboBox alloc] initWithData:data]]; if (data->self->current_filter) { GListModel *filters; guint i, n; guint current_filter_index = GTK_INVALID_LIST_POSITION; filters = gtk_file_chooser_get_filters (GTK_FILE_CHOOSER (data->self)); n = g_list_model_get_n_items (filters); for (i = 0; i < n; i++) { gpointer item = g_list_model_get_item (filters, i); if (item == data->self->current_filter) { g_object_unref (item); current_filter_index = i; break; } g_object_unref (item); } g_object_unref (filters); if (current_filter_index != GTK_INVALID_LIST_POSITION) [data->filter_combo_box selectItemAtIndex:current_filter_index]; else [data->filter_combo_box selectItemAtIndex:0]; } else { [data->filter_combo_box selectItemAtIndex:0]; } [data->filter_combo_box setToolTip:[NSString stringWithUTF8String:_("Select which types of files are shown")]]; [data->panel setAccessoryView:data->filter_combo_box]; if ([data->panel isKindOfClass:[NSOpenPanel class]] && [data->panel respondsToSelector:@selector(setAccessoryViewDisclosed:)]) { [(id) data->panel setAccessoryViewDisclosed:YES]; } } 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->parent) { [data->panel orderOut:nil]; [data->parent makeKeyAndOrderFront:nil]; } else { [data->key_window makeKeyAndOrderFront:nil]; } 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 beginSheetModalForWindow:data->parent completionHandler:handler]; } else { [data->panel beginWithCompletionHandler:handler]; } return TRUE; } static char * strip_mnemonic (const char *s) { char *escaped; char *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); } } static gboolean file_filter_to_quartz (GtkFileFilter *file_filter, NSMutableArray *filters, NSMutableArray *filter_names) { const char *name; NSArray *pattern_nsstrings; pattern_nsstrings = _gtk_file_filter_get_as_pattern_nsstrings (file_filter); if (pattern_nsstrings == NULL) return FALSE; name = gtk_file_filter_get_name (file_filter); NSString *name_nsstring; if (name == NULL) { name_nsstring = [pattern_nsstrings componentsJoinedByString:@","];; } else { name_nsstring = [NSString stringWithUTF8String:name]; [name_nsstring retain]; } [filter_names addObject:name_nsstring]; [filters addObject:pattern_nsstrings]; return TRUE; } gboolean gtk_file_chooser_native_quartz_show (GtkFileChooserNative *self) { FileChooserQuartzData *data; GtkWindow *transient_for; GtkFileChooserAction action; GListModel *filters; guint n_filters, i; char *message = NULL; data = g_new0 (FileChooserQuartzData, 1); // examine filters! filters = gtk_file_chooser_get_filters (GTK_FILE_CHOOSER (self)); n_filters = g_list_model_get_n_items (filters); if (n_filters > 0) { data->filters = [NSMutableArray arrayWithCapacity:n_filters]; [data->filters retain]; data->filter_names = [NSMutableArray arrayWithCapacity:n_filters]; [data->filter_names retain]; for (i = 0; i < n_filters; i++) { GtkFileFilter *filter = g_list_model_get_item (filters, i); if (!file_filter_to_quartz (filter, data->filters, data->filter_names)) { filechooser_quartz_data_free (data); g_object_unref (filter); g_object_unref (filters); return FALSE; } g_object_unref (filter); } self->current_filter = gtk_file_chooser_get_filter (GTK_FILE_CHOOSER (self)); } else { self->current_filter = NULL; } g_object_unref (filters); 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) data->save = TRUE; if (action == GTK_FILE_CHOOSER_ACTION_SELECT_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; 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_macos_surface_get_native (gtk_native_get_surface (GTK_NATIVE (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))); data->message = message; 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) data->current_name = g_strdup (self->current_name); } data->key_window = [NSApp keyWindow]; 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 makeKeyAndOrderFront:nil]; } else { [data->key_window makeKeyAndOrderFront:nil]; } data->panel = NULL; }