fix macOS build (following Projucer changes made in Windows, which removed /Applications/JUCE/modules from its headers). move JUCE headers under source control, so that Windows and macOS can both build against same version of JUCE. remove AUv3 target (I think it's an iOS thing, so it will never work with this macOS fluidsynth dylib).
This commit is contained in:
114
modules/juce_gui_basics/native/juce_MultiTouchMapper.h
Normal file
114
modules/juce_gui_basics/native/juce_MultiTouchMapper.h
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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 <typename IDType>
|
||||
class MultiTouchMapper
|
||||
{
|
||||
public:
|
||||
MultiTouchMapper() {}
|
||||
|
||||
int getIndexOfTouch (ComponentPeer* peer, IDType touchID)
|
||||
{
|
||||
jassert (touchID != 0); // need to rethink this if IDs can be 0!
|
||||
TouchInfo info {touchID, peer};
|
||||
|
||||
int touchIndex = currentTouches.indexOf (info);
|
||||
|
||||
if (touchIndex < 0)
|
||||
{
|
||||
auto emptyTouchIndex = currentTouches.indexOf ({});
|
||||
touchIndex = (emptyTouchIndex >= 0 ? emptyTouchIndex : currentTouches.size());
|
||||
|
||||
currentTouches.set (touchIndex, info);
|
||||
}
|
||||
|
||||
return touchIndex;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
currentTouches.clear();
|
||||
}
|
||||
|
||||
void clearTouch (int index)
|
||||
{
|
||||
currentTouches.set (index, {});
|
||||
}
|
||||
|
||||
bool areAnyTouchesActive() const noexcept
|
||||
{
|
||||
for (auto& t : currentTouches)
|
||||
if (t.touchId != 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void deleteAllTouchesForPeer (ComponentPeer* peer)
|
||||
{
|
||||
for (auto& t : currentTouches)
|
||||
if (t.owner == peer)
|
||||
t.touchId = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct TouchInfo
|
||||
{
|
||||
TouchInfo() noexcept : touchId (0), owner (nullptr) {}
|
||||
TouchInfo (IDType idToUse, ComponentPeer* peer) noexcept : touchId (idToUse), owner (peer) {}
|
||||
|
||||
TouchInfo (const TouchInfo&) = default;
|
||||
TouchInfo& operator= (const TouchInfo&) = default;
|
||||
|
||||
// VS2013 can't default move constructors
|
||||
TouchInfo (TouchInfo&& other) noexcept : touchId (other.touchId), owner (other.owner) {}
|
||||
|
||||
// VS2013 can't default move assignments
|
||||
TouchInfo& operator= (TouchInfo&& other) noexcept
|
||||
{
|
||||
touchId = other.touchId;
|
||||
owner = other.owner;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
IDType touchId;
|
||||
ComponentPeer* owner;
|
||||
|
||||
bool operator== (const TouchInfo& o) const noexcept { return (touchId == o.touchId); }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
Array<TouchInfo> currentTouches;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiTouchMapper)
|
||||
};
|
||||
|
||||
} // namespace juce
|
846
modules/juce_gui_basics/native/juce_android_ContentSharer.cpp
Normal file
846
modules/juce_gui_basics/native/juce_android_ContentSharer.cpp
Normal file
@ -0,0 +1,846 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||||
FIELD (providers, "providers", "[Landroid/content/pm/ProviderInfo;")
|
||||
|
||||
DECLARE_JNI_CLASS (AndroidPackageInfo, "android/content/pm/PackageInfo");
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||||
FIELD (authority, "authority", "Ljava/lang/String;")
|
||||
|
||||
DECLARE_JNI_CLASS (AndroidProviderInfo, "android/content/pm/ProviderInfo");
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||||
METHOD (constructor, "<init>", "(Landroid/os/ParcelFileDescriptor;JJ)V") \
|
||||
METHOD (createInputStream, "createInputStream", "()Ljava/io/FileInputStream;") \
|
||||
METHOD (getLength, "getLength", "()J")
|
||||
|
||||
DECLARE_JNI_CLASS (AssetFileDescriptor, "android/content/res/AssetFileDescriptor");
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||||
METHOD (close, "close", "()V")
|
||||
|
||||
DECLARE_JNI_CLASS (JavaCloseable, "java/io/Closeable");
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||||
METHOD (constructor, "<init>", "(L" JUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSPATH ";JLjava/lang/String;I)V") \
|
||||
METHOD (startWatching, "startWatching", "()V") \
|
||||
METHOD (stopWatching, "stopWatching", "()V")
|
||||
|
||||
DECLARE_JNI_CLASS (JuceContentProviderFileObserver, JUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSPATH "$ProviderFileObserver");
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||||
METHOD (addRow, "addRow", "([Ljava/lang/Object;)V") \
|
||||
METHOD (constructor, "<init>", "(L" JUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSPATH ";J[Ljava/lang/String;)V")
|
||||
|
||||
DECLARE_JNI_CLASS (JuceContentProviderFileObserverCursor, JUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSPATH "$ProviderCursor");
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||||
STATICMETHOD (open, "open", "(Ljava/io/File;I)Landroid/os/ParcelFileDescriptor;")
|
||||
|
||||
DECLARE_JNI_CLASS (ParcelFileDescriptor, "android/os/ParcelFileDescriptor");
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
//==============================================================================
|
||||
class AndroidContentSharerCursor
|
||||
{
|
||||
public:
|
||||
class Owner
|
||||
{
|
||||
public:
|
||||
virtual ~Owner() {}
|
||||
|
||||
virtual void cursorClosed (const AndroidContentSharerCursor&) = 0;
|
||||
};
|
||||
|
||||
AndroidContentSharerCursor (Owner& ownerToUse, JNIEnv* env,
|
||||
const LocalRef<jobject>& contentProvider,
|
||||
const LocalRef<jobjectArray>& resultColumns)
|
||||
: owner (ownerToUse),
|
||||
cursor (GlobalRef (LocalRef<jobject> (env->NewObject (JuceContentProviderFileObserverCursor,
|
||||
JuceContentProviderFileObserverCursor.constructor,
|
||||
contentProvider.get(),
|
||||
reinterpret_cast<jlong> (this),
|
||||
resultColumns.get()))))
|
||||
{
|
||||
// the content provider must be created first
|
||||
jassert (contentProvider.get() != 0);
|
||||
}
|
||||
|
||||
jobject getNativeCursor() { return cursor.get(); }
|
||||
|
||||
void cursorClosed()
|
||||
{
|
||||
MessageManager::callAsync ([this] { owner.cursorClosed (*this); });
|
||||
}
|
||||
|
||||
private:
|
||||
Owner& owner;
|
||||
GlobalRef cursor;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AndroidContentSharerFileObserver
|
||||
{
|
||||
public:
|
||||
class Owner
|
||||
{
|
||||
public:
|
||||
virtual ~Owner() {}
|
||||
|
||||
virtual void fileHandleClosed (const AndroidContentSharerFileObserver&) = 0;
|
||||
};
|
||||
|
||||
AndroidContentSharerFileObserver (Owner& ownerToUse, JNIEnv* env,
|
||||
const LocalRef<jobject>& contentProvider,
|
||||
const String& filepathToUse)
|
||||
: owner (ownerToUse),
|
||||
filepath (filepathToUse),
|
||||
fileObserver (GlobalRef (LocalRef<jobject> (env->NewObject (JuceContentProviderFileObserver,
|
||||
JuceContentProviderFileObserver.constructor,
|
||||
contentProvider.get(),
|
||||
reinterpret_cast<jlong> (this),
|
||||
javaString (filepath).get(),
|
||||
open | access | closeWrite | closeNoWrite))))
|
||||
{
|
||||
// the content provider must be created first
|
||||
jassert (contentProvider.get() != 0);
|
||||
|
||||
env->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.startWatching);
|
||||
}
|
||||
|
||||
void onFileEvent (int event, const LocalRef<jstring>& path)
|
||||
{
|
||||
ignoreUnused (path);
|
||||
|
||||
if (event == open)
|
||||
{
|
||||
++numOpenedHandles;
|
||||
}
|
||||
else if (event == access)
|
||||
{
|
||||
fileWasRead = true;
|
||||
}
|
||||
else if (event == closeNoWrite || event == closeWrite)
|
||||
{
|
||||
--numOpenedHandles;
|
||||
|
||||
// numOpenedHandles may get negative if we don't receive open handle event.
|
||||
if (fileWasRead && numOpenedHandles <= 0)
|
||||
{
|
||||
MessageManager::callAsync ([this]
|
||||
{
|
||||
getEnv()->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.stopWatching);
|
||||
owner.fileHandleClosed (*this);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int open = 32;
|
||||
static constexpr int access = 1;
|
||||
static constexpr int closeWrite = 8;
|
||||
static constexpr int closeNoWrite = 16;
|
||||
|
||||
bool fileWasRead = false;
|
||||
int numOpenedHandles = 0;
|
||||
|
||||
Owner& owner;
|
||||
String filepath;
|
||||
GlobalRef fileObserver;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AndroidContentSharerPrepareFilesThread : private Thread
|
||||
{
|
||||
public:
|
||||
AndroidContentSharerPrepareFilesThread (AsyncUpdater& ownerToUse,
|
||||
const Array<URL>& fileUrlsToUse,
|
||||
const String& packageNameToUse,
|
||||
const String& uriBaseToUse)
|
||||
: Thread ("AndroidContentSharerPrepareFilesThread"),
|
||||
owner (ownerToUse),
|
||||
fileUrls (fileUrlsToUse),
|
||||
resultFileUris (GlobalRef (LocalRef<jobject> (getEnv()->NewObject (JavaArrayList,
|
||||
JavaArrayList.constructor,
|
||||
fileUrls.size())))),
|
||||
packageName (packageNameToUse),
|
||||
uriBase (uriBaseToUse)
|
||||
{
|
||||
startThread();
|
||||
}
|
||||
|
||||
~AndroidContentSharerPrepareFilesThread()
|
||||
{
|
||||
signalThreadShouldExit();
|
||||
waitForThreadToExit (10000);
|
||||
|
||||
for (auto& f : temporaryFilesFromAssetFiles)
|
||||
f.deleteFile();
|
||||
}
|
||||
|
||||
jobject getResultFileUris() { return resultFileUris.get(); }
|
||||
const StringArray& getMimeTypes() const { return mimeTypes; }
|
||||
const StringArray& getFilePaths() const { return filePaths; }
|
||||
|
||||
private:
|
||||
struct StreamCloser
|
||||
{
|
||||
StreamCloser (jobject streamToUse)
|
||||
: stream (GlobalRef (streamToUse))
|
||||
{
|
||||
}
|
||||
|
||||
~StreamCloser()
|
||||
{
|
||||
if (stream.get() != 0)
|
||||
getEnv()->CallVoidMethod (stream, JavaCloseable.close);
|
||||
}
|
||||
|
||||
GlobalRef stream;
|
||||
};
|
||||
|
||||
void run() override
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
bool canSpecifyMimeTypes = true;
|
||||
|
||||
for (auto f : fileUrls)
|
||||
{
|
||||
auto scheme = f.getScheme();
|
||||
|
||||
// Only "file://" scheme or no scheme (for files in app bundle) are allowed!
|
||||
jassert (scheme.isEmpty() || scheme == "file");
|
||||
|
||||
if (scheme.isEmpty())
|
||||
{
|
||||
// Raw resource names need to be all lower case
|
||||
jassert (f.toString (true).toLowerCase() == f.toString (true));
|
||||
|
||||
// This will get us a file with file:// URI
|
||||
f = copyAssetFileToTemporaryFile (env, f.toString (true));
|
||||
|
||||
if (f.isEmpty())
|
||||
continue;
|
||||
}
|
||||
|
||||
if (threadShouldExit())
|
||||
return;
|
||||
|
||||
auto filepath = URL::removeEscapeChars (f.toString (true).fromFirstOccurrenceOf ("file://", false, false));
|
||||
|
||||
filePaths.add (filepath);
|
||||
|
||||
auto filename = filepath.fromLastOccurrenceOf ("/", false, true);
|
||||
auto fileExtension = filename.fromLastOccurrenceOf (".", false, true);
|
||||
auto contentString = uriBase + String (filePaths.size() - 1) + "/" + filename;
|
||||
|
||||
auto uri = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidUri, AndroidUri.parse,
|
||||
javaString (contentString).get()));
|
||||
|
||||
if (canSpecifyMimeTypes)
|
||||
canSpecifyMimeTypes = fileExtension.isNotEmpty();
|
||||
|
||||
if (canSpecifyMimeTypes)
|
||||
mimeTypes.addArray (getMimeTypesForFileExtension (fileExtension));
|
||||
else
|
||||
mimeTypes.clear();
|
||||
|
||||
env->CallBooleanMethod (resultFileUris, JavaArrayList.add, uri.get());
|
||||
}
|
||||
|
||||
owner.triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
URL copyAssetFileToTemporaryFile (JNIEnv* env, const String& filename)
|
||||
{
|
||||
auto resources = LocalRef<jobject> (env->CallObjectMethod (android.activity, JuceAppActivity.getResources));
|
||||
int fileId = env->CallIntMethod (resources, AndroidResources.getIdentifier, javaString (filename).get(),
|
||||
javaString ("raw").get(), javaString (packageName).get());
|
||||
|
||||
// Raw resource not found. Please make sure that you include your file as a raw resource
|
||||
// and that you specify just the file name, without an extention.
|
||||
jassert (fileId != 0);
|
||||
|
||||
if (fileId == 0)
|
||||
return {};
|
||||
|
||||
auto assetFd = LocalRef<jobject> (env->CallObjectMethod (resources,
|
||||
AndroidResources.openRawResourceFd,
|
||||
fileId));
|
||||
|
||||
auto inputStream = StreamCloser (LocalRef<jobject> (env->CallObjectMethod (assetFd,
|
||||
AssetFileDescriptor.createInputStream)));
|
||||
|
||||
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||||
|
||||
if (exception != 0)
|
||||
{
|
||||
// Failed to open file stream for resource
|
||||
jassertfalse;
|
||||
|
||||
env->ExceptionClear();
|
||||
return {};
|
||||
}
|
||||
|
||||
auto tempFile = File::createTempFile ({});
|
||||
tempFile.createDirectory();
|
||||
tempFile = tempFile.getChildFile (filename);
|
||||
|
||||
auto outputStream = StreamCloser (LocalRef<jobject> (env->NewObject (JavaFileOutputStream,
|
||||
JavaFileOutputStream.constructor,
|
||||
javaString (tempFile.getFullPathName()).get())));
|
||||
|
||||
exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||||
|
||||
if (exception != 0)
|
||||
{
|
||||
// Failed to open file stream for temporary file
|
||||
jassertfalse;
|
||||
|
||||
env->ExceptionClear();
|
||||
return {};
|
||||
}
|
||||
|
||||
auto buffer = LocalRef<jbyteArray> (env->NewByteArray (1024));
|
||||
int bytesRead = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (threadShouldExit())
|
||||
return {};
|
||||
|
||||
bytesRead = env->CallIntMethod (inputStream.stream, JavaFileInputStream.read, buffer.get());
|
||||
|
||||
exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||||
|
||||
if (exception != 0)
|
||||
{
|
||||
// Failed to read from resource file.
|
||||
jassertfalse;
|
||||
|
||||
env->ExceptionClear();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (bytesRead < 0)
|
||||
break;
|
||||
|
||||
env->CallVoidMethod (outputStream.stream, JavaFileOutputStream.write, buffer.get(), 0, bytesRead);
|
||||
|
||||
if (exception != 0)
|
||||
{
|
||||
// Failed to write to temporary file.
|
||||
jassertfalse;
|
||||
|
||||
env->ExceptionClear();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
temporaryFilesFromAssetFiles.add (tempFile);
|
||||
|
||||
return URL (tempFile);
|
||||
}
|
||||
|
||||
AsyncUpdater& owner;
|
||||
Array<URL> fileUrls;
|
||||
|
||||
GlobalRef resultFileUris;
|
||||
String packageName;
|
||||
String uriBase;
|
||||
|
||||
StringArray filePaths;
|
||||
Array<File> temporaryFilesFromAssetFiles;
|
||||
StringArray mimeTypes;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class ContentSharer::ContentSharerNativeImpl : public ContentSharer::Pimpl,
|
||||
public AndroidContentSharerFileObserver::Owner,
|
||||
public AndroidContentSharerCursor::Owner,
|
||||
public AsyncUpdater,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
ContentSharerNativeImpl (ContentSharer& cs)
|
||||
: owner (cs),
|
||||
packageName (juceString (LocalRef<jstring> ((jstring) getEnv()->CallObjectMethod (android.activity,
|
||||
JuceAppActivity.getPackageName)))),
|
||||
uriBase ("content://" + packageName + ".sharingcontentprovider/")
|
||||
{
|
||||
}
|
||||
|
||||
~ContentSharerNativeImpl()
|
||||
{
|
||||
masterReference.clear();
|
||||
}
|
||||
|
||||
void shareFiles (const Array<URL>& files) override
|
||||
{
|
||||
if (! isContentSharingEnabled())
|
||||
{
|
||||
// You need to enable "Content Sharing" in Projucer's Android exporter.
|
||||
jassertfalse;
|
||||
owner.sharingFinished (false, {});
|
||||
}
|
||||
|
||||
prepareFilesThread.reset (new AndroidContentSharerPrepareFilesThread (*this, files, packageName, uriBase));
|
||||
}
|
||||
|
||||
void shareText (const String& text) override
|
||||
{
|
||||
if (! isContentSharingEnabled())
|
||||
{
|
||||
// You need to enable "Content Sharing" in Projucer's Android exporter.
|
||||
jassertfalse;
|
||||
owner.sharingFinished (false, {});
|
||||
}
|
||||
|
||||
auto* env = getEnv();
|
||||
|
||||
auto intent = LocalRef<jobject> (env->NewObject (AndroidIntent, AndroidIntent.constructor));
|
||||
env->CallObjectMethod (intent, AndroidIntent.setAction,
|
||||
javaString ("android.intent.action.SEND").get());
|
||||
env->CallObjectMethod (intent, AndroidIntent.putExtra,
|
||||
javaString ("android.intent.extra.TEXT").get(),
|
||||
javaString (text).get());
|
||||
env->CallObjectMethod (intent, AndroidIntent.setType, javaString ("text/plain").get());
|
||||
|
||||
auto chooserIntent = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidIntent, AndroidIntent.createChooser,
|
||||
intent.get(), javaString ("Choose share target").get()));
|
||||
|
||||
env->CallVoidMethod (android.activity, JuceAppActivity.startActivityForResult, chooserIntent.get(), 1003);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void cursorClosed (const AndroidContentSharerCursor& cursor) override
|
||||
{
|
||||
cursors.removeObject (&cursor);
|
||||
}
|
||||
|
||||
void fileHandleClosed (const AndroidContentSharerFileObserver&) override
|
||||
{
|
||||
decrementPendingFileCountAndNotifyOwnerIfReady();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void* openFile (const LocalRef<jobject>& contentProvider,
|
||||
const LocalRef<jobject>& uri, const LocalRef<jstring>& mode)
|
||||
{
|
||||
ignoreUnused (mode);
|
||||
|
||||
WeakReference<ContentSharerNativeImpl> weakRef (this);
|
||||
|
||||
if (weakRef == nullptr)
|
||||
return nullptr;
|
||||
|
||||
auto* env = getEnv();
|
||||
|
||||
auto uriElements = getContentUriElements (env, uri);
|
||||
|
||||
if (uriElements.filepath.isEmpty())
|
||||
return nullptr;
|
||||
|
||||
return getAssetFileDescriptor (env, contentProvider, uriElements.filepath);
|
||||
}
|
||||
|
||||
void* query (const LocalRef<jobject>& contentProvider, const LocalRef<jobject>& uri,
|
||||
const LocalRef<jobjectArray>& projection, const LocalRef<jobject>& selection,
|
||||
const LocalRef<jobjectArray>& selectionArgs, const LocalRef<jobject>& sortOrder)
|
||||
{
|
||||
ignoreUnused (selection, selectionArgs, sortOrder);
|
||||
|
||||
StringArray requestedColumns = javaStringArrayToJuce (projection);
|
||||
StringArray supportedColumns = getSupportedColumns();
|
||||
|
||||
StringArray resultColumns;
|
||||
|
||||
for (const auto& col : supportedColumns)
|
||||
{
|
||||
if (requestedColumns.contains (col))
|
||||
resultColumns.add (col);
|
||||
}
|
||||
|
||||
// Unsupported columns were queried, file sharing may fail.
|
||||
if (resultColumns.isEmpty())
|
||||
return nullptr;
|
||||
|
||||
auto resultJavaColumns = juceStringArrayToJava (resultColumns);
|
||||
|
||||
auto* env = getEnv();
|
||||
|
||||
auto cursor = cursors.add (new AndroidContentSharerCursor (*this, env, contentProvider,
|
||||
resultJavaColumns));
|
||||
|
||||
auto uriElements = getContentUriElements (env, uri);
|
||||
|
||||
if (uriElements.filepath.isEmpty())
|
||||
return cursor->getNativeCursor();
|
||||
|
||||
auto values = LocalRef<jobjectArray> (env->NewObjectArray ((jsize) resultColumns.size(),
|
||||
JavaObject, 0));
|
||||
|
||||
for (int i = 0; i < resultColumns.size(); ++i)
|
||||
{
|
||||
if (resultColumns.getReference (i) == "_display_name")
|
||||
{
|
||||
env->SetObjectArrayElement (values, i, javaString (uriElements.filename).get());
|
||||
}
|
||||
else if (resultColumns.getReference (i) == "_size")
|
||||
{
|
||||
auto javaFile = LocalRef<jobject> (env->NewObject (JavaFile, JavaFile.constructor,
|
||||
javaString (uriElements.filepath).get()));
|
||||
|
||||
jlong fileLength = env->CallLongMethod (javaFile, JavaFile.length);
|
||||
|
||||
env->SetObjectArrayElement (values, i, env->NewObject (JavaLong,
|
||||
JavaLong.constructor,
|
||||
fileLength));
|
||||
}
|
||||
}
|
||||
|
||||
auto nativeCursor = cursor->getNativeCursor();
|
||||
env->CallVoidMethod (nativeCursor, JuceContentProviderFileObserverCursor.addRow, values.get());
|
||||
|
||||
return nativeCursor;
|
||||
}
|
||||
|
||||
void* getStreamTypes (const LocalRef<jobject>& uri, const LocalRef<jstring>& mimeTypeFilter)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
auto extension = getContentUriElements (env, uri).filename.fromLastOccurrenceOf (".", false, true);
|
||||
|
||||
if (extension.isEmpty())
|
||||
return nullptr;
|
||||
|
||||
return juceStringArrayToJava (filterMimeTypes (getMimeTypesForFileExtension (extension),
|
||||
juceString (mimeTypeFilter.get())));
|
||||
}
|
||||
|
||||
void sharingFinished (int resultCode)
|
||||
{
|
||||
sharingActivityDidFinish = true;
|
||||
|
||||
succeeded = resultCode == -1;
|
||||
|
||||
// Give content sharer a chance to request file access.
|
||||
if (nonAssetFilesPendingShare.get() == 0)
|
||||
startTimer (2000);
|
||||
else
|
||||
notifyOwnerIfReady();
|
||||
}
|
||||
|
||||
private:
|
||||
bool isContentSharingEnabled() const
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
auto packageManager = LocalRef<jobject> (env->CallObjectMethod (android.activity,
|
||||
JuceAppActivity.getPackageManager));
|
||||
|
||||
constexpr int getProviders = 8;
|
||||
auto packageInfo = LocalRef<jobject> (env->CallObjectMethod (packageManager,
|
||||
AndroidPackageManager.getPackageInfo,
|
||||
javaString (packageName).get(),
|
||||
getProviders));
|
||||
auto providers = LocalRef<jobjectArray> ((jobjectArray) env->GetObjectField (packageInfo,
|
||||
AndroidPackageInfo.providers));
|
||||
|
||||
if (providers == nullptr)
|
||||
return false;
|
||||
|
||||
auto sharingContentProviderAuthority = packageName + ".sharingcontentprovider";
|
||||
const int numProviders = env->GetArrayLength (providers.get());
|
||||
|
||||
for (int i = 0; i < numProviders; ++i)
|
||||
{
|
||||
auto providerInfo = LocalRef<jobject> (env->GetObjectArrayElement (providers, i));
|
||||
auto authority = LocalRef<jstring> ((jstring) env->GetObjectField (providerInfo,
|
||||
AndroidProviderInfo.authority));
|
||||
|
||||
if (juceString (authority) == sharingContentProviderAuthority)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
jassert (prepareFilesThread != nullptr);
|
||||
|
||||
if (prepareFilesThread == nullptr)
|
||||
return;
|
||||
|
||||
filesPrepared (prepareFilesThread->getResultFileUris(), prepareFilesThread->getMimeTypes());
|
||||
}
|
||||
|
||||
void filesPrepared (jobject fileUris, const StringArray& mimeTypes)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
auto intent = LocalRef<jobject> (env->NewObject (AndroidIntent, AndroidIntent.constructor));
|
||||
env->CallObjectMethod (intent, AndroidIntent.setAction,
|
||||
javaString ("android.intent.action.SEND_MULTIPLE").get());
|
||||
|
||||
env->CallObjectMethod (intent, AndroidIntent.setType,
|
||||
javaString (getCommonMimeType (mimeTypes)).get());
|
||||
|
||||
constexpr int grantReadPermission = 1;
|
||||
env->CallObjectMethod (intent, AndroidIntent.setFlags, grantReadPermission);
|
||||
|
||||
env->CallObjectMethod (intent, AndroidIntent.putParcelableArrayListExtra,
|
||||
javaString ("android.intent.extra.STREAM").get(),
|
||||
fileUris);
|
||||
|
||||
auto chooserIntent = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidIntent,
|
||||
AndroidIntent.createChooser,
|
||||
intent.get(),
|
||||
javaString ("Choose share target").get()));
|
||||
|
||||
env->CallVoidMethod (android.activity, JuceAppActivity.startActivityForResult, chooserIntent.get(), 1003);
|
||||
}
|
||||
|
||||
void decrementPendingFileCountAndNotifyOwnerIfReady()
|
||||
{
|
||||
--nonAssetFilesPendingShare;
|
||||
|
||||
notifyOwnerIfReady();
|
||||
}
|
||||
|
||||
void notifyOwnerIfReady()
|
||||
{
|
||||
if (sharingActivityDidFinish && nonAssetFilesPendingShare.get() == 0)
|
||||
owner.sharingFinished (succeeded, {});
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
stopTimer();
|
||||
|
||||
notifyOwnerIfReady();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct ContentUriElements
|
||||
{
|
||||
String index;
|
||||
String filename;
|
||||
String filepath;
|
||||
};
|
||||
|
||||
ContentUriElements getContentUriElements (JNIEnv* env, const LocalRef<jobject>& uri) const
|
||||
{
|
||||
jassert (prepareFilesThread != nullptr);
|
||||
|
||||
if (prepareFilesThread == nullptr)
|
||||
return {};
|
||||
|
||||
auto fullUri = juceString ((jstring) env->CallObjectMethod (uri.get(), AndroidUri.toString));
|
||||
|
||||
auto index = fullUri.fromFirstOccurrenceOf (uriBase, false, false)
|
||||
.upToFirstOccurrenceOf ("/", false, true);
|
||||
|
||||
auto filename = fullUri.fromLastOccurrenceOf ("/", false, true);
|
||||
|
||||
return { index, filename, prepareFilesThread->getFilePaths()[index.getIntValue()] };
|
||||
}
|
||||
|
||||
static StringArray getSupportedColumns()
|
||||
{
|
||||
return StringArray ("_display_name", "_size");
|
||||
}
|
||||
|
||||
void* getAssetFileDescriptor (JNIEnv* env, const LocalRef<jobject>& contentProvider,
|
||||
const String& filepath)
|
||||
{
|
||||
// This function can be called from multiple threads.
|
||||
{
|
||||
const ScopedLock sl (nonAssetFileOpenLock);
|
||||
|
||||
if (! nonAssetFilePathsPendingShare.contains (filepath))
|
||||
{
|
||||
nonAssetFilePathsPendingShare.add (filepath);
|
||||
++nonAssetFilesPendingShare;
|
||||
|
||||
nonAssetFileObservers.add (new AndroidContentSharerFileObserver (*this, env,
|
||||
contentProvider,
|
||||
filepath));
|
||||
}
|
||||
}
|
||||
|
||||
auto javaFile = LocalRef<jobject> (env->NewObject (JavaFile, JavaFile.constructor,
|
||||
javaString (filepath).get()));
|
||||
|
||||
constexpr int modeReadOnly = 268435456;
|
||||
auto parcelFileDescriptor = LocalRef<jobject> (env->CallStaticObjectMethod (ParcelFileDescriptor,
|
||||
ParcelFileDescriptor.open,
|
||||
javaFile.get(), modeReadOnly));
|
||||
|
||||
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||||
|
||||
if (exception != 0)
|
||||
{
|
||||
// Failed to create file descriptor. Have you provided a valid file path/resource name?
|
||||
jassertfalse;
|
||||
|
||||
env->ExceptionClear();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jlong startOffset = 0;
|
||||
jlong unknownLength = -1;
|
||||
|
||||
assetFileDescriptors.add (GlobalRef (LocalRef<jobject> (env->NewObject (AssetFileDescriptor,
|
||||
AssetFileDescriptor.constructor,
|
||||
parcelFileDescriptor.get(),
|
||||
startOffset, unknownLength))));
|
||||
|
||||
return assetFileDescriptors.getReference (assetFileDescriptors.size() - 1).get();
|
||||
}
|
||||
|
||||
ContentSharer& owner;
|
||||
String packageName;
|
||||
String uriBase;
|
||||
|
||||
std::unique_ptr<AndroidContentSharerPrepareFilesThread> prepareFilesThread;
|
||||
|
||||
bool succeeded = false;
|
||||
String errorDescription;
|
||||
|
||||
bool sharingActivityDidFinish = false;
|
||||
|
||||
OwnedArray<AndroidContentSharerCursor> cursors;
|
||||
|
||||
Array<GlobalRef> assetFileDescriptors;
|
||||
|
||||
CriticalSection nonAssetFileOpenLock;
|
||||
StringArray nonAssetFilePathsPendingShare;
|
||||
Atomic<int> nonAssetFilesPendingShare { 0 };
|
||||
OwnedArray<AndroidContentSharerFileObserver> nonAssetFileObservers;
|
||||
|
||||
WeakReference<ContentSharerNativeImpl>::Master masterReference;
|
||||
friend class WeakReference<ContentSharerNativeImpl>;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
ContentSharer::Pimpl* ContentSharer::createPimpl()
|
||||
{
|
||||
return new ContentSharerNativeImpl (*this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void* juce_contentSharerQuery (void* contentProvider, void* uri, void* projection,
|
||||
void* selection, void* selectionArgs, void* sortOrder)
|
||||
{
|
||||
auto* pimpl = (ContentSharer::ContentSharerNativeImpl*) ContentSharer::getInstance()->pimpl.get();
|
||||
return pimpl->query (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
|
||||
LocalRef<jobject> (static_cast<jobject> (uri)),
|
||||
LocalRef<jobjectArray> (static_cast<jobjectArray> (projection)),
|
||||
LocalRef<jobject> (static_cast<jobject> (selection)),
|
||||
LocalRef<jobjectArray> (static_cast<jobjectArray> (selectionArgs)),
|
||||
LocalRef<jobject> (static_cast<jobject> (sortOrder)));
|
||||
}
|
||||
|
||||
void* juce_contentSharerOpenFile (void* contentProvider, void* uri, void* mode)
|
||||
{
|
||||
auto* pimpl = (ContentSharer::ContentSharerNativeImpl*) ContentSharer::getInstance()->pimpl.get();
|
||||
return pimpl->openFile (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
|
||||
LocalRef<jobject> (static_cast<jobject> (uri)),
|
||||
LocalRef<jstring> (static_cast<jstring> (mode)));
|
||||
}
|
||||
|
||||
void juce_contentSharingCompleted (int resultCode)
|
||||
{
|
||||
auto* pimpl = (ContentSharer::ContentSharerNativeImpl*) ContentSharer::getInstance()->pimpl.get();
|
||||
return pimpl->sharingFinished (resultCode);
|
||||
}
|
||||
|
||||
void* juce_contentSharerGetStreamTypes (void* uri, void* mimeTypeFilter)
|
||||
{
|
||||
auto* pimpl = (ContentSharer::ContentSharerNativeImpl*) ContentSharer::getInstance()->pimpl.get();
|
||||
return pimpl->getStreamTypes (LocalRef<jobject> (static_cast<jobject> (uri)),
|
||||
LocalRef<jstring> (static_cast<jstring> (mimeTypeFilter)));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
JUCE_JNI_CALLBACK (JUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSNAME, contentSharerFileObserverEvent, void,
|
||||
(JNIEnv* env, jobject /*fileObserver*/, jlong host, int event, jstring path))
|
||||
{
|
||||
setEnv (env);
|
||||
|
||||
reinterpret_cast<AndroidContentSharerFileObserver*> (host)->onFileEvent (event, LocalRef<jstring> (path));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
JUCE_JNI_CALLBACK (JUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSNAME, contentSharerQuery, jobject,
|
||||
(JNIEnv* env, jobject contentProvider, jobject uri, jobjectArray projection,
|
||||
jobject selection, jobjectArray selectionArgs, jobject sortOrder))
|
||||
{
|
||||
setEnv (env);
|
||||
|
||||
return (jobject) juce_contentSharerQuery (contentProvider, uri, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
JUCE_JNI_CALLBACK (JUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSNAME, contentSharerCursorClosed, void,
|
||||
(JNIEnv* env, jobject /*cursor*/, jlong host))
|
||||
{
|
||||
setEnv (env);
|
||||
|
||||
reinterpret_cast<AndroidContentSharerCursor*> (host)->cursorClosed();
|
||||
}
|
||||
|
||||
JUCE_JNI_CALLBACK (JUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSNAME, contentSharerOpenFile, jobject,
|
||||
(JNIEnv* env, jobject contentProvider, jobject uri, jstring mode))
|
||||
{
|
||||
setEnv (env);
|
||||
|
||||
return (jobject) juce_contentSharerOpenFile ((void*) contentProvider, (void*) uri, (void*) mode);
|
||||
}
|
||||
|
||||
JUCE_JNI_CALLBACK (JUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSNAME, contentSharerGetStreamTypes, jobject,
|
||||
(JNIEnv* env, jobject /*contentProvider*/, jobject uri, jstring mimeTypeFilter))
|
||||
{
|
||||
setEnv (env);
|
||||
|
||||
return (jobject) juce_contentSharerGetStreamTypes ((void*) uri, (void*) mimeTypeFilter);
|
||||
}
|
||||
|
||||
} // namespace juce
|
228
modules/juce_gui_basics/native/juce_android_FileChooser.cpp
Normal file
228
modules/juce_gui_basics/native/juce_android_FileChooser.cpp
Normal file
@ -0,0 +1,228 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
class FileChooser::Native : public FileChooser::Pimpl
|
||||
{
|
||||
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 save a directory
|
||||
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 (AndroidUri, AndroidUri.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[i].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
|
1184
modules/juce_gui_basics/native/juce_android_Windowing.cpp
Normal file
1184
modules/juce_gui_basics/native/juce_android_Windowing.cpp
Normal file
File diff suppressed because it is too large
Load Diff
735
modules/juce_gui_basics/native/juce_common_MimeTypes.cpp
Normal file
735
modules/juce_gui_basics/native/juce_common_MimeTypes.cpp
Normal file
@ -0,0 +1,735 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static StringArray filterMimeTypes (const StringArray& mimeTypes, const String& filter)
|
||||
{
|
||||
String filterToUse (filter.removeCharacters ("*"));
|
||||
|
||||
if (filterToUse.isEmpty() || filterToUse == "/")
|
||||
return mimeTypes;
|
||||
|
||||
StringArray result;
|
||||
|
||||
for (const auto& type : mimeTypes)
|
||||
if (String (type).contains (filterToUse))
|
||||
result.add (type);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static String getCommonMimeType (const StringArray& mimeTypes)
|
||||
{
|
||||
if (mimeTypes.isEmpty())
|
||||
return "*/*";
|
||||
|
||||
auto commonMime = mimeTypes[0];
|
||||
bool lookForCommonGroup = false;
|
||||
|
||||
for (int i = 1; i < mimeTypes.size(); ++i)
|
||||
{
|
||||
if (mimeTypes[i] == commonMime)
|
||||
continue;
|
||||
|
||||
if (! lookForCommonGroup)
|
||||
{
|
||||
lookForCommonGroup = true;
|
||||
commonMime = commonMime.upToFirstOccurrenceOf ("/", true, false);
|
||||
}
|
||||
|
||||
if (! mimeTypes[i].startsWith (commonMime))
|
||||
return "*/*";
|
||||
}
|
||||
|
||||
return lookForCommonGroup ? commonMime + "*" : commonMime;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
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
|
210
modules/juce_gui_basics/native/juce_ios_ContentSharer.cpp
Normal file
210
modules/juce_gui_basics/native/juce_ios_ContentSharer.cpp
Normal file
@ -0,0 +1,210 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
class ContentSharer::ContentSharerNativeImpl : public ContentSharer::Pimpl,
|
||||
private Component
|
||||
{
|
||||
public:
|
||||
ContentSharerNativeImpl (ContentSharer& cs)
|
||||
: owner (cs)
|
||||
{
|
||||
static PopoverDelegateClass cls;
|
||||
popoverDelegate.reset ([cls.createInstance() init]);
|
||||
}
|
||||
|
||||
~ContentSharerNativeImpl()
|
||||
{
|
||||
exitModalState (0);
|
||||
}
|
||||
|
||||
void shareFiles (const Array<URL>& files) override
|
||||
{
|
||||
auto* urls = [NSMutableArray arrayWithCapacity: (NSUInteger) files.size()];
|
||||
|
||||
for (const auto& f : files)
|
||||
{
|
||||
NSString* nativeFilePath = nil;
|
||||
|
||||
if (f.isLocalFile())
|
||||
{
|
||||
nativeFilePath = juceStringToNS (f.getLocalFile().getFullPathName());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto filePath = f.toString (false);
|
||||
|
||||
auto* fileDirectory = filePath.contains ("/")
|
||||
? juceStringToNS (filePath.upToLastOccurrenceOf ("/", false, false))
|
||||
: [NSString string];
|
||||
|
||||
auto fileName = juceStringToNS (filePath.fromLastOccurrenceOf ("/", false, false)
|
||||
.upToLastOccurrenceOf (".", false, false));
|
||||
|
||||
auto fileExt = juceStringToNS (filePath.fromLastOccurrenceOf (".", false, false));
|
||||
|
||||
if ([fileDirectory length] == NSUInteger (0))
|
||||
nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName
|
||||
ofType: fileExt];
|
||||
else
|
||||
nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName
|
||||
ofType: fileExt
|
||||
inDirectory: fileDirectory];
|
||||
}
|
||||
|
||||
if (nativeFilePath != nil)
|
||||
[urls addObject: [NSURL fileURLWithPath: nativeFilePath]];
|
||||
}
|
||||
|
||||
share (urls);
|
||||
}
|
||||
|
||||
void shareText (const String& text) override
|
||||
{
|
||||
auto* array = [NSArray arrayWithObject: juceStringToNS (text)];
|
||||
share (array);
|
||||
}
|
||||
|
||||
private:
|
||||
void share (NSArray* items)
|
||||
{
|
||||
if ([items count] == 0)
|
||||
{
|
||||
jassertfalse;
|
||||
owner.sharingFinished (false, "No valid items found for sharing.");
|
||||
return;
|
||||
}
|
||||
|
||||
controller.reset ([[UIActivityViewController alloc] initWithActivityItems: items
|
||||
applicationActivities: nil]);
|
||||
|
||||
controller.get().excludedActivityTypes = nil;
|
||||
|
||||
controller.get().completionWithItemsHandler = ^ (UIActivityType type, BOOL completed,
|
||||
NSArray* returnedItems, NSError* error)
|
||||
{
|
||||
ignoreUnused (type);
|
||||
ignoreUnused (returnedItems);
|
||||
|
||||
succeeded = completed;
|
||||
|
||||
if (error != nil)
|
||||
errorDescription = nsStringToJuce ([error localizedDescription]);
|
||||
|
||||
exitModalState (0);
|
||||
};
|
||||
|
||||
controller.get().modalTransitionStyle = UIModalTransitionStyleCoverVertical;
|
||||
|
||||
auto bounds = Desktop::getInstance().getDisplays().getMainDisplay().userArea;
|
||||
setBounds (bounds);
|
||||
|
||||
setAlwaysOnTop (true);
|
||||
addToDesktop (0);
|
||||
|
||||
enterModalState (true,
|
||||
ModalCallbackFunction::create ([this] (int)
|
||||
{
|
||||
owner.sharingFinished (succeeded, errorDescription);
|
||||
}),
|
||||
false);
|
||||
}
|
||||
|
||||
static bool isIPad()
|
||||
{
|
||||
return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void parentHierarchyChanged() override
|
||||
{
|
||||
auto* newPeer = dynamic_cast<UIViewComponentPeer*> (getPeer());
|
||||
|
||||
if (peer != newPeer)
|
||||
{
|
||||
peer = newPeer;
|
||||
|
||||
if (isIPad())
|
||||
{
|
||||
controller.get().preferredContentSize = peer->view.frame.size;
|
||||
|
||||
auto screenBounds = [UIScreen mainScreen].bounds;
|
||||
|
||||
auto* popoverController = controller.get().popoverPresentationController;
|
||||
popoverController.sourceView = peer->view;
|
||||
popoverController.sourceRect = CGRectMake (0.f, screenBounds.size.height - 10.f, screenBounds.size.width, 10.f);
|
||||
popoverController.canOverlapSourceViewRect = YES;
|
||||
popoverController.delegate = popoverDelegate.get();
|
||||
}
|
||||
|
||||
if (auto* parentController = peer->controller)
|
||||
[parentController showViewController: controller.get() sender: parentController];
|
||||
|
||||
if (peer->view.window != nil)
|
||||
peer->view.window.autoresizesSubviews = YES;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct PopoverDelegateClass : public ObjCClass<NSObject<UIPopoverPresentationControllerDelegate>>
|
||||
{
|
||||
PopoverDelegateClass() : ObjCClass<NSObject<UIPopoverPresentationControllerDelegate>> ("PopoverDelegateClass_")
|
||||
{
|
||||
addMethod (@selector (popoverPresentationController:willRepositionPopoverToRect:inView:), willRepositionPopover, "v@:@@@");
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static void willRepositionPopover (id, SEL, UIPopoverPresentationController*, CGRect* rect, UIView*)
|
||||
{
|
||||
auto screenBounds = [UIScreen mainScreen].bounds;
|
||||
|
||||
rect->origin.x = 0.f;
|
||||
rect->origin.y = screenBounds.size.height - 10.f;
|
||||
rect->size.width = screenBounds.size.width;
|
||||
rect->size.height = 10.f;
|
||||
}
|
||||
};
|
||||
|
||||
ContentSharer& owner;
|
||||
UIViewComponentPeer* peer = nullptr;
|
||||
std::unique_ptr<UIActivityViewController, NSObjectDeleter> controller;
|
||||
std::unique_ptr<NSObject<UIPopoverPresentationControllerDelegate>, NSObjectDeleter> popoverDelegate;
|
||||
|
||||
bool succeeded = false;
|
||||
String errorDescription;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
ContentSharer::Pimpl* ContentSharer::createPimpl()
|
||||
{
|
||||
return new ContentSharerNativeImpl (*this);
|
||||
}
|
||||
|
||||
} // namespace juce
|
321
modules/juce_gui_basics/native/juce_ios_FileChooser.mm
Normal file
321
modules/juce_gui_basics/native/juce_ios_FileChooser.mm
Normal file
@ -0,0 +1,321 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
class FileChooser::Native : private Component,
|
||||
public FileChooser::Pimpl
|
||||
{
|
||||
public:
|
||||
Native (FileChooser& fileChooser, int flags)
|
||||
: owner (fileChooser)
|
||||
{
|
||||
String firstFileExtension;
|
||||
|
||||
static FileChooserDelegateClass cls;
|
||||
delegate.reset ([cls.createInstance() init]);
|
||||
FileChooserDelegateClass::setOwner (delegate.get(), this);
|
||||
|
||||
auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters, firstFileExtension));
|
||||
|
||||
if ((flags & FileBrowserComponent::saveMode) != 0)
|
||||
{
|
||||
auto currentFileOrDirectory = owner.startingFile;
|
||||
|
||||
UIDocumentPickerMode pickerMode = currentFileOrDirectory.existsAsFile()
|
||||
? UIDocumentPickerModeExportToService
|
||||
: UIDocumentPickerModeMoveToService;
|
||||
|
||||
if (! currentFileOrDirectory.existsAsFile())
|
||||
{
|
||||
auto filename = getFilename (currentFileOrDirectory, firstFileExtension);
|
||||
auto tmpDirectory = File::createTempFile ("JUCE-filepath");
|
||||
|
||||
if (tmpDirectory.createDirectory().wasOk())
|
||||
{
|
||||
currentFileOrDirectory = tmpDirectory.getChildFile (filename);
|
||||
currentFileOrDirectory.replaceWithText ("");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Temporary directory creation failed! You need to specify a
|
||||
// path you have write access to. Saving will not work for
|
||||
// current path.
|
||||
jassertfalse;
|
||||
}
|
||||
}
|
||||
|
||||
auto url = [[NSURL alloc] initFileURLWithPath: juceStringToNS (currentFileOrDirectory.getFullPathName())];
|
||||
controller.reset ([[UIDocumentPickerViewController alloc] initWithURL: url
|
||||
inMode: pickerMode]);
|
||||
[url release];
|
||||
}
|
||||
else
|
||||
{
|
||||
controller.reset ([[UIDocumentPickerViewController alloc] initWithDocumentTypes: utTypeArray
|
||||
inMode: UIDocumentPickerModeOpen]);
|
||||
}
|
||||
|
||||
[controller.get() setDelegate: delegate.get()];
|
||||
[controller.get() 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.get() sender: parentController];
|
||||
|
||||
if (peer->view.window != nil)
|
||||
peer->view.window.autoresizesSubviews = YES;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static StringArray getUTTypesForWildcards (const String& filterWildcards, String& firstExtension)
|
||||
{
|
||||
auto filters = StringArray::fromTokens (filterWildcards, ";", "");
|
||||
StringArray result;
|
||||
|
||||
firstExtension = {};
|
||||
|
||||
if (! filters.contains ("*") && filters.size() > 0)
|
||||
{
|
||||
for (auto filter : filters)
|
||||
{
|
||||
if (filter.isEmpty())
|
||||
continue;
|
||||
|
||||
// iOS only supports file extension wild cards
|
||||
jassert (filter.upToLastOccurrenceOf (".", true, false) == "*.");
|
||||
|
||||
auto fileExtension = filter.fromLastOccurrenceOf (".", false, false);
|
||||
auto fileExtensionCF = fileExtension.toCFString();
|
||||
|
||||
if (firstExtension.isEmpty())
|
||||
firstExtension = fileExtension;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static String getFilename (const File& path, const String& fallbackExtension)
|
||||
{
|
||||
auto filename = path.getFileNameWithoutExtension();
|
||||
auto extension = path.getFileExtension().substring (1);
|
||||
|
||||
if (filename.isEmpty())
|
||||
filename = "Untitled";
|
||||
|
||||
if (extension.isEmpty())
|
||||
extension = fallbackExtension;
|
||||
|
||||
if (extension.isNotEmpty())
|
||||
filename += "." + extension;
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void didPickDocumentAtURL (NSURL* url)
|
||||
{
|
||||
bool isWriting = controller.get().documentPickerMode == UIDocumentPickerModeExportToService
|
||||
| controller.get().documentPickerMode == UIDocumentPickerModeMoveToService;
|
||||
|
||||
NSUInteger accessOptions = isWriting ? 0 : NSFileCoordinatorReadingWithoutChanges;
|
||||
|
||||
auto* fileAccessIntent = isWriting
|
||||
? [NSFileAccessIntent writingIntentWithURL: url options: accessOptions]
|
||||
: [NSFileAccessIntent readingIntentWithURL: url options: accessOptions];
|
||||
|
||||
NSArray<NSFileAccessIntent*>* intents = @[fileAccessIntent];
|
||||
|
||||
auto* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter: nil];
|
||||
|
||||
[fileCoordinator coordinateAccessWithIntents: intents queue: [NSOperationQueue mainQueue] byAccessor: ^(NSError* err)
|
||||
{
|
||||
Array<URL> chooserResults;
|
||||
|
||||
if (err == nil)
|
||||
{
|
||||
[url startAccessingSecurityScopedResource];
|
||||
|
||||
NSError* error = nil;
|
||||
|
||||
NSData* bookmark = [url bookmarkDataWithOptions: 0
|
||||
includingResourceValuesForKeys: nil
|
||||
relativeToURL: nil
|
||||
error: &error];
|
||||
|
||||
[bookmark retain];
|
||||
|
||||
[url stopAccessingSecurityScopedResource];
|
||||
|
||||
URL juceUrl (nsStringToJuce ([url absoluteString]));
|
||||
|
||||
if (error == nil)
|
||||
{
|
||||
setURLBookmark (juceUrl, (void*) bookmark);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto* desc = [error localizedDescription];
|
||||
ignoreUnused (desc);
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
chooserResults.add (juceUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto* desc = [err localizedDescription];
|
||||
ignoreUnused (desc);
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
owner.finished (chooserResults);
|
||||
}];
|
||||
}
|
||||
|
||||
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;
|
||||
std::unique_ptr<NSObject<UIDocumentPickerDelegate>, NSObjectDeleter> delegate;
|
||||
std::unique_ptr<UIDocumentPickerViewController, NSObjectDeleter> 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
|
1163
modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm
Normal file
1163
modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm
Normal file
File diff suppressed because it is too large
Load Diff
753
modules/juce_gui_basics/native/juce_ios_Windowing.mm
Normal file
753
modules/juce_gui_basics/native/juce_ios_Windowing.mm
Normal file
@ -0,0 +1,753 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
extern bool isIOSAppActive;
|
||||
|
||||
struct AppInactivityCallback // NB: careful, this declaration is duplicated in other modules
|
||||
{
|
||||
virtual ~AppInactivityCallback() {}
|
||||
virtual void appBecomingInactive() = 0;
|
||||
};
|
||||
|
||||
// This is an internal list of callbacks (but currently used between modules)
|
||||
Array<AppInactivityCallback*> appBecomingInactiveCallbacks;
|
||||
}
|
||||
|
||||
#if JUCE_PUSH_NOTIFICATIONS && defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
|
||||
@interface JuceAppStartupDelegate : NSObject <UIApplicationDelegate, UNUserNotificationCenterDelegate>
|
||||
#else
|
||||
@interface JuceAppStartupDelegate : NSObject <UIApplicationDelegate>
|
||||
#endif
|
||||
{
|
||||
UIBackgroundTaskIdentifier appSuspendTask;
|
||||
}
|
||||
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
- (id)init;
|
||||
- (void) applicationDidFinishLaunching: (UIApplication*) application;
|
||||
- (void) applicationWillTerminate: (UIApplication*) application;
|
||||
- (void) applicationDidEnterBackground: (UIApplication*) application;
|
||||
- (void) applicationWillEnterForeground: (UIApplication*) application;
|
||||
- (void) applicationDidBecomeActive: (UIApplication*) application;
|
||||
- (void) applicationWillResignActive: (UIApplication*) application;
|
||||
- (void) application: (UIApplication*) application handleEventsForBackgroundURLSession: (NSString*) identifier
|
||||
completionHandler: (void (^)(void)) completionHandler;
|
||||
- (void) applicationDidReceiveMemoryWarning: (UIApplication *) application;
|
||||
#if JUCE_PUSH_NOTIFICATIONS
|
||||
- (void) application: (UIApplication*) application didRegisterUserNotificationSettings: (UIUserNotificationSettings*) notificationSettings;
|
||||
- (void) application: (UIApplication*) application didRegisterForRemoteNotificationsWithDeviceToken: (NSData*) deviceToken;
|
||||
- (void) application: (UIApplication*) application didFailToRegisterForRemoteNotificationsWithError: (NSError*) error;
|
||||
- (void) application: (UIApplication*) application didReceiveRemoteNotification: (NSDictionary*) userInfo;
|
||||
- (void) application: (UIApplication*) application didReceiveRemoteNotification: (NSDictionary*) userInfo
|
||||
fetchCompletionHandler: (void (^)(UIBackgroundFetchResult result)) completionHandler;
|
||||
- (void) application: (UIApplication*) application handleActionWithIdentifier: (NSString*) identifier
|
||||
forRemoteNotification: (NSDictionary*) userInfo withResponseInfo: (NSDictionary*) responseInfo
|
||||
completionHandler: (void(^)()) completionHandler;
|
||||
- (void) application: (UIApplication*) application didReceiveLocalNotification: (UILocalNotification*) notification;
|
||||
- (void) application: (UIApplication*) application handleActionWithIdentifier: (NSString*) identifier
|
||||
forLocalNotification: (UILocalNotification*) notification completionHandler: (void(^)()) completionHandler;
|
||||
- (void) application: (UIApplication*) application handleActionWithIdentifier: (NSString*) identifier
|
||||
forLocalNotification: (UILocalNotification*) notification withResponseInfo: (NSDictionary*) responseInfo
|
||||
completionHandler: (void(^)()) completionHandler;
|
||||
#if defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
|
||||
- (void) userNotificationCenter: (UNUserNotificationCenter*) center willPresentNotification: (UNNotification*) notification
|
||||
withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler;
|
||||
- (void) userNotificationCenter: (UNUserNotificationCenter*) center didReceiveNotificationResponse: (UNNotificationResponse*) response
|
||||
withCompletionHandler: (void(^)())completionHandler;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@implementation JuceAppStartupDelegate
|
||||
|
||||
NSObject* _pushNotificationsDelegate;
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
appSuspendTask = UIBackgroundTaskInvalid;
|
||||
|
||||
#if JUCE_PUSH_NOTIFICATIONS && defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
|
||||
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
|
||||
#endif
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) applicationDidFinishLaunching: (UIApplication*) application
|
||||
{
|
||||
ignoreUnused (application);
|
||||
initialiseJuce_GUI();
|
||||
|
||||
if (auto* app = JUCEApplicationBase::createInstance())
|
||||
{
|
||||
if (! app->initialiseApp())
|
||||
exit (app->shutdownApp());
|
||||
}
|
||||
else
|
||||
{
|
||||
jassertfalse; // you must supply an application object for an iOS app!
|
||||
}
|
||||
}
|
||||
|
||||
- (void) applicationWillTerminate: (UIApplication*) application
|
||||
{
|
||||
ignoreUnused (application);
|
||||
JUCEApplicationBase::appWillTerminateByForce();
|
||||
}
|
||||
|
||||
- (void) applicationDidEnterBackground: (UIApplication*) application
|
||||
{
|
||||
if (auto* app = JUCEApplicationBase::getInstance())
|
||||
{
|
||||
#if JUCE_EXECUTE_APP_SUSPEND_ON_BACKGROUND_TASK
|
||||
appSuspendTask = [application beginBackgroundTaskWithName:@"JUCE Suspend Task" expirationHandler:^{
|
||||
if (appSuspendTask != UIBackgroundTaskInvalid)
|
||||
{
|
||||
[application endBackgroundTask:appSuspendTask];
|
||||
appSuspendTask = UIBackgroundTaskInvalid;
|
||||
}
|
||||
}];
|
||||
|
||||
MessageManager::callAsync ([app] { app->suspended(); });
|
||||
#else
|
||||
ignoreUnused (application);
|
||||
app->suspended();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (void) applicationWillEnterForeground: (UIApplication*) application
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
if (auto* app = JUCEApplicationBase::getInstance())
|
||||
app->resumed();
|
||||
}
|
||||
|
||||
- (void) applicationDidBecomeActive: (UIApplication*) application
|
||||
{
|
||||
application.applicationIconBadgeNumber = 0;
|
||||
|
||||
isIOSAppActive = true;
|
||||
}
|
||||
|
||||
- (void) applicationWillResignActive: (UIApplication*) application
|
||||
{
|
||||
ignoreUnused (application);
|
||||
isIOSAppActive = false;
|
||||
|
||||
for (int i = appBecomingInactiveCallbacks.size(); --i >= 0;)
|
||||
appBecomingInactiveCallbacks.getReference(i)->appBecomingInactive();
|
||||
}
|
||||
|
||||
- (void) application: (UIApplication*) application handleEventsForBackgroundURLSession: (NSString*)identifier
|
||||
completionHandler: (void (^)(void))completionHandler
|
||||
{
|
||||
ignoreUnused (application);
|
||||
URL::DownloadTask::juce_iosURLSessionNotify (nsStringToJuce (identifier));
|
||||
completionHandler();
|
||||
}
|
||||
|
||||
- (void) applicationDidReceiveMemoryWarning: (UIApplication*) application
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
if (auto* app = JUCEApplicationBase::getInstance())
|
||||
app->memoryWarningReceived();
|
||||
}
|
||||
|
||||
- (void) setPushNotificationsDelegateToUse: (NSObject*) delegate
|
||||
{
|
||||
_pushNotificationsDelegate = delegate;
|
||||
}
|
||||
|
||||
#if JUCE_PUSH_NOTIFICATIONS
|
||||
- (void) application: (UIApplication*) application didRegisterUserNotificationSettings: (UIUserNotificationSettings*) notificationSettings
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"application:didRegisterUserNotificationSettings:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: &application atIndex:2];
|
||||
[invocation setArgument: ¬ificationSettings atIndex:3];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) application: (UIApplication*) application didRegisterForRemoteNotificationsWithDeviceToken: (NSData*) deviceToken
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"application:didRegisterForRemoteNotificationsWithDeviceToken:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: &application atIndex:2];
|
||||
[invocation setArgument: &deviceToken atIndex:3];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) application: (UIApplication*) application didFailToRegisterForRemoteNotificationsWithError: (NSError*) error
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"application:didFailToRegisterForRemoteNotificationsWithError:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: &application atIndex:2];
|
||||
[invocation setArgument: &error atIndex:3];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) application: (UIApplication*) application didReceiveRemoteNotification: (NSDictionary*) userInfo
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"application:didReceiveRemoteNotification:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: &application atIndex:2];
|
||||
[invocation setArgument: &userInfo atIndex:3];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) application: (UIApplication*) application didReceiveRemoteNotification: (NSDictionary*) userInfo
|
||||
fetchCompletionHandler: (void (^)(UIBackgroundFetchResult result)) completionHandler
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"application:didReceiveRemoteNotification:fetchCompletionHandler:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: &application atIndex:2];
|
||||
[invocation setArgument: &userInfo atIndex:3];
|
||||
[invocation setArgument: &completionHandler atIndex:4];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) application: (UIApplication*) application handleActionWithIdentifier: (NSString*) identifier
|
||||
forRemoteNotification: (NSDictionary*) userInfo withResponseInfo: (NSDictionary*) responseInfo
|
||||
completionHandler: (void(^)()) completionHandler
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"application:handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: &application atIndex:2];
|
||||
[invocation setArgument: &identifier atIndex:3];
|
||||
[invocation setArgument: &userInfo atIndex:4];
|
||||
[invocation setArgument: &responseInfo atIndex:5];
|
||||
[invocation setArgument: &completionHandler atIndex:6];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) application: (UIApplication*) application didReceiveLocalNotification: (UILocalNotification*) notification
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"application:didReceiveLocalNotification:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: &application atIndex:2];
|
||||
[invocation setArgument: ¬ification atIndex:3];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) application: (UIApplication*) application handleActionWithIdentifier: (NSString*) identifier
|
||||
forLocalNotification: (UILocalNotification*) notification completionHandler: (void(^)()) completionHandler
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"application:handleActionWithIdentifier:forLocalNotification:completionHandler:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: &application atIndex:2];
|
||||
[invocation setArgument: &identifier atIndex:3];
|
||||
[invocation setArgument: ¬ification atIndex:4];
|
||||
[invocation setArgument: &completionHandler atIndex:5];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) application: (UIApplication*) application handleActionWithIdentifier: (NSString*) identifier
|
||||
forLocalNotification: (UILocalNotification*) notification withResponseInfo: (NSDictionary*) responseInfo
|
||||
completionHandler: (void(^)()) completionHandler
|
||||
{
|
||||
ignoreUnused (application);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"application:handleActionWithIdentifier:forLocalNotification:withResponseInfo:completionHandler:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: &application atIndex:2];
|
||||
[invocation setArgument: &identifier atIndex:3];
|
||||
[invocation setArgument: ¬ification atIndex:4];
|
||||
[invocation setArgument: &responseInfo atIndex:5];
|
||||
[invocation setArgument: &completionHandler atIndex:6];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
#if defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
|
||||
- (void) userNotificationCenter: (UNUserNotificationCenter*) center willPresentNotification: (UNNotification*) notification
|
||||
withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler
|
||||
{
|
||||
ignoreUnused (center);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"userNotificationCenter:willPresentNotification:withCompletionHandler:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: ¢er atIndex:2];
|
||||
[invocation setArgument: ¬ification atIndex:3];
|
||||
[invocation setArgument: &completionHandler atIndex:4];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) userNotificationCenter: (UNUserNotificationCenter*) center didReceiveNotificationResponse: (UNNotificationResponse*) response
|
||||
withCompletionHandler: (void(^)()) completionHandler
|
||||
{
|
||||
ignoreUnused (center);
|
||||
|
||||
SEL selector = NSSelectorFromString (@"userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:");
|
||||
|
||||
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
|
||||
{
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
|
||||
[invocation setSelector: selector];
|
||||
[invocation setTarget: _pushNotificationsDelegate];
|
||||
[invocation setArgument: ¢er atIndex:2];
|
||||
[invocation setArgument: &response atIndex:3];
|
||||
[invocation setArgument: &completionHandler atIndex:4];
|
||||
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
int juce_iOSMain (int argc, const char* argv[], void* customDelegatePtr);
|
||||
int juce_iOSMain (int argc, const char* argv[], void* customDelegatePtr)
|
||||
{
|
||||
Class delegateClass = (customDelegatePtr != nullptr ? reinterpret_cast<Class> (customDelegatePtr) : [JuceAppStartupDelegate class]);
|
||||
|
||||
return UIApplicationMain (argc, const_cast<char**> (argv), nil, NSStringFromClass (delegateClass));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void LookAndFeel::playAlertSound()
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
class iOSMessageBox;
|
||||
|
||||
#if defined (__IPHONE_8_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_8_0
|
||||
#define JUCE_USE_NEW_IOS_ALERTWINDOW 1
|
||||
#endif
|
||||
|
||||
#if ! JUCE_USE_NEW_IOS_ALERTWINDOW
|
||||
} // (juce namespace)
|
||||
|
||||
@interface JuceAlertBoxDelegate : NSObject <UIAlertViewDelegate>
|
||||
{
|
||||
@public
|
||||
iOSMessageBox* owner;
|
||||
}
|
||||
|
||||
- (void) alertView: (UIAlertView*) alertView clickedButtonAtIndex: (NSInteger) buttonIndex;
|
||||
|
||||
@end
|
||||
|
||||
namespace juce
|
||||
{
|
||||
#endif
|
||||
|
||||
|
||||
class iOSMessageBox
|
||||
{
|
||||
public:
|
||||
iOSMessageBox (const String& title, const String& message,
|
||||
NSString* button1, NSString* button2, NSString* button3,
|
||||
ModalComponentManager::Callback* cb, const bool async)
|
||||
: result (0), resultReceived (false), callback (cb), isAsync (async)
|
||||
{
|
||||
#if JUCE_USE_NEW_IOS_ALERTWINDOW
|
||||
if (currentlyFocusedPeer != nullptr)
|
||||
{
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle: juceStringToNS (title)
|
||||
message: juceStringToNS (message)
|
||||
preferredStyle: UIAlertControllerStyleAlert];
|
||||
addButton (alert, button1, 0);
|
||||
addButton (alert, button2, 1);
|
||||
addButton (alert, button3, 2);
|
||||
|
||||
[currentlyFocusedPeer->controller presentViewController: alert
|
||||
animated: YES
|
||||
completion: nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Since iOS8, alert windows need to be associated with a window, so you need to
|
||||
// have at least one window on screen when you use this
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
#else
|
||||
delegate = [[JuceAlertBoxDelegate alloc] init];
|
||||
delegate->owner = this;
|
||||
|
||||
alert = [[UIAlertView alloc] initWithTitle: juceStringToNS (title)
|
||||
message: juceStringToNS (message)
|
||||
delegate: delegate
|
||||
cancelButtonTitle: button1
|
||||
otherButtonTitles: button2, button3, nil];
|
||||
[alert retain];
|
||||
[alert show];
|
||||
#endif
|
||||
}
|
||||
|
||||
~iOSMessageBox()
|
||||
{
|
||||
#if ! JUCE_USE_NEW_IOS_ALERTWINDOW
|
||||
[alert release];
|
||||
[delegate release];
|
||||
#endif
|
||||
}
|
||||
|
||||
int getResult()
|
||||
{
|
||||
jassert (callback == nullptr);
|
||||
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
#if JUCE_USE_NEW_IOS_ALERTWINDOW
|
||||
while (! resultReceived)
|
||||
#else
|
||||
while (! (alert.hidden || resultReceived))
|
||||
#endif
|
||||
[[NSRunLoop mainRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.01]];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void buttonClicked (const int buttonIndex) noexcept
|
||||
{
|
||||
result = buttonIndex;
|
||||
resultReceived = true;
|
||||
|
||||
if (callback != nullptr)
|
||||
callback->modalStateFinished (result);
|
||||
|
||||
if (isAsync)
|
||||
delete this;
|
||||
}
|
||||
|
||||
private:
|
||||
int result;
|
||||
bool resultReceived;
|
||||
std::unique_ptr<ModalComponentManager::Callback> callback;
|
||||
const bool isAsync;
|
||||
|
||||
#if JUCE_USE_NEW_IOS_ALERTWINDOW
|
||||
void addButton (UIAlertController* alert, NSString* text, int index)
|
||||
{
|
||||
if (text != nil)
|
||||
[alert addAction: [UIAlertAction actionWithTitle: text
|
||||
style: UIAlertActionStyleDefault
|
||||
handler: ^(UIAlertAction*) { this->buttonClicked (index); }]];
|
||||
}
|
||||
#else
|
||||
UIAlertView* alert;
|
||||
JuceAlertBoxDelegate* delegate;
|
||||
#endif
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (iOSMessageBox)
|
||||
};
|
||||
|
||||
|
||||
#if ! JUCE_USE_NEW_IOS_ALERTWINDOW
|
||||
} // (juce namespace)
|
||||
|
||||
@implementation JuceAlertBoxDelegate
|
||||
|
||||
- (void) alertView: (UIAlertView*) alertView clickedButtonAtIndex: (NSInteger) buttonIndex
|
||||
{
|
||||
owner->buttonClicked ((int) buttonIndex);
|
||||
alertView.hidden = true;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace juce
|
||||
{
|
||||
#endif
|
||||
|
||||
|
||||
//==============================================================================
|
||||
#if JUCE_MODAL_LOOPS_PERMITTED
|
||||
void JUCE_CALLTYPE NativeMessageBox::showMessageBox (AlertWindow::AlertIconType /*iconType*/,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
iOSMessageBox mb (title, message, @"OK", nil, nil, nullptr, false);
|
||||
ignoreUnused (mb.getResult());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync (AlertWindow::AlertIconType /*iconType*/,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/,
|
||||
ModalComponentManager::Callback* callback)
|
||||
{
|
||||
new iOSMessageBox (title, message, @"OK", nil, nil, callback, true);
|
||||
}
|
||||
|
||||
bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox (AlertWindow::AlertIconType /*iconType*/,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/,
|
||||
ModalComponentManager::Callback* callback)
|
||||
{
|
||||
std::unique_ptr<iOSMessageBox> mb (new iOSMessageBox (title, message, @"Cancel", @"OK",
|
||||
nil, callback, callback != nullptr));
|
||||
|
||||
if (callback == nullptr)
|
||||
return mb->getResult() == 1;
|
||||
|
||||
mb.release();
|
||||
return false;
|
||||
}
|
||||
|
||||
int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (AlertWindow::AlertIconType /*iconType*/,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/,
|
||||
ModalComponentManager::Callback* callback)
|
||||
{
|
||||
std::unique_ptr<iOSMessageBox> mb (new iOSMessageBox (title, message, @"Cancel", @"Yes", @"No", callback, callback != nullptr));
|
||||
|
||||
if (callback == nullptr)
|
||||
return mb->getResult();
|
||||
|
||||
mb.release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (AlertWindow::AlertIconType /*iconType*/,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/,
|
||||
ModalComponentManager::Callback* callback)
|
||||
{
|
||||
std::unique_ptr<iOSMessageBox> mb (new iOSMessageBox (title, message, @"No", @"Yes", nil, callback, callback != nullptr));
|
||||
|
||||
if (callback == nullptr)
|
||||
return mb->getResult();
|
||||
|
||||
mb.release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray&, bool, Component*)
|
||||
{
|
||||
jassertfalse; // no such thing on iOS!
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DragAndDropContainer::performExternalDragDropOfText (const String&, Component*)
|
||||
{
|
||||
jassertfalse; // no such thing on iOS!
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void Desktop::setScreenSaverEnabled (const bool isEnabled)
|
||||
{
|
||||
if (! SystemStats::isRunningInAppExtensionSandbox())
|
||||
[[UIApplication sharedApplication] setIdleTimerDisabled: ! isEnabled];
|
||||
}
|
||||
|
||||
bool Desktop::isScreenSaverEnabled()
|
||||
{
|
||||
if (SystemStats::isRunningInAppExtensionSandbox())
|
||||
return true;
|
||||
|
||||
return ! [[UIApplication sharedApplication] isIdleTimerDisabled];
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool juce_areThereAnyAlwaysOnTopWindows()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Image juce_createIconForFile (const File&)
|
||||
{
|
||||
return Image();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void SystemClipboard::copyTextToClipboard (const String& text)
|
||||
{
|
||||
[[UIPasteboard generalPasteboard] setValue: juceStringToNS (text)
|
||||
forPasteboardType: @"public.text"];
|
||||
}
|
||||
|
||||
String SystemClipboard::getTextFromClipboard()
|
||||
{
|
||||
return nsStringToJuce ([[UIPasteboard generalPasteboard] valueForPasteboardType: @"public.text"]);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool MouseInputSource::SourceList::addSource()
|
||||
{
|
||||
addSource (sources.size(), MouseInputSource::InputSourceType::touch);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MouseInputSource::SourceList::canUseTouch()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Desktop::canUseSemiTransparentWindows() noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Point<float> MouseInputSource::getCurrentRawMousePosition()
|
||||
{
|
||||
return juce_lastMousePos;
|
||||
}
|
||||
|
||||
void MouseInputSource::setRawMousePosition (Point<float>)
|
||||
{
|
||||
}
|
||||
|
||||
double Desktop::getDefaultMasterScale()
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
Desktop::DisplayOrientation Desktop::getCurrentOrientation() const
|
||||
{
|
||||
UIInterfaceOrientation orientation = SystemStats::isRunningInAppExtensionSandbox() ? UIInterfaceOrientationPortrait
|
||||
: [[UIApplication sharedApplication] statusBarOrientation];
|
||||
|
||||
return Orientations::convertToJuce (orientation);
|
||||
}
|
||||
|
||||
void Desktop::Displays::findDisplays (float masterScale)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
UIScreen* s = [UIScreen mainScreen];
|
||||
|
||||
Display d;
|
||||
d.userArea = d.totalArea = UIViewComponentPeer::realScreenPosToRotated (convertToRectInt ([s bounds])) / masterScale;
|
||||
d.isMain = true;
|
||||
d.scale = masterScale;
|
||||
|
||||
if ([s respondsToSelector: @selector (scale)])
|
||||
d.scale *= s.scale;
|
||||
|
||||
d.dpi = 160 * d.scale;
|
||||
|
||||
displays.add (d);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace juce
|
256
modules/juce_gui_basics/native/juce_linux_FileChooser.cpp
Normal file
256
modules/juce_gui_basics/native/juce_linux_FileChooser.cpp
Normal file
@ -0,0 +1,256 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
static bool exeIsAvailable (const char* const executable)
|
||||
{
|
||||
ChildProcess child;
|
||||
const bool ok = child.start ("which " + String (executable))
|
||||
&& child.readAllProcessOutput().trim().isNotEmpty();
|
||||
|
||||
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
|
||||
return false;
|
||||
#else
|
||||
static bool canUseNativeBox = exeIsAvailable ("zenity") || exeIsAvailable ("kdialog");
|
||||
return canUseNativeBox;
|
||||
#endif
|
||||
}
|
||||
|
||||
FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags, FilePreviewComponent*)
|
||||
{
|
||||
return new Native (owner, flags);
|
||||
}
|
||||
|
||||
} // namespace juce
|
341
modules/juce_gui_basics/native/juce_linux_X11.cpp
Normal file
341
modules/juce_gui_basics/native/juce_linux_X11.cpp
Normal file
@ -0,0 +1,341 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
typedef void (*WindowMessageReceiveCallback) (XEvent&);
|
||||
WindowMessageReceiveCallback dispatchWindowMessage = nullptr;
|
||||
|
||||
typedef void (*SelectionRequestCallback) (XSelectionRequestEvent&);
|
||||
SelectionRequestCallback handleSelectionRequest = nullptr;
|
||||
|
||||
::Window juce_messageWindowHandle;
|
||||
XContext windowHandleXContext;
|
||||
|
||||
//==============================================================================
|
||||
namespace X11ErrorHandling
|
||||
{
|
||||
static XErrorHandler oldErrorHandler = {};
|
||||
static XIOErrorHandler oldIOErrorHandler = {};
|
||||
|
||||
//==============================================================================
|
||||
// Usually happens when client-server connection is broken
|
||||
int ioErrorHandler (::Display*)
|
||||
{
|
||||
DBG ("ERROR: connection to X server broken.. terminating.");
|
||||
|
||||
if (JUCEApplicationBase::isStandaloneApp())
|
||||
MessageManager::getInstance()->stopDispatchLoop();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int errorHandler (::Display* display, XErrorEvent* event)
|
||||
{
|
||||
ignoreUnused (display, event);
|
||||
|
||||
#if JUCE_DEBUG_XERRORS
|
||||
char errorStr[64] = { 0 };
|
||||
char requestStr[64] = { 0 };
|
||||
|
||||
XGetErrorText (display, event->error_code, errorStr, 64);
|
||||
XGetErrorDatabaseText (display, "XRequest", String (event->request_code).toUTF8(), "Unknown", requestStr, 64);
|
||||
DBG ("ERROR: X returned " << errorStr << " for operation " << requestStr);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void installXErrorHandlers()
|
||||
{
|
||||
oldIOErrorHandler = XSetIOErrorHandler (ioErrorHandler);
|
||||
oldErrorHandler = XSetErrorHandler (errorHandler);
|
||||
}
|
||||
|
||||
void removeXErrorHandlers()
|
||||
{
|
||||
XSetIOErrorHandler (oldIOErrorHandler);
|
||||
oldIOErrorHandler = {};
|
||||
|
||||
XSetErrorHandler (oldErrorHandler);
|
||||
oldErrorHandler = {};
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
XWindowSystem::XWindowSystem() noexcept
|
||||
{
|
||||
if (JUCEApplicationBase::isStandaloneApp())
|
||||
{
|
||||
// Initialise xlib for multiple thread support
|
||||
static bool initThreadCalled = false;
|
||||
|
||||
if (! initThreadCalled)
|
||||
{
|
||||
if (! XInitThreads())
|
||||
{
|
||||
// This is fatal! Print error and closedown
|
||||
Logger::outputDebugString ("Failed to initialise xlib thread support.");
|
||||
Process::terminate();
|
||||
return;
|
||||
}
|
||||
|
||||
initThreadCalled = true;
|
||||
}
|
||||
|
||||
X11ErrorHandling::installXErrorHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
XWindowSystem::~XWindowSystem() noexcept
|
||||
{
|
||||
if (JUCEApplicationBase::isStandaloneApp())
|
||||
X11ErrorHandling::removeXErrorHandlers();
|
||||
|
||||
clearSingletonInstance();
|
||||
}
|
||||
|
||||
::Display* XWindowSystem::displayRef() noexcept
|
||||
{
|
||||
if (++displayCount == 1)
|
||||
{
|
||||
jassert (display == nullptr);
|
||||
|
||||
String displayName (getenv ("DISPLAY"));
|
||||
|
||||
if (displayName.isEmpty())
|
||||
displayName = ":0.0";
|
||||
|
||||
// it seems that on some systems XOpenDisplay will occasionally
|
||||
// fail the first time, but succeed on a second attempt..
|
||||
for (int retries = 2; --retries >= 0;)
|
||||
{
|
||||
display = XOpenDisplay (displayName.toUTF8());
|
||||
|
||||
if (display != nullptr)
|
||||
break;
|
||||
}
|
||||
|
||||
initialiseXDisplay();
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
::Display* XWindowSystem::displayUnref() noexcept
|
||||
{
|
||||
jassert (display != nullptr);
|
||||
jassert (displayCount.get() > 0);
|
||||
|
||||
if (--displayCount == 0)
|
||||
{
|
||||
destroyXDisplay();
|
||||
XCloseDisplay (display);
|
||||
display = nullptr;
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
void XWindowSystem::initialiseXDisplay() noexcept
|
||||
{
|
||||
// This is fatal! Print error and closedown
|
||||
if (display == nullptr)
|
||||
{
|
||||
Logger::outputDebugString ("Failed to connect to the X Server.");
|
||||
Process::terminate();
|
||||
}
|
||||
|
||||
// Create a context to store user data associated with Windows we create
|
||||
windowHandleXContext = XUniqueContext();
|
||||
|
||||
// We're only interested in client messages for this window, which are always sent
|
||||
XSetWindowAttributes swa;
|
||||
swa.event_mask = NoEventMask;
|
||||
|
||||
// Create our message window (this will never be mapped)
|
||||
const int screen = DefaultScreen (display);
|
||||
juce_messageWindowHandle = XCreateWindow (display, RootWindow (display, screen),
|
||||
0, 0, 1, 1, 0, 0, InputOnly,
|
||||
DefaultVisual (display, screen),
|
||||
CWEventMask, &swa);
|
||||
|
||||
XSync (display, False);
|
||||
|
||||
// Setup input event handler
|
||||
int fd = XConnectionNumber (display);
|
||||
|
||||
LinuxEventLoop::setWindowSystemFd (fd,
|
||||
[this](int /*fd*/)
|
||||
{
|
||||
do
|
||||
{
|
||||
XEvent evt;
|
||||
|
||||
{
|
||||
ScopedXLock xlock (display);
|
||||
|
||||
if (! XPending (display))
|
||||
return false;
|
||||
|
||||
XNextEvent (display, &evt);
|
||||
}
|
||||
|
||||
if (evt.type == SelectionRequest && evt.xany.window == juce_messageWindowHandle
|
||||
&& handleSelectionRequest != nullptr)
|
||||
{
|
||||
handleSelectionRequest (evt.xselectionrequest);
|
||||
}
|
||||
else if (evt.xany.window != juce_messageWindowHandle
|
||||
&& dispatchWindowMessage != nullptr)
|
||||
{
|
||||
dispatchWindowMessage (evt);
|
||||
}
|
||||
|
||||
} while (display != nullptr);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void XWindowSystem::destroyXDisplay() noexcept
|
||||
{
|
||||
ScopedXLock xlock (display);
|
||||
XDestroyWindow (display, juce_messageWindowHandle);
|
||||
juce_messageWindowHandle = 0;
|
||||
XSync (display, True);
|
||||
LinuxEventLoop::removeWindowSystemFd();
|
||||
}
|
||||
|
||||
JUCE_IMPLEMENT_SINGLETON (XWindowSystem)
|
||||
|
||||
//==============================================================================
|
||||
ScopedXDisplay::ScopedXDisplay() : display (XWindowSystem::getInstance()->displayRef())
|
||||
{
|
||||
}
|
||||
|
||||
ScopedXDisplay::~ScopedXDisplay()
|
||||
{
|
||||
XWindowSystem::getInstance()->displayUnref();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
ScopedXLock::ScopedXLock (::Display* d) : display (d)
|
||||
{
|
||||
if (display != nullptr)
|
||||
XLockDisplay (display);
|
||||
}
|
||||
|
||||
ScopedXLock::~ScopedXLock()
|
||||
{
|
||||
if (display != nullptr)
|
||||
XUnlockDisplay (display);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Atoms::Atoms (::Display* display)
|
||||
{
|
||||
protocols = getIfExists (display, "WM_PROTOCOLS");
|
||||
protocolList [TAKE_FOCUS] = getIfExists (display, "WM_TAKE_FOCUS");
|
||||
protocolList [DELETE_WINDOW] = getIfExists (display, "WM_DELETE_WINDOW");
|
||||
protocolList [PING] = getIfExists (display, "_NET_WM_PING");
|
||||
changeState = getIfExists (display, "WM_CHANGE_STATE");
|
||||
state = getIfExists (display, "WM_STATE");
|
||||
userTime = getCreating (display, "_NET_WM_USER_TIME");
|
||||
activeWin = getCreating (display, "_NET_ACTIVE_WINDOW");
|
||||
pid = getCreating (display, "_NET_WM_PID");
|
||||
windowType = getIfExists (display, "_NET_WM_WINDOW_TYPE");
|
||||
windowState = getIfExists (display, "_NET_WM_STATE");
|
||||
|
||||
XdndAware = getCreating (display, "XdndAware");
|
||||
XdndEnter = getCreating (display, "XdndEnter");
|
||||
XdndLeave = getCreating (display, "XdndLeave");
|
||||
XdndPosition = getCreating (display, "XdndPosition");
|
||||
XdndStatus = getCreating (display, "XdndStatus");
|
||||
XdndDrop = getCreating (display, "XdndDrop");
|
||||
XdndFinished = getCreating (display, "XdndFinished");
|
||||
XdndSelection = getCreating (display, "XdndSelection");
|
||||
|
||||
XdndTypeList = getCreating (display, "XdndTypeList");
|
||||
XdndActionList = getCreating (display, "XdndActionList");
|
||||
XdndActionCopy = getCreating (display, "XdndActionCopy");
|
||||
XdndActionPrivate = getCreating (display, "XdndActionPrivate");
|
||||
XdndActionDescription = getCreating (display, "XdndActionDescription");
|
||||
|
||||
XembedMsgType = getCreating (display, "_XEMBED");
|
||||
XembedInfo = getCreating (display, "_XEMBED_INFO");
|
||||
|
||||
allowedMimeTypes[0] = getCreating (display, "UTF8_STRING");
|
||||
allowedMimeTypes[1] = getCreating (display, "text/plain;charset=utf-8");
|
||||
allowedMimeTypes[2] = getCreating (display, "text/plain");
|
||||
allowedMimeTypes[3] = getCreating (display, "text/uri-list");
|
||||
|
||||
allowedActions[0] = getCreating (display, "XdndActionMove");
|
||||
allowedActions[1] = XdndActionCopy;
|
||||
allowedActions[2] = getCreating (display, "XdndActionLink");
|
||||
allowedActions[3] = getCreating (display, "XdndActionAsk");
|
||||
allowedActions[4] = XdndActionPrivate;
|
||||
}
|
||||
|
||||
Atom Atoms::getIfExists (::Display* display, const char* name) { return XInternAtom (display, name, True); }
|
||||
Atom Atoms::getCreating (::Display* display, const char* name) { return XInternAtom (display, name, False); }
|
||||
|
||||
String Atoms::getName (::Display* display, const Atom atom)
|
||||
{
|
||||
if (atom == None)
|
||||
return "None";
|
||||
|
||||
return String (XGetAtomName (display, atom));
|
||||
}
|
||||
|
||||
bool Atoms::isMimeTypeFile (::Display* display, const Atom atom)
|
||||
{
|
||||
return getName (display, atom).equalsIgnoreCase ("text/uri-list");
|
||||
}
|
||||
|
||||
|
||||
const unsigned long Atoms::DndVersion = 3;
|
||||
|
||||
//==============================================================================
|
||||
GetXProperty::GetXProperty (::Display* display, Window window, Atom atom,
|
||||
long offset, long length, bool shouldDelete,
|
||||
Atom requestedType)
|
||||
{
|
||||
success = (XGetWindowProperty (display, window, atom, offset, length,
|
||||
(Bool) shouldDelete, requestedType, &actualType,
|
||||
&actualFormat, &numItems, &bytesLeft, &data) == Success)
|
||||
&& data != nullptr;
|
||||
}
|
||||
|
||||
GetXProperty::~GetXProperty()
|
||||
{
|
||||
if (data != nullptr)
|
||||
XFree (data);
|
||||
}
|
||||
|
||||
} // namespace juce
|
142
modules/juce_gui_basics/native/juce_linux_X11.h
Normal file
142
modules/juce_gui_basics/native/juce_linux_X11.h
Normal file
@ -0,0 +1,142 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
struct _XDisplay;
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
typedef ::_XDisplay* XDisplay;
|
||||
|
||||
typedef unsigned long AtomType;
|
||||
typedef unsigned long WindowType;
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class XWindowSystem : public DeletedAtShutdown
|
||||
{
|
||||
public:
|
||||
XDisplay displayRef() noexcept;
|
||||
XDisplay displayUnref() noexcept;
|
||||
|
||||
JUCE_DECLARE_SINGLETON (XWindowSystem, false)
|
||||
|
||||
private:
|
||||
XDisplay display = {};
|
||||
Atomic<int> displayCount;
|
||||
|
||||
XWindowSystem() noexcept;
|
||||
~XWindowSystem() noexcept;
|
||||
|
||||
void initialiseXDisplay() noexcept;
|
||||
void destroyXDisplay() noexcept;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Creates and holds a reference to the X display.
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
struct ScopedXDisplay
|
||||
{
|
||||
ScopedXDisplay();
|
||||
~ScopedXDisplay();
|
||||
|
||||
const XDisplay display;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** A handy class that uses XLockDisplay and XUnlockDisplay to lock the X server
|
||||
using RAII (Only available in Linux!).
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class ScopedXLock
|
||||
{
|
||||
public:
|
||||
/** Creating a ScopedXLock object locks the X display.
|
||||
This uses XLockDisplay() to grab the display that JUCE is using.
|
||||
*/
|
||||
ScopedXLock (XDisplay);
|
||||
|
||||
/** Deleting a ScopedXLock object unlocks the X display.
|
||||
This calls XUnlockDisplay() to release the lock.
|
||||
*/
|
||||
~ScopedXLock();
|
||||
|
||||
private:
|
||||
// defined in juce_linux_X11.h
|
||||
XDisplay display;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct Atoms
|
||||
{
|
||||
Atoms (XDisplay);
|
||||
|
||||
enum ProtocolItems
|
||||
{
|
||||
TAKE_FOCUS = 0,
|
||||
DELETE_WINDOW = 1,
|
||||
PING = 2
|
||||
};
|
||||
|
||||
AtomType protocols, protocolList[3], changeState, state, userTime,
|
||||
activeWin, pid, windowType, windowState,
|
||||
XdndAware, XdndEnter, XdndLeave, XdndPosition, XdndStatus,
|
||||
XdndDrop, XdndFinished, XdndSelection, XdndTypeList, XdndActionList,
|
||||
XdndActionDescription, XdndActionCopy, XdndActionPrivate,
|
||||
XembedMsgType, XembedInfo,
|
||||
allowedActions[5],
|
||||
allowedMimeTypes[4];
|
||||
|
||||
static const unsigned long DndVersion;
|
||||
|
||||
static AtomType getIfExists (XDisplay, const char* name);
|
||||
static AtomType getCreating (XDisplay, const char* name);
|
||||
|
||||
static String getName (XDisplay, AtomType);
|
||||
|
||||
static bool isMimeTypeFile (XDisplay, AtomType);
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct GetXProperty
|
||||
{
|
||||
GetXProperty (XDisplay, WindowType, AtomType,
|
||||
long offset, long length, bool shouldDelete,
|
||||
AtomType requestedType);
|
||||
|
||||
~GetXProperty();
|
||||
|
||||
bool success;
|
||||
unsigned char* data = nullptr;
|
||||
unsigned long numItems, bytesLeft;
|
||||
AtomType actualType;
|
||||
int actualFormat;
|
||||
};
|
||||
|
||||
} // namespace juce
|
280
modules/juce_gui_basics/native/juce_linux_X11_Clipboard.cpp
Normal file
280
modules/juce_gui_basics/native/juce_linux_X11_Clipboard.cpp
Normal file
@ -0,0 +1,280 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
extern ::Window juce_messageWindowHandle;
|
||||
|
||||
namespace ClipboardHelpers
|
||||
{
|
||||
static String localClipboardContent;
|
||||
static Atom atom_UTF8_STRING;
|
||||
static Atom atom_CLIPBOARD;
|
||||
static Atom atom_TARGETS;
|
||||
|
||||
//==============================================================================
|
||||
static void initSelectionAtoms (::Display* display)
|
||||
{
|
||||
static bool isInitialised = false;
|
||||
|
||||
if (! isInitialised)
|
||||
{
|
||||
isInitialised = true;
|
||||
|
||||
atom_UTF8_STRING = Atoms::getCreating (display, "UTF8_STRING");
|
||||
atom_CLIPBOARD = Atoms::getCreating (display, "CLIPBOARD");
|
||||
atom_TARGETS = Atoms::getCreating (display, "TARGETS");
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Read the content of a window property as either a locale-dependent string or an utf8 string
|
||||
// works only for strings shorter than 1000000 bytes
|
||||
static String readWindowProperty (::Display* display, Window window, Atom prop)
|
||||
{
|
||||
String returnData;
|
||||
|
||||
if (display != nullptr)
|
||||
{
|
||||
char* clipData;
|
||||
Atom actualType;
|
||||
int actualFormat;
|
||||
unsigned long numItems, bytesLeft;
|
||||
|
||||
if (XGetWindowProperty (display, window, prop,
|
||||
0L /* offset */, 1000000 /* length (max) */, False,
|
||||
AnyPropertyType /* format */,
|
||||
&actualType, &actualFormat, &numItems, &bytesLeft,
|
||||
(unsigned char**) &clipData) == Success)
|
||||
{
|
||||
if (actualType == atom_UTF8_STRING && actualFormat == 8)
|
||||
returnData = String::fromUTF8 (clipData, (int) numItems);
|
||||
else if (actualType == XA_STRING && actualFormat == 8)
|
||||
returnData = String (clipData, numItems);
|
||||
|
||||
if (clipData != nullptr)
|
||||
XFree (clipData);
|
||||
|
||||
jassert (bytesLeft == 0 || numItems == 1000000);
|
||||
}
|
||||
|
||||
XDeleteProperty (display, window, prop);
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Send a SelectionRequest to the window owning the selection and waits for its answer (with a timeout) */
|
||||
static bool requestSelectionContent (::Display* display, String& selectionContent,
|
||||
Atom selection, Atom requestedFormat)
|
||||
{
|
||||
Atom property_name = XInternAtom (display, "JUCE_SEL", false);
|
||||
|
||||
// The selection owner will be asked to set the JUCE_SEL property on the
|
||||
// juce_messageWindowHandle with the selection content
|
||||
XConvertSelection (display, selection, requestedFormat, property_name,
|
||||
juce_messageWindowHandle, CurrentTime);
|
||||
|
||||
int count = 50; // will wait at most for 200 ms
|
||||
|
||||
while (--count >= 0)
|
||||
{
|
||||
XEvent event;
|
||||
|
||||
if (XCheckTypedWindowEvent (display, juce_messageWindowHandle, SelectionNotify, &event))
|
||||
{
|
||||
if (event.xselection.property == property_name)
|
||||
{
|
||||
jassert (event.xselection.requestor == juce_messageWindowHandle);
|
||||
|
||||
selectionContent = readWindowProperty (display, event.xselection.requestor,
|
||||
event.xselection.property);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // the format we asked for was denied.. (event.xselection.property == None)
|
||||
}
|
||||
|
||||
// not very elegant.. we could do a select() or something like that...
|
||||
// however clipboard content requesting is inherently slow on x11, it
|
||||
// often takes 50ms or more so...
|
||||
Thread::sleep (4);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Called from the event loop in juce_linux_Messaging in response to SelectionRequest events
|
||||
static void handleSelection (XSelectionRequestEvent& evt)
|
||||
{
|
||||
ClipboardHelpers::initSelectionAtoms (evt.display);
|
||||
|
||||
// the selection content is sent to the target window as a window property
|
||||
XSelectionEvent reply;
|
||||
reply.type = SelectionNotify;
|
||||
reply.display = evt.display;
|
||||
reply.requestor = evt.requestor;
|
||||
reply.selection = evt.selection;
|
||||
reply.target = evt.target;
|
||||
reply.property = None; // == "fail"
|
||||
reply.time = evt.time;
|
||||
|
||||
HeapBlock<char> data;
|
||||
int propertyFormat = 0;
|
||||
size_t numDataItems = 0;
|
||||
|
||||
if (evt.selection == XA_PRIMARY || evt.selection == ClipboardHelpers::atom_CLIPBOARD)
|
||||
{
|
||||
if (evt.target == XA_STRING || evt.target == ClipboardHelpers::atom_UTF8_STRING)
|
||||
{
|
||||
// translate to utf8
|
||||
numDataItems = ClipboardHelpers::localClipboardContent.getNumBytesAsUTF8() + 1;
|
||||
data.calloc (numDataItems + 1);
|
||||
ClipboardHelpers::localClipboardContent.copyToUTF8 (data, numDataItems);
|
||||
propertyFormat = 8; // bits/item
|
||||
}
|
||||
else if (evt.target == ClipboardHelpers::atom_TARGETS)
|
||||
{
|
||||
// another application wants to know what we are able to send
|
||||
numDataItems = 2;
|
||||
propertyFormat = 32; // atoms are 32-bit
|
||||
data.calloc (numDataItems * 4);
|
||||
Atom* atoms = reinterpret_cast<Atom*> (data.getData());
|
||||
atoms[0] = ClipboardHelpers::atom_UTF8_STRING;
|
||||
atoms[1] = XA_STRING;
|
||||
|
||||
evt.target = XA_ATOM;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DBG ("requested unsupported clipboard");
|
||||
}
|
||||
|
||||
if (data != nullptr)
|
||||
{
|
||||
const size_t maxReasonableSelectionSize = 1000000;
|
||||
|
||||
// for very big chunks of data, we should use the "INCR" protocol , which is a pain in the *ss
|
||||
if (evt.property != None && numDataItems < maxReasonableSelectionSize)
|
||||
{
|
||||
XChangeProperty (evt.display, evt.requestor,
|
||||
evt.property, evt.target,
|
||||
propertyFormat /* 8 or 32 */, PropModeReplace,
|
||||
reinterpret_cast<const unsigned char*> (data.getData()), (int) numDataItems);
|
||||
reply.property = evt.property; // " == success"
|
||||
}
|
||||
}
|
||||
|
||||
XSendEvent (evt.display, evt.requestor, 0, NoEventMask, (XEvent*) &reply);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
typedef void (*SelectionRequestCallback) (XSelectionRequestEvent&);
|
||||
extern SelectionRequestCallback handleSelectionRequest;
|
||||
|
||||
struct ClipboardCallbackInitialiser
|
||||
{
|
||||
ClipboardCallbackInitialiser()
|
||||
{
|
||||
handleSelectionRequest = ClipboardHelpers::handleSelection;
|
||||
}
|
||||
};
|
||||
|
||||
static ClipboardCallbackInitialiser clipboardInitialiser;
|
||||
|
||||
//==============================================================================
|
||||
void SystemClipboard::copyTextToClipboard (const String& clipText)
|
||||
{
|
||||
ScopedXDisplay xDisplay;
|
||||
|
||||
if (auto display = xDisplay.display)
|
||||
{
|
||||
ClipboardHelpers::initSelectionAtoms (display);
|
||||
ClipboardHelpers::localClipboardContent = clipText;
|
||||
|
||||
XSetSelectionOwner (display, XA_PRIMARY, juce_messageWindowHandle, CurrentTime);
|
||||
XSetSelectionOwner (display, ClipboardHelpers::atom_CLIPBOARD, juce_messageWindowHandle, CurrentTime);
|
||||
}
|
||||
}
|
||||
|
||||
String SystemClipboard::getTextFromClipboard()
|
||||
{
|
||||
String content;
|
||||
ScopedXDisplay xDisplay;
|
||||
|
||||
if (auto display = xDisplay.display)
|
||||
{
|
||||
ClipboardHelpers::initSelectionAtoms (display);
|
||||
|
||||
/* 1) try to read from the "CLIPBOARD" selection first (the "high
|
||||
level" clipboard that is supposed to be filled by ctrl-C
|
||||
etc). When a clipboard manager is running, the content of this
|
||||
selection is preserved even when the original selection owner
|
||||
exits.
|
||||
|
||||
2) and then try to read from "PRIMARY" selection (the "legacy" selection
|
||||
filled by good old x11 apps such as xterm)
|
||||
*/
|
||||
Atom selection = XA_PRIMARY;
|
||||
Window selectionOwner = None;
|
||||
|
||||
if ((selectionOwner = XGetSelectionOwner (display, selection)) == None)
|
||||
{
|
||||
selection = ClipboardHelpers::atom_CLIPBOARD;
|
||||
selectionOwner = XGetSelectionOwner (display, selection);
|
||||
}
|
||||
|
||||
if (selectionOwner != None)
|
||||
{
|
||||
if (selectionOwner == juce_messageWindowHandle)
|
||||
{
|
||||
content = ClipboardHelpers::localClipboardContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
// first try: we want an utf8 string
|
||||
bool ok = ClipboardHelpers::requestSelectionContent (display, content,
|
||||
selection, ClipboardHelpers::atom_UTF8_STRING);
|
||||
|
||||
if (! ok)
|
||||
{
|
||||
// second chance, ask for a good old locale-dependent string ..
|
||||
ok = ClipboardHelpers::requestSelectionContent (display, content,
|
||||
selection, XA_STRING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
} // namespace juce
|
4336
modules/juce_gui_basics/native/juce_linux_X11_Windowing.cpp
Normal file
4336
modules/juce_gui_basics/native/juce_linux_X11_Windowing.cpp
Normal file
File diff suppressed because it is too large
Load Diff
349
modules/juce_gui_basics/native/juce_mac_FileChooser.mm
Normal file
349
modules/juce_gui_basics/native/juce_mac_FileChooser.mm
Normal file
@ -0,0 +1,349 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
static NSMutableArray* createAllowedTypesArray (const StringArray& filters)
|
||||
{
|
||||
if (filters.size() == 0)
|
||||
return nil;
|
||||
|
||||
NSMutableArray* filterArray = [[[NSMutableArray alloc] init] autorelease];
|
||||
|
||||
for (int i = 0; i < filters.size(); ++i)
|
||||
{
|
||||
#if defined (MAC_OS_X_VERSION_10_6) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6)
|
||||
// From OS X 10.6 you can only specify allowed extensions, so any filters containing wildcards
|
||||
// must be of the form "*.extension"
|
||||
jassert (filters[i] == "*"
|
||||
|| (filters[i].startsWith ("*.") && filters[i].lastIndexOfChar ('*') == 0));
|
||||
#endif
|
||||
|
||||
const String f (filters[i].replace ("*.", ""));
|
||||
|
||||
if (f == "*")
|
||||
return nil;
|
||||
|
||||
[filterArray addObject: juceStringToNS (f)];
|
||||
}
|
||||
|
||||
return filterArray;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
class FileChooser::Native : public Component,
|
||||
public FileChooser::Pimpl
|
||||
{
|
||||
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])
|
||||
{
|
||||
setBounds (0, 0, 0, 0);
|
||||
setOpaque (true);
|
||||
|
||||
static DelegateClass cls;
|
||||
|
||||
delegate = [cls.createInstance() init];
|
||||
object_setInstanceVariable (delegate, "cppObject", this);
|
||||
|
||||
[panel setDelegate: delegate];
|
||||
|
||||
filters.addTokens (owner.filters.replaceCharacters (",:", ";;"), ";", String());
|
||||
filters.trim();
|
||||
filters.removeEmptyStrings();
|
||||
|
||||
[panel setTitle: juceStringToNS (owner.title)];
|
||||
[panel setAllowedFileTypes: createAllowedTypesArray (filters)];
|
||||
|
||||
if (! isSave)
|
||||
{
|
||||
NSOpenPanel* openPanel = (NSOpenPanel*) panel;
|
||||
|
||||
[openPanel setCanChooseDirectories: selectsDirectories];
|
||||
[openPanel setCanChooseFiles: selectsFiles];
|
||||
[openPanel setAllowsMultipleSelection: selectMultiple];
|
||||
[openPanel setResolvesAliases: YES];
|
||||
|
||||
if (owner.treatFilePackagesAsDirs)
|
||||
[openPanel setTreatsFilePackagesAsDirectories: YES];
|
||||
}
|
||||
|
||||
if (preview != nullptr)
|
||||
{
|
||||
nsViewPreview = [[NSView alloc] initWithFrame: makeNSRect (preview->getLocalBounds())];
|
||||
preview->addToDesktop (0, (void*) nsViewPreview);
|
||||
preview->setVisible (true);
|
||||
|
||||
[panel setAccessoryView: nsViewPreview];
|
||||
}
|
||||
|
||||
if (isSave || selectsDirectories)
|
||||
[panel setCanCreateDirectories: YES];
|
||||
|
||||
[panel setLevel:NSModalPanelWindowLevel];
|
||||
|
||||
if (owner.startingFile.isDirectory())
|
||||
{
|
||||
startingDirectory = owner.startingFile.getFullPathName();
|
||||
}
|
||||
else
|
||||
{
|
||||
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 (startingDirectory)];
|
||||
[panel setNameFieldStringValue: juceStringToNS (filename)];
|
||||
#endif
|
||||
}
|
||||
|
||||
~Native()
|
||||
{
|
||||
exitModalState (0);
|
||||
removeFromDesktop();
|
||||
|
||||
if (panel != nil)
|
||||
{
|
||||
[panel setDelegate: nil];
|
||||
|
||||
if (nsViewPreview != nil)
|
||||
{
|
||||
[panel setAccessoryView: nil];
|
||||
|
||||
[nsViewPreview release];
|
||||
|
||||
nsViewPreview = nil;
|
||||
preview = nullptr;
|
||||
}
|
||||
|
||||
[panel close];
|
||||
[panel release];
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
std::unique_ptr<TemporaryMainMenuWithStandardCommands> tempMenu;
|
||||
|
||||
if (JUCEApplicationBase::isStandaloneApp())
|
||||
tempMenu.reset (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)
|
||||
{
|
||||
auto addURLResult = [&chooserResults] (NSURL* urlToAdd)
|
||||
{
|
||||
auto scheme = nsStringToJuce ([urlToAdd scheme]);
|
||||
auto path = nsStringToJuce ([urlToAdd path]);
|
||||
chooserResults.add (URL (scheme + "://" + path));
|
||||
};
|
||||
|
||||
if (isSave)
|
||||
{
|
||||
addURLResult ([panel URL]);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto* openPanel = (NSOpenPanel*) panel;
|
||||
auto* urls = [openPanel URLs];
|
||||
|
||||
for (unsigned int i = 0; i < [urls count]; ++i)
|
||||
addURLResult ([urls objectAtIndex: i]);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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()
|
||||
{
|
||||
#if JUCE_DISABLE_NATIVE_FILECHOOSERS
|
||||
return false;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
811
modules/juce_gui_basics/native/juce_mac_MainMenu.mm
Normal file
811
modules/juce_gui_basics/native/juce_mac_MainMenu.mm
Normal file
@ -0,0 +1,811 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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 JuceMainMenuBarHolder : private DeletedAtShutdown
|
||||
{
|
||||
JuceMainMenuBarHolder()
|
||||
: mainMenuBar ([[NSMenu alloc] initWithTitle: nsStringLiteral ("MainMenu")])
|
||||
{
|
||||
auto* item = [mainMenuBar addItemWithTitle: nsStringLiteral ("Apple")
|
||||
action: nil
|
||||
keyEquivalent: nsEmptyString()];
|
||||
|
||||
auto* appMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Apple")];
|
||||
|
||||
[NSApp performSelector: @selector (setAppleMenu:) withObject: appMenu];
|
||||
[mainMenuBar setSubmenu: appMenu forItem: item];
|
||||
[appMenu release];
|
||||
|
||||
[NSApp setMainMenu: mainMenuBar];
|
||||
}
|
||||
|
||||
~JuceMainMenuBarHolder()
|
||||
{
|
||||
clearSingletonInstance();
|
||||
|
||||
[NSApp setMainMenu: nil];
|
||||
[mainMenuBar release];
|
||||
}
|
||||
|
||||
NSMenu* mainMenuBar = nil;
|
||||
|
||||
JUCE_DECLARE_SINGLETON_SINGLETHREADED (JuceMainMenuBarHolder, true)
|
||||
};
|
||||
|
||||
JUCE_IMPLEMENT_SINGLETON (JuceMainMenuBarHolder)
|
||||
|
||||
//==============================================================================
|
||||
class JuceMainMenuHandler : private MenuBarModel::Listener,
|
||||
private DeletedAtShutdown
|
||||
{
|
||||
public:
|
||||
JuceMainMenuHandler()
|
||||
{
|
||||
static JuceMenuCallbackClass cls;
|
||||
callback = [cls.createInstance() init];
|
||||
JuceMenuCallbackClass::setOwner (callback, this);
|
||||
}
|
||||
|
||||
~JuceMainMenuHandler()
|
||||
{
|
||||
setMenu (nullptr, nullptr, String());
|
||||
|
||||
jassert (instance == this);
|
||||
instance = nullptr;
|
||||
|
||||
[callback release];
|
||||
}
|
||||
|
||||
void setMenu (MenuBarModel* const newMenuBarModel,
|
||||
const PopupMenu* newExtraAppleMenuItems,
|
||||
const String& recentItemsName)
|
||||
{
|
||||
recentItemsMenuName = recentItemsName;
|
||||
|
||||
if (currentModel != newMenuBarModel)
|
||||
{
|
||||
if (currentModel != nullptr)
|
||||
currentModel->removeListener (this);
|
||||
|
||||
currentModel = newMenuBarModel;
|
||||
|
||||
if (currentModel != nullptr)
|
||||
currentModel->addListener (this);
|
||||
|
||||
menuBarItemsChanged (nullptr);
|
||||
}
|
||||
|
||||
extraAppleMenuItems.reset (createCopyIfNotNull (newExtraAppleMenuItems));
|
||||
}
|
||||
|
||||
void addTopLevelMenu (NSMenu* parent, const PopupMenu& child, const String& name, int menuId, int topLevelIndex)
|
||||
{
|
||||
NSMenuItem* item = [parent addItemWithTitle: juceStringToNS (name)
|
||||
action: nil
|
||||
keyEquivalent: nsEmptyString()];
|
||||
|
||||
NSMenu* sub = createMenu (child, name, menuId, topLevelIndex, true);
|
||||
|
||||
[parent setSubmenu: sub forItem: item];
|
||||
[sub setAutoenablesItems: false];
|
||||
[sub release];
|
||||
}
|
||||
|
||||
void updateTopLevelMenu (NSMenuItem* parentItem, const PopupMenu& menuToCopy, const String& name, int menuId, int topLevelIndex)
|
||||
{
|
||||
// Note: This method used to update the contents of the existing menu in-place, but that caused
|
||||
// weird side-effects which messed-up keyboard focus when switching between windows. By creating
|
||||
// a new menu and replacing the old one with it, that problem seems to be avoided..
|
||||
NSMenu* menu = [[NSMenu alloc] initWithTitle: juceStringToNS (name)];
|
||||
|
||||
for (PopupMenu::MenuItemIterator iter (menuToCopy); iter.next();)
|
||||
addMenuItem (iter, menu, menuId, topLevelIndex);
|
||||
|
||||
[menu setAutoenablesItems: false];
|
||||
[menu update];
|
||||
|
||||
removeItemRecursive ([parentItem submenu]);
|
||||
[parentItem setSubmenu: menu];
|
||||
|
||||
[menu release];
|
||||
}
|
||||
|
||||
void updateTopLevelMenu (NSMenu* menu)
|
||||
{
|
||||
NSMenu* superMenu = [menu supermenu];
|
||||
auto menuNames = currentModel->getMenuBarNames();
|
||||
auto indexOfMenu = (int) [superMenu indexOfItemWithSubmenu: menu] - 1;
|
||||
|
||||
if (indexOfMenu >= 0)
|
||||
{
|
||||
removeItemRecursive (menu);
|
||||
|
||||
auto updatedPopup = currentModel->getMenuForIndex (indexOfMenu, menuNames[indexOfMenu]);
|
||||
|
||||
for (PopupMenu::MenuItemIterator iter (updatedPopup); iter.next();)
|
||||
addMenuItem (iter, menu, 1, indexOfMenu);
|
||||
|
||||
[menu update];
|
||||
}
|
||||
}
|
||||
|
||||
void menuBarItemsChanged (MenuBarModel*) override
|
||||
{
|
||||
if (isOpen)
|
||||
{
|
||||
defferedUpdateRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
lastUpdateTime = Time::getMillisecondCounter();
|
||||
|
||||
StringArray menuNames;
|
||||
if (currentModel != nullptr)
|
||||
menuNames = currentModel->getMenuBarNames();
|
||||
|
||||
auto* menuBar = getMainMenuBar();
|
||||
|
||||
while ([menuBar numberOfItems] > 1 + menuNames.size())
|
||||
removeItemRecursive (menuBar, static_cast<int> ([menuBar numberOfItems] - 1));
|
||||
|
||||
int menuId = 1;
|
||||
|
||||
for (int i = 0; i < menuNames.size(); ++i)
|
||||
{
|
||||
const PopupMenu menu (currentModel->getMenuForIndex (i, menuNames[i]));
|
||||
|
||||
if (i >= [menuBar numberOfItems] - 1)
|
||||
addTopLevelMenu (menuBar, menu, menuNames[i], menuId, i);
|
||||
else
|
||||
updateTopLevelMenu ([menuBar itemAtIndex: 1 + i], menu, menuNames[i], menuId, i);
|
||||
}
|
||||
}
|
||||
|
||||
void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo& info) override
|
||||
{
|
||||
if ((info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) == 0
|
||||
&& info.invocationMethod != ApplicationCommandTarget::InvocationInfo::fromKeyPress)
|
||||
if (auto* item = findMenuItemWithCommandID (getMainMenuBar(), info.commandID))
|
||||
flashMenuBar ([item menu]);
|
||||
}
|
||||
|
||||
void invoke (const PopupMenu::Item& item, int topLevelIndex) const
|
||||
{
|
||||
if (currentModel != nullptr)
|
||||
{
|
||||
if (item.customCallback != nullptr)
|
||||
if (! item.customCallback->menuItemTriggered())
|
||||
return;
|
||||
|
||||
if (item.commandManager != nullptr)
|
||||
{
|
||||
ApplicationCommandTarget::InvocationInfo info (item.itemID);
|
||||
info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
|
||||
|
||||
item.commandManager->invoke (info, true);
|
||||
}
|
||||
|
||||
MessageManager::callAsync ([=]
|
||||
{
|
||||
if (instance != nullptr)
|
||||
instance->invokeDirectly (item.itemID, topLevelIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void invokeDirectly (int commandId, int topLevelIndex)
|
||||
{
|
||||
if (currentModel != nullptr)
|
||||
currentModel->menuItemSelected (commandId, topLevelIndex);
|
||||
}
|
||||
|
||||
void addMenuItem (PopupMenu::MenuItemIterator& iter, NSMenu* menuToAddTo,
|
||||
const int topLevelMenuId, const int topLevelIndex)
|
||||
{
|
||||
const PopupMenu::Item& i = iter.getItem();
|
||||
NSString* text = juceStringToNS (i.text);
|
||||
|
||||
if (text == nil)
|
||||
text = nsEmptyString();
|
||||
|
||||
if (i.isSeparator)
|
||||
{
|
||||
[menuToAddTo addItem: [NSMenuItem separatorItem]];
|
||||
}
|
||||
else if (i.isSectionHeader)
|
||||
{
|
||||
NSMenuItem* item = [menuToAddTo addItemWithTitle: text
|
||||
action: nil
|
||||
keyEquivalent: nsEmptyString()];
|
||||
|
||||
[item setEnabled: false];
|
||||
}
|
||||
else if (i.subMenu != nullptr)
|
||||
{
|
||||
if (i.text == recentItemsMenuName)
|
||||
{
|
||||
if (recent == nullptr)
|
||||
recent.reset (new RecentFilesMenuItem());
|
||||
|
||||
if (recent->recentItem != nil)
|
||||
{
|
||||
if (NSMenu* parent = [recent->recentItem menu])
|
||||
[parent removeItem: recent->recentItem];
|
||||
|
||||
[menuToAddTo addItem: recent->recentItem];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NSMenuItem* item = [menuToAddTo addItemWithTitle: text
|
||||
action: nil
|
||||
keyEquivalent: nsEmptyString()];
|
||||
|
||||
[item setTag: i.itemID];
|
||||
[item setEnabled: i.isEnabled];
|
||||
|
||||
NSMenu* sub = createMenu (*i.subMenu, i.text, topLevelMenuId, topLevelIndex, false);
|
||||
[menuToAddTo setSubmenu: sub forItem: item];
|
||||
[sub release];
|
||||
}
|
||||
else
|
||||
{
|
||||
auto* item = [[NSMenuItem alloc] initWithTitle: text
|
||||
action: @selector (menuItemInvoked:)
|
||||
keyEquivalent: nsEmptyString()];
|
||||
|
||||
[item setTag: topLevelIndex];
|
||||
[item setEnabled: i.isEnabled];
|
||||
[item setState: i.isTicked ? NSOnState : NSOffState];
|
||||
[item setTarget: (id) callback];
|
||||
|
||||
auto* juceItem = new PopupMenu::Item (i);
|
||||
juceItem->customComponent = nullptr;
|
||||
|
||||
[item setRepresentedObject: [createNSObjectFromJuceClass (juceItem) autorelease]];
|
||||
|
||||
if (i.commandManager != nullptr)
|
||||
{
|
||||
for (auto& kp : i.commandManager->getKeyMappings()->getKeyPressesAssignedToCommand (i.itemID))
|
||||
{
|
||||
if (kp != KeyPress::backspaceKey // (adding these is annoying because it flashes the menu bar
|
||||
&& kp != KeyPress::deleteKey) // every time you press the key while editing text)
|
||||
{
|
||||
juce_wchar key = kp.getTextCharacter();
|
||||
|
||||
if (key == 0)
|
||||
key = (juce_wchar) kp.getKeyCode();
|
||||
|
||||
[item setKeyEquivalent: juceStringToNS (String::charToString (key).toLowerCase())];
|
||||
[item setKeyEquivalentModifierMask: juceModsToNSMods (kp.getModifiers())];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[menuToAddTo addItem: item];
|
||||
[item release];
|
||||
}
|
||||
}
|
||||
|
||||
NSMenu* createMenu (const PopupMenu menu,
|
||||
const String& menuName,
|
||||
const int topLevelMenuId,
|
||||
const int topLevelIndex,
|
||||
const bool addDelegate)
|
||||
{
|
||||
NSMenu* m = [[NSMenu alloc] initWithTitle: juceStringToNS (menuName)];
|
||||
|
||||
[m setAutoenablesItems: false];
|
||||
|
||||
if (addDelegate)
|
||||
{
|
||||
#if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
|
||||
[m setDelegate: (id<NSMenuDelegate>) callback];
|
||||
#else
|
||||
[m setDelegate: callback];
|
||||
#endif
|
||||
}
|
||||
|
||||
for (PopupMenu::MenuItemIterator iter (menu); iter.next();)
|
||||
addMenuItem (iter, m, topLevelMenuId, topLevelIndex);
|
||||
|
||||
[m update];
|
||||
return m;
|
||||
}
|
||||
|
||||
static JuceMainMenuHandler* instance;
|
||||
|
||||
MenuBarModel* currentModel = nullptr;
|
||||
std::unique_ptr<PopupMenu> extraAppleMenuItems;
|
||||
uint32 lastUpdateTime = 0;
|
||||
NSObject* callback = nil;
|
||||
String recentItemsMenuName;
|
||||
bool isOpen = false, defferedUpdateRequested = false;
|
||||
|
||||
private:
|
||||
struct RecentFilesMenuItem
|
||||
{
|
||||
RecentFilesMenuItem() : recentItem (nil)
|
||||
{
|
||||
if (NSNib* menuNib = [[[NSNib alloc] initWithNibNamed: @"RecentFilesMenuTemplate" bundle: nil] autorelease])
|
||||
{
|
||||
NSArray* array = nil;
|
||||
|
||||
#if (! defined (MAC_OS_X_VERSION_10_8)) || MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_8
|
||||
[menuNib instantiateNibWithOwner: NSApp topLevelObjects: &array];
|
||||
#else
|
||||
[menuNib instantiateWithOwner: NSApp topLevelObjects: &array];
|
||||
#endif
|
||||
|
||||
for (id object in array)
|
||||
{
|
||||
if ([object isKindOfClass: [NSMenu class]])
|
||||
{
|
||||
if (NSArray* items = [object itemArray])
|
||||
{
|
||||
if (NSMenuItem* item = findRecentFilesItem (items))
|
||||
{
|
||||
recentItem = [item retain];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~RecentFilesMenuItem()
|
||||
{
|
||||
[recentItem release];
|
||||
}
|
||||
|
||||
static NSMenuItem* findRecentFilesItem (NSArray* const items)
|
||||
{
|
||||
for (id object in items)
|
||||
if (NSArray* subMenuItems = [[object submenu] itemArray])
|
||||
for (id subObject in subMenuItems)
|
||||
if ([subObject isKindOfClass: [NSMenuItem class]])
|
||||
return subObject;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMenuItem* recentItem;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RecentFilesMenuItem)
|
||||
};
|
||||
|
||||
std::unique_ptr<RecentFilesMenuItem> recent;
|
||||
|
||||
//==============================================================================
|
||||
static NSMenuItem* findMenuItemWithCommandID (NSMenu* const menu, int commandID)
|
||||
{
|
||||
for (NSInteger i = [menu numberOfItems]; --i >= 0;)
|
||||
{
|
||||
NSMenuItem* m = [menu itemAtIndex: i];
|
||||
if (auto* menuItem = getJuceClassFromNSObject<PopupMenu::Item> ([m representedObject]))
|
||||
if (menuItem->itemID == commandID)
|
||||
return m;
|
||||
|
||||
if (NSMenu* sub = [m submenu])
|
||||
if (NSMenuItem* found = findMenuItemWithCommandID (sub, commandID))
|
||||
return found;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
static void flashMenuBar (NSMenu* menu)
|
||||
{
|
||||
if ([[menu title] isEqualToString: nsStringLiteral ("Apple")])
|
||||
return;
|
||||
|
||||
[menu retain];
|
||||
|
||||
const unichar f35Key = NSF35FunctionKey;
|
||||
NSString* f35String = [NSString stringWithCharacters: &f35Key length: 1];
|
||||
|
||||
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle: nsStringLiteral ("x")
|
||||
action: nil
|
||||
keyEquivalent: f35String];
|
||||
[item setTarget: nil];
|
||||
[menu insertItem: item atIndex: [menu numberOfItems]];
|
||||
[item release];
|
||||
|
||||
if ([menu indexOfItem: item] >= 0)
|
||||
{
|
||||
NSEvent* f35Event = [NSEvent keyEventWithType: NSEventTypeKeyDown
|
||||
location: NSZeroPoint
|
||||
modifierFlags: NSEventModifierFlagCommand
|
||||
timestamp: 0
|
||||
windowNumber: 0
|
||||
context: [NSGraphicsContext currentContext]
|
||||
characters: f35String
|
||||
charactersIgnoringModifiers: f35String
|
||||
isARepeat: NO
|
||||
keyCode: 0];
|
||||
|
||||
[menu performKeyEquivalent: f35Event];
|
||||
|
||||
if ([menu indexOfItem: item] >= 0)
|
||||
[menu removeItem: item]; // (this throws if the item isn't actually in the menu)
|
||||
}
|
||||
|
||||
[menu release];
|
||||
}
|
||||
|
||||
static unsigned int juceModsToNSMods (const ModifierKeys mods)
|
||||
{
|
||||
unsigned int m = 0;
|
||||
if (mods.isShiftDown()) m |= NSEventModifierFlagShift;
|
||||
if (mods.isCtrlDown()) m |= NSEventModifierFlagControl;
|
||||
if (mods.isAltDown()) m |= NSEventModifierFlagOption;
|
||||
if (mods.isCommandDown()) m |= NSEventModifierFlagCommand;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Apple Bug: For some reason [NSMenu removeAllItems] seems to leak it's objects
|
||||
// on shutdown, so we need this method to release the items one-by-one manually
|
||||
static void removeItemRecursive (NSMenu* parentMenu, int menuItemIndex)
|
||||
{
|
||||
if (isPositiveAndBelow (menuItemIndex, (int) [parentMenu numberOfItems]))
|
||||
{
|
||||
auto* menuItem = [parentMenu itemAtIndex:menuItemIndex];
|
||||
|
||||
if (auto* submenu = [menuItem submenu])
|
||||
removeItemRecursive (submenu);
|
||||
|
||||
[parentMenu removeItem:menuItem];
|
||||
}
|
||||
else
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
static void removeItemRecursive (NSMenu* menu)
|
||||
{
|
||||
if (menu != nullptr)
|
||||
{
|
||||
auto n = static_cast<int> ([menu numberOfItems]);
|
||||
|
||||
for (auto i = n; --i >= 0;)
|
||||
removeItemRecursive (menu, i);
|
||||
}
|
||||
}
|
||||
|
||||
static NSMenu* getMainMenuBar()
|
||||
{
|
||||
return JuceMainMenuBarHolder::getInstance()->mainMenuBar;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct JuceMenuCallbackClass : public ObjCClass<NSObject>
|
||||
{
|
||||
JuceMenuCallbackClass() : ObjCClass<NSObject> ("JUCEMainMenu_")
|
||||
{
|
||||
addIvar<JuceMainMenuHandler*> ("owner");
|
||||
|
||||
addMethod (@selector (menuItemInvoked:), menuItemInvoked, "v@:@");
|
||||
addMethod (@selector (menuNeedsUpdate:), menuNeedsUpdate, "v@:@");
|
||||
|
||||
#if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
|
||||
addProtocol (@protocol (NSMenuDelegate));
|
||||
#endif
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
static void setOwner (id self, JuceMainMenuHandler* owner)
|
||||
{
|
||||
object_setInstanceVariable (self, "owner", owner);
|
||||
}
|
||||
|
||||
private:
|
||||
static void menuItemInvoked (id self, SEL, NSMenuItem* item)
|
||||
{
|
||||
auto owner = getIvar<JuceMainMenuHandler*> (self, "owner");
|
||||
|
||||
if (auto* juceItem = getJuceClassFromNSObject<PopupMenu::Item> ([item representedObject]))
|
||||
{
|
||||
// If the menu is being triggered by a keypress, the OS will have picked it up before we had a chance to offer it to
|
||||
// our own components, which may have wanted to intercept it. So, rather than dispatching directly, we'll feed it back
|
||||
// into the focused component and let it trigger the menu item indirectly.
|
||||
NSEvent* e = [NSApp currentEvent];
|
||||
|
||||
if ([e type] == NSEventTypeKeyDown || [e type] == NSEventTypeKeyUp)
|
||||
{
|
||||
if (auto* focused = juce::Component::getCurrentlyFocusedComponent())
|
||||
{
|
||||
if (auto peer = dynamic_cast<juce::NSViewComponentPeer*> (focused->getPeer()))
|
||||
{
|
||||
if ([e type] == NSEventTypeKeyDown)
|
||||
peer->redirectKeyDown (e);
|
||||
else
|
||||
peer->redirectKeyUp (e);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
owner->invoke (*juceItem, static_cast<int> ([item tag]));
|
||||
}
|
||||
}
|
||||
|
||||
static void menuNeedsUpdate (id self, SEL, NSMenu* menu)
|
||||
{
|
||||
getIvar<JuceMainMenuHandler*> (self, "owner")->updateTopLevelMenu (menu);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
JuceMainMenuHandler* JuceMainMenuHandler::instance = nullptr;
|
||||
|
||||
//==============================================================================
|
||||
class TemporaryMainMenuWithStandardCommands
|
||||
{
|
||||
public:
|
||||
TemporaryMainMenuWithStandardCommands()
|
||||
: oldMenu (MenuBarModel::getMacMainMenu())
|
||||
{
|
||||
if (auto* appleMenu = MenuBarModel::getMacExtraAppleItemsMenu())
|
||||
oldAppleMenu.reset (new PopupMenu (*appleMenu));
|
||||
|
||||
if (auto* handler = JuceMainMenuHandler::instance)
|
||||
oldRecentItems = handler->recentItemsMenuName;
|
||||
|
||||
MenuBarModel::setMacMainMenu (nullptr);
|
||||
|
||||
if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
|
||||
{
|
||||
NSMenu* menu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Edit")];
|
||||
NSMenuItem* item;
|
||||
|
||||
item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Cut"), nil)
|
||||
action: @selector (cut:) keyEquivalent: nsStringLiteral ("x")];
|
||||
[menu addItem: item];
|
||||
[item release];
|
||||
|
||||
item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Copy"), nil)
|
||||
action: @selector (copy:) keyEquivalent: nsStringLiteral ("c")];
|
||||
[menu addItem: item];
|
||||
[item release];
|
||||
|
||||
item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Paste"), nil)
|
||||
action: @selector (paste:) keyEquivalent: nsStringLiteral ("v")];
|
||||
[menu addItem: item];
|
||||
[item release];
|
||||
|
||||
editMenuIndex = [mainMenu numberOfItems];
|
||||
|
||||
item = [mainMenu addItemWithTitle: NSLocalizedString (nsStringLiteral ("Edit"), nil)
|
||||
action: nil keyEquivalent: nsEmptyString()];
|
||||
[mainMenu setSubmenu: menu forItem: item];
|
||||
[menu release];
|
||||
}
|
||||
|
||||
// use a dummy modal component so that apps can tell that something is currently modal.
|
||||
dummyModalComponent.enterModalState (false);
|
||||
}
|
||||
|
||||
~TemporaryMainMenuWithStandardCommands()
|
||||
{
|
||||
if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
|
||||
[mainMenu removeItemAtIndex:editMenuIndex];
|
||||
|
||||
MenuBarModel::setMacMainMenu (oldMenu, oldAppleMenu.get(), oldRecentItems);
|
||||
}
|
||||
|
||||
private:
|
||||
MenuBarModel* const oldMenu;
|
||||
std::unique_ptr<PopupMenu> oldAppleMenu;
|
||||
String oldRecentItems;
|
||||
NSInteger editMenuIndex;
|
||||
|
||||
// The OS view already plays an alert when clicking outside
|
||||
// the modal comp, so this override avoids adding extra
|
||||
// inappropriate noises when the cancel button is pressed.
|
||||
// This override is also important because it stops the base class
|
||||
// calling ModalComponentManager::bringToFront, which can get
|
||||
// recursive when file dialogs are involved
|
||||
struct SilentDummyModalComp : public Component
|
||||
{
|
||||
SilentDummyModalComp() {}
|
||||
void inputAttemptWhenModal() override {}
|
||||
};
|
||||
|
||||
SilentDummyModalComp dummyModalComponent;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
namespace MainMenuHelpers
|
||||
{
|
||||
static NSString* translateMenuName (const String& name)
|
||||
{
|
||||
return NSLocalizedString (juceStringToNS (TRANS (name)), nil);
|
||||
}
|
||||
|
||||
static NSMenuItem* createMenuItem (NSMenu* menu, const String& name, SEL sel, NSString* key)
|
||||
{
|
||||
NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle: translateMenuName (name)
|
||||
action: sel
|
||||
keyEquivalent: key] autorelease];
|
||||
[item setTarget: NSApp];
|
||||
[menu addItem: item];
|
||||
return item;
|
||||
}
|
||||
|
||||
static void createStandardAppMenu (NSMenu* menu, const String& appName, const PopupMenu* extraItems)
|
||||
{
|
||||
if (extraItems != nullptr && JuceMainMenuHandler::instance != nullptr && extraItems->getNumItems() > 0)
|
||||
{
|
||||
for (PopupMenu::MenuItemIterator iter (*extraItems); iter.next();)
|
||||
JuceMainMenuHandler::instance->addMenuItem (iter, menu, 0, -1);
|
||||
|
||||
[menu addItem: [NSMenuItem separatorItem]];
|
||||
}
|
||||
|
||||
// Services...
|
||||
NSMenuItem* services = [[[NSMenuItem alloc] initWithTitle: translateMenuName ("Services")
|
||||
action: nil keyEquivalent: nsEmptyString()] autorelease];
|
||||
[menu addItem: services];
|
||||
|
||||
NSMenu* servicesMenu = [[[NSMenu alloc] initWithTitle: translateMenuName ("Services")] autorelease];
|
||||
[menu setSubmenu: servicesMenu forItem: services];
|
||||
[NSApp setServicesMenu: servicesMenu];
|
||||
[menu addItem: [NSMenuItem separatorItem]];
|
||||
|
||||
createMenuItem (menu, TRANS("Hide") + String (" ") + appName, @selector (hide:), nsStringLiteral ("h"));
|
||||
|
||||
[createMenuItem (menu, TRANS("Hide Others"), @selector (hideOtherApplications:), nsStringLiteral ("h"))
|
||||
setKeyEquivalentModifierMask: NSEventModifierFlagCommand | NSEventModifierFlagOption];
|
||||
|
||||
createMenuItem (menu, TRANS("Show All"), @selector (unhideAllApplications:), nsEmptyString());
|
||||
|
||||
[menu addItem: [NSMenuItem separatorItem]];
|
||||
|
||||
createMenuItem (menu, TRANS("Quit") + String (" ") + appName, @selector (terminate:), nsStringLiteral ("q"));
|
||||
}
|
||||
|
||||
// Since our app has no NIB, this initialises a standard app menu...
|
||||
static void rebuildMainMenu (const PopupMenu* extraItems)
|
||||
{
|
||||
// this can't be used in a plugin!
|
||||
jassert (JUCEApplicationBase::isStandaloneApp());
|
||||
|
||||
if (auto* app = JUCEApplicationBase::getInstance())
|
||||
{
|
||||
if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
|
||||
{
|
||||
if ([mainMenu numberOfItems] > 0)
|
||||
{
|
||||
if (auto* appMenu = [[mainMenu itemAtIndex:0] submenu])
|
||||
{
|
||||
[appMenu removeAllItems];
|
||||
MainMenuHelpers::createStandardAppMenu (appMenu, app->getApplicationName(), extraItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MenuBarModel::setMacMainMenu (MenuBarModel* newMenuBarModel,
|
||||
const PopupMenu* extraAppleMenuItems,
|
||||
const String& recentItemsMenuName)
|
||||
{
|
||||
if (getMacMainMenu() != newMenuBarModel)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
if (newMenuBarModel == nullptr)
|
||||
{
|
||||
delete JuceMainMenuHandler::instance;
|
||||
jassert (JuceMainMenuHandler::instance == nullptr); // should be zeroed in the destructor
|
||||
jassert (extraAppleMenuItems == nullptr); // you can't specify some extra items without also supplying a model
|
||||
|
||||
extraAppleMenuItems = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (JuceMainMenuHandler::instance == nullptr)
|
||||
JuceMainMenuHandler::instance = new JuceMainMenuHandler();
|
||||
|
||||
JuceMainMenuHandler::instance->setMenu (newMenuBarModel, extraAppleMenuItems, recentItemsMenuName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MainMenuHelpers::rebuildMainMenu (extraAppleMenuItems);
|
||||
|
||||
if (newMenuBarModel != nullptr)
|
||||
newMenuBarModel->menuItemsChanged();
|
||||
}
|
||||
|
||||
MenuBarModel* MenuBarModel::getMacMainMenu()
|
||||
{
|
||||
if (auto* mm = JuceMainMenuHandler::instance)
|
||||
return mm->currentModel;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const PopupMenu* MenuBarModel::getMacExtraAppleItemsMenu()
|
||||
{
|
||||
if (auto* mm = JuceMainMenuHandler::instance)
|
||||
return mm->extraAppleMenuItems.get();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
typedef void (*MenuTrackingChangedCallback) (bool);
|
||||
extern MenuTrackingChangedCallback menuTrackingChangedCallback;
|
||||
|
||||
static void mainMenuTrackingChanged (bool isTracking)
|
||||
{
|
||||
PopupMenu::dismissAllActiveMenus();
|
||||
|
||||
if (auto* menuHandler = JuceMainMenuHandler::instance)
|
||||
{
|
||||
menuHandler->isOpen = isTracking;
|
||||
|
||||
if (auto* model = menuHandler->currentModel)
|
||||
model->handleMenuBarActivate (isTracking);
|
||||
|
||||
if (menuHandler->defferedUpdateRequested && ! isTracking)
|
||||
{
|
||||
menuHandler->defferedUpdateRequested = false;
|
||||
menuHandler->menuBarItemsChanged (menuHandler->currentModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void juce_initialiseMacMainMenu()
|
||||
{
|
||||
menuTrackingChangedCallback = mainMenuTrackingChanged;
|
||||
|
||||
if (JuceMainMenuHandler::instance == nullptr)
|
||||
MainMenuHelpers::rebuildMainMenu (nullptr);
|
||||
}
|
||||
|
||||
// (used from other modules that need to create an NSMenu)
|
||||
NSMenu* createNSMenu (const PopupMenu&, const String&, int, int, bool);
|
||||
NSMenu* createNSMenu (const PopupMenu& menu, const String& name, int topLevelMenuId, int topLevelIndex, bool addDelegate)
|
||||
{
|
||||
juce_initialiseMacMainMenu();
|
||||
|
||||
if (auto* mm = JuceMainMenuHandler::instance)
|
||||
return mm->createMenu (menu, name, topLevelMenuId, topLevelIndex, addDelegate);
|
||||
|
||||
jassertfalse; // calling this before making sure the OSX main menu stuff was initialised?
|
||||
return nil;
|
||||
}
|
||||
|
||||
} // namespace juce
|
192
modules/juce_gui_basics/native/juce_mac_MouseCursor.mm
Normal file
192
modules/juce_gui_basics/native/juce_mac_MouseCursor.mm
Normal file
@ -0,0 +1,192 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
#if JUCE_MAC
|
||||
|
||||
//==============================================================================
|
||||
namespace MouseCursorHelpers
|
||||
{
|
||||
static NSCursor* fromNSImage (NSImage* im, NSPoint hotspot)
|
||||
{
|
||||
NSCursor* c = [[NSCursor alloc] initWithImage: im
|
||||
hotSpot: hotspot];
|
||||
[im release];
|
||||
return c;
|
||||
}
|
||||
|
||||
static void* fromHIServices (const char* filename)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
auto cursorPath = String ("/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/"
|
||||
"HIServices.framework/Versions/A/Resources/cursors/")
|
||||
+ filename;
|
||||
|
||||
NSImage* originalImage = [[NSImage alloc] initByReferencingFile: juceStringToNS (cursorPath + "/cursor.pdf")];
|
||||
NSSize originalSize = [originalImage size];
|
||||
NSImage* resultImage = [[NSImage alloc] initWithSize: originalSize];
|
||||
|
||||
for (int scale = 1; scale <= 4; ++scale)
|
||||
{
|
||||
NSAffineTransform* scaleTransform = [NSAffineTransform transform];
|
||||
[scaleTransform scaleBy: (float) scale];
|
||||
|
||||
if (CGImageRef rasterCGImage = [originalImage CGImageForProposedRect: nil
|
||||
context: nil
|
||||
hints: [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
NSImageHintCTM, scaleTransform, nil]])
|
||||
{
|
||||
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithCGImage: rasterCGImage];
|
||||
[imageRep setSize: originalSize];
|
||||
|
||||
[resultImage addRepresentation: imageRep];
|
||||
[imageRep release];
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
[originalImage release];
|
||||
|
||||
NSDictionary* info = [NSDictionary dictionaryWithContentsOfFile: juceStringToNS (cursorPath + "/info.plist")];
|
||||
|
||||
auto hotspotX = (float) [[info valueForKey: nsStringLiteral ("hotx")] doubleValue];
|
||||
auto hotspotY = (float) [[info valueForKey: nsStringLiteral ("hoty")] doubleValue];
|
||||
|
||||
return fromNSImage (resultImage, NSMakePoint (hotspotX, hotspotY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void* CustomMouseCursorInfo::create() const
|
||||
{
|
||||
return MouseCursorHelpers::fromNSImage (imageToNSImage (image, scaleFactor),
|
||||
NSMakePoint (hotspot.x, hotspot.y));
|
||||
}
|
||||
|
||||
void* MouseCursor::createStandardMouseCursor (MouseCursor::StandardCursorType type)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
NSCursor* c = nil;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case NormalCursor:
|
||||
case ParentCursor: c = [NSCursor arrowCursor]; break;
|
||||
case NoCursor: return CustomMouseCursorInfo (Image (Image::ARGB, 8, 8, true), {}).create();
|
||||
case DraggingHandCursor: c = [NSCursor openHandCursor]; break;
|
||||
case WaitCursor: c = [NSCursor arrowCursor]; break; // avoid this on the mac, let the OS provide the beachball
|
||||
case IBeamCursor: c = [NSCursor IBeamCursor]; break;
|
||||
case PointingHandCursor: c = [NSCursor pointingHandCursor]; break;
|
||||
case LeftEdgeResizeCursor: c = [NSCursor resizeLeftCursor]; break;
|
||||
case RightEdgeResizeCursor: c = [NSCursor resizeRightCursor]; break;
|
||||
case CrosshairCursor: c = [NSCursor crosshairCursor]; break;
|
||||
|
||||
case CopyingCursor:
|
||||
{
|
||||
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_6
|
||||
if (void* m = MouseCursorHelpers::fromHIServices ("copy"))
|
||||
return m;
|
||||
#endif
|
||||
|
||||
c = [NSCursor dragCopyCursor]; // added in 10.6
|
||||
break;
|
||||
}
|
||||
|
||||
case UpDownResizeCursor:
|
||||
case TopEdgeResizeCursor:
|
||||
case BottomEdgeResizeCursor:
|
||||
if (void* m = MouseCursorHelpers::fromHIServices ("resizenorthsouth"))
|
||||
return m;
|
||||
|
||||
c = [NSCursor resizeUpDownCursor];
|
||||
break;
|
||||
|
||||
case LeftRightResizeCursor:
|
||||
if (void* m = MouseCursorHelpers::fromHIServices ("resizeeastwest"))
|
||||
return m;
|
||||
|
||||
c = [NSCursor resizeLeftRightCursor];
|
||||
break;
|
||||
|
||||
case TopLeftCornerResizeCursor:
|
||||
case BottomRightCornerResizeCursor:
|
||||
return MouseCursorHelpers::fromHIServices ("resizenorthwestsoutheast");
|
||||
|
||||
case TopRightCornerResizeCursor:
|
||||
case BottomLeftCornerResizeCursor:
|
||||
return MouseCursorHelpers::fromHIServices ("resizenortheastsouthwest");
|
||||
|
||||
case UpDownLeftRightResizeCursor:
|
||||
return MouseCursorHelpers::fromHIServices ("move");
|
||||
|
||||
default:
|
||||
jassertfalse;
|
||||
break;
|
||||
}
|
||||
|
||||
[c retain];
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
void MouseCursor::deleteMouseCursor (void* const cursorHandle, const bool /*isStandard*/)
|
||||
{
|
||||
[((NSCursor*) cursorHandle) release];
|
||||
}
|
||||
|
||||
void MouseCursor::showInAllWindows() const
|
||||
{
|
||||
showInWindow (nullptr);
|
||||
}
|
||||
|
||||
void MouseCursor::showInWindow (ComponentPeer*) const
|
||||
{
|
||||
auto c = (NSCursor*) getHandle();
|
||||
|
||||
if (c == nil)
|
||||
c = [NSCursor arrowCursor];
|
||||
|
||||
[c set];
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void* CustomMouseCursorInfo::create() const { return nullptr; }
|
||||
void* MouseCursor::createStandardMouseCursor (MouseCursor::StandardCursorType) { return nullptr; }
|
||||
void MouseCursor::deleteMouseCursor (void*, bool) {}
|
||||
void MouseCursor::showInAllWindows() const {}
|
||||
void MouseCursor::showInWindow (ComponentPeer*) const {}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
2234
modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm
Normal file
2234
modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm
Normal file
File diff suppressed because it is too large
Load Diff
609
modules/juce_gui_basics/native/juce_mac_Windowing.mm
Normal file
609
modules/juce_gui_basics/native/juce_mac_Windowing.mm
Normal file
@ -0,0 +1,609 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
void LookAndFeel::playAlertSound()
|
||||
{
|
||||
NSBeep();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
class OSXMessageBox : private AsyncUpdater
|
||||
{
|
||||
public:
|
||||
OSXMessageBox (AlertWindow::AlertIconType type, const String& t, const String& m,
|
||||
const char* b1, const char* b2, const char* b3,
|
||||
ModalComponentManager::Callback* c, const bool runAsync)
|
||||
: iconType (type), title (t), message (m), callback (c),
|
||||
button1 (b1), button2 (b2), button3 (b3)
|
||||
{
|
||||
if (runAsync)
|
||||
triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
int getResult() const
|
||||
{
|
||||
switch (getRawResult())
|
||||
{
|
||||
case NSAlertFirstButtonReturn: return 1;
|
||||
case NSAlertThirdButtonReturn: return 2;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int show (AlertWindow::AlertIconType iconType, const String& title, const String& message,
|
||||
ModalComponentManager::Callback* callback, const char* b1, const char* b2, const char* b3,
|
||||
bool runAsync)
|
||||
{
|
||||
std::unique_ptr<OSXMessageBox> mb (new OSXMessageBox (iconType, title, message, b1, b2, b3,
|
||||
callback, runAsync));
|
||||
if (! runAsync)
|
||||
return mb->getResult();
|
||||
|
||||
mb.release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
AlertWindow::AlertIconType iconType;
|
||||
String title, message;
|
||||
std::unique_ptr<ModalComponentManager::Callback> callback;
|
||||
const char* button1;
|
||||
const char* button2;
|
||||
const char* button3;
|
||||
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
auto result = getResult();
|
||||
|
||||
if (callback != nullptr)
|
||||
callback->modalStateFinished (result);
|
||||
|
||||
delete this;
|
||||
}
|
||||
|
||||
NSInteger getRawResult() const
|
||||
{
|
||||
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
|
||||
|
||||
[alert setMessageText: juceStringToNS (title)];
|
||||
[alert setInformativeText: juceStringToNS (message)];
|
||||
|
||||
[alert setAlertStyle: iconType == AlertWindow::WarningIcon ? NSAlertStyleCritical
|
||||
: NSAlertStyleInformational];
|
||||
addButton (alert, button1);
|
||||
addButton (alert, button2);
|
||||
addButton (alert, button3);
|
||||
|
||||
return [alert runModal];
|
||||
}
|
||||
|
||||
static void addButton (NSAlert* alert, const char* button)
|
||||
{
|
||||
if (button != nullptr)
|
||||
[alert addButtonWithTitle: juceStringToNS (TRANS (button))];
|
||||
}
|
||||
};
|
||||
|
||||
#if JUCE_MODAL_LOOPS_PERMITTED
|
||||
void JUCE_CALLTYPE NativeMessageBox::showMessageBox (AlertWindow::AlertIconType iconType,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/)
|
||||
{
|
||||
OSXMessageBox::show (iconType, title, message, nullptr, "OK", nullptr, nullptr, false);
|
||||
}
|
||||
#endif
|
||||
|
||||
void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync (AlertWindow::AlertIconType iconType,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/,
|
||||
ModalComponentManager::Callback* callback)
|
||||
{
|
||||
OSXMessageBox::show (iconType, title, message, callback, "OK", nullptr, nullptr, true);
|
||||
}
|
||||
|
||||
bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox (AlertWindow::AlertIconType iconType,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/,
|
||||
ModalComponentManager::Callback* callback)
|
||||
{
|
||||
return OSXMessageBox::show (iconType, title, message, callback,
|
||||
"OK", "Cancel", nullptr, callback != nullptr) == 1;
|
||||
}
|
||||
|
||||
int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (AlertWindow::AlertIconType iconType,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/,
|
||||
ModalComponentManager::Callback* callback)
|
||||
{
|
||||
return OSXMessageBox::show (iconType, title, message, callback,
|
||||
"Yes", "Cancel", "No", callback != nullptr);
|
||||
}
|
||||
|
||||
int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (AlertWindow::AlertIconType iconType,
|
||||
const String& title, const String& message,
|
||||
Component* /*associatedComponent*/,
|
||||
ModalComponentManager::Callback* callback)
|
||||
{
|
||||
return OSXMessageBox::show (iconType, title, message, callback,
|
||||
"Yes", "No", nullptr, callback != nullptr);
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
static NSRect getDragRect (NSView* view, NSEvent* event)
|
||||
{
|
||||
auto eventPos = [event locationInWindow];
|
||||
|
||||
return [view convertRect: NSMakeRect (eventPos.x - 16.0f, eventPos.y - 16.0f, 32.0f, 32.0f)
|
||||
fromView: nil];
|
||||
}
|
||||
|
||||
static NSView* getNSViewForDragEvent (Component* sourceComp)
|
||||
{
|
||||
if (sourceComp == nullptr)
|
||||
if (auto* draggingSource = Desktop::getInstance().getDraggingMouseSource(0))
|
||||
sourceComp = draggingSource->getComponentUnderMouse();
|
||||
|
||||
if (sourceComp != nullptr)
|
||||
return (NSView*) sourceComp->getWindowHandle();
|
||||
|
||||
jassertfalse; // This method must be called in response to a component's mouseDown or mouseDrag event!
|
||||
return nil;
|
||||
}
|
||||
|
||||
struct TextDragDataProviderClass : public ObjCClass<NSObject>
|
||||
{
|
||||
TextDragDataProviderClass() : ObjCClass<NSObject> ("JUCE_NSTextDragDataProvider_")
|
||||
{
|
||||
addIvar<String*> ("text");
|
||||
addMethod (@selector (dealloc), dealloc, "v@:");
|
||||
addMethod (@selector (pasteboard:item:provideDataForType:), provideDataForType, "v@:@@@");
|
||||
addProtocol (@protocol (NSPasteboardItemDataProvider));
|
||||
registerClass();
|
||||
}
|
||||
|
||||
static void setText (id self, const String& text)
|
||||
{
|
||||
object_setInstanceVariable (self, "text", new String (text));
|
||||
}
|
||||
|
||||
private:
|
||||
static void dealloc (id self, SEL)
|
||||
{
|
||||
delete getIvar<String*> (self, "text");
|
||||
sendSuperclassMessage (self, @selector (dealloc));
|
||||
}
|
||||
|
||||
static void provideDataForType (id self, SEL, NSPasteboard* sender, NSPasteboardItem*, NSString* type)
|
||||
{
|
||||
if ([type compare: NSPasteboardTypeString] == NSOrderedSame)
|
||||
if (auto* text = getIvar<String*> (self, "text"))
|
||||
[sender setData: [juceStringToNS (*text) dataUsingEncoding: NSUTF8StringEncoding]
|
||||
forType: NSPasteboardTypeString];
|
||||
}
|
||||
};
|
||||
|
||||
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component* sourceComponent)
|
||||
{
|
||||
if (text.isEmpty())
|
||||
return false;
|
||||
|
||||
if (auto* view = getNSViewForDragEvent (sourceComponent))
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
if (auto* event = [[view window] currentEvent])
|
||||
{
|
||||
static TextDragDataProviderClass dataProviderClass;
|
||||
id delegate = [dataProviderClass.createInstance() init];
|
||||
TextDragDataProviderClass::setText (delegate, text);
|
||||
|
||||
auto* pasteboardItem = [[NSPasteboardItem new] autorelease];
|
||||
[pasteboardItem setDataProvider: delegate
|
||||
forTypes: [NSArray arrayWithObjects: NSPasteboardTypeString, nil]];
|
||||
|
||||
auto* dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter: pasteboardItem] autorelease];
|
||||
|
||||
NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile: nsEmptyString()];
|
||||
[dragItem setDraggingFrame: getDragRect (view, event) contents: image];
|
||||
|
||||
auto* draggingSession = [view beginDraggingSessionWithItems: [NSArray arrayWithObject: dragItem]
|
||||
event: event
|
||||
source: delegate];
|
||||
|
||||
draggingSession.animatesToStartingPositionsOnCancelOrFail = YES;
|
||||
draggingSession.draggingFormation = NSDraggingFormationNone;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
struct NSDraggingSourceHelper : public ObjCClass<NSObject<NSDraggingSource>>
|
||||
{
|
||||
NSDraggingSourceHelper() : ObjCClass<NSObject<NSDraggingSource>> ("JUCENSDraggingSourceHelper_")
|
||||
{
|
||||
addMethod (@selector (draggingSession:sourceOperationMaskForDraggingContext:), sourceOperationMaskForDraggingContext, "c@:@@");
|
||||
registerClass();
|
||||
}
|
||||
|
||||
static NSDragOperation sourceOperationMaskForDraggingContext (id, SEL, NSDraggingSession*, NSDraggingContext)
|
||||
{
|
||||
return NSDragOperationCopy;
|
||||
}
|
||||
};
|
||||
|
||||
static NSDraggingSourceHelper draggingSourceHelper;
|
||||
|
||||
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, bool /*canMoveFiles*/,
|
||||
Component* sourceComponent)
|
||||
{
|
||||
if (files.isEmpty())
|
||||
return false;
|
||||
|
||||
if (auto* view = getNSViewForDragEvent (sourceComponent))
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
if (auto* event = [[view window] currentEvent])
|
||||
{
|
||||
auto* dragItems = [[[NSMutableArray alloc] init] autorelease];
|
||||
|
||||
for (auto& filename : files)
|
||||
{
|
||||
auto* nsFilename = juceStringToNS (filename);
|
||||
auto* fileURL = [NSURL fileURLWithPath: nsFilename];
|
||||
auto* dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: fileURL];
|
||||
|
||||
auto eventPos = [event locationInWindow];
|
||||
auto dragRect = [view convertRect: NSMakeRect (eventPos.x - 16.0f, eventPos.y - 16.0f, 32.0f, 32.0f)
|
||||
fromView: nil];
|
||||
auto* dragImage = [[NSWorkspace sharedWorkspace] iconForFile: nsFilename];
|
||||
[dragItem setDraggingFrame: dragRect
|
||||
contents: dragImage];
|
||||
|
||||
[dragItems addObject: dragItem];
|
||||
[dragItem release];
|
||||
}
|
||||
|
||||
auto* helper = [draggingSourceHelper.createInstance() autorelease];
|
||||
|
||||
return [view beginDraggingSessionWithItems: dragItems
|
||||
event: event
|
||||
source: helper];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool Desktop::canUseSemiTransparentWindows() noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Point<float> MouseInputSource::getCurrentRawMousePosition()
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
auto p = [NSEvent mouseLocation];
|
||||
return { (float) p.x, (float) (getMainScreenHeight() - p.y) };
|
||||
}
|
||||
}
|
||||
|
||||
void MouseInputSource::setRawMousePosition (Point<float> newPosition)
|
||||
{
|
||||
// this rubbish needs to be done around the warp call, to avoid causing a
|
||||
// bizarre glitch..
|
||||
CGAssociateMouseAndMouseCursorPosition (false);
|
||||
CGWarpMouseCursorPosition (convertToCGPoint (newPosition));
|
||||
CGAssociateMouseAndMouseCursorPosition (true);
|
||||
}
|
||||
|
||||
double Desktop::getDefaultMasterScale()
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
Desktop::DisplayOrientation Desktop::getCurrentOrientation() const
|
||||
{
|
||||
return upright;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
#if defined (MAC_OS_X_VERSION_10_7) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_7)
|
||||
#define JUCE_USE_IOPM_SCREENSAVER_DEFEAT 1
|
||||
#endif
|
||||
|
||||
#if ! (defined (JUCE_USE_IOPM_SCREENSAVER_DEFEAT) || defined (__POWER__))
|
||||
extern "C" { extern OSErr UpdateSystemActivity (UInt8); } // Some versions of the SDK omit this function..
|
||||
#endif
|
||||
|
||||
class ScreenSaverDefeater : public Timer
|
||||
{
|
||||
public:
|
||||
#if JUCE_USE_IOPM_SCREENSAVER_DEFEAT
|
||||
ScreenSaverDefeater()
|
||||
{
|
||||
startTimer (5000);
|
||||
timerCallback();
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
if (Process::isForegroundProcess())
|
||||
{
|
||||
if (assertion == nullptr)
|
||||
assertion.reset (new PMAssertion());
|
||||
}
|
||||
else
|
||||
{
|
||||
assertion.reset();
|
||||
}
|
||||
}
|
||||
|
||||
struct PMAssertion
|
||||
{
|
||||
PMAssertion() : assertionID (kIOPMNullAssertionID)
|
||||
{
|
||||
IOReturn res = IOPMAssertionCreateWithName (kIOPMAssertionTypePreventUserIdleDisplaySleep,
|
||||
kIOPMAssertionLevelOn,
|
||||
CFSTR ("JUCE Playback"),
|
||||
&assertionID);
|
||||
jassert (res == kIOReturnSuccess); ignoreUnused (res);
|
||||
}
|
||||
|
||||
~PMAssertion()
|
||||
{
|
||||
if (assertionID != kIOPMNullAssertionID)
|
||||
IOPMAssertionRelease (assertionID);
|
||||
}
|
||||
|
||||
IOPMAssertionID assertionID;
|
||||
};
|
||||
|
||||
std::unique_ptr<PMAssertion> assertion;
|
||||
#else
|
||||
ScreenSaverDefeater()
|
||||
{
|
||||
startTimer (10000);
|
||||
timerCallback();
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
if (Process::isForegroundProcess())
|
||||
UpdateSystemActivity (1 /*UsrActivity*/);
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
static std::unique_ptr<ScreenSaverDefeater> screenSaverDefeater;
|
||||
|
||||
void Desktop::setScreenSaverEnabled (const bool isEnabled)
|
||||
{
|
||||
if (isEnabled)
|
||||
screenSaverDefeater.reset();
|
||||
else if (screenSaverDefeater == nullptr)
|
||||
screenSaverDefeater.reset (new ScreenSaverDefeater());
|
||||
}
|
||||
|
||||
bool Desktop::isScreenSaverEnabled()
|
||||
{
|
||||
return screenSaverDefeater == nullptr;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct DisplaySettingsChangeCallback : private DeletedAtShutdown
|
||||
{
|
||||
DisplaySettingsChangeCallback()
|
||||
{
|
||||
CGDisplayRegisterReconfigurationCallback (displayReconfigurationCallBack, 0);
|
||||
}
|
||||
|
||||
~DisplaySettingsChangeCallback()
|
||||
{
|
||||
CGDisplayRemoveReconfigurationCallback (displayReconfigurationCallBack, 0);
|
||||
clearSingletonInstance();
|
||||
}
|
||||
|
||||
static void displayReconfigurationCallBack (CGDirectDisplayID, CGDisplayChangeSummaryFlags, void*)
|
||||
{
|
||||
const_cast<Desktop::Displays&> (Desktop::getInstance().getDisplays()).refresh();
|
||||
}
|
||||
|
||||
JUCE_DECLARE_SINGLETON_SINGLETHREADED_MINIMAL (DisplaySettingsChangeCallback)
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DisplaySettingsChangeCallback)
|
||||
};
|
||||
|
||||
JUCE_IMPLEMENT_SINGLETON (DisplaySettingsChangeCallback)
|
||||
|
||||
static Rectangle<int> convertDisplayRect (NSRect r, CGFloat mainScreenBottom)
|
||||
{
|
||||
r.origin.y = mainScreenBottom - (r.origin.y + r.size.height);
|
||||
return convertToRectInt (r);
|
||||
}
|
||||
|
||||
static Desktop::Displays::Display getDisplayFromScreen (NSScreen* s, CGFloat& mainScreenBottom, const float masterScale)
|
||||
{
|
||||
Desktop::Displays::Display d;
|
||||
|
||||
d.isMain = (mainScreenBottom == 0);
|
||||
|
||||
if (d.isMain)
|
||||
mainScreenBottom = [s frame].size.height;
|
||||
|
||||
d.userArea = convertDisplayRect ([s visibleFrame], mainScreenBottom) / masterScale;
|
||||
d.totalArea = convertDisplayRect ([s frame], mainScreenBottom) / masterScale;
|
||||
d.scale = masterScale;
|
||||
|
||||
#if defined (MAC_OS_X_VERSION_10_7) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7)
|
||||
if ([s respondsToSelector: @selector (backingScaleFactor)])
|
||||
d.scale *= s.backingScaleFactor;
|
||||
#endif
|
||||
|
||||
NSSize dpi = [[[s deviceDescription] objectForKey: NSDeviceResolution] sizeValue];
|
||||
d.dpi = (dpi.width + dpi.height) / 2.0;
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
void Desktop::Displays::findDisplays (const float masterScale)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
DisplaySettingsChangeCallback::getInstance();
|
||||
|
||||
CGFloat mainScreenBottom = 0;
|
||||
|
||||
for (NSScreen* s in [NSScreen screens])
|
||||
displays.add (getDisplayFromScreen (s, mainScreenBottom, masterScale));
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool juce_areThereAnyAlwaysOnTopWindows()
|
||||
{
|
||||
for (NSWindow* window in [NSApp windows])
|
||||
if ([window level] > NSNormalWindowLevel)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static void selectImageForDrawing (const Image& image)
|
||||
{
|
||||
[NSGraphicsContext saveGraphicsState];
|
||||
[NSGraphicsContext setCurrentContext: [NSGraphicsContext graphicsContextWithGraphicsPort: juce_getImageContext (image)
|
||||
flipped: false]];
|
||||
}
|
||||
|
||||
static void releaseImageAfterDrawing()
|
||||
{
|
||||
[[NSGraphicsContext currentContext] flushGraphics];
|
||||
[NSGraphicsContext restoreGraphicsState];
|
||||
}
|
||||
|
||||
Image juce_createIconForFile (const File& file)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile: juceStringToNS (file.getFullPathName())];
|
||||
|
||||
Image result (Image::ARGB, (int) [image size].width, (int) [image size].height, true);
|
||||
|
||||
selectImageForDrawing (result);
|
||||
[image drawAtPoint: NSMakePoint (0, 0)
|
||||
fromRect: NSMakeRect (0, 0, [image size].width, [image size].height)
|
||||
operation: NSCompositingOperationSourceOver fraction: 1.0f];
|
||||
releaseImageAfterDrawing();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static Image createNSWindowSnapshot (NSWindow* nsWindow)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
CGImageRef screenShot = CGWindowListCreateImage (CGRectNull,
|
||||
kCGWindowListOptionIncludingWindow,
|
||||
(CGWindowID) [nsWindow windowNumber],
|
||||
kCGWindowImageBoundsIgnoreFraming);
|
||||
|
||||
NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage: screenShot];
|
||||
|
||||
Image result (Image::ARGB, (int) [bitmapRep size].width, (int) [bitmapRep size].height, true);
|
||||
|
||||
selectImageForDrawing (result);
|
||||
[bitmapRep drawAtPoint: NSMakePoint (0, 0)];
|
||||
releaseImageAfterDrawing();
|
||||
|
||||
[bitmapRep release];
|
||||
CGImageRelease (screenShot);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Image createSnapshotOfNativeWindow (void*);
|
||||
Image createSnapshotOfNativeWindow (void* nativeWindowHandle)
|
||||
{
|
||||
if (id windowOrView = (id) nativeWindowHandle)
|
||||
{
|
||||
if ([windowOrView isKindOfClass: [NSWindow class]])
|
||||
return createNSWindowSnapshot ((NSWindow*) windowOrView);
|
||||
|
||||
if ([windowOrView isKindOfClass: [NSView class]])
|
||||
return createNSWindowSnapshot ([(NSView*) windowOrView window]);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void SystemClipboard::copyTextToClipboard (const String& text)
|
||||
{
|
||||
NSPasteboard* pb = [NSPasteboard generalPasteboard];
|
||||
|
||||
[pb declareTypes: [NSArray arrayWithObject: NSStringPboardType]
|
||||
owner: nil];
|
||||
|
||||
[pb setString: juceStringToNS (text)
|
||||
forType: NSStringPboardType];
|
||||
}
|
||||
|
||||
String SystemClipboard::getTextFromClipboard()
|
||||
{
|
||||
return nsStringToJuce ([[NSPasteboard generalPasteboard] stringForType: NSStringPboardType]);
|
||||
}
|
||||
|
||||
void Process::setDockIconVisible (bool isVisible)
|
||||
{
|
||||
ProcessSerialNumber psn { 0, kCurrentProcess };
|
||||
|
||||
OSStatus err = TransformProcessType (&psn, isVisible ? kProcessTransformToForegroundApplication
|
||||
: kProcessTransformToUIElementApplication);
|
||||
jassert (err == 0);
|
||||
ignoreUnused (err);
|
||||
}
|
||||
|
||||
bool Desktop::isOSXDarkModeActive()
|
||||
{
|
||||
return [[[NSUserDefaults standardUserDefaults] stringForKey: nsStringLiteral ("AppleInterfaceStyle")]
|
||||
isEqualToString: nsStringLiteral ("Dark")];
|
||||
}
|
||||
|
||||
} // namespace juce
|
289
modules/juce_gui_basics/native/juce_win32_DragAndDrop.cpp
Normal file
289
modules/juce_gui_basics/native/juce_win32_DragAndDrop.cpp
Normal file
@ -0,0 +1,289 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
namespace DragAndDropHelpers
|
||||
{
|
||||
//==============================================================================
|
||||
struct JuceDropSource : public ComBaseClassHelper<IDropSource>
|
||||
{
|
||||
JuceDropSource() {}
|
||||
|
||||
JUCE_COMRESULT QueryContinueDrag (BOOL escapePressed, DWORD keys) override
|
||||
{
|
||||
if (escapePressed)
|
||||
return DRAGDROP_S_CANCEL;
|
||||
|
||||
if ((keys & (MK_LBUTTON | MK_RBUTTON)) == 0)
|
||||
return DRAGDROP_S_DROP;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT GiveFeedback (DWORD) override
|
||||
{
|
||||
return DRAGDROP_S_USEDEFAULTCURSORS;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct JuceEnumFormatEtc : public ComBaseClassHelper<IEnumFORMATETC>
|
||||
{
|
||||
JuceEnumFormatEtc (const FORMATETC* f) : format (f) {}
|
||||
|
||||
JUCE_COMRESULT Clone (IEnumFORMATETC** result) override
|
||||
{
|
||||
if (result == 0)
|
||||
return E_POINTER;
|
||||
|
||||
auto newOne = new JuceEnumFormatEtc (format);
|
||||
newOne->index = index;
|
||||
*result = newOne;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT Next (ULONG celt, LPFORMATETC lpFormatEtc, ULONG* pceltFetched) override
|
||||
{
|
||||
if (pceltFetched != nullptr)
|
||||
*pceltFetched = 0;
|
||||
else if (celt != 1)
|
||||
return S_FALSE;
|
||||
|
||||
if (index == 0 && celt > 0 && lpFormatEtc != 0)
|
||||
{
|
||||
copyFormatEtc (lpFormatEtc [0], *format);
|
||||
++index;
|
||||
|
||||
if (pceltFetched != nullptr)
|
||||
*pceltFetched = 1;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT Skip (ULONG celt) override
|
||||
{
|
||||
if (index + (int) celt >= 1)
|
||||
return S_FALSE;
|
||||
|
||||
index += celt;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT Reset() override
|
||||
{
|
||||
index = 0;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
const FORMATETC* const format;
|
||||
int index = 0;
|
||||
|
||||
static void copyFormatEtc (FORMATETC& dest, const FORMATETC& source)
|
||||
{
|
||||
dest = source;
|
||||
|
||||
if (source.ptd != 0)
|
||||
{
|
||||
dest.ptd = (DVTARGETDEVICE*) CoTaskMemAlloc (sizeof (DVTARGETDEVICE));
|
||||
*(dest.ptd) = *(source.ptd);
|
||||
}
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (JuceEnumFormatEtc)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class JuceDataObject : public ComBaseClassHelper <IDataObject>
|
||||
{
|
||||
public:
|
||||
JuceDataObject (JuceDropSource* s, const FORMATETC* f, const STGMEDIUM* m)
|
||||
: dropSource (s), format (f), medium (m)
|
||||
{
|
||||
}
|
||||
|
||||
~JuceDataObject()
|
||||
{
|
||||
jassert (refCount == 0);
|
||||
}
|
||||
|
||||
JUCE_COMRESULT GetData (FORMATETC* pFormatEtc, STGMEDIUM* pMedium)
|
||||
{
|
||||
if ((pFormatEtc->tymed & format->tymed) != 0
|
||||
&& pFormatEtc->cfFormat == format->cfFormat
|
||||
&& pFormatEtc->dwAspect == format->dwAspect)
|
||||
{
|
||||
pMedium->tymed = format->tymed;
|
||||
pMedium->pUnkForRelease = 0;
|
||||
|
||||
if (format->tymed == TYMED_HGLOBAL)
|
||||
{
|
||||
auto len = GlobalSize (medium->hGlobal);
|
||||
void* const src = GlobalLock (medium->hGlobal);
|
||||
void* const dst = GlobalAlloc (GMEM_FIXED, len);
|
||||
|
||||
memcpy (dst, src, len);
|
||||
|
||||
GlobalUnlock (medium->hGlobal);
|
||||
|
||||
pMedium->hGlobal = dst;
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return DV_E_FORMATETC;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT QueryGetData (FORMATETC* f)
|
||||
{
|
||||
if (f == 0)
|
||||
return E_INVALIDARG;
|
||||
|
||||
if (f->tymed == format->tymed
|
||||
&& f->cfFormat == format->cfFormat
|
||||
&& f->dwAspect == format->dwAspect)
|
||||
return S_OK;
|
||||
|
||||
return DV_E_FORMATETC;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT GetCanonicalFormatEtc (FORMATETC*, FORMATETC* pFormatEtcOut)
|
||||
{
|
||||
pFormatEtcOut->ptd = 0;
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT EnumFormatEtc (DWORD direction, IEnumFORMATETC** result)
|
||||
{
|
||||
if (result == 0)
|
||||
return E_POINTER;
|
||||
|
||||
if (direction == DATADIR_GET)
|
||||
{
|
||||
*result = new JuceEnumFormatEtc (format);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
*result = 0;
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT GetDataHere (FORMATETC*, STGMEDIUM*) { return DATA_E_FORMATETC; }
|
||||
JUCE_COMRESULT SetData (FORMATETC*, STGMEDIUM*, BOOL) { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT DAdvise (FORMATETC*, DWORD, IAdviseSink*, DWORD*) { return OLE_E_ADVISENOTSUPPORTED; }
|
||||
JUCE_COMRESULT DUnadvise (DWORD) { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT EnumDAdvise (IEnumSTATDATA**) { return OLE_E_ADVISENOTSUPPORTED; }
|
||||
|
||||
private:
|
||||
JuceDropSource* const dropSource;
|
||||
const FORMATETC* const format;
|
||||
const STGMEDIUM* const medium;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (JuceDataObject)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
HDROP createHDrop (const StringArray& fileNames)
|
||||
{
|
||||
int totalBytes = 0;
|
||||
for (int i = fileNames.size(); --i >= 0;)
|
||||
totalBytes += (int) CharPointer_UTF16::getBytesRequiredFor (fileNames[i].getCharPointer()) + sizeof (WCHAR);
|
||||
|
||||
HDROP hDrop = (HDROP) GlobalAlloc (GMEM_MOVEABLE | GMEM_ZEROINIT, sizeof (DROPFILES) + totalBytes + 4);
|
||||
|
||||
if (hDrop != 0)
|
||||
{
|
||||
auto pDropFiles = (LPDROPFILES) GlobalLock (hDrop);
|
||||
pDropFiles->pFiles = sizeof (DROPFILES);
|
||||
pDropFiles->fWide = true;
|
||||
|
||||
auto* fname = reinterpret_cast<WCHAR*> (addBytesToPointer (pDropFiles, sizeof (DROPFILES)));
|
||||
|
||||
for (int i = 0; i < fileNames.size(); ++i)
|
||||
{
|
||||
auto bytesWritten = fileNames[i].copyToUTF16 (fname, 2048);
|
||||
fname = reinterpret_cast<WCHAR*> (addBytesToPointer (fname, bytesWritten));
|
||||
}
|
||||
|
||||
*fname = 0;
|
||||
|
||||
GlobalUnlock (hDrop);
|
||||
}
|
||||
|
||||
return hDrop;
|
||||
}
|
||||
|
||||
bool performDragDrop (FORMATETC* const format, STGMEDIUM* const medium, const DWORD whatToDo)
|
||||
{
|
||||
auto source = new JuceDropSource();
|
||||
auto data = new JuceDataObject (source, format, medium);
|
||||
|
||||
DWORD effect;
|
||||
auto res = DoDragDrop (data, source, whatToDo, &effect);
|
||||
|
||||
data->Release();
|
||||
source->Release();
|
||||
|
||||
return res == DRAGDROP_S_DROP;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, const bool canMove, Component*)
|
||||
{
|
||||
FORMATETC format = { CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
|
||||
STGMEDIUM medium = { TYMED_HGLOBAL, { 0 }, 0 };
|
||||
|
||||
medium.hGlobal = DragAndDropHelpers::createHDrop (files);
|
||||
|
||||
return DragAndDropHelpers::performDragDrop (&format, &medium, canMove ? (DWORD) (DROPEFFECT_COPY | DROPEFFECT_MOVE)
|
||||
: (DWORD) DROPEFFECT_COPY);
|
||||
}
|
||||
|
||||
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component*)
|
||||
{
|
||||
FORMATETC format = { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
|
||||
STGMEDIUM medium = { TYMED_HGLOBAL, { 0 }, 0 };
|
||||
|
||||
auto numBytes = CharPointer_UTF16::getBytesRequiredFor (text.getCharPointer());
|
||||
|
||||
medium.hGlobal = GlobalAlloc (GMEM_MOVEABLE | GMEM_ZEROINIT, numBytes + 2);
|
||||
WCHAR* const data = static_cast<WCHAR*> (GlobalLock (medium.hGlobal));
|
||||
|
||||
text.copyToUTF16 (data, numBytes);
|
||||
format.cfFormat = CF_UNICODETEXT;
|
||||
|
||||
GlobalUnlock (medium.hGlobal);
|
||||
|
||||
return DragAndDropHelpers::performDragDrop (&format, &medium, DROPEFFECT_COPY | DROPEFFECT_MOVE);
|
||||
}
|
||||
|
||||
} // namespace juce
|
577
modules/juce_gui_basics/native/juce_win32_FileChooser.cpp
Normal file
577
modules/juce_gui_basics/native/juce_win32_FileChooser.cpp
Normal file
@ -0,0 +1,577 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
// 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
|
||||
{
|
||||
public:
|
||||
using Ptr = ReferenceCountedObjectPtr<Win32NativeFileChooser>;
|
||||
|
||||
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)
|
||||
{
|
||||
auto parentDirectory = startingFile.getParentDirectory();
|
||||
|
||||
// Handle nonexistent root directories in the same way as existing ones
|
||||
files.calloc (static_cast<size_t> (charsAvailableForResult) + 1);
|
||||
|
||||
if (startingFile.isDirectory() ||startingFile.isRoot())
|
||||
{
|
||||
initialPath = startingFile.getFullPathName();
|
||||
}
|
||||
else
|
||||
{
|
||||
startingFile.getFileName().copyToUTF16 (files,
|
||||
static_cast<size_t> (charsAvailableForResult) * sizeof (WCHAR));
|
||||
initialPath = parentDirectory.getFullPathName();
|
||||
}
|
||||
|
||||
if (! selectsDirectories)
|
||||
{
|
||||
if (previewComp != nullptr)
|
||||
customComponent.reset (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:
|
||||
CustomComponentHolder (Component* const customComp)
|
||||
{
|
||||
setVisible (true);
|
||||
setOpaque (true);
|
||||
addAndMakeVisible (customComp);
|
||||
setSize (jlimit (20, 800, customComp->getWidth()), customComp->getHeight());
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (Colours::lightgrey);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
if (Component* const c = getChildComponent(0))
|
||||
c->setBounds (getLocalBounds());
|
||||
}
|
||||
|
||||
private:
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomComponentHolder)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
Component::SafePointer<Component> owner;
|
||||
String title, filtersString;
|
||||
std::unique_ptr<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.get());
|
||||
|
||||
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))
|
||||
{
|
||||
auto 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()
|
||||
{
|
||||
#if JUCE_DISABLE_NATIVE_FILECHOOSERS
|
||||
return false;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags,
|
||||
FilePreviewComponent* preview)
|
||||
{
|
||||
return new FileChooser::Native (owner, flags, preview);
|
||||
}
|
||||
|
||||
} // namespace juce
|
4230
modules/juce_gui_basics/native/juce_win32_Windowing.cpp
Normal file
4230
modules/juce_gui_basics/native/juce_win32_Windowing.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user