Browse Source

Made the DragAndDropContainer::performExternalDragDropOfFiles() and ::performExternalDragDropOfText() methods asynchronous on Windows so that behaviour is consistent across all platforms and updated the documentation to reflect this

tags/2021-05-28
ed 6 years ago
parent
commit
4280b51d09
8 changed files with 207 additions and 81 deletions
  1. +23
    -2
      BREAKING-CHANGES.txt
  2. +17
    -13
      modules/juce_gui_basics/mouse/juce_DragAndDropContainer.h
  3. +1
    -3
      modules/juce_gui_basics/mouse/juce_DragAndDropTarget.h
  4. +5
    -2
      modules/juce_gui_basics/native/juce_android_Windowing.cpp
  5. +2
    -2
      modules/juce_gui_basics/native/juce_ios_Windowing.mm
  6. +15
    -9
      modules/juce_gui_basics/native/juce_linux_X11_Windowing.cpp
  7. +65
    -34
      modules/juce_gui_basics/native/juce_mac_Windowing.mm
  8. +79
    -16
      modules/juce_gui_basics/native/juce_win32_DragAndDrop.cpp

+ 23
- 2
BREAKING-CHANGES.txt View File

@@ -6,7 +6,29 @@ Develop
Change Change
------ ------
AudioProcessor::getTailLengthSeconds can now return infinity for VST/VST3/AU/AUv3
DragAndDropContainer::performExternalDragDropOfFiles() and ::performExternalDragDropOfText()
are now asynchronous on Windows.
Possible Issues
---------------
Code that previously relied on these operations being synchronous and blocking until
completion will no longer work as the methods will return immediately and run
asynchronously.
Workaround
----------
Use the callback argument that has been added to these methods to register a lambda
that will be called when the operation has been completed.
Rationale
---------
The behaviour of these methods is now consistent across all platforms and the method
no longer blocks the message thread on Windows.
Change
------
AudioProcessor::getTailLengthSeconds can now return infinity for VST/VST3/AU/AUv3.
Possible Issues Possible Issues
--------------- ---------------
@@ -20,7 +42,6 @@ a buffer.
Rationale Rationale
--------- ---------
Before this change there was no way for a JUCE plug-in to report an infinite tail time. Before this change there was no way for a JUCE plug-in to report an infinite tail time.


+ 17
- 13
modules/juce_gui_basics/mouse/juce_DragAndDropContainer.h View File

