| @@ -331,17 +331,34 @@ private: | |||||
| useNativeVersion); | useNativeVersion); | ||||
| fc->launchAsync (FileBrowserComponent::saveMode | FileBrowserComponent::canSelectFiles, | fc->launchAsync (FileBrowserComponent::saveMode | FileBrowserComponent::canSelectFiles, | ||||
| [] (const FileChooser& chooser) | |||||
| [fileToSave] (const FileChooser& chooser) | |||||
| { | { | ||||
| auto result = chooser.getURLResult(); | auto result = chooser.getURLResult(); | ||||
| auto name = result.isEmpty() ? String() | auto name = result.isEmpty() ? String() | ||||
| : (result.isLocalFile() ? result.getLocalFile().getFullPathName() | : (result.isLocalFile() ? result.getLocalFile().getFullPathName() | ||||
| : result.toString (true)); | : 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, | AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, | ||||
| "File Chooser...", | "File Chooser...", | ||||
| "You picked: " + name); | "You picked: " + name); | ||||
| }, nullptr, fileToSave); | |||||
| }); | |||||
| } | } | ||||
| else if (type == directoryChooser) | else if (type == directoryChooser) | ||||
| { | { | ||||
| @@ -33,8 +33,9 @@ DECLARE_JNI_CLASS (MediaScannerConnection, "android/media/MediaScannerConnection | |||||
| #undef JNI_CLASS_MEMBERS | #undef JNI_CLASS_MEMBERS | ||||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | #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"); | DECLARE_JNI_CLASS (ContentResolver, "android/content/ContentResolver"); | ||||
| #undef JNI_CLASS_MEMBERS | #undef JNI_CLASS_MEMBERS | ||||
| @@ -61,6 +62,14 @@ DECLARE_JNI_CLASS (AndroidEnvironment, "android/os/Environment"); | |||||
| DECLARE_JNI_CLASS (AndroidFile, "java/io/File"); | DECLARE_JNI_CLASS (AndroidFile, "java/io/File"); | ||||
| #undef JNI_CLASS_MEMBERS | #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) \ | #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | ||||
| STATICMETHOD (withAppendedId, "withAppendedId", "(Landroid/net/Uri;J)Landroid/net/Uri;") \ | STATICMETHOD (withAppendedId, "withAppendedId", "(Landroid/net/Uri;J)Landroid/net/Uri;") \ | ||||
| @@ -71,7 +80,7 @@ DECLARE_JNI_CLASS (ContentUris, "android/content/ContentUris"); | |||||
| struct AndroidContentUriResolver | struct AndroidContentUriResolver | ||||
| { | { | ||||
| public: | public: | ||||
| static LocalRef<jobject> getInputStreamForContentUri (const URL& url) | |||||
| static LocalRef<jobject> getStreamForContentUri (const URL& url, bool inputStream) | |||||
| { | { | ||||
| // only use this method for content URIs | // only use this method for content URIs | ||||
| jassert (url.getScheme() == "content"); | jassert (url.getScheme() == "content"); | ||||
| @@ -80,7 +89,10 @@ public: | |||||
| LocalRef<jobject> contentResolver (android.activity.callObjectMethod (JuceAppActivity.getContentResolver)); | LocalRef<jobject> contentResolver (android.activity.callObjectMethod (JuceAppActivity.getContentResolver)); | ||||
| if (contentResolver) | 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>(); | 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 | class MediaScannerConnectionClient : public AndroidInterfaceImplementer | ||||
| { | { | ||||
| @@ -137,7 +137,7 @@ public: | |||||
| if (isContentURL) | if (isContentURL) | ||||
| { | { | ||||
| auto inputStream = AndroidContentUriResolver::getInputStreamForContentUri (url); | |||||
| auto inputStream = AndroidContentUriResolver::getStreamForContentUri (url, true); | |||||
| if (inputStream != nullptr) | if (inputStream != nullptr) | ||||
| { | { | ||||
| @@ -197,6 +197,9 @@ URL::URL (URL&& other) | |||||
| parameterNames (static_cast<StringArray&&> (other.parameterNames)), | parameterNames (static_cast<StringArray&&> (other.parameterNames)), | ||||
| parameterValues (static_cast<StringArray&&> (other.parameterValues)), | parameterValues (static_cast<StringArray&&> (other.parameterValues)), | ||||
| filesToUpload (static_cast<ReferenceCountedArray<Upload>&&> (other.filesToUpload)) | 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); | parameterNames = static_cast<StringArray&&> (other.parameterNames); | ||||
| parameterValues = static_cast<StringArray&&> (other.parameterValues); | parameterValues = static_cast<StringArray&&> (other.parameterValues); | ||||
| filesToUpload = static_cast<ReferenceCountedArray<Upload>&&> (other.filesToUpload); | filesToUpload = static_cast<ReferenceCountedArray<Upload>&&> (other.filesToUpload); | ||||
| #if JUCE_IOS | |||||
| bookmark = other.bookmark; | |||||
| #endif | |||||
| return *this; | return *this; | ||||
| } | } | ||||
| @@ -494,6 +500,114 @@ bool URL::isProbablyAnEmailAddress (const String& possibleEmailAddress) | |||||
| && ! possibleEmailAddress.endsWithChar ('.'); | && ! 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, | InputStream* URL::createInputStream (const bool usePostCommand, | ||||
| OpenStreamProgressCallback* const progressCallback, | OpenStreamProgressCallback* const progressCallback, | ||||
| @@ -506,7 +620,15 @@ InputStream* URL::createInputStream (const bool usePostCommand, | |||||
| String httpRequestCmd) const | String httpRequestCmd) const | ||||
| { | { | ||||
| if (isLocalFile()) | 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(); | return getLocalFile().createInputStream(); | ||||
| #endif | |||||
| } | |||||
| ScopedPointer<WebInputStream> wi (new WebInputStream (*this, usePostCommand)); | ScopedPointer<WebInputStream> wi (new WebInputStream (*this, usePostCommand)); | ||||
| @@ -558,6 +680,29 @@ InputStream* URL::createInputStream (const bool usePostCommand, | |||||
| return wi.release(); | 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 | bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) const | ||||
| { | { | ||||
| @@ -327,6 +327,13 @@ public: | |||||
| int numRedirectsToFollow = 5, | int numRedirectsToFollow = 5, | ||||
| String httpRequestCmd = String()) const; | 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. | /** Represents a download task. | ||||
| Returned by downloadToFile to allow querying and controling the download task. | Returned by downloadToFile to allow querying and controling the download task. | ||||
| @@ -521,6 +528,13 @@ private: | |||||
| friend struct ContainerDeletePolicy<Upload>; | friend struct ContainerDeletePolicy<Upload>; | ||||
| ReferenceCountedArray<Upload> filesToUpload; | ReferenceCountedArray<Upload> filesToUpload; | ||||
| #if JUCE_IOS | |||||
| void* bookmark; | |||||
| friend void setURLBookmark (URL&, void*); | |||||
| friend void* getURLBookmark (URL&); | |||||
| #endif | |||||
| URL (const String&, int); | URL (const String&, int); | ||||
| void init(); | void init(); | ||||
| void addParameter (const String&, const String&); | void addParameter (const String&, const String&); | ||||
| @@ -73,7 +73,7 @@ private: | |||||
| result.add (URL (browserComponent.getSelectedFile (i))); | result.add (URL (browserComponent.getSelectedFile (i))); | ||||
| } | } | ||||
| owner.finished (result, true); | |||||
| owner.finished (result); | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -137,13 +137,12 @@ bool FileChooser::browseForMultipleFilesOrDirectories (FilePreviewComponent* pre | |||||
| previewComp); | previewComp); | ||||
| } | } | ||||
| bool FileChooser::browseForFileToSave (const bool warnAboutOverwrite, | |||||
| const File& fileWhichShouldBeSaved) | |||||
| bool FileChooser::browseForFileToSave (const bool warnAboutOverwrite) | |||||
| { | { | ||||
| return showDialog (FileBrowserComponent::saveMode | return showDialog (FileBrowserComponent::saveMode | ||||
| | FileBrowserComponent::canSelectFiles | | FileBrowserComponent::canSelectFiles | ||||
| | (warnAboutOverwrite ? FileBrowserComponent::warnAboutOverwriting : 0), | | (warnAboutOverwrite ? FileBrowserComponent::warnAboutOverwriting : 0), | ||||
| nullptr, fileWhichShouldBeSaved); | |||||
| nullptr); | |||||
| } | } | ||||
| bool FileChooser::browseForDirectory() | bool FileChooser::browseForDirectory() | ||||
| @@ -153,11 +152,8 @@ bool FileChooser::browseForDirectory() | |||||
| nullptr); | 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; | FocusRestorer focusRestorer; | ||||
| pimpl = createPimpl (flags, previewComp); | pimpl = createPimpl (flags, previewComp); | ||||
| @@ -171,8 +167,7 @@ bool FileChooser::showDialog (const int flags, FilePreviewComponent* const previ | |||||
| #endif | #endif | ||||
| void FileChooser::launchAsync (int flags, std::function<void (const FileChooser&)> callback, | 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 | // You must specify a callback when using launchAsync | ||||
| jassert (callback); | 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 | // you cannot run two file chooser dialog boxes at the same time | ||||
| jassert (asyncCallback == nullptr); | jassert (asyncCallback == nullptr); | ||||
| fileToSave = (flags & FileBrowserComponent::saveMode) != 0 ? fileWhichShouldBeSaved : File(); | |||||
| asyncCallback = static_cast<std::function<void (const FileChooser&)>&&> (callback); | asyncCallback = static_cast<std::function<void (const FileChooser&)>&&> (callback); | ||||
| pimpl = createPimpl (flags, previewComp); | pimpl = createPimpl (flags, previewComp); | ||||
| @@ -256,30 +249,13 @@ URL FileChooser::getURLResult() const | |||||
| return results.getFirst(); | 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::function<void (const FileChooser&)> callback; | ||||
| std::swap (callback, asyncCallback); | std::swap (callback, asyncCallback); | ||||
| results = asyncResults; | 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; | pimpl = nullptr; | ||||
| if (callback) | if (callback) | ||||
| @@ -66,8 +66,27 @@ public: | |||||
| @param initialFileOrDirectory the file or directory that should be selected | @param initialFileOrDirectory the file or directory that should be selected | ||||
| when the dialog box opens. If this parameter is | when the dialog box opens. If this parameter is | ||||
| set to File(), a sensible default directory will | 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 | @param filePatternsAllowed a set of file patterns to specify which files | ||||
| can be selected - each pattern should be | can be selected - each pattern should be | ||||
| separated by a comma or semi-colon, e.g. "*" or | separated by a comma or semi-colon, e.g. "*" or | ||||
| @@ -123,22 +142,12 @@ public: | |||||
| @param warnAboutOverwritingExistingFiles if true, the dialog box will ask | @param warnAboutOverwritingExistingFiles if true, the dialog box will ask | ||||
| the user if they're sure they want to overwrite a file that already | the user if they're sure they want to overwrite a file that already | ||||
| exists | 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 | @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 | the getResult() method to find out what the file was. Returns false | ||||
| if they cancelled instead. | if they cancelled instead. | ||||
| @see browseForFileToOpen, browseForDirectory | @see browseForFileToOpen, browseForDirectory | ||||
| */ | */ | ||||
| bool browseForFileToSave (bool warnAboutOverwritingExistingFiles, | |||||
| const File& fileWhichShouldBeSaved = File()); | |||||
| bool browseForFileToSave (bool warnAboutOverwritingExistingFiles); | |||||
| /** Shows a dialog box to choose a directory. | /** Shows a dialog box to choose a directory. | ||||
| @@ -163,24 +172,12 @@ public: | |||||
| /** Runs a dialog box for the given set of option flags. | /** Runs a dialog box for the given set of option flags. | ||||
| The flag values used are those in FileBrowserComponent::FileChooserFlags. | 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 | @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 | the getResult() method to find out what they chose. Returns false | ||||
| if they cancelled instead. | if they cancelled instead. | ||||
| @see FileBrowserComponent::FileChooserFlags | @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. | /** 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 | You must ensure that the lifetime of the callback object is longer than | ||||
| the lifetime of the file-chooser. | 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, | void launchAsync (int flags, | ||||
| std::function<void (const FileChooser&)>, | 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. | /** 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 | 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. | 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 | @see getResult, URL::getLocalFile | ||||
| */ | */ | ||||
| URL getURLResult() const; | URL getURLResult() const; | ||||
| @@ -266,6 +255,10 @@ public: | |||||
| This array may be empty if no files were chosen, or can contain multiple entries | This array may be empty if no files were chosen, or can contain multiple entries | ||||
| if multiple files were chosen. | 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 | @see getResults, URL::getLocalFile | ||||
| */ | */ | ||||
| const Array<URL>& getURLResults() const noexcept { return results; } | const Array<URL>& getURLResults() const noexcept { return results; } | ||||
| @@ -288,14 +281,14 @@ public: | |||||
| private: | private: | ||||
| //============================================================================== | //============================================================================== | ||||
| String title, filters; | String title, filters; | ||||
| File startingFile, fileToSave; | |||||
| File startingFile; | |||||
| Array<URL> results; | Array<URL> results; | ||||
| const bool useNativeDialogBox; | const bool useNativeDialogBox; | ||||
| const bool treatFilePackagesAsDirs; | const bool treatFilePackagesAsDirs; | ||||
| std::function<void (const FileChooser&)> asyncCallback; | std::function<void (const FileChooser&)> asyncCallback; | ||||
| //============================================================================== | //============================================================================== | ||||
| void finished (const Array<URL>&, bool); | |||||
| void finished (const Array<URL>&); | |||||
| //============================================================================== | //============================================================================== | ||||
| struct Pimpl | struct Pimpl | ||||
| @@ -303,7 +296,7 @@ private: | |||||
| virtual ~Pimpl() {} | virtual ~Pimpl() {} | ||||
| virtual void launch() = 0; | virtual void launch() = 0; | ||||
| virtual void runModally() = 0; | |||||
| virtual void runModally() = 0; | |||||
| }; | }; | ||||
| ScopedPointer<Pimpl> pimpl; | ScopedPointer<Pimpl> pimpl; | ||||
| @@ -109,7 +109,7 @@ public: | |||||
| { | { | ||||
| env->SetObjectArrayElement (jMimeTypes.get(), i, javaString (mimeTypes[i]).get()); | 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; | allMimeTypesHaveSameGroup = false; | ||||
| } | } | ||||
| @@ -168,7 +168,7 @@ public: | |||||
| } | } | ||||
| } | } | ||||
| owner.finished (chosenURLs, true); | |||||
| owner.finished (chosenURLs); | |||||
| } | } | ||||
| static Native* currentFileChooser; | 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 | class FileChooser::Native : private Component, public FileChooser::Pimpl | ||||
| { | { | ||||
| @@ -37,32 +37,54 @@ public: | |||||
| Native (FileChooser& fileChooser, int flags) | Native (FileChooser& fileChooser, int flags) | ||||
| : owner (fileChooser) | : owner (fileChooser) | ||||
| { | { | ||||
| String firstFileExtension; | |||||
| static FileChooserDelegateClass cls; | static FileChooserDelegateClass cls; | ||||
| delegate = [cls.createInstance() init]; | delegate = [cls.createInstance() init]; | ||||
| FileChooserDelegateClass::setOwner (delegate, this); | FileChooserDelegateClass::setOwner (delegate, this); | ||||
| auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters)); | |||||
| auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters, firstFileExtension)); | |||||
| if ((flags & FileBrowserComponent::saveMode) != 0) | 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]; | [url release]; | ||||
| } | } | ||||
| else | 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); | setOpaque (false); | ||||
| @@ -101,7 +123,7 @@ private: | |||||
| peer = newPeer; | peer = newPeer; | ||||
| if (auto* parentController = peer->controller) | if (auto* parentController = peer->controller) | ||||
| [parentController showViewController:controller sender:parentController]; | |||||
| [parentController showViewController: controller sender: parentController]; | |||||
| if (peer->view.window != nil) | if (peer->view.window != nil) | ||||
| peer->view.window.autoresizesSubviews = YES; | 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, ";", ""); | auto filters = StringArray::fromTokens (filterWildcards, ";", ""); | ||||
| StringArray result; | StringArray result; | ||||
| firstExtension = {}; | |||||
| if (! filters.contains ("*") && filters.size() > 0) | if (! filters.contains ("*") && filters.size() > 0) | ||||
| { | { | ||||
| for (auto filter : filters) | for (auto filter : filters) | ||||
| @@ -124,6 +148,9 @@ private: | |||||
| auto fileExtension = filter.fromLastOccurrenceOf (".", false, false); | auto fileExtension = filter.fromLastOccurrenceOf (".", false, false); | ||||
| auto fileExtensionCF = fileExtension.toCFString(); | auto fileExtensionCF = fileExtension.toCFString(); | ||||
| if (firstExtension.isEmpty()) | |||||
| firstExtension = fileExtension; | |||||
| auto tag = UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension, fileExtensionCF, nullptr); | auto tag = UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension, fileExtensionCF, nullptr); | ||||
| if (tag != nullptr) | if (tag != nullptr) | ||||
| @@ -141,21 +168,87 @@ private: | |||||
| return result; | 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) | 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() | void pickerWasCancelled() | ||||
| { | { | ||||
| Array<URL> chooserResults; | Array<URL> chooserResults; | ||||
| owner.finished (chooserResults, false); | |||||
| owner.finished (chooserResults); | |||||
| exitModalState (0); | exitModalState (0); | ||||
| } | } | ||||
| @@ -174,7 +267,7 @@ private: | |||||
| registerClass(); | 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"); } | static Native* getOwner (id self) { return getIvar<Native*> (self, "owner"); } | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -122,7 +122,7 @@ private: | |||||
| if (! shouldKill) | if (! shouldKill) | ||||
| { | { | ||||
| child.waitForProcessToFinish (60 * 1000); | 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) | bool shouldShowFilename (const String& filenameToTest) | ||||
| @@ -535,7 +535,7 @@ public: | |||||
| [safeThis] (int) | [safeThis] (int) | ||||
| { | { | ||||
| if (safeThis != nullptr) | if (safeThis != nullptr) | ||||
| safeThis->owner.finished (safeThis->nativeFileChooser->results, true); | |||||
| safeThis->owner.finished (safeThis->nativeFileChooser->results); | |||||
| })); | })); | ||||
| nativeFileChooser->open (true); | nativeFileChooser->open (true); | ||||
| @@ -548,7 +548,7 @@ public: | |||||
| exitModalState (nativeFileChooser->results.size() > 0 ? 1 : 0); | exitModalState (nativeFileChooser->results.size() > 0 ? 1 : 0); | ||||
| nativeFileChooser->cancel(); | nativeFileChooser->cancel(); | ||||
| owner.finished (nativeFileChooser->results, true); | |||||
| owner.finished (nativeFileChooser->results); | |||||
| } | } | ||||
| private: | private: | ||||