diff --git a/source/backend/plugin/JucePlugin.cpp b/source/backend/plugin/JucePlugin.cpp index 66c5f2215..9a15f8daf 100644 --- a/source/backend/plugin/JucePlugin.cpp +++ b/source/backend/plugin/JucePlugin.cpp @@ -25,6 +25,8 @@ #include "juce_audio_processors.h" +// TODO - use setPlayConfigDetails + using namespace juce; CARLA_BACKEND_START_NAMESPACE diff --git a/source/backend/standalone/Makefile b/source/backend/standalone/Makefile index d8cdd58cf..a3f182680 100644 --- a/source/backend/standalone/Makefile +++ b/source/backend/standalone/Makefile @@ -33,6 +33,7 @@ STATIC_LIBS += ../../modules/juce_data_structures.a STATIC_LIBS += ../../modules/juce_events.a STATIC_LIBS += ../../modules/juce_graphics.a STATIC_LIBS += ../../modules/juce_gui_basics.a +STATIC_LIBS += ../../modules/juce_gui_extra.a LINK_FLAGS += $(JUCE_AUDIO_BASICS_LIBS) LINK_FLAGS += $(JUCE_AUDIO_DEVICES_LIBS) LINK_FLAGS += $(JUCE_CORE_LIBS) @@ -40,6 +41,7 @@ LINK_FLAGS += $(JUCE_DATA_STRUCTURES_LIBS) LINK_FLAGS += $(JUCE_EVENTS_LIBS) LINK_FLAGS += $(JUCE_GRAPHICS_LIBS) LINK_FLAGS += $(JUCE_GUI_BASICS_LIBS) +LINK_FLAGS += $(JUCE_GUI_EXTRA_LIBS) endif ifeq ($(HAVE_DGL),true) diff --git a/source/modules/Makefile b/source/modules/Makefile index 40630db75..512ec8560 100644 --- a/source/modules/Makefile +++ b/source/modules/Makefile @@ -93,6 +93,14 @@ juce_gui_basics_%: # -------------------------------------------------------------- +juce_gui_extra: + $(MAKE) -C juce_gui_extra + +juce_gui_extra_%: + $(MAKE) -C juce_gui_extra $* + +# -------------------------------------------------------------- + lilv: $(MAKE) -C lilv @@ -211,6 +219,6 @@ clean: .PHONY: \ daz-plugins dgl jackbridge lilv rtaudio rtmidi rtmempool stk theme \ - juce_audio_basics juce_audio_devices juce_audio_formats juce_audio_processors juce_core juce_data_structures juce_events juce_graphics juce_gui_basics + juce_audio_basics juce_audio_devices juce_audio_formats juce_audio_processors juce_core juce_data_structures juce_events juce_graphics juce_gui_basics juce_gui_extra # -------------------------------------------------------------- diff --git a/source/modules/daz-plugins/Makefile b/source/modules/daz-plugins/Makefile index 2450826eb..92b780312 100644 --- a/source/modules/daz-plugins/Makefile +++ b/source/modules/daz-plugins/Makefile @@ -100,6 +100,7 @@ OBJS += \ ifeq ($(HAVE_JUCE),true) OBJS += \ + juce-patchbay.cpp.o \ vex-fx.cpp.o \ vex-synth.cpp.o \ vex-src.cpp.o @@ -257,6 +258,9 @@ distrho-bigmeterM.cpp.o: distrho-bigmeterM.cpp bigmeterM/*.cpp bigmeterM/*.h big distrho-notes.cpp.o: distrho-notes.cpp notes/*.cpp notes/*.h notes/*.hpp distrho/DistrhoPluginCarla.cpp $(CXXDEPS) $(CXX) $< $(BUILD_CXX_FLAGS) -I../dgl -Inotes -DDISTRHO_NAMESPACE=DISTRHO_Notes -c -o $@ +juce-patchbay.cpp.o: juce-patchbay.cpp + $(CXX) $< $(BUILD_CXX_FLAGS) -c -o $@ + midi-file.cpp.o: midi-file.cpp midi-base.hpp $(CXXDEPS) $(CXX) $< $(MF_CXX_FLAGS) -c -o $@ diff --git a/source/modules/daz-plugins/_all.c b/source/modules/daz-plugins/_all.c index d3b1f78ff..195178cde 100644 --- a/source/modules/daz-plugins/_all.c +++ b/source/modules/daz-plugins/_all.c @@ -36,6 +36,7 @@ extern void carla_register_native_plugin_carla(); #endif #ifdef HAVE_JUCE +extern void carla_register_native_plugin_jucePatchbay(); extern void carla_register_native_plugin_vex_fx(); extern void carla_register_native_plugin_vex_synth(); #endif @@ -93,6 +94,7 @@ void carla_register_all_plugins() #endif #ifdef HAVE_JUCE + carla_register_native_plugin_jucePatchbay(); carla_register_native_plugin_vex_fx(); carla_register_native_plugin_vex_synth(); #endif diff --git a/source/modules/daz-plugins/juce-host/FilterGraph.cpp b/source/modules/daz-plugins/juce-host/FilterGraph.cpp new file mode 100644 index 000000000..37de5d04c --- /dev/null +++ b/source/modules/daz-plugins/juce-host/FilterGraph.cpp @@ -0,0 +1,362 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +//#include "MainHostWindow.h" +#include "FilterGraph.h" +#include "InternalFilters.h" +//#include "GraphEditorPanel.h" + + +//============================================================================== +const int FilterGraph::midiChannelNumber = 0x1000; + +FilterGraph::FilterGraph (AudioPluginFormatManager& formatManager_) + : FileBasedDocument (filenameSuffix, + filenameWildcard, + "Load a filter graph", + "Save a filter graph"), + formatManager (formatManager_), lastUID (0) +{ + setChangedFlag (false); +} + +FilterGraph::~FilterGraph() +{ + graph.clear(); +} + +void FilterGraph::ready() +{ + InternalPluginFormat internalFormat; + + addFilter (internalFormat.getDescriptionFor (InternalPluginFormat::audioInputFilter), 0.5f, 0.1f); + addFilter (internalFormat.getDescriptionFor (InternalPluginFormat::midiInputFilter), 0.25f, 0.1f); + addFilter (internalFormat.getDescriptionFor (InternalPluginFormat::audioOutputFilter), 0.5f, 0.9f); +} + +uint32 FilterGraph::getNextUID() noexcept +{ + return ++lastUID; +} + +//============================================================================== +int FilterGraph::getNumFilters() const noexcept +{ + return graph.getNumNodes(); +} + +const AudioProcessorGraph::Node::Ptr FilterGraph::getNode (const int index) const noexcept +{ + return graph.getNode (index); +} + +const AudioProcessorGraph::Node::Ptr FilterGraph::getNodeForId (const uint32 uid) const noexcept +{ + return graph.getNodeForId (uid); +} + +void FilterGraph::addFilter (const PluginDescription* desc, double x, double y) +{ + if (desc != nullptr) + { + AudioProcessorGraph::Node* node = nullptr; + + String errorMessage; + + if (AudioPluginInstance* instance = formatManager.createPluginInstance (*desc, graph.getSampleRate(), graph.getBlockSize(), errorMessage)) + node = graph.addNode (instance); + + if (node != nullptr) + { + node->properties.set ("x", x); + node->properties.set ("y", y); + changed(); + } + else + { + AlertWindow::showMessageBox (AlertWindow::WarningIcon, + TRANS("Couldn't create filter"), + errorMessage); + } + } +} + +void FilterGraph::removeFilter (const uint32 id) +{ + //PluginWindow::closeCurrentlyOpenWindowsFor (id); + + if (graph.removeNode (id)) + changed(); +} + +void FilterGraph::disconnectFilter (const uint32 id) +{ + if (graph.disconnectNode (id)) + changed(); +} + +void FilterGraph::removeIllegalConnections() +{ + if (graph.removeIllegalConnections()) + changed(); +} + +void FilterGraph::setNodePosition (const int nodeId, double x, double y) +{ + const AudioProcessorGraph::Node::Ptr n (graph.getNodeForId (nodeId)); + + if (n != nullptr) + { + n->properties.set ("x", jlimit (0.0, 1.0, x)); + n->properties.set ("y", jlimit (0.0, 1.0, y)); + } +} + +void FilterGraph::getNodePosition (const int nodeId, double& x, double& y) const +{ + x = y = 0; + + const AudioProcessorGraph::Node::Ptr n (graph.getNodeForId (nodeId)); + + if (n != nullptr) + { + x = (double) n->properties ["x"]; + y = (double) n->properties ["y"]; + } +} + +//============================================================================== +int FilterGraph::getNumConnections() const noexcept +{ + return graph.getNumConnections(); +} + +const AudioProcessorGraph::Connection* FilterGraph::getConnection (const int index) const noexcept +{ + return graph.getConnection (index); +} + +const AudioProcessorGraph::Connection* FilterGraph::getConnectionBetween (uint32 sourceFilterUID, int sourceFilterChannel, + uint32 destFilterUID, int destFilterChannel) const noexcept +{ + return graph.getConnectionBetween (sourceFilterUID, sourceFilterChannel, + destFilterUID, destFilterChannel); +} + +bool FilterGraph::canConnect (uint32 sourceFilterUID, int sourceFilterChannel, + uint32 destFilterUID, int destFilterChannel) const noexcept +{ + return graph.canConnect (sourceFilterUID, sourceFilterChannel, + destFilterUID, destFilterChannel); +} + +bool FilterGraph::addConnection (uint32 sourceFilterUID, int sourceFilterChannel, + uint32 destFilterUID, int destFilterChannel) +{ + const bool result = graph.addConnection (sourceFilterUID, sourceFilterChannel, + destFilterUID, destFilterChannel); + + if (result) + changed(); + + return result; +} + +void FilterGraph::removeConnection (const int index) +{ + graph.removeConnection (index); + changed(); +} + +void FilterGraph::removeConnection (uint32 sourceFilterUID, int sourceFilterChannel, + uint32 destFilterUID, int destFilterChannel) +{ + if (graph.removeConnection (sourceFilterUID, sourceFilterChannel, + destFilterUID, destFilterChannel)) + changed(); +} + +void FilterGraph::clear() +{ + //PluginWindow::closeAllCurrentlyOpenWindows(); + + graph.clear(); + changed(); +} + +//============================================================================== +String FilterGraph::getDocumentTitle() +{ + if (! getFile().exists()) + return "Unnamed"; + + return getFile().getFileNameWithoutExtension(); +} + +Result FilterGraph::loadDocument (const File& file) +{ + XmlDocument doc (file); + ScopedPointer xml (doc.getDocumentElement()); + + if (xml == nullptr || ! xml->hasTagName ("FILTERGRAPH")) + return Result::fail ("Not a valid filter graph file"); + + restoreFromXml (*xml); + return Result::ok(); +} + +Result FilterGraph::saveDocument (const File& file) +{ + ScopedPointer xml (createXml()); + + if (! xml->writeToFile (file, String::empty)) + return Result::fail ("Couldn't write to the file"); + + return Result::ok(); +} + +File FilterGraph::getLastDocumentOpened() +{ + return File(); +} + +void FilterGraph::setLastDocumentOpened (const File&) +{ +} + +//============================================================================== +static XmlElement* createNodeXml (AudioProcessorGraph::Node* const node) noexcept +{ + AudioPluginInstance* plugin = dynamic_cast (node->getProcessor()); + + if (plugin == nullptr) + { + jassertfalse + return nullptr; + } + + XmlElement* e = new XmlElement ("FILTER"); + e->setAttribute ("uid", (int) node->nodeId); + e->setAttribute ("x", node->properties ["x"].toString()); + e->setAttribute ("y", node->properties ["y"].toString()); + e->setAttribute ("uiLastX", node->properties ["uiLastX"].toString()); + e->setAttribute ("uiLastY", node->properties ["uiLastY"].toString()); + + PluginDescription pd; + plugin->fillInPluginDescription (pd); + + e->addChildElement (pd.createXml()); + + XmlElement* state = new XmlElement ("STATE"); + + MemoryBlock m; + node->getProcessor()->getStateInformation (m); + state->addTextElement (m.toBase64Encoding()); + e->addChildElement (state); + + return e; +} + +void FilterGraph::createNodeFromXml (const XmlElement& xml) +{ + PluginDescription pd; + + forEachXmlChildElement (xml, e) + { + if (pd.loadFromXml (*e)) + break; + } + + String errorMessage; + + AudioPluginInstance* instance = formatManager.createPluginInstance (pd, graph.getSampleRate(), graph.getBlockSize(), errorMessage); + + if (instance == nullptr) + { + // xxx handle ins + outs + } + + if (instance == nullptr) + return; + + AudioProcessorGraph::Node::Ptr node (graph.addNode (instance, xml.getIntAttribute ("uid"))); + + if (const XmlElement* const state = xml.getChildByName ("STATE")) + { + MemoryBlock m; + m.fromBase64Encoding (state->getAllSubText()); + + node->getProcessor()->setStateInformation (m.getData(), (int) m.getSize()); + } + + node->properties.set ("x", xml.getDoubleAttribute ("x")); + node->properties.set ("y", xml.getDoubleAttribute ("y")); + node->properties.set ("uiLastX", xml.getIntAttribute ("uiLastX")); + node->properties.set ("uiLastY", xml.getIntAttribute ("uiLastY")); +} + +XmlElement* FilterGraph::createXml() const +{ + XmlElement* xml = new XmlElement ("FILTERGRAPH"); + + for (int i = 0; i < graph.getNumNodes(); ++i) + xml->addChildElement (createNodeXml (graph.getNode (i))); + + for (int i = 0; i < graph.getNumConnections(); ++i) + { + const AudioProcessorGraph::Connection* const fc = graph.getConnection(i); + + XmlElement* e = new XmlElement ("CONNECTION"); + + e->setAttribute ("srcFilter", (int) fc->sourceNodeId); + e->setAttribute ("srcChannel", fc->sourceChannelIndex); + e->setAttribute ("dstFilter", (int) fc->destNodeId); + e->setAttribute ("dstChannel", fc->destChannelIndex); + + xml->addChildElement (e); + } + + return xml; +} + +void FilterGraph::restoreFromXml (const XmlElement& xml) +{ + clear(); + + forEachXmlChildElementWithTagName (xml, e, "FILTER") + { + createNodeFromXml (*e); + changed(); + } + + forEachXmlChildElementWithTagName (xml, e, "CONNECTION") + { + addConnection ((uint32) e->getIntAttribute ("srcFilter"), + e->getIntAttribute ("srcChannel"), + (uint32) e->getIntAttribute ("dstFilter"), + e->getIntAttribute ("dstChannel")); + } + + graph.removeIllegalConnections(); +} diff --git a/source/modules/daz-plugins/juce-host/FilterGraph.h b/source/modules/daz-plugins/juce-host/FilterGraph.h new file mode 100644 index 000000000..c3e6586f4 --- /dev/null +++ b/source/modules/daz-plugins/juce-host/FilterGraph.h @@ -0,0 +1,114 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#ifndef __FILTERGRAPH_JUCEHEADER__ +#define __FILTERGRAPH_JUCEHEADER__ + +class FilterInGraph; +class FilterGraph; + +const char* const filenameSuffix = ".filtergraph"; +const char* const filenameWildcard = "*.filtergraph"; + +//============================================================================== +/** + A collection of filters and some connections between them. +*/ +class FilterGraph : public FileBasedDocument +{ +public: + //============================================================================== + FilterGraph (AudioPluginFormatManager& formatManager); + ~FilterGraph(); + void ready(); + + //============================================================================== + AudioProcessorGraph& getGraph() noexcept { return graph; } + + int getNumFilters() const noexcept; + const AudioProcessorGraph::Node::Ptr getNode (const int index) const noexcept; + const AudioProcessorGraph::Node::Ptr getNodeForId (const uint32 uid) const noexcept; + + void addFilter (const PluginDescription* desc, double x, double y); + + void removeFilter (const uint32 filterUID); + void disconnectFilter (const uint32 filterUID); + + void removeIllegalConnections(); + + void setNodePosition (const int nodeId, double x, double y); + void getNodePosition (const int nodeId, double& x, double& y) const; + + //============================================================================== + int getNumConnections() const noexcept; + const AudioProcessorGraph::Connection* getConnection (const int index) const noexcept; + + const AudioProcessorGraph::Connection* getConnectionBetween (uint32 sourceFilterUID, int sourceFilterChannel, + uint32 destFilterUID, int destFilterChannel) const noexcept; + + bool canConnect (uint32 sourceFilterUID, int sourceFilterChannel, + uint32 destFilterUID, int destFilterChannel) const noexcept; + + bool addConnection (uint32 sourceFilterUID, int sourceFilterChannel, + uint32 destFilterUID, int destFilterChannel); + + void removeConnection (const int index); + + void removeConnection (uint32 sourceFilterUID, int sourceFilterChannel, + uint32 destFilterUID, int destFilterChannel); + + void clear(); + + + //============================================================================== + + XmlElement* createXml() const; + void restoreFromXml (const XmlElement& xml); + + //============================================================================== + String getDocumentTitle(); + Result loadDocument (const File& file); + Result saveDocument (const File& file); + File getLastDocumentOpened(); + void setLastDocumentOpened (const File& file); + + /** The special channel index used to refer to a filter's midi channel. + */ + static const int midiChannelNumber; + +private: + //============================================================================== + AudioPluginFormatManager& formatManager; + AudioProcessorGraph graph; + + uint32 lastUID; + uint32 getNextUID() noexcept; + + void createNodeFromXml (const XmlElement& xml); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FilterGraph) +}; + + +#endif // __FILTERGRAPH_JUCEHEADER__ diff --git a/source/modules/daz-plugins/juce-host/GraphEditorPanel.cpp b/source/modules/daz-plugins/juce-host/GraphEditorPanel.cpp new file mode 100644 index 000000000..73fe0a3f3 --- /dev/null +++ b/source/modules/daz-plugins/juce-host/GraphEditorPanel.cpp @@ -0,0 +1,1125 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#include "juce_gui_basics.h" +#include "GraphEditorPanel.h" +#include "InternalFilters.h" +//#include "MainHostWindow.h" + + +//============================================================================== +class PluginWindow; +static Array activePluginWindows; + +PluginWindow::PluginWindow (Component* const pluginEditor, + AudioProcessorGraph::Node* const o, + WindowFormatType t) + : DocumentWindow (pluginEditor->getName(), Colours::lightblue, + DocumentWindow::minimiseButton | DocumentWindow::closeButton), + owner (o), + type (t) +{ + setSize (400, 300); + + setContentOwned (pluginEditor, true); + + setTopLeftPosition (owner->properties.getWithDefault ("uiLastX", Random::getSystemRandom().nextInt (500)), + owner->properties.getWithDefault ("uiLastY", Random::getSystemRandom().nextInt (500))); + setVisible (true); + + activePluginWindows.add (this); +} + +void PluginWindow::closeCurrentlyOpenWindowsFor (const uint32 nodeId) +{ + for (int i = activePluginWindows.size(); --i >= 0;) + if (activePluginWindows.getUnchecked(i)->owner->nodeId == nodeId) + delete activePluginWindows.getUnchecked (i); +} + +void PluginWindow::closeAllCurrentlyOpenWindows() +{ + if (activePluginWindows.size() > 0) + { + for (int i = activePluginWindows.size(); --i >= 0;) + delete activePluginWindows.getUnchecked (i); + + Component dummyModalComp; + dummyModalComp.enterModalState(); + MessageManager::getInstance()->runDispatchLoopUntil (50); + } +} + +//============================================================================== +class ProcessorProgramPropertyComp : public PropertyComponent, + private AudioProcessorListener +{ +public: + ProcessorProgramPropertyComp (const String& name, AudioProcessor& p, int index_) + : PropertyComponent (name), + owner (p), + index (index_) + { + owner.addListener (this); + } + + ~ProcessorProgramPropertyComp() + { + owner.removeListener (this); + } + + void refresh() { } + void audioProcessorChanged (AudioProcessor*) { } + void audioProcessorParameterChanged (AudioProcessor*, int, float) { } + +private: + AudioProcessor& owner; + const int index; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProcessorProgramPropertyComp) +}; + +class ProgramAudioProcessorEditor : public AudioProcessorEditor +{ +public: + ProgramAudioProcessorEditor (AudioProcessor* const p) + : AudioProcessorEditor (p) + { + jassert (p != nullptr); + setOpaque (true); + + addAndMakeVisible (panel); + + Array programs; + + const int numPrograms = p->getNumPrograms(); + int totalHeight = 0; + + for (int i = 0; i < numPrograms; ++i) + { + String name (p->getProgramName (i).trim()); + + if (name.isEmpty()) + name = "Unnamed"; + + ProcessorProgramPropertyComp* const pc = new ProcessorProgramPropertyComp (name, *p, i); + programs.add (pc); + totalHeight += pc->getPreferredHeight(); + } + + panel.addProperties (programs); + + setSize (400, jlimit (25, 400, totalHeight)); + } + + void paint (Graphics& g) + { + g.fillAll (Colours::grey); + } + + void resized() + { + panel.setBounds (getLocalBounds()); + } + +private: + PropertyPanel panel; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProgramAudioProcessorEditor) +}; + +//============================================================================== +PluginWindow* PluginWindow::getWindowFor (AudioProcessorGraph::Node* const node, + WindowFormatType type) +{ + jassert (node != nullptr); + + for (int i = activePluginWindows.size(); --i >= 0;) + if (activePluginWindows.getUnchecked(i)->owner == node + && activePluginWindows.getUnchecked(i)->type == type) + return activePluginWindows.getUnchecked(i); + + AudioProcessor* processor = node->getProcessor(); + AudioProcessorEditor* ui = nullptr; + + if (type == Normal) + { + ui = processor->createEditorIfNeeded(); + + if (ui == nullptr) + type = Generic; + } + + if (ui == nullptr) + { + if (type == Generic || type == Parameters) + ui = new GenericAudioProcessorEditor (processor); + else if (type == Programs) + ui = new ProgramAudioProcessorEditor (processor); + } + + if (ui != nullptr) + { + if (AudioPluginInstance* const plugin = dynamic_cast (processor)) + ui->setName (plugin->getName()); + + return new PluginWindow (ui, node, type); + } + + return nullptr; +} + +PluginWindow::~PluginWindow() +{ + activePluginWindows.removeFirstMatchingValue (this); + clearContentComponent(); +} + +void PluginWindow::moved() +{ + owner->properties.set ("uiLastX", getX()); + owner->properties.set ("uiLastY", getY()); +} + +void PluginWindow::closeButtonPressed() +{ + delete this; +} + +//============================================================================== +class PinComponent : public Component, + public SettableTooltipClient +{ +public: + PinComponent (FilterGraph& graph_, + const uint32 filterID_, const int index_, const bool isInput_) + : filterID (filterID_), + index (index_), + isInput (isInput_), + graph (graph_) + { + if (const AudioProcessorGraph::Node::Ptr node = graph.getNodeForId (filterID_)) + { + String tip; + + if (index_ == FilterGraph::midiChannelNumber) + { + tip = isInput ? "MIDI Input" : "MIDI Output"; + } + else + { + if (isInput) + tip = node->getProcessor()->getInputChannelName (index_); + else + tip = node->getProcessor()->getOutputChannelName (index_); + + if (tip.isEmpty()) + tip = (isInput ? "Input " : "Output ") + String (index_ + 1); + } + + setTooltip (tip); + } + + setSize (16, 16); + } + + void paint (Graphics& g) + { + const float w = (float) getWidth(); + const float h = (float) getHeight(); + + Path p; + p.addEllipse (w * 0.25f, h * 0.25f, w * 0.5f, h * 0.5f); + + p.addRectangle (w * 0.4f, isInput ? (0.5f * h) : 0.0f, w * 0.2f, h * 0.5f); + + g.setColour (index == FilterGraph::midiChannelNumber ? Colours::red : Colours::green); + g.fillPath (p); + } + + void mouseDown (const MouseEvent& e) + { + getGraphPanel()->beginConnectorDrag (isInput ? 0 : filterID, + index, + isInput ? filterID : 0, + index, + e); + } + + void mouseDrag (const MouseEvent& e) + { + getGraphPanel()->dragConnector (e); + } + + void mouseUp (const MouseEvent& e) + { + getGraphPanel()->endDraggingConnector (e); + } + + const uint32 filterID; + const int index; + const bool isInput; + +private: + FilterGraph& graph; + + GraphEditorPanel* getGraphPanel() const noexcept + { + return findParentComponentOfClass(); + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PinComponent) +}; + +//============================================================================== +class FilterComponent : public Component +{ +public: + FilterComponent (FilterGraph& graph_, + const uint32 filterID_) + : graph (graph_), + filterID (filterID_), + numInputs (0), + numOutputs (0), + pinSize (16), + font (13.0f, Font::bold), + numIns (0), + numOuts (0) + { + shadow.setShadowProperties (DropShadow (Colours::black.withAlpha (0.5f), 3, Point (0, 1))); + setComponentEffect (&shadow); + + setSize (150, 60); + } + + ~FilterComponent() + { + deleteAllChildren(); + } + + void mouseDown (const MouseEvent& e) + { + originalPos = localPointToGlobal (Point()); + + toFront (true); + + if (e.mods.isPopupMenu()) + { + PopupMenu m; + m.addItem (1, "Delete this filter"); + m.addItem (2, "Disconnect all pins"); + m.addSeparator(); + m.addItem (3, "Show plugin UI"); + m.addItem (4, "Show all programs"); + m.addItem (5, "Show all parameters"); + m.addItem (6, "Test state save/load"); + + const int r = m.show(); + + if (r == 1) + { + graph.removeFilter (filterID); + return; + } + else if (r == 2) + { + graph.disconnectFilter (filterID); + } + else + { + if (AudioProcessorGraph::Node::Ptr f = graph.getNodeForId (filterID)) + { + AudioProcessor* const processor = f->getProcessor(); + jassert (processor != nullptr); + + if (r == 6) + { + MemoryBlock state; + processor->getStateInformation (state); + processor->setStateInformation (state.getData(), (int) state.getSize()); + } + else + { + PluginWindow::WindowFormatType type = processor->hasEditor() ? PluginWindow::Normal + : PluginWindow::Generic; + + switch (r) + { + case 4: type = PluginWindow::Programs; break; + case 5: type = PluginWindow::Parameters; break; + + default: break; + }; + + if (PluginWindow* const w = PluginWindow::getWindowFor (f, type)) + w->toFront (true); + } + } + } + } + } + + void mouseDrag (const MouseEvent& e) + { + if (! e.mods.isPopupMenu()) + { + Point pos (originalPos + Point (e.getDistanceFromDragStartX(), e.getDistanceFromDragStartY())); + + if (getParentComponent() != nullptr) + pos = getParentComponent()->getLocalPoint (nullptr, pos); + + graph.setNodePosition (filterID, + (pos.getX() + getWidth() / 2) / (double) getParentWidth(), + (pos.getY() + getHeight() / 2) / (double) getParentHeight()); + + getGraphPanel()->updateComponents(); + } + } + + void mouseUp (const MouseEvent& e) + { + if (e.mouseWasClicked() && e.getNumberOfClicks() == 2) + { + if (const AudioProcessorGraph::Node::Ptr f = graph.getNodeForId (filterID)) + if (PluginWindow* const w = PluginWindow::getWindowFor (f, PluginWindow::Normal)) + w->toFront (true); + } + else if (! e.mouseWasClicked()) + { + graph.setChangedFlag (true); + } + } + + bool hitTest (int x, int y) + { + for (int i = getNumChildComponents(); --i >= 0;) + if (getChildComponent(i)->getBounds().contains (x, y)) + return true; + + return x >= 3 && x < getWidth() - 6 && y >= pinSize && y < getHeight() - pinSize; + } + + void paint (Graphics& g) + { + g.setColour (Colours::lightgrey); + + const int x = 4; + const int y = pinSize; + const int w = getWidth() - x * 2; + const int h = getHeight() - pinSize * 2; + + g.fillRect (x, y, w, h); + + g.setColour (Colours::black); + g.setFont (font); + g.drawFittedText (getName(), getLocalBounds().reduced (4, 2), Justification::centred, 2); + + g.setColour (Colours::grey); + g.drawRect (x, y, w, h); + } + + void resized() + { + for (int i = 0; i < getNumChildComponents(); ++i) + { + if (PinComponent* const pc = dynamic_cast (getChildComponent(i))) + { + const int total = pc->isInput ? numIns : numOuts; + const int index = pc->index == FilterGraph::midiChannelNumber ? (total - 1) : pc->index; + + pc->setBounds (proportionOfWidth ((1 + index) / (total + 1.0f)) - pinSize / 2, + pc->isInput ? 0 : (getHeight() - pinSize), + pinSize, pinSize); + } + } + } + + void getPinPos (const int index, const bool isInput, float& x, float& y) + { + for (int i = 0; i < getNumChildComponents(); ++i) + { + if (PinComponent* const pc = dynamic_cast (getChildComponent(i))) + { + if (pc->index == index && isInput == pc->isInput) + { + x = getX() + pc->getX() + pc->getWidth() * 0.5f; + y = getY() + pc->getY() + pc->getHeight() * 0.5f; + break; + } + } + } + } + + void update() + { + const AudioProcessorGraph::Node::Ptr f (graph.getNodeForId (filterID)); + + if (f == nullptr) + { + delete this; + return; + } + + numIns = f->getProcessor()->getNumInputChannels(); + if (f->getProcessor()->acceptsMidi()) + ++numIns; + + numOuts = f->getProcessor()->getNumOutputChannels(); + if (f->getProcessor()->producesMidi()) + ++numOuts; + + int w = 100; + int h = 60; + + w = jmax (w, (jmax (numIns, numOuts) + 1) * 20); + + const int textWidth = font.getStringWidth (f->getProcessor()->getName()); + w = jmax (w, 16 + jmin (textWidth, 300)); + if (textWidth > 300) + h = 100; + + setSize (w, h); + + setName (f->getProcessor()->getName()); + + { + double x, y; + graph.getNodePosition (filterID, x, y); + setCentreRelative ((float) x, (float) y); + } + + if (numIns != numInputs || numOuts != numOutputs) + { + numInputs = numIns; + numOutputs = numOuts; + + deleteAllChildren(); + + int i; + for (i = 0; i < f->getProcessor()->getNumInputChannels(); ++i) + addAndMakeVisible (new PinComponent (graph, filterID, i, true)); + + if (f->getProcessor()->acceptsMidi()) + addAndMakeVisible (new PinComponent (graph, filterID, FilterGraph::midiChannelNumber, true)); + + for (i = 0; i < f->getProcessor()->getNumOutputChannels(); ++i) + addAndMakeVisible (new PinComponent (graph, filterID, i, false)); + + if (f->getProcessor()->producesMidi()) + addAndMakeVisible (new PinComponent (graph, filterID, FilterGraph::midiChannelNumber, false)); + + resized(); + } + } + + FilterGraph& graph; + const uint32 filterID; + int numInputs, numOutputs; + +private: + int pinSize; + Point originalPos; + Font font; + int numIns, numOuts; + DropShadowEffect shadow; + + GraphEditorPanel* getGraphPanel() const noexcept + { + return findParentComponentOfClass(); + } + + FilterComponent (const FilterComponent&); + FilterComponent& operator= (const FilterComponent&); +}; + +//============================================================================== +class ConnectorComponent : public Component, + public SettableTooltipClient +{ +public: + ConnectorComponent (FilterGraph& graph_) + : sourceFilterID (0), + destFilterID (0), + sourceFilterChannel (0), + destFilterChannel (0), + graph (graph_), + lastInputX (0), + lastInputY (0), + lastOutputX (0), + lastOutputY (0) + { + setAlwaysOnTop (true); + } + + void setInput (const uint32 sourceFilterID_, const int sourceFilterChannel_) + { + if (sourceFilterID != sourceFilterID_ || sourceFilterChannel != sourceFilterChannel_) + { + sourceFilterID = sourceFilterID_; + sourceFilterChannel = sourceFilterChannel_; + update(); + } + } + + void setOutput (const uint32 destFilterID_, const int destFilterChannel_) + { + if (destFilterID != destFilterID_ || destFilterChannel != destFilterChannel_) + { + destFilterID = destFilterID_; + destFilterChannel = destFilterChannel_; + update(); + } + } + + void dragStart (int x, int y) + { + lastInputX = (float) x; + lastInputY = (float) y; + resizeToFit(); + } + + void dragEnd (int x, int y) + { + lastOutputX = (float) x; + lastOutputY = (float) y; + resizeToFit(); + } + + void update() + { + float x1, y1, x2, y2; + getPoints (x1, y1, x2, y2); + + if (lastInputX != x1 + || lastInputY != y1 + || lastOutputX != x2 + || lastOutputY != y2) + { + resizeToFit(); + } + } + + void resizeToFit() + { + float x1, y1, x2, y2; + getPoints (x1, y1, x2, y2); + + const Rectangle newBounds ((int) jmin (x1, x2) - 4, + (int) jmin (y1, y2) - 4, + (int) std::abs (x1 - x2) + 8, + (int) std::abs (y1 - y2) + 8); + + if (newBounds != getBounds()) + setBounds (newBounds); + else + resized(); + + repaint(); + } + + void getPoints (float& x1, float& y1, float& x2, float& y2) const + { + x1 = lastInputX; + y1 = lastInputY; + x2 = lastOutputX; + y2 = lastOutputY; + + if (GraphEditorPanel* const hostPanel = getGraphPanel()) + { + if (FilterComponent* srcFilterComp = hostPanel->getComponentForFilter (sourceFilterID)) + srcFilterComp->getPinPos (sourceFilterChannel, false, x1, y1); + + if (FilterComponent* dstFilterComp = hostPanel->getComponentForFilter (destFilterID)) + dstFilterComp->getPinPos (destFilterChannel, true, x2, y2); + } + } + + void paint (Graphics& g) + { + if (sourceFilterChannel == FilterGraph::midiChannelNumber + || destFilterChannel == FilterGraph::midiChannelNumber) + { + g.setColour (Colours::red); + } + else + { + g.setColour (Colours::green); + } + + g.fillPath (linePath); + } + + bool hitTest (int x, int y) + { + if (hitPath.contains ((float) x, (float) y)) + { + double distanceFromStart, distanceFromEnd; + getDistancesFromEnds (x, y, distanceFromStart, distanceFromEnd); + + // avoid clicking the connector when over a pin + return distanceFromStart > 7.0 && distanceFromEnd > 7.0; + } + + return false; + } + + void mouseDown (const MouseEvent&) + { + dragging = false; + } + + void mouseDrag (const MouseEvent& e) + { + if ((! dragging) && ! e.mouseWasClicked()) + { + dragging = true; + + graph.removeConnection (sourceFilterID, sourceFilterChannel, destFilterID, destFilterChannel); + + double distanceFromStart, distanceFromEnd; + getDistancesFromEnds (e.x, e.y, distanceFromStart, distanceFromEnd); + const bool isNearerSource = (distanceFromStart < distanceFromEnd); + + getGraphPanel()->beginConnectorDrag (isNearerSource ? 0 : sourceFilterID, + sourceFilterChannel, + isNearerSource ? destFilterID : 0, + destFilterChannel, + e); + } + else if (dragging) + { + getGraphPanel()->dragConnector (e); + } + } + + void mouseUp (const MouseEvent& e) + { + if (dragging) + getGraphPanel()->endDraggingConnector (e); + } + + void resized() + { + float x1, y1, x2, y2; + getPoints (x1, y1, x2, y2); + + lastInputX = x1; + lastInputY = y1; + lastOutputX = x2; + lastOutputY = y2; + + x1 -= getX(); + y1 -= getY(); + x2 -= getX(); + y2 -= getY(); + + linePath.clear(); + linePath.startNewSubPath (x1, y1); + linePath.cubicTo (x1, y1 + (y2 - y1) * 0.33f, + x2, y1 + (y2 - y1) * 0.66f, + x2, y2); + + PathStrokeType wideStroke (8.0f); + wideStroke.createStrokedPath (hitPath, linePath); + + PathStrokeType stroke (2.5f); + stroke.createStrokedPath (linePath, linePath); + + const float arrowW = 5.0f; + const float arrowL = 4.0f; + + Path arrow; + arrow.addTriangle (-arrowL, arrowW, + -arrowL, -arrowW, + arrowL, 0.0f); + + arrow.applyTransform (AffineTransform::identity + .rotated (float_Pi * 0.5f - (float) atan2 (x2 - x1, y2 - y1)) + .translated ((x1 + x2) * 0.5f, + (y1 + y2) * 0.5f)); + + linePath.addPath (arrow); + linePath.setUsingNonZeroWinding (true); + } + + uint32 sourceFilterID, destFilterID; + int sourceFilterChannel, destFilterChannel; + +private: + FilterGraph& graph; + float lastInputX, lastInputY, lastOutputX, lastOutputY; + Path linePath, hitPath; + bool dragging; + + GraphEditorPanel* getGraphPanel() const noexcept + { + return findParentComponentOfClass(); + } + + void getDistancesFromEnds (int x, int y, double& distanceFromStart, double& distanceFromEnd) const + { + float x1, y1, x2, y2; + getPoints (x1, y1, x2, y2); + + distanceFromStart = juce_hypot (x - (x1 - getX()), y - (y1 - getY())); + distanceFromEnd = juce_hypot (x - (x2 - getX()), y - (y2 - getY())); + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConnectorComponent) +}; + + +//============================================================================== +GraphEditorPanel::GraphEditorPanel (FilterGraph& graph_) + : graph (graph_) +{ + graph.addChangeListener (this); + setOpaque (true); +} + +GraphEditorPanel::~GraphEditorPanel() +{ + graph.removeChangeListener (this); + draggingConnector = nullptr; + deleteAllChildren(); +} + +void GraphEditorPanel::paint (Graphics& g) +{ + g.fillAll (Colours::white); +} + +void GraphEditorPanel::mouseDown (const MouseEvent& e) +{ + if (e.mods.isPopupMenu()) + { +#if 0 + PopupMenu m; + + if (MainHostWindow* const mainWindow = findParentComponentOfClass()) + { + mainWindow->addPluginsToMenu (m); + + const int r = m.show(); + + createNewPlugin (mainWindow->getChosenType (r), e.x, e.y); + } +#endif + } +} + +void GraphEditorPanel::createNewPlugin (const PluginDescription* desc, int x, int y) +{ + graph.addFilter (desc, x / (double) getWidth(), y / (double) getHeight()); +} + +FilterComponent* GraphEditorPanel::getComponentForFilter (const uint32 filterID) const +{ + for (int i = getNumChildComponents(); --i >= 0;) + { + if (FilterComponent* const fc = dynamic_cast (getChildComponent (i))) + if (fc->filterID == filterID) + return fc; + } + + return nullptr; +} + +ConnectorComponent* GraphEditorPanel::getComponentForConnection (const AudioProcessorGraph::Connection& conn) const +{ + for (int i = getNumChildComponents(); --i >= 0;) + { + if (ConnectorComponent* const c = dynamic_cast (getChildComponent (i))) + if (c->sourceFilterID == conn.sourceNodeId + && c->destFilterID == conn.destNodeId + && c->sourceFilterChannel == conn.sourceChannelIndex + && c->destFilterChannel == conn.destChannelIndex) + return c; + } + + return nullptr; +} + +PinComponent* GraphEditorPanel::findPinAt (const int x, const int y) const +{ + for (int i = getNumChildComponents(); --i >= 0;) + { + if (FilterComponent* fc = dynamic_cast (getChildComponent (i))) + { + if (PinComponent* pin = dynamic_cast (fc->getComponentAt (x - fc->getX(), + y - fc->getY()))) + return pin; + } + } + + return nullptr; +} + +void GraphEditorPanel::resized() +{ + updateComponents(); +} + +void GraphEditorPanel::changeListenerCallback (ChangeBroadcaster*) +{ + updateComponents(); +} + +void GraphEditorPanel::updateComponents() +{ + for (int i = getNumChildComponents(); --i >= 0;) + { + if (FilterComponent* const fc = dynamic_cast (getChildComponent (i))) + fc->update(); + } + + for (int i = getNumChildComponents(); --i >= 0;) + { + ConnectorComponent* const cc = dynamic_cast (getChildComponent (i)); + + if (cc != nullptr && cc != draggingConnector) + { + if (graph.getConnectionBetween (cc->sourceFilterID, cc->sourceFilterChannel, + cc->destFilterID, cc->destFilterChannel) == nullptr) + { + delete cc; + } + else + { + cc->update(); + } + } + } + + for (int i = graph.getNumFilters(); --i >= 0;) + { + const AudioProcessorGraph::Node::Ptr f (graph.getNode (i)); + + if (getComponentForFilter (f->nodeId) == 0) + { + FilterComponent* const comp = new FilterComponent (graph, f->nodeId); + addAndMakeVisible (comp); + comp->update(); + } + } + + for (int i = graph.getNumConnections(); --i >= 0;) + { + const AudioProcessorGraph::Connection* const c = graph.getConnection (i); + + if (getComponentForConnection (*c) == 0) + { + ConnectorComponent* const comp = new ConnectorComponent (graph); + addAndMakeVisible (comp); + + comp->setInput (c->sourceNodeId, c->sourceChannelIndex); + comp->setOutput (c->destNodeId, c->destChannelIndex); + } + } +} + +void GraphEditorPanel::beginConnectorDrag (const uint32 sourceFilterID, const int sourceFilterChannel, + const uint32 destFilterID, const int destFilterChannel, + const MouseEvent& e) +{ + draggingConnector = dynamic_cast (e.originalComponent); + + if (draggingConnector == nullptr) + draggingConnector = new ConnectorComponent (graph); + + draggingConnector->setInput (sourceFilterID, sourceFilterChannel); + draggingConnector->setOutput (destFilterID, destFilterChannel); + + addAndMakeVisible (draggingConnector); + draggingConnector->toFront (false); + + dragConnector (e); +} + +void GraphEditorPanel::dragConnector (const MouseEvent& e) +{ + const MouseEvent e2 (e.getEventRelativeTo (this)); + + if (draggingConnector != nullptr) + { + draggingConnector->setTooltip (String::empty); + + int x = e2.x; + int y = e2.y; + + if (PinComponent* const pin = findPinAt (x, y)) + { + uint32 srcFilter = draggingConnector->sourceFilterID; + int srcChannel = draggingConnector->sourceFilterChannel; + uint32 dstFilter = draggingConnector->destFilterID; + int dstChannel = draggingConnector->destFilterChannel; + + if (srcFilter == 0 && ! pin->isInput) + { + srcFilter = pin->filterID; + srcChannel = pin->index; + } + else if (dstFilter == 0 && pin->isInput) + { + dstFilter = pin->filterID; + dstChannel = pin->index; + } + + if (graph.canConnect (srcFilter, srcChannel, dstFilter, dstChannel)) + { + x = pin->getParentComponent()->getX() + pin->getX() + pin->getWidth() / 2; + y = pin->getParentComponent()->getY() + pin->getY() + pin->getHeight() / 2; + + draggingConnector->setTooltip (pin->getTooltip()); + } + } + + if (draggingConnector->sourceFilterID == 0) + draggingConnector->dragStart (x, y); + else + draggingConnector->dragEnd (x, y); + } +} + +void GraphEditorPanel::endDraggingConnector (const MouseEvent& e) +{ + if (draggingConnector == nullptr) + return; + + draggingConnector->setTooltip (String::empty); + + const MouseEvent e2 (e.getEventRelativeTo (this)); + + uint32 srcFilter = draggingConnector->sourceFilterID; + int srcChannel = draggingConnector->sourceFilterChannel; + uint32 dstFilter = draggingConnector->destFilterID; + int dstChannel = draggingConnector->destFilterChannel; + + draggingConnector = nullptr; + + if (PinComponent* const pin = findPinAt (e2.x, e2.y)) + { + if (srcFilter == 0) + { + if (pin->isInput) + return; + + srcFilter = pin->filterID; + srcChannel = pin->index; + } + else + { + if (! pin->isInput) + return; + + dstFilter = pin->filterID; + dstChannel = pin->index; + } + + graph.addConnection (srcFilter, srcChannel, dstFilter, dstChannel); + } +} + + +//============================================================================== +class TooltipBar : public Component, + private Timer +{ +public: + TooltipBar() + { + startTimer (100); + } + + void paint (Graphics& g) + { + g.setFont (Font (getHeight() * 0.7f, Font::bold)); + g.setColour (Colours::black); + g.drawFittedText (tip, 10, 0, getWidth() - 12, getHeight(), Justification::centredLeft, 1); + } + + void timerCallback() + { + Component* const underMouse = Desktop::getInstance().getMainMouseSource().getComponentUnderMouse(); + TooltipClient* const ttc = dynamic_cast (underMouse); + + String newTip; + + if (ttc != nullptr && ! (underMouse->isMouseButtonDown() || underMouse->isCurrentlyBlockedByAnotherModalComponent())) + newTip = ttc->getTooltip(); + + if (newTip != tip) + { + tip = newTip; + repaint(); + } + } + +private: + String tip; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TooltipBar) +}; + +//============================================================================== +GraphDocumentComponent::GraphDocumentComponent (FilterGraph& g) + : graph (g) +{ + addAndMakeVisible (graphPanel = new GraphEditorPanel (graph)); + + //deviceManager->addChangeListener (graphPanel); + + //keyState.addListener (&graphPlayer.getMidiMessageCollector()); + + addAndMakeVisible (keyboardComp = new MidiKeyboardComponent (keyState, + MidiKeyboardComponent::horizontalKeyboard)); + + addAndMakeVisible (statusBar = new TooltipBar()); + + graphPanel->updateComponents(); +} + +GraphDocumentComponent::~GraphDocumentComponent() +{ + //deviceManager->removeChangeListener (graphPanel); + + deleteAllChildren(); + + //keyState.removeListener (&graphPlayer.getMidiMessageCollector()); +} + +void GraphDocumentComponent::resized() +{ + const int keysHeight = 60; + const int statusHeight = 20; + + graphPanel->setBounds (0, 0, getWidth(), getHeight() - keysHeight); + statusBar->setBounds (0, getHeight() - keysHeight - statusHeight, getWidth(), statusHeight); + keyboardComp->setBounds (0, getHeight() - keysHeight, getWidth(), keysHeight); +} + +void GraphDocumentComponent::createNewPlugin (const PluginDescription* desc, int x, int y) +{ + graphPanel->createNewPlugin (desc, x, y); +} diff --git a/source/modules/daz-plugins/juce-host/GraphEditorPanel.h b/source/modules/daz-plugins/juce-host/GraphEditorPanel.h new file mode 100644 index 000000000..e9b78581c --- /dev/null +++ b/source/modules/daz-plugins/juce-host/GraphEditorPanel.h @@ -0,0 +1,141 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#ifndef __GRAPHEDITORPANEL_JUCEHEADER__ +#define __GRAPHEDITORPANEL_JUCEHEADER__ + +#include "FilterGraph.h" + +class FilterComponent; +class ConnectorComponent; +class PinComponent; + + +//============================================================================== +/** + A panel that displays and edits a FilterGraph. +*/ +class GraphEditorPanel : public Component, + public ChangeListener +{ +public: + GraphEditorPanel (FilterGraph& graph); + ~GraphEditorPanel(); + + void paint (Graphics& g); + void mouseDown (const MouseEvent& e); + + void createNewPlugin (const PluginDescription* desc, int x, int y); + + FilterComponent* getComponentForFilter (uint32 filterID) const; + ConnectorComponent* getComponentForConnection (const AudioProcessorGraph::Connection& conn) const; + PinComponent* findPinAt (int x, int y) const; + + void resized(); + void changeListenerCallback (ChangeBroadcaster*); + void updateComponents(); + + //============================================================================== + void beginConnectorDrag (uint32 sourceFilterID, int sourceFilterChannel, + uint32 destFilterID, int destFilterChannel, + const MouseEvent& e); + void dragConnector (const MouseEvent& e); + void endDraggingConnector (const MouseEvent& e); + + //============================================================================== +private: + FilterGraph& graph; + ScopedPointer draggingConnector; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GraphEditorPanel) +}; + + +//============================================================================== +/** + A panel that embeds a GraphEditorPanel with a midi keyboard at the bottom. + + It also manages the graph itself, and plays it. +*/ +class GraphDocumentComponent : public Component +{ +public: + //============================================================================== + GraphDocumentComponent (FilterGraph& graph); + ~GraphDocumentComponent(); + + //============================================================================== + void createNewPlugin (const PluginDescription* desc, int x, int y); + + //============================================================================== + FilterGraph& graph; + + //============================================================================== + void resized(); + +private: + //============================================================================== + MidiKeyboardState keyState; + + GraphEditorPanel* graphPanel; + Component* keyboardComp; + Component* statusBar; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GraphDocumentComponent) +}; + +//============================================================================== +/** A desktop window containing a plugin's UI. */ +class PluginWindow : public DocumentWindow +{ +public: + enum WindowFormatType + { + Normal = 0, + Generic, + Programs, + Parameters + }; + + PluginWindow (Component* pluginEditor, AudioProcessorGraph::Node*, WindowFormatType); + ~PluginWindow(); + + static PluginWindow* getWindowFor (AudioProcessorGraph::Node*, WindowFormatType); + + static void closeCurrentlyOpenWindowsFor (const uint32 nodeId); + static void closeAllCurrentlyOpenWindows(); + + void moved() override; + void closeButtonPressed() override; + +private: + AudioProcessorGraph::Node* owner; + WindowFormatType type; + + float getDesktopScaleFactor() const override { return 1.0f; } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginWindow) +}; + +#endif // __GRAPHEDITORPANEL_JUCEHEADER__ diff --git a/source/modules/daz-plugins/juce-host/InternalFilters.cpp b/source/modules/daz-plugins/juce-host/InternalFilters.cpp new file mode 100644 index 000000000..9c6645206 --- /dev/null +++ b/source/modules/daz-plugins/juce-host/InternalFilters.cpp @@ -0,0 +1,80 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#include "InternalFilters.h" +#include "FilterGraph.h" + + +//============================================================================== +InternalPluginFormat::InternalPluginFormat() +{ + { + AudioProcessorGraph::AudioGraphIOProcessor p (AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode); + p.fillInPluginDescription (audioOutDesc); + } + + { + AudioProcessorGraph::AudioGraphIOProcessor p (AudioProcessorGraph::AudioGraphIOProcessor::audioInputNode); + p.fillInPluginDescription (audioInDesc); + } + + { + AudioProcessorGraph::AudioGraphIOProcessor p (AudioProcessorGraph::AudioGraphIOProcessor::midiInputNode); + p.fillInPluginDescription (midiInDesc); + } +} + +AudioPluginInstance* InternalPluginFormat::createInstanceFromDescription (const PluginDescription& desc, + double /*sampleRate*/, int /*blockSize*/) +{ + if (desc.name == audioOutDesc.name) + return new AudioProcessorGraph::AudioGraphIOProcessor (AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode); + + if (desc.name == audioInDesc.name) + return new AudioProcessorGraph::AudioGraphIOProcessor (AudioProcessorGraph::AudioGraphIOProcessor::audioInputNode); + + if (desc.name == midiInDesc.name) + return new AudioProcessorGraph::AudioGraphIOProcessor (AudioProcessorGraph::AudioGraphIOProcessor::midiInputNode); + + return 0; +} + +const PluginDescription* InternalPluginFormat::getDescriptionFor (const InternalFilterType type) +{ + switch (type) + { + case audioInputFilter: return &audioInDesc; + case audioOutputFilter: return &audioOutDesc; + case midiInputFilter: return &midiInDesc; + default: break; + } + + return 0; +} + +void InternalPluginFormat::getAllTypes (OwnedArray & results) +{ + for (int i = 0; i < (int) endOfFilterTypes; ++i) + results.add (new PluginDescription (*getDescriptionFor ((InternalFilterType) i))); +} diff --git a/source/modules/daz-plugins/juce-host/InternalFilters.h b/source/modules/daz-plugins/juce-host/InternalFilters.h new file mode 100644 index 000000000..189be003f --- /dev/null +++ b/source/modules/daz-plugins/juce-host/InternalFilters.h @@ -0,0 +1,76 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#ifndef __INTERNALFILTERS_JUCEHEADER__ +#define __INTERNALFILTERS_JUCEHEADER__ + +#include "FilterGraph.h" + + +//============================================================================== +/** + Manages the internal plugin types. +*/ +class InternalPluginFormat : public AudioPluginFormat +{ +public: + //============================================================================== + InternalPluginFormat(); + ~InternalPluginFormat() {} + + //============================================================================== + enum InternalFilterType + { + audioInputFilter = 0, + audioOutputFilter, + midiInputFilter, + + endOfFilterTypes + }; + + const PluginDescription* getDescriptionFor (const InternalFilterType type); + + void getAllTypes (OwnedArray & results); + + //============================================================================== + String getName() const override { return "Internal"; } + bool fileMightContainThisPluginType (const String&) override { return false; } + FileSearchPath getDefaultLocationsToSearch() override { return FileSearchPath(); } + bool canScanForPlugins() const override { return false; } + void findAllTypesForFile (OwnedArray &, const String&) override {} + bool doesPluginStillExist (const PluginDescription&) override { return true; } + String getNameOfPluginFromIdentifier (const String& fileOrIdentifier) override { return fileOrIdentifier; } + bool pluginNeedsRescanning (const PluginDescription&) override { return false; } + StringArray searchPathsForPlugins (const FileSearchPath&, bool) override { return StringArray(); } + AudioPluginInstance* createInstanceFromDescription (const PluginDescription&, double, int) override; + +private: + //============================================================================== + PluginDescription audioInDesc; + PluginDescription audioOutDesc; + PluginDescription midiInDesc; +}; + + +#endif // __INTERNALFILTERS_JUCEHEADER__ diff --git a/source/modules/daz-plugins/juce-host/MainHostWindow.cpp b/source/modules/daz-plugins/juce-host/MainHostWindow.cpp new file mode 100644 index 000000000..e86d1045c --- /dev/null +++ b/source/modules/daz-plugins/juce-host/MainHostWindow.cpp @@ -0,0 +1,439 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#include "juce_gui_basics.h" +#include "MainHostWindow.h" +#include "InternalFilters.h" + + +//============================================================================== +class MainHostWindow::PluginListWindow : public DocumentWindow +{ +public: + PluginListWindow (MainHostWindow& owner_, AudioPluginFormatManager& formatManager) + : DocumentWindow ("Available Plugins", Colours::white, + DocumentWindow::minimiseButton | DocumentWindow::closeButton), + owner (owner_) + { + const File deadMansPedalFile (getAppProperties().getUserSettings() + ->getFile().getSiblingFile ("RecentlyCrashedPluginsList")); + + setContentOwned (new PluginListComponent (formatManager, + owner.knownPluginList, + deadMansPedalFile, + getAppProperties().getUserSettings()), true); + + setResizable (true, false); + setResizeLimits (300, 400, 800, 1500); + setTopLeftPosition (60, 60); + + restoreWindowStateFromString (getAppProperties().getUserSettings()->getValue ("listWindowPos")); + setVisible (true); + } + + ~PluginListWindow() + { + getAppProperties().getUserSettings()->setValue ("listWindowPos", getWindowStateAsString()); + + clearContentComponent(); + } + + void closeButtonPressed() + { + owner.pluginListWindow = nullptr; + } + +private: + MainHostWindow& owner; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginListWindow) +}; + +//============================================================================== +MainHostWindow::MainHostWindow() + : DocumentWindow (JUCEApplication::getInstance()->getApplicationName(), Colours::lightgrey, + DocumentWindow::allButtons) +{ + formatManager.addDefaultFormats(); + formatManager.addFormat (new InternalPluginFormat()); + + ScopedPointer savedAudioState (getAppProperties().getUserSettings() + ->getXmlValue ("audioDeviceState")); + + //deviceManager.initialise (256, 256, savedAudioState, true); + + setResizable (true, false); + setResizeLimits (500, 400, 10000, 10000); + centreWithSize (800, 600); + + //setContentOwned (new GraphDocumentComponent (formatManager, &deviceManager), false); + + restoreWindowStateFromString (getAppProperties().getUserSettings()->getValue ("mainWindowPos")); + + setVisible (true); + + InternalPluginFormat internalFormat; + internalFormat.getAllTypes (internalTypes); + + ScopedPointer savedPluginList (getAppProperties().getUserSettings()->getXmlValue ("pluginList")); + + if (savedPluginList != nullptr) + knownPluginList.recreateFromXml (*savedPluginList); + + pluginSortMethod = (KnownPluginList::SortMethod) getAppProperties().getUserSettings() + ->getIntValue ("pluginSortMethod", KnownPluginList::sortByManufacturer); + + knownPluginList.addChangeListener (this); + + addKeyListener (getCommandManager().getKeyMappings()); + + Process::setPriority (Process::HighPriority); + + #if JUCE_MAC + setMacMainMenu (this); + #else + setMenuBar (this); + #endif + + getCommandManager().setFirstCommandTarget (this); +} + +MainHostWindow::~MainHostWindow() +{ + pluginListWindow = nullptr; + + #if JUCE_MAC + setMacMainMenu (nullptr); + #else + setMenuBar (nullptr); + #endif + + knownPluginList.removeChangeListener (this); + + getAppProperties().getUserSettings()->setValue ("mainWindowPos", getWindowStateAsString()); + clearContentComponent(); +} + +void MainHostWindow::closeButtonPressed() +{ + tryToQuitApplication(); +} + +bool MainHostWindow::tryToQuitApplication() +{ + PluginWindow::closeAllCurrentlyOpenWindows(); + + if (getGraphEditor() == nullptr + || getGraphEditor()->graph.saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk) + { + JUCEApplication::quit(); + return true; + } + + return false; +} + +void MainHostWindow::changeListenerCallback (ChangeBroadcaster*) +{ + menuItemsChanged(); + + // save the plugin list every time it gets chnaged, so that if we're scanning + // and it crashes, we've still saved the previous ones + ScopedPointer savedPluginList (knownPluginList.createXml()); + + if (savedPluginList != nullptr) + { + getAppProperties().getUserSettings()->setValue ("pluginList", savedPluginList); + getAppProperties().saveIfNeeded(); + } +} + +StringArray MainHostWindow::getMenuBarNames() +{ + const char* const names[] = { "File", "Plugins", "Options", nullptr }; + + return StringArray (names); +} + +PopupMenu MainHostWindow::getMenuForIndex (int topLevelMenuIndex, const String& /*menuName*/) +{ + PopupMenu menu; + + if (topLevelMenuIndex == 0) + { + // "File" menu + menu.addCommandItem (&getCommandManager(), CommandIDs::open); + + RecentlyOpenedFilesList recentFiles; + recentFiles.restoreFromString (getAppProperties().getUserSettings() + ->getValue ("recentFilterGraphFiles")); + + PopupMenu recentFilesMenu; + recentFiles.createPopupMenuItems (recentFilesMenu, 100, true, true); + menu.addSubMenu ("Open recent file", recentFilesMenu); + + menu.addCommandItem (&getCommandManager(), CommandIDs::save); + menu.addCommandItem (&getCommandManager(), CommandIDs::saveAs); + menu.addSeparator(); + menu.addCommandItem (&getCommandManager(), StandardApplicationCommandIDs::quit); + } + else if (topLevelMenuIndex == 1) + { + // "Plugins" menu + PopupMenu pluginsMenu; + addPluginsToMenu (pluginsMenu); + menu.addSubMenu ("Create plugin", pluginsMenu); + menu.addSeparator(); + menu.addItem (250, "Delete all plugins"); + } + else if (topLevelMenuIndex == 2) + { + // "Options" menu + + menu.addCommandItem (&getCommandManager(), CommandIDs::showPluginListEditor); + + PopupMenu sortTypeMenu; + sortTypeMenu.addItem (200, "List plugins in default order", true, pluginSortMethod == KnownPluginList::defaultOrder); + sortTypeMenu.addItem (201, "List plugins in alphabetical order", true, pluginSortMethod == KnownPluginList::sortAlphabetically); + sortTypeMenu.addItem (202, "List plugins by category", true, pluginSortMethod == KnownPluginList::sortByCategory); + sortTypeMenu.addItem (203, "List plugins by manufacturer", true, pluginSortMethod == KnownPluginList::sortByManufacturer); + sortTypeMenu.addItem (204, "List plugins based on the directory structure", true, pluginSortMethod == KnownPluginList::sortByFileSystemLocation); + menu.addSubMenu ("Plugin menu type", sortTypeMenu); + + menu.addSeparator(); + menu.addCommandItem (&getCommandManager(), CommandIDs::aboutBox); + } + + return menu; +} + +void MainHostWindow::menuItemSelected (int menuItemID, int /*topLevelMenuIndex*/) +{ + GraphDocumentComponent* const graphEditor = getGraphEditor(); + + if (menuItemID == 250) + { + if (graphEditor != nullptr) + graphEditor->graph.clear(); + } + else if (menuItemID >= 100 && menuItemID < 200) + { + RecentlyOpenedFilesList recentFiles; + recentFiles.restoreFromString (getAppProperties().getUserSettings() + ->getValue ("recentFilterGraphFiles")); + + if (graphEditor != nullptr && graphEditor->graph.saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk) + graphEditor->graph.loadFrom (recentFiles.getFile (menuItemID - 100), true); + } + else if (menuItemID >= 200 && menuItemID < 210) + { + if (menuItemID == 200) pluginSortMethod = KnownPluginList::defaultOrder; + else if (menuItemID == 201) pluginSortMethod = KnownPluginList::sortAlphabetically; + else if (menuItemID == 202) pluginSortMethod = KnownPluginList::sortByCategory; + else if (menuItemID == 203) pluginSortMethod = KnownPluginList::sortByManufacturer; + else if (menuItemID == 204) pluginSortMethod = KnownPluginList::sortByFileSystemLocation; + + getAppProperties().getUserSettings()->setValue ("pluginSortMethod", (int) pluginSortMethod); + + menuItemsChanged(); + } + else + { + createPlugin (getChosenType (menuItemID), + proportionOfWidth (0.3f + Random::getSystemRandom().nextFloat() * 0.6f), + proportionOfHeight (0.3f + Random::getSystemRandom().nextFloat() * 0.6f)); + } +} + +void MainHostWindow::createPlugin (const PluginDescription* desc, int x, int y) +{ + GraphDocumentComponent* const graphEditor = getGraphEditor(); + + if (graphEditor != nullptr) + graphEditor->createNewPlugin (desc, x, y); +} + +void MainHostWindow::addPluginsToMenu (PopupMenu& m) const +{ + for (int i = 0; i < internalTypes.size(); ++i) + m.addItem (i + 1, internalTypes.getUnchecked(i)->name); + + m.addSeparator(); + + knownPluginList.addToMenu (m, pluginSortMethod); +} + +const PluginDescription* MainHostWindow::getChosenType (const int menuID) const +{ + if (menuID >= 1 && menuID < 1 + internalTypes.size()) + return internalTypes [menuID - 1]; + + return knownPluginList.getType (knownPluginList.getIndexChosenByMenu (menuID)); +} + +//============================================================================== +ApplicationCommandTarget* MainHostWindow::getNextCommandTarget() +{ + return findFirstTargetParentComponent(); +} + +void MainHostWindow::getAllCommands (Array & commands) +{ + // this returns the set of all commands that this target can perform.. + const CommandID ids[] = { CommandIDs::open, + CommandIDs::save, + CommandIDs::saveAs, + CommandIDs::showPluginListEditor, + CommandIDs::aboutBox + }; + + commands.addArray (ids, numElementsInArray (ids)); +} + +void MainHostWindow::getCommandInfo (const CommandID commandID, ApplicationCommandInfo& result) +{ + const String category ("General"); + + switch (commandID) + { + case CommandIDs::open: + result.setInfo ("Open...", + "Opens a filter graph file", + category, 0); + result.defaultKeypresses.add (KeyPress ('o', ModifierKeys::commandModifier, 0)); + break; + + case CommandIDs::save: + result.setInfo ("Save", + "Saves the current graph to a file", + category, 0); + result.defaultKeypresses.add (KeyPress ('s', ModifierKeys::commandModifier, 0)); + break; + + case CommandIDs::saveAs: + result.setInfo ("Save As...", + "Saves a copy of the current graph to a file", + category, 0); + result.defaultKeypresses.add (KeyPress ('s', ModifierKeys::shiftModifier | ModifierKeys::commandModifier, 0)); + break; + + case CommandIDs::showPluginListEditor: + result.setInfo ("Edit the list of available plug-Ins...", String::empty, category, 0); + result.addDefaultKeypress ('p', ModifierKeys::commandModifier); + break; + + case CommandIDs::aboutBox: + result.setInfo ("About...", String::empty, category, 0); + break; + + default: + break; + } +} + +bool MainHostWindow::perform (const InvocationInfo& info) +{ + GraphDocumentComponent* const graphEditor = getGraphEditor(); + + switch (info.commandID) + { + case CommandIDs::open: + if (graphEditor != nullptr && graphEditor->graph.saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk) + graphEditor->graph.loadFromUserSpecifiedFile (true); + + break; + + case CommandIDs::save: + if (graphEditor != nullptr) + graphEditor->graph.save (true, true); + break; + + case CommandIDs::saveAs: + if (graphEditor != nullptr) + graphEditor->graph.saveAs (File::nonexistent, true, true, true); + break; + + case CommandIDs::showPluginListEditor: + if (pluginListWindow == nullptr) + pluginListWindow = new PluginListWindow (*this, formatManager); + + pluginListWindow->toFront (true); + break; + + case CommandIDs::aboutBox: + // TODO + break; + + default: + return false; + } + + return true; +} + +bool MainHostWindow::isInterestedInFileDrag (const StringArray&) +{ + return true; +} + +void MainHostWindow::fileDragEnter (const StringArray&, int, int) +{ +} + +void MainHostWindow::fileDragMove (const StringArray&, int, int) +{ +} + +void MainHostWindow::fileDragExit (const StringArray&) +{ +} + +void MainHostWindow::filesDropped (const StringArray& files, int x, int y) +{ + GraphDocumentComponent* const graphEditor = getGraphEditor(); + + if (graphEditor != nullptr) + { + if (files.size() == 1 && File (files[0]).hasFileExtension (filenameSuffix)) + { + if (graphEditor->graph.saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk) + graphEditor->graph.loadFrom (File (files[0]), true); + } + else + { + OwnedArray typesFound; + knownPluginList.scanAndAddDragAndDroppedFiles (formatManager, files, typesFound); + + Point pos (graphEditor->getLocalPoint (this, Point (x, y))); + + for (int i = 0; i < jmin (5, typesFound.size()); ++i) + createPlugin (typesFound.getUnchecked(i), pos.getX(), pos.getY()); + } + } +} + +GraphDocumentComponent* MainHostWindow::getGraphEditor() const +{ + return dynamic_cast (getContentComponent()); +} diff --git a/source/modules/daz-plugins/juce-host/MainHostWindow.h b/source/modules/daz-plugins/juce-host/MainHostWindow.h new file mode 100644 index 000000000..dd02735de --- /dev/null +++ b/source/modules/daz-plugins/juce-host/MainHostWindow.h @@ -0,0 +1,99 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#ifndef __MAINHOSTWINDOW_JUCEHEADER__ +#define __MAINHOSTWINDOW_JUCEHEADER__ + +#include "FilterGraph.h" +#include "GraphEditorPanel.h" + + +//============================================================================== +namespace CommandIDs +{ + static const int open = 0x30000; + static const int save = 0x30001; + static const int saveAs = 0x30002; + static const int showPluginListEditor = 0x30100; + static const int aboutBox = 0x30300; +} + +//============================================================================== +/** +*/ +class MainHostWindow : public DocumentWindow, + public MenuBarModel, + public ApplicationCommandTarget, + public ChangeListener, + public FileDragAndDropTarget +{ +public: + //============================================================================== + MainHostWindow(); + ~MainHostWindow(); + + //============================================================================== + void closeButtonPressed(); + void changeListenerCallback (ChangeBroadcaster*); + + bool isInterestedInFileDrag (const StringArray& files); + void fileDragEnter (const StringArray& files, int, int); + void fileDragMove (const StringArray& files, int, int); + void fileDragExit (const StringArray& files); + void filesDropped (const StringArray& files, int, int); + + StringArray getMenuBarNames(); + PopupMenu getMenuForIndex (int topLevelMenuIndex, const String& menuName); + void menuItemSelected (int menuItemID, int topLevelMenuIndex); + ApplicationCommandTarget* getNextCommandTarget(); + void getAllCommands (Array & commands); + void getCommandInfo (CommandID commandID, ApplicationCommandInfo& result); + bool perform (const InvocationInfo& info); + + bool tryToQuitApplication(); + + void createPlugin (const PluginDescription* desc, int x, int y); + + void addPluginsToMenu (PopupMenu& m) const; + const PluginDescription* getChosenType (const int menuID) const; + + GraphDocumentComponent* getGraphEditor() const; + +private: + //============================================================================== + //AudioDeviceManager deviceManager; + AudioPluginFormatManager formatManager; + + OwnedArray internalTypes; + KnownPluginList knownPluginList; + KnownPluginList::SortMethod pluginSortMethod; + + class PluginListWindow; + ScopedPointer pluginListWindow; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainHostWindow) +}; + + +#endif // __MAINHOSTWINDOW_JUCEHEADER__ diff --git a/source/modules/daz-plugins/juce-host/juce_MidiKeyboardComponent.cpp b/source/modules/daz-plugins/juce-host/juce_MidiKeyboardComponent.cpp new file mode 100644 index 000000000..fc9b90e9e --- /dev/null +++ b/source/modules/daz-plugins/juce-host/juce_MidiKeyboardComponent.cpp @@ -0,0 +1,901 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +class MidiKeyboardUpDownButton : public Button +{ +public: + MidiKeyboardUpDownButton (MidiKeyboardComponent& comp, const int d) + : Button (String::empty), + owner (comp), + delta (d) + { + setOpaque (true); + } + + void clicked() override + { + int note = owner.getLowestVisibleKey(); + + if (delta < 0) + note = (note - 1) / 12; + else + note = note / 12 + 1; + + owner.setLowestVisibleKey (note * 12); + } + + void paintButton (Graphics& g, bool isMouseOverButton, bool isButtonDown) override + { + owner.drawUpDownButton (g, getWidth(), getHeight(), + isMouseOverButton, isButtonDown, + delta > 0); + } + +private: + MidiKeyboardComponent& owner; + const int delta; + + JUCE_DECLARE_NON_COPYABLE (MidiKeyboardUpDownButton) +}; + +//============================================================================== +MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& s, + const Orientation o) + : state (s), + xOffset (0), + blackNoteLength (1), + keyWidth (16.0f), + orientation (o), + midiChannel (1), + midiInChannelMask (0xffff), + velocity (1.0f), + shouldCheckState (false), + rangeStart (0), + rangeEnd (127), + firstKey (12 * 4.0f), + canScroll (true), + useMousePositionForVelocity (true), + shouldCheckMousePos (false), + keyMappingOctave (6), + octaveNumForMiddleC (3) +{ + addChildComponent (scrollDown = new MidiKeyboardUpDownButton (*this, -1)); + addChildComponent (scrollUp = new MidiKeyboardUpDownButton (*this, 1)); + + // initialise with a default set of querty key-mappings.. + const char* const keymap = "awsedftgyhujkolp;"; + + for (int i = 0; keymap[i] != 0; ++i) + setKeyPressForNote (KeyPress (keymap[i], 0, 0), i); + + mouseOverNotes.insertMultiple (0, -1, 32); + mouseDownNotes.insertMultiple (0, -1, 32); + + setOpaque (true); + setWantsKeyboardFocus (true); + + state.addListener (this); + + startTimer (1000 / 20); +} + +MidiKeyboardComponent::~MidiKeyboardComponent() +{ + state.removeListener (this); +} + +//============================================================================== +void MidiKeyboardComponent::setKeyWidth (const float widthInPixels) +{ + keyWidth = widthInPixels; + resized(); +} + +void MidiKeyboardComponent::setOrientation (const Orientation newOrientation) +{ + if (orientation != newOrientation) + { + orientation = newOrientation; + resized(); + } +} + +void MidiKeyboardComponent::setAvailableRange (const int lowestNote, + const int highestNote) +{ + jassert (lowestNote >= 0 && lowestNote <= 127); + jassert (highestNote >= 0 && highestNote <= 127); + jassert (lowestNote <= highestNote); + + if (rangeStart != lowestNote || rangeEnd != highestNote) + { + rangeStart = jlimit (0, 127, lowestNote); + rangeEnd = jlimit (0, 127, highestNote); + firstKey = jlimit ((float) rangeStart, (float) rangeEnd, firstKey); + resized(); + } +} + +void MidiKeyboardComponent::setLowestVisibleKey (int noteNumber) +{ + setLowestVisibleKeyFloat ((float) noteNumber); +} + +void MidiKeyboardComponent::setLowestVisibleKeyFloat (float noteNumber) +{ + noteNumber = jlimit ((float) rangeStart, (float) rangeEnd, noteNumber); + + if (noteNumber != firstKey) + { + const bool hasMoved = (((int) firstKey) != (int) noteNumber); + firstKey = noteNumber; + + if (hasMoved) + sendChangeMessage(); + + resized(); + } +} + +void MidiKeyboardComponent::setScrollButtonsVisible (const bool newCanScroll) +{ + if (canScroll != newCanScroll) + { + canScroll = newCanScroll; + resized(); + } +} + +void MidiKeyboardComponent::colourChanged() +{ + repaint(); +} + +//============================================================================== +void MidiKeyboardComponent::setMidiChannel (const int midiChannelNumber) +{ + jassert (midiChannelNumber > 0 && midiChannelNumber <= 16); + + if (midiChannel != midiChannelNumber) + { + resetAnyKeysInUse(); + midiChannel = jlimit (1, 16, midiChannelNumber); + } +} + +void MidiKeyboardComponent::setMidiChannelsToDisplay (const int midiChannelMask) +{ + midiInChannelMask = midiChannelMask; + shouldCheckState = true; +} + +void MidiKeyboardComponent::setVelocity (const float v, const bool useMousePosition) +{ + velocity = jlimit (0.0f, 1.0f, v); + useMousePositionForVelocity = useMousePosition; +} + +//============================================================================== +void MidiKeyboardComponent::getKeyPosition (int midiNoteNumber, const float keyWidth_, int& x, int& w) const +{ + jassert (midiNoteNumber >= 0 && midiNoteNumber < 128); + + static const float blackNoteWidth = 0.7f; + + static const float notePos[] = { 0.0f, 1 - blackNoteWidth * 0.6f, + 1.0f, 2 - blackNoteWidth * 0.4f, + 2.0f, + 3.0f, 4 - blackNoteWidth * 0.7f, + 4.0f, 5 - blackNoteWidth * 0.5f, + 5.0f, 6 - blackNoteWidth * 0.3f, + 6.0f }; + + static const float widths[] = { 1.0f, blackNoteWidth, + 1.0f, blackNoteWidth, + 1.0f, + 1.0f, blackNoteWidth, + 1.0f, blackNoteWidth, + 1.0f, blackNoteWidth, + 1.0f }; + + const int octave = midiNoteNumber / 12; + const int note = midiNoteNumber % 12; + + x = roundToInt (octave * 7.0f * keyWidth_ + notePos [note] * keyWidth_); + w = roundToInt (widths [note] * keyWidth_); +} + +void MidiKeyboardComponent::getKeyPos (int midiNoteNumber, int& x, int& w) const +{ + getKeyPosition (midiNoteNumber, keyWidth, x, w); + + int rx, rw; + getKeyPosition (rangeStart, keyWidth, rx, rw); + + x -= xOffset + rx; +} + +Rectangle MidiKeyboardComponent::getWhiteNotePos (int noteNum) const +{ + int x, w; + getKeyPos (noteNum, x, w); + Rectangle pos; + + switch (orientation) + { + case horizontalKeyboard: pos.setBounds (x, 0, w, getHeight()); break; + case verticalKeyboardFacingLeft: pos.setBounds (0, x, getWidth(), w); break; + case verticalKeyboardFacingRight: pos.setBounds (0, getHeight() - x - w, getWidth(), w); break; + default: break; + } + + return pos; +} + +int MidiKeyboardComponent::getKeyStartPosition (const int midiNoteNumber) const +{ + int x, w; + getKeyPos (midiNoteNumber, x, w); + return x; +} + +int MidiKeyboardComponent::getNoteAtPosition (Point p) +{ + float v; + return xyToNote (p, v); +} + +const uint8 MidiKeyboardComponent::whiteNotes[] = { 0, 2, 4, 5, 7, 9, 11 }; +const uint8 MidiKeyboardComponent::blackNotes[] = { 1, 3, 6, 8, 10 }; + +int MidiKeyboardComponent::xyToNote (Point pos, float& mousePositionVelocity) +{ + if (! reallyContains (pos, false)) + return -1; + + Point p (pos); + + if (orientation != horizontalKeyboard) + { + p = Point (p.y, p.x); + + if (orientation == verticalKeyboardFacingLeft) + p = Point (p.x, getWidth() - p.y); + else + p = Point (getHeight() - p.x, p.y); + } + + return remappedXYToNote (p + Point (xOffset, 0), mousePositionVelocity); +} + +int MidiKeyboardComponent::remappedXYToNote (Point pos, float& mousePositionVelocity) const +{ + if (pos.getY() < blackNoteLength) + { + for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12) + { + for (int i = 0; i < 5; ++i) + { + const int note = octaveStart + blackNotes [i]; + + if (note >= rangeStart && note <= rangeEnd) + { + int kx, kw; + getKeyPos (note, kx, kw); + kx += xOffset; + + if (pos.x >= kx && pos.x < kx + kw) + { + mousePositionVelocity = pos.y / (float) blackNoteLength; + return note; + } + } + } + } + } + + for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12) + { + for (int i = 0; i < 7; ++i) + { + const int note = octaveStart + whiteNotes [i]; + + if (note >= rangeStart && note <= rangeEnd) + { + int kx, kw; + getKeyPos (note, kx, kw); + kx += xOffset; + + if (pos.x >= kx && pos.x < kx + kw) + { + const int whiteNoteLength = (orientation == horizontalKeyboard) ? getHeight() : getWidth(); + mousePositionVelocity = pos.y / (float) whiteNoteLength; + return note; + } + } + } + } + + mousePositionVelocity = 0; + return -1; +} + +//============================================================================== +void MidiKeyboardComponent::repaintNote (const int noteNum) +{ + if (noteNum >= rangeStart && noteNum <= rangeEnd) + repaint (getWhiteNotePos (noteNum)); +} + +void MidiKeyboardComponent::paint (Graphics& g) +{ + g.fillAll (Colours::white.overlaidWith (findColour (whiteNoteColourId))); + + const Colour lineColour (findColour (keySeparatorLineColourId)); + const Colour textColour (findColour (textLabelColourId)); + + int octave; + + for (octave = 0; octave < 128; octave += 12) + { + for (int white = 0; white < 7; ++white) + { + const int noteNum = octave + whiteNotes [white]; + + if (noteNum >= rangeStart && noteNum <= rangeEnd) + { + const Rectangle pos (getWhiteNotePos (noteNum)); + + drawWhiteNote (noteNum, g, pos.getX(), pos.getY(), pos.getWidth(), pos.getHeight(), + state.isNoteOnForChannels (midiInChannelMask, noteNum), + mouseOverNotes.contains (noteNum), lineColour, textColour); + } + } + } + + float x1 = 0.0f, y1 = 0.0f, x2 = 0.0f, y2 = 0.0f; + const int width = getWidth(); + const int height = getHeight(); + + if (orientation == verticalKeyboardFacingLeft) + { + x1 = width - 1.0f; + x2 = width - 5.0f; + } + else if (orientation == verticalKeyboardFacingRight) + x2 = 5.0f; + else + y2 = 5.0f; + + int x, w; + getKeyPos (rangeEnd, x, w); + x += w; + + const Colour shadowCol (findColour (shadowColourId)); + g.setGradientFill (ColourGradient (shadowCol, x1, y1, shadowCol.withAlpha (0.0f), x2, y2, false)); + + switch (orientation) + { + case horizontalKeyboard: g.fillRect (0, 0, x, 5); break; + case verticalKeyboardFacingLeft: g.fillRect (width - 5, 0, 5, x); break; + case verticalKeyboardFacingRight: g.fillRect (0, 0, 5, x); break; + default: break; + } + + g.setColour (lineColour); + + switch (orientation) + { + case horizontalKeyboard: g.fillRect (0, height - 1, x, 1); break; + case verticalKeyboardFacingLeft: g.fillRect (0, 0, 1, x); break; + case verticalKeyboardFacingRight: g.fillRect (width - 1, 0, 1, x); break; + default: break; + } + + const Colour blackNoteColour (findColour (blackNoteColourId)); + + for (octave = 0; octave < 128; octave += 12) + { + for (int black = 0; black < 5; ++black) + { + const int noteNum = octave + blackNotes [black]; + + if (noteNum >= rangeStart && noteNum <= rangeEnd) + { + getKeyPos (noteNum, x, w); + Rectangle pos; + + switch (orientation) + { + case horizontalKeyboard: pos.setBounds (x, 0, w, blackNoteLength); break; + case verticalKeyboardFacingLeft: pos.setBounds (width - blackNoteLength, x, blackNoteLength, w); break; + case verticalKeyboardFacingRight: pos.setBounds (0, height - x - w, blackNoteLength, w); break; + default: break; + } + + drawBlackNote (noteNum, g, pos.getX(), pos.getY(), pos.getWidth(), pos.getHeight(), + state.isNoteOnForChannels (midiInChannelMask, noteNum), + mouseOverNotes.contains (noteNum), blackNoteColour); + } + } + } +} + +void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber, + Graphics& g, int x, int y, int w, int h, + bool isDown, bool isOver, + const Colour& lineColour, + const Colour& textColour) +{ + Colour c (Colours::transparentWhite); + + if (isDown) c = findColour (keyDownOverlayColourId); + if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId)); + + g.setColour (c); + g.fillRect (x, y, w, h); + + const String text (getWhiteNoteText (midiNoteNumber)); + + if (text.isNotEmpty()) + { + g.setColour (textColour); + g.setFont (Font (jmin (12.0f, keyWidth * 0.9f)).withHorizontalScale (0.8f)); + + switch (orientation) + { + case horizontalKeyboard: g.drawFittedText (text, x + 1, y, w - 1, h - 2, Justification::centredBottom, 1); break; + case verticalKeyboardFacingLeft: g.drawFittedText (text, x + 2, y + 2, w - 4, h - 4, Justification::centredLeft, 1); break; + case verticalKeyboardFacingRight: g.drawFittedText (text, x + 2, y + 2, w - 4, h - 4, Justification::centredRight, 1); break; + default: break; + } + } + + g.setColour (lineColour); + + switch (orientation) + { + case horizontalKeyboard: g.fillRect (x, y, 1, h); break; + case verticalKeyboardFacingLeft: g.fillRect (x, y, w, 1); break; + case verticalKeyboardFacingRight: g.fillRect (x, y + h - 1, w, 1); break; + default: break; + } + + if (midiNoteNumber == rangeEnd) + { + switch (orientation) + { + case horizontalKeyboard: g.fillRect (x + w, y, 1, h); break; + case verticalKeyboardFacingLeft: g.fillRect (x, y + h, w, 1); break; + case verticalKeyboardFacingRight: g.fillRect (x, y - 1, w, 1); break; + default: break; + } + } +} + +void MidiKeyboardComponent::drawBlackNote (int /*midiNoteNumber*/, + Graphics& g, int x, int y, int w, int h, + bool isDown, bool isOver, + const Colour& noteFillColour) +{ + Colour c (noteFillColour); + + if (isDown) c = c.overlaidWith (findColour (keyDownOverlayColourId)); + if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId)); + + g.setColour (c); + g.fillRect (x, y, w, h); + + if (isDown) + { + g.setColour (noteFillColour); + g.drawRect (x, y, w, h); + } + else + { + g.setColour (c.brighter()); + const int xIndent = jmax (1, jmin (w, h) / 8); + + switch (orientation) + { + case horizontalKeyboard: g.fillRect (x + xIndent, y, w - xIndent * 2, 7 * h / 8); break; + case verticalKeyboardFacingLeft: g.fillRect (x + w / 8, y + xIndent, w - w / 8, h - xIndent * 2); break; + case verticalKeyboardFacingRight: g.fillRect (x, y + xIndent, 7 * w / 8, h - xIndent * 2); break; + default: break; + } + } +} + +void MidiKeyboardComponent::setOctaveForMiddleC (const int octaveNum) +{ + octaveNumForMiddleC = octaveNum; + repaint(); +} + +String MidiKeyboardComponent::getWhiteNoteText (const int midiNoteNumber) +{ + if (keyWidth > 11.0f && midiNoteNumber % 12 == 0) + return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, octaveNumForMiddleC); + + return String::empty; +} + +void MidiKeyboardComponent::drawUpDownButton (Graphics& g, int w, int h, + const bool mouseOver, + const bool buttonDown, + const bool movesOctavesUp) +{ + g.fillAll (findColour (upDownButtonBackgroundColourId)); + + float angle; + + switch (orientation) + { + case horizontalKeyboard: angle = movesOctavesUp ? 0.0f : 0.5f; break; + case verticalKeyboardFacingLeft: angle = movesOctavesUp ? 0.25f : 0.75f; break; + case verticalKeyboardFacingRight: angle = movesOctavesUp ? 0.75f : 0.25f; break; + default: jassertfalse; angle = 0; break; + } + + Path path; + path.addTriangle (0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f); + path.applyTransform (AffineTransform::rotation (float_Pi * 2.0f * angle, 0.5f, 0.5f)); + + g.setColour (findColour (upDownButtonArrowColourId) + .withAlpha (buttonDown ? 1.0f : (mouseOver ? 0.6f : 0.4f))); + + g.fillPath (path, path.getTransformToScaleToFit (1.0f, 1.0f, w - 2.0f, h - 2.0f, true)); +} + +void MidiKeyboardComponent::resized() +{ + int w = getWidth(); + int h = getHeight(); + + if (w > 0 && h > 0) + { + if (orientation != horizontalKeyboard) + std::swap (w, h); + + blackNoteLength = roundToInt (h * 0.7f); + + int kx2, kw2; + getKeyPos (rangeEnd, kx2, kw2); + + kx2 += kw2; + + if ((int) firstKey != rangeStart) + { + int kx1, kw1; + getKeyPos (rangeStart, kx1, kw1); + + if (kx2 - kx1 <= w) + { + firstKey = (float) rangeStart; + sendChangeMessage(); + repaint(); + } + } + + scrollDown->setVisible (canScroll && firstKey > (float) rangeStart); + + xOffset = 0; + + if (canScroll) + { + const int scrollButtonW = jmin (12, w / 2); + Rectangle r (getLocalBounds()); + + if (orientation == horizontalKeyboard) + { + scrollDown->setBounds (r.removeFromLeft (scrollButtonW)); + scrollUp ->setBounds (r.removeFromRight (scrollButtonW)); + } + else if (orientation == verticalKeyboardFacingLeft) + { + scrollDown->setBounds (r.removeFromTop (scrollButtonW)); + scrollUp ->setBounds (r.removeFromBottom (scrollButtonW)); + } + else + { + scrollDown->setBounds (r.removeFromBottom (scrollButtonW)); + scrollUp ->setBounds (r.removeFromTop (scrollButtonW)); + } + + int endOfLastKey, kw; + getKeyPos (rangeEnd, endOfLastKey, kw); + endOfLastKey += kw; + + float mousePositionVelocity; + const int spaceAvailable = w; + const int lastStartKey = remappedXYToNote (Point (endOfLastKey - spaceAvailable, 0), mousePositionVelocity) + 1; + + if (lastStartKey >= 0 && ((int) firstKey) > lastStartKey) + { + firstKey = (float) jlimit (rangeStart, rangeEnd, lastStartKey); + sendChangeMessage(); + } + + int newOffset = 0; + getKeyPos ((int) firstKey, newOffset, kw); + xOffset = newOffset; + } + else + { + firstKey = (float) rangeStart; + } + + getKeyPos (rangeEnd, kx2, kw2); + scrollUp->setVisible (canScroll && kx2 > w); + repaint(); + } +} + +//============================================================================== +void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/) +{ + shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here) +} + +void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/) +{ + shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here) +} + +//============================================================================== +void MidiKeyboardComponent::resetAnyKeysInUse() +{ + if (! keysPressed.isZero()) + { + for (int i = 128; --i >= 0;) + if (keysPressed[i]) + state.noteOff (midiChannel, i); + + keysPressed.clear(); + } + + for (int i = mouseDownNotes.size(); --i >= 0;) + { + const int noteDown = mouseDownNotes.getUnchecked(i); + + if (noteDown >= 0) + { + state.noteOff (midiChannel, noteDown); + mouseDownNotes.set (i, -1); + } + + mouseOverNotes.set (i, -1); + } +} + +void MidiKeyboardComponent::updateNoteUnderMouse (const MouseEvent& e, bool isDown) +{ + updateNoteUnderMouse (e.getPosition(), isDown, e.source.getIndex()); +} + +void MidiKeyboardComponent::updateNoteUnderMouse (Point pos, bool isDown, int fingerNum) +{ + float mousePositionVelocity = 0.0f; + const int newNote = xyToNote (pos, mousePositionVelocity); + const int oldNote = mouseOverNotes.getUnchecked (fingerNum); + + if (oldNote != newNote) + { + repaintNote (oldNote); + repaintNote (newNote); + mouseOverNotes.set (fingerNum, newNote); + } + + int oldNoteDown = mouseDownNotes.getUnchecked (fingerNum); + + if (isDown) + { + if (newNote != oldNoteDown) + { + if (oldNoteDown >= 0) + { + mouseDownNotes.set (fingerNum, -1); + + if (! mouseDownNotes.contains (oldNoteDown)) + state.noteOff (midiChannel, oldNoteDown); + } + + if (newNote >= 0) + { + if (! useMousePositionForVelocity) + mousePositionVelocity = 1.0f; + + state.noteOn (midiChannel, newNote, mousePositionVelocity * velocity); + mouseDownNotes.set (fingerNum, newNote); + } + } + } + else if (oldNoteDown >= 0) + { + mouseDownNotes.set (fingerNum, -1); + + if (! mouseDownNotes.contains (oldNoteDown)) + state.noteOff (midiChannel, oldNoteDown); + } +} + +void MidiKeyboardComponent::mouseMove (const MouseEvent& e) +{ + updateNoteUnderMouse (e, false); + shouldCheckMousePos = false; +} + +void MidiKeyboardComponent::mouseDrag (const MouseEvent& e) +{ + float mousePositionVelocity; + const int newNote = xyToNote (e.getPosition(), mousePositionVelocity); + + if (newNote >= 0) + mouseDraggedToKey (newNote, e); + + updateNoteUnderMouse (e, true); +} + +bool MidiKeyboardComponent::mouseDownOnKey (int, const MouseEvent&) { return true; } +void MidiKeyboardComponent::mouseDraggedToKey (int, const MouseEvent&) {} +void MidiKeyboardComponent::mouseUpOnKey (int, const MouseEvent&) {} + +void MidiKeyboardComponent::mouseDown (const MouseEvent& e) +{ + float mousePositionVelocity; + const int newNote = xyToNote (e.getPosition(), mousePositionVelocity); + + if (newNote >= 0 && mouseDownOnKey (newNote, e)) + { + updateNoteUnderMouse (e, true); + shouldCheckMousePos = true; + } +} + +void MidiKeyboardComponent::mouseUp (const MouseEvent& e) +{ + updateNoteUnderMouse (e, false); + shouldCheckMousePos = false; + + float mousePositionVelocity; + const int note = xyToNote (e.getPosition(), mousePositionVelocity); + if (note >= 0) + mouseUpOnKey (note, e); +} + +void MidiKeyboardComponent::mouseEnter (const MouseEvent& e) +{ + updateNoteUnderMouse (e, false); +} + +void MidiKeyboardComponent::mouseExit (const MouseEvent& e) +{ + updateNoteUnderMouse (e, false); +} + +void MidiKeyboardComponent::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) +{ + const float amount = (orientation == horizontalKeyboard && wheel.deltaX != 0) + ? wheel.deltaX : (orientation == verticalKeyboardFacingLeft ? wheel.deltaY + : -wheel.deltaY); + + setLowestVisibleKeyFloat (firstKey - amount * keyWidth); +} + +void MidiKeyboardComponent::timerCallback() +{ + if (shouldCheckState) + { + shouldCheckState = false; + + for (int i = rangeStart; i <= rangeEnd; ++i) + { + if (keysCurrentlyDrawnDown[i] != state.isNoteOnForChannels (midiInChannelMask, i)) + { + keysCurrentlyDrawnDown.setBit (i, state.isNoteOnForChannels (midiInChannelMask, i)); + repaintNote (i); + } + } + } + + if (shouldCheckMousePos) + { + const Array& mouseSources = Desktop::getInstance().getMouseSources(); + + for (MouseInputSource* mi = mouseSources.begin(), * const e = mouseSources.end(); mi != e; ++mi) + updateNoteUnderMouse (getLocalPoint (nullptr, mi->getScreenPosition()), mi->isDragging(), mi->getIndex()); + } +} + +//============================================================================== +void MidiKeyboardComponent::clearKeyMappings() +{ + resetAnyKeysInUse(); + keyPressNotes.clear(); + keyPresses.clear(); +} + +void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key, + const int midiNoteOffsetFromC) +{ + removeKeyPressForNote (midiNoteOffsetFromC); + + keyPressNotes.add (midiNoteOffsetFromC); + keyPresses.add (key); +} + +void MidiKeyboardComponent::removeKeyPressForNote (const int midiNoteOffsetFromC) +{ + for (int i = keyPressNotes.size(); --i >= 0;) + { + if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC) + { + keyPressNotes.remove (i); + keyPresses.remove (i); + } + } +} + +void MidiKeyboardComponent::setKeyPressBaseOctave (const int newOctaveNumber) +{ + jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10); + + keyMappingOctave = newOctaveNumber; +} + +bool MidiKeyboardComponent::keyStateChanged (const bool /*isKeyDown*/) +{ + bool keyPressUsed = false; + + for (int i = keyPresses.size(); --i >= 0;) + { + const int note = 12 * keyMappingOctave + keyPressNotes.getUnchecked (i); + + if (keyPresses.getReference(i).isCurrentlyDown()) + { + if (! keysPressed [note]) + { + keysPressed.setBit (note); + state.noteOn (midiChannel, note, velocity); + keyPressUsed = true; + } + } + else + { + if (keysPressed [note]) + { + keysPressed.clearBit (note); + state.noteOff (midiChannel, note); + keyPressUsed = true; + } + } + } + + return keyPressUsed; +} + +void MidiKeyboardComponent::focusLost (FocusChangeType) +{ + resetAnyKeysInUse(); +} diff --git a/source/modules/daz-plugins/juce-host/juce_MidiKeyboardComponent.h b/source/modules/daz-plugins/juce-host/juce_MidiKeyboardComponent.h new file mode 100644 index 000000000..c53890cfb --- /dev/null +++ b/source/modules/daz-plugins/juce-host/juce_MidiKeyboardComponent.h @@ -0,0 +1,409 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#ifndef JUCE_MIDIKEYBOARDCOMPONENT_H_INCLUDED +#define JUCE_MIDIKEYBOARDCOMPONENT_H_INCLUDED + + +//============================================================================== +/** + A component that displays a piano keyboard, whose notes can be clicked on. + + This component will mimic a physical midi keyboard, showing the current state of + a MidiKeyboardState object. When the on-screen keys are clicked on, it will play these + notes by calling the noteOn() and noteOff() methods of its MidiKeyboardState object. + + Another feature is that the computer keyboard can also be used to play notes. By + default it maps the top two rows of a standard querty keyboard to the notes, but + these can be remapped if needed. It will only respond to keypresses when it has + the keyboard focus, so to disable this feature you can call setWantsKeyboardFocus (false). + + The component is also a ChangeBroadcaster, so if you want to be informed when the + keyboard is scrolled, you can register a ChangeListener for callbacks. + + @see MidiKeyboardState +*/ +class JUCE_API MidiKeyboardComponent : public Component, + public MidiKeyboardStateListener, + public ChangeBroadcaster, + private Timer +{ +public: + //============================================================================== + /** The direction of the keyboard. + @see setOrientation + */ + enum Orientation + { + horizontalKeyboard, + verticalKeyboardFacingLeft, + verticalKeyboardFacingRight, + }; + + /** Creates a MidiKeyboardComponent. + + @param state the midi keyboard model that this component will represent + @param orientation whether the keyboard is horizonal or vertical + */ + MidiKeyboardComponent (MidiKeyboardState& state, + Orientation orientation); + + /** Destructor. */ + ~MidiKeyboardComponent(); + + //============================================================================== + /** Changes the velocity used in midi note-on messages that are triggered by clicking + on the component. + + Values are 0 to 1.0, where 1.0 is the heaviest. + + @see setMidiChannel + */ + void setVelocity (float velocity, bool useMousePositionForVelocity); + + /** Changes the midi channel number that will be used for events triggered by clicking + on the component. + + The channel must be between 1 and 16 (inclusive). This is the channel that will be + passed on to the MidiKeyboardState::noteOn() method when the user clicks the component. + + Although this is the channel used for outgoing events, the component can display + incoming events from more than one channel - see setMidiChannelsToDisplay() + + @see setVelocity + */ + void setMidiChannel (int midiChannelNumber); + + /** Returns the midi channel that the keyboard is using for midi messages. + @see setMidiChannel + */ + int getMidiChannel() const noexcept { return midiChannel; } + + /** Sets a mask to indicate which incoming midi channels should be represented by + key movements. + + The mask is a set of bits, where bit 0 = midi channel 1, bit 1 = midi channel 2, etc. + + If the MidiKeyboardState has a key down for any of the channels whose bits are set + in this mask, the on-screen keys will also go down. + + By default, this mask is set to 0xffff (all channels displayed). + + @see setMidiChannel + */ + void setMidiChannelsToDisplay (int midiChannelMask); + + /** Returns the current set of midi channels represented by the component. + This is the value that was set with setMidiChannelsToDisplay(). + */ + int getMidiChannelsToDisplay() const noexcept { return midiInChannelMask; } + + //============================================================================== + /** Changes the width used to draw the white keys. */ + void setKeyWidth (float widthInPixels); + + /** Returns the width that was set by setKeyWidth(). */ + float getKeyWidth() const noexcept { return keyWidth; } + + /** Changes the keyboard's current direction. */ + void setOrientation (Orientation newOrientation); + + /** Returns the keyboard's current direction. */ + Orientation getOrientation() const noexcept { return orientation; } + + /** Sets the range of midi notes that the keyboard will be limited to. + + By default the range is 0 to 127 (inclusive), but you can limit this if you + only want a restricted set of the keys to be shown. + + Note that the values here are inclusive and must be between 0 and 127. + */ + void setAvailableRange (int lowestNote, + int highestNote); + + /** Returns the first note in the available range. + @see setAvailableRange + */ + int getRangeStart() const noexcept { return rangeStart; } + + /** Returns the last note in the available range. + @see setAvailableRange + */ + int getRangeEnd() const noexcept { return rangeEnd; } + + /** If the keyboard extends beyond the size of the component, this will scroll + it to show the given key at the start. + + Whenever the keyboard's position is changed, this will use the ChangeBroadcaster + base class to send a callback to any ChangeListeners that have been registered. + */ + void setLowestVisibleKey (int noteNumber); + + /** Returns the number of the first key shown in the component. + @see setLowestVisibleKey + */ + int getLowestVisibleKey() const noexcept { return (int) firstKey; } + + /** Returns the length of the black notes. + This will be their vertical or horizontal length, depending on the keyboard's orientation. + */ + int getBlackNoteLength() const noexcept { return blackNoteLength; } + + /** If set to true, then scroll buttons will appear at either end of the keyboard + if there are too many notes to fit them all in the component at once. + */ + void setScrollButtonsVisible (bool canScroll); + + //============================================================================== + /** A set of colour IDs to use to change the colour of various aspects of the keyboard. + + These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() + methods. + + @see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour + */ + enum ColourIds + { + whiteNoteColourId = 0x1005000, + blackNoteColourId = 0x1005001, + keySeparatorLineColourId = 0x1005002, + mouseOverKeyOverlayColourId = 0x1005003, /**< This colour will be overlaid on the normal note colour. */ + keyDownOverlayColourId = 0x1005004, /**< This colour will be overlaid on the normal note colour. */ + textLabelColourId = 0x1005005, + upDownButtonBackgroundColourId = 0x1005006, + upDownButtonArrowColourId = 0x1005007, + shadowColourId = 0x1005008 + }; + + /** Returns the position within the component of the left-hand edge of a key. + + Depending on the keyboard's orientation, this may be a horizontal or vertical + distance, in either direction. + */ + int getKeyStartPosition (int midiNoteNumber) const; + + /** Returns the key at a given coordinate. */ + int getNoteAtPosition (Point position); + + //============================================================================== + /** Deletes all key-mappings. + @see setKeyPressForNote + */ + void clearKeyMappings(); + + /** Maps a key-press to a given note. + + @param key the key that should trigger the note + @param midiNoteOffsetFromC how many semitones above C the triggered note should + be. The actual midi note that gets played will be + this value + (12 * the current base octave). To change + the base octave, see setKeyPressBaseOctave() + */ + void setKeyPressForNote (const KeyPress& key, + int midiNoteOffsetFromC); + + /** Removes any key-mappings for a given note. + For a description of what the note number means, see setKeyPressForNote(). + */ + void removeKeyPressForNote (int midiNoteOffsetFromC); + + /** Changes the base note above which key-press-triggered notes are played. + + The set of key-mappings that trigger notes can be moved up and down to cover + the entire scale using this method. + + The value passed in is an octave number between 0 and 10 (inclusive), and + indicates which C is the base note to which the key-mapped notes are + relative. + */ + void setKeyPressBaseOctave (int newOctaveNumber); + + /** This sets the octave number which is shown as the octave number for middle C. + + This affects only the default implementation of getWhiteNoteText(), which + passes this octave number to MidiMessage::getMidiNoteName() in order to + get the note text. See MidiMessage::getMidiNoteName() for more info about + the parameter. + + By default this value is set to 3. + + @see getOctaveForMiddleC + */ + void setOctaveForMiddleC (int octaveNumForMiddleC); + + /** This returns the value set by setOctaveForMiddleC(). + @see setOctaveForMiddleC + */ + int getOctaveForMiddleC() const noexcept { return octaveNumForMiddleC; } + + //============================================================================== + /** @internal */ + void paint (Graphics&) override; + /** @internal */ + void resized() override; + /** @internal */ + void mouseMove (const MouseEvent&) override; + /** @internal */ + void mouseDrag (const MouseEvent&) override; + /** @internal */ + void mouseDown (const MouseEvent&) override; + /** @internal */ + void mouseUp (const MouseEvent&) override; + /** @internal */ + void mouseEnter (const MouseEvent&) override; + /** @internal */ + void mouseExit (const MouseEvent&) override; + /** @internal */ + void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override; + /** @internal */ + void timerCallback() override; + /** @internal */ + bool keyStateChanged (bool isKeyDown) override; + /** @internal */ + void focusLost (FocusChangeType) override; + /** @internal */ + void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override; + /** @internal */ + void handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber) override; + /** @internal */ + void colourChanged() override; + +protected: + //============================================================================== + /** Draws a white note in the given rectangle. + + isOver indicates whether the mouse is over the key, isDown indicates whether the key is + currently pressed down. + + When doing this, be sure to note the keyboard's orientation. + */ + virtual void drawWhiteNote (int midiNoteNumber, + Graphics& g, + int x, int y, int w, int h, + bool isDown, bool isOver, + const Colour& lineColour, + const Colour& textColour); + + /** Draws a black note in the given rectangle. + + isOver indicates whether the mouse is over the key, isDown indicates whether the key is + currently pressed down. + + When doing this, be sure to note the keyboard's orientation. + */ + virtual void drawBlackNote (int midiNoteNumber, + Graphics& g, + int x, int y, int w, int h, + bool isDown, bool isOver, + const Colour& noteFillColour); + + /** Allows text to be drawn on the white notes. + By default this is used to label the C in each octave, but could be used for other things. + @see setOctaveForMiddleC + */ + virtual String getWhiteNoteText (const int midiNoteNumber); + + /** Draws the up and down buttons that change the base note. */ + virtual void drawUpDownButton (Graphics& g, int w, int h, + const bool isMouseOver, + const bool isButtonPressed, + const bool movesOctavesUp); + + /** Callback when the mouse is clicked on a key. + + You could use this to do things like handle right-clicks on keys, etc. + + Return true if you want the click to trigger the note, or false if you + want to handle it yourself and not have the note played. + + @see mouseDraggedToKey + */ + virtual bool mouseDownOnKey (int midiNoteNumber, const MouseEvent& e); + + /** Callback when the mouse is dragged from one key onto another. + @see mouseDownOnKey + */ + virtual void mouseDraggedToKey (int midiNoteNumber, const MouseEvent& e); + + /** Callback when the mouse is released from a key. + @see mouseDownOnKey + */ + virtual void mouseUpOnKey (int midiNoteNumber, const MouseEvent& e); + + /** Calculates the positon of a given midi-note. + + This can be overridden to create layouts with custom key-widths. + + @param midiNoteNumber the note to find + @param keyWidth the desired width in pixels of one key - see setKeyWidth() + @param x the x position of the left-hand edge of the key (this method + always works in terms of a horizontal keyboard) + @param w the width of the key + */ + virtual void getKeyPosition (int midiNoteNumber, float keyWidth, + int& x, int& w) const; + +private: + //============================================================================== + friend class MidiKeyboardUpDownButton; + + MidiKeyboardState& state; + int xOffset, blackNoteLength; + float keyWidth; + Orientation orientation; + + int midiChannel, midiInChannelMask; + float velocity; + + Array mouseOverNotes, mouseDownNotes; + BigInteger keysPressed, keysCurrentlyDrawnDown; + bool shouldCheckState; + + int rangeStart, rangeEnd; + float firstKey; + bool canScroll, useMousePositionForVelocity, shouldCheckMousePos; + ScopedPointer