@@ -153,42 +153,46 @@ public:
//============================================================================== //==============================================================================
/** This performs a synchronous drag-and-drop of a set of files to some external
/** This performs an asynchronous drag-and-drop of a set of files to some external
application. application.
You can call this function in response to a mouseDrag callback, and it will You can call this function in response to a mouseDrag callback, and it will
block, running its own internal message loop and tracking the mouse, while it
uses a native operating system drag-and-drop operation to move or copy some
use a native operating system drag-and-drop operation to move or copy some
files to another application. files to another application.
@param files a list of filenames to drag @param files a list of filenames to drag
@param canMoveFiles if true, the app that receives the files is allowed to move the files to a new location @param canMoveFiles if true, the app that receives the files is allowed to move the files to a new location
(if this is appropriate). If false, the receiver is expected to make a copy of them. (if this is appropriate). If false, the receiver is expected to make a copy of them.
@param sourceComponent Normally, JUCE will assume that the component under the mouse is the source component
@param sourceComponent normally, JUCE will assume that the component under the mouse is the source component
of the drag, but you can use this parameter to override this. of the drag, but you can use this parameter to override this.
@returns true if the files were successfully dropped somewhere, or false if it
was interrupted
@param callback an optional completion callback that will be called when the operation has ended.
@returns true if the drag operation was successfully started, or false if it failed for some reason
@see performExternalDragDropOfText @see performExternalDragDropOfText
*/ */
static bool performExternalDragDropOfFiles (const StringArray& files, bool canMoveFiles, static bool performExternalDragDropOfFiles (const StringArray& files, bool canMoveFiles,
Component* sourceComponent = nullptr);
Component* sourceComponent = nullptr,
std::function<void()> callback = nullptr);
/** This performs a synchronous drag-and-drop of a block of text to some external
/** This performs an asynchronous drag-and-drop of a block of text to some external
application. application.
You can call this function in response to a mouseDrag callback, and it will You can call this function in response to a mouseDrag callback, and it will
block, running its own internal message loop and tracking the mouse, while it
uses a native operating system drag-and-drop operation to move or copy some
use a native operating system drag-and-drop operation to move or copy some
text to another application. text to another application.
@param text the text to copy @param text the text to copy
@param sourceComponent Normally, JUCE will assume that the component under the mouse is the source component @param sourceComponent Normally, JUCE will assume that the component under the mouse is the source component
of the drag, but you can use this parameter to override this. of the drag, but you can use this parameter to override this.
@returns true if the text was successfully dropped somewhere, or false if it
was interrupted
@param callback an optional completion callback that will be called when the operation has ended.
@returns true if the drag operation was successfully started, or false if it failed for some reason
@see performExternalDragDropOfFiles @see performExternalDragDropOfFiles
*/ */
static bool performExternalDragDropOfText (const String& text, Component* sourceComponent = nullptr);
static bool performExternalDragDropOfText (const String& text, Component* sourceComponent = nullptr,
std::function<void()> callback = nullptr);
protected: protected:
/** Override this if you want to be able to perform an external drag of a set of files /** Override this if you want to be able to perform an external drag of a set of files


+ 1
- 3
modules/juce_gui_basics/mouse/juce_DragAndDropTarget.h View File

@@ -50,9 +50,7 @@ public:
virtual ~DragAndDropTarget() {} virtual ~DragAndDropTarget() {}
//============================================================================== //==============================================================================
/** Contains details about the source of a drag-and-drop operation.
The contents of this
*/
/** Contains details about the source of a drag-and-drop operation. */
class JUCE_API SourceDetails class JUCE_API SourceDetails
{ {
public: public:


+ 5
- 2
modules/juce_gui_basics/native/juce_android_Windowing.cpp View File

@@ -1077,13 +1077,16 @@ void MouseCursor::showInAllWindows() const {}
//============================================================================== //==============================================================================
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& /*files*/, const bool /*canMove*/, bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& /*files*/, const bool /*canMove*/,
Component* /*srcComp*/)
Component* /*srcComp*/, std::function<void()> /*callback*/)
{ {
jassertfalse; // no such thing on Android!
return false; return false;
} }
bool DragAndDropContainer::performExternalDragDropOfText (const String& /*text*/, Component* /*srcComp*/)
bool DragAndDropContainer::performExternalDragDropOfText (const String& /*text*/, Component* /*srcComp*/,
std::function<void()> /*callback*/)
{ {
jassertfalse; // no such thing on Android!
return false; return false;
} }


+ 2
- 2
modules/juce_gui_basics/native/juce_ios_Windowing.mm View File

