1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Added iOS/Android native file chooser support and support for asynchronous invocation of file choosers

This commit is contained in:
hogliux 2017-11-20 10:56:08 +00:00
parent 7e1ec31df9
commit 7e23bf28ae
15 changed files with 2426 additions and 675 deletions

View file

@ -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<DialogType> (i));
}
ImagePreviewComponent imagePreview;
ScopedPointer<FileChooser> fc;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DialogsDemo)
};

View file

@ -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",
"<array>\n"
" <string>iCloud.$(CFBundleIdentifier)</string>\n"
" </array>");
entitlements.set ("com.apple.developer.icloud-services",
"<array>\n"
" <string>CloudDocuments</string>\n"
" </array>");
entitlements.set ("com.apple.developer.ubiquity-container-identifiers",
"<array>\n"
" <string>iCloud.$(CFBundleIdentifier)</string>\n"
" </array>");
}
return entitlements;
}

View file

@ -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);

View file

@ -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;") \

View file

@ -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<URL> 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<void (const FileChooser&)> 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<std::function<void (const FileChooser&)>&&> (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<File> FileChooser::getResults() const noexcept
{
Array<File> 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<URL>& asyncResults)
{
std::function<void (const FileChooser&)> callback;
std::swap (callback, asyncCallback);
results = asyncResults;
pimpl = nullptr;
if (callback)
callback (*this);
}
//==============================================================================
FilePreviewComponent::FilePreviewComponent() {}
FilePreviewComponent::~FilePreviewComponent() {}

View file

@ -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<void (const FileChooser&)>,
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<File>& getResults() const noexcept { return results; }
Array<File> 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<URL>& 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<File> results;
Array<URL> results;
const bool useNativeDialogBox;
const bool treatFilePackagesAsDirs;
std::function<void (const FileChooser&)> asyncCallback;
static void showPlatformDialog (Array<File>& 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<URL>&);
//==============================================================================
struct Pimpl
{
virtual ~Pimpl() {}
virtual void launch() = 0;
virtual void runModally() = 0;
};
ScopedPointer<Pimpl> 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)
};

View file

@ -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

View file

@ -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

View file

@ -27,26 +27,198 @@
namespace juce
{
void FileChooser::showPlatformDialog (Array<File>& /*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<jobject> 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<jobjectArray> 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<URL> chosenURLs;
if (resultCode == /*Activity.RESULT_OK*/ -1 && intentData != nullptr)
{
LocalRef<jobject> 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

View file

@ -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))

View file

@ -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

View file

@ -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<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
{
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<UIViewComponentPeer*> (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<URL> chooserResults;
chooserResults.add (URL (nsStringToJuce ([url absoluteString])));
owner.finished (chooserResults);
exitModalState (1);
}
void pickerWasCancelled()
{
Array<URL> chooserResults;
owner.finished (chooserResults);
exitModalState (0);
}
//==============================================================================
struct FileChooserDelegateClass : public ObjCClass<NSObject<UIDocumentPickerDelegate>>
{
FileChooserDelegateClass() : ObjCClass<NSObject<UIDocumentPickerDelegate>> ("FileChooserDelegate_")
{
addIvar<Native*> ("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<Native*> (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<NSObject<UIDocumentPickerDelegate>> delegate;
ScopedPointer<UIDocumentPickerViewController> 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

View file

@ -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<URL> 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<File>& 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

View file

@ -27,95 +27,7 @@
namespace juce
{
#if JUCE_MAC
struct FileChooserDelegateClass : public ObjCClass <NSObject>
{
FileChooserDelegateClass() : ObjCClass <NSObject> ("JUCEFileChooser_")
{
addIvar<StringArray*> ("filters");
addIvar<FilePreviewComponent*> ("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<StringArray*> (self, "filters"); }
static FilePreviewComponent* getFilePreviewComponent (id self) { return getIvar<FilePreviewComponent*> (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<File>& 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<NSSavePanel> { 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<TemporaryMainMenuWithStandardCommands> 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<NSOpenSavePanelDelegate> 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<TemporaryMainMenuWithStandardCommands> 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<NSOpenSavePanelDelegate> DelegateType;
#else
typedef NSObject DelegateType;
#endif
void finished (NSInteger result)
{
Array<URL> 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<File>& 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<NSSavePanel> panel;
DelegateType* delegate;
StringArray filters;
String startingDirectory, filename;
//==============================================================================
struct DelegateClass : ObjCClass<DelegateType>
{
DelegateClass() : ObjCClass <DelegateType> ("JUCEFileChooser_")
{
addIvar<Native*> ("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<Native*> (self, "cppObject");
return _this->shouldShowFilename (nsStringToJuce (filename)) ? YES : NO;
}
static void panelSelectionDidChange (id self, SEL, id sender)
{
auto* _this = getIvar<Native*> (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<File>&,
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

View file

@ -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<Win32NativeFileChooser> 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<Component> 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<size_t> (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<FilePreviewComponent*> (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<size_t> (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<URL> results;
private:
//==============================================================================
class CustomComponentHolder : public Component
{
public:
@ -120,7 +148,413 @@ namespace FileChooserHelpers
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomComponentHolder)
};
}
//==============================================================================
Component::SafePointer<Component> owner;
String title, filtersString;
ScopedPointer<CustomComponentHolder> customComponent;
String initialPath, returnedString, defaultExtension;
WaitableEvent threadHasReference;
CriticalSection deletingDialog;
bool selectsDirectories, selectsFiles, isSave, warnAboutOverwrite, selectMultiple;
HeapBlock<WCHAR> files;
HeapBlock<WCHAR> filters;
Atomic<HWND> nativeDialogRef;
Atomic<int> shouldCancel;
//==============================================================================
Array<URL> openDialog (bool async)
{
Array<URL> 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<URL> r = openDialog (true);
MessageManager::callAsync ([safeThis, r] ()
{
safeThis->results = r;
if (safeThis->owner != nullptr)
safeThis->owner->exitModalState (r.size() > 0 ? 1 : 0);
});
}
static HashMap<HWND, Win32NativeFileChooser*>& getNativeDialogList()
{
static HashMap<HWND, Win32NativeFileChooser*> 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<Component> 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<FilePreviewComponent*> (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<FilePreviewComponent> 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<Win32NativeFileChooser*> (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<Win32NativeFileChooser*> (((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<LPOFNOTIFY> (lParam);
if (ofn->hdr.code == CDN_SELCHANGE)
if (auto* self = reinterpret_cast<Win32NativeFileChooser*> (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<int> 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<Native> 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<File>& 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<WCHAR> 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<int> 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<WCHAR> 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