From 7e23bf28ae46556ca01c231114359805ffd27500 Mon Sep 17 00:00:00 2001 From: hogliux Date: Mon, 20 Nov 2017 10:56:08 +0000 Subject: [PATCH] Added iOS/Android native file chooser support and support for asynchronous invocation of file choosers --- examples/Demo/Source/Demos/DialogsDemo.cpp | 118 +-- .../ProjectSaving/jucer_ProjectExport_Xcode.h | 57 +- .../Source/Utility/Helpers/jucer_PresetIDs.h | 1 + .../native/juce_android_JNIHelpers.h | 4 + .../filebrowser/juce_FileChooser.cpp | 180 ++++- .../filebrowser/juce_FileChooser.h | 101 ++- modules/juce_gui_basics/juce_gui_basics.cpp | 4 +- modules/juce_gui_basics/juce_gui_basics.h | 2 +- .../native/juce_android_FileChooser.cpp | 196 ++++- .../native/juce_android_Windowing.cpp | 14 +- .../native/juce_common_MimeTypes.cpp | 693 +++++++++++++++++ .../native/juce_ios_FileChooser.mm | 237 ++++++ .../native/juce_linux_FileChooser.cpp | 373 +++++---- .../native/juce_mac_FileChooser.mm | 403 +++++----- .../native/juce_win32_FileChooser.cpp | 718 ++++++++++++------ 15 files changed, 2426 insertions(+), 675 deletions(-) create mode 100644 modules/juce_gui_basics/native/juce_common_MimeTypes.cpp create mode 100644 modules/juce_gui_basics/native/juce_ios_FileChooser.mm diff --git a/examples/Demo/Source/Demos/DialogsDemo.cpp b/examples/Demo/Source/Demos/DialogsDemo.cpp index ccc5280173..b0bb5f9830 100644 --- a/examples/Demo/Source/Demos/DialogsDemo.cpp +++ b/examples/Demo/Source/Demos/DialogsDemo.cpp @@ -266,81 +266,94 @@ private: } else if (type >= loadChooser && type <= saveChooser) { - #if JUCE_MODAL_LOOPS_PERMITTED const bool useNativeVersion = nativeButton.getToggleState(); if (type == loadChooser) { - FileChooser fc ("Choose a file to open...", - File::getCurrentWorkingDirectory(), - "*", - useNativeVersion); + fc = new FileChooser ("Choose a file to open...", + File::getCurrentWorkingDirectory(), + "*", + useNativeVersion); - if (fc.browseForMultipleFilesToOpen()) - { - String chosen; - for (int i = 0; i < fc.getResults().size(); ++i) - chosen << fc.getResults().getReference(i).getFullPathName() << "\n"; + fc->launchAsync (FileBrowserComponent::canSelectMultipleItems | FileBrowserComponent::openMode + | FileBrowserComponent::canSelectFiles, + [] (const FileChooser& chooser) + { + String chosen; + auto results = chooser.getURLResults(); - AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, - "File Chooser...", - "You picked: " + chosen); - } + for (auto result : results) + chosen << (result.isLocalFile() ? result.getLocalFile().getFullPathName() : result.toString (false)) << "\n"; + + AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, + "File Chooser...", + "You picked: " + chosen); + }); } else if (type == loadWithPreviewChooser) { - ImagePreviewComponent imagePreview; imagePreview.setSize (200, 200); - FileChooser fc ("Choose an image to open...", - File::getSpecialLocation (File::userPicturesDirectory), - "*.jpg;*.jpeg;*.png;*.gif", - useNativeVersion); + fc = new FileChooser ("Choose an image to open...", + File::getCurrentWorkingDirectory(), + "*.jpg;*.jpeg;*.png;*.gif", + useNativeVersion); - if (fc.browseForMultipleFilesToOpen (&imagePreview)) - { - String chosen; - for (int i = 0; i < fc.getResults().size(); ++i) - chosen << fc.getResults().getReference (i).getFullPathName() << "\n"; + fc->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles + | FileBrowserComponent::canSelectMultipleItems, + [] (const FileChooser& chooser) + { + String chosen; + auto results = chooser.getURLResults(); - AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, - "File Chooser...", - "You picked: " + chosen); - } + for (auto result : results) + chosen << (result.isLocalFile() ? result.getLocalFile().getFullPathName() : result.toString (false)) << "\n"; + + AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, + "File Chooser...", + "You picked: " + chosen); + }, + &imagePreview); } else if (type == saveChooser) { - FileChooser fc ("Choose a file to save...", - File::getCurrentWorkingDirectory(), - "*", - useNativeVersion); + fc = new FileChooser ("Choose a file to save...", + File::getCurrentWorkingDirectory(), + "*", + useNativeVersion); - if (fc.browseForFileToSave (true)) - { - File chosenFile = fc.getResult(); + fc->launchAsync (FileBrowserComponent::saveMode | FileBrowserComponent::canSelectFiles, + [] (const FileChooser& chooser) + { + auto result = chooser.getURLResult(); + auto name = result.isEmpty() ? String() + : (result.isLocalFile() ? result.getLocalFile().getFullPathName() + : result.toString (true)); - AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, - "File Chooser...", - "You picked: " + chosenFile.getFullPathName()); - } + AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, + "File Chooser...", + "You picked: " + name); + }); } else if (type == directoryChooser) { - FileChooser fc ("Choose a directory...", - File::getCurrentWorkingDirectory(), - "*", - useNativeVersion); + fc = new FileChooser ("Choose a directory...", + File::getCurrentWorkingDirectory(), + "*", + useNativeVersion); - if (fc.browseForDirectory()) - { - File chosenDirectory = fc.getResult(); + fc->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories, + [] (const FileChooser& chooser) + { + auto result = chooser.getURLResult(); + auto name = result.isLocalFile() ? result.getLocalFile().getFullPathName() + : result.toString (true); - AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, - "File Chooser...", - "You picked: " + chosenDirectory.getFullPathName()); - } + AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, + "File Chooser...", + "You picked: " + name); + }); } - #endif } } @@ -358,6 +371,9 @@ private: return showWindow (*button, static_cast (i)); } + ImagePreviewComponent imagePreview; + ScopedPointer fc; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DialogsDemo) }; diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h index 6926b5a58f..6950b06e72 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h @@ -132,6 +132,8 @@ public: bool isPushNotificationsEnabled() const { return settings [Ids::iosPushNotifications]; } Value getAppGroupsEnabledValue() { return getSetting (Ids::iosAppGroups); } bool isAppGroupsEnabled() const { return settings [Ids::iosAppGroups]; } + Value getiCloudPermissionsEnabled() { return getSetting (Ids::iCloudPermissions); } + bool isiCloudPermissionsEnabled() const { return settings [Ids::iCloudPermissions]; } Value getIosDevelopmentTeamIDValue() { return getSetting (Ids::iosDevelopmentTeamID); } String getIosDevelopmentTeamIDString() const { return settings [Ids::iosDevelopmentTeamID]; } @@ -252,6 +254,9 @@ public: props.add (new BooleanPropertyComponent (getAppGroupsEnabledValue(), "App groups capability", "Enabled"), "Enable this to grant your app the capability to share resources between apps using the same app group ID."); + + props.add (new BooleanPropertyComponent (getiCloudPermissionsEnabled(), "iCloud Permissions", "Enabled"), + "Enable this to grant your app the capability to use native file load/save browser windows on iOS."); } props.add (new TextPropertyComponent (getPListToMergeValue(), "Custom PList", 8192, true), @@ -845,7 +850,10 @@ public: auto developmentTeamID = owner.getIosDevelopmentTeamIDString(); if (developmentTeamID.isNotEmpty()) + { attributes << "DevelopmentTeam = " << developmentTeamID << "; "; + attributes << "ProvisioningStyle = Automatic; "; + } auto appGroupsEnabled = (owner.iOS && owner.isAppGroupsEnabled() ? 1 : 0); auto inAppPurchasesEnabled = owner.isInAppPurchasesEnabled() ? 1 : 0; @@ -862,6 +870,10 @@ public: attributes << "com.apple.InterAppAudio = { enabled = " << interAppAudioEnabled << "; }; "; attributes << "com.apple.Push = { enabled = " << pushNotificationsEnabled << "; }; "; attributes << "com.apple.Sandbox = { enabled = " << sandboxEnabled << "; }; "; + + if (owner.iOS && owner.isiCloudPermissionsEnabled()) + attributes << "com.apple.iCloud = { enabled = 1; }; "; + attributes << "}; };"; return attributes; @@ -901,7 +913,7 @@ public: //============================================================================== bool shouldAddEntitlements() const { - if (owner.isPushNotificationsEnabled() || owner.isAppGroupsEnabled()) + if (owner.isPushNotificationsEnabled() || owner.isAppGroupsEnabled() || (owner.isiOS() && owner.isiCloudPermissionsEnabled())) return true; if (owner.project.getProjectType().isAudioPlugin() @@ -1063,8 +1075,15 @@ public: s.set ("GCC_VERSION", gccVersion); s.set ("CLANG_LINK_OBJC_RUNTIME", "NO"); - if (! config.codeSignIdentity.isUsingDefault()) - s.set ("CODE_SIGN_IDENTITY", config.codeSignIdentity.get().quoted()); + if (isUsingCodeSigning (config)) + { + s.set (owner.iOS ? "\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\"" : "CODE_SIGN_IDENTITY", + getCodeSignIdentity (config).quoted()); + s.set ("PROVISIONING_PROFILE_SPECIFIER", "\"\""); + } + + if (owner.getIosDevelopmentTeamIDString().isNotEmpty()) + s.set ("DEVELOPMENT_TEAM", owner.getIosDevelopmentTeamIDString()); if (shouldAddEntitlements()) s.set ("CODE_SIGN_ENTITLEMENTS", owner.getEntitlementsFileName().quoted()); @@ -1628,6 +1647,20 @@ public: return deploymentTarget; } + String getCodeSignIdentity (const XcodeBuildConfiguration& config) const + { + if (config.codeSignIdentity.isUsingDefault()) + return owner.iOS ? "iPhone Developer" : "Mac Developer"; + + return config.codeSignIdentity.get(); + } + + bool isUsingCodeSigning (const XcodeBuildConfiguration& config) const + { + return (! config.codeSignIdentity.isUsingDefault()) + || owner.getIosDevelopmentTeamIDString().isNotEmpty(); + } + //============================================================================== const XcodeProjectExporter& owner; @@ -2544,6 +2577,24 @@ private: entitlements.set ("com.apple.security.application-groups", groups); } + if (isiOS() && isiCloudPermissionsEnabled()) + { + entitlements.set ("com.apple.developer.icloud-container-identifiers", + "\n" + " iCloud.$(CFBundleIdentifier)\n" + " "); + + entitlements.set ("com.apple.developer.icloud-services", + "\n" + " CloudDocuments\n" + " "); + + entitlements.set ("com.apple.developer.ubiquity-container-identifiers", + "\n" + " iCloud.$(CFBundleIdentifier)\n" + " "); + } + return entitlements; } diff --git a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h index 8f37a1bc72..de0a47888e 100644 --- a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h +++ b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h @@ -209,6 +209,7 @@ namespace Ids DECLARE_ID (iosBackgroundBle); DECLARE_ID (iosPushNotifications); DECLARE_ID (iosAppGroups); + DECLARE_ID (iCloudPermissions); DECLARE_ID (iosDevelopmentTeamID); DECLARE_ID (iosAppGroupsId); DECLARE_ID (iosAppExtensionDuplicateResourcesFolder); diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index 544e440288..0d74e0c4fa 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -324,6 +324,7 @@ extern AndroidSystem android; METHOD (startIntentSenderForResult, "startIntentSenderForResult", "(Landroid/content/IntentSender;ILandroid/content/Intent;III)V") \ METHOD (moveTaskToBack, "moveTaskToBack", "(Z)Z") \ METHOD (startActivity, "startActivity", "(Landroid/content/Intent;)V") \ + METHOD (startActivityForResult, "startActivityForResult", "(Landroid/content/Intent;I)V") \ METHOD (getContentResolver, "getContentResolver", "()Landroid/content/ContentResolver;") \ DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH); @@ -355,6 +356,9 @@ DECLARE_JNI_CLASS (AndroidBitmapConfig, "android/graphics/Bitmap$Config"); METHOD (getIntExtra, "getIntExtra", "(Ljava/lang/String;I)I") \ METHOD (getStringExtra, "getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;") \ METHOD (putExtras, "putExtras", "(Landroid/os/Bundle;)Landroid/content/Intent;") \ + METHOD (putExtraString, "putExtra", "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;") \ + METHOD (putExtraStrings, "putExtra", "(Ljava/lang/String;[Ljava/lang/String;)Landroid/content/Intent;") \ + METHOD (putExtraParcelable, "putExtra", "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;") \ METHOD (setAction, "setAction", "(Ljava/lang/String;)Landroid/content/Intent;") \ METHOD (setPackage, "setPackage", "(Ljava/lang/String;)Landroid/content/Intent;") \ METHOD (setType, "setType", "(Ljava/lang/String;)Landroid/content/Intent;") \ diff --git a/modules/juce_gui_basics/filebrowser/juce_FileChooser.cpp b/modules/juce_gui_basics/filebrowser/juce_FileChooser.cpp index 024bafead1..3218e400ca 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FileChooser.cpp +++ b/modules/juce_gui_basics/filebrowser/juce_FileChooser.cpp @@ -27,6 +27,67 @@ namespace juce { +//============================================================================== +class FileChooser::NonNative : public FileChooser::Pimpl +{ +public: + NonNative (FileChooser& fileChooser, int flags, FilePreviewComponent* preview) + : owner (fileChooser), + selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0), + selectsFiles ((flags & FileBrowserComponent::canSelectFiles) != 0), + warnAboutOverwrite ((flags & FileBrowserComponent::warnAboutOverwriting) != 0), + + filter (selectsFiles ? owner.filters : String(), selectsDirectories ? "*" : String(), {}), + browserComponent (flags, owner.startingFile, &filter, preview), + dialogBox (owner.title, {}, browserComponent, warnAboutOverwrite, browserComponent.findColour (AlertWindow::backgroundColourId)) + {} + + ~NonNative() + { + dialogBox.exitModalState (0); + } + + void launch() override + { + dialogBox.centreWithDefaultSize (nullptr); + dialogBox.enterModalState (true, ModalCallbackFunction::create ([this] (int r) { modalStateFinished (r); }), true); + } + + void runModally() override + { + #if JUCE_MODAL_LOOPS_PERMITTED + modalStateFinished (dialogBox.show() ? 1 : 0); + #else + jassertfalse; + #endif + } + +private: + void modalStateFinished (int returnValue) + { + Array result; + + if (returnValue != 0) + { + for (int i = 0; i < browserComponent.getNumSelectedFiles(); ++i) + result.add (URL (browserComponent.getSelectedFile (i))); + } + + owner.finished (result); + } + + //============================================================================== + FileChooser& owner; + bool selectsDirectories, selectsFiles, warnAboutOverwrite; + + WildcardFileFilter filter; + FileBrowserComponent browserComponent; + FileChooserDialogBox dialogBox; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NonNative) +}; + +//============================================================================== FileChooser::FileChooser (const String& chooserBoxTitle, const File& currentFileOrDirectory, const String& fileFilters, @@ -38,11 +99,18 @@ FileChooser::FileChooser (const String& chooserBoxTitle, useNativeDialogBox (useNativeBox && isPlatformDialogAvailable()), treatFilePackagesAsDirs (treatFilePackagesAsDirectories) { + #ifndef JUCE_MAC + ignoreUnused (treatFilePackagesAsDirs); + #endif + if (! fileFilters.containsNonWhitespaceChars()) filters = "*"; } -FileChooser::~FileChooser() {} +FileChooser::~FileChooser() +{ + asyncCallback = nullptr; +} #if JUCE_MODAL_LOOPS_PERMITTED bool FileChooser::browseForFileToOpen (FilePreviewComponent* previewComp) @@ -88,60 +156,91 @@ bool FileChooser::showDialog (const int flags, FilePreviewComponent* const previ { FocusRestorer focusRestorer; + pimpl = createPimpl (flags, previewComp); + pimpl->runModally(); + + // ensure that the finished function was invoked + jassert (pimpl == nullptr); + + return (results.size() > 0); +} +#endif + +void FileChooser::launchAsync (int flags, std::function callback, + FilePreviewComponent* previewComp) +{ + // You must specify a callback when using launchAsync + jassert (callback); + + // you cannot run two file chooser dialog boxes at the same time + jassert (asyncCallback == nullptr); + + asyncCallback = static_cast&&> (callback); + + pimpl = createPimpl (flags, previewComp); + pimpl->launch(); +} + + +FileChooser::Pimpl* FileChooser::createPimpl (int flags, FilePreviewComponent* previewComp) +{ results.clear(); // the preview component needs to be the right size before you pass it in here.. jassert (previewComp == nullptr || (previewComp->getWidth() > 10 && previewComp->getHeight() > 10)); - const bool selectsDirectories = (flags & FileBrowserComponent::canSelectDirectories) != 0; - const bool selectsFiles = (flags & FileBrowserComponent::canSelectFiles) != 0; - const bool isSave = (flags & FileBrowserComponent::saveMode) != 0; - const bool warnAboutOverwrite = (flags & FileBrowserComponent::warnAboutOverwriting) != 0; - const bool selectMultiple = (flags & FileBrowserComponent::canSelectMultipleItems) != 0; + if (pimpl != nullptr) + { + // you cannot run two file chooser dialog boxes at the same time + jassertfalse; + pimpl = nullptr; + } // You've set the flags for both saveMode and openMode! - jassert (! (isSave && (flags & FileBrowserComponent::openMode) != 0)); + jassert (! (((flags & FileBrowserComponent::saveMode) != 0) + && ((flags & FileBrowserComponent::openMode) != 0))); #if JUCE_WINDOWS + const bool selectsFiles = (flags & FileBrowserComponent::canSelectFiles) != 0; + const bool selectsDirectories = (flags & FileBrowserComponent::canSelectDirectories) != 0; + if (useNativeDialogBox && ! (selectsFiles && selectsDirectories)) - #elif JUCE_MAC || JUCE_LINUX - if (useNativeDialogBox) #else - if (false) + if (useNativeDialogBox) #endif { - showPlatformDialog (results, title, startingFile, filters, - selectsDirectories, selectsFiles, isSave, - warnAboutOverwrite, selectMultiple, treatFilePackagesAsDirs, - previewComp); + return showPlatformDialog (*this, flags, previewComp); } else { - ignoreUnused (selectMultiple); - - WildcardFileFilter wildcard (selectsFiles ? filters : String(), - selectsDirectories ? "*" : String(), - String()); - - FileBrowserComponent browserComponent (flags, startingFile, &wildcard, previewComp); - - FileChooserDialogBox box (title, String(), - browserComponent, warnAboutOverwrite, - browserComponent.findColour (AlertWindow::backgroundColourId)); - - if (box.show()) - { - for (int i = 0; i < browserComponent.getNumSelectedFiles(); ++i) - results.add (browserComponent.getSelectedFile (i)); - } + return new NonNative (*this, flags, previewComp); } - - return results.size() > 0; } -#endif + +Array FileChooser::getResults() const noexcept +{ + Array files; + + for (auto url : getURLResults()) + if (url.isLocalFile()) + files.add (url.getLocalFile()); + + return files; +} File FileChooser::getResult() const +{ + auto fileResults = getResults(); + + // if you've used a multiple-file select, you should use the getResults() method + // to retrieve all the files that were chosen. + jassert (fileResults.size() <= 1); + + return fileResults.getFirst(); +} + +URL FileChooser::getURLResult() const { // if you've used a multiple-file select, you should use the getResults() method // to retrieve all the files that were chosen. @@ -150,6 +249,19 @@ File FileChooser::getResult() const return results.getFirst(); } +void FileChooser::finished (const Array& asyncResults) +{ + std::function callback; + std::swap (callback, asyncCallback); + + results = asyncResults; + + pimpl = nullptr; + + if (callback) + callback (*this); +} + //============================================================================== FilePreviewComponent::FilePreviewComponent() {} FilePreviewComponent::~FilePreviewComponent() {} diff --git a/modules/juce_gui_basics/filebrowser/juce_FileChooser.h b/modules/juce_gui_basics/filebrowser/juce_FileChooser.h index 4584acc659..9bf7d168e3 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FileChooser.h +++ b/modules/juce_gui_basics/filebrowser/juce_FileChooser.h @@ -159,6 +159,25 @@ public: */ bool showDialog (int flags, FilePreviewComponent* previewComponent); + /** Use this method to launch the file browser window asynchronously. + + This will create a file browser dialog based on the settings in this + structure and will launch it modally, returning immediately. + + You must specify a callback which is called when the file browser is + canceled or a file is selected. To abort the file selection, simply + delete the FileChooser object. + + You can use the ModalCallbackFunction::create method to wrap a lambda + into a modal Callback object. + + You must ensure that the lifetime of the callback object is longer than + the lifetime of the file-chooser. + */ + void launchAsync (int flags, + std::function, + FilePreviewComponent* previewComponent = nullptr); + //============================================================================== /** Returns the last file that was chosen by one of the browseFor methods. @@ -168,36 +187,100 @@ public: Note that the file returned is only valid if the browse method returned true (i.e. if the user pressed 'ok' rather than cancelling). + On mobile platforms, the file browser may return a URL instead of a local file. + Therefore, om mobile platforms, you should call getURLResult() instead. + If you're using a multiple-file select, then use the getResults() method instead, to obtain the list of all files chosen. - @see getResults + @see getURLResult, getResults */ File getResult() const; /** Returns a list of all the files that were chosen during the last call to a browse method. + On mobile platforms, the file browser may return a URL instead of a local file. + Therefore, om mobile platforms, you should call getURLResults() instead. + This array may be empty if no files were chosen, or can contain multiple entries if multiple files were chosen. - @see getResult + @see getURLResults, getResult */ - const Array& getResults() const noexcept { return results; } + Array getResults() const noexcept; + + //============================================================================== + /** Returns the last document that was chosen by one of the browseFor methods. + + Use this method if you are using the FileChooser on a mobile platform which + may return a URL to a remote document. If a local file is chosen then you can + convert this file to a JUCE File class via the URL::getLocalFile method. + + @see getResult, URL::getLocalFile + */ + URL getURLResult() const; + + /** Returns a list of all the files that were chosen during the last call to a + browse method. + + Use this method if you are using the FileChooser on a mobile platform which + may return a URL to a remote document. If a local file is chosen then you can + convert this file to a JUCE File class via the URL::getLocalFile method. + + This array may be empty if no files were chosen, or can contain multiple entries + if multiple files were chosen. + + @see getResults, URL::getLocalFile + */ + const Array& getURLResults() const noexcept { return results; } + + //============================================================================== + /** Returns if a native filechooser is currently available on this platform. + + Note: On iOS this will only return true if you have iCloud permissions + and code-signing enabled in the Projucer and have added iCloud containers + to your app in Apple's online developer portal. Additionally, the user must + have installed the iCloud app on their device and used the app at leat once. + */ + static bool isPlatformDialogAvailable(); + + //============================================================================== + #ifndef DOXYGEN + class Native; + #endif private: //============================================================================== String title, filters; const File startingFile; - Array results; + Array results; const bool useNativeDialogBox; const bool treatFilePackagesAsDirs; + std::function asyncCallback; - static void showPlatformDialog (Array& results, const String& title, const File& file, - const String& filters, bool selectsDirectories, bool selectsFiles, - bool isSave, bool warnAboutOverwritingExistingFiles, bool selectMultipleFiles, - bool treatFilePackagesAsDirs, FilePreviewComponent* previewComponent); - static bool isPlatformDialogAvailable(); + //============================================================================== + void finished (const Array&); + + //============================================================================== + struct Pimpl + { + virtual ~Pimpl() {} + + virtual void launch() = 0; + virtual void runModally() = 0; + }; + + ScopedPointer pimpl; + + //============================================================================== + Pimpl* createPimpl (int, FilePreviewComponent*); + static Pimpl* showPlatformDialog (FileChooser&, int, + FilePreviewComponent*); + + class NonNative; + friend class NonNative; + friend class Native; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileChooser) }; diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index 51e3fe1fb8..11684107d5 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -289,10 +289,12 @@ namespace juce #if JUCE_IOS #include "native/juce_ios_UIViewComponentPeer.mm" #include "native/juce_ios_Windowing.mm" + #include "native/juce_ios_FileChooser.mm" #else #include "native/juce_mac_NSViewComponentPeer.mm" #include "native/juce_mac_Windowing.mm" #include "native/juce_mac_MainMenu.mm" + #include "native/juce_mac_FileChooser.mm" #endif #if JUCE_CLANG @@ -300,7 +302,6 @@ namespace juce #endif #include "native/juce_mac_MouseCursor.mm" - #include "native/juce_mac_FileChooser.mm" #elif JUCE_WINDOWS #include "native/juce_win32_Windowing.cpp" @@ -315,6 +316,7 @@ namespace juce #elif JUCE_ANDROID #include "native/juce_android_Windowing.cpp" + #include "native/juce_common_MimeTypes.cpp" #include "native/juce_android_FileChooser.cpp" #endif diff --git a/modules/juce_gui_basics/juce_gui_basics.h b/modules/juce_gui_basics/juce_gui_basics.h index 79d478a1cc..e839431cad 100644 --- a/modules/juce_gui_basics/juce_gui_basics.h +++ b/modules/juce_gui_basics/juce_gui_basics.h @@ -43,7 +43,7 @@ dependencies: juce_events juce_graphics juce_data_structures OSXFrameworks: Cocoa Carbon QuartzCore - iOSFrameworks: UIKit + iOSFrameworks: UIKit MobileCoreServices linuxPackages: x11 xinerama xext END_JUCE_MODULE_DECLARATION diff --git a/modules/juce_gui_basics/native/juce_android_FileChooser.cpp b/modules/juce_gui_basics/native/juce_android_FileChooser.cpp index 27fb00d238..1ff4ec89e6 100644 --- a/modules/juce_gui_basics/native/juce_android_FileChooser.cpp +++ b/modules/juce_gui_basics/native/juce_android_FileChooser.cpp @@ -27,26 +27,198 @@ namespace juce { -void FileChooser::showPlatformDialog (Array& /*results*/, - const String& /*title*/, - const File& /*currentFileOrDirectory*/, - const String& /*filter*/, - bool /*selectsDirectory*/, - bool /*selectsFiles*/, - bool /*isSaveDialogue*/, - bool /*warnAboutOverwritingExistingFiles*/, - bool /*selectMultipleFiles*/, - bool /*treatFilePackagesAsDirs*/, - FilePreviewComponent* /*extraInfoComponent*/) +class FileChooser::Native : public FileChooser::Pimpl { - // TODO +public: + Native (FileChooser& fileChooser, int flags) : owner (fileChooser) + { + if (currentFileChooser == nullptr) + { + currentFileChooser = this; + auto* env = getEnv(); + + auto sdkVersion = env->CallStaticIntMethod (JuceAppActivity, JuceAppActivity.getAndroidSDKVersion); + auto saveMode = ((flags & FileBrowserComponent::saveMode) != 0); + auto selectsDirectories = ((flags & FileBrowserComponent::canSelectDirectories) != 0); + + // You cannot have try to save a direcrtory + jassert (! (saveMode && selectsDirectories)); + + if (sdkVersion < 19) + { + // native save dialogs are only supported in Android versions > 19 + jassert (! saveMode); + saveMode = false; + } + + if (sdkVersion < 21) + { + // native directory chooser dialogs are only supported in Android versions > 21 + jassert (! selectsDirectories); + selectsDirectories = false; + } + + const char* action = (selectsDirectories ? "android.intent.action.OPEN_DOCUMENT_TREE" + : (saveMode ? "android.intent.action.CREATE_DOCUMENT" + : (sdkVersion >= 19 ? "android.intent.action.OPEN_DOCUMENT" + : "android.intent.action.GET_CONTENT"))); + intent = GlobalRef (env->NewObject (AndroidIntent, AndroidIntent.constructWithString, javaString (action).get())); + + if (owner.startingFile != File()) + { + if (saveMode && (! owner.startingFile.isDirectory())) + env->CallObjectMethod (intent.get(), AndroidIntent.putExtraString, + javaString ("android.intent.extra.TITLE").get(), + javaString (owner.startingFile.getFileName()).get()); + + + URL url (owner.startingFile); + LocalRef uri (env->CallStaticObjectMethod (Uri, Uri.parse, javaString (url.toString (true)).get())); + + if (uri) + env->CallObjectMethod (intent.get(), AndroidIntent.putExtraParcelable, + javaString ("android.provider.extra.INITIAL_URI").get(), + uri.get()); + } + + + if (! selectsDirectories) + { + env->CallObjectMethod (intent.get(), AndroidIntent.addCategory, javaString ("android.intent.category.OPENABLE").get()); + + auto mimeTypes = convertFiltersToMimeTypes (owner.filters); + + if (mimeTypes.size() == 1) + { + env->CallObjectMethod (intent.get(), AndroidIntent.setType, javaString (mimeTypes[0]).get()); + } + else + { + String mimeGroup = "*"; + + if (mimeTypes.size() > 0) + { + mimeGroup = mimeTypes[0].upToFirstOccurrenceOf ("/", false, false); + auto allMimeTypesHaveSameGroup = true; + + LocalRef jMimeTypes (env->NewObjectArray (mimeTypes.size(), JavaString, javaString("").get())); + + for (int i = 0; i < mimeTypes.size(); ++i) + { + env->SetObjectArrayElement (jMimeTypes.get(), i, javaString (mimeTypes[i]).get()); + + if (mimeGroup != mimeTypes[0].upToFirstOccurrenceOf ("/", false, false)) + allMimeTypesHaveSameGroup = false; + } + + env->CallObjectMethod (intent.get(), AndroidIntent.putExtraStrings, + javaString ("android.intent.extra.MIME_TYPES").get(), + jMimeTypes.get()); + + if (! allMimeTypesHaveSameGroup) + mimeGroup = "*"; + } + + env->CallObjectMethod (intent.get(), AndroidIntent.setType, javaString (mimeGroup + "/*").get()); + } + } + } + else + jassertfalse; // there can only be a single file chooser + } + + ~Native() + { + currentFileChooser = nullptr; + } + + void runModally() override + { + // Android does not support modal file choosers + jassertfalse; + } + + void launch() override + { + if (currentFileChooser != nullptr) + android.activity.callVoidMethod (JuceAppActivity.startActivityForResult, intent.get(), /*READ_REQUEST_CODE*/ 42); + else + jassertfalse; // There is already a file chooser running + } + + void completed (int resultCode, jobject intentData) + { + currentFileChooser = nullptr; + auto* env = getEnv(); + + Array chosenURLs; + + if (resultCode == /*Activity.RESULT_OK*/ -1 && intentData != nullptr) + { + LocalRef uri (env->CallObjectMethod (intentData, AndroidIntent.getData)); + + if (uri != nullptr) + { + auto jStr = (jstring) env->CallObjectMethod (uri, JavaObject.toString); + + if (jStr != nullptr) + chosenURLs.add (URL (juceString (env, jStr))); + } + } + + owner.finished (chosenURLs); + } + + static Native* currentFileChooser; + + static StringArray convertFiltersToMimeTypes (const String& fileFilters) + { + StringArray result; + auto wildcards = StringArray::fromTokens (fileFilters, ";", ""); + + for (auto wildcard : wildcards) + { + if (wildcard.upToLastOccurrenceOf (".", false, false) == "*") + { + auto extension = wildcard.fromLastOccurrenceOf (".", false, false); + + result.addArray (getMimeTypesForFileExtension (extension)); + } + } + + result.removeDuplicates (false); + return result; + } + +private: + FileChooser& owner; + GlobalRef intent; +}; + +FileChooser::Native* FileChooser::Native::currentFileChooser = nullptr; + +void juce_fileChooserCompleted (int resultCode, void* intentData) +{ + if (FileChooser::Native::currentFileChooser != nullptr) + FileChooser::Native::currentFileChooser->completed (resultCode, (jobject) intentData); +} + + +FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags, + FilePreviewComponent*) +{ + return new FileChooser::Native (owner, flags); } bool FileChooser::isPlatformDialogAvailable() { + #if JUCE_DISABLE_NATIVE_FILECHOOSERS return false; + #else + return true; + #endif } } // namespace juce diff --git a/modules/juce_gui_basics/native/juce_android_Windowing.cpp b/modules/juce_gui_basics/native/juce_android_Windowing.cpp index 53fbfb4537..49440967de 100644 --- a/modules/juce_gui_basics/native/juce_android_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_android_Windowing.cpp @@ -44,6 +44,10 @@ namespace juce extern void juce_inAppPurchaseCompleted (void*); #endif +#if ! JUCE_DISABLE_NATIVE_FILECHOOSERS + extern void juce_fileChooserCompleted (int, void*); +#endif + //============================================================================== JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, launchApp, void, (JNIEnv* env, jobject activity, jstring appFile, jstring appDataDir)) @@ -96,16 +100,20 @@ JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, quitApp, void, (JNIEnv* env, android.shutdown (env); } -JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, appActivityResult, void, (JNIEnv* env, jobject, jint requestCode, jint /*resultCode*/, jobject intentData)) +JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, appActivityResult, void, (JNIEnv* env, jobject, jint requestCode, jint resultCode, jobject intentData)) { setEnv (env); #if JUCE_IN_APP_PURCHASES && JUCE_MODULE_AVAILABLE_juce_product_unlocking if (requestCode == 1001) juce_inAppPurchaseCompleted (intentData); - #else - ignoreUnused (intentData, requestCode); #endif + + #if ! JUCE_DISABLE_NATIVE_FILECHOOSERS + if (requestCode == /*READ_REQUEST_CODE*/42) + juce_fileChooserCompleted (resultCode, intentData); + #endif + ignoreUnused (intentData, requestCode); } JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, appNewIntent, void, (JNIEnv* env, jobject, jobject intentData)) diff --git a/modules/juce_gui_basics/native/juce_common_MimeTypes.cpp b/modules/juce_gui_basics/native/juce_common_MimeTypes.cpp new file mode 100644 index 0000000000..ea3df43a67 --- /dev/null +++ b/modules/juce_gui_basics/native/juce_common_MimeTypes.cpp @@ -0,0 +1,693 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +struct MimeTypeTableEntry +{ + const char* fileExtension, *mimeType; + + static MimeTypeTableEntry table[640]; +}; + +static StringArray getMimeTypesForFileExtension (const String& fileExtension) +{ + StringArray result; + + for (auto type : MimeTypeTableEntry::table) + if (fileExtension == type.fileExtension) + result.add (type.mimeType); + + return result; +} + +//============================================================================== +MimeTypeTableEntry MimeTypeTableEntry::table[640] = +{ + {"3dm", "x-world/x-3dmf"}, + {"3dmf", "x-world/x-3dmf"}, + {"a", "application/octet-stream"}, + {"aab", "application/x-authorware-bin"}, + {"aam", "application/x-authorware-map"}, + {"aas", "application/x-authorware-seg"}, + {"abc", "text/vnd.abc"}, + {"acgi", "text/html"}, + {"afl", "video/animaflex"}, + {"ai", "application/postscript"}, + {"aif", "audio/aiff"}, + {"aif", "audio/x-aiff"}, + {"aifc", "audio/aiff"}, + {"aifc", "audio/x-aiff"}, + {"aiff", "audio/aiff"}, + {"aiff", "audio/x-aiff"}, + {"aim", "application/x-aim"}, + {"aip", "text/x-audiosoft-intra"}, + {"ani", "application/x-navi-animation"}, + {"aos", "application/x-nokia-9000-communicator-add-on-software"}, + {"aps", "application/mime"}, + {"arc", "application/octet-stream"}, + {"arj", "application/arj"}, + {"arj", "application/octet-stream"}, + {"art", "image/x-jg"}, + {"asf", "video/x-ms-asf"}, + {"asm", "text/x-asm"}, + {"asp", "text/asp"}, + {"asx", "application/x-mplayer2"}, + {"asx", "video/x-ms-asf"}, + {"asx", "video/x-ms-asf-plugin"}, + {"au", "audio/basic"}, + {"au", "audio/x-au"}, + {"avi", "application/x-troff-msvideo"}, + {"avi", "video/avi"}, + {"avi", "video/msvideo"}, + {"avi", "video/x-msvideo"}, + {"avs", "video/avs-video"}, + {"bcpio", "application/x-bcpio"}, + {"bin", "application/mac-binary"}, + {"bin", "application/macbinary"}, + {"bin", "application/octet-stream"}, + {"bin", "application/x-binary"}, + {"bin", "application/x-macbinary"}, + {"bm", "image/bmp"}, + {"bmp", "image/bmp"}, + {"bmp", "image/x-windows-bmp"}, + {"boo", "application/book"}, + {"book", "application/book"}, + {"boz", "application/x-bzip2"}, + {"bsh", "application/x-bsh"}, + {"bz", "application/x-bzip"}, + {"bz2", "application/x-bzip2"}, + {"c", "text/plain"}, + {"c", "text/x-c"}, + {"c++", "text/plain"}, + {"cat", "application/vnd.ms-pki.seccat"}, + {"cc", "text/plain"}, + {"cc", "text/x-c"}, + {"ccad", "application/clariscad"}, + {"cco", "application/x-cocoa"}, + {"cdf", "application/cdf"}, + {"cdf", "application/x-cdf"}, + {"cdf", "application/x-netcdf"}, + {"cer", "application/pkix-cert"}, + {"cer", "application/x-x509-ca-cert"}, + {"cha", "application/x-chat"}, + {"chat", "application/x-chat"}, + {"class", "application/java"}, + {"class", "application/java-byte-code"}, + {"class", "application/x-java-class"}, + {"com", "application/octet-stream"}, + {"com", "text/plain"}, + {"conf", "text/plain"}, + {"cpio", "application/x-cpio"}, + {"cpp", "text/x-c"}, + {"cpt", "application/mac-compactpro"}, + {"cpt", "application/x-compactpro"}, + {"cpt", "application/x-cpt"}, + {"crl", "application/pkcs-crl"}, + {"crl", "application/pkix-crl"}, + {"crt", "application/pkix-cert"}, + {"crt", "application/x-x509-ca-cert"}, + {"crt", "application/x-x509-user-cert"}, + {"csh", "application/x-csh"}, + {"csh", "text/x-script.csh"}, + {"css", "application/x-pointplus"}, + {"css", "text/css"}, + {"cxx", "text/plain"}, + {"dcr", "application/x-director"}, + {"deepv", "application/x-deepv"}, + {"def", "text/plain"}, + {"der", "application/x-x509-ca-cert"}, + {"dif", "video/x-dv"}, + {"dir", "application/x-director"}, + {"dl", "video/dl"}, + {"dl", "video/x-dl"}, + {"doc", "application/msword"}, + {"dot", "application/msword"}, + {"dp", "application/commonground"}, + {"drw", "application/drafting"}, + {"dump", "application/octet-stream"}, + {"dv", "video/x-dv"}, + {"dvi", "application/x-dvi"}, + {"dwf", "drawing/x-dwf"}, + {"dwf", "model/vnd.dwf"}, + {"dwg", "application/acad"}, + {"dwg", "image/vnd.dwg"}, + {"dwg", "image/x-dwg"}, + {"dxf", "application/dxf"}, + {"dxf", "image/vnd.dwg"}, + {"dxf", "image/x-dwg"}, + {"dxr", "application/x-director"}, + {"el", "text/x-script.elisp"}, + {"elc", "application/x-bytecode.elisp"}, + {"elc", "application/x-elc"}, + {"env", "application/x-envoy"}, + {"eps", "application/postscript"}, + {"es", "application/x-esrehber"}, + {"etx", "text/x-setext"}, + {"evy", "application/envoy"}, + {"evy", "application/x-envoy"}, + {"exe", "application/octet-stream"}, + {"f", "text/plain"}, + {"f", "text/x-fortran"}, + {"f77", "text/x-fortran"}, + {"f90", "text/plain"}, + {"f90", "text/x-fortran"}, + {"fdf", "application/vnd.fdf"}, + {"fif", "application/fractals"}, + {"fif", "image/fif"}, + {"fli", "video/fli"}, + {"fli", "video/x-fli"}, + {"flo", "image/florian"}, + {"flx", "text/vnd.fmi.flexstor"}, + {"fmf", "video/x-atomic3d-feature"}, + {"for", "text/plain"}, + {"for", "text/x-fortran"}, + {"fpx", "image/vnd.fpx"}, + {"fpx", "image/vnd.net-fpx"}, + {"frl", "application/freeloader"}, + {"funk", "audio/make"}, + {"g", "text/plain"}, + {"g3", "image/g3fax"}, + {"gif", "image/gif"}, + {"gl", "video/gl"}, + {"gl", "video/x-gl"}, + {"gsd", "audio/x-gsm"}, + {"gsm", "audio/x-gsm"}, + {"gsp", "application/x-gsp"}, + {"gss", "application/x-gss"}, + {"gtar", "application/x-gtar"}, + {"gz", "application/x-compressed"}, + {"gz", "application/x-gzip"}, + {"gzip", "application/x-gzip"}, + {"gzip", "multipart/x-gzip"}, + {"h", "text/plain"}, + {"h", "text/x-h"}, + {"hdf", "application/x-hdf"}, + {"help", "application/x-helpfile"}, + {"hgl", "application/vnd.hp-hpgl"}, + {"hh", "text/plain"}, + {"hh", "text/x-h"}, + {"hlb", "text/x-script"}, + {"hlp", "application/hlp"}, + {"hlp", "application/x-helpfile"}, + {"hlp", "application/x-winhelp"}, + {"hpg", "application/vnd.hp-hpgl"}, + {"hpgl", "application/vnd.hp-hpgl"}, + {"hqx", "application/binhex"}, + {"hqx", "application/binhex4"}, + {"hqx", "application/mac-binhex"}, + {"hqx", "application/mac-binhex40"}, + {"hqx", "application/x-binhex40"}, + {"hqx", "application/x-mac-binhex40"}, + {"hta", "application/hta"}, + {"htc", "text/x-component"}, + {"htm", "text/html"}, + {"html", "text/html"}, + {"htmls", "text/html"}, + {"htt", "text/webviewhtml"}, + {"htx", "text/html"}, + {"ice", "x-conference/x-cooltalk"}, + {"ico", "image/x-icon"}, + {"idc", "text/plain"}, + {"ief", "image/ief"}, + {"iefs", "image/ief"}, + {"iges", "application/iges"}, + {"iges", "model/iges"}, + {"igs", "application/iges"}, + {"igs", "model/iges"}, + {"ima", "application/x-ima"}, + {"imap", "application/x-httpd-imap"}, + {"inf", "application/inf"}, + {"ins", "application/x-internett-signup"}, + {"ip", "application/x-ip2"}, + {"isu", "video/x-isvideo"}, + {"it", "audio/it"}, + {"iv", "application/x-inventor"}, + {"ivr", "i-world/i-vrml"}, + {"ivy", "application/x-livescreen"}, + {"jam", "audio/x-jam"}, + {"jav", "text/plain"}, + {"jav", "text/x-java-source"}, + {"java", "text/plain"}, + {"java", "text/x-java-source"}, + {"jcm", "application/x-java-commerce"}, + {"jfif", "image/jpeg"}, + {"jfif", "image/pjpeg"}, + {"jpe", "image/jpeg"}, + {"jpe", "image/pjpeg"}, + {"jpeg", "image/jpeg"}, + {"jpeg", "image/pjpeg"}, + {"jpg", "image/jpeg"}, + {"jpg", "image/pjpeg"}, + {"jps", "image/x-jps"}, + {"js", "application/x-javascript"}, + {"jut", "image/jutvision"}, + {"kar", "audio/midi"}, + {"kar", "music/x-karaoke"}, + {"ksh", "application/x-ksh"}, + {"ksh", "text/x-script.ksh"}, + {"la", "audio/nspaudio"}, + {"la", "audio/x-nspaudio"}, + {"lam", "audio/x-liveaudio"}, + {"latex", "application/x-latex"}, + {"lha", "application/lha"}, + {"lha", "application/octet-stream"}, + {"lha", "application/x-lha"}, + {"lhx", "application/octet-stream"}, + {"list", "text/plain"}, + {"lma", "audio/nspaudio"}, + {"lma", "audio/x-nspaudio"}, + {"log", "text/plain"}, + {"lsp", "application/x-lisp"}, + {"lsp", "text/x-script.lisp"}, + {"lst", "text/plain"}, + {"lsx", "text/x-la-asf"}, + {"ltx", "application/x-latex"}, + {"lzh", "application/octet-stream"}, + {"lzh", "application/x-lzh"}, + {"lzx", "application/lzx"}, + {"lzx", "application/octet-stream"}, + {"lzx", "application/x-lzx"}, + {"m", "text/plain"}, + {"m", "text/x-m"}, + {"m1v", "video/mpeg"}, + {"m2a", "audio/mpeg"}, + {"m2v", "video/mpeg"}, + {"m3u", "audio/x-mpequrl"}, + {"man", "application/x-troff-man"}, + {"map", "application/x-navimap"}, + {"mar", "text/plain"}, + {"mbd", "application/mbedlet"}, + {"mc$", "application/x-magic-cap-package-1.0"}, + {"mcd", "application/mcad"}, + {"mcd", "application/x-mathcad"}, + {"mcf", "image/vasa"}, + {"mcf", "text/mcf"}, + {"mcp", "application/netmc"}, + {"me", "application/x-troff-me"}, + {"mht", "message/rfc822"}, + {"mhtml", "message/rfc822"}, + {"mid", "application/x-midi"}, + {"mid", "audio/midi"}, + {"mid", "audio/x-mid"}, + {"mid", "audio/x-midi"}, + {"mid", "music/crescendo"}, + {"mid", "x-music/x-midi"}, + {"midi", "application/x-midi"}, + {"midi", "audio/midi"}, + {"midi", "audio/x-mid"}, + {"midi", "audio/x-midi"}, + {"midi", "music/crescendo"}, + {"midi", "x-music/x-midi"}, + {"mif", "application/x-frame"}, + {"mif", "application/x-mif"}, + {"mime", "message/rfc822"}, + {"mime", "www/mime"}, + {"mjf", "audio/x-vnd.audioexplosion.mjuicemediafile"}, + {"mjpg", "video/x-motion-jpeg"}, + {"mm", "application/base64"}, + {"mm", "application/x-meme"}, + {"mme", "application/base64"}, + {"mod", "audio/mod"}, + {"mod", "audio/x-mod"}, + {"moov", "video/quicktime"}, + {"mov", "video/quicktime"}, + {"movie", "video/x-sgi-movie"}, + {"mp2", "audio/mpeg"}, + {"mp2", "audio/x-mpeg"}, + {"mp2", "video/mpeg"}, + {"mp2", "video/x-mpeg"}, + {"mp2", "video/x-mpeq2a"}, + {"mp3", "audio/mpeg3"}, + {"mp3", "audio/x-mpeg-3"}, + {"mp3", "video/mpeg"}, + {"mp3", "video/x-mpeg"}, + {"mpa", "audio/mpeg"}, + {"mpa", "video/mpeg"}, + {"mpc", "application/x-project"}, + {"mpe", "video/mpeg"}, + {"mpeg", "video/mpeg"}, + {"mpg", "audio/mpeg"}, + {"mpg", "video/mpeg"}, + {"mpga", "audio/mpeg"}, + {"mpp", "application/vnd.ms-project"}, + {"mpt", "application/x-project"}, + {"mpv", "application/x-project"}, + {"mpx", "application/x-project"}, + {"mrc", "application/marc"}, + {"ms", "application/x-troff-ms"}, + {"mv", "video/x-sgi-movie"}, + {"my", "audio/make"}, + {"mzz", "application/x-vnd.audioexplosion.mzz"}, + {"nap", "image/naplps"}, + {"naplps", "image/naplps"}, + {"nc", "application/x-netcdf"}, + {"ncm", "application/vnd.nokia.configuration-message"}, + {"nif", "image/x-niff"}, + {"niff", "image/x-niff"}, + {"nix", "application/x-mix-transfer"}, + {"nsc", "application/x-conference"}, + {"nvd", "application/x-navidoc"}, + {"o", "application/octet-stream"}, + {"oda", "application/oda"}, + {"omc", "application/x-omc"}, + {"omcd", "application/x-omcdatamaker"}, + {"omcr", "application/x-omcregerator"}, + {"p", "text/x-pascal"}, + {"p10", "application/pkcs10"}, + {"p10", "application/x-pkcs10"}, + {"p12", "application/pkcs-12"}, + {"p12", "application/x-pkcs12"}, + {"p7a", "application/x-pkcs7-signature"}, + {"p7c", "application/pkcs7-mime"}, + {"p7c", "application/x-pkcs7-mime"}, + {"p7m", "application/pkcs7-mime"}, + {"p7m", "application/x-pkcs7-mime"}, + {"p7r", "application/x-pkcs7-certreqresp"}, + {"p7s", "application/pkcs7-signature"}, + {"part", "application/pro_eng"}, + {"pas", "text/pascal"}, + {"pbm", "image/x-portable-bitmap"}, + {"pcl", "application/vnd.hp-pcl"}, + {"pcl", "application/x-pcl"}, + {"pct", "image/x-pict"}, + {"pcx", "image/x-pcx"}, + {"pdb", "chemical/x-pdb"}, + {"pdf", "application/pdf"}, + {"pfunk", "audio/make"}, + {"pfunk", "audio/make.my.funk"}, + {"pgm", "image/x-portable-graymap"}, + {"pgm", "image/x-portable-greymap"}, + {"pic", "image/pict"}, + {"pict", "image/pict"}, + {"pkg", "application/x-newton-compatible-pkg"}, + {"pko", "application/vnd.ms-pki.pko"}, + {"pl", "text/plain"}, + {"pl", "text/x-script.perl"}, + {"plx", "application/x-pixclscript"}, + {"pm", "image/x-xpixmap"}, + {"pm", "text/x-script.perl-module"}, + {"pm4", "application/x-pagemaker"}, + {"pm5", "application/x-pagemaker"}, + {"png", "image/png"}, + {"pnm", "application/x-portable-anymap"}, + {"pnm", "image/x-portable-anymap"}, + {"pot", "application/mspowerpoint"}, + {"pot", "application/vnd.ms-powerpoint"}, + {"pov", "model/x-pov"}, + {"ppa", "application/vnd.ms-powerpoint"}, + {"ppm", "image/x-portable-pixmap"}, + {"pps", "application/mspowerpoint"}, + {"pps", "application/vnd.ms-powerpoint"}, + {"ppt", "application/mspowerpoint"}, + {"ppt", "application/powerpoint"}, + {"ppt", "application/vnd.ms-powerpoint"}, + {"ppt", "application/x-mspowerpoint"}, + {"ppz", "application/mspowerpoint"}, + {"pre", "application/x-freelance"}, + {"prt", "application/pro_eng"}, + {"ps", "application/postscript"}, + {"psd", "application/octet-stream"}, + {"pvu", "paleovu/x-pv"}, + {"pwz", "application/vnd.ms-powerpoint"}, + {"py", "text/x-script.phyton"}, + {"pyc", "applicaiton/x-bytecode.python"}, + {"qcp", "audio/vnd.qcelp"}, + {"qd3", "x-world/x-3dmf"}, + {"qd3d", "x-world/x-3dmf"}, + {"qif", "image/x-quicktime"}, + {"qt", "video/quicktime"}, + {"qtc", "video/x-qtc"}, + {"qti", "image/x-quicktime"}, + {"qtif", "image/x-quicktime"}, + {"ra", "audio/x-pn-realaudio"}, + {"ra", "audio/x-pn-realaudio-plugin"}, + {"ra", "audio/x-realaudio"}, + {"ram", "audio/x-pn-realaudio"}, + {"ras", "application/x-cmu-raster"}, + {"ras", "image/cmu-raster"}, + {"ras", "image/x-cmu-raster"}, + {"rast", "image/cmu-raster"}, + {"rexx", "text/x-script.rexx"}, + {"rf", "image/vnd.rn-realflash"}, + {"rgb", "image/x-rgb"}, + {"rm", "application/vnd.rn-realmedia"}, + {"rm", "audio/x-pn-realaudio"}, + {"rmi", "audio/mid"}, + {"rmm", "audio/x-pn-realaudio"}, + {"rmp", "audio/x-pn-realaudio"}, + {"rmp", "audio/x-pn-realaudio-plugin"}, + {"rng", "application/ringing-tones"}, + {"rng", "application/vnd.nokia.ringing-tone"}, + {"rnx", "application/vnd.rn-realplayer"}, + {"roff", "application/x-troff"}, + {"rp", "image/vnd.rn-realpix"}, + {"rpm", "audio/x-pn-realaudio-plugin"}, + {"rt", "text/richtext"}, + {"rt", "text/vnd.rn-realtext"}, + {"rtf", "application/rtf"}, + {"rtf", "application/x-rtf"}, + {"rtf", "text/richtext"}, + {"rtx", "application/rtf"}, + {"rtx", "text/richtext"}, + {"rv", "video/vnd.rn-realvideo"}, + {"s", "text/x-asm"}, + {"s3m", "audio/s3m"}, + {"saveme", "application/octet-stream"}, + {"sbk", "application/x-tbook"}, + {"scm", "application/x-lotusscreencam"}, + {"scm", "text/x-script.guile"}, + {"scm", "text/x-script.scheme"}, + {"scm", "video/x-scm"}, + {"sdml", "text/plain"}, + {"sdp", "application/sdp"}, + {"sdp", "application/x-sdp"}, + {"sdr", "application/sounder"}, + {"sea", "application/sea"}, + {"sea", "application/x-sea"}, + {"set", "application/set"}, + {"sgm", "text/sgml"}, + {"sgm", "text/x-sgml"}, + {"sgml", "text/sgml"}, + {"sgml", "text/x-sgml"}, + {"sh", "application/x-bsh"}, + {"sh", "application/x-sh"}, + {"sh", "application/x-shar"}, + {"sh", "text/x-script.sh"}, + {"shar", "application/x-bsh"}, + {"shar", "application/x-shar"}, + {"shtml", "text/html"}, + {"shtml", "text/x-server-parsed-html"}, + {"sid", "audio/x-psid"}, + {"sit", "application/x-sit"}, + {"sit", "application/x-stuffit"}, + {"skd", "application/x-koan"}, + {"skm", "application/x-koan"}, + {"skp", "application/x-koan"}, + {"skt", "application/x-koan"}, + {"sl", "application/x-seelogo"}, + {"smi", "application/smil"}, + {"smil", "application/smil"}, + {"snd", "audio/basic"}, + {"snd", "audio/x-adpcm"}, + {"sol", "application/solids"}, + {"spc", "application/x-pkcs7-certificates"}, + {"spc", "text/x-speech"}, + {"spl", "application/futuresplash"}, + {"spr", "application/x-sprite"}, + {"sprite", "application/x-sprite"}, + {"src", "application/x-wais-source"}, + {"ssi", "text/x-server-parsed-html"}, + {"ssm", "application/streamingmedia"}, + {"sst", "application/vnd.ms-pki.certstore"}, + {"step", "application/step"}, + {"stl", "application/sla"}, + {"stl", "application/vnd.ms-pki.stl"}, + {"stl", "application/x-navistyle"}, + {"stp", "application/step"}, + {"sv4cpio,", "application/x-sv4cpio"}, + {"sv4crc", "application/x-sv4crc"}, + {"svf", "image/vnd.dwg"}, + {"svf", "image/x-dwg"}, + {"svr", "application/x-world"}, + {"svr", "x-world/x-svr"}, + {"swf", "application/x-shockwave-flash"}, + {"t", "application/x-troff"}, + {"talk", "text/x-speech"}, + {"tar", "application/x-tar"}, + {"tbk", "application/toolbook"}, + {"tbk", "application/x-tbook"}, + {"tcl", "application/x-tcl"}, + {"tcl", "text/x-script.tcl"}, + {"tcsh", "text/x-script.tcsh"}, + {"tex", "application/x-tex"}, + {"texi", "application/x-texinfo"}, + {"texinfo,", "application/x-texinfo"}, + {"text", "application/plain"}, + {"text", "text/plain"}, + {"tgz", "application/gnutar"}, + {"tgz", "application/x-compressed"}, + {"tif", "image/tiff"}, + {"tif", "image/x-tiff"}, + {"tiff", "image/tiff"}, + {"tiff", "image/x-tiff"}, + {"tr", "application/x-troff"}, + {"tsi", "audio/tsp-audio"}, + {"tsp", "application/dsptype"}, + {"tsp", "audio/tsplayer"}, + {"tsv", "text/tab-separated-values"}, + {"turbot", "image/florian"}, + {"txt", "text/plain"}, + {"uil", "text/x-uil"}, + {"uni", "text/uri-list"}, + {"unis", "text/uri-list"}, + {"unv", "application/i-deas"}, + {"uri", "text/uri-list"}, + {"uris", "text/uri-list"}, + {"ustar", "application/x-ustar"}, + {"ustar", "multipart/x-ustar"}, + {"uu", "application/octet-stream"}, + {"uu", "text/x-uuencode"}, + {"uue", "text/x-uuencode"}, + {"vcd", "application/x-cdlink"}, + {"vcs", "text/x-vcalendar"}, + {"vda", "application/vda"}, + {"vdo", "video/vdo"}, + {"vew", "application/groupwise"}, + {"viv", "video/vivo"}, + {"viv", "video/vnd.vivo"}, + {"vivo", "video/vivo"}, + {"vivo", "video/vnd.vivo"}, + {"vmd", "application/vocaltec-media-desc"}, + {"vmf", "application/vocaltec-media-file"}, + {"voc", "audio/voc"}, + {"voc", "audio/x-voc"}, + {"vos", "video/vosaic"}, + {"vox", "audio/voxware"}, + {"vqe", "audio/x-twinvq-plugin"}, + {"vqf", "audio/x-twinvq"}, + {"vql", "audio/x-twinvq-plugin"}, + {"vrml", "application/x-vrml"}, + {"vrml", "model/vrml"}, + {"vrml", "x-world/x-vrml"}, + {"vrt", "x-world/x-vrt"}, + {"vsd", "application/x-visio"}, + {"vst", "application/x-visio"}, + {"vsw", "application/x-visio"}, + {"w60", "application/wordperfect6.0"}, + {"w61", "application/wordperfect6.1"}, + {"w6w", "application/msword"}, + {"wav", "audio/wav"}, + {"wav", "audio/x-wav"}, + {"wb1", "application/x-qpro"}, + {"wbmp", "image/vnd.wap.wbmp"}, + {"web", "application/vnd.xara"}, + {"wiz", "application/msword"}, + {"wk1", "application/x-123"}, + {"wmf", "windows/metafile"}, + {"wml", "text/vnd.wap.wml"}, + {"wmlc", "application/vnd.wap.wmlc"}, + {"wmls", "text/vnd.wap.wmlscript"}, + {"wmlsc", "application/vnd.wap.wmlscriptc"}, + {"word", "application/msword"}, + {"wp", "application/wordperfect"}, + {"wp5", "application/wordperfect"}, + {"wp5", "application/wordperfect6.0"}, + {"wp6", "application/wordperfect"}, + {"wpd", "application/wordperfect"}, + {"wpd", "application/x-wpwin"}, + {"wq1", "application/x-lotus"}, + {"wri", "application/mswrite"}, + {"wri", "application/x-wri"}, + {"wrl", "application/x-world"}, + {"wrl", "model/vrml"}, + {"wrl", "x-world/x-vrml"}, + {"wrz", "model/vrml"}, + {"wrz", "x-world/x-vrml"}, + {"wsc", "text/scriplet"}, + {"wsrc", "application/x-wais-source"}, + {"wtk", "application/x-wintalk"}, + {"xbm", "image/x-xbitmap"}, + {"xbm", "image/x-xbm"}, + {"xbm", "image/xbm"}, + {"xdr", "video/x-amt-demorun"}, + {"xgz", "xgl/drawing"}, + {"xif", "image/vnd.xiff"}, + {"xl", "application/excel"}, + {"xla", "application/excel"}, + {"xla", "application/x-excel"}, + {"xla", "application/x-msexcel"}, + {"xlb", "application/excel"}, + {"xlb", "application/vnd.ms-excel"}, + {"xlb", "application/x-excel"}, + {"xlc", "application/excel"}, + {"xlc", "application/vnd.ms-excel"}, + {"xlc", "application/x-excel"}, + {"xld", "application/excel"}, + {"xld", "application/x-excel"}, + {"xlk", "application/excel"}, + {"xlk", "application/x-excel"}, + {"xll", "application/excel"}, + {"xll", "application/vnd.ms-excel"}, + {"xll", "application/x-excel"}, + {"xlm", "application/excel"}, + {"xlm", "application/vnd.ms-excel"}, + {"xlm", "application/x-excel"}, + {"xls", "application/excel"}, + {"xls", "application/vnd.ms-excel"}, + {"xls", "application/x-excel"}, + {"xls", "application/x-msexcel"}, + {"xlt", "application/excel"}, + {"xlt", "application/x-excel"}, + {"xlv", "application/excel"}, + {"xlv", "application/x-excel"}, + {"xlw", "application/excel"}, + {"xlw", "application/vnd.ms-excel"}, + {"xlw", "application/x-excel"}, + {"xlw", "application/x-msexcel"}, + {"xm", "audio/xm"}, + {"xml", "application/xml"}, + {"xml", "text/xml"}, + {"xmz", "xgl/movie"}, + {"xpix", "application/x-vnd.ls-xpix"}, + {"xpm", "image/x-xpixmap"}, + {"xpm", "image/xpm"}, + {"x-png", "image/png"}, + {"xsr", "video/x-amt-showrun"}, + {"xwd", "image/x-xwd"}, + {"xwd", "image/x-xwindowdump"}, + {"xyz", "chemical/x-pdb"}, + {"z", "application/x-compress"}, + {"z", "application/x-compressed"}, + {"zip", "application/x-compressed"}, + {"zip", "application/x-zip-compressed"}, + {"zip", "application/zip"}, + {"zip", "multipart/x-zip"}, + {"zoo", "application/octet-stream"} +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_ios_FileChooser.mm b/modules/juce_gui_basics/native/juce_ios_FileChooser.mm new file mode 100644 index 0000000000..2b9d70ca36 --- /dev/null +++ b/modules/juce_gui_basics/native/juce_ios_FileChooser.mm @@ -0,0 +1,237 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +template <> struct ContainerDeletePolicy { static void destroy (NSObject* o) { [o release]; } }; +template <> struct ContainerDeletePolicy> { static void destroy (NSObject* o) { [o release]; } }; + +class FileChooser::Native : private Component, public FileChooser::Pimpl +{ +public: + Native (FileChooser& fileChooser, int flags) + : owner (fileChooser) + { + static FileChooserDelegateClass cls; + delegate = [cls.createInstance() init]; + FileChooserDelegateClass::setOwner (delegate, this); + + auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters)); + + if ((flags & FileBrowserComponent::saveMode) != 0) + { + auto currentFileOrDirectory = owner.startingFile; + + if (! currentFileOrDirectory.existsAsFile()) + { + auto filename = (currentFileOrDirectory.isDirectory() ? "Untitled" : currentFileOrDirectory.getFileName()); + + auto tmpDirectory = File::createTempFile ("iosDummyFiles"); + + if (tmpDirectory.createDirectory().wasOk()) + { + currentFileOrDirectory = tmpDirectory.getChildFile (filename); + currentFileOrDirectory.replaceWithText (""); + } + } + + auto url = [[NSURL alloc] initFileURLWithPath:juceStringToNS (currentFileOrDirectory.getFullPathName())]; + + controller = [[UIDocumentPickerViewController alloc] initWithURL:url + inMode:UIDocumentPickerModeMoveToService]; + [url release]; + } + else + { + controller = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:utTypeArray + inMode:UIDocumentPickerModeOpen]; + } + + [controller setDelegate:delegate]; + [controller setModalTransitionStyle:UIModalTransitionStyleCrossDissolve]; + + setOpaque (false); + + auto chooserBounds = Desktop::getInstance().getDisplays().getMainDisplay().userArea; + setBounds (chooserBounds); + + setAlwaysOnTop (true); + addToDesktop (0); + } + + ~Native() + { + exitModalState (0); + } + + void launch() override + { + enterModalState (true, nullptr, true); + } + + void runModally() override + { + #if JUCE_MODAL_LOOPS_PERMITTED + runModalLoop(); + #endif + } + +private: + //============================================================================== + void parentHierarchyChanged() override + { + auto* newPeer = dynamic_cast (getPeer()); + + if (peer != newPeer) + { + peer = newPeer; + + if (auto* parentController = peer->controller) + [parentController showViewController:controller sender:parentController]; + + if (peer->view.window != nil) + peer->view.window.autoresizesSubviews = YES; + } + } + + //============================================================================== + static StringArray getUTTypesForWildcards (const String& filterWildcards) + { + auto filters = StringArray::fromTokens (filterWildcards, ";", ""); + StringArray result; + + if (! filters.contains ("*") && filters.size() > 0) + { + for (auto filter : filters) + { + // iOS only supports file extension wild cards + jassert (filter.upToLastOccurrenceOf (".", true, false) == "*."); + + auto fileExtension = filter.fromLastOccurrenceOf (".", false, false); + auto fileExtensionCF = fileExtension.toCFString(); + + auto tag = UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension, fileExtensionCF, nullptr); + + if (tag != nullptr) + { + result.add (String::fromCFString (tag)); + CFRelease (tag); + } + + CFRelease (fileExtensionCF); + } + } + else + result.add ("public.data"); + + return result; + } + + //============================================================================== + void didPickDocumentAtURL (NSURL* url) + { + Array chooserResults; + chooserResults.add (URL (nsStringToJuce ([url absoluteString]))); + + owner.finished (chooserResults); + exitModalState (1); + } + + void pickerWasCancelled() + { + Array chooserResults; + + owner.finished (chooserResults); + exitModalState (0); + } + + //============================================================================== + struct FileChooserDelegateClass : public ObjCClass> + { + FileChooserDelegateClass() : ObjCClass> ("FileChooserDelegate_") + { + addIvar ("owner"); + + addMethod (@selector (documentPicker:didPickDocumentAtURL:), didPickDocumentAtURL, "v@:@@"); + addMethod (@selector (documentPickerWasCancelled:), documentPickerWasCancelled, "v@:@"); + + addProtocol (@protocol (UIDocumentPickerDelegate)); + + registerClass(); + } + + static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); } + static Native* getOwner (id self) { return getIvar (self, "owner"); } + + //============================================================================== + static void didPickDocumentAtURL (id self, SEL, UIDocumentPickerViewController*, NSURL* url) + { + auto picker = getOwner (self); + + if (picker != nullptr) + picker->didPickDocumentAtURL (url); + } + + static void documentPickerWasCancelled (id self, SEL, UIDocumentPickerViewController*) + { + auto picker = getOwner (self); + + if (picker != nullptr) + picker->pickerWasCancelled(); + } + }; + + //============================================================================== + FileChooser& owner; + ScopedPointer> delegate; + ScopedPointer controller; + UIViewComponentPeer* peer = nullptr; + + static FileChooserDelegateClass fileChooserDelegateClass; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native) +}; + +//============================================================================== +bool FileChooser::isPlatformDialogAvailable() +{ + #if JUCE_DISABLE_NATIVE_FILECHOOSERS + return false; + #else + return [[NSFileManager defaultManager] ubiquityIdentityToken] != nil; + #endif +} + +FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags, + FilePreviewComponent*) +{ + return new FileChooser::Native (owner, flags); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_linux_FileChooser.cpp b/modules/juce_gui_basics/native/juce_linux_FileChooser.cpp index ceab192946..9155759064 100644 --- a/modules/juce_gui_basics/native/juce_linux_FileChooser.cpp +++ b/modules/juce_gui_basics/native/juce_linux_FileChooser.cpp @@ -29,14 +29,215 @@ namespace juce static bool exeIsAvailable (const char* const executable) { - ChildProcess child; - const bool ok = child.start ("which " + String (executable)) - && child.readAllProcessOutput().trim().isNotEmpty(); + ChildProcess child; + const bool ok = child.start ("which " + String (executable)) + && child.readAllProcessOutput().trim().isNotEmpty(); - child.waitForProcessToFinish (60 * 1000); - return ok; + child.waitForProcessToFinish (60 * 1000); + return ok; } + +class FileChooser::Native : public FileChooser::Pimpl, + private Timer +{ +public: + Native (FileChooser& fileChooser, int flags) + : owner (fileChooser), + isDirectory ((flags & FileBrowserComponent::canSelectDirectories) != 0), + isSave ((flags & FileBrowserComponent::saveMode) != 0), + selectMultipleFiles ((flags & FileBrowserComponent::canSelectMultipleItems) != 0) + { + const File previousWorkingDirectory (File::getCurrentWorkingDirectory()); + + // use kdialog for KDE sessions or if zenity is missing + if (exeIsAvailable ("kdialog") && (isKdeFullSession() || ! exeIsAvailable ("zenity"))) + addKDialogArgs(); + else + addZenityArgs(); + } + + ~Native() + { + finish (true); + } + + void runModally() override + { + child.start (args, ChildProcess::wantStdOut); + + while (child.isRunning()) + if (! MessageManager::getInstance()->runDispatchLoopUntil(20)) + break; + + finish (false); + } + + void launch() override + { + child.start (args, ChildProcess::wantStdOut); + startTimer (100); + } + +private: + FileChooser& owner; + bool isDirectory, isSave, selectMultipleFiles; + + ChildProcess child; + StringArray args; + String separator; + + void timerCallback() override + { + if (! child.isRunning()) + { + stopTimer(); + finish (false); + } + } + + void finish (bool shouldKill) + { + String result; + Array selection; + + if (shouldKill) + child.kill(); + else + result = child.readAllProcessOutput().trim(); + + if (result.isNotEmpty()) + { + StringArray tokens; + + if (selectMultipleFiles) + tokens.addTokens (result, separator, "\""); + else + tokens.add (result); + + for (auto& token : tokens) + selection.add (URL (File::getCurrentWorkingDirectory().getChildFile (token))); + } + + if (! shouldKill) + { + child.waitForProcessToFinish (60 * 1000); + owner.finished (selection); + } + } + + static uint64 getTopWindowID() noexcept + { + if (TopLevelWindow* top = TopLevelWindow::getActiveTopLevelWindow()) + return (uint64) (pointer_sized_uint) top->getWindowHandle(); + + return 0; + } + + static bool isKdeFullSession() + { + return SystemStats::getEnvironmentVariable ("KDE_FULL_SESSION", String()) + .equalsIgnoreCase ("true"); + } + + void addKDialogArgs() + { + args.add ("kdialog"); + + if (owner.title.isNotEmpty()) + args.add ("--title=" + owner.title); + + if (uint64 topWindowID = getTopWindowID()) + { + args.add ("--attach"); + args.add (String (topWindowID)); + } + + if (selectMultipleFiles) + { + separator = "\n"; + args.add ("--multiple"); + args.add ("--separate-output"); + args.add ("--getopenfilename"); + } + else + { + if (isSave) args.add ("--getsavefilename"); + else if (isDirectory) args.add ("--getexistingdirectory"); + else args.add ("--getopenfilename"); + } + + File startPath; + + if (owner.startingFile.exists()) + { + startPath = owner.startingFile; + } + else if (owner.startingFile.getParentDirectory().exists()) + { + startPath = owner.startingFile.getParentDirectory(); + } + else + { + startPath = File::getSpecialLocation (File::userHomeDirectory); + + if (isSave) + startPath = startPath.getChildFile (owner.startingFile.getFileName()); + } + + args.add (startPath.getFullPathName()); + args.add (owner.filters.replaceCharacter (';', ' ')); + } + + void addZenityArgs() + { + args.add ("zenity"); + args.add ("--file-selection"); + + if (owner.title.isNotEmpty()) + args.add ("--title=" + owner.title); + + if (selectMultipleFiles) + { + separator = ":"; + args.add ("--multiple"); + args.add ("--separator=" + separator); + } + else + { + if (isDirectory) args.add ("--directory"); + if (isSave) args.add ("--save"); + } + + if (owner.filters.isNotEmpty() && owner.filters != "*" && owner.filters != "*.*") + { + StringArray tokens; + tokens.addTokens (owner.filters, ";,|", "\""); + + for (int i = 0; i < tokens.size(); ++i) + args.add ("--file-filter=" + tokens[i]); + } + + if (owner.startingFile.isDirectory()) + owner.startingFile.setAsCurrentWorkingDirectory(); + else if (owner.startingFile.getParentDirectory().exists()) + owner.startingFile.getParentDirectory().setAsCurrentWorkingDirectory(); + else + File::getSpecialLocation (File::userHomeDirectory).setAsCurrentWorkingDirectory(); + + auto filename = owner.startingFile.getFileName(); + + if (! filename.isEmpty()) + args.add ("--filename=" + filename); + + // supplying the window ID of the topmost window makes sure that Zenity pops up.. + if (uint64 topWindowID = getTopWindowID()) + setenv ("WINDOWID", String (topWindowID).toRawUTF8(), true); + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native) +}; + bool FileChooser::isPlatformDialogAvailable() { #if JUCE_DISABLE_NATIVE_FILECHOOSERS @@ -47,167 +248,9 @@ bool FileChooser::isPlatformDialogAvailable() #endif } -static uint64 getTopWindowID() noexcept +FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags, FilePreviewComponent*) { - if (TopLevelWindow* top = TopLevelWindow::getActiveTopLevelWindow()) - return (uint64) (pointer_sized_uint) top->getWindowHandle(); - - return 0; -} - -static bool isKdeFullSession() -{ - return SystemStats::getEnvironmentVariable ("KDE_FULL_SESSION", String()) - .equalsIgnoreCase ("true"); -} - -static void addKDialogArgs (StringArray& args, String& separator, - const String& title, const File& file, const String& filters, - bool isDirectory, bool isSave, bool selectMultipleFiles) -{ - args.add ("kdialog"); - - if (title.isNotEmpty()) - args.add ("--title=" + title); - - if (uint64 topWindowID = getTopWindowID()) - { - args.add ("--attach"); - args.add (String (topWindowID)); - } - - if (selectMultipleFiles) - { - separator = "\n"; - args.add ("--multiple"); - args.add ("--separate-output"); - args.add ("--getopenfilename"); - } - else - { - if (isSave) args.add ("--getsavefilename"); - else if (isDirectory) args.add ("--getexistingdirectory"); - else args.add ("--getopenfilename"); - } - - File startPath; - - if (file.exists()) - { - startPath = file; - } - else if (file.getParentDirectory().exists()) - { - startPath = file.getParentDirectory(); - } - else - { - startPath = File::getSpecialLocation (File::userHomeDirectory); - - if (isSave) - startPath = startPath.getChildFile (file.getFileName()); - } - - args.add (startPath.getFullPathName()); - args.add (filters.replaceCharacter (';', ' ')); -} - -static void addZenityArgs (StringArray& args, String& separator, - const String& title, const File& file, const String& filters, - bool isDirectory, bool isSave, bool selectMultipleFiles) -{ - args.add ("zenity"); - args.add ("--file-selection"); - - if (title.isNotEmpty()) - args.add ("--title=" + title); - - if (selectMultipleFiles) - { - separator = ":"; - args.add ("--multiple"); - args.add ("--separator=" + separator); - } - else - { - if (isDirectory) args.add ("--directory"); - if (isSave) args.add ("--save"); - } - - if (filters.isNotEmpty() && filters != "*" && filters != "*.*") - { - StringArray tokens; - tokens.addTokens (filters, ";,|", "\""); - - for (int i = 0; i < tokens.size(); ++i) - args.add ("--file-filter=" + tokens[i]); - } - - if (file.isDirectory()) - file.setAsCurrentWorkingDirectory(); - else if (file.getParentDirectory().exists()) - file.getParentDirectory().setAsCurrentWorkingDirectory(); - else - File::getSpecialLocation (File::userHomeDirectory).setAsCurrentWorkingDirectory(); - - if (! file.getFileName().isEmpty()) - args.add ("--filename=" + file.getFileName()); - - // supplying the window ID of the topmost window makes sure that Zenity pops up.. - if (uint64 topWindowID = getTopWindowID()) - setenv ("WINDOWID", String (topWindowID).toRawUTF8(), true); -} - -void FileChooser::showPlatformDialog (Array& results, - const String& title, const File& file, const String& filters, - bool isDirectory, bool /* selectsFiles */, - bool isSave, bool /* warnAboutOverwritingExistingFiles */, - bool /*treatFilePackagesAsDirs*/, - bool selectMultipleFiles, FilePreviewComponent*) -{ - const File previousWorkingDirectory (File::getCurrentWorkingDirectory()); - - StringArray args; - String separator; - - // use kdialog for KDE sessions or if zenity is missing - if (exeIsAvailable ("kdialog") && (isKdeFullSession() || ! exeIsAvailable ("zenity"))) - addKDialogArgs (args, separator, title, file, filters, isDirectory, isSave, selectMultipleFiles); - else - addZenityArgs (args, separator, title, file, filters, isDirectory, isSave, selectMultipleFiles); - - ChildProcess child; - - if (child.start (args, ChildProcess::wantStdOut)) - { - if (MessageManager::getInstance()->isThisTheMessageThread()) - { - while (child.isRunning()) - { - if (! MessageManager::getInstance()->runDispatchLoopUntil(20)) - break; - } - } - - const String result (child.readAllProcessOutput().trim()); - - if (result.isNotEmpty()) - { - StringArray tokens; - - if (selectMultipleFiles) - tokens.addTokens (result, separator, "\""); - else - tokens.add (result); - - for (int i = 0; i < tokens.size(); ++i) - results.add (File::getCurrentWorkingDirectory().getChildFile (tokens[i])); - } - - child.waitForProcessToFinish (60 * 1000); - } - - previousWorkingDirectory.setAsCurrentWorkingDirectory(); + return new Native (owner, flags); } } // namespace juce diff --git a/modules/juce_gui_basics/native/juce_mac_FileChooser.mm b/modules/juce_gui_basics/native/juce_mac_FileChooser.mm index 333850c5d7..55ec816a4b 100644 --- a/modules/juce_gui_basics/native/juce_mac_FileChooser.mm +++ b/modules/juce_gui_basics/native/juce_mac_FileChooser.mm @@ -27,95 +27,7 @@ namespace juce { -#if JUCE_MAC - -struct FileChooserDelegateClass : public ObjCClass -{ - FileChooserDelegateClass() : ObjCClass ("JUCEFileChooser_") - { - addIvar ("filters"); - addIvar ("filePreviewComponent"); - - addMethod (@selector (dealloc), dealloc, "v@:"); - addMethod (@selector (panel:shouldShowFilename:), shouldShowFilename, "c@:@@"); - addMethod (@selector (panelSelectionDidChange:), panelSelectionDidChange, "c@"); - - #if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 - addProtocol (@protocol (NSOpenSavePanelDelegate)); - #endif - - registerClass(); - } - - static void setFilters (id self, StringArray* filters) { object_setInstanceVariable (self, "filters", filters); } - static void setFilePreviewComponent (id self, FilePreviewComponent* comp) { object_setInstanceVariable (self, "filePreviewComponent", comp); } - static StringArray* getFilters (id self) { return getIvar (self, "filters"); } - static FilePreviewComponent* getFilePreviewComponent (id self) { return getIvar (self, "filePreviewComponent"); } - -private: - static void dealloc (id self, SEL) - { - delete getFilters (self); - sendSuperclassMessage (self, @selector (dealloc)); - } - - static BOOL shouldShowFilename (id self, SEL, id /*sender*/, NSString* filename) - { - StringArray* const filters = getFilters (self); - - const File f (nsStringToJuce (filename)); - - for (int i = filters->size(); --i >= 0;) - if (f.getFileName().matchesWildcard ((*filters)[i], true)) - return true; - - #if (! defined (MAC_OS_X_VERSION_10_7)) || MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_7 - NSError* error; - NSString* name = [[NSWorkspace sharedWorkspace] typeOfFile: filename error: &error]; - - if ([name isEqualToString: nsStringLiteral ("com.apple.alias-file")]) - { - FSRef ref; - FSPathMakeRef ((const UInt8*) [filename fileSystemRepresentation], &ref, nullptr); - - Boolean targetIsFolder = false, wasAliased = false; - FSResolveAliasFileWithMountFlags (&ref, true, &targetIsFolder, &wasAliased, 0); - - return wasAliased && targetIsFolder; - } - #endif - - return f.isDirectory() - && ! [[NSWorkspace sharedWorkspace] isFilePackageAtPath: filename]; - } - - static StringArray getSelectedPaths (id sender) - { - StringArray paths; - - if ([sender isKindOfClass: [NSOpenPanel class]]) - { - NSArray* urls = [(NSOpenPanel*) sender URLs]; - - for (NSUInteger i = 0; i < [urls count]; ++i) - paths.add (nsStringToJuce ([[urls objectAtIndex: i] path])); - } - else if ([sender isKindOfClass: [NSSavePanel class]]) - { - paths.add (nsStringToJuce ([[(NSSavePanel*) sender URL] path])); - } - - return paths; - } - - static void panelSelectionDidChange (id self, SEL, id sender) - { - // NB: would need to extend FilePreviewComponent to handle the full list rather than just the first one - if (FilePreviewComponent* const previewComp = getFilePreviewComponent (self)) - previewComp->selectedFileChanged (File (getSelectedPaths (sender)[0])); - } -}; - +//============================================================================== static NSMutableArray* createAllowedTypesArray (const StringArray& filters) { if (filters.size() == 0) @@ -137,97 +49,159 @@ static NSMutableArray* createAllowedTypesArray (const StringArray& filters) } //============================================================================== -void FileChooser::showPlatformDialog (Array& results, - const String& title, - const File& currentFileOrDirectory, - const String& filter, - bool selectsDirectory, - bool selectsFiles, - bool isSaveDialogue, - bool /*warnAboutOverwritingExistingFiles*/, - bool selectMultipleFiles, - bool treatFilePackagesAsDirs, - FilePreviewComponent* extraInfoComponent) +template <> struct ContainerDeletePolicy { static void destroy (NSObject* o) { [o release]; } }; + +class FileChooser::Native : public Component, public FileChooser::Pimpl { - JUCE_AUTORELEASEPOOL +public: + Native (FileChooser& fileChooser, int flags, FilePreviewComponent* previewComponent) + : owner (fileChooser), preview (previewComponent), + selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0), + selectsFiles ((flags & FileBrowserComponent::canSelectFiles) != 0), + isSave ((flags & FileBrowserComponent::saveMode) != 0), + selectMultiple ((flags & FileBrowserComponent::canSelectMultipleItems) != 0), + panel (isSave ? [[NSSavePanel alloc] init] : [[NSOpenPanel alloc] init]) { - ScopedPointer tempMenu; - if (JUCEApplicationBase::isStandaloneApp()) - tempMenu = new TemporaryMainMenuWithStandardCommands(); + setBounds (0, 0, 0, 0); + setOpaque (true); - StringArray* filters = new StringArray(); - filters->addTokens (filter.replaceCharacters (",:", ";;"), ";", String()); - filters->trim(); - filters->removeEmptyStrings(); + static DelegateClass cls; - #if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 - typedef NSObject DelegateType; - #else - typedef NSObject DelegateType; - #endif + delegate = [cls.createInstance() init]; + object_setInstanceVariable (delegate, "cppObject", this); - static FileChooserDelegateClass cls; - DelegateType* delegate = (DelegateType*) [[cls.createInstance() init] autorelease]; - FileChooserDelegateClass::setFilters (delegate, filters); + [panel setDelegate:delegate]; - NSSavePanel* panel = isSaveDialogue ? [NSSavePanel savePanel] - : [NSOpenPanel openPanel]; + filters.addTokens (owner.filters.replaceCharacters (",:", ";;"), ";", String()); + filters.trim(); + filters.removeEmptyStrings(); - [panel setTitle: juceStringToNS (title)]; - [panel setAllowedFileTypes: createAllowedTypesArray (*filters)]; + [panel setTitle: juceStringToNS (owner.title)]; + [panel setAllowedFileTypes: createAllowedTypesArray (filters)]; - if (! isSaveDialogue) + if (! isSave) { NSOpenPanel* openPanel = (NSOpenPanel*) panel; - [openPanel setCanChooseDirectories: selectsDirectory]; + + [openPanel setCanChooseDirectories: selectsDirectories]; [openPanel setCanChooseFiles: selectsFiles]; - [openPanel setAllowsMultipleSelection: selectMultipleFiles]; + [openPanel setAllowsMultipleSelection: selectMultiple]; [openPanel setResolvesAliases: YES]; - if (treatFilePackagesAsDirs) + if (owner.treatFilePackagesAsDirs) [openPanel setTreatsFilePackagesAsDirectories: YES]; } - if (extraInfoComponent != nullptr) + if (preview != nullptr) { - NSView* view = [[[NSView alloc] initWithFrame: makeNSRect (extraInfoComponent->getLocalBounds())] autorelease]; - extraInfoComponent->addToDesktop (0, (void*) view); - extraInfoComponent->setVisible (true); - FileChooserDelegateClass::setFilePreviewComponent (delegate, extraInfoComponent); + nsViewPreview = [[NSView alloc] initWithFrame: makeNSRect (preview->getLocalBounds())]; + preview->addToDesktop (0, (void*) nsViewPreview); + preview->setVisible (true); - [panel setAccessoryView: view]; + [panel setAccessoryView: nsViewPreview]; } - [panel setDelegate: delegate]; - - if (isSaveDialogue || selectsDirectory) + if (isSave || selectsDirectories) [panel setCanCreateDirectories: YES]; - String directory, filename; + [panel setLevel:NSModalPanelWindowLevel]; - if (currentFileOrDirectory.isDirectory()) + if (owner.startingFile.isDirectory()) { - directory = currentFileOrDirectory.getFullPathName(); + startingDirectory = owner.startingFile.getFullPathName(); } else { - directory = currentFileOrDirectory.getParentDirectory().getFullPathName(); - filename = currentFileOrDirectory.getFileName(); + startingDirectory = owner.startingFile.getParentDirectory().getFullPathName(); + filename = owner.startingFile.getFileName(); } #if defined (MAC_OS_X_VERSION_10_6) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6) - [panel setDirectoryURL: createNSURLFromFile (directory)]; + [panel setDirectoryURL: createNSURLFromFile (startingDirectory)]; [panel setNameFieldStringValue: juceStringToNS (filename)]; - - if ([panel runModal] == 1 /*NSModalResponseOK*/) - #else - if ([panel runModalForDirectory: juceStringToNS (directory) - file: juceStringToNS (filename)] == 1 /*NSModalResponseOK*/) #endif + } + + ~Native() + { + exitModalState (0); + removeFromDesktop(); + + if (panel != nil) { - if (isSaveDialogue) + [panel setDelegate:nil]; + + if (nsViewPreview != nil) { - results.add (File (nsStringToJuce ([[panel URL] path]))); + [panel setAccessoryView: nil]; + + [nsViewPreview release]; + + nsViewPreview = nil; + preview = nullptr; + } + + [panel close]; + } + + panel = nullptr; + + if (delegate != nil) + { + [delegate release]; + delegate = nil; + } + } + + void launch() override + { + if (panel != nil) + { + setAlwaysOnTop (juce_areThereAnyAlwaysOnTopWindows()); + addToDesktop (0); + + enterModalState (true); + [panel beginWithCompletionHandler:CreateObjCBlock (this, &Native::finished)]; + } + } + + void runModally() override + { + ScopedPointer tempMenu; + + if (JUCEApplicationBase::isStandaloneApp()) + tempMenu = new TemporaryMainMenuWithStandardCommands(); + + jassert (panel != nil); + #if defined (MAC_OS_X_VERSION_10_6) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6) + auto result = [panel runModal]; + #else + auto result = [panel runModalForDirectory: juceStringToNS (startingDirectory) + file: juceStringToNS (filename)]; + #endif + + finished (result); + } + +private: + //============================================================================== + #if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 + typedef NSObject DelegateType; + #else + typedef NSObject DelegateType; + #endif + + void finished (NSInteger result) + { + Array chooserResults; + + exitModalState (0); + + if (panel != nil && result == NSFileHandlingPanelOKButton) + { + if (isSave) + { + chooserResults.add (URL (nsStringToJuce ([[panel URL] absoluteString]))); } else { @@ -235,12 +209,120 @@ void FileChooser::showPlatformDialog (Array& results, NSArray* urls = [openPanel URLs]; for (unsigned int i = 0; i < [urls count]; ++i) - results.add (File (nsStringToJuce ([[urls objectAtIndex: i] path]))); + chooserResults.add (URL (nsStringToJuce ([[urls objectAtIndex: i] absoluteString]))); } } - [panel setDelegate: nil]; + owner.finished (chooserResults); } + + bool shouldShowFilename (const String& filenameToTest) + { + const File f (filenameToTest); + auto nsFilename = juceStringToNS (filenameToTest); + + for (int i = filters.size(); --i >= 0;) + if (f.getFileName().matchesWildcard (filters[i], true)) + return true; + + #if (! defined (MAC_OS_X_VERSION_10_7)) || MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_7 + NSError* error; + NSString* name = [[NSWorkspace sharedWorkspace] typeOfFile: nsFilename error: &error]; + + if ([name isEqualToString: nsStringLiteral ("com.apple.alias-file")]) + { + FSRef ref; + FSPathMakeRef ((const UInt8*) [nsFilename fileSystemRepresentation], &ref, nullptr); + + Boolean targetIsFolder = false, wasAliased = false; + FSResolveAliasFileWithMountFlags (&ref, true, &targetIsFolder, &wasAliased, 0); + + return wasAliased && targetIsFolder; + } + #endif + + return f.isDirectory() + && ! [[NSWorkspace sharedWorkspace] isFilePackageAtPath: nsFilename]; + } + + void panelSelectionDidChange (id sender) + { + // NB: would need to extend FilePreviewComponent to handle the full list rather than just the first one + if (preview != nullptr) + preview->selectedFileChanged (File (getSelectedPaths (sender)[0])); + } + + static StringArray getSelectedPaths (id sender) + { + StringArray paths; + + if ([sender isKindOfClass: [NSOpenPanel class]]) + { + NSArray* urls = [(NSOpenPanel*) sender URLs]; + + for (NSUInteger i = 0; i < [urls count]; ++i) + paths.add (nsStringToJuce ([[urls objectAtIndex: i] path])); + } + else if ([sender isKindOfClass: [NSSavePanel class]]) + { + paths.add (nsStringToJuce ([[(NSSavePanel*) sender URL] path])); + } + + return paths; + } + + //============================================================================== + FileChooser& owner; + FilePreviewComponent* preview; + NSView* nsViewPreview = nullptr; + bool selectsDirectories, selectsFiles, isSave, selectMultiple; + + ScopedPointer panel; + DelegateType* delegate; + + StringArray filters; + String startingDirectory, filename; + + //============================================================================== + struct DelegateClass : ObjCClass + { + DelegateClass() : ObjCClass ("JUCEFileChooser_") + { + addIvar ("cppObject"); + + addMethod (@selector (panel:shouldShowFilename:), shouldShowFilename, "c@:@@"); + addMethod (@selector (panelSelectionDidChange:), panelSelectionDidChange, "c@"); + + #if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 + addProtocol (@protocol (NSOpenSavePanelDelegate)); + #endif + + registerClass(); + } + + private: + static BOOL shouldShowFilename (id self, SEL, id /*sender*/, NSString* filename) + { + auto* _this = getIvar (self, "cppObject"); + + return _this->shouldShowFilename (nsStringToJuce (filename)) ? YES : NO; + } + + static void panelSelectionDidChange (id self, SEL, id sender) + { + auto* _this = getIvar (self, "cppObject"); + + _this->panelSelectionDidChange (sender); + } + }; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native) +}; + +FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags, + FilePreviewComponent* preview) +{ + return new FileChooser::Native (owner, flags, preview); } bool FileChooser::isPlatformDialogAvailable() @@ -252,29 +334,4 @@ bool FileChooser::isPlatformDialogAvailable() #endif } -#else - -//============================================================================== -bool FileChooser::isPlatformDialogAvailable() -{ - return false; } - -void FileChooser::showPlatformDialog (Array&, - const String& /*title*/, - const File& /*currentFileOrDirectory*/, - const String& /*filter*/, - bool /*selectsDirectory*/, - bool /*selectsFiles*/, - bool /*isSaveDialogue*/, - bool /*warnAboutOverwritingExistingFiles*/, - bool /*selectMultipleFiles*/, - bool /*treatFilePackagesAsDirs*/, - FilePreviewComponent*) -{ - jassertfalse; //there's no such thing in iOS -} - -#endif - -} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_win32_FileChooser.cpp b/modules/juce_gui_basics/native/juce_win32_FileChooser.cpp index bb352b23b0..d1c8c5c40f 100644 --- a/modules/juce_gui_basics/native/juce_win32_FileChooser.cpp +++ b/modules/juce_gui_basics/native/juce_win32_FileChooser.cpp @@ -27,74 +27,102 @@ namespace juce { -namespace FileChooserHelpers +// Win32NativeFileChooser needs to be a reference counted object as there +// is no way for the parent to know when the dialog HWND has actually been +// created without pumping the message thread (which is forbidden when modal +// loops are disabled). However, the HWND pointer is the only way to cancel +// the dialog box. This means that the actual native FileChooser HWND may +// not have been created yet when the user deletes JUCE's FileChooser class. If this +// occurs the Win32NativeFileChooser will still have a reference count of 1 and will +// simply delete itself immedietely once the HWND will have been created a while later. +class Win32NativeFileChooser : public ReferenceCountedObject, private Thread { - struct FileChooserCallbackInfo +public: + typedef ReferenceCountedObjectPtr Ptr; + + enum { charsAvailableForResult = 32768 }; + + Win32NativeFileChooser (Component* parent, int flags, FilePreviewComponent* previewComp, + const File& startingFile, const String& titleToUse, + const String& filtersToUse) + : Thread ("Native Win32 FileChooser"), + owner (parent), title (titleToUse), filtersString (filtersToUse), + selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0), + selectsFiles ((flags & FileBrowserComponent::canSelectFiles) != 0), + isSave ((flags & FileBrowserComponent::saveMode) != 0), + warnAboutOverwrite ((flags & FileBrowserComponent::warnAboutOverwriting) != 0), + selectMultiple ((flags & FileBrowserComponent::canSelectMultipleItems) != 0), + nativeDialogRef (nullptr), shouldCancel (0) { - String initialPath; - String returnedString; // need this to get non-existent pathnames from the directory chooser - ScopedPointer customComponent; - }; + auto parentDirectory = startingFile.getParentDirectory(); - static int CALLBACK browseCallbackProc (HWND hWnd, UINT msg, LPARAM lParam, LPARAM lpData) - { - FileChooserCallbackInfo* info = (FileChooserCallbackInfo*) lpData; + // Handle nonexistent root directories in the same way as existing ones + files.calloc (static_cast (charsAvailableForResult) + 1); - if (msg == BFFM_INITIALIZED) - SendMessage (hWnd, BFFM_SETSELECTIONW, TRUE, (LPARAM) info->initialPath.toWideCharPointer()); - else if (msg == BFFM_VALIDATEFAILEDW) - info->returnedString = (LPCWSTR) lParam; - else if (msg == BFFM_VALIDATEFAILEDA) - info->returnedString = (const char*) lParam; - - return 0; - } - - static UINT_PTR CALLBACK openCallback (HWND hdlg, UINT uiMsg, WPARAM /*wParam*/, LPARAM lParam) - { - if (uiMsg == WM_INITDIALOG) + if (startingFile.isDirectory() ||startingFile.isRoot()) { - Component* customComp = ((FileChooserCallbackInfo*) (((OPENFILENAMEW*) lParam)->lCustData))->customComponent; - - HWND dialogH = GetParent (hdlg); - jassert (dialogH != 0); - if (dialogH == 0) - dialogH = hdlg; - - RECT r, cr; - GetWindowRect (dialogH, &r); - GetClientRect (dialogH, &cr); - - SetWindowPos (dialogH, 0, - r.left, r.top, - customComp->getWidth() + jmax (150, (int) (r.right - r.left)), - jmax (150, (int) (r.bottom - r.top)), - SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER); - - customComp->setBounds (cr.right, cr.top, customComp->getWidth(), cr.bottom - cr.top); - customComp->addToDesktop (0, dialogH); + initialPath = startingFile.getFullPathName(); } - else if (uiMsg == WM_NOTIFY) + else { - LPOFNOTIFY ofn = (LPOFNOTIFY) lParam; - - if (ofn->hdr.code == CDN_SELCHANGE) - { - FileChooserCallbackInfo* info = (FileChooserCallbackInfo*) ofn->lpOFN->lCustData; - - if (FilePreviewComponent* comp = dynamic_cast (info->customComponent->getChildComponent(0))) - { - WCHAR path [MAX_PATH * 2] = { 0 }; - CommDlg_OpenSave_GetFilePath (GetParent (hdlg), (LPARAM) &path, MAX_PATH); - - comp->selectedFileChanged (File (path)); - } - } + startingFile.getFileName().copyToUTF16 (files, + static_cast (charsAvailableForResult) * sizeof (WCHAR)); + initialPath = parentDirectory.getFullPathName(); } - return 0; + if (! selectsDirectories) + { + if (previewComp != nullptr) + customComponent = new CustomComponentHolder (previewComp); + + setupFilters(); + } } + ~Win32NativeFileChooser() + { + signalThreadShouldExit(); + waitForThreadToExit (-1); + } + + void open (bool async) + { + results.clear(); + + // the thread should not be running + nativeDialogRef.set (nullptr); + + if (async) + { + jassert (! isThreadRunning()); + + threadHasReference.reset(); + startThread(); + + threadHasReference.wait (-1); + } + else + { + results = openDialog (false); + owner->exitModalState (results.size() > 0 ? 1 : 0); + } + } + + void cancel() + { + ScopedLock lock (deletingDialog); + + customComponent = nullptr; + shouldCancel.set (1); + + if (auto hwnd = nativeDialogRef.get()) + EndDialog (hwnd, 0); + } + + Array results; + +private: + //============================================================================== class CustomComponentHolder : public Component { public: @@ -120,7 +148,413 @@ namespace FileChooserHelpers private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomComponentHolder) }; -} + + //============================================================================== + Component::SafePointer owner; + String title, filtersString; + ScopedPointer customComponent; + String initialPath, returnedString, defaultExtension; + + WaitableEvent threadHasReference; + CriticalSection deletingDialog; + + bool selectsDirectories, selectsFiles, isSave, warnAboutOverwrite, selectMultiple; + + HeapBlock files; + HeapBlock filters; + + Atomic nativeDialogRef; + Atomic shouldCancel; + + //============================================================================== + Array openDialog (bool async) + { + Array selections; + + if (selectsDirectories) + { + BROWSEINFO bi = { 0 }; + bi.hwndOwner = (HWND) (async ? nullptr : owner->getWindowHandle()); + bi.pszDisplayName = files; + bi.lpszTitle = title.toWideCharPointer(); + bi.lParam = (LPARAM) this; + bi.lpfn = browseCallbackProc; + #ifdef BIF_USENEWUI + bi.ulFlags = BIF_USENEWUI | BIF_VALIDATE; + #else + bi.ulFlags = 0x50; + #endif + + LPITEMIDLIST list = SHBrowseForFolder (&bi); + + if (! SHGetPathFromIDListW (list, files)) + { + files[0] = 0; + returnedString.clear(); + } + + LPMALLOC al; + + if (list != nullptr && SUCCEEDED (SHGetMalloc (&al))) + al->Free (list); + + if (files[0] != 0) + { + File result (String (files.get())); + + if (returnedString.isNotEmpty()) + result = result.getSiblingFile (returnedString); + + selections.add (URL (result)); + } + } + else + { + OPENFILENAMEW of = { 0 }; + + #ifdef OPENFILENAME_SIZE_VERSION_400W + of.lStructSize = OPENFILENAME_SIZE_VERSION_400W; + #else + of.lStructSize = sizeof (of); + #endif + of.hwndOwner = (HWND) (async ? nullptr : owner->getWindowHandle()); + of.lpstrFilter = filters.getData(); + of.nFilterIndex = 1; + of.lpstrFile = files; + of.nMaxFile = (DWORD) charsAvailableForResult; + of.lpstrInitialDir = initialPath.toWideCharPointer(); + of.lpstrTitle = title.toWideCharPointer(); + of.Flags = getOpenFilenameFlags (async); + of.lCustData = (LPARAM) this; + of.lpfnHook = &openCallback; + + if (isSave) + { + StringArray tokens; + tokens.addTokens (filtersString, ";,", "\"'"); + tokens.trim(); + tokens.removeEmptyStrings(); + + if (tokens.size() == 1 && tokens[0].removeCharacters ("*.").isNotEmpty()) + { + defaultExtension = tokens[0].fromFirstOccurrenceOf (".", false, false); + of.lpstrDefExt = defaultExtension.toWideCharPointer(); + } + + if (! GetSaveFileName (&of)) + return {}; + } + else + { + if (! GetOpenFileName (&of)) + return {}; + } + + if (selectMultiple && of.nFileOffset > 0 && files [of.nFileOffset - 1] == 0) + { + const WCHAR* filename = files + of.nFileOffset; + + while (*filename != 0) + { + selections.add (URL (File (String (files.get())).getChildFile (String (filename)))); + filename += wcslen (filename) + 1; + } + } + else if (files[0] != 0) + { + selections.add (URL (File (String (files.get())))); + } + } + + getNativeDialogList().removeValue (this); + + return selections; + } + + void run() override + { + // as long as the thread is running, don't delete this class + Ptr safeThis (this); + threadHasReference.signal(); + + Array r = openDialog (true); + MessageManager::callAsync ([safeThis, r] () + { + safeThis->results = r; + + if (safeThis->owner != nullptr) + safeThis->owner->exitModalState (r.size() > 0 ? 1 : 0); + }); + } + + static HashMap& getNativeDialogList() + { + static HashMap dialogs; + return dialogs; + } + + static Win32NativeFileChooser* getNativePointerForDialog (HWND hWnd) + { + return getNativeDialogList()[hWnd]; + } + + //============================================================================== + void setupFilters() + { + const size_t filterSpaceNumChars = 2048; + filters.calloc (filterSpaceNumChars); + + const size_t bytesWritten = filtersString.copyToUTF16 (filters.getData(), filterSpaceNumChars * sizeof (WCHAR)); + filtersString.copyToUTF16 (filters + (bytesWritten / sizeof (WCHAR)), + ((filterSpaceNumChars - 1) * sizeof (WCHAR) - bytesWritten)); + + for (size_t i = 0; i < filterSpaceNumChars; ++i) + if (filters[i] == '|') + filters[i] = 0; + } + + DWORD getOpenFilenameFlags (bool async) + { + DWORD ofFlags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | OFN_HIDEREADONLY | OFN_ENABLESIZING; + + if (warnAboutOverwrite) + ofFlags |= OFN_OVERWRITEPROMPT; + + if (selectMultiple) + ofFlags |= OFN_ALLOWMULTISELECT; + + if (async || customComponent != nullptr) + ofFlags |= OFN_ENABLEHOOK; + + return ofFlags; + } + + //============================================================================== + void initialised (HWND hWnd) + { + SendMessage (hWnd, BFFM_SETSELECTIONW, TRUE, (LPARAM) initialPath.toWideCharPointer()); + initDialog (hWnd); + } + + void validateFailed (const String& path) + { + returnedString = path; + } + + void initDialog (HWND hdlg) + { + ScopedLock lock (deletingDialog); + getNativeDialogList().set (hdlg, this); + + if (shouldCancel.get() != 0) + { + EndDialog (hdlg, 0); + } + else + { + nativeDialogRef.set (hdlg); + + if (customComponent) + { + Component::SafePointer custom (customComponent); + + RECT r, cr; + GetWindowRect (hdlg, &r); + GetClientRect (hdlg, &cr); + + auto componentWidth = custom->getWidth(); + + SetWindowPos (hdlg, 0, + r.left, r.top, + componentWidth + jmax (150, (int) (r.right - r.left)), + jmax (150, (int) (r.bottom - r.top)), + SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER); + + if (MessageManager::getInstance()->isThisTheMessageThread()) + { + custom->setBounds (cr.right, cr.top, componentWidth, cr.bottom - cr.top); + custom->addToDesktop (0, hdlg); + } + else + { + MessageManager::callAsync ([custom, cr, componentWidth, hdlg] () mutable + { + if (custom != nullptr) + { + custom->setBounds (cr.right, cr.top, componentWidth, cr.bottom - cr.top); + custom->addToDesktop (0, hdlg); + } + }); + } + } + } + } + + void destroyDialog (HWND hdlg) + { + ScopedLock exiting (deletingDialog); + + getNativeDialogList().remove (hdlg); + nativeDialogRef.set (nullptr); + } + + void selectionChanged (HWND hdlg) + { + ScopedLock lock (deletingDialog); + + if (customComponent != nullptr && shouldCancel.get() == 0) + { + if (FilePreviewComponent* comp = dynamic_cast (customComponent->getChildComponent(0))) + { + WCHAR path [MAX_PATH * 2] = { 0 }; + CommDlg_OpenSave_GetFilePath (hdlg, (LPARAM) &path, MAX_PATH); + + if (MessageManager::getInstance()->isThisTheMessageThread()) + { + comp->selectedFileChanged (File (path)); + } + else + { + Component::SafePointer safeComp (comp); + + File selectedFile (path); + MessageManager::callAsync ([safeComp, selectedFile] () mutable + { + safeComp->selectedFileChanged (selectedFile); + }); + } + } + } + } + + //============================================================================== + static int CALLBACK browseCallbackProc (HWND hWnd, UINT msg, LPARAM lParam, LPARAM lpData) + { + auto* self = reinterpret_cast (lpData); + + switch (msg) + { + case BFFM_INITIALIZED: self->initialised (hWnd); break; + case BFFM_VALIDATEFAILEDW: self->validateFailed (String ((LPCWSTR) lParam)); break; + case BFFM_VALIDATEFAILEDA: self->validateFailed (String ((const char*) lParam)); break; + default: break; + } + + return 0; + } + + static UINT_PTR CALLBACK openCallback (HWND hwnd, UINT uiMsg, WPARAM /*wParam*/, LPARAM lParam) + { + auto hdlg = getDialogFromHWND (hwnd); + + switch (uiMsg) + { + case WM_INITDIALOG: + { + if (auto* self = reinterpret_cast (((OPENFILENAMEW*) lParam)->lCustData)) + self->initDialog (hdlg); + + break; + } + + case WM_DESTROY: + { + if (auto* self = getNativeDialogList()[hdlg]) + self->destroyDialog (hdlg); + + break; + } + + case WM_NOTIFY: + { + auto ofn = reinterpret_cast (lParam); + + if (ofn->hdr.code == CDN_SELCHANGE) + if (auto* self = reinterpret_cast (ofn->lpOFN->lCustData)) + self->selectionChanged (hdlg); + + break; + } + + default: + break; + } + + return 0; + } + + static HWND getDialogFromHWND (HWND hwnd) + { + if (hwnd == nullptr) + return nullptr; + + HWND dialogH = GetParent (hwnd); + + if (dialogH == 0) + dialogH = hwnd; + + return dialogH; + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32NativeFileChooser) +}; + +class FileChooser::Native : public Component, public FileChooser::Pimpl +{ +public: + + Native (FileChooser& fileChooser, int flags, FilePreviewComponent* previewComp) + : owner (fileChooser), + nativeFileChooser (new Win32NativeFileChooser (this, flags, previewComp, fileChooser.startingFile, + fileChooser.title, fileChooser.filters)) + { + const Rectangle mainMon (Desktop::getInstance().getDisplays().getMainDisplay().userArea); + + setBounds (mainMon.getX() + mainMon.getWidth() / 4, + mainMon.getY() + mainMon.getHeight() / 4, + 0, 0); + + setOpaque (true); + setAlwaysOnTop (juce_areThereAnyAlwaysOnTopWindows()); + addToDesktop (0); + } + + ~Native() + { + exitModalState (0); + nativeFileChooser->cancel(); + nativeFileChooser = nullptr; + } + + void launch() override + { + SafePointer safeThis (this); + + enterModalState (true, ModalCallbackFunction::create( + [safeThis] (int) + { + if (safeThis != nullptr) + safeThis->owner.finished (safeThis->nativeFileChooser->results); + })); + + nativeFileChooser->open (true); + } + + void runModally() override + { + enterModalState (true); + nativeFileChooser->open (false); + exitModalState (nativeFileChooser->results.size() > 0 ? 1 : 0); + nativeFileChooser->cancel(); + + owner.finished (nativeFileChooser->results); + } + +private: + FileChooser& owner; + Win32NativeFileChooser::Ptr nativeFileChooser; +}; //============================================================================== bool FileChooser::isPlatformDialogAvailable() @@ -132,172 +566,10 @@ bool FileChooser::isPlatformDialogAvailable() #endif } -void FileChooser::showPlatformDialog (Array& results, const String& title_, const File& currentFileOrDirectory, - const String& filter, bool selectsDirectory, bool /*selectsFiles*/, - bool isSaveDialogue, bool warnAboutOverwritingExistingFiles, - bool selectMultipleFiles, bool /*treatFilePackagesAsDirs*/, - FilePreviewComponent* extraInfoComponent) +FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags, + FilePreviewComponent* preview) { - using namespace FileChooserHelpers; - - const String title (title_); - String defaultExtension; // scope of these strings must extend beyond dialog's lifetime. - - HeapBlock files; - const size_t charsAvailableForResult = 32768; - files.calloc (charsAvailableForResult + 1); - int filenameOffset = 0; - - FileChooserCallbackInfo info; - - // use a modal window as the parent for this dialog box - // to block input from other app windows - Component parentWindow; - const Rectangle mainMon (Desktop::getInstance().getDisplays().getMainDisplay().userArea); - parentWindow.setBounds (mainMon.getX() + mainMon.getWidth() / 4, - mainMon.getY() + mainMon.getHeight() / 4, - 0, 0); - parentWindow.setOpaque (true); - parentWindow.setAlwaysOnTop (juce_areThereAnyAlwaysOnTopWindows()); - parentWindow.addToDesktop (0); - - if (extraInfoComponent == nullptr) - parentWindow.enterModalState(); - - auto parentDirectory = currentFileOrDirectory.getParentDirectory(); - - // Handle nonexistent root directories in the same way as existing ones - if (currentFileOrDirectory.isDirectory() || currentFileOrDirectory.isRoot()) - { - info.initialPath = currentFileOrDirectory.getFullPathName(); - } - else - { - currentFileOrDirectory.getFileName().copyToUTF16 (files, charsAvailableForResult * sizeof (WCHAR)); - info.initialPath = parentDirectory.getFullPathName(); - } - - if (selectsDirectory) - { - BROWSEINFO bi = { 0 }; - bi.hwndOwner = (HWND) parentWindow.getWindowHandle(); - bi.pszDisplayName = files; - bi.lpszTitle = title.toWideCharPointer(); - bi.lParam = (LPARAM) &info; - bi.lpfn = browseCallbackProc; - #ifdef BIF_USENEWUI - bi.ulFlags = BIF_USENEWUI | BIF_VALIDATE; - #else - bi.ulFlags = 0x50; - #endif - - LPITEMIDLIST list = SHBrowseForFolder (&bi); - - if (! SHGetPathFromIDListW (list, files)) - { - files[0] = 0; - info.returnedString.clear(); - } - - LPMALLOC al; - if (list != nullptr && SUCCEEDED (SHGetMalloc (&al))) - al->Free (list); - - if (info.returnedString.isNotEmpty()) - { - results.add (File (String (files.get())).getSiblingFile (info.returnedString)); - return; - } - } - else - { - DWORD flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | OFN_HIDEREADONLY | OFN_ENABLESIZING; - - if (warnAboutOverwritingExistingFiles) - flags |= OFN_OVERWRITEPROMPT; - - if (selectMultipleFiles) - flags |= OFN_ALLOWMULTISELECT; - - if (extraInfoComponent != nullptr) - { - flags |= OFN_ENABLEHOOK; - - info.customComponent = new CustomComponentHolder (extraInfoComponent); - info.customComponent->enterModalState (false); - } - - const size_t filterSpaceNumChars = 2048; - HeapBlock filters; - filters.calloc (filterSpaceNumChars); - const size_t bytesWritten = filter.copyToUTF16 (filters.getData(), filterSpaceNumChars * sizeof (WCHAR)); - filter.copyToUTF16 (filters + (bytesWritten / sizeof (WCHAR)), - ((filterSpaceNumChars - 1) * sizeof (WCHAR) - bytesWritten)); - - for (size_t i = 0; i < filterSpaceNumChars; ++i) - if (filters[i] == '|') - filters[i] = 0; - - OPENFILENAMEW of = { 0 }; - String localPath (info.initialPath); - - #ifdef OPENFILENAME_SIZE_VERSION_400W - of.lStructSize = OPENFILENAME_SIZE_VERSION_400W; - #else - of.lStructSize = sizeof (of); - #endif - of.hwndOwner = (HWND) parentWindow.getWindowHandle(); - of.lpstrFilter = filters.getData(); - of.nFilterIndex = 1; - of.lpstrFile = files; - of.nMaxFile = (DWORD) charsAvailableForResult; - of.lpstrInitialDir = localPath.toWideCharPointer(); - of.lpstrTitle = title.toWideCharPointer(); - of.Flags = flags; - of.lCustData = (LPARAM) &info; - - if (extraInfoComponent != nullptr) - of.lpfnHook = &openCallback; - - if (isSaveDialogue) - { - StringArray tokens; - tokens.addTokens (filter, ";,", "\"'"); - tokens.trim(); - tokens.removeEmptyStrings(); - - if (tokens.size() == 1 && tokens[0].removeCharacters ("*.").isNotEmpty()) - { - defaultExtension = tokens[0].fromFirstOccurrenceOf (".", false, false); - of.lpstrDefExt = defaultExtension.toWideCharPointer(); - } - - if (! GetSaveFileName (&of)) - return; - } - else - { - if (! GetOpenFileName (&of)) - return; - } - - filenameOffset = of.nFileOffset; - } - - if (selectMultipleFiles && filenameOffset > 0 && files [filenameOffset - 1] == 0) - { - const WCHAR* filename = files + filenameOffset; - - while (*filename != 0) - { - results.add (File (String (files.get())).getChildFile (String (filename))); - filename += wcslen (filename) + 1; - } - } - else if (files[0] != 0) - { - results.add (File (String (files.get()))); - } + return new FileChooser::Native (owner, flags, preview); } } // namespace juce