1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE examples.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    The code included in this file is provided under the terms of the ISC license
8    http://www.isc.org/downloads/software-support-policy/isc-license. Permission
9    To use, copy, modify, and/or distribute this software for any purpose with or
10    without fee is hereby granted provided that the above copyright notice and
11    this permission notice appear in all copies.
12 
13    THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
14    WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
15    PURPOSE, ARE DISCLAIMED.
16 
17   ==============================================================================
18 */
19 
20 /*******************************************************************************
21  The block below describes the properties of this PIP. A PIP is a short snippet
22  of code that can be read by the Projucer and used to generate a JUCE project.
23 
24  BEGIN_JUCE_PIP_METADATA
25 
26  name:             BlocksMonitorDemo
27  version:          1.0.0
28  vendor:           JUCE
29  website:          http://juce.com
30  description:      Application to monitor Blocks devices.
31 
32  dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats,
33                    juce_audio_processors, juce_audio_utils, juce_blocks_basics,
34                    juce_core, juce_data_structures, juce_events, juce_graphics,
35                    juce_gui_basics, juce_gui_extra
36  exporters:        xcode_mac, vs2019, linux_make, xcode_iphone
37 
38  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1
39 
40  type:             Component
41  mainClass:        BlocksMonitorDemo
42 
43  useLocalCopy:     1
44 
45  END_JUCE_PIP_METADATA
46 
47 *******************************************************************************/
48 
49 #pragma once
50 
51 
52 //==============================================================================
53 /**
54     Base class that renders a Block on the screen
55 */
56 class BlockComponent : public Component,
57                        public SettableTooltipClient,
58                        private TouchSurface::Listener,
59                        private ControlButton::Listener,
60                        private Timer
61 {
62 public:
BlockComponent(Block::Ptr blockToUse)63     BlockComponent (Block::Ptr blockToUse)
64         : block (blockToUse)
65     {
66         updateStatsAndTooltip();
67 
68         // Register BlockComponent as a listener to the touch surface
69         if (auto touchSurface = block->getTouchSurface())
70             touchSurface->addListener (this);
71 
72         // Register BlockComponent as a listener to any buttons
73         for (auto button : block->getButtons())
74             button->addListener (this);
75 
76         // If this is a Lightpad then set the grid program to be blank
77         if (block->getLEDGrid() != nullptr)
78             block->setProgram (std::make_unique<BitmapLEDProgram>(*block));
79 
80         // If this is a Lightpad then redraw it at 25Hz
81         if (block->getType() == Block::lightPadBlock)
82             startTimerHz (25);
83 
84         // Make sure the component can't go offscreen if it is draggable
85         constrainer.setMinimumOnscreenAmounts (50, 50, 50, 50);
86     }
87 
~BlockComponent()88     ~BlockComponent() override
89     {
90         // Remove any listeners
91         if (auto touchSurface = block->getTouchSurface())
92             touchSurface->removeListener (this);
93 
94         for (auto button : block->getButtons())
95             button->removeListener (this);
96     }
97 
98     /** Called periodically to update the tooltip with information about the Block */
updateStatsAndTooltip()99     void updateStatsAndTooltip()
100     {
101         // Get the battery level of this Block and inform any subclasses
102         auto batteryLevel = block->getBatteryLevel();
103         handleBatteryLevelUpdate (batteryLevel);
104 
105         // Update the tooltip
106         setTooltip ("Name = "          + block->getDeviceDescription() + "\n"
107                   + "UID = "           + String (block->uid) + "\n"
108                   + "Serial number = " + block->serialNumber + "\n"
109                   + "Battery level = " + String ((int) (batteryLevel * 100)) + "%"
110                   + (block->isBatteryCharging() ? "++"
111                                                 : "--"));
112     }
113 
114     /** Subclasses should override this to paint the Block object on the screen */
115     void paint (Graphics&) override = 0;
116 
117     /** Subclasses can override this to receive button down events from the Block */
handleButtonPressed(ControlButton::ButtonFunction,uint32)118     virtual void handleButtonPressed  (ControlButton::ButtonFunction, uint32) {}
119 
120     /** Subclasses can override this to receive button up events from the Block */
handleButtonReleased(ControlButton::ButtonFunction,uint32)121     virtual void handleButtonReleased (ControlButton::ButtonFunction, uint32) {}
122 
123     /** Subclasses can override this to receive touch events from the Block */
handleTouchChange(TouchSurface::Touch)124     virtual void handleTouchChange (TouchSurface::Touch) {}
125 
126     /** Subclasses can override this to battery level updates from the Block */
handleBatteryLevelUpdate(float)127     virtual void handleBatteryLevelUpdate (float) {}
128 
129     /** The Block object that this class represents */
130     Block::Ptr block;
131 
132     //==============================================================================
133     /** Returns an integer index corresponding to a physical position on the hardware
134         for each type of Control Block. */
controlButtonFunctionToIndex(ControlButton::ButtonFunction f)135     static int controlButtonFunctionToIndex (ControlButton::ButtonFunction f)
136     {
137         using CB = ControlButton;
138 
139         static Array<ControlButton::ButtonFunction> map[] =
140         {
141             { CB::mode,     CB::button0,  CB::velocitySensitivity },
142             { CB::volume,   CB::button1,  CB::glideSensitivity    },
143             { CB::scale,    CB::button2,  CB::slideSensitivity,  CB::click       },
144             { CB::chord,    CB::button3,  CB::pressSensitivity,  CB::snap        },
145             { CB::arp,      CB::button4,  CB::liftSensitivity,   CB::back        },
146             { CB::sustain,  CB::button5,  CB::fixedVelocity,     CB::playOrPause },
147             { CB::octave,   CB::button6,  CB::glideLock,         CB::record      },
148             { CB::love,     CB::button7,  CB::pianoMode,         CB::learn       },
149             { CB::up   },
150             { CB::down }
151         };
152 
153         for (int i = 0; i < numElementsInArray (map); ++i)
154             if (map[i].contains (f))
155                 return i;
156 
157         return -1;
158     }
159 
getOffsetForPort(Block::ConnectionPort port)160     Point<float> getOffsetForPort (Block::ConnectionPort port)
161     {
162         using e = Block::ConnectionPort::DeviceEdge;
163 
164         switch (rotation)
165         {
166             case 0:
167             {
168                 switch (port.edge)
169                 {
170                     case e::north:
171                         return { static_cast<float> (port.index), 0.0f };
172                     case e::east:
173                         return { static_cast<float> (block->getWidth()), static_cast<float> (port.index) };
174                     case e::south:
175                         return { static_cast<float> (port.index), static_cast<float> (block->getHeight()) };
176                     case e::west:
177                         return { 0.0f, static_cast<float> (port.index) };
178                     default:
179                         break;
180                 }
181 
182                 break;
183             }
184             case 90:
185             {
186                 switch (port.edge)
187                 {
188                     case e::north:
189                         return { 0.0f, static_cast<float> (port.index) };
190                     case e::east:
191                         return { static_cast<float> (-1.0f - (float) port.index), static_cast<float> (block->getWidth()) };
192                     case e::south:
193                         return { static_cast<float> (0.0f - (float) block->getHeight()), static_cast<float> (port.index) };
194                     case e::west:
195                         return { static_cast<float> (-1.0f - (float) port.index), 0.0f };
196                     default:
197                         break;
198                 }
199 
200                 break;
201             }
202             case 180:
203             {
204                 switch (port.edge)
205                 {
206                     case e::north:
207                         return { static_cast<float> (-1.0f - (float) port.index), 0.0f };
208                     case e::east:
209                         return { static_cast<float> (0.0f - (float) block->getWidth()), static_cast<float> (-1.0f - (float) port.index) };
210                     case e::south:
211                         return { static_cast<float> (-1.0f - (float) port.index), static_cast<float> (0.0f - (float) block->getHeight()) };
212                     case e::west:
213                         return { 0.0f, static_cast<float> (-1.0f - (float) port.index) };
214                     default:
215                         break;
216                 }
217 
218                 break;
219             }
220             case 270:
221             {
222                 switch (port.edge)
223                 {
224                     case e::north:
225                         return { 0.0f, static_cast<float> (-1.0f - (float) port.index) };
226                     case e::east:
227                         return { static_cast<float> (port.index), static_cast<float> (0 - (float) block->getWidth()) };
228                     case e::south:
229                         return { static_cast<float> (block->getHeight()), static_cast<float> (-1.0f - (float) port.index) };
230                     case e::west:
231                         return { static_cast<float> (port.index), 0.0f };
232                     default:
233                         break;
234                 }
235 
236                 break;
237             }
238 
239             default:
240                 break;
241         }
242 
243         return {};
244     }
245 
246     int rotation = 0;
247     Point<float> topLeft = { 0.0f, 0.0f };
248 
249 private:
250     /** Used to call repaint() periodically */
timerCallback()251     void timerCallback() override   { repaint(); }
252 
253     /** Overridden from TouchSurface::Listener */
touchChanged(TouchSurface &,const TouchSurface::Touch & t)254     void touchChanged (TouchSurface&, const TouchSurface::Touch& t) override { handleTouchChange (t); }
255 
256     /** Overridden from ControlButton::Listener */
buttonPressed(ControlButton & b,Block::Timestamp t)257     void buttonPressed  (ControlButton& b, Block::Timestamp t) override      { handleButtonPressed  (b.getType(), t); }
258 
259     /** Overridden from ControlButton::Listener */
buttonReleased(ControlButton & b,Block::Timestamp t)260     void buttonReleased (ControlButton& b, Block::Timestamp t) override      { handleButtonReleased (b.getType(), t); }
261 
262     /** Overridden from MouseListener. Prepares the master Block component for dragging. */
mouseDown(const MouseEvent & e)263     void mouseDown (const MouseEvent& e) override
264     {
265         if (block->isMasterBlock())
266             componentDragger.startDraggingComponent (this, e);
267     }
268 
269     /** Overridden from MouseListener. Drags the master Block component */
mouseDrag(const MouseEvent & e)270     void mouseDrag (const MouseEvent& e) override
271     {
272         if (block->isMasterBlock())
273         {
274             componentDragger.dragComponent (this, e, &constrainer);
275             getParentComponent()->resized();
276         }
277     }
278 
279     ComponentDragger componentDragger;
280     ComponentBoundsConstrainer constrainer;
281 
282     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlockComponent)
283 };
284 
285 //==============================================================================
286 /**
287     Class that renders a Lightpad on the screen
288 */
289 class LightpadComponent   : public BlockComponent
290 {
291 public:
LightpadComponent(Block::Ptr blockToUse)292     LightpadComponent (Block::Ptr blockToUse)
293         : BlockComponent (blockToUse)
294     {}
295 
paint(Graphics & g)296     void paint (Graphics& g) override
297     {
298         auto r = getLocalBounds().toFloat();
299 
300         // clip the drawing area to only draw in the block area
301         {
302             Path clipArea;
303             clipArea.addRoundedRectangle (r, r.getWidth() / 20.0f);
304 
305             g.reduceClipRegion (clipArea);
306         }
307 
308         // Fill a black square for the Lightpad
309         g.fillAll (Colours::black);
310 
311         // size ration between physical and on-screen blocks
312         Point<float> ratio (r.getWidth()  / (float) block->getWidth(),
313                             r.getHeight() / (float) block->getHeight());
314 
315         auto maxCircleSize = (float) block->getWidth() / 3.0f;
316 
317         // iterate over the list of current touches and draw them on the onscreen Block
318         for (auto touch : touches)
319         {
320             auto circleSize = touch.touch.z * maxCircleSize;
321 
322             Point<float> touchPosition (touch.touch.x,
323                                         touch.touch.y);
324 
325             auto blob = Rectangle<float> (circleSize, circleSize)
326                            .withCentre (touchPosition) * ratio;
327 
328             ColourGradient cg (colourArray[touch.touch.index],  blob.getCentreX(), blob.getCentreY(),
329                                Colours::transparentBlack,       blob.getRight(),   blob.getBottom(),
330                                true);
331 
332             g.setGradientFill (cg);
333             g.fillEllipse (blob);
334         }
335     }
336 
handleTouchChange(TouchSurface::Touch touch)337     void handleTouchChange (TouchSurface::Touch touch) override { touches.updateTouch (touch); }
338 
339 private:
340     /** An Array of colours to use for touches */
341     Array<Colour> colourArray = { Colours::red,
342                                   Colours::blue,
343                                   Colours::green,
344                                   Colours::yellow,
345                                   Colours::white,
346                                   Colours::hotpink,
347                                   Colours::mediumpurple };
348 
349     /** A list of current Touch events */
350     TouchList<TouchSurface::Touch> touches;
351 
352     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LightpadComponent)
353 };
354 
355 
356 //==============================================================================
357 /**
358     Class that renders a Control Block on the screen
359 */
360 class ControlBlockComponent   : public BlockComponent
361 {
362 public:
ControlBlockComponent(Block::Ptr blockToUse)363     ControlBlockComponent (Block::Ptr blockToUse)
364         : BlockComponent (blockToUse),
365           numLeds (block->getLEDRow()->getNumLEDs())
366     {
367         addAndMakeVisible (roundedRectangleButton);
368 
369         // Display the battery level on the LEDRow
370         auto numLedsToTurnOn = static_cast<int> ((float) numLeds * block->getBatteryLevel());
371 
372         // add LEDs
373         for (auto i = 0; i < numLeds; ++i)
374         {
375             auto ledComponent = new LEDComponent();
376             ledComponent->setOnState (i < numLedsToTurnOn);
377 
378             addAndMakeVisible (leds.add (ledComponent));
379         }
380 
381         previousNumLedsOn = numLedsToTurnOn;
382 
383         // add buttons
384         for (auto i = 0; i < 8; ++i)
385             addAndMakeVisible (circleButtons[i]);
386     }
387 
resized()388     void resized() override
389     {
390         auto r = getLocalBounds().reduced (10);
391 
392         auto rowHeight   = r.getHeight() / 5;
393         auto ledWidth    = (r.getWidth() - 70) / numLeds;
394         auto buttonWidth = (r.getWidth() - 40) / 5;
395 
396         auto row = r;
397 
398         auto ledRow     = row.removeFromTop (rowHeight)    .withSizeKeepingCentre (r.getWidth(), ledWidth);
399         auto buttonRow1 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);
400         auto buttonRow2 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);
401 
402         for (auto* led : leds)
403         {
404             led->setBounds (ledRow.removeFromLeft (ledWidth).reduced (2));
405             ledRow.removeFromLeft (5);
406         }
407 
408         for (auto i = 0; i < 5; ++i)
409         {
410             circleButtons[i].setBounds (buttonRow1.removeFromLeft (buttonWidth).reduced (2));
411             buttonRow1.removeFromLeft (10);
412         }
413 
414         for (auto i = 5; i < 8; ++i)
415         {
416             circleButtons[i].setBounds (buttonRow2.removeFromLeft (buttonWidth).reduced (2));
417             buttonRow2.removeFromLeft (10);
418         }
419 
420         roundedRectangleButton.setBounds (buttonRow2);
421     }
422 
paint(Graphics & g)423     void paint (Graphics& g) override
424     {
425         auto r = getLocalBounds().toFloat();
426 
427         // Fill a black rectangle for the Control Block
428         g.setColour (Colours::black);
429         g.fillRoundedRectangle (r, r.getWidth() / 20.0f);
430     }
431 
handleButtonPressed(ControlButton::ButtonFunction function,uint32)432     void handleButtonPressed  (ControlButton::ButtonFunction function, uint32) override
433     {
434         displayButtonInteraction (controlButtonFunctionToIndex (function), true);
435     }
436 
handleButtonReleased(ControlButton::ButtonFunction function,uint32)437     void handleButtonReleased (ControlButton::ButtonFunction function, uint32) override
438     {
439         displayButtonInteraction (controlButtonFunctionToIndex (function), false);
440     }
441 
handleBatteryLevelUpdate(float batteryLevel)442     void handleBatteryLevelUpdate (float batteryLevel) override
443     {
444         // Update the number of LEDs that are on to represent the battery level
445         auto numLedsOn = static_cast<int> ((float) numLeds * batteryLevel);
446 
447         if (numLedsOn != previousNumLedsOn)
448             for (auto i = 0; i < numLeds; ++i)
449                 leds.getUnchecked (i)->setOnState (i < numLedsOn);
450 
451         previousNumLedsOn = numLedsOn;
452         repaint();
453     }
454 
455 private:
456     //==============================================================================
457     /**
458         Base class that renders a Control Block button
459     */
460     struct ControlBlockSubComponent   : public Component,
461                                         public TooltipClient
462     {
ControlBlockSubComponentControlBlockSubComponent463         ControlBlockSubComponent (Colour componentColourToUse)
464             : componentColour (componentColourToUse)
465         {}
466 
467         /** Subclasses should override this to paint the button on the screen */
468         void paint (Graphics&) override = 0;
469 
470         /** Sets the colour of the button */
setColourControlBlockSubComponent471         void setColour (Colour c)   { componentColour = c; }
472 
473         /** Sets the on state of the button */
setOnStateControlBlockSubComponent474         void setOnState (bool isOn)
475         {
476             onState = isOn;
477             repaint();
478         }
479 
480         /** Returns the Control Block tooltip */
getTooltipControlBlockSubComponent481         String getTooltip() override
482         {
483             for (Component* comp = this; comp != nullptr; comp = comp->getParentComponent())
484                 if (auto* sttc = dynamic_cast<SettableTooltipClient*> (comp))
485                     return sttc->getTooltip();
486 
487             return {};
488         }
489 
490         //==============================================================================
491         Colour componentColour;
492         bool onState = false;
493 
494         //==============================================================================
495         JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockSubComponent)
496     };
497 
498     /**
499         Class that renders a Control Block LED on the screen
500     */
501     struct LEDComponent  : public ControlBlockSubComponent
502     {
LEDComponentLEDComponent503         LEDComponent() : ControlBlockSubComponent (Colours::green) {}
504 
paintLEDComponent505         void paint (Graphics& g) override
506         {
507             g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
508             g.fillEllipse (getLocalBounds().toFloat());
509         }
510     };
511 
512     /**
513         Class that renders a Control Block single circular button on the screen
514     */
515     struct CircleButtonComponent  : public ControlBlockSubComponent
516     {
CircleButtonComponentCircleButtonComponent517         CircleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}
518 
paintCircleButtonComponent519         void paint (Graphics& g) override
520         {
521             g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
522             g.fillEllipse (getLocalBounds().toFloat());
523         }
524     };
525 
526     /**
527         Class that renders a Control Block rounded rectangular button containing two buttons
528         on the screen
529     */
530     struct RoundedRectangleButtonComponent  : public ControlBlockSubComponent
531     {
RoundedRectangleButtonComponentRoundedRectangleButtonComponent532         RoundedRectangleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}
533 
paintRoundedRectangleButtonComponent534         void paint (Graphics& g) override
535         {
536             auto r = getLocalBounds().toFloat();
537 
538             g.setColour (componentColour.withAlpha (0.2f));
539             g.fillRoundedRectangle (r.toFloat(), 20.0f);
540             g.setColour (componentColour.withAlpha (1.0f));
541 
542             // is a button pressed?
543             if (doubleButtonOnState[0] || doubleButtonOnState[1])
544             {
545                 auto semiButtonWidth = r.getWidth() / 2.0f;
546 
547                 auto semiButtonBounds = r.withWidth (semiButtonWidth)
548                                          .withX (doubleButtonOnState[1] ? semiButtonWidth : 0)
549                                          .reduced (5.0f, 2.0f);
550 
551                 g.fillEllipse (semiButtonBounds);
552             }
553         }
554 
setPressedStateRoundedRectangleButtonComponent555         void setPressedState (bool isPressed, int button)
556         {
557             doubleButtonOnState[button] = isPressed;
558             repaint();
559         }
560 
561     private:
562         bool doubleButtonOnState[2] = { false, false };
563     };
564 
565     /** Displays a button press or release interaction for a button at a given index */
displayButtonInteraction(int buttonIndex,bool isPressed)566     void displayButtonInteraction (int buttonIndex, bool isPressed)
567     {
568         if (! isPositiveAndBelow (buttonIndex, 10))
569             return;
570 
571         if (buttonIndex >= 8)
572             roundedRectangleButton.setPressedState (isPressed, buttonIndex == 8);
573         else
574             circleButtons[buttonIndex].setOnState (isPressed);
575     }
576 
577     //==============================================================================
578     int numLeds;
579     OwnedArray<LEDComponent> leds;
580     CircleButtonComponent circleButtons[8];
581     RoundedRectangleButtonComponent roundedRectangleButton;
582     int previousNumLedsOn;
583 
584     //==============================================================================
585     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockComponent)
586 };
587 
588 //==============================================================================
589 /**
590     The main component where the Block components will be displayed
591 */
592 class BlocksMonitorDemo   : public Component,
593                             public TopologySource::Listener,
594                             private Timer
595 {
596 public:
BlocksMonitorDemo()597     BlocksMonitorDemo()
598     {
599         noBlocksLabel.setText ("No BLOCKS connected...", dontSendNotification);
600         noBlocksLabel.setJustificationType (Justification::centred);
601 
602         zoomOutButton.setButtonText ("+");
603         zoomOutButton.onClick = [this] { blockUnitInPixels = (int) ((float) blockUnitInPixels * 1.05f); resized(); };
604         zoomOutButton.setAlwaysOnTop (true);
605 
606         zoomInButton.setButtonText ("-");
607         zoomInButton.onClick = [this] { blockUnitInPixels = (int) ((float) blockUnitInPixels * 0.95f); resized(); };
608         zoomInButton.setAlwaysOnTop (true);
609 
610         // Register BlocksMonitorDemo as a listener to the PhysicalTopologySource object
611         topologySource.addListener (this);
612 
613         startTimer (10000);
614 
615         addAndMakeVisible (noBlocksLabel);
616         addAndMakeVisible (zoomOutButton);
617         addAndMakeVisible (zoomInButton);
618 
619        #if JUCE_IOS
620         connectButton.setButtonText ("Connect");
621         connectButton.onClick = [] { BluetoothMidiDevicePairingDialogue::open(); };
622         connectButton.setAlwaysOnTop (true);
623         addAndMakeVisible (connectButton);
624        #endif
625 
626         setSize (600, 600);
627 
628         topologyChanged();
629     }
630 
~BlocksMonitorDemo()631     ~BlocksMonitorDemo() override
632     {
633         topologySource.removeListener (this);
634     }
635 
paint(Graphics &)636     void paint (Graphics&) override {}
637 
resized()638     void resized() override
639     {
640        #if JUCE_IOS
641         connectButton.setBounds (getRight() - 100, 20, 80, 30);
642        #endif
643 
644         noBlocksLabel.setVisible (false);
645         auto numBlockComponents = blockComponents.size();
646 
647         // If there are no currently connected Blocks then display some text on the screen
648         if (masterBlockComponent == nullptr || numBlockComponents == 0)
649         {
650             noBlocksLabel.setVisible (true);
651             noBlocksLabel.setBounds (0, (getHeight() / 2) - 50, getWidth(), 100);
652             return;
653         }
654 
655         zoomOutButton.setBounds (10, getHeight() - 40, 40, 30);
656         zoomInButton.setBounds  (zoomOutButton.getRight(), zoomOutButton.getY(), 40, 30);
657 
658         if (isInitialResized)
659         {
660             // Work out the area needed in terms of Block units
661             Rectangle<float> maxArea;
662             for (auto blockComponent : blockComponents)
663             {
664                 auto topLeft = blockComponent->topLeft;
665                 auto rotation = blockComponent->rotation;
666                 auto blockSize = 0;
667 
668                 if (rotation == 180)
669                     blockSize = blockComponent->block->getWidth();
670                 else if (rotation == 90)
671                     blockSize = blockComponent->block->getHeight();
672 
673                 if (topLeft.x - (float) blockSize < maxArea.getX())
674                     maxArea.setX (topLeft.x - (float) blockSize);
675 
676                 blockSize = 0;
677                 if (rotation == 0)
678                     blockSize = blockComponent->block->getWidth();
679                 else if (rotation == 270)
680                     blockSize = blockComponent->block->getHeight();
681 
682                 if (topLeft.x + (float) blockSize > maxArea.getRight())
683                     maxArea.setWidth (topLeft.x + (float) blockSize);
684 
685                 blockSize = 0;
686                 if (rotation == 180)
687                     blockSize = blockComponent->block->getHeight();
688                 else if (rotation == 270)
689                     blockSize = blockComponent->block->getWidth();
690 
691                 if (topLeft.y - (float) blockSize < maxArea.getY())
692                     maxArea.setY (topLeft.y - (float) blockSize);
693 
694                 blockSize = 0;
695                 if (rotation == 0)
696                     blockSize = blockComponent->block->getHeight();
697                 else if (rotation == 90)
698                     blockSize = blockComponent->block->getWidth();
699 
700                 if (topLeft.y + (float) blockSize > maxArea.getBottom())
701                     maxArea.setHeight (topLeft.y + (float) blockSize);
702             }
703 
704             auto totalWidth  = std::abs (maxArea.getX()) + maxArea.getWidth();
705             auto totalHeight = std::abs (maxArea.getY()) + maxArea.getHeight();
706 
707             blockUnitInPixels = static_cast<int> (jmin (((float) getHeight() / totalHeight) - 50, ((float) getWidth() / totalWidth) - 50));
708 
709             masterBlockComponent->centreWithSize (masterBlockComponent->block->getWidth()  * blockUnitInPixels,
710                                                   masterBlockComponent->block->getHeight() * blockUnitInPixels);
711 
712             isInitialResized = false;
713         }
714         else
715         {
716             masterBlockComponent->setSize (masterBlockComponent->block->getWidth() * blockUnitInPixels, masterBlockComponent->block->getHeight() * blockUnitInPixels);
717         }
718 
719         for (auto blockComponent : blockComponents)
720         {
721             if (blockComponent == masterBlockComponent)
722                 continue;
723 
724             blockComponent->setBounds (masterBlockComponent->getX() + static_cast<int> (blockComponent->topLeft.x * (float) blockUnitInPixels),
725                                        masterBlockComponent->getY() + static_cast<int> (blockComponent->topLeft.y * (float) blockUnitInPixels),
726                                        blockComponent->block->getWidth()  * blockUnitInPixels,
727                                        blockComponent->block->getHeight() * blockUnitInPixels);
728 
729             if (blockComponent->rotation != 0)
730                 blockComponent->setTransform (AffineTransform::rotation (static_cast<float> (degreesToRadians (blockComponent->rotation)),
731                                                                          static_cast<float> (blockComponent->getX()),
732                                                                          static_cast<float> (blockComponent->getY())));
733         }
734     }
735 
736     /** Overridden from TopologySource::Listener, called when the topology changes */
topologyChanged()737     void topologyChanged() override
738     {
739         // Clear the array of Block components
740         blockComponents.clear();
741         masterBlockComponent = nullptr;
742 
743         // Get the current topology
744         auto topology = topologySource.getCurrentTopology();
745 
746         // Create a BlockComponent object for each Block object and store a pointer to the master
747         for (auto& block : topology.blocks)
748         {
749             if (auto* blockComponent = createBlockComponent (block))
750             {
751                 addAndMakeVisible (blockComponents.add (blockComponent));
752 
753                 if (blockComponent->block->isMasterBlock())
754                     masterBlockComponent = blockComponent;
755             }
756         }
757 
758         // Must have a master Block!
759         if (topology.blocks.size() > 0)
760             jassert (masterBlockComponent != nullptr);
761 
762         // Calculate the relative position and rotation for each Block
763         positionBlocks (topology);
764 
765         // Update the display
766         isInitialResized = true;
767         resized();
768     }
769 
770 private:
771     /** Creates a BlockComponent object for a new Block and adds it to the content component */
createBlockComponent(Block::Ptr newBlock)772     BlockComponent* createBlockComponent (Block::Ptr newBlock)
773     {
774         auto type = newBlock->getType();
775 
776         if (type == Block::lightPadBlock)
777             return new LightpadComponent (newBlock);
778 
779         if (type == Block::loopBlock || type == Block::liveBlock
780             || type == Block::touchBlock || type == Block::developerControlBlock)
781             return new ControlBlockComponent (newBlock);
782 
783         // Should only be connecting a Lightpad or Control Block!
784         jassertfalse;
785         return nullptr;
786     }
787 
788     /** Periodically updates the displayed BlockComponent tooltips */
timerCallback()789     void timerCallback() override
790     {
791         for (auto c : blockComponents)
792             c->updateStatsAndTooltip();
793     }
794 
795     /** Calculates the position and rotation of each connected Block relative to the master Block */
positionBlocks(BlockTopology topology)796     void positionBlocks (BlockTopology topology)
797     {
798         if (masterBlockComponent == nullptr)
799             return;
800 
801         Array<BlockComponent*> blocksConnectedToMaster;
802 
803         auto maxDelta = std::numeric_limits<float>::max();
804         auto maxLoops = 50;
805 
806         // Store all the connections to the master Block
807         Array<BlockDeviceConnection> masterBlockConnections;
808         for (auto connection : topology.connections)
809             if (connection.device1 == masterBlockComponent->block->uid || connection.device2 == masterBlockComponent->block->uid)
810                 masterBlockConnections.add (connection);
811 
812         // Position all the Blocks that are connected to the master Block
813         while (maxDelta > 0.001f && --maxLoops)
814         {
815             maxDelta = 0.0f;
816 
817             // Loop through each connection on the master Block
818             for (auto connection : masterBlockConnections)
819             {
820                 // Work out whether the master Block is device 1 or device 2 in the BlockDeviceConnection struct
821                 bool isDevice1 = true;
822                 if (masterBlockComponent->block->uid == connection.device2)
823                     isDevice1 = false;
824 
825                 // Get the connected ports
826                 auto masterPort = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
827                 auto otherPort  = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;
828 
829                 for (auto otherBlockComponent : blockComponents)
830                 {
831                     // Get the other block
832                     if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
833                     {
834                         blocksConnectedToMaster.addIfNotAlreadyThere (otherBlockComponent);
835 
836                         // Get the rotation of the other Block relative to the master Block
837                         otherBlockComponent->rotation = getRotation (masterPort.edge, otherPort.edge);
838 
839                         // Get the offsets for the connected ports
840                         auto masterBlockOffset = masterBlockComponent->getOffsetForPort (masterPort);
841                         auto otherBlockOffset  = otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort);
842 
843                         // Work out the distance between the two connected ports
844                         auto delta = masterBlockOffset - otherBlockOffset;
845 
846                         // Move the other block half the distance to the connection
847                         otherBlockComponent->topLeft += delta / 2.0f;
848 
849                         // Work out whether we are close enough for the loop to end
850                         maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
851                     }
852                 }
853             }
854         }
855 
856         // Check if there are any Blocks that have not been positioned yet
857         Array<BlockComponent*> unpositionedBlocks;
858 
859         for (auto blockComponent : blockComponents)
860             if (blockComponent != masterBlockComponent && ! blocksConnectedToMaster.contains (blockComponent))
861                 unpositionedBlocks.add (blockComponent);
862 
863         if (unpositionedBlocks.size() > 0)
864         {
865             // Reset the loop conditions
866             maxDelta = std::numeric_limits<float>::max();
867             maxLoops = 50;
868 
869             // Position all the remaining Blocks
870             while (maxDelta > 0.001f && --maxLoops)
871             {
872                 maxDelta = 0.0f;
873 
874                 // Loop through each unpositioned Block
875                 for (auto blockComponent : unpositionedBlocks)
876                 {
877                     // Store all the connections to this Block
878                     Array<BlockDeviceConnection> blockConnections;
879                     for (auto connection : topology.connections)
880                         if (connection.device1 == blockComponent->block->uid || connection.device2 == blockComponent->block->uid)
881                             blockConnections.add (connection);
882 
883                     // Loop through each connection on this Block
884                     for (auto connection : blockConnections)
885                     {
886                         // Work out whether this Block is device 1 or device 2 in the BlockDeviceConnection struct
887                         auto isDevice1 = true;
888                         if (blockComponent->block->uid == connection.device2)
889                             isDevice1 = false;
890 
891                         // Get the connected ports
892                         auto thisPort  = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
893                         auto otherPort = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;
894 
895                         // Get the other Block
896                         for (auto otherBlockComponent : blockComponents)
897                         {
898                             if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
899                             {
900                                 // Get the rotation
901                                 auto rotation = getRotation (otherPort.edge, thisPort.edge) + otherBlockComponent->rotation;
902                                 if (rotation > 360)
903                                     rotation -= 360;
904 
905                                 blockComponent->rotation = rotation;
906 
907                                 // Get the offsets for the connected ports
908                                 auto otherBlockOffset = (otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort));
909                                 auto thisBlockOffset  = (blockComponent->topLeft + blockComponent->getOffsetForPort (thisPort));
910 
911                                 // Work out the distance between the two connected ports
912                                 auto delta = otherBlockOffset - thisBlockOffset;
913 
914                                 // Move this block half the distance to the connection
915                                 blockComponent->topLeft += delta / 2.0f;
916 
917                                 // Work out whether we are close enough for the loop to end
918                                 maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
919                             }
920                         }
921                     }
922                 }
923             }
924         }
925     }
926 
927     /** Returns a rotation in degrees based on the connected edges of two blocks */
getRotation(Block::ConnectionPort::DeviceEdge staticEdge,Block::ConnectionPort::DeviceEdge rotatedEdge)928     int getRotation (Block::ConnectionPort::DeviceEdge staticEdge, Block::ConnectionPort::DeviceEdge rotatedEdge)
929     {
930         using edge = Block::ConnectionPort::DeviceEdge;
931 
932         switch (staticEdge)
933         {
934             case edge::north:
935             {
936                 switch (rotatedEdge)
937                 {
938                     case edge::north:
939                         return 180;
940                     case edge::south:
941                         return 0;
942                     case edge::east:
943                         return 90;
944                     case edge::west:
945                         return 270;
946                     default:
947                         break;
948                 }
949 
950                 break;
951             }
952             case edge::south:
953             {
954                 switch (rotatedEdge)
955                 {
956                     case edge::north:
957                         return 0;
958                     case edge::south:
959                         return 180;
960                     case edge::east:
961                         return 270;
962                     case edge::west:
963                         return 90;
964                     default:
965                         break;
966                 }
967 
968                 break;
969             }
970             case edge::east:
971             {
972                 switch (rotatedEdge)
973                 {
974                     case edge::north:
975                         return 270;
976                     case edge::south:
977                         return 90;
978                     case edge::east:
979                         return 180;
980                     case edge::west:
981                         return 0;
982                     default:
983                         break;
984                 }
985 
986                 break;
987             }
988 
989             case edge::west:
990             {
991                 switch (rotatedEdge)
992                 {
993                     case edge::north:
994                         return 90;
995                     case edge::south:
996                         return 270;
997                     case edge::east:
998                         return 0;
999                     case edge::west:
1000                         return 180;
1001                     default:
1002                         break;
1003                 }
1004 
1005                 break;
1006             }
1007 
1008             default:
1009                 break;
1010         }
1011 
1012         return 0;
1013     }
1014 
1015     //==============================================================================
1016     TooltipWindow tooltipWindow;
1017 
1018     PhysicalTopologySource topologySource;
1019     OwnedArray<BlockComponent> blockComponents;
1020     BlockComponent* masterBlockComponent = nullptr;
1021 
1022     Label noBlocksLabel;
1023 
1024     TextButton zoomOutButton;
1025     TextButton zoomInButton;
1026 
1027     int blockUnitInPixels;
1028     bool isInitialResized;
1029 
1030    #if JUCE_IOS
1031     TextButton connectButton;
1032    #endif
1033 
1034     //==============================================================================
1035     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlocksMonitorDemo)
1036 };
1037