@@ -640,13 +640,13 @@ int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (AlertWindow::AlertIconType /*i
} }
//============================================================================== //==============================================================================
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray&, bool, Component*)
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray&, bool, Component*, std::function<void()>)
{ {
jassertfalse; // no such thing on iOS! jassertfalse; // no such thing on iOS!
return false; return false;
} }
bool DragAndDropContainer::performExternalDragDropOfText (const String&, Component*)
bool DragAndDropContainer::performExternalDragDropOfText (const String&, Component*, std::function<void()>)
{ {
jassertfalse; // no such thing on iOS! jassertfalse; // no such thing on iOS!
return false; return false;


+ 15
- 9
modules/juce_gui_basics/native/juce_linux_X11_Windowing.cpp View File

@@ -2471,15 +2471,15 @@ public:
} }
} }
bool externalDragTextInit (const String& text)
bool externalDragTextInit (const String& text, std::function<void()> cb)
{ {
if (dragState->dragging) if (dragState->dragging)
return false; return false;
return externalDragInit (true, text);
return externalDragInit (true, text, cb);
} }
bool externalDragFileInit (const StringArray& files, bool /*canMoveFiles*/)
bool externalDragFileInit (const StringArray& files, bool /*canMoveFiles*/, std::function<void()> cb)
{ {
if (dragState->dragging) if (dragState->dragging)
return false; return false;
@@ -2494,7 +2494,7 @@ public:
uriList.add ("file://" + f); uriList.add ("file://" + f);
} }
return externalDragInit (false, uriList.joinIntoString ("\r\n"));
return externalDragInit (false, uriList.joinIntoString ("\r\n"), cb);
} }
//============================================================================== //==============================================================================
@@ -3183,6 +3183,7 @@ private:
Rectangle<int> silentRect; Rectangle<int> silentRect;
String textOrFiles; String textOrFiles;
Array<Atom> allowedTypes; Array<Atom> allowedTypes;
std::function<void()> completionCallback;
}; };
//============================================================================== //==============================================================================
@@ -3624,7 +3625,7 @@ private:
return externalFindDragTargetWindow (child); return externalFindDragTargetWindow (child);
} }
bool externalDragInit (bool isText, const String& textOrFiles)
bool externalDragInit (bool isText, const String& textOrFiles, std::function<void()> cb)
{ {
ScopedXLock xlock (display); ScopedXLock xlock (display);
@@ -3632,6 +3633,7 @@ private:
dragState->isText = isText; dragState->isText = isText;
dragState->textOrFiles = textOrFiles; dragState->textOrFiles = textOrFiles;
dragState->targetWindow = windowH; dragState->targetWindow = windowH;
dragState->completionCallback = cb;
const int pointerGrabMask = Button1MotionMask | ButtonReleaseMask; const int pointerGrabMask = Button1MotionMask | ButtonReleaseMask;
@@ -3664,6 +3666,9 @@ private:
XUngrabPointer (display, CurrentTime); XUngrabPointer (display, CurrentTime);
} }
if (dragState->completionCallback != nullptr)
dragState->completionCallback();
resetExternalDragState(); resetExternalDragState();
} }
@@ -4262,26 +4267,27 @@ static LinuxComponentPeer* getPeerForDragEvent (Component* sourceComp)
} }
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, bool canMoveFiles, bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, bool canMoveFiles,
Component* sourceComp)
Component* sourceComp, std::function<void()> callback)
{ {
if (files.isEmpty()) if (files.isEmpty())
return false; return false;
if (auto* lp = getPeerForDragEvent (sourceComp)) if (auto* lp = getPeerForDragEvent (sourceComp))
return lp->externalDragFileInit (files, canMoveFiles);
return lp->externalDragFileInit (files, canMoveFiles, callback);
// This method must be called in response to a component's mouseDown or mouseDrag event! // This method must be called in response to a component's mouseDown or mouseDrag event!
jassertfalse; jassertfalse;
return false; return false;
} }
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component* sourceComp)
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component* sourceComp,
std::function<void()> callback)
{ {
if (text.isEmpty()) if (text.isEmpty())
return false; return false;
if (auto* lp = getPeerForDragEvent (sourceComp)) if (auto* lp = getPeerForDragEvent (sourceComp))
return lp->externalDragTextInit (text);
return lp->externalDragTextInit (text, callback);
// This method must be called in response to a component's mouseDown or mouseDrag event! // This method must be called in response to a component's mouseDown or mouseDrag event!
jassertfalse; jassertfalse;


+ 65
- 34
modules/juce_gui_basics/native/juce_mac_Windowing.mm View File

@@ -177,14 +177,22 @@ static NSView* getNSViewForDragEvent (Component* sourceComp)
return nil; return nil;
} }
struct TextDragDataProviderClass : public ObjCClass<NSObject>
struct NSDraggingSourceHelper : public ObjCClass<NSObject<NSDraggingSource>>
{ {
TextDragDataProviderClass() : ObjCClass<NSObject> ("JUCE_NSTextDragDataProvider_")
NSDraggingSourceHelper() : ObjCClass<NSObject<NSDraggingSource>> ("JUCENSDraggingSourceHelper_")
{ {
addIvar<std::function<void()>*> ("callback");
addIvar<String*> ("text"); addIvar<String*> ("text");
addIvar<NSDragOperation*> ("operation");
addMethod (@selector (dealloc), dealloc, "v@:"); addMethod (@selector (dealloc), dealloc, "v@:");
addMethod (@selector (pasteboard:item:provideDataForType:), provideDataForType, "v@:@@@"); addMethod (@selector (pasteboard:item:provideDataForType:), provideDataForType, "v@:@@@");
addMethod (@selector (draggingSession:sourceOperationMaskForDraggingContext:), sourceOperationMaskForDraggingContext, "c@:@@");
addMethod (@selector (draggingSession:endedAtPoint:operation:), draggingSessionEnded, "v@:@@@");
addProtocol (@protocol (NSPasteboardItemDataProvider)); addProtocol (@protocol (NSPasteboardItemDataProvider));
registerClass(); registerClass();
} }
@@ -193,10 +201,23 @@ struct TextDragDataProviderClass : public ObjCClass<NSObject>
object_setInstanceVariable (self, "text", new String (text)); object_setInstanceVariable (self, "text", new String (text));
} }
static void setCompletionCallback (id self, std::function<void()> cb)
{
object_setInstanceVariable (self, "callback", new std::function<void()> (cb));
}
static void setDragOperation (id self, NSDragOperation op)
{
object_setInstanceVariable (self, "operation", new NSDragOperation (op));
}
private: private:
static void dealloc (id self, SEL) static void dealloc (id self, SEL)
{ {
delete getIvar<String*> (self, "text"); delete getIvar<String*> (self, "text");
delete getIvar<std::function<void()>*> (self, "callback");
delete getIvar<NSDragOperation*> (self, "operation");
sendSuperclassMessage (self, @selector (dealloc)); sendSuperclassMessage (self, @selector (dealloc));
} }
@@ -207,9 +228,23 @@ private:
[sender setData: [juceStringToNS (*text) dataUsingEncoding: NSUTF8StringEncoding] [sender setData: [juceStringToNS (*text) dataUsingEncoding: NSUTF8StringEncoding]
forType: NSPasteboardTypeString]; forType: NSPasteboardTypeString];
} }
static NSDragOperation sourceOperationMaskForDraggingContext (id self, SEL, NSDraggingSession*, NSDraggingContext)
{
return *getIvar<NSDragOperation*> (self, "operation");
}
static void draggingSessionEnded (id self, SEL, NSDraggingSession*, NSPoint, NSDragOperation)
{
if (auto* cb = getIvar<std::function<void()>*> (self, "callback"))
cb->operator()();
}
}; };
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component* sourceComponent)
static NSDraggingSourceHelper draggingSourceHelper;
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component* sourceComponent,
std::function<void()> callback)
{ {
if (text.isEmpty()) if (text.isEmpty())
return false; return false;
@@ -220,12 +255,15 @@ bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Co
{ {
if (auto* event = [[view window] currentEvent]) if (auto* event = [[view window] currentEvent])
{ {
static TextDragDataProviderClass dataProviderClass;
id delegate = [dataProviderClass.createInstance() init];
TextDragDataProviderClass::setText (delegate, text);
id helper = [draggingSourceHelper.createInstance() init];
NSDraggingSourceHelper::setText (helper, text);
NSDraggingSourceHelper::setDragOperation (helper, NSDragOperationCopy);
if (callback != nullptr)
NSDraggingSourceHelper::setCompletionCallback (helper, callback);
auto* pasteboardItem = [[NSPasteboardItem new] autorelease]; auto* pasteboardItem = [[NSPasteboardItem new] autorelease];
[pasteboardItem setDataProvider: delegate
[pasteboardItem setDataProvider: helper
forTypes: [NSArray arrayWithObjects: NSPasteboardTypeString, nil]]; forTypes: [NSArray arrayWithObjects: NSPasteboardTypeString, nil]];
auto* dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter: pasteboardItem] autorelease]; auto* dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter: pasteboardItem] autorelease];
@@ -233,13 +271,15 @@ bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Co
NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile: nsEmptyString()]; NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile: nsEmptyString()];
[dragItem setDraggingFrame: getDragRect (view, event) contents: image]; [dragItem setDraggingFrame: getDragRect (view, event) contents: image];
auto* draggingSession = [view beginDraggingSessionWithItems: [NSArray arrayWithObject: dragItem]
event: event
source: delegate];
if (auto* session = [view beginDraggingSessionWithItems: [NSArray arrayWithObject: dragItem]
event: event
source: helper])
{
session.animatesToStartingPositionsOnCancelOrFail = YES;
session.draggingFormation = NSDraggingFormationNone;
draggingSession.animatesToStartingPositionsOnCancelOrFail = YES;
draggingSession.draggingFormation = NSDraggingFormationNone;
return true;
return true;
}
} }
} }
} }
@@ -247,24 +287,8 @@ bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Co
return false; 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)
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, bool canMoveFiles,
Component* sourceComponent, std::function<void()> callback)
{ {
if (files.isEmpty()) if (files.isEmpty())
return false; return false;
@@ -296,9 +320,16 @@ bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& fi
auto* helper = [draggingSourceHelper.createInstance() autorelease]; auto* helper = [draggingSourceHelper.createInstance() autorelease];
return [view beginDraggingSessionWithItems: dragItems
event: event
source: helper];
if (callback != nullptr)
NSDraggingSourceHelper::setCompletionCallback (helper, callback);
NSDraggingSourceHelper::setDragOperation (helper, canMoveFiles ? NSDragOperationMove
: NSDragOperationCopy);
if (auto* session = [view beginDraggingSessionWithItems: dragItems
event: event
source: helper])
return true;
} }
} }
} }


