From 7c97c8809bc991609f0148298e5a6ed8aa137cd3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 2 Oct 2024 11:35:01 -0700 Subject: [PATCH] [file_selector] Convert Linux to Pigeon (#7770) Replaces manual method channel code with Pigeon. I made a couple of simplifications to the Dart/C boundary while converting: - The return value is a always a list now; it's easier to handle that on the Dart side than track and handle in the native code. - It's a single method with a type enum, instead of several different methods, since that mirrors the underlying SDK. These changes are consistent with our general trend toward putting more logic in Dart and less in native code, for ease of maintenance. Fixes https://github.com/flutter/flutter/issues/117906 --- .../file_selector_linux/CHANGELOG.md | 3 +- .../lib/file_selector_linux.dart | 171 +++--- .../lib/src/messages.g.dart | 195 +++++++ .../file_selector_linux/linux/CMakeLists.txt | 3 +- .../linux/file_selector_plugin.cc | 211 +++---- .../linux/file_selector_plugin_private.h | 13 +- .../file_selector_linux/linux/messages.g.cc | 545 ++++++++++++++++++ .../file_selector_linux/linux/messages.g.h | 238 ++++++++ .../linux/test/file_selector_plugin_test.cc | 123 ++-- .../file_selector_linux/pigeons/copyright.txt | 3 + .../file_selector_linux/pigeons/messages.dart | 62 ++ .../file_selector_linux/pubspec.yaml | 3 +- .../test/file_selector_linux_test.dart | 493 ++++++---------- 13 files changed, 1482 insertions(+), 581 deletions(-) create mode 100644 packages/file_selector/file_selector_linux/lib/src/messages.g.dart create mode 100644 packages/file_selector/file_selector_linux/linux/messages.g.cc create mode 100644 packages/file_selector/file_selector_linux/linux/messages.g.h create mode 100644 packages/file_selector/file_selector_linux/pigeons/copyright.txt create mode 100644 packages/file_selector/file_selector_linux/pigeons/messages.dart diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md index 9934d46c0575..74a5f6edc7d2 100644 --- a/packages/file_selector/file_selector_linux/CHANGELOG.md +++ b/packages/file_selector/file_selector_linux/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.9.3 +* Updates method channel implementation to use Pigeon. * Updates minimum supported SDK version to Flutter 3.19/Dart 3.3. ## 0.9.2+1 diff --git a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart index b06523b27d32..3a05243c3110 100644 --- a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart +++ b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart @@ -4,30 +4,17 @@ import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.dev/file_selector_linux'); - -const String _typeGroupLabelKey = 'label'; -const String _typeGroupExtensionsKey = 'extensions'; -const String _typeGroupMimeTypesKey = 'mimeTypes'; - -const String _openFileMethod = 'openFile'; -const String _getSavePathMethod = 'getSavePath'; -const String _getDirectoryPathMethod = 'getDirectoryPath'; - -const String _acceptedTypeGroupsKey = 'acceptedTypeGroups'; -const String _confirmButtonTextKey = 'confirmButtonText'; -const String _initialDirectoryKey = 'initialDirectory'; -const String _multipleKey = 'multiple'; -const String _suggestedNameKey = 'suggestedName'; +import 'src/messages.g.dart'; /// An implementation of [FileSelectorPlatform] for Linux. class FileSelectorLinux extends FileSelectorPlatform { - /// The MethodChannel that is being used by this implementation of the plugin. - @visibleForTesting - MethodChannel get channel => _channel; + /// Creates a new plugin implementation instance. + FileSelectorLinux({ + @visibleForTesting FileSelectorApi? api, + }) : _hostApi = api ?? FileSelectorApi(); + + final FileSelectorApi _hostApi; /// Registers the Linux implementation. static void registerWith() { @@ -40,19 +27,16 @@ class FileSelectorLinux extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List> serializedTypeGroups = - _serializeTypeGroups(acceptedTypeGroups); - final List? path = await _channel.invokeListMethod( - _openFileMethod, - { - if (serializedTypeGroups.isNotEmpty) - _acceptedTypeGroupsKey: serializedTypeGroups, - 'initialDirectory': initialDirectory, - _confirmButtonTextKey: confirmButtonText, - _multipleKey: false, - }, - ); - return path == null ? null : XFile(path.first); + final List paths = await _hostApi.showFileChooser( + PlatformFileChooserActionType.open, + PlatformFileChooserOptions( + allowedFileTypes: + _platformTypeGroupsFromXTypeGroups(acceptedTypeGroups), + currentFolderPath: initialDirectory, + acceptButtonLabel: confirmButtonText, + selectMultiple: false, + )); + return paths.isEmpty ? null : XFile(paths.first); } @override @@ -61,19 +45,16 @@ class FileSelectorLinux extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List> serializedTypeGroups = - _serializeTypeGroups(acceptedTypeGroups); - final List? pathList = await _channel.invokeListMethod( - _openFileMethod, - { - if (serializedTypeGroups.isNotEmpty) - _acceptedTypeGroupsKey: serializedTypeGroups, - _initialDirectoryKey: initialDirectory, - _confirmButtonTextKey: confirmButtonText, - _multipleKey: true, - }, - ); - return pathList?.map((String path) => XFile(path)).toList() ?? []; + final List paths = await _hostApi.showFileChooser( + PlatformFileChooserActionType.open, + PlatformFileChooserOptions( + allowedFileTypes: + _platformTypeGroupsFromXTypeGroups(acceptedTypeGroups), + currentFolderPath: initialDirectory, + acceptButtonLabel: confirmButtonText, + selectMultiple: true, + )); + return paths.map((String path) => XFile(path)).toList(); } @override @@ -98,21 +79,18 @@ class FileSelectorLinux extends FileSelectorPlatform { List? acceptedTypeGroups, SaveDialogOptions options = const SaveDialogOptions(), }) async { - final List> serializedTypeGroups = - _serializeTypeGroups(acceptedTypeGroups); // TODO(stuartmorgan): Add the selected type group here and return it. See // https://github.com/flutter/flutter/issues/107093 - final String? path = await _channel.invokeMethod( - _getSavePathMethod, - { - if (serializedTypeGroups.isNotEmpty) - _acceptedTypeGroupsKey: serializedTypeGroups, - _initialDirectoryKey: options.initialDirectory, - _suggestedNameKey: options.suggestedName, - _confirmButtonTextKey: options.confirmButtonText, - }, - ); - return path == null ? null : FileSaveLocation(path); + final List paths = await _hostApi.showFileChooser( + PlatformFileChooserActionType.save, + PlatformFileChooserOptions( + allowedFileTypes: + _platformTypeGroupsFromXTypeGroups(acceptedTypeGroups), + currentFolderPath: options.initialDirectory, + currentName: options.suggestedName, + acceptButtonLabel: options.confirmButtonText, + )); + return paths.isEmpty ? null : FileSaveLocation(paths.first); } @override @@ -120,12 +98,14 @@ class FileSelectorLinux extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List? path = await _channel - .invokeListMethod(_getDirectoryPathMethod, { - _initialDirectoryKey: initialDirectory, - _confirmButtonTextKey: confirmButtonText, - }); - return path?.first; + final List paths = await _hostApi.showFileChooser( + PlatformFileChooserActionType.chooseDirectory, + PlatformFileChooserOptions( + currentFolderPath: initialDirectory, + acceptButtonLabel: confirmButtonText, + selectMultiple: false, + )); + return paths.isEmpty ? null : paths.first; } @override @@ -133,43 +113,42 @@ class FileSelectorLinux extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List? pathList = await _channel - .invokeListMethod(_getDirectoryPathMethod, { - _initialDirectoryKey: initialDirectory, - _confirmButtonTextKey: confirmButtonText, - _multipleKey: true, - }); - return pathList ?? []; + return _hostApi.showFileChooser( + PlatformFileChooserActionType.chooseDirectory, + PlatformFileChooserOptions( + currentFolderPath: initialDirectory, + acceptButtonLabel: confirmButtonText, + selectMultiple: true, + )); } } -List> _serializeTypeGroups(List? groups) { - return (groups ?? []).map(_serializeTypeGroup).toList(); +List? _platformTypeGroupsFromXTypeGroups( + List? groups) { + return groups?.map(_platformTypeGroupFromXTypeGroup).toList(); } -Map _serializeTypeGroup(XTypeGroup group) { - final Map serialization = { - _typeGroupLabelKey: group.label ?? '', - }; +PlatformTypeGroup _platformTypeGroupFromXTypeGroup(XTypeGroup group) { + final String label = group.label ?? ''; if (group.allowsAny) { - serialization[_typeGroupExtensionsKey] = ['*']; - } else { - if ((group.extensions?.isEmpty ?? true) && - (group.mimeTypes?.isEmpty ?? true)) { - throw ArgumentError('Provided type group $group does not allow ' - 'all files, but does not set any of the Linux-supported filter ' - 'categories. "extensions" or "mimeTypes" must be non-empty for Linux ' - 'if anything is non-empty.'); - } - if (group.extensions?.isNotEmpty ?? false) { - serialization[_typeGroupExtensionsKey] = group.extensions + return PlatformTypeGroup( + label: label, + extensions: ['*'], + ); + } + if ((group.extensions?.isEmpty ?? true) && + (group.mimeTypes?.isEmpty ?? true)) { + throw ArgumentError('Provided type group $group does not allow ' + 'all files, but does not set any of the Linux-supported filter ' + 'categories. "extensions" or "mimeTypes" must be non-empty for Linux ' + 'if anything is non-empty.'); + } + return PlatformTypeGroup( + label: label, + // Covert to GtkFileFilter's *. format. + extensions: group.extensions ?.map((String extension) => '*.$extension') .toList() ?? - []; - } - if (group.mimeTypes?.isNotEmpty ?? false) { - serialization[_typeGroupMimeTypesKey] = group.mimeTypes ?? []; - } - } - return serialization; + [], + mimeTypes: group.mimeTypes ?? []); } diff --git a/packages/file_selector/file_selector_linux/lib/src/messages.g.dart b/packages/file_selector/file_selector_linux/lib/src/messages.g.dart new file mode 100644 index 000000000000..7a91c769716b --- /dev/null +++ b/packages/file_selector/file_selector_linux/lib/src/messages.g.dart @@ -0,0 +1,195 @@ +// 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. +// Autogenerated from Pigeon (v22.4.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +/// A Pigeon representation of the GTK_FILE_CHOOSER_ACTION_* options. +enum PlatformFileChooserActionType { + open, + chooseDirectory, + save, +} + +/// A Pigeon representation of the Linux portion of an `XTypeGroup`. +class PlatformTypeGroup { + PlatformTypeGroup({ + this.label = '', + this.extensions = const [], + this.mimeTypes = const [], + }); + + String label; + + List extensions; + + List mimeTypes; + + Object encode() { + return [ + label, + extensions, + mimeTypes, + ]; + } + + static PlatformTypeGroup decode(Object result) { + result as List; + return PlatformTypeGroup( + label: result[0]! as String, + extensions: (result[1] as List?)!.cast(), + mimeTypes: (result[2] as List?)!.cast(), + ); + } +} + +/// Options for GKT file chooser. +/// +/// These correspond to gtk_file_chooser_set_* options. +class PlatformFileChooserOptions { + PlatformFileChooserOptions({ + this.allowedFileTypes, + this.currentFolderPath, + this.currentName, + this.acceptButtonLabel, + this.selectMultiple, + }); + + List? allowedFileTypes; + + String? currentFolderPath; + + String? currentName; + + String? acceptButtonLabel; + + /// Whether to allow multiple file selection. + /// + /// Nullable because it does not apply to the "save" action. + bool? selectMultiple; + + Object encode() { + return [ + allowedFileTypes, + currentFolderPath, + currentName, + acceptButtonLabel, + selectMultiple, + ]; + } + + static PlatformFileChooserOptions decode(Object result) { + result as List; + return PlatformFileChooserOptions( + allowedFileTypes: + (result[0] as List?)?.cast(), + currentFolderPath: result[1] as String?, + currentName: result[2] as String?, + acceptButtonLabel: result[3] as String?, + selectMultiple: result[4] as bool?, + ); + } +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is PlatformFileChooserActionType) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PlatformTypeGroup) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is PlatformFileChooserOptions) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final int? value = readValue(buffer) as int?; + return value == null + ? null + : PlatformFileChooserActionType.values[value]; + case 130: + return PlatformTypeGroup.decode(readValue(buffer)!); + case 131: + return PlatformFileChooserOptions.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class FileSelectorApi { + /// Constructor for [FileSelectorApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + FileSelectorApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Shows an file chooser with the given [type] and [options], returning the + /// list of selected paths. + /// + /// An empty list corresponds to a cancelled selection. + Future> showFileChooser(PlatformFileChooserActionType type, + PlatformFileChooserOptions options) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.file_selector_linux.FileSelectorApi.showFileChooser$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = await pigeonVar_channel + .send([type, options]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } +} diff --git a/packages/file_selector/file_selector_linux/linux/CMakeLists.txt b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt index 1b1af0790de7..148819dc3c5d 100644 --- a/packages/file_selector/file_selector_linux/linux/CMakeLists.txt +++ b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt @@ -8,10 +8,11 @@ set(PLUGIN_NAME "${PROJECT_NAME}_plugin") list(APPEND PLUGIN_SOURCES "file_selector_plugin.cc" + "messages.g.cc" ) add_library(${PLUGIN_NAME} SHARED - "file_selector_plugin.cc" + ${PLUGIN_SOURCES} ) apply_standard_settings(${PLUGIN_NAME}) set_target_properties(${PLUGIN_NAME} PROPERTIES diff --git a/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc index 5a8cc2132595..db27d38c80c2 100644 --- a/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc +++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc @@ -8,25 +8,9 @@ #include #include "file_selector_plugin_private.h" +#include "messages.g.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 +// Error codes. const char kBadArgumentsError[] = "Bad Arguments"; const char kNoScreenError[] = "No Screen"; @@ -34,39 +18,28 @@ 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) { +static GtkFileFilter* type_group_to_filter(FfsPlatformTypeGroup* group) { 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)); - } + const gchar* label = ffs_platform_type_group_get_label(group); + gtk_file_filter_set_name(filter, 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* extensions = ffs_platform_type_group_get_extensions(group); + 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); - } + FlValue* mime_types = ffs_platform_type_group_get_mime_types(group); + 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)); @@ -75,39 +48,45 @@ static GtkFileFilter* type_group_to_filter(FlValue* value) { // 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); + const gchar* default_confirm_button_text, + FfsPlatformFileChooserOptions* options) { + const gchar* confirm_button_text = + ffs_platform_file_chooser_options_get_accept_button_label(options); + if (confirm_button_text == nullptr) { + confirm_button_text = default_confirm_button_text; + } 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) { + const gboolean* select_multiple = + ffs_platform_file_chooser_options_get_select_multiple(options); + if (select_multiple != nullptr) { gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), - fl_value_get_bool(value)); + *select_multiple); } - value = fl_value_lookup_string(properties, kInitialDirectoryKey); - if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) { + const gchar* current_folder = + ffs_platform_file_chooser_options_get_current_folder_path(options); + if (current_folder != nullptr) { gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), - fl_value_get_string(value)); + current_folder); } - 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)); + const gchar* current_name = + ffs_platform_file_chooser_options_get_current_name(options); + if (current_name != nullptr) { + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), current_name); } - 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); + FlValue* type_groups = + ffs_platform_file_chooser_options_get_allowed_file_types(options); + if (type_groups != nullptr) { + for (size_t i = 0; i < fl_value_get_length(type_groups); i++) { + FlValue* type_group = fl_value_get_list_value(type_groups, i); + GtkFileFilter* filter = type_group_to_filter(FFS_PLATFORM_TYPE_GROUP( + fl_value_get_custom_value_object(type_group))); if (filter == nullptr) { return nullptr; } @@ -118,99 +97,65 @@ static GtkFileChooserNative* create_dialog( 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); +GtkFileChooserNative* create_dialog_of_type( + GtkWindow* window, FfsPlatformFileChooserActionType type, + FfsPlatformFileChooserOptions* options) { + switch (type) { + case FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_OPEN: + return create_dialog(window, GTK_FILE_CHOOSER_ACTION_OPEN, "Open File", + "_Open", options); + case FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_CHOOSE_DIRECTORY: + return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, + "Choose Directory", "_Open", options); + case FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_SAVE: + return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SAVE, "Save File", + "_Save", options); } 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)); - } +static FfsFileSelectorApiShowFileChooserResponse* handle_show_file_chooser( + FfsPlatformFileChooserActionType type, + FfsPlatformFileChooserOptions* options, gpointer user_data) { + FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(user_data); FlView* view = fl_plugin_registrar_get_view(self->registrar); if (view == nullptr) { - return FL_METHOD_RESPONSE( - fl_method_error_response_new(kNoScreenError, nullptr, nullptr)); + return ffs_file_selector_api_show_file_chooser_response_new_error( + 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); + create_dialog_of_type(window, type, options); if (dialog == nullptr) { - return FL_METHOD_RESPONSE(fl_method_error_response_new( - kBadArgumentsError, "Unable to create dialog from arguments", nullptr)); + return ffs_file_selector_api_show_file_chooser_response_new_error( + 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(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); + 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(link->data); + fl_value_append_take(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); + return ffs_file_selector_api_show_file_chooser_response_new(result); } static void fl_file_selector_plugin_dispose(GObject* object) { FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(object); + ffs_file_selector_api_clear_method_handlers( + fl_plugin_registrar_get_messenger(self->registrar), nullptr); g_clear_object(&self->registrar); - g_clear_object(&self->channel); G_OBJECT_CLASS(fl_file_selector_plugin_parent_class)->dispose(object); } @@ -229,12 +174,12 @@ FlFileSelectorPlugin* fl_file_selector_plugin_new( 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); + static FfsFileSelectorApiVTable api_vtable = { + .show_file_chooser = handle_show_file_chooser, + }; + ffs_file_selector_api_set_method_handlers( + fl_plugin_registrar_get_messenger(registrar), nullptr, &api_vtable, + g_object_ref(self), g_object_unref); return self; } diff --git a/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h b/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h index e58a78ccda37..d8ee91bdd8a7 100644 --- a/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h +++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h @@ -5,8 +5,15 @@ #include #include "include/file_selector_linux/file_selector_plugin.h" +#include "messages.g.h" // Creates a GtkFileChooserNative for the given method call. -GtkFileChooserNative* create_dialog_for_method(GtkWindow* window, - const gchar* method, - FlValue* properties); +// +// TODO(stuartmorgan): Make this private/static once the tests are restructured +// as descibed in the file_selector_plugin_test.cc TODOs, and then test through +// the Pigeon API handler instead (making that non-static). This only exists to +// move as much logic as possible behind an entry point currently callable by +// unit tests. +GtkFileChooserNative* create_dialog_of_type( + GtkWindow* window, FfsPlatformFileChooserActionType type, + FfsPlatformFileChooserOptions* options); diff --git a/packages/file_selector/file_selector_linux/linux/messages.g.cc b/packages/file_selector/file_selector_linux/linux/messages.g.cc new file mode 100644 index 000000000000..cdcc18b27a26 --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/messages.g.cc @@ -0,0 +1,545 @@ +// 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. +// Autogenerated from Pigeon (v22.4.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#include "messages.g.h" + +struct _FfsPlatformTypeGroup { + GObject parent_instance; + + gchar* label; + FlValue* extensions; + FlValue* mime_types; +}; + +G_DEFINE_TYPE(FfsPlatformTypeGroup, ffs_platform_type_group, G_TYPE_OBJECT) + +static void ffs_platform_type_group_dispose(GObject* object) { + FfsPlatformTypeGroup* self = FFS_PLATFORM_TYPE_GROUP(object); + g_clear_pointer(&self->label, g_free); + g_clear_pointer(&self->extensions, fl_value_unref); + g_clear_pointer(&self->mime_types, fl_value_unref); + G_OBJECT_CLASS(ffs_platform_type_group_parent_class)->dispose(object); +} + +static void ffs_platform_type_group_init(FfsPlatformTypeGroup* self) {} + +static void ffs_platform_type_group_class_init( + FfsPlatformTypeGroupClass* klass) { + G_OBJECT_CLASS(klass)->dispose = ffs_platform_type_group_dispose; +} + +FfsPlatformTypeGroup* ffs_platform_type_group_new(const gchar* label, + FlValue* extensions, + FlValue* mime_types) { + FfsPlatformTypeGroup* self = FFS_PLATFORM_TYPE_GROUP( + g_object_new(ffs_platform_type_group_get_type(), nullptr)); + self->label = g_strdup(label); + self->extensions = fl_value_ref(extensions); + self->mime_types = fl_value_ref(mime_types); + return self; +} + +const gchar* ffs_platform_type_group_get_label(FfsPlatformTypeGroup* self) { + g_return_val_if_fail(FFS_IS_PLATFORM_TYPE_GROUP(self), nullptr); + return self->label; +} + +FlValue* ffs_platform_type_group_get_extensions(FfsPlatformTypeGroup* self) { + g_return_val_if_fail(FFS_IS_PLATFORM_TYPE_GROUP(self), nullptr); + return self->extensions; +} + +FlValue* ffs_platform_type_group_get_mime_types(FfsPlatformTypeGroup* self) { + g_return_val_if_fail(FFS_IS_PLATFORM_TYPE_GROUP(self), nullptr); + return self->mime_types; +} + +static FlValue* ffs_platform_type_group_to_list(FfsPlatformTypeGroup* self) { + FlValue* values = fl_value_new_list(); + fl_value_append_take(values, fl_value_new_string(self->label)); + fl_value_append_take(values, fl_value_ref(self->extensions)); + fl_value_append_take(values, fl_value_ref(self->mime_types)); + return values; +} + +static FfsPlatformTypeGroup* ffs_platform_type_group_new_from_list( + FlValue* values) { + FlValue* value0 = fl_value_get_list_value(values, 0); + const gchar* label = fl_value_get_string(value0); + FlValue* value1 = fl_value_get_list_value(values, 1); + FlValue* extensions = value1; + FlValue* value2 = fl_value_get_list_value(values, 2); + FlValue* mime_types = value2; + return ffs_platform_type_group_new(label, extensions, mime_types); +} + +struct _FfsPlatformFileChooserOptions { + GObject parent_instance; + + FlValue* allowed_file_types; + gchar* current_folder_path; + gchar* current_name; + gchar* accept_button_label; + gboolean* select_multiple; +}; + +G_DEFINE_TYPE(FfsPlatformFileChooserOptions, ffs_platform_file_chooser_options, + G_TYPE_OBJECT) + +static void ffs_platform_file_chooser_options_dispose(GObject* object) { + FfsPlatformFileChooserOptions* self = + FFS_PLATFORM_FILE_CHOOSER_OPTIONS(object); + g_clear_pointer(&self->allowed_file_types, fl_value_unref); + g_clear_pointer(&self->current_folder_path, g_free); + g_clear_pointer(&self->current_name, g_free); + g_clear_pointer(&self->accept_button_label, g_free); + g_clear_pointer(&self->select_multiple, g_free); + G_OBJECT_CLASS(ffs_platform_file_chooser_options_parent_class) + ->dispose(object); +} + +static void ffs_platform_file_chooser_options_init( + FfsPlatformFileChooserOptions* self) {} + +static void ffs_platform_file_chooser_options_class_init( + FfsPlatformFileChooserOptionsClass* klass) { + G_OBJECT_CLASS(klass)->dispose = ffs_platform_file_chooser_options_dispose; +} + +FfsPlatformFileChooserOptions* ffs_platform_file_chooser_options_new( + FlValue* allowed_file_types, const gchar* current_folder_path, + const gchar* current_name, const gchar* accept_button_label, + gboolean* select_multiple) { + FfsPlatformFileChooserOptions* self = FFS_PLATFORM_FILE_CHOOSER_OPTIONS( + g_object_new(ffs_platform_file_chooser_options_get_type(), nullptr)); + if (allowed_file_types != nullptr) { + self->allowed_file_types = fl_value_ref(allowed_file_types); + } else { + self->allowed_file_types = nullptr; + } + if (current_folder_path != nullptr) { + self->current_folder_path = g_strdup(current_folder_path); + } else { + self->current_folder_path = nullptr; + } + if (current_name != nullptr) { + self->current_name = g_strdup(current_name); + } else { + self->current_name = nullptr; + } + if (accept_button_label != nullptr) { + self->accept_button_label = g_strdup(accept_button_label); + } else { + self->accept_button_label = nullptr; + } + if (select_multiple != nullptr) { + self->select_multiple = static_cast(malloc(sizeof(gboolean))); + *self->select_multiple = *select_multiple; + } else { + self->select_multiple = nullptr; + } + return self; +} + +FlValue* ffs_platform_file_chooser_options_get_allowed_file_types( + FfsPlatformFileChooserOptions* self) { + g_return_val_if_fail(FFS_IS_PLATFORM_FILE_CHOOSER_OPTIONS(self), nullptr); + return self->allowed_file_types; +} + +const gchar* ffs_platform_file_chooser_options_get_current_folder_path( + FfsPlatformFileChooserOptions* self) { + g_return_val_if_fail(FFS_IS_PLATFORM_FILE_CHOOSER_OPTIONS(self), nullptr); + return self->current_folder_path; +} + +const gchar* ffs_platform_file_chooser_options_get_current_name( + FfsPlatformFileChooserOptions* self) { + g_return_val_if_fail(FFS_IS_PLATFORM_FILE_CHOOSER_OPTIONS(self), nullptr); + return self->current_name; +} + +const gchar* ffs_platform_file_chooser_options_get_accept_button_label( + FfsPlatformFileChooserOptions* self) { + g_return_val_if_fail(FFS_IS_PLATFORM_FILE_CHOOSER_OPTIONS(self), nullptr); + return self->accept_button_label; +} + +gboolean* ffs_platform_file_chooser_options_get_select_multiple( + FfsPlatformFileChooserOptions* self) { + g_return_val_if_fail(FFS_IS_PLATFORM_FILE_CHOOSER_OPTIONS(self), nullptr); + return self->select_multiple; +} + +static FlValue* ffs_platform_file_chooser_options_to_list( + FfsPlatformFileChooserOptions* self) { + FlValue* values = fl_value_new_list(); + fl_value_append_take(values, self->allowed_file_types != nullptr + ? fl_value_ref(self->allowed_file_types) + : fl_value_new_null()); + fl_value_append_take(values, + self->current_folder_path != nullptr + ? fl_value_new_string(self->current_folder_path) + : fl_value_new_null()); + fl_value_append_take(values, self->current_name != nullptr + ? fl_value_new_string(self->current_name) + : fl_value_new_null()); + fl_value_append_take(values, + self->accept_button_label != nullptr + ? fl_value_new_string(self->accept_button_label) + : fl_value_new_null()); + fl_value_append_take(values, self->select_multiple != nullptr + ? fl_value_new_bool(*self->select_multiple) + : fl_value_new_null()); + return values; +} + +static FfsPlatformFileChooserOptions* +ffs_platform_file_chooser_options_new_from_list(FlValue* values) { + FlValue* value0 = fl_value_get_list_value(values, 0); + FlValue* allowed_file_types = nullptr; + if (fl_value_get_type(value0) != FL_VALUE_TYPE_NULL) { + allowed_file_types = value0; + } + FlValue* value1 = fl_value_get_list_value(values, 1); + const gchar* current_folder_path = nullptr; + if (fl_value_get_type(value1) != FL_VALUE_TYPE_NULL) { + current_folder_path = fl_value_get_string(value1); + } + FlValue* value2 = fl_value_get_list_value(values, 2); + const gchar* current_name = nullptr; + if (fl_value_get_type(value2) != FL_VALUE_TYPE_NULL) { + current_name = fl_value_get_string(value2); + } + FlValue* value3 = fl_value_get_list_value(values, 3); + const gchar* accept_button_label = nullptr; + if (fl_value_get_type(value3) != FL_VALUE_TYPE_NULL) { + accept_button_label = fl_value_get_string(value3); + } + FlValue* value4 = fl_value_get_list_value(values, 4); + gboolean* select_multiple = nullptr; + gboolean select_multiple_value; + if (fl_value_get_type(value4) != FL_VALUE_TYPE_NULL) { + select_multiple_value = fl_value_get_bool(value4); + select_multiple = &select_multiple_value; + } + return ffs_platform_file_chooser_options_new( + allowed_file_types, current_folder_path, current_name, + accept_button_label, select_multiple); +} + +G_DECLARE_FINAL_TYPE(FfsMessageCodec, ffs_message_codec, FFS, MESSAGE_CODEC, + FlStandardMessageCodec) + +struct _FfsMessageCodec { + FlStandardMessageCodec parent_instance; +}; + +G_DEFINE_TYPE(FfsMessageCodec, ffs_message_codec, + fl_standard_message_codec_get_type()) + +static gboolean ffs_message_codec_write_ffs_platform_file_chooser_action_type( + FlStandardMessageCodec* codec, GByteArray* buffer, FlValue* value, + GError** error) { + uint8_t type = 129; + g_byte_array_append(buffer, &type, sizeof(uint8_t)); + return fl_standard_message_codec_write_value(codec, buffer, value, error); +} + +static gboolean ffs_message_codec_write_ffs_platform_type_group( + FlStandardMessageCodec* codec, GByteArray* buffer, + FfsPlatformTypeGroup* value, GError** error) { + uint8_t type = 130; + g_byte_array_append(buffer, &type, sizeof(uint8_t)); + g_autoptr(FlValue) values = ffs_platform_type_group_to_list(value); + return fl_standard_message_codec_write_value(codec, buffer, values, error); +} + +static gboolean ffs_message_codec_write_ffs_platform_file_chooser_options( + FlStandardMessageCodec* codec, GByteArray* buffer, + FfsPlatformFileChooserOptions* value, GError** error) { + uint8_t type = 131; + g_byte_array_append(buffer, &type, sizeof(uint8_t)); + g_autoptr(FlValue) values = ffs_platform_file_chooser_options_to_list(value); + return fl_standard_message_codec_write_value(codec, buffer, values, error); +} + +static gboolean ffs_message_codec_write_value(FlStandardMessageCodec* codec, + GByteArray* buffer, + FlValue* value, GError** error) { + if (fl_value_get_type(value) == FL_VALUE_TYPE_CUSTOM) { + switch (fl_value_get_custom_type(value)) { + case 129: + return ffs_message_codec_write_ffs_platform_file_chooser_action_type( + codec, buffer, + reinterpret_cast( + const_cast(fl_value_get_custom_value(value))), + error); + case 130: + return ffs_message_codec_write_ffs_platform_type_group( + codec, buffer, + FFS_PLATFORM_TYPE_GROUP(fl_value_get_custom_value_object(value)), + error); + case 131: + return ffs_message_codec_write_ffs_platform_file_chooser_options( + codec, buffer, + FFS_PLATFORM_FILE_CHOOSER_OPTIONS( + fl_value_get_custom_value_object(value)), + error); + } + } + + return FL_STANDARD_MESSAGE_CODEC_CLASS(ffs_message_codec_parent_class) + ->write_value(codec, buffer, value, error); +} + +static FlValue* ffs_message_codec_read_ffs_platform_file_chooser_action_type( + FlStandardMessageCodec* codec, GBytes* buffer, size_t* offset, + GError** error) { + return fl_value_new_custom( + 129, fl_standard_message_codec_read_value(codec, buffer, offset, error), + (GDestroyNotify)fl_value_unref); +} + +static FlValue* ffs_message_codec_read_ffs_platform_type_group( + FlStandardMessageCodec* codec, GBytes* buffer, size_t* offset, + GError** error) { + g_autoptr(FlValue) values = + fl_standard_message_codec_read_value(codec, buffer, offset, error); + if (values == nullptr) { + return nullptr; + } + + g_autoptr(FfsPlatformTypeGroup) value = + ffs_platform_type_group_new_from_list(values); + if (value == nullptr) { + g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, + "Invalid data received for MessageData"); + return nullptr; + } + + return fl_value_new_custom_object(130, G_OBJECT(value)); +} + +static FlValue* ffs_message_codec_read_ffs_platform_file_chooser_options( + FlStandardMessageCodec* codec, GBytes* buffer, size_t* offset, + GError** error) { + g_autoptr(FlValue) values = + fl_standard_message_codec_read_value(codec, buffer, offset, error); + if (values == nullptr) { + return nullptr; + } + + g_autoptr(FfsPlatformFileChooserOptions) value = + ffs_platform_file_chooser_options_new_from_list(values); + if (value == nullptr) { + g_set_error(error, FL_MESSAGE_CODEC_ERROR, FL_MESSAGE_CODEC_ERROR_FAILED, + "Invalid data received for MessageData"); + return nullptr; + } + + return fl_value_new_custom_object(131, G_OBJECT(value)); +} + +static FlValue* ffs_message_codec_read_value_of_type( + FlStandardMessageCodec* codec, GBytes* buffer, size_t* offset, int type, + GError** error) { + switch (type) { + case 129: + return ffs_message_codec_read_ffs_platform_file_chooser_action_type( + codec, buffer, offset, error); + case 130: + return ffs_message_codec_read_ffs_platform_type_group(codec, buffer, + offset, error); + case 131: + return ffs_message_codec_read_ffs_platform_file_chooser_options( + codec, buffer, offset, error); + default: + return FL_STANDARD_MESSAGE_CODEC_CLASS(ffs_message_codec_parent_class) + ->read_value_of_type(codec, buffer, offset, type, error); + } +} + +static void ffs_message_codec_init(FfsMessageCodec* self) {} + +static void ffs_message_codec_class_init(FfsMessageCodecClass* klass) { + FL_STANDARD_MESSAGE_CODEC_CLASS(klass)->write_value = + ffs_message_codec_write_value; + FL_STANDARD_MESSAGE_CODEC_CLASS(klass)->read_value_of_type = + ffs_message_codec_read_value_of_type; +} + +static FfsMessageCodec* ffs_message_codec_new() { + FfsMessageCodec* self = + FFS_MESSAGE_CODEC(g_object_new(ffs_message_codec_get_type(), nullptr)); + return self; +} + +struct _FfsFileSelectorApiShowFileChooserResponse { + GObject parent_instance; + + FlValue* value; +}; + +G_DEFINE_TYPE(FfsFileSelectorApiShowFileChooserResponse, + ffs_file_selector_api_show_file_chooser_response, G_TYPE_OBJECT) + +static void ffs_file_selector_api_show_file_chooser_response_dispose( + GObject* object) { + FfsFileSelectorApiShowFileChooserResponse* self = + FFS_FILE_SELECTOR_API_SHOW_FILE_CHOOSER_RESPONSE(object); + g_clear_pointer(&self->value, fl_value_unref); + G_OBJECT_CLASS(ffs_file_selector_api_show_file_chooser_response_parent_class) + ->dispose(object); +} + +static void ffs_file_selector_api_show_file_chooser_response_init( + FfsFileSelectorApiShowFileChooserResponse* self) {} + +static void ffs_file_selector_api_show_file_chooser_response_class_init( + FfsFileSelectorApiShowFileChooserResponseClass* klass) { + G_OBJECT_CLASS(klass)->dispose = + ffs_file_selector_api_show_file_chooser_response_dispose; +} + +FfsFileSelectorApiShowFileChooserResponse* +ffs_file_selector_api_show_file_chooser_response_new(FlValue* return_value) { + FfsFileSelectorApiShowFileChooserResponse* self = + FFS_FILE_SELECTOR_API_SHOW_FILE_CHOOSER_RESPONSE(g_object_new( + ffs_file_selector_api_show_file_chooser_response_get_type(), + nullptr)); + self->value = fl_value_new_list(); + fl_value_append_take(self->value, fl_value_ref(return_value)); + return self; +} + +FfsFileSelectorApiShowFileChooserResponse* +ffs_file_selector_api_show_file_chooser_response_new_error(const gchar* code, + const gchar* message, + FlValue* details) { + FfsFileSelectorApiShowFileChooserResponse* self = + FFS_FILE_SELECTOR_API_SHOW_FILE_CHOOSER_RESPONSE(g_object_new( + ffs_file_selector_api_show_file_chooser_response_get_type(), + nullptr)); + self->value = fl_value_new_list(); + fl_value_append_take(self->value, fl_value_new_string(code)); + fl_value_append_take(self->value, + fl_value_new_string(message != nullptr ? message : "")); + fl_value_append_take(self->value, details != nullptr ? fl_value_ref(details) + : fl_value_new_null()); + return self; +} + +G_DECLARE_FINAL_TYPE(FfsFileSelectorApi, ffs_file_selector_api, FFS, + FILE_SELECTOR_API, GObject) + +struct _FfsFileSelectorApi { + GObject parent_instance; + + const FfsFileSelectorApiVTable* vtable; + gpointer user_data; + GDestroyNotify user_data_free_func; +}; + +G_DEFINE_TYPE(FfsFileSelectorApi, ffs_file_selector_api, G_TYPE_OBJECT) + +static void ffs_file_selector_api_dispose(GObject* object) { + FfsFileSelectorApi* self = FFS_FILE_SELECTOR_API(object); + if (self->user_data != nullptr) { + self->user_data_free_func(self->user_data); + } + self->user_data = nullptr; + G_OBJECT_CLASS(ffs_file_selector_api_parent_class)->dispose(object); +} + +static void ffs_file_selector_api_init(FfsFileSelectorApi* self) {} + +static void ffs_file_selector_api_class_init(FfsFileSelectorApiClass* klass) { + G_OBJECT_CLASS(klass)->dispose = ffs_file_selector_api_dispose; +} + +static FfsFileSelectorApi* ffs_file_selector_api_new( + const FfsFileSelectorApiVTable* vtable, gpointer user_data, + GDestroyNotify user_data_free_func) { + FfsFileSelectorApi* self = FFS_FILE_SELECTOR_API( + g_object_new(ffs_file_selector_api_get_type(), nullptr)); + self->vtable = vtable; + self->user_data = user_data; + self->user_data_free_func = user_data_free_func; + return self; +} + +static void ffs_file_selector_api_show_file_chooser_cb( + FlBasicMessageChannel* channel, FlValue* message_, + FlBasicMessageChannelResponseHandle* response_handle, gpointer user_data) { + FfsFileSelectorApi* self = FFS_FILE_SELECTOR_API(user_data); + + if (self->vtable == nullptr || self->vtable->show_file_chooser == nullptr) { + return; + } + + FlValue* value0 = fl_value_get_list_value(message_, 0); + FfsPlatformFileChooserActionType type = + static_cast( + fl_value_get_int(reinterpret_cast( + const_cast(fl_value_get_custom_value(value0))))); + FlValue* value1 = fl_value_get_list_value(message_, 1); + FfsPlatformFileChooserOptions* options = FFS_PLATFORM_FILE_CHOOSER_OPTIONS( + fl_value_get_custom_value_object(value1)); + g_autoptr(FfsFileSelectorApiShowFileChooserResponse) response = + self->vtable->show_file_chooser(type, options, self->user_data); + if (response == nullptr) { + g_warning("No response returned to %s.%s", "FileSelectorApi", + "showFileChooser"); + return; + } + + g_autoptr(GError) error = NULL; + if (!fl_basic_message_channel_respond(channel, response_handle, + response->value, &error)) { + g_warning("Failed to send response to %s.%s: %s", "FileSelectorApi", + "showFileChooser", error->message); + } +} + +void ffs_file_selector_api_set_method_handlers( + FlBinaryMessenger* messenger, const gchar* suffix, + const FfsFileSelectorApiVTable* vtable, gpointer user_data, + GDestroyNotify user_data_free_func) { + g_autofree gchar* dot_suffix = + suffix != nullptr ? g_strdup_printf(".%s", suffix) : g_strdup(""); + g_autoptr(FfsFileSelectorApi) api_data = + ffs_file_selector_api_new(vtable, user_data, user_data_free_func); + + g_autoptr(FfsMessageCodec) codec = ffs_message_codec_new(); + g_autofree gchar* show_file_chooser_channel_name = g_strdup_printf( + "dev.flutter.pigeon.file_selector_linux.FileSelectorApi.showFileChooser%" + "s", + dot_suffix); + g_autoptr(FlBasicMessageChannel) show_file_chooser_channel = + fl_basic_message_channel_new(messenger, show_file_chooser_channel_name, + FL_MESSAGE_CODEC(codec)); + fl_basic_message_channel_set_message_handler( + show_file_chooser_channel, ffs_file_selector_api_show_file_chooser_cb, + g_object_ref(api_data), g_object_unref); +} + +void ffs_file_selector_api_clear_method_handlers(FlBinaryMessenger* messenger, + const gchar* suffix) { + g_autofree gchar* dot_suffix = + suffix != nullptr ? g_strdup_printf(".%s", suffix) : g_strdup(""); + + g_autoptr(FfsMessageCodec) codec = ffs_message_codec_new(); + g_autofree gchar* show_file_chooser_channel_name = g_strdup_printf( + "dev.flutter.pigeon.file_selector_linux.FileSelectorApi.showFileChooser%" + "s", + dot_suffix); + g_autoptr(FlBasicMessageChannel) show_file_chooser_channel = + fl_basic_message_channel_new(messenger, show_file_chooser_channel_name, + FL_MESSAGE_CODEC(codec)); + fl_basic_message_channel_set_message_handler(show_file_chooser_channel, + nullptr, nullptr, nullptr); +} diff --git a/packages/file_selector/file_selector_linux/linux/messages.g.h b/packages/file_selector/file_selector_linux/linux/messages.g.h new file mode 100644 index 000000000000..2f6251987132 --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/messages.g.h @@ -0,0 +1,238 @@ +// 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. +// Autogenerated from Pigeon (v22.4.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_MESSAGES_G_H_ +#define PIGEON_MESSAGES_G_H_ + +#include + +G_BEGIN_DECLS + +/** + * FfsPlatformFileChooserActionType: + * FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_OPEN: + * FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_CHOOSE_DIRECTORY: + * FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_SAVE: + * + * A Pigeon representation of the GTK_FILE_CHOOSER_ACTION_* options. + */ +typedef enum { + FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_OPEN = 0, + FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_CHOOSE_DIRECTORY = 1, + FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_SAVE = 2 +} FfsPlatformFileChooserActionType; + +/** + * FfsPlatformTypeGroup: + * + * A Pigeon representation of the Linux portion of an `XTypeGroup`. + */ + +G_DECLARE_FINAL_TYPE(FfsPlatformTypeGroup, ffs_platform_type_group, FFS, + PLATFORM_TYPE_GROUP, GObject) + +/** + * ffs_platform_type_group_new: + * label: field in this object. + * extensions: field in this object. + * mime_types: field in this object. + * + * Creates a new #PlatformTypeGroup object. + * + * Returns: a new #FfsPlatformTypeGroup + */ +FfsPlatformTypeGroup* ffs_platform_type_group_new(const gchar* label, + FlValue* extensions, + FlValue* mime_types); + +/** + * ffs_platform_type_group_get_label + * @object: a #FfsPlatformTypeGroup. + * + * Gets the value of the label field of @object. + * + * Returns: the field value. + */ +const gchar* ffs_platform_type_group_get_label(FfsPlatformTypeGroup* object); + +/** + * ffs_platform_type_group_get_extensions + * @object: a #FfsPlatformTypeGroup. + * + * Gets the value of the extensions field of @object. + * + * Returns: the field value. + */ +FlValue* ffs_platform_type_group_get_extensions(FfsPlatformTypeGroup* object); + +/** + * ffs_platform_type_group_get_mime_types + * @object: a #FfsPlatformTypeGroup. + * + * Gets the value of the mimeTypes field of @object. + * + * Returns: the field value. + */ +FlValue* ffs_platform_type_group_get_mime_types(FfsPlatformTypeGroup* object); + +/** + * FfsPlatformFileChooserOptions: + * + * Options for GKT file chooser. + * + * These correspond to gtk_file_chooser_set_* options. + */ + +G_DECLARE_FINAL_TYPE(FfsPlatformFileChooserOptions, + ffs_platform_file_chooser_options, FFS, + PLATFORM_FILE_CHOOSER_OPTIONS, GObject) + +/** + * ffs_platform_file_chooser_options_new: + * allowed_file_types: field in this object. + * current_folder_path: field in this object. + * current_name: field in this object. + * accept_button_label: field in this object. + * select_multiple: field in this object. + * + * Creates a new #PlatformFileChooserOptions object. + * + * Returns: a new #FfsPlatformFileChooserOptions + */ +FfsPlatformFileChooserOptions* ffs_platform_file_chooser_options_new( + FlValue* allowed_file_types, const gchar* current_folder_path, + const gchar* current_name, const gchar* accept_button_label, + gboolean* select_multiple); + +/** + * ffs_platform_file_chooser_options_get_allowed_file_types + * @object: a #FfsPlatformFileChooserOptions. + * + * Gets the value of the allowedFileTypes field of @object. + * + * Returns: the field value. + */ +FlValue* ffs_platform_file_chooser_options_get_allowed_file_types( + FfsPlatformFileChooserOptions* object); + +/** + * ffs_platform_file_chooser_options_get_current_folder_path + * @object: a #FfsPlatformFileChooserOptions. + * + * Gets the value of the currentFolderPath field of @object. + * + * Returns: the field value. + */ +const gchar* ffs_platform_file_chooser_options_get_current_folder_path( + FfsPlatformFileChooserOptions* object); + +/** + * ffs_platform_file_chooser_options_get_current_name + * @object: a #FfsPlatformFileChooserOptions. + * + * Gets the value of the currentName field of @object. + * + * Returns: the field value. + */ +const gchar* ffs_platform_file_chooser_options_get_current_name( + FfsPlatformFileChooserOptions* object); + +/** + * ffs_platform_file_chooser_options_get_accept_button_label + * @object: a #FfsPlatformFileChooserOptions. + * + * Gets the value of the acceptButtonLabel field of @object. + * + * Returns: the field value. + */ +const gchar* ffs_platform_file_chooser_options_get_accept_button_label( + FfsPlatformFileChooserOptions* object); + +/** + * ffs_platform_file_chooser_options_get_select_multiple + * @object: a #FfsPlatformFileChooserOptions. + * + * Whether to allow multiple file selection. + * + * Nullable because it does not apply to the "save" action. + * + * Returns: the field value. + */ +gboolean* ffs_platform_file_chooser_options_get_select_multiple( + FfsPlatformFileChooserOptions* object); + +G_DECLARE_FINAL_TYPE(FfsFileSelectorApiShowFileChooserResponse, + ffs_file_selector_api_show_file_chooser_response, FFS, + FILE_SELECTOR_API_SHOW_FILE_CHOOSER_RESPONSE, GObject) + +/** + * ffs_file_selector_api_show_file_chooser_response_new: + * + * Creates a new response to FileSelectorApi.showFileChooser. + * + * Returns: a new #FfsFileSelectorApiShowFileChooserResponse + */ +FfsFileSelectorApiShowFileChooserResponse* +ffs_file_selector_api_show_file_chooser_response_new(FlValue* return_value); + +/** + * ffs_file_selector_api_show_file_chooser_response_new_error: + * @code: error code. + * @message: error message. + * @details: (allow-none): error details or %NULL. + * + * Creates a new error response to FileSelectorApi.showFileChooser. + * + * Returns: a new #FfsFileSelectorApiShowFileChooserResponse + */ +FfsFileSelectorApiShowFileChooserResponse* +ffs_file_selector_api_show_file_chooser_response_new_error(const gchar* code, + const gchar* message, + FlValue* details); + +/** + * FfsFileSelectorApiVTable: + * + * Table of functions exposed by FileSelectorApi to be implemented by the API + * provider. + */ +typedef struct { + FfsFileSelectorApiShowFileChooserResponse* (*show_file_chooser)( + FfsPlatformFileChooserActionType type, + FfsPlatformFileChooserOptions* options, gpointer user_data); +} FfsFileSelectorApiVTable; + +/** + * ffs_file_selector_api_set_method_handlers: + * + * @messenger: an #FlBinaryMessenger. + * @suffix: (allow-none): a suffix to add to the API or %NULL for none. + * @vtable: implementations of the methods in this API. + * @user_data: (closure): user data to pass to the functions in @vtable. + * @user_data_free_func: (allow-none): a function which gets called to free + * @user_data, or %NULL. + * + * Connects the method handlers in the FileSelectorApi API. + */ +void ffs_file_selector_api_set_method_handlers( + FlBinaryMessenger* messenger, const gchar* suffix, + const FfsFileSelectorApiVTable* vtable, gpointer user_data, + GDestroyNotify user_data_free_func); + +/** + * ffs_file_selector_api_clear_method_handlers: + * + * @messenger: an #FlBinaryMessenger. + * @suffix: (allow-none): a suffix to add to the API or %NULL for none. + * + * Clears the method handlers in the FileSelectorApi API. + */ +void ffs_file_selector_api_clear_method_handlers(FlBinaryMessenger* messenger, + const gchar* suffix); + +G_END_DECLS + +#endif // PIGEON_MESSAGES_G_H_ diff --git a/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc index 8762b4a5f9f6..bbd285a7ca12 100644 --- a/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc +++ b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc @@ -9,6 +9,7 @@ #include #include "file_selector_plugin_private.h" +#include "messages.g.h" // TODO(stuartmorgan): Restructure the helper to take a callback for showing // the dialog, so that the tests can mock out that callback with something @@ -18,11 +19,20 @@ // gtk_file_chooser_native_new to allow for testing values that are given as // construction paramaters and can't be queried later. +// TODO(stuartmorgan): Remove this once +// https://github.com/flutter/flutter/issues/156100 is fixed. For now, this may +// need to be updated to make unit tests pass again any time the +// Pigeon-generated files are updated. +static const int platform_type_group_object_id = 130; + TEST(FileSelectorPlugin, TestOpenSimple) { - g_autoptr(FlValue) args = fl_value_new_map(); + g_autoptr(FfsPlatformFileChooserOptions) options = + ffs_platform_file_chooser_options_new(nullptr, nullptr, nullptr, nullptr, + nullptr); - g_autoptr(GtkFileChooserNative) dialog = - create_dialog_for_method(nullptr, "openFile", args); + g_autoptr(GtkFileChooserNative) dialog = create_dialog_of_type( + nullptr, FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_OPEN, + options); ASSERT_NE(dialog, nullptr); EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), @@ -32,11 +42,14 @@ TEST(FileSelectorPlugin, TestOpenSimple) { } TEST(FileSelectorPlugin, TestOpenMultiple) { - g_autoptr(FlValue) args = fl_value_new_map(); - fl_value_set_string_take(args, "multiple", fl_value_new_bool(true)); + gboolean select_multiple = true; + g_autoptr(FfsPlatformFileChooserOptions) options = + ffs_platform_file_chooser_options_new(nullptr, nullptr, nullptr, nullptr, + &select_multiple); - g_autoptr(GtkFileChooserNative) dialog = - create_dialog_for_method(nullptr, "openFile", args); + g_autoptr(GtkFileChooserNative) dialog = create_dialog_of_type( + nullptr, FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_OPEN, + options); ASSERT_NE(dialog, nullptr); EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), @@ -49,42 +62,54 @@ TEST(FileSelectorPlugin, TestOpenWithFilter) { g_autoptr(FlValue) type_groups = fl_value_new_list(); { + g_autoptr(FlValue) text_group_extensions = fl_value_new_list(); + g_autoptr(FlValue) text_group_mime_types = fl_value_new_list(); fl_value_append_take(text_group_mime_types, fl_value_new_string("text/plain")); - g_autoptr(FlValue) text_group = fl_value_new_map(); - fl_value_set_string_take(text_group, "label", fl_value_new_string("Text")); - fl_value_set_string(text_group, "mimeTypes", text_group_mime_types); - fl_value_append(type_groups, text_group); + + g_autoptr(FfsPlatformTypeGroup) text_group = ffs_platform_type_group_new( + "Text", text_group_extensions, text_group_mime_types); + fl_value_append_take( + type_groups, fl_value_new_custom_object(platform_type_group_object_id, + G_OBJECT(text_group))); } { g_autoptr(FlValue) image_group_extensions = fl_value_new_list(); fl_value_append_take(image_group_extensions, fl_value_new_string("*.png")); fl_value_append_take(image_group_extensions, fl_value_new_string("*.gif")); - fl_value_append_take(image_group_extensions, - fl_value_new_string("*.jgpeg")); - g_autoptr(FlValue) image_group = fl_value_new_map(); - fl_value_set_string_take(image_group, "label", - fl_value_new_string("Images")); - fl_value_set_string(image_group, "extensions", image_group_extensions); - fl_value_append(type_groups, image_group); + fl_value_append_take(image_group_extensions, fl_value_new_string("*.jpeg")); + + g_autoptr(FlValue) image_group_mime_types = fl_value_new_list(); + + g_autoptr(FfsPlatformTypeGroup) image_group = ffs_platform_type_group_new( + "Images", image_group_extensions, image_group_mime_types); + fl_value_append_take( + type_groups, fl_value_new_custom_object(platform_type_group_object_id, + G_OBJECT(image_group))); } { g_autoptr(FlValue) any_group_extensions = fl_value_new_list(); fl_value_append_take(any_group_extensions, fl_value_new_string("*")); - g_autoptr(FlValue) any_group = fl_value_new_map(); - fl_value_set_string_take(any_group, "label", fl_value_new_string("Any")); - fl_value_set_string(any_group, "extensions", any_group_extensions); - fl_value_append(type_groups, any_group); + + g_autoptr(FlValue) any_group_mime_types = fl_value_new_list(); + + g_autoptr(FfsPlatformTypeGroup) any_group = ffs_platform_type_group_new( + "Any", any_group_extensions, any_group_mime_types); + fl_value_append_take( + type_groups, fl_value_new_custom_object(platform_type_group_object_id, + G_OBJECT(any_group))); } - g_autoptr(FlValue) args = fl_value_new_map(); - fl_value_set_string(args, "acceptedTypeGroups", type_groups); + g_autoptr(FfsPlatformFileChooserOptions) options = + ffs_platform_file_chooser_options_new(type_groups, nullptr, nullptr, + nullptr, nullptr); - g_autoptr(GtkFileChooserNative) dialog = - create_dialog_for_method(nullptr, "openFile", args); + g_autoptr(GtkFileChooserNative) dialog = create_dialog_of_type( + nullptr, FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_OPEN, + options); ASSERT_NE(dialog, nullptr); EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), @@ -122,10 +147,13 @@ TEST(FileSelectorPlugin, TestOpenWithFilter) { } TEST(FileSelectorPlugin, TestSaveSimple) { - g_autoptr(FlValue) args = fl_value_new_map(); + g_autoptr(FfsPlatformFileChooserOptions) options = + ffs_platform_file_chooser_options_new(nullptr, nullptr, nullptr, nullptr, + nullptr); - g_autoptr(GtkFileChooserNative) dialog = - create_dialog_for_method(nullptr, "getSavePath", args); + g_autoptr(GtkFileChooserNative) dialog = create_dialog_of_type( + nullptr, FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_SAVE, + options); ASSERT_NE(dialog, nullptr); EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), @@ -135,14 +163,13 @@ TEST(FileSelectorPlugin, TestSaveSimple) { } TEST(FileSelectorPlugin, TestSaveWithArguments) { - g_autoptr(FlValue) args = fl_value_new_map(); - fl_value_set_string_take(args, "initialDirectory", - fl_value_new_string("/tmp")); - fl_value_set_string_take(args, "suggestedName", - fl_value_new_string("foo.txt")); + g_autoptr(FfsPlatformFileChooserOptions) options = + ffs_platform_file_chooser_options_new(nullptr, "/tmp", "foo.txt", nullptr, + nullptr); - g_autoptr(GtkFileChooserNative) dialog = - create_dialog_for_method(nullptr, "getSavePath", args); + g_autoptr(GtkFileChooserNative) dialog = create_dialog_of_type( + nullptr, FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_SAVE, + options); ASSERT_NE(dialog, nullptr); EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), @@ -158,10 +185,14 @@ TEST(FileSelectorPlugin, TestSaveWithArguments) { } TEST(FileSelectorPlugin, TestGetDirectory) { - g_autoptr(FlValue) args = fl_value_new_map(); + g_autoptr(FfsPlatformFileChooserOptions) options = + ffs_platform_file_chooser_options_new(nullptr, nullptr, nullptr, nullptr, + nullptr); - g_autoptr(GtkFileChooserNative) dialog = - create_dialog_for_method(nullptr, "getDirectoryPath", args); + g_autoptr(GtkFileChooserNative) dialog = create_dialog_of_type( + nullptr, + FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_CHOOSE_DIRECTORY, + options); ASSERT_NE(dialog, nullptr); EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), @@ -171,11 +202,15 @@ TEST(FileSelectorPlugin, TestGetDirectory) { } TEST(FileSelectorPlugin, TestGetMultipleDirectories) { - g_autoptr(FlValue) args = fl_value_new_map(); - fl_value_set_string_take(args, "multiple", fl_value_new_bool(true)); - - g_autoptr(GtkFileChooserNative) dialog = - create_dialog_for_method(nullptr, "getDirectoryPath", args); + gboolean select_multiple = true; + g_autoptr(FfsPlatformFileChooserOptions) options = + ffs_platform_file_chooser_options_new(nullptr, nullptr, nullptr, nullptr, + &select_multiple); + + g_autoptr(GtkFileChooserNative) dialog = create_dialog_of_type( + nullptr, + FILE_SELECTOR_LINUX_PLATFORM_FILE_CHOOSER_ACTION_TYPE_CHOOSE_DIRECTORY, + options); ASSERT_NE(dialog, nullptr); EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), diff --git a/packages/file_selector/file_selector_linux/pigeons/copyright.txt b/packages/file_selector/file_selector_linux/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/file_selector/file_selector_linux/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/file_selector/file_selector_linux/pigeons/messages.dart b/packages/file_selector/file_selector_linux/pigeons/messages.dart new file mode 100644 index 000000000000..f10d67cd515e --- /dev/null +++ b/packages/file_selector/file_selector_linux/pigeons/messages.dart @@ -0,0 +1,62 @@ +// 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. +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + input: 'pigeons/messages.dart', + gobjectHeaderOut: 'linux/messages.g.h', + gobjectSourceOut: 'linux/messages.g.cc', + gobjectOptions: GObjectOptions(module: 'Ffs'), + dartOut: 'lib/src/messages.g.dart', + copyrightHeader: 'pigeons/copyright.txt', +)) + +/// A Pigeon representation of the GTK_FILE_CHOOSER_ACTION_* options. +enum PlatformFileChooserActionType { open, chooseDirectory, save } + +/// A Pigeon representation of the Linux portion of an `XTypeGroup`. +class PlatformTypeGroup { + const PlatformTypeGroup({ + this.label = '', + this.extensions = const [], + this.mimeTypes = const [], + }); + + final String label; + final List extensions; + final List mimeTypes; +} + +/// Options for GKT file chooser. +/// +/// These correspond to gtk_file_chooser_set_* options. +class PlatformFileChooserOptions { + PlatformFileChooserOptions({ + required this.allowedFileTypes, + required this.currentFolderPath, + required this.currentName, + required this.acceptButtonLabel, + this.selectMultiple, + }); + + final List? allowedFileTypes; + final String? currentFolderPath; + final String? currentName; + final String? acceptButtonLabel; + + /// Whether to allow multiple file selection. + /// + /// Nullable because it does not apply to the "save" action. + final bool? selectMultiple; +} + +@HostApi(dartHostTestHandler: 'TestFileSelectorApi') +abstract class FileSelectorApi { + /// Shows an file chooser with the given [type] and [options], returning the + /// list of selected paths. + /// + /// An empty list corresponds to a cancelled selection. + List showFileChooser( + PlatformFileChooserActionType type, PlatformFileChooserOptions options); +} diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml index 06a1fa4c3b3a..8a9d48b9ad0f 100644 --- a/packages/file_selector/file_selector_linux/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_linux description: Liunx implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.2+1 +version: 0.9.3 environment: sdk: ^3.3.0 @@ -25,6 +25,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: ^22.4.1 topics: - files diff --git a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart index 8370a3820a84..dbf4bcf4bc3b 100644 --- a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart +++ b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file_selector_linux/file_selector_linux.dart'; +import 'package:file_selector_linux/src/messages.g.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -10,20 +11,12 @@ import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); + late FakeFileSelectorApi api; late FileSelectorLinux plugin; - late List log; setUp(() { - plugin = FileSelectorLinux(); - log = []; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - plugin.channel, - (MethodCall methodCall) async { - log.add(methodCall); - return null; - }, - ); + api = FakeFileSelectorApi(); + plugin = FileSelectorLinux(api: api); }); test('registers instance', () { @@ -32,6 +25,22 @@ void main() { }); group('openFile', () { + test('passes the core flags correctly', () async { + const String path = '/foo/bar'; + api.result = [path]; + + expect((await plugin.openFile())?.path, path); + + expect(api.passedType, PlatformFileChooserActionType.open); + expect(api.passedOptions?.selectMultiple, false); + }); + + test('handles empty return for cancel', () async { + api.result = []; + + expect(await plugin.openFile(), null); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -47,55 +56,31 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); - expectMethodCall( - log, - 'openFile', - arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }, - ); + expect(api.passedOptions?.allowedFileTypes?[0].label, group.label); + // Extensions should be converted to *. format. + expect(api.passedOptions?.allowedFileTypes?[0].extensions, + ['*.txt']); + expect( + api.passedOptions?.allowedFileTypes?[0].mimeTypes, group.mimeTypes); + expect(api.passedOptions?.allowedFileTypes?[1].label, groupTwo.label); + expect(api.passedOptions?.allowedFileTypes?[1].extensions, + ['*.jpg']); + expect(api.passedOptions?.allowedFileTypes?[1].mimeTypes, + groupTwo.mimeTypes); }); test('passes initialDirectory correctly', () async { - await plugin.openFile(initialDirectory: '/example/directory'); - - expectMethodCall( - log, - 'openFile', - arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': false, - }, - ); + const String path = '/example/directory'; + await plugin.openFile(initialDirectory: path); + + expect(api.passedOptions?.currentFolderPath, path); }); test('passes confirmButtonText correctly', () async { - await plugin.openFile(confirmButtonText: 'Open File'); - - expectMethodCall( - log, - 'openFile', - arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': false, - }, - ); + const String button = 'Open File'; + await plugin.openFile(confirmButtonText: button); + + expect(api.passedOptions?.acceptButtonLabel, button); }); test('throws for a type group that does not support Linux', () async { @@ -116,25 +101,24 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group]); - expectMethodCall( - log, - 'openFile', - arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }, - ); + expect(api.passedOptions?.allowedFileTypes?[0].extensions, ['*']); }); }); group('openFiles', () { + test('passes the core flags correctly', () async { + api.result = ['/foo/bar', 'baz']; + + final List files = await plugin.openFiles(); + + expect(files.length, 2); + expect(files[0].path, api.result[0]); + expect(files[1].path, api.result[1]); + + expect(api.passedType, PlatformFileChooserActionType.open); + expect(api.passedOptions?.selectMultiple, true); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -150,55 +134,31 @@ void main() { await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); - expectMethodCall( - log, - 'openFile', - arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }, - ); + expect(api.passedOptions?.allowedFileTypes?[0].label, group.label); + // Extensions should be converted to *. format. + expect(api.passedOptions?.allowedFileTypes?[0].extensions, + ['*.txt']); + expect( + api.passedOptions?.allowedFileTypes?[0].mimeTypes, group.mimeTypes); + expect(api.passedOptions?.allowedFileTypes?[1].label, groupTwo.label); + expect(api.passedOptions?.allowedFileTypes?[1].extensions, + ['*.jpg']); + expect(api.passedOptions?.allowedFileTypes?[1].mimeTypes, + groupTwo.mimeTypes); }); test('passes initialDirectory correctly', () async { - await plugin.openFiles(initialDirectory: '/example/directory'); - - expectMethodCall( - log, - 'openFile', - arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': true, - }, - ); + const String path = '/example/directory'; + await plugin.openFiles(initialDirectory: path); + + expect(api.passedOptions?.currentFolderPath, path); }); test('passes confirmButtonText correctly', () async { - await plugin.openFiles(confirmButtonText: 'Open File'); - - expectMethodCall( - log, - 'openFile', - arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': true, - }, - ); + const String button = 'Open File'; + await plugin.openFiles(confirmButtonText: button); + + expect(api.passedOptions?.acceptButtonLabel, button); }); test('throws for a type group that does not support Linux', () async { @@ -219,25 +179,20 @@ void main() { await plugin.openFiles(acceptedTypeGroups: [group]); - expectMethodCall( - log, - 'openFile', - arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }, - ); + expect(api.passedOptions?.allowedFileTypes?[0].extensions, ['*']); }); }); group('getSaveLocation', () { + test('passes the core flags correctly', () async { + const String path = '/foo/bar'; + api.result = [path]; + + expect((await plugin.getSaveLocation())?.path, path); + + expect(api.passedType, PlatformFileChooserActionType.save); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -254,58 +209,33 @@ void main() { await plugin .getSaveLocation(acceptedTypeGroups: [group, groupTwo]); - expectMethodCall( - log, - 'getSavePath', - arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }, - ); + expect(api.passedOptions?.allowedFileTypes?[0].label, group.label); + // Extensions should be converted to *. format. + expect(api.passedOptions?.allowedFileTypes?[0].extensions, + ['*.txt']); + expect( + api.passedOptions?.allowedFileTypes?[0].mimeTypes, group.mimeTypes); + expect(api.passedOptions?.allowedFileTypes?[1].label, groupTwo.label); + expect(api.passedOptions?.allowedFileTypes?[1].extensions, + ['*.jpg']); + expect(api.passedOptions?.allowedFileTypes?[1].mimeTypes, + groupTwo.mimeTypes); }); test('passes initialDirectory correctly', () async { + const String path = '/example/directory'; await plugin.getSaveLocation( - options: - const SaveDialogOptions(initialDirectory: '/example/directory')); - - expectMethodCall( - log, - 'getSavePath', - arguments: { - 'initialDirectory': '/example/directory', - 'suggestedName': null, - 'confirmButtonText': null, - }, - ); + options: const SaveDialogOptions(initialDirectory: path)); + + expect(api.passedOptions?.currentFolderPath, path); }); test('passes confirmButtonText correctly', () async { + const String button = 'Open File'; await plugin.getSaveLocation( - options: const SaveDialogOptions(confirmButtonText: 'Open File')); - - expectMethodCall( - log, - 'getSavePath', - arguments: { - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': 'Open File', - }, - ); + options: const SaveDialogOptions(confirmButtonText: button)); + + expect(api.passedOptions?.acceptButtonLabel, button); }); test('throws for a type group that does not support Linux', () async { @@ -326,25 +256,20 @@ void main() { await plugin.getSaveLocation(acceptedTypeGroups: [group]); - expectMethodCall( - log, - 'getSavePath', - arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }, - ); + expect(api.passedOptions?.allowedFileTypes?[0].extensions, ['*']); }); }); group('getSavePath (deprecated)', () { + test('passes the core flags correctly', () async { + const String path = '/foo/bar'; + api.result = [path]; + + expect(await plugin.getSavePath(), path); + + expect(api.passedType, PlatformFileChooserActionType.save); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -361,55 +286,31 @@ void main() { await plugin .getSavePath(acceptedTypeGroups: [group, groupTwo]); - expectMethodCall( - log, - 'getSavePath', - arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }, - ); + expect(api.passedOptions?.allowedFileTypes?[0].label, group.label); + // Extensions should be converted to *. format. + expect(api.passedOptions?.allowedFileTypes?[0].extensions, + ['*.txt']); + expect( + api.passedOptions?.allowedFileTypes?[0].mimeTypes, group.mimeTypes); + expect(api.passedOptions?.allowedFileTypes?[1].label, groupTwo.label); + expect(api.passedOptions?.allowedFileTypes?[1].extensions, + ['*.jpg']); + expect(api.passedOptions?.allowedFileTypes?[1].mimeTypes, + groupTwo.mimeTypes); }); test('passes initialDirectory correctly', () async { - await plugin.getSavePath(initialDirectory: '/example/directory'); - - expectMethodCall( - log, - 'getSavePath', - arguments: { - 'initialDirectory': '/example/directory', - 'suggestedName': null, - 'confirmButtonText': null, - }, - ); + const String path = '/example/directory'; + await plugin.getSavePath(initialDirectory: path); + + expect(api.passedOptions?.currentFolderPath, path); }); test('passes confirmButtonText correctly', () async { - await plugin.getSavePath(confirmButtonText: 'Open File'); - - expectMethodCall( - log, - 'getSavePath', - arguments: { - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': 'Open File', - }, - ); + const String button = 'Open File'; + await plugin.getSavePath(confirmButtonText: button); + + expect(api.passedOptions?.acceptButtonLabel, button); }); test('throws for a type group that does not support Linux', () async { @@ -430,99 +331,87 @@ void main() { await plugin.getSavePath(acceptedTypeGroups: [group]); - expectMethodCall( - log, - 'getSavePath', - arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }, - ); + expect(api.passedOptions?.allowedFileTypes?[0].extensions, ['*']); }); }); group('getDirectoryPath', () { + test('passes the core flags correctly', () async { + const String path = '/foo/bar'; + api.result = [path]; + + expect(await plugin.getDirectoryPath(), path); + + expect(api.passedType, PlatformFileChooserActionType.chooseDirectory); + expect(api.passedOptions?.selectMultiple, false); + }); + test('passes initialDirectory correctly', () async { - await plugin.getDirectoryPath(initialDirectory: '/example/directory'); - - expectMethodCall( - log, - 'getDirectoryPath', - arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - }, - ); + const String path = '/example/directory'; + await plugin.getDirectoryPath(initialDirectory: path); + + expect(api.passedOptions?.currentFolderPath, path); }); + test('passes confirmButtonText correctly', () async { - await plugin.getDirectoryPath(confirmButtonText: 'Select Folder'); - - expectMethodCall( - log, - 'getDirectoryPath', - arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Select Folder', - }, - ); + const String button = 'Select Folder'; + await plugin.getDirectoryPath(confirmButtonText: button); + + expect(api.passedOptions?.acceptButtonLabel, button); }); }); group('getDirectoryPaths', () { + test('passes the core flags correctly', () async { + api.result = ['/foo/bar', 'baz']; + + expect(await plugin.getDirectoryPaths(), api.result); + + expect(api.passedType, PlatformFileChooserActionType.chooseDirectory); + expect(api.passedOptions?.selectMultiple, true); + }); + test('passes initialDirectory correctly', () async { - await plugin.getDirectoryPaths(initialDirectory: '/example/directory'); - - expectMethodCall( - log, - 'getDirectoryPath', - arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': true, - }, - ); + const String path = '/example/directory'; + await plugin.getDirectoryPaths(initialDirectory: path); + + expect(api.passedOptions?.currentFolderPath, path); }); + test('passes confirmButtonText correctly', () async { - await plugin.getDirectoryPaths( - confirmButtonText: 'Select one or mode folders'); - - expectMethodCall( - log, - 'getDirectoryPath', - arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Select one or mode folders', - 'multiple': true, - }, - ); + const String button = 'Select one or mode folders'; + await plugin.getDirectoryPaths(confirmButtonText: button); + + expect(api.passedOptions?.acceptButtonLabel, button); }); + test('passes multiple flag correctly', () async { await plugin.getDirectoryPaths(); - expectMethodCall( - log, - 'getDirectoryPath', - arguments: { - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }, - ); + expect(api.passedOptions?.selectMultiple, true); }); }); } -void expectMethodCall( - List log, - String methodName, { - Map? arguments, -}) { - expect(log, [isMethodCall(methodName, arguments: arguments)]); +/// Fake implementation that stores arguments and provides a canned response. +class FakeFileSelectorApi implements FileSelectorApi { + List result = []; + PlatformFileChooserActionType? passedType; + PlatformFileChooserOptions? passedOptions; + + @override + Future> showFileChooser(PlatformFileChooserActionType type, + PlatformFileChooserOptions options) async { + passedType = type; + passedOptions = options; + return result; + } + + @override + // ignore: non_constant_identifier_names + BinaryMessenger? get pigeonVar_binaryMessenger => null; + + @override + // ignore: non_constant_identifier_names + String get pigeonVar_messageChannelSuffix => ''; }