/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2020 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 6 End-User License Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). End User License Agreement: www.juce.com/juce-6-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ /** Runs the master node, calls the demo to update the canvas, broadcasts those changes out to slaves, and shows a view of all the clients to allow them to be dragged around. */ struct MasterContentComponent : public Component, private Timer, private OSCSender, private OSCReceiver, private OSCReceiver::Listener { MasterContentComponent (PropertiesFile& props) : properties (props) { setWantsKeyboardFocus (true); createAllDemos (demos); setContent (0); setSize ((int) (15.0f * currentCanvas.getLimits().getWidth()), (int) (15.0f * currentCanvas.getLimits().getHeight())); if (! OSCSender::connect (getBroadcastIPAddress(), masterPortNumber)) error = "Master app OSC sender: network connection error."; if (! OSCReceiver::connect (clientPortNumber)) error = "Master app OSC receiver: network connection error."; OSCReceiver::addListener (this); startTimerHz (30); } ~MasterContentComponent() override { OSCReceiver::removeListener (this); } //============================================================================== struct Client { String name, ipAddress; float widthInches, heightInches; Point centre; // in inches float scaleFactor; }; Array clients; void addClient (String name, String ipAddress, String areaDescription) { auto area = Rectangle::fromString (areaDescription); if (auto c = getClient (name)) { c->ipAddress = ipAddress; c->widthInches = area.getWidth(); c->heightInches = area.getHeight(); return; } DBG (name + " " + ipAddress); removeClient (name); clients.add ({ name, ipAddress, area.getWidth(), area.getHeight(), {}, 1.0f }); String lastX = properties.getValue ("lastX_" + name); String lastY = properties.getValue ("lastY_" + name); String lastScale = properties.getValue ("scale_" + name); if (lastX.isEmpty() || lastY.isEmpty()) setClientCentre (name, { Random().nextFloat() * 10.0f, Random().nextFloat() * 10.0f }); else setClientCentre (name, Point (lastX.getFloatValue(), lastY.getFloatValue())); if (lastScale.isNotEmpty()) setClientScale (name, lastScale.getFloatValue()); else setClientScale (name, 1.0f); updateDeviceComponents(); } void removeClient (String name) { for (int i = clients.size(); --i >= 0;) if (clients.getReference (0).name == name) clients.remove (i); updateDeviceComponents(); } void setClientCentre (const String& name, Point newCentre) { if (auto c = getClient (name)) { newCentre = currentCanvas.getLimits().getConstrainedPoint (newCentre); c->centre = newCentre; properties.setValue ("lastX_" + name, String (newCentre.x)); properties.setValue ("lastY_" + name, String (newCentre.y)); startTimer (1); } } float getClientScale (const String& name) const { if (auto c = getClient (name)) return c->scaleFactor; return 1.0f; } void setClientScale (const String& name, float newScale) { if (auto c = getClient (name)) { c->scaleFactor = jlimit (0.5f, 2.0f, newScale); properties.setValue ("scale_" + name, String (newScale)); } } Point getClientCentre (const String& name) const { if (auto c = getClient (name)) return c->centre; return {}; } Rectangle getClientArea (const String& name) const { if (auto c = getClient (name)) return Rectangle (c->widthInches, c->heightInches) .withCentre (c->centre); return {}; } Rectangle getActiveCanvasArea() const { Rectangle r; if (clients.size() > 0) r = Rectangle (1.0f, 1.0f).withCentre (clients.getReference (0).centre); for (int i = 1; i < clients.size(); ++i) r = r.getUnion (Rectangle (1.0f, 1.0f).withCentre (clients.getReference (i).centre)); return r.expanded (6.0f); } int getContentIndex() const { return demos.indexOf (content); } void setContent (int demoIndex) { content = demos[demoIndex]; if (content != nullptr) content->reset(); } bool keyPressed (const KeyPress& key) override { if (key == KeyPress::spaceKey || key == KeyPress::rightKey || key == KeyPress::downKey) { setContent ((getContentIndex() + 1) % demos.size()); return true; } if (key == KeyPress::upKey || key == KeyPress::leftKey) { setContent ((getContentIndex() + demos.size() - 1) % demos.size()); return true; } return Component::keyPressed (key); } private: //============================================================================== void paint (Graphics& g) override { g.fillAll (Colours::black); currentCanvas.draw (g, getLocalBounds().toFloat(), currentCanvas.getLimits()); if (error.isNotEmpty()) { g.setColour (Colours::red); g.setFont (20.0f); g.drawText (error, getLocalBounds().reduced (10).removeFromBottom (80), Justification::centredRight, true); } if (content != nullptr) { g.setColour (Colours::white); g.setFont (17.0f); g.drawText ("Demo: " + content->getName(), getLocalBounds().reduced (10).removeFromTop (30), Justification::centredLeft, true); } } void resized() override { updateDeviceComponents(); } void updateDeviceComponents() { for (int i = devices.size(); --i >= 0;) if (getClient (devices.getUnchecked(i)->getName()) == nullptr) devices.remove (i); for (const auto& c : clients) if (getDeviceComponent (c.name) == nullptr) addAndMakeVisible (devices.add (new DeviceComponent (*this, c.name))); for (auto d : devices) d->setBounds (virtualSpaceToLocal (getClientArea (d->getName())).getSmallestIntegerContainer()); } Point virtualSpaceToLocal (Point p) const { auto total = currentCanvas.getLimits(); return { (float) getWidth() * (p.x - total.getX()) / total.getWidth(), (float) getHeight() * (p.y - total.getY()) / total.getHeight() }; } Rectangle virtualSpaceToLocal (Rectangle p) const { return { virtualSpaceToLocal (p.getTopLeft()), virtualSpaceToLocal (p.getBottomRight()) }; } Point localSpaceToVirtual (Point p) const { auto total = currentCanvas.getLimits(); return { total.getX() + total.getWidth() * (p.x / (float) getWidth()), total.getY() + total.getHeight() * (p.y / (float) getHeight()) }; } //============================================================================== struct DeviceComponent : public Component { DeviceComponent (MasterContentComponent& e, String name) : Component (name), editor (e) { setMouseCursor (MouseCursor::DraggingHandCursor); } void paint (Graphics& g) override { g.fillAll (Colours::blue.withAlpha (0.4f)); g.setColour (Colours::white); g.setFont (11.0f); g.drawFittedText (getName(), getLocalBounds(), Justification::centred, 2); } void mouseDown (const MouseEvent&) override { dragStartLocation = editor.getClientCentre (getName()); } void mouseDrag (const MouseEvent& e) override { editor.setClientCentre (getName(), dragStartLocation + editor.localSpaceToVirtual (e.getPosition().toFloat()) - editor.localSpaceToVirtual (e.getMouseDownPosition().toFloat())); } void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& e) override { editor.setClientScale (getName(), editor.getClientScale (getName()) + 0.1f * e.deltaY); } void mouseDoubleClick (const MouseEvent&) override { editor.setClientScale (getName(), 1.0f); } MasterContentComponent& editor; Point dragStartLocation; Rectangle clientArea; }; DeviceComponent* getDeviceComponent (const String& name) const { for (auto d : devices) if (d->getName() == name) return d; return nullptr; } //============================================================================== void broadcastNewCanvasState (const MemoryBlock& canvasData) { BlockPacketiser packetiser; packetiser.createBlocksFromData (canvasData, 1000); for (const auto& client : clients) for (auto& b : packetiser.blocks) sendToIPAddress (client.ipAddress, masterPortNumber, canvasStateOSCAddress, b); } void timerCallback() override { startTimerHz (30); currentCanvas.reset(); updateCanvasInfo (currentCanvas); { std::unique_ptr context (new CanvasGeneratingContext (currentCanvas)); Graphics g (*context); if (content != nullptr) content->generateCanvas (g, currentCanvas, getActiveCanvasArea()); } broadcastNewCanvasState (currentCanvas.toMemoryBlock()); updateDeviceComponents(); repaint(); } void updateCanvasInfo (SharedCanvasDescription& canvas) { canvas.backgroundColour = Colours::black; for (const auto& c : clients) canvas.clients.add ({ c.name, c.centre, c.scaleFactor }); } const Client* getClient (const String& name) const { for (auto& c : clients) if (c.name == name) return &c; return nullptr; } Client* getClient (const String& name) { return const_cast (static_cast (*this).getClient (name)); } //============================================================================== void oscMessageReceived (const OSCMessage& message) override { auto address = message.getAddressPattern(); if (address.matches (newClientOSCAddress)) newClientOSCMessageReceived (message); else if (address.matches (userInputOSCAddress)) userInputOSCMessageReceived (message); } void newClientOSCMessageReceived (const OSCMessage& message) { if (message.isEmpty() || ! message[0].isString()) return; StringArray tokens = StringArray::fromTokens (message[0].getString(), ":", ""); addClient (tokens[0], tokens[1], tokens[2]); } void userInputOSCMessageReceived (const OSCMessage& message) { if (message.size() == 3 && message[0].isString() && message[1].isFloat32() && message[2].isFloat32()) { content->handleTouch ({ message[1].getFloat32(), message[2].getFloat32() }); } } //============================================================================== AnimatedContent* content = nullptr; PropertiesFile& properties; OwnedArray devices; SharedCanvasDescription currentCanvas; String error; OwnedArray demos; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MasterContentComponent) };