/* ============================================================================== 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. ============================================================================== */ /** This component runs in a slave process, draws the part of the canvas that this particular client covers, and updates itself when messages arrive from the master containing new canvas states. */ class SlaveCanvasComponent : public Component, private OSCSender, private OSCReceiver, private OSCReceiver::Listener, private AsyncUpdater, private Timer { public: SlaveCanvasComponent (PropertiesFile& p, int windowIndex) : properties (p) { { String uuidPropName ("UUID" + String (windowIndex)); clientName = properties.getValue (uuidPropName); if (clientName.isEmpty()) { clientName = "CLIENT_" + String (Random().nextInt (10000)).toUpperCase(); properties.setValue (uuidPropName, clientName); } } setOpaque (true); setSize (1500, 900); if (! OSCSender::connect (getBroadcastIPAddress(), clientPortNumber)) error = "Client app OSC sender: network connection error."; if (! OSCReceiver::connect (masterPortNumber)) error = "Client app OSC receiver: network connection error."; OSCReceiver::addListener (this); timerCallback(); startTimer (2000); } ~SlaveCanvasComponent() override { OSCReceiver::removeListener (this); } private: void mouseDrag (const MouseEvent& e) override { auto clientArea = getAreaInGlobalSpace(); if (! clientArea.isEmpty()) { OSCMessage message (userInputOSCAddress); message.addString (clientName); message.addFloat32 (e.position.x * clientArea.getWidth() / (float) getWidth() + clientArea.getX()); message.addFloat32 (e.position.y * clientArea.getHeight() / (float) getHeight() + clientArea.getY()); send (message); } } //============================================================================== void oscMessageReceived (const OSCMessage& message) override { auto address = message.getAddressPattern(); if (address.matches (canvasStateOSCAddress)) canvasStateOSCMessageReceived (message); } struct NewStateMessage : public Message { NewStateMessage (const MemoryBlock& d) : data (d) {} MemoryBlock data; }; void canvasStateOSCMessageReceived (const OSCMessage& message) { if (message.isEmpty() || ! message[0].isBlob()) return; if (packetiser.appendIncomingBlock (message[0].getBlob())) { const ScopedLock sl (canvasLock); MemoryBlock newCanvasData; if (packetiser.reassemble (newCanvasData)) { MemoryInputStream i (newCanvasData.getData(), newCanvasData.getSize(), false); canvas2.load (i); triggerAsyncUpdate(); } } } //============================================================================== String getMachineInfoToDisplay() const { auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (getScreenBounds().getCentre()); return getOSName() + " " + String (display->dpi) + " " + String (display->scale); } static String getOSName() { #if JUCE_MAC return "Mac OSX"; #elif JUCE_ANDROID return "Android"; #elif JUCE_IOS return "iOS"; #elif JUCE_WINDOWS return "Windows"; #elif JUCE_LINUX return "Linux"; #elif JUCE_BSD return "BSD"; #endif } void paint (Graphics& g) override { g.fillAll (canvas.backgroundColour); auto clientArea = getAreaInGlobalSpace(); if (clientArea.isEmpty()) { g.setColour (Colours::red.withAlpha (0.5f)); g.setFont (20.0f); g.drawText ("Not Connected", getLocalBounds(), Justification::centred, false); return; } canvas.draw (g, getLocalBounds().toFloat(), clientArea); g.setFont (Font (34.0f)); g.setColour (Colours::white.withAlpha (0.6f)); g.drawText (getMachineInfoToDisplay(), getLocalBounds().reduced (10).removeFromBottom (20), Justification::centredRight, true); if (error.isNotEmpty()) { g.setColour (Colours::red); g.drawText (error, getLocalBounds().reduced (10).removeFromBottom (80), Justification::centredRight, true); } } Rectangle getAreaInGlobalSpace() const { if (auto client = canvas.findClient (clientName)) { auto screenBounds = getScreenBounds(); auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (screenBounds.getCentre()); return ((screenBounds - display->userArea.getCentre()).toFloat() / (client->scaleFactor * display->dpi / display->scale)) + client->centre; } return {}; } Rectangle getScreenAreaInGlobalSpace() const { if (auto client = canvas.findClient (clientName)) { auto* display = Desktop::getInstance().getDisplays().getDisplayForPoint (getScreenBounds().getCentre()); return (display->userArea.toFloat() / (client->scaleFactor * display->dpi / display->scale)).withCentre (client->centre); } return {}; } void timerCallback() override { send (newClientOSCAddress, clientName + ":" + IPAddress::getLocalAddress().toString() + ":" + getScreenAreaInGlobalSpace().toString()); } void handleAsyncUpdate() override { const ScopedLock sl (canvasLock); canvas.swapWith (canvas2); repaint(); } SharedCanvasDescription canvas, canvas2; PropertiesFile& properties; String clientName, error; CriticalSection canvasLock; BlockPacketiser packetiser; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SlaveCanvasComponent) };