| @@ -331,17 +331,34 @@ private: | |||
| useNativeVersion); | |||
| fc->launchAsync (FileBrowserComponent::saveMode | FileBrowserComponent::canSelectFiles, | |||
| [] (const FileChooser& chooser) | |||
| [fileToSave] (const FileChooser& chooser) | |||
| { | |||
| auto result = chooser.getURLResult(); | |||
| auto name = result.isEmpty() ? String() | |||
| : (result.isLocalFile() ? result.getLocalFile().getFullPathName() | |||
| : result.toString (true)); | |||
| // Android and iOS file choosers will create placeholder files for chosen | |||
| // paths, so we may as well write into those files. | |||
| #if JUCE_ANDROID || JUCE_IOS | |||
| if (! result.isEmpty()) | |||
| { | |||
| ScopedPointer<InputStream> wi (fileToSave.createInputStream()); | |||
| ScopedPointer<OutputStream> wo (result.createOutputStream()); | |||
| if (wi != nullptr && wo != nullptr) | |||
| { | |||
| auto numWritten = wo->writeFromInputStream (*wi, -1); | |||
| jassert (numWritten > 0); | |||
| ignoreUnused (numWritten); | |||
| } | |||
| } | |||
| #endif | |||
| AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, | |||
| "File Chooser...", | |||
| "You picked: " + name); | |||
| }, nullptr, fileToSave); | |||
| }); | |||
| } | |||
| else if (type == directoryChooser) | |||
| { | |||
| @@ -33,8 +33,9 @@ DECLARE_JNI_CLASS (MediaScannerConnection, "android/media/MediaScannerConnection | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (query, "query", "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;") \ | |||
| METHOD (openInputStream, "openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;") \ | |||
| METHOD (query, "query", "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;") \ | |||
| METHOD (openInputStream, "openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;") \ | |||
| METHOD (openOutputStream, "openOutputStream", "(Landroid/net/Uri;)Ljava/io/OutputStream;") | |||
| DECLARE_JNI_CLASS (ContentResolver, "android/content/ContentResolver"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| @@ -61,6 +62,14 @@ DECLARE_JNI_CLASS (AndroidEnvironment, "android/os/Environment"); | |||
| DECLARE_JNI_CLASS (AndroidFile, "java/io/File"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (close, "close", "()V") \ | |||
| METHOD (flush, "flush", "()V") \ | |||
| METHOD (write, "write", "([BII)V") | |||
| DECLARE_JNI_CLASS (AndroidOutputStream, "java/io/OutputStream"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| STATICMETHOD (withAppendedId, "withAppendedId", "(Landroid/net/Uri;J)Landroid/net/Uri;") \ | |||
| @@ -71,7 +80,7 @@ DECLARE_JNI_CLASS (ContentUris, "android/content/ContentUris"); | |||
| struct AndroidContentUriResolver | |||
| { | |||
| public: | |||
| static LocalRef<jobject> getInputStreamForContentUri (const URL& url) | |||
| static LocalRef<jobject> getStreamForContentUri (const URL& url, bool inputStream) | |||
| { | |||
| // only use this method for content URIs | |||
| jassert (url.getScheme() == "content"); | |||
| @@ -80,7 +89,10 @@ public: | |||
| LocalRef<jobject> contentResolver (android.activity.callObjectMethod (JuceAppActivity.getContentResolver)); | |||
| if (contentResolver) | |||
| return LocalRef<jobject> ((env->CallObjectMethod (contentResolver.get(), ContentResolver.openInputStream, urlToUri (url).get()))); | |||
| return LocalRef<jobject> ((env->CallObjectMethod (contentResolver.get(), | |||
| inputStream ? ContentResolver.openInputStream | |||
| : ContentResolver.openOutputStream, | |||
| urlToUri (url).get()))); | |||
| return LocalRef<jobject>(); | |||
| } | |||
| @@ -354,6 +366,62 @@ private: | |||
| } | |||
| }; | |||
| //============================================================================== | |||
| struct AndroidContentUriOutputStream : public OutputStream | |||
| { | |||
| AndroidContentUriOutputStream (LocalRef<jobject>&& outputStream) | |||
| : stream (outputStream) | |||
| { | |||
| } | |||
| ~AndroidContentUriOutputStream() | |||
| { | |||
| stream.callVoidMethod (AndroidOutputStream.close); | |||
| } | |||
| void flush() override | |||
| { | |||
| stream.callVoidMethod (AndroidOutputStream.flush); | |||
| } | |||
| bool setPosition (int64 newPos) override | |||
| { | |||
| return (newPos == pos); | |||
| } | |||
| int64 getPosition() override | |||
| { | |||
| return pos; | |||
| } | |||
| bool write (const void* dataToWrite, size_t numberOfBytes) override | |||
| { | |||
| if (numberOfBytes == 0) | |||
| return true; | |||
| JNIEnv* env = getEnv(); | |||
| jbyteArray javaArray = env->NewByteArray ((jsize) numberOfBytes); | |||
| env->SetByteArrayRegion (javaArray, 0, (jsize) numberOfBytes, (const jbyte*) dataToWrite); | |||
| stream.callVoidMethod (AndroidOutputStream.write, javaArray, 0, (jint) numberOfBytes); | |||
| env->DeleteLocalRef (javaArray); | |||
| pos += static_cast<int64> (numberOfBytes); | |||
| return true; | |||
| } | |||
| GlobalRef stream; | |||
| int64 pos = 0; | |||
| }; | |||
| OutputStream* juce_CreateContentURIOutputStream (const URL& url) | |||
| { | |||
| auto stream = AndroidContentUriResolver::getStreamForContentUri (url, false); | |||
| return (stream.get() != 0 ? new AndroidContentUriOutputStream (static_cast<LocalRef<jobject>&&> (stream)) : nullptr); | |||
| } | |||
| //============================================================================== | |||
| class MediaScannerConnectionClient : public AndroidInterfaceImplementer | |||
| { | |||
| @@ -137,7 +137,7 @@ public: | |||
| if (isContentURL) | |||
| { | |||
| auto inputStream = AndroidContentUriResolver::getInputStreamForContentUri (url); | |||
| auto inputStream = AndroidContentUriResolver::getStreamForContentUri (url, true); | |||
| if (inputStream != nullptr) | |||
| { | |||
| @@ -197,6 +197,9 @@ URL::URL (URL&& other) | |||
| parameterNames (static_cast<StringArray&&> (other.parameterNames)), | |||
| parameterValues (static_cast<StringArray&&> (other.parameterValues)), | |||
| filesToUpload (static_cast<ReferenceCountedArray<Upload>&&> (other.filesToUpload)) | |||
| #if JUCE_IOS | |||
| , bookmark (other.bookmark) | |||
| #endif | |||
| { | |||
| } | |||
| @@ -207,6 +210,9 @@ URL& URL::operator= (URL&& other) | |||
| parameterNames = static_cast<StringArray&&> (other.parameterNames); | |||
| parameterValues = static_cast<StringArray&&> (other.parameterValues); | |||
| filesToUpload = static_cast<ReferenceCountedArray<Upload>&&> (other.filesToUpload); | |||
| #if JUCE_IOS | |||
| bookmark = other.bookmark; | |||
| #endif | |||
| return *this; | |||
| } | |||
| @@ -494,6 +500,114 @@ bool URL::isProbablyAnEmailAddress (const String& possibleEmailAddress) | |||
| && ! possibleEmailAddress.endsWithChar ('.'); | |||
| } | |||
| #if JUCE_IOS | |||
| void setURLBookmark (URL& u, void* bookmark) | |||
| { | |||
| u.bookmark = bookmark; | |||
| } | |||
| void* getURLBookmark (URL& u) | |||
| { | |||
| return u.bookmark; | |||
| } | |||
| template <typename Stream> struct iOSFileStreamWrapperFlush { static void flush (Stream*) {} }; | |||
| template <> struct iOSFileStreamWrapperFlush<FileOutputStream> { static void flush (OutputStream* o) { o->flush(); } }; | |||
| template <typename Stream> | |||
| class iOSFileStreamWrapper : public Stream | |||
| { | |||
| public: | |||
| iOSFileStreamWrapper (URL& urlToUse) | |||
| : Stream (getLocalFileAccess (urlToUse)), | |||
| url (urlToUse) | |||
| {} | |||
| ~iOSFileStreamWrapper() | |||
| { | |||
| iOSFileStreamWrapperFlush<Stream>::flush (this); | |||
| if (NSData* bookmark = (NSData*) getURLBookmark (url)) | |||
| { | |||
| BOOL isBookmarkStale = false; | |||
| NSError* error = nil; | |||
| auto* nsURL = [NSURL URLByResolvingBookmarkData: bookmark | |||
| options: 0 | |||
| relativeToURL: nil | |||
| bookmarkDataIsStale: &isBookmarkStale | |||
| error: &error]; | |||
| if (error == nil) | |||
| { | |||
| if (isBookmarkStale) | |||
| updateStaleBookmark (nsURL, url); | |||
| [nsURL stopAccessingSecurityScopedResource]; | |||
| } | |||
| else | |||
| { | |||
| auto* desc = [error localizedDescription]; | |||
| ignoreUnused (desc); | |||
| jassertfalse; | |||
| } | |||
| } | |||
| } | |||
| private: | |||
| URL url; | |||
| bool securityAccessSucceeded = false; | |||
| File getLocalFileAccess (URL& urlToUse) | |||
| { | |||
| if (NSData* bookmark = (NSData*) getURLBookmark (urlToUse)) | |||
| { | |||
| BOOL isBookmarkStale = false; | |||
| NSError* error = nil; | |||
| auto* nsURL = [NSURL URLByResolvingBookmarkData: bookmark | |||
| options: 0 | |||
| relativeToURL: nil | |||
| bookmarkDataIsStale: &isBookmarkStale | |||
| error: &error]; | |||
| if (error == nil) | |||
| { | |||
| securityAccessSucceeded = [nsURL startAccessingSecurityScopedResource]; | |||
| if (isBookmarkStale) | |||
| updateStaleBookmark (nsURL, urlToUse); | |||
| return urlToUse.getLocalFile(); | |||
| } | |||
| else | |||
| { | |||
| auto* desc = [error localizedDescription]; | |||
| ignoreUnused (desc); | |||
| jassertfalse; | |||
| } | |||
| } | |||
| return urlToUse.getLocalFile(); | |||
| } | |||
| void updateStaleBookmark (NSURL* nsURL, URL& juceUrl) | |||
| { | |||
| NSError* error = nil; | |||
| NSData* bookmark = [nsURL bookmarkDataWithOptions: NSURLBookmarkCreationSuitableForBookmarkFile | |||
| includingResourceValuesForKeys: nil | |||
| relativeToURL: nil | |||
| error: &error]; | |||
| if (error == nil) | |||
| setURLBookmark (juceUrl, (void*) bookmark); | |||
| else | |||
| jassertfalse; | |||
| } | |||
| }; | |||
| #endif | |||
| //============================================================================== | |||
| InputStream* URL::createInputStream (const bool usePostCommand, | |||
| OpenStreamProgressCallback* const progressCallback, | |||
| @@ -506,7 +620,15 @@ InputStream* URL::createInputStream (const bool usePostCommand, | |||
| String httpRequestCmd) const | |||
| { | |||
| if (isLocalFile()) | |||
| { | |||
| #if JUCE_IOS | |||
| // We may need to refresh the embedded bookmark. | |||
| return new iOSFileStreamWrapper<FileInputStream> (const_cast<URL&>(*this)); | |||
| #else | |||
| return getLocalFile().createInputStream(); | |||
| #endif | |||
| } | |||
| ScopedPointer<WebInputStream> wi (new WebInputStream (*this, usePostCommand)); | |||
| @@ -558,6 +680,29 @@ InputStream* URL::createInputStream (const bool usePostCommand, | |||
| return wi.release(); | |||
| } | |||
| #if JUCE_ANDROID | |||
| OutputStream* juce_CreateContentURIOutputStream (const URL&); | |||
| #endif | |||
| OutputStream* URL::createOutputStream() const | |||
| { | |||
| if (isLocalFile()) | |||
| { | |||
| #if JUCE_IOS | |||
| // We may need to refresh the embedded bookmark. | |||
| return new iOSFileStreamWrapper<FileOutputStream> (const_cast<URL&> (*this)); | |||
| #else | |||
| return new FileOutputStream (getLocalFile()); | |||
| #endif | |||
| } | |||
| #if JUCE_ANDROID | |||
| return juce_CreateContentURIOutputStream (*this); | |||
| #else | |||
| return nullptr; | |||
| #endif | |||
| } | |||
| //============================================================================== | |||
| bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) const | |||
| { | |||
| @@ -327,6 +327,13 @@ public: | |||
| int numRedirectsToFollow = 5, | |||
| String httpRequestCmd = String()) const; | |||
| /** Attempts to open an output stream to a URL for writing | |||
| This method can only be used for certain scheme types such as local files | |||
| and content:// URIs on Android. | |||
| */ | |||
| OutputStream* createOutputStream() const; | |||
| //============================================================================== | |||
| /** Represents a download task. | |||
| Returned by downloadToFile to allow querying and controling the download task. | |||
| @@ -521,6 +528,13 @@ private: | |||
| friend struct ContainerDeletePolicy<Upload>; | |||
| ReferenceCountedArray<Upload> filesToUpload; | |||
| #if JUCE_IOS | |||
| void* bookmark; | |||
| friend void setURLBookmark (URL&, void*); | |||
| friend void* getURLBookmark (URL&); | |||
| #endif | |||
| URL (const String&, int); | |||
| void init(); | |||
| void addParameter (const String&, const String&); | |||
| @@ -73,7 +73,7 @@ private: | |||
| result.add (URL (browserComponent.getSelectedFile (i))); | |||
| } | |||
| owner.finished (result, true); | |||
| owner.finished (result); | |||
| } | |||
| //============================================================================== | |||
| @@ -137,13 +137,12 @@ bool FileChooser::browseForMultipleFilesOrDirectories (FilePreviewComponent* pre | |||
| previewComp); | |||
| } | |||
| bool FileChooser::browseForFileToSave (const bool warnAboutOverwrite, | |||
| const File& fileWhichShouldBeSaved) | |||
| bool FileChooser::browseForFileToSave (const bool warnAboutOverwrite) | |||
| { | |||
| return showDialog (FileBrowserComponent::saveMode | |||
| | FileBrowserComponent::canSelectFiles | |||
| | (warnAboutOverwrite ? FileBrowserComponent::warnAboutOverwriting : 0), | |||
| nullptr, fileWhichShouldBeSaved); | |||
| nullptr); | |||
| } | |||
| bool FileChooser::browseForDirectory() | |||
| @@ -153,11 +152,8 @@ bool FileChooser::browseForDirectory() | |||
| nullptr); | |||
| } | |||
| bool FileChooser::showDialog (const int flags, FilePreviewComponent* const previewComp, | |||
| const File& fileWhichShouldBeSaved) | |||
| bool FileChooser::showDialog (const int flags, FilePreviewComponent* const previewComp) | |||
| { | |||
| fileToSave = (flags & FileBrowserComponent::saveMode) != 0 ? fileWhichShouldBeSaved : File(); | |||
| FocusRestorer focusRestorer; | |||
| pimpl = createPimpl (flags, previewComp); | |||
| @@ -171,8 +167,7 @@ bool FileChooser::showDialog (const int flags, FilePreviewComponent* const previ | |||
| #endif | |||
| void FileChooser::launchAsync (int flags, std::function<void (const FileChooser&)> callback, | |||
| FilePreviewComponent* previewComp, | |||
| const File& fileWhichShouldBeSaved) | |||
| FilePreviewComponent* previewComp) | |||
| { | |||
| // You must specify a callback when using launchAsync | |||
| jassert (callback); | |||
| @@ -180,8 +175,6 @@ void FileChooser::launchAsync (int flags, std::function<void (const FileChooser& | |||
| // you cannot run two file chooser dialog boxes at the same time | |||
| jassert (asyncCallback == nullptr); | |||
| fileToSave = (flags & FileBrowserComponent::saveMode) != 0 ? fileWhichShouldBeSaved : File(); | |||
| asyncCallback = static_cast<std::function<void (const FileChooser&)>&&> (callback); | |||
| pimpl = createPimpl (flags, previewComp); | |||
| @@ -256,30 +249,13 @@ URL FileChooser::getURLResult() const | |||
| return results.getFirst(); | |||
| } | |||
| void FileChooser::finished (const Array<URL>& asyncResults, bool shouldMove) | |||
| void FileChooser::finished (const Array<URL>& asyncResults) | |||
| { | |||
| std::function<void (const FileChooser&)> callback; | |||
| std::swap (callback, asyncCallback); | |||
| results = asyncResults; | |||
| if (shouldMove && fileToSave.existsAsFile()) | |||
| { | |||
| if (results.size() > 0) | |||
| { | |||
| // The user either selected multiple files or wants to save the file to a URL | |||
| // Both are not supported | |||
| jassert (results.size() == 1 && results.getReference (0).isLocalFile()); | |||
| if (! fileToSave.moveFileTo (results.getReference (0).getLocalFile())) | |||
| results.clear(); | |||
| } | |||
| else | |||
| { | |||
| fileToSave.deleteFile(); | |||
| } | |||
| } | |||
| pimpl = nullptr; | |||
| if (callback) | |||
| @@ -66,8 +66,27 @@ public: | |||
| @param initialFileOrDirectory the file or directory that should be selected | |||
| when the dialog box opens. If this parameter is | |||
| set to File(), a sensible default directory will | |||
| be used instead. This parameter is ignored for native | |||
| iOS file choosers. | |||
| be used instead. | |||
| Note: on iOS when saving a file, a user will not | |||
| be able to change a file name, so it may be a good | |||
| idea to include at list a valid file name in | |||
| initialFileOrDirectory. When no filename is found, | |||
| "Untitled" will be used. | |||
| Also, if you pass an already existing file on iOS, | |||
| that file will be automatically copied to the | |||
| destination chosen by user and if it can be previewed, | |||
| its preview will be presented in the dialog too. You | |||
| will still be able to write into this file copy, since | |||
| its URL will be returned by getURLResult(). This can be | |||
| useful when you want to save e.g. an image, so that | |||
| you can pass a (temporary) file with low quality | |||
| preview and after the user picks the destination, | |||
| you can write a high quality image into the copied | |||
| file. If you create such a temporary file, you need | |||
| to delete it yourself, once it is not needed anymore. | |||
| @param filePatternsAllowed a set of file patterns to specify which files | |||
| can be selected - each pattern should be | |||
| separated by a comma or semi-colon, e.g. "*" or | |||
| @@ -123,22 +142,12 @@ public: | |||
| @param warnAboutOverwritingExistingFiles if true, the dialog box will ask | |||
| the user if they're sure they want to overwrite a file that already | |||
| exists | |||
| @param fileWhichShouldBeSaved if this parameter is specified, then, if the the user | |||
| selects a valid location to save the file, fileWhichShouldBeSaved will | |||
| automaitcally be moved to the location selected by the user when the user | |||
| clicks 'ok'. If you do not specify this parameter, then it is your | |||
| responsibility to save your file at the location that is returned from this | |||
| file chooser. Typically, when using this parameter, you already write the | |||
| file you wish to save to a temporary location and then supply the path to | |||
| this file to this parameter. This parameter is required on iOS when using | |||
| native file save dialogs but can be used on all other platforms. | |||
| @returns true if the user chose a file and pressed 'ok', in which case, use | |||
| the getResult() method to find out what the file was. Returns false | |||
| if they cancelled instead. | |||
| @see browseForFileToOpen, browseForDirectory | |||
| */ | |||
| bool browseForFileToSave (bool warnAboutOverwritingExistingFiles, | |||
| const File& fileWhichShouldBeSaved = File()); | |||
| bool browseForFileToSave (bool warnAboutOverwritingExistingFiles); | |||
| /** Shows a dialog box to choose a directory. | |||
| @@ -163,24 +172,12 @@ public: | |||
| /** Runs a dialog box for the given set of option flags. | |||
| The flag values used are those in FileBrowserComponent::FileChooserFlags. | |||
| @param fileWhichShouldBeSaved if this parameter is specified and saveMode is | |||
| specified, then, if the the user selects a valid location to save the file, | |||
| fileWhichShouldBeSaved will automaitcally be moved to the location selected | |||
| by the user when the user clicks 'ok'. If you do not specify this parameter, | |||
| then it is your responsibility to save your file at the location that is | |||
| returned from this file chooser. Typically, when using this parameter, | |||
| you already write the file you wish to save to a temporary location and | |||
| then supply the path to this file to this parameter. This parameter is | |||
| required on iOS when using native file save dialogs but can be used on all | |||
| other platforms. | |||
| @returns true if the user chose a directory and pressed 'ok', in which case, use | |||
| the getResult() method to find out what they chose. Returns false | |||
| if they cancelled instead. | |||
| @see FileBrowserComponent::FileChooserFlags | |||
| */ | |||
| bool showDialog (int flags, FilePreviewComponent* previewComponent, | |||
| const File& fileWhichShouldBeSaved = File()); | |||
| bool showDialog (int flags, FilePreviewComponent* previewComponent); | |||
| /** Use this method to launch the file browser window asynchronously. | |||
| @@ -196,22 +193,10 @@ public: | |||
| You must ensure that the lifetime of the callback object is longer than | |||
| the lifetime of the file-chooser. | |||
| @param fileWhichShouldBeSaved if this parameter is specified and saveMode is | |||
| specified, then, if the the user selects a valid location to save the file, | |||
| fileWhichShouldBeSaved will automaitcally be moved to the location selected | |||
| by the user when the user clicks 'ok'. If you do not specify this parameter, | |||
| then it is your responsibility to save your file at the location that is | |||
| returned from this file chooser. Typically, when using this parameter, | |||
| you already write the file you wish to save to a temporary location and | |||
| then supply the path to this file to this parameter. This parameter is | |||
| required on iOS when using native file save dialogs but can be used on all | |||
| other platforms. | |||
| */ | |||
| void launchAsync (int flags, | |||
| std::function<void (const FileChooser&)>, | |||
| FilePreviewComponent* previewComponent = nullptr, | |||
| const File& fileWhichShouldBeSaved = File()); | |||
| FilePreviewComponent* previewComponent = nullptr); | |||
| //============================================================================== | |||
| /** Returns the last file that was chosen by one of the browseFor methods. | |||
| @@ -252,6 +237,10 @@ public: | |||
| 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. | |||
| Note: on iOS it is best to dispose any copies of returned URL as soon as | |||
| you finish dealing with the file. This is because URL might be security | |||
| scoped and a system allows only for a limited number of such URLs. | |||
| @see getResult, URL::getLocalFile | |||
| */ | |||
| URL getURLResult() const; | |||
| @@ -266,6 +255,10 @@ public: | |||
| This array may be empty if no files were chosen, or can contain multiple entries | |||
| if multiple files were chosen. | |||
| Note: on iOS it is best to dispose any copies of returned URLs as soon as | |||
| you finish dealing with the file. This is because URLs might be security | |||
| scoped and a system allows only for a limited number of such URLs. | |||
| @see getResults, URL::getLocalFile | |||
| */ | |||
| const Array<URL>& getURLResults() const noexcept { return results; } | |||
| @@ -288,14 +281,14 @@ public: | |||
| private: | |||
| //============================================================================== | |||
| String title, filters; | |||
| File startingFile, fileToSave; | |||
| File startingFile; | |||
| Array<URL> results; | |||
| const bool useNativeDialogBox; | |||
| const bool treatFilePackagesAsDirs; | |||
| std::function<void (const FileChooser&)> asyncCallback; | |||
| //============================================================================== | |||
| void finished (const Array<URL>&, bool); | |||
| void finished (const Array<URL>&); | |||
| //============================================================================== | |||
| struct Pimpl | |||
| @@ -303,7 +296,7 @@ private: | |||
| virtual ~Pimpl() {} | |||
| virtual void launch() = 0; | |||
| virtual void runModally() = 0; | |||
| virtual void runModally() = 0; | |||
| }; | |||
| ScopedPointer<Pimpl> pimpl; | |||
| @@ -109,7 +109,7 @@ public: | |||
| { | |||
| env->SetObjectArrayElement (jMimeTypes.get(), i, javaString (mimeTypes[i]).get()); | |||
| if (mimeGroup != mimeTypes[0].upToFirstOccurrenceOf ("/", false, false)) | |||
| if (mimeGroup != mimeTypes[i].upToFirstOccurrenceOf ("/", false, false)) | |||
| allMimeTypesHaveSameGroup = false; | |||
| } | |||
| @@ -168,7 +168,7 @@ public: | |||
| } | |||
| } | |||
| owner.finished (chosenURLs, true); | |||
| owner.finished (chosenURLs); | |||
| } | |||
| static Native* currentFileChooser; | |||
| @@ -28,8 +28,8 @@ namespace juce | |||
| { | |||
| //============================================================================== | |||
| template <> struct ContainerDeletePolicy<UIDocumentPickerViewController> { static void destroy (NSObject* o) { [o release]; } }; | |||
| template <> struct ContainerDeletePolicy<NSObject<UIDocumentPickerDelegate>> { static void destroy (NSObject* o) { [o release]; } }; | |||
| template <> struct ContainerDeletePolicy<UIDocumentPickerViewController> { static void destroy (NSObject* o) { [o release]; } }; | |||
| template <> struct ContainerDeletePolicy<NSObject<UIDocumentPickerDelegate>> { static void destroy (NSObject* o) { [o release]; } }; | |||
| class FileChooser::Native : private Component, public FileChooser::Pimpl | |||
| { | |||
| @@ -37,32 +37,54 @@ public: | |||
| Native (FileChooser& fileChooser, int flags) | |||
| : owner (fileChooser) | |||
| { | |||
| String firstFileExtension; | |||
| static FileChooserDelegateClass cls; | |||
| delegate = [cls.createInstance() init]; | |||
| FileChooserDelegateClass::setOwner (delegate, this); | |||
| auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters)); | |||
| auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters, firstFileExtension)); | |||
| if ((flags & FileBrowserComponent::saveMode) != 0) | |||
| { | |||
| // You must specify the fileWhichShouldBeSaved parameter when using | |||
| // the native save dialog on iOS! | |||
| jassert (owner.fileToSave.existsAsFile()); | |||
| auto currentFileOrDirectory = owner.startingFile; | |||
| UIDocumentPickerMode pickerMode = currentFileOrDirectory.existsAsFile() | |||
| ? UIDocumentPickerModeExportToService | |||
| : UIDocumentPickerModeMoveToService; | |||
| auto url = [[NSURL alloc] initFileURLWithPath:juceStringToNS (owner.fileToSave.getFullPathName())]; | |||
| if (! currentFileOrDirectory.existsAsFile()) | |||
| { | |||
| auto filename = getFilename (currentFileOrDirectory, firstFileExtension); | |||
| auto tmpDirectory = File::createTempFile ("JUCE-filepath"); | |||
| if (tmpDirectory.createDirectory().wasOk()) | |||
| { | |||
| currentFileOrDirectory = tmpDirectory.getChildFile (filename); | |||
| currentFileOrDirectory.replaceWithText (""); | |||
| } | |||
| else | |||
| { | |||
| // Temporary directory creation failed! You need to specify a | |||
| // path you have write access to. Saving will not work for | |||
| // current path. | |||
| jassertfalse; | |||
| } | |||
| } | |||
| controller = [[UIDocumentPickerViewController alloc] initWithURL:url | |||
| inMode:UIDocumentPickerModeExportToService]; | |||
| auto url = [[NSURL alloc] initFileURLWithPath: juceStringToNS (currentFileOrDirectory.getFullPathName())]; | |||
| controller = [[UIDocumentPickerViewController alloc] initWithURL: url | |||
| inMode: pickerMode]; | |||
| [url release]; | |||
| } | |||
| else | |||
| { | |||
| controller = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:utTypeArray | |||
| inMode:UIDocumentPickerModeOpen]; | |||
| controller = [[UIDocumentPickerViewController alloc] initWithDocumentTypes: utTypeArray | |||
| inMode: UIDocumentPickerModeOpen]; | |||
| } | |||
| [controller setDelegate:delegate]; | |||
| [controller setModalTransitionStyle:UIModalTransitionStyleCrossDissolve]; | |||
| [controller setDelegate: delegate]; | |||
| [controller setModalTransitionStyle: UIModalTransitionStyleCrossDissolve]; | |||
| setOpaque (false); | |||
| @@ -101,7 +123,7 @@ private: | |||
| peer = newPeer; | |||
| if (auto* parentController = peer->controller) | |||
| [parentController showViewController:controller sender:parentController]; | |||
| [parentController showViewController: controller sender: parentController]; | |||
| if (peer->view.window != nil) | |||
| peer->view.window.autoresizesSubviews = YES; | |||
| @@ -109,11 +131,13 @@ private: | |||
| } | |||
| //============================================================================== | |||
| static StringArray getUTTypesForWildcards (const String& filterWildcards) | |||
| static StringArray getUTTypesForWildcards (const String& filterWildcards, String& firstExtension) | |||
| { | |||
| auto filters = StringArray::fromTokens (filterWildcards, ";", ""); | |||
| StringArray result; | |||
| firstExtension = {}; | |||
| if (! filters.contains ("*") && filters.size() > 0) | |||
| { | |||
| for (auto filter : filters) | |||
| @@ -124,6 +148,9 @@ private: | |||
| auto fileExtension = filter.fromLastOccurrenceOf (".", false, false); | |||
| auto fileExtensionCF = fileExtension.toCFString(); | |||
| if (firstExtension.isEmpty()) | |||
| firstExtension = fileExtension; | |||
| auto tag = UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension, fileExtensionCF, nullptr); | |||
| if (tag != nullptr) | |||
| @@ -141,21 +168,87 @@ private: | |||
| return result; | |||
| } | |||
| static String getFilename (const File& path, const String& fallbackExtension) | |||
| { | |||
| auto filename = path.getFileNameWithoutExtension(); | |||
| auto extension = path.getFileExtension().substring (1); | |||
| if (filename.isEmpty()) | |||
| filename = "Untitled"; | |||
| if (extension.isEmpty()) | |||
| extension = fallbackExtension; | |||
| if (extension.isNotEmpty()) | |||
| filename += String (".") + extension; | |||
| return filename; | |||
| } | |||
| //============================================================================== | |||
| void didPickDocumentAtURL (NSURL* url) | |||
| { | |||
| Array<URL> chooserResults; | |||
| chooserResults.add (URL (nsStringToJuce ([url absoluteString]))); | |||
| bool isWriting = controller.get().documentPickerMode == UIDocumentPickerModeExportToService | |||
| | controller.get().documentPickerMode == UIDocumentPickerModeMoveToService; | |||
| NSUInteger accessOptions = isWriting ? 0 : NSFileCoordinatorReadingWithoutChanges; | |||
| auto* fileAccessIntent = isWriting | |||
| ? [NSFileAccessIntent writingIntentWithURL: url options: accessOptions] | |||
| : [NSFileAccessIntent readingIntentWithURL: url options: accessOptions]; | |||
| NSArray<NSFileAccessIntent*>* intents = @[fileAccessIntent]; | |||
| auto* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter: nil]; | |||
| [fileCoordinator coordinateAccessWithIntents: intents queue: [NSOperationQueue mainQueue] byAccessor: ^(NSError* err) | |||
| { | |||
| Array<URL> chooserResults; | |||
| if (err == nil) | |||
| { | |||
| [url startAccessingSecurityScopedResource]; | |||
| NSError* error = nil; | |||
| NSData* bookmark = [url bookmarkDataWithOptions: 0 | |||
| includingResourceValuesForKeys: nil | |||
| relativeToURL: nil | |||
| error: &error]; | |||
| [url stopAccessingSecurityScopedResource]; | |||
| URL juceUrl (nsStringToJuce ([url absoluteString])); | |||
| if (error == nil) | |||
| { | |||
| setURLBookmark (juceUrl, (void*) bookmark); | |||
| } | |||
| else | |||
| { | |||
| auto* desc = [error localizedDescription]; | |||
| ignoreUnused (desc); | |||
| jassertfalse; | |||
| } | |||
| chooserResults.add (juceUrl); | |||
| } | |||
| else | |||
| { | |||
| auto* desc = [err localizedDescription]; | |||
| ignoreUnused (desc); | |||
| jassertfalse; | |||
| } | |||
| owner.finished (chooserResults, false); | |||
| exitModalState (1); | |||
| owner.finished (chooserResults); | |||
| }]; | |||
| } | |||
| void pickerWasCancelled() | |||
| { | |||
| Array<URL> chooserResults; | |||
| owner.finished (chooserResults, false); | |||
| owner.finished (chooserResults); | |||
| exitModalState (0); | |||
| } | |||
| @@ -174,7 +267,7 @@ private: | |||
| registerClass(); | |||
| } | |||
| static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); } | |||
| static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); } | |||
| static Native* getOwner (id self) { return getIvar<Native*> (self, "owner"); } | |||
| //============================================================================== | |||
| @@ -122,7 +122,7 @@ private: | |||
| if (! shouldKill) | |||
| { | |||
| child.waitForProcessToFinish (60 * 1000); | |||
| owner.finished (selection, true); | |||
| owner.finished (selection); | |||
| } | |||
| } | |||
| @@ -213,7 +213,7 @@ private: | |||
| } | |||
| } | |||
| owner.finished (chooserResults, true); | |||
| owner.finished (chooserResults); | |||
| } | |||
| bool shouldShowFilename (const String& filenameToTest) | |||
| @@ -535,7 +535,7 @@ public: | |||
| [safeThis] (int) | |||
| { | |||
| if (safeThis != nullptr) | |||
| safeThis->owner.finished (safeThis->nativeFileChooser->results, true); | |||
| safeThis->owner.finished (safeThis->nativeFileChooser->results); | |||
| })); | |||
| nativeFileChooser->open (true); | |||
| @@ -548,7 +548,7 @@ public: | |||
| exitModalState (nativeFileChooser->results.size() > 0 ? 1 : 0); | |||
| nativeFileChooser->cancel(); | |||
| owner.finished (nativeFileChooser->results, true); | |||
| owner.finished (nativeFileChooser->results); | |||
| } | |||
| private: | |||