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