| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "include/file_selector_linux/file_selector_plugin.h" |
| |
| #include <flutter_linux/flutter_linux.h> |
| #include <gtk/gtk.h> |
| |
| #include "file_selector_plugin_private.h" |
| |
| // From file_selector_linux.dart |
| const char kChannelName[] = "plugins.flutter.dev/file_selector_linux"; |
| |
| const char kOpenFileMethod[] = "openFile"; |
| const char kGetSavePathMethod[] = "getSavePath"; |
| const char kGetDirectoryPathMethod[] = "getDirectoryPath"; |
| |
| const char kAcceptedTypeGroupsKey[] = "acceptedTypeGroups"; |
| const char kConfirmButtonTextKey[] = "confirmButtonText"; |
| const char kInitialDirectoryKey[] = "initialDirectory"; |
| const char kMultipleKey[] = "multiple"; |
| const char kSuggestedNameKey[] = "suggestedName"; |
| |
| const char kTypeGroupLabelKey[] = "label"; |
| const char kTypeGroupExtensionsKey[] = "extensions"; |
| const char kTypeGroupMimeTypesKey[] = "mimeTypes"; |
| |
| // Errors |
| const char kBadArgumentsError[] = "Bad Arguments"; |
| const char kNoScreenError[] = "No Screen"; |
| |
| struct _FlFileSelectorPlugin { |
| GObject parent_instance; |
| |
| FlPluginRegistrar* registrar; |
| |
| // Connection to Flutter engine. |
| FlMethodChannel* channel; |
| }; |
| |
| G_DEFINE_TYPE(FlFileSelectorPlugin, fl_file_selector_plugin, G_TYPE_OBJECT) |
| |
| // Converts a type group received from Flutter into a GTK file filter. |
| static GtkFileFilter* type_group_to_filter(FlValue* value) { |
| g_autoptr(GtkFileFilter) filter = gtk_file_filter_new(); |
| |
| FlValue* label = fl_value_lookup_string(value, kTypeGroupLabelKey); |
| if (label != nullptr && fl_value_get_type(label) == FL_VALUE_TYPE_STRING) { |
| gtk_file_filter_set_name(filter, fl_value_get_string(label)); |
| } |
| |
| FlValue* extensions = fl_value_lookup_string(value, kTypeGroupExtensionsKey); |
| if (extensions != nullptr && |
| fl_value_get_type(extensions) == FL_VALUE_TYPE_LIST) { |
| for (size_t i = 0; i < fl_value_get_length(extensions); i++) { |
| FlValue* v = fl_value_get_list_value(extensions, i); |
| const gchar* pattern = fl_value_get_string(v); |
| gtk_file_filter_add_pattern(filter, pattern); |
| } |
| } |
| FlValue* mime_types = fl_value_lookup_string(value, kTypeGroupMimeTypesKey); |
| if (mime_types != nullptr && |
| fl_value_get_type(mime_types) == FL_VALUE_TYPE_LIST) { |
| for (size_t i = 0; i < fl_value_get_length(mime_types); i++) { |
| FlValue* v = fl_value_get_list_value(mime_types, i); |
| const gchar* pattern = fl_value_get_string(v); |
| gtk_file_filter_add_mime_type(filter, pattern); |
| } |
| } |
| |
| return GTK_FILE_FILTER(g_object_ref(filter)); |
| } |
| |
| // Creates a GtkFileChooserNative for the given method call details. |
| static GtkFileChooserNative* create_dialog( |
| GtkWindow* window, GtkFileChooserAction action, const gchar* title, |
| const gchar* default_confirm_button_text, FlValue* properties) { |
| const gchar* confirm_button_text = default_confirm_button_text; |
| FlValue* value = fl_value_lookup_string(properties, kConfirmButtonTextKey); |
| if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) |
| confirm_button_text = fl_value_get_string(value); |
| |
| g_autoptr(GtkFileChooserNative) dialog = |
| GTK_FILE_CHOOSER_NATIVE(gtk_file_chooser_native_new( |
| title, window, action, confirm_button_text, "_Cancel")); |
| |
| value = fl_value_lookup_string(properties, kMultipleKey); |
| if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_BOOL) { |
| gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), |
| fl_value_get_bool(value)); |
| } |
| |
| value = fl_value_lookup_string(properties, kInitialDirectoryKey); |
| if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) { |
| gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), |
| fl_value_get_string(value)); |
| } |
| |
| value = fl_value_lookup_string(properties, kSuggestedNameKey); |
| if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) { |
| gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), |
| fl_value_get_string(value)); |
| } |
| |
| value = fl_value_lookup_string(properties, kAcceptedTypeGroupsKey); |
| if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_LIST) { |
| for (size_t i = 0; i < fl_value_get_length(value); i++) { |
| FlValue* type_group = fl_value_get_list_value(value, i); |
| GtkFileFilter* filter = type_group_to_filter(type_group); |
| if (filter == nullptr) { |
| return nullptr; |
| } |
| gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter); |
| } |
| } |
| |
| return GTK_FILE_CHOOSER_NATIVE(g_object_ref(dialog)); |
| } |
| |
| // TODO(stuartmorgan): Move this logic back into method_call_cb once |
| // https://github.com/flutter/flutter/issues/88724 is fixed, and test |
| // through the public API instead. This only exists to move as much |
| // logic as possible behind the private entry point used by unit tests. |
| GtkFileChooserNative* create_dialog_for_method(GtkWindow* window, |
| const gchar* method, |
| FlValue* properties) { |
| if (strcmp(method, kOpenFileMethod) == 0) { |
| return create_dialog(window, GTK_FILE_CHOOSER_ACTION_OPEN, "Open File", |
| "_Open", properties); |
| } else if (strcmp(method, kGetDirectoryPathMethod) == 0) { |
| return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, |
| "Choose Directory", "_Open", properties); |
| } else if (strcmp(method, kGetSavePathMethod) == 0) { |
| return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SAVE, "Save File", |
| "_Save", properties); |
| } |
| return nullptr; |
| } |
| |
| // Shows the requested dialog type. |
| static FlMethodResponse* show_dialog(FlFileSelectorPlugin* self, |
| const gchar* method, FlValue* properties, |
| bool return_list) { |
| if (fl_value_get_type(properties) != FL_VALUE_TYPE_MAP) { |
| return FL_METHOD_RESPONSE(fl_method_error_response_new( |
| kBadArgumentsError, "Argument map missing or malformed", nullptr)); |
| } |
| |
| FlView* view = fl_plugin_registrar_get_view(self->registrar); |
| if (view == nullptr) { |
| return FL_METHOD_RESPONSE( |
| fl_method_error_response_new(kNoScreenError, nullptr, nullptr)); |
| } |
| GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(view))); |
| |
| g_autoptr(GtkFileChooserNative) dialog = |
| create_dialog_for_method(window, method, properties); |
| |
| if (dialog == nullptr) { |
| return FL_METHOD_RESPONSE(fl_method_error_response_new( |
| kBadArgumentsError, "Unable to create dialog from arguments", nullptr)); |
| } |
| |
| gint response = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog)); |
| g_autoptr(FlValue) result = nullptr; |
| if (response == GTK_RESPONSE_ACCEPT) { |
| if (return_list) { |
| result = fl_value_new_list(); |
| g_autoptr(GSList) filenames = |
| gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog)); |
| for (GSList* link = filenames; link != nullptr; link = link->next) { |
| g_autofree gchar* filename = static_cast<gchar*>(link->data); |
| fl_value_append_take(result, fl_value_new_string(filename)); |
| } |
| } else { |
| g_autofree gchar* filename = |
| gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); |
| result = fl_value_new_string(filename); |
| } |
| } |
| |
| return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); |
| } |
| |
| // Called when a method call is received from Flutter. |
| static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, |
| gpointer user_data) { |
| FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(user_data); |
| |
| const gchar* method = fl_method_call_get_name(method_call); |
| FlValue* args = fl_method_call_get_args(method_call); |
| |
| g_autoptr(FlMethodResponse) response = nullptr; |
| if (strcmp(method, kOpenFileMethod) == 0 || |
| strcmp(method, kGetDirectoryPathMethod) == 0) { |
| response = show_dialog(self, method, args, true); |
| } else if (strcmp(method, kGetSavePathMethod) == 0) { |
| response = show_dialog(self, method, args, false); |
| } else { |
| response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); |
| } |
| |
| g_autoptr(GError) error = nullptr; |
| if (!fl_method_call_respond(method_call, response, &error)) |
| g_warning("Failed to send method call response: %s", error->message); |
| } |
| |
| static void fl_file_selector_plugin_dispose(GObject* object) { |
| FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(object); |
| |
| g_clear_object(&self->registrar); |
| g_clear_object(&self->channel); |
| |
| G_OBJECT_CLASS(fl_file_selector_plugin_parent_class)->dispose(object); |
| } |
| |
| static void fl_file_selector_plugin_class_init( |
| FlFileSelectorPluginClass* klass) { |
| G_OBJECT_CLASS(klass)->dispose = fl_file_selector_plugin_dispose; |
| } |
| |
| static void fl_file_selector_plugin_init(FlFileSelectorPlugin* self) {} |
| |
| FlFileSelectorPlugin* fl_file_selector_plugin_new( |
| FlPluginRegistrar* registrar) { |
| FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN( |
| g_object_new(fl_file_selector_plugin_get_type(), nullptr)); |
| |
| self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar)); |
| |
| g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); |
| self->channel = |
| fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), |
| kChannelName, FL_METHOD_CODEC(codec)); |
| fl_method_channel_set_method_call_handler(self->channel, method_call_cb, |
| g_object_ref(self), g_object_unref); |
| |
| return self; |
| } |
| |
| void file_selector_plugin_register_with_registrar( |
| FlPluginRegistrar* registrar) { |
| FlFileSelectorPlugin* plugin = fl_file_selector_plugin_new(registrar); |
| g_object_unref(plugin); |
| } |