+ 79
- 16
modules/juce_gui_basics/native/juce_win32_DragAndDrop.cpp View File

@@ -241,49 +241,112 @@ namespace DragAndDropHelpers
return hDrop; return hDrop;
} }
bool performDragDrop (FORMATETC* const format, STGMEDIUM* const medium, const DWORD whatToDo)
struct DragAndDropJob : public ThreadPoolJob
{ {
auto source = new JuceDropSource();
auto data = new JuceDataObject (source, format, medium);
DragAndDropJob (FORMATETC f, STGMEDIUM m, DWORD d, std::function<void()> cb)
: ThreadPoolJob ("DragAndDrop"),
format (f), medium (m), whatToDo (d),
completionCallback (cb)
{
}
DWORD effect;
auto res = DoDragDrop (data, source, whatToDo, &effect);
JobStatus runJob() override
{
OleInitialize (0);
data->Release();
source->Release();
auto source = new JuceDropSource();
auto data = new JuceDataObject (source, &format, &medium);
return res == DRAGDROP_S_DROP;
}
DWORD effect;
DoDragDrop (data, source, whatToDo, &effect);
data->Release();
source->Release();
if (completionCallback != nullptr)
MessageManager::callAsync (completionCallback);
return jobHasFinished;
}
FORMATETC format;
STGMEDIUM medium;
DWORD whatToDo;
std::function<void()> completionCallback;
};
class ThreadPoolHolder : private DeletedAtShutdown
{
public:
ThreadPoolHolder() = default;
~ThreadPoolHolder()
{
// Wait forever if there's a job running. The user needs to cancel the transfer
// in the GUI.
pool.removeAllJobs (true, -1);
clearSingletonInstance();
}
juce_DeclareSingleton_SingleThreaded (ThreadPoolHolder, true)
// We need to make sure we don't do simultaneous text and file drag and drops,
// so use a pool that can only run a single job.
ThreadPool pool { 1 };
};
juce_ImplementSingleton_SingleThreaded (ThreadPoolHolder)
} }
//============================================================================== //==============================================================================
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, const bool canMove, Component*)
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, const bool canMove,
Component*, std::function<void()> callback)
{ {
if (files.isEmpty())
return false;
FORMATETC format = { CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; FORMATETC format = { CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM medium = { TYMED_HGLOBAL, { 0 }, 0 }; STGMEDIUM medium = { TYMED_HGLOBAL, { 0 }, 0 };
medium.hGlobal = DragAndDropHelpers::createHDrop (files); medium.hGlobal = DragAndDropHelpers::createHDrop (files);
return DragAndDropHelpers::performDragDrop (&format, &medium, canMove ? (DWORD) (DROPEFFECT_COPY | DROPEFFECT_MOVE)
: (DWORD) DROPEFFECT_COPY);
auto& pool = DragAndDropHelpers::ThreadPoolHolder::getInstance()->pool;
pool.addJob (new DragAndDropHelpers::DragAndDropJob (format, medium,
canMove ? (DROPEFFECT_COPY | DROPEFFECT_MOVE) : DROPEFFECT_COPY,
callback),
true);
return true;
} }
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component*)
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component*, std::function<void()> callback)
{ {
if (text.isEmpty())
return false;
FORMATETC format = { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; FORMATETC format = { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM medium = { TYMED_HGLOBAL, { 0 }, 0 }; STGMEDIUM medium = { TYMED_HGLOBAL, { 0 }, 0 };
auto numBytes = CharPointer_UTF16::getBytesRequiredFor (text.getCharPointer()); auto numBytes = CharPointer_UTF16::getBytesRequiredFor (text.getCharPointer());
medium.hGlobal = GlobalAlloc (GMEM_MOVEABLE | GMEM_ZEROINIT, numBytes + 2); medium.hGlobal = GlobalAlloc (GMEM_MOVEABLE | GMEM_ZEROINIT, numBytes + 2);
WCHAR* const data = static_cast<WCHAR*> (GlobalLock (medium.hGlobal));
auto* data = static_cast<WCHAR*> (GlobalLock (medium.hGlobal));
text.copyToUTF16 (data, numBytes);
text.copyToUTF16 (data, numBytes + 2);
format.cfFormat = CF_UNICODETEXT; format.cfFormat = CF_UNICODETEXT;
GlobalUnlock (medium.hGlobal); GlobalUnlock (medium.hGlobal);
return DragAndDropHelpers::performDragDrop (&format, &medium, DROPEFFECT_COPY | DROPEFFECT_MOVE);
auto& pool = DragAndDropHelpers::ThreadPoolHolder::getInstance()->pool;
pool.addJob (new DragAndDropHelpers::DragAndDropJob (format,
medium,
DROPEFFECT_COPY | DROPEFFECT_MOVE,
callback),
true);
return true;
} }
} // namespace juce } // namespace juce

Loading…
Cancel
Save