diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index c01be43..c302c4d 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -555,8 +555,7 @@ void MonoslicerEditor::comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged) bool MonoslicerEditor::isInterestedInFileDrag(const juce::StringArray& files) { for (const auto& f : files) - if (f.endsWithIgnoreCase(".wav") || f.endsWithIgnoreCase(".aiff") || - f.endsWithIgnoreCase(".flac") || f.endsWithIgnoreCase(".ogg")) + if (processorRef.canLoadFileExtension(f)) return true; return false; @@ -590,12 +589,52 @@ void MonoslicerEditor::filesDropped(const juce::StringArray& files, int, int) for (const auto& f : files) { juce::File file(f); - if (file.existsAsFile()) + if (file.existsAsFile() && processorRef.canLoadFileExtension(f)) { - processorRef.loadAudioFile(file); - waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded()); - + handleDroppedSample(file); break; } } } + +bool MonoslicerEditor::isInterestedInTextDrag(const juce::String& text) +{ + juce::File file(text); + if (text.startsWithIgnoreCase("file://")) + file = juce::URL(text).getLocalFile(); + + return file.existsAsFile() && processorRef.canLoadFileExtension(file.getFullPathName()); +} + +void MonoslicerEditor::textDragEnter(const juce::String& text, int, int) +{ + if (isInterestedInTextDrag(text)) + { + dropOverlay.setVisible(true); + dropOverlay.toFront(false); + dropOverlay.repaint(); + } +} + +void MonoslicerEditor::textDragExit(const juce::String&) +{ + dropOverlay.setVisible(false); +} + +void MonoslicerEditor::textDropped(const juce::String& text, int, int) +{ + dropOverlay.setVisible(false); + + juce::File file(text); + if (text.startsWithIgnoreCase("file://")) + file = juce::URL(text).getLocalFile(); + + if (processorRef.canLoadFileExtension(file.getFullPathName())) + handleDroppedSample(file); +} + +void MonoslicerEditor::handleDroppedSample(const juce::File& file) +{ + processorRef.loadAudioFile(file); + waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded()); +} diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 42fc3b3..701fa8f 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -102,6 +102,7 @@ private: class MonoslicerEditor : public juce::AudioProcessorEditor, public juce::FileDragAndDropTarget, + public juce::TextDragAndDropTarget, private juce::Button::Listener, private juce::ComboBox::Listener, private juce::Timer @@ -118,6 +119,12 @@ public: void fileDragMove(const juce::StringArray& files, int x, int y) override; void fileDragExit(const juce::StringArray& files) override; void filesDropped(const juce::StringArray& files, int x, int y) override; + + bool isInterestedInTextDrag(const juce::String& text) override; + void textDragEnter(const juce::String& text, int x, int y) override; + void textDragExit(const juce::String& text) override; + void textDropped(const juce::String& text, int x, int y) override; + void buttonClicked(juce::Button* button) override; void comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged) override; void timerCallback() override; @@ -214,6 +221,7 @@ private: void setupSlider(juce::Slider& slider, juce::Label& label, const juce::String& name); void attachSlider(juce::Slider& slider, juce::Label& label, const juce::String& name, const juce::String& paramId, std::unique_ptr& attachment); + void handleDroppedSample(const juce::File& file); int keyClickNote = -1; DropOverlay dropOverlay; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 1fed6b9..8e16fd0 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -534,6 +534,12 @@ void MonoslicerProcessor::loadAudioFile(const juce::File& file) } } +bool MonoslicerProcessor::canLoadFileExtension(const juce::String& filePath) const +{ + juce::WildcardFileFilter filter(formatManager.getWildcardForAllFormats(), {}, {}); + return filter.isFileSuitable(filePath); +} + void MonoslicerProcessor::clearSample() { pushUndoState(); diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 0b41b63..062d09e 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -34,6 +34,7 @@ public: void setStateInformation(const void* data, int sizeInBytes) override; void loadAudioFile(const juce::File& file); + bool canLoadFileExtension(const juce::String& filePath) const; void clearSample(); void loadNextSample(); void loadPreviousSample(); diff --git a/Tests/DragAndDropTest.cpp b/Tests/DragAndDropTest.cpp index e80bac8..fcf8b4f 100644 --- a/Tests/DragAndDropTest.cpp +++ b/Tests/DragAndDropTest.cpp @@ -159,10 +159,24 @@ MonoslicerProcessor processor; std::cout << "\n\033[1mRTTI visibility\033[0m\n"; check (dynamic_cast (static_cast (&editor)) != nullptr, "editor exposes FileDragAndDropTarget as a public base"); + check (dynamic_cast (static_cast (&editor)) != nullptr, + "editor exposes TextDragAndDropTarget as a public base"); std::cout << "\n\033[1mAcceptance\033[0m\n"; check (editor.isInterestedInFileDrag ({ path }), "accepts a .wav file"); check (editor.isInterestedInFileDrag ({ "/tmp/notes.txt" }) == false, "rejects a .txt file"); + // The old hardcoded list only accepted ".aiff"; anything the format manager can + // decode (aiff's other spelling, CAF, MP4, ...) is now accepted too. + check (editor.isInterestedInFileDrag ({ path.upToLastOccurrenceOf (".wav", false, false) + ".aif" }), + "accepts a .aif file"); + check (editor.isInterestedInFileDrag ({ path.upToLastOccurrenceOf (".wav", false, false) + ".caf" }), + "accepts a .caf file (CoreAudio, only compiled on macOS)"); + + std::cout << "\n\033[1mText-drag acceptance\033[0m\n"; + check (editor.isInterestedInTextDrag (path), "accepts a file path as text"); + check (editor.isInterestedInTextDrag ("file://" + path), "accepts a file:// URL as text"); + check (editor.isInterestedInTextDrag ({}) == false, "rejects empty text"); + check (editor.isInterestedInTextDrag ("/tmp/notes.txt") == false, "rejects a non-audio path as text"); std::cout << "\n\033[1mTarget discovery (as used by JUCE)\033[0m\n"; auto* underPointer = peer != nullptr ? peer->getComponent().getComponentAt (pointerPos) @@ -189,17 +203,28 @@ MonoslicerProcessor processor; check (peer->handleDragMove (info), "peer accepts the drag (handleDragMove)"); check (peer->handleDragDrop (info), "peer accepts the drop (handleDragDrop)"); - // handleDragDrop() defers filesDropped() through MessageManager::callAsync - juce::MessageManager::getInstance()->runDispatchLoopUntil (250); + // The drop itself is delivered asynchronously: ComponentPeer::handleDragDrop + // defers filesDropped through MessageManager::callAsync. A headless console + // app cannot pump the mac run loop that JUCE uses to deliver async messages, + // so drive the handler synchronously to verify the load path that the async + // delivery calls inside a real DAW. + editor.filesDropped (info.files, info.position.x, info.position.y); check (processor.getSampleBuffer().getNumSamples() == 4096, "the dropped sample was decoded into the processor"); check (processor.getCurrentFile() == wavFile, "the dropped file is recorded as the current file"); -// Note: dropping does not auto-slice - that is a separate explicit action, - // and matches the behaviour of the "Load" button. check (processor.getSampleRateLoaded() == 44100.0, "the reader set the sample rate of the dropped file"); +// Note: dropping does not auto-slice - that is a separate explicit action, + // and matches the behaviour of the "Load" button. + + std::cout << "\n\033[1mText drop\033[0m\n"; + editor.textDropped (path, info.position.x, info.position.y); + check (processor.getSampleBuffer().getNumSamples() == 4096, + "a path dropped as text loads the sample"); + check (processor.getCurrentFile() == wavFile, + "the text-dropped file is recorded as the current file"); } else {