// Regression test: dropping an audio file onto the plugin editor. // // Drag and drop discovery is driven entirely by RTTI. JUCE's // ComponentPeer::DragHelpers::findDragAndDropTarget walks up the component // chain and does dynamic_cast on each candidate, so the // interface only has to be visible as a *public* base. Inheriting it privately // makes the cast return null and the drop handlers are never called - silently, // with no warning at build time. // // Build and run: // cmake --build build --target MonoslicerDnDTest // ./build/MonoslicerDnDTest_artefacts/Release/MonoslicerDnDTest #include "../Source/PluginEditor.h" #include "../Source/PluginProcessor.h" #include namespace { int failures = 0; void check (bool condition, const juce::String& what) { std::cout << (condition ? " \033[32mPASS\033[0m " : " \033[1;31mFAIL\033[0m ") << what << std::endl; if (!condition) ++failures; } // Mirrors JUCE's DragHelpers::findDragAndDropTarget (juce_ComponentPeer.cpp). // Returns the winning *component* rather than a FileDragAndDropTarget* so that // this compiles - and fails at runtime rather than at build time - even when the // interface is inherited privately. That is precisely the regression being // guarded against: JUCE casts from Component*, which is always legal. juce::Component* findDragTargetComponent (juce::Component* c, const juce::StringArray& files) { for (; c != nullptr; c = c->getParentComponent()) { if (auto* target = dynamic_cast (c)) if (target->isInterestedInFileDrag (files)) return c; } return nullptr; } // Mirrors the host structure: JUCE's VST3 wrapper puts the plugin editor inside a // ContentWrapperComponent and adds *that* to the desktop // (juce_audio_plugin_client_VST3.cpp:1996). The peer therefore knows nothing about // the editor directly - it can only reach it by walking the component chain. struct HostWrapper final : juce::Component { explicit HostWrapper (juce::Component& child) { addAndMakeVisible (child); setSize (child.getWidth(), child.getHeight()); } void paint (juce::Graphics& g) override { g.fillAll (juce::Colours::black); } }; void dumpChain (juce::Component* c) { std::cout << " chain:"; for (; c != nullptr; c = c->getParentComponent()) std::cout << " \"" << juce::String (typeid (*c).name()).upToLastOccurrenceOf ("::", true, false) << "\"" << (c->isVisible() ? "" : "(hidden)") << (dynamic_cast (c) != nullptr ? "[fdnd]" : "") << " " << c->getWidth() << "x" << c->getHeight(); std::cout << std::endl; } // The editor scales itself via setTransform() in resized(); at the base size // that transform is the identity, which keeps pointer maths straightforward. constexpr int baseWidth = 920; constexpr int baseHeight = 606; juce::File writeTestWav() { auto file = juce::File::getSpecialLocation (juce::File::tempDirectory) .getChildFile ("monoslicer_dnd_test.wav"); constexpr double sampleRate = 44100.0; constexpr int numSamples = 4096; juce::AudioBuffer buffer (1, numSamples); for (int i = 0; i < numSamples; ++i) buffer.setSample (0, i, std::sin (i / 20.0f)); juce::WavAudioFormat format; std::unique_ptr out (file.createOutputStream()); if (out == nullptr) { std::cout << " \033[1;31mFAIL\033[0m could not create " << file.getFullPathName() << std::endl; ++failures; return {}; } { // The writer has to be destroyed before the stream it writes to. std::unique_ptr writer ( format.createWriterFor (out.get(), sampleRate, 1, 16, {}, 0)); if (writer == nullptr) { std::cout << " \033[1;31mFAIL\033[0m could not create a WAV writer" << std::endl; ++failures; return {}; } writer->writeFromAudioSampleBuffer (buffer, 0, numSamples); } out.release(); // commits and closes the file if (file.getSize() == 0) { std::cout << " \033[1;31mFAIL\033[0m test WAV was written empty" << std::endl; ++failures; return {}; } return file; } } int main() { juce::ScopedJuceInitialiser_GUI juceInitialiser; std::cout << "\n\033[1mMonoslicer - file drag and drop\033[0m\n\n"; const auto wavFile = writeTestWav(); if (!wavFile.existsAsFile()) return 1; const auto path = wavFile.getFullPathName(); const juce::Point pointerPos (400, 300); MonoslicerProcessor processor; MonoslicerEditor editor (processor); editor.setSize (baseWidth, baseHeight); HostWrapper wrapper (editor); wrapper.addToDesktop (0); wrapper.setVisible (true); auto* peer = wrapper.getPeer(); std::cout << "\n\033[1mRTTI visibility\033[0m\n"; check (dynamic_cast (static_cast (&editor)) != nullptr, "editor exposes FileDragAndDropTarget 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"); std::cout << "\n\033[1mTarget discovery (as used by JUCE)\033[0m\n"; auto* underPointer = peer != nullptr ? peer->getComponent().getComponentAt (pointerPos) : wrapper.getComponentAt (pointerPos); if (underPointer == nullptr) std::cout << " (no component under the pointer)" << std::endl; dumpChain (underPointer); check (findDragTargetComponent (underPointer, { path }) == static_cast (&editor), "lookup from the component under the pointer finds the editor"); check (findDragTargetComponent (underPointer, { "/tmp/notes.txt" }) == nullptr, "lookup for an unsupported file finds no target"); std::cout << "\n\033[1mEnd-to-end drop via a real native peer\033[0m\n"; if (peer != nullptr) { juce::ComponentPeer::DragInfo info; info.files.add (path); info.position = pointerPos; 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); 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"); } else { std::cout << " \033[33mSKIP\033[0m no native peer available (headless session)" << std::endl; } std::cout << "\n" << (failures == 0 ? "\033[32;1mAll checks passed.\033[0m" : juce::String (failures) + " check(s) failed.") << "\n\n"; return failures == 0 ? 0 : 1; }