The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

745 lines
25KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. #pragma once
  20. class LiveBuildCodeEditorDocument;
  21. //==============================================================================
  22. class LiveBuildCodeEditor : public CppCodeEditorComponent,
  23. private Timer
  24. {
  25. public:
  26. LiveBuildCodeEditor (LiveBuildCodeEditorDocument& edDoc, CodeDocument& doc)
  27. : CppCodeEditorComponent (edDoc.getFile(), doc),
  28. editorDoc (edDoc),
  29. classList (*this, edDoc)
  30. {
  31. }
  32. ~LiveBuildCodeEditor() override
  33. {
  34. for (int i = getNumChildComponents(); --i >= 0;)
  35. if (auto* c = dynamic_cast<DiagnosticOverlayComponent*> (getChildComponent (i)))
  36. delete c;
  37. }
  38. CompileEngineChildProcess::Ptr getChildProcess() const
  39. {
  40. return editorDoc.getChildProcess();
  41. }
  42. Component* addDiagnosticOverlay (CodeDocument::Position start, CodeDocument::Position end,
  43. DiagnosticMessage::Type diagType)
  44. {
  45. auto* d = new DiagnosticOverlayComponent (*this, start, end, diagType);
  46. addAndMakeVisible (d);
  47. return d;
  48. }
  49. private:
  50. LiveBuildCodeEditorDocument& editorDoc;
  51. //==============================================================================
  52. struct OverlayComponent : public Component,
  53. private GenericCodeEditorComponent::Listener,
  54. private CodeDocument::Listener
  55. {
  56. OverlayComponent (CodeDocument::Position start,
  57. CodeDocument::Position end)
  58. : startPosition (start),
  59. endPosition (end)
  60. {
  61. startPosition.setPositionMaintained (true);
  62. endPosition.setPositionMaintained (true);
  63. }
  64. ~OverlayComponent() override
  65. {
  66. setEditor (nullptr);
  67. }
  68. void setEditor (GenericCodeEditorComponent* editor)
  69. {
  70. if (editor != codeEditor)
  71. {
  72. if (codeEditor != nullptr)
  73. {
  74. codeEditor->removeListener (this);
  75. codeEditor->getDocument().removeListener (this);
  76. codeEditor->removeChildComponent (this);
  77. }
  78. codeEditor = editor;
  79. if (codeEditor != nullptr)
  80. {
  81. codeEditor->addListener (this);
  82. codeEditor->getDocument().addListener (this);
  83. codeEditor->addAndMakeVisible (this);
  84. }
  85. if (editor != nullptr)
  86. updatePosition();
  87. }
  88. }
  89. void codeEditorViewportMoved (CodeEditorComponent& editor) override
  90. {
  91. setEditor (dynamic_cast<GenericCodeEditorComponent*> (&editor));
  92. updatePosition();
  93. }
  94. void codeDocumentTextInserted (const String&, int) override { updatePosition(); }
  95. void codeDocumentTextDeleted (int, int) override { updatePosition(); }
  96. void parentSizeChanged() override
  97. {
  98. updatePosition();
  99. }
  100. virtual void updatePosition() = 0;
  101. Component::SafePointer<GenericCodeEditorComponent> codeEditor;
  102. CodeDocument::Position startPosition, endPosition;
  103. };
  104. //==============================================================================
  105. struct LaunchClassOverlayComponent : public OverlayComponent
  106. {
  107. LaunchClassOverlayComponent (GenericCodeEditorComponent& editor,
  108. CodeDocument::Position start, CodeDocument::Position end,
  109. const String className)
  110. : OverlayComponent (start, end),
  111. launchButton (className.fromLastOccurrenceOf ("::", false, false)),
  112. name (className)
  113. {
  114. setAlwaysOnTop (true);
  115. setEditor (&editor);
  116. addAndMakeVisible (launchButton);
  117. }
  118. void updatePosition() override
  119. {
  120. if (codeEditor != nullptr)
  121. {
  122. jassert (isVisible());
  123. const auto charArea = codeEditor->getCharacterBounds (startPosition);
  124. const int height = charArea.getHeight() + 8;
  125. Font f (height * 0.7f);
  126. const int width = jmin (height * 2 + f.getStringWidth (launchButton.getName()),
  127. jmax (120, codeEditor->proportionOfWidth (0.2f)));
  128. setBounds (codeEditor->getWidth() - width - 10, charArea.getY() - 4,
  129. width, height);
  130. }
  131. }
  132. void resized() override
  133. {
  134. launchButton.setBounds (getLocalBounds());
  135. }
  136. struct LaunchButton : public Button
  137. {
  138. LaunchButton (const String& nm) : Button (nm)
  139. {
  140. setMouseCursor (MouseCursor::PointingHandCursor);
  141. }
  142. void paintButton (Graphics& g, bool isMouseOverButton, bool isButtonDown) override
  143. {
  144. Colour background (findColour (CodeEditorComponent::backgroundColourId)
  145. .contrasting()
  146. .overlaidWith (Colours::yellow.withAlpha (0.5f))
  147. .withAlpha (0.4f));
  148. g.setColour (background);
  149. g.fillRoundedRectangle (getLocalBounds().toFloat(), 3.0f);
  150. const Path& path = getIcons().play;
  151. Colour col (background.contrasting (Colours::lightgreen, 0.6f));
  152. Rectangle<int> r (getLocalBounds().reduced (getHeight() / 5));
  153. Icon (path, col.withAlpha (isButtonDown ? 1.0f : (isMouseOverButton ? 0.8f : 0.5f)))
  154. .draw (g, r.removeFromLeft (getHeight()).toFloat(), false);
  155. g.setColour (Colours::white);
  156. g.setFont (getHeight() * 0.7f);
  157. g.drawFittedText (getName(), r, Justification::centredLeft, 1);
  158. }
  159. void clicked() override
  160. {
  161. if (auto* l = findParentComponentOfClass<LaunchClassOverlayComponent>())
  162. l->launch();
  163. }
  164. using Button::clicked;
  165. };
  166. void launch()
  167. {
  168. if (auto* e = findParentComponentOfClass<LiveBuildCodeEditor>())
  169. e->launch (name);
  170. }
  171. private:
  172. LaunchButton launchButton;
  173. String name;
  174. };
  175. struct ComponentClassList : private Timer
  176. {
  177. ComponentClassList (GenericCodeEditorComponent& e, LiveBuildCodeEditorDocument& edDoc)
  178. : owner (e),
  179. childProcess (edDoc.getChildProcess()),
  180. file (edDoc.getFile())
  181. {
  182. startTimer (600);
  183. }
  184. ~ComponentClassList() override
  185. {
  186. deleteOverlays();
  187. }
  188. void timerCallback() override
  189. {
  190. Array<WeakReference<ClassDatabase::Class>> newClasses;
  191. if (childProcess != nullptr)
  192. const_cast <ClassDatabase::ClassList&> (childProcess->getComponentList()).globalNamespace.findClassesDeclaredInFile (newClasses, file);
  193. for (int i = newClasses.size(); --i >= 0;)
  194. {
  195. auto& c = newClasses.getReference (i);
  196. if (c == nullptr || ! c->getInstantiationFlags().canBeInstantiated())
  197. newClasses.remove (i);
  198. }
  199. if (newClasses != classes)
  200. {
  201. classes = newClasses;
  202. deleteOverlays();
  203. for (auto c : classes)
  204. {
  205. if (c != nullptr)
  206. {
  207. CodeDocument::Position pos (owner.getDocument(), c->getClassDeclarationRange().range.getStart());
  208. overlays.add (new LaunchClassOverlayComponent (owner, pos, pos, c->getName()));
  209. }
  210. }
  211. }
  212. }
  213. void deleteOverlays()
  214. {
  215. for (auto& o : overlays)
  216. o.deleteAndZero();
  217. overlays.clear();
  218. }
  219. GenericCodeEditorComponent& owner;
  220. CompileEngineChildProcess::Ptr childProcess;
  221. File file;
  222. Array<WeakReference<ClassDatabase::Class>> classes;
  223. Array<Component::SafePointer<Component>> overlays;
  224. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentClassList)
  225. };
  226. ComponentClassList classList;
  227. //==============================================================================
  228. struct DiagnosticOverlayComponent : public OverlayComponent
  229. {
  230. DiagnosticOverlayComponent (GenericCodeEditorComponent& editor,
  231. CodeDocument::Position start, CodeDocument::Position end,
  232. DiagnosticMessage::Type diagType)
  233. : OverlayComponent (start, end), diagnosticType (diagType)
  234. {
  235. setInterceptsMouseClicks (false, false);
  236. setEditor (&editor);
  237. }
  238. void updatePosition() override
  239. {
  240. if (codeEditor != nullptr)
  241. {
  242. jassert (isVisible());
  243. const auto charStartRect = codeEditor->getCharacterBounds (startPosition);
  244. const auto charEndRect = codeEditor->getCharacterBounds (endPosition);
  245. auto charHeight = charStartRect.getHeight();
  246. const auto editorBounds = codeEditor->getBounds();
  247. arrowXMin = static_cast<int> (jmin (charStartRect.getX(), charEndRect.getX()));
  248. arrowXMax = static_cast<int> (jmax (charStartRect.getX() + charStartRect.getWidth(), charEndRect.getX() + charEndRect.getWidth()));
  249. lineYMin = charStartRect.getY();
  250. lineOffset = charHeight;
  251. setBounds (0, lineYMin, editorBounds.getWidth(), lineOffset + charHeight);
  252. repaint();
  253. }
  254. }
  255. void paint (Graphics& g) override
  256. {
  257. const auto diagColour = diagnosticType == DiagnosticMessage::Type::error ? Colours::red
  258. : Colour (200, 200, 64);
  259. g.setColour (diagColour.withAlpha (0.2f));
  260. g.fillRect (getLocalBounds().withTrimmedBottom (lineOffset));
  261. Path path;
  262. const float bottomY = getHeight() - (lineOffset / 2.0f);
  263. path.addTriangle ((float) arrowXMin, bottomY,
  264. (arrowXMax + arrowXMin) / 2.0f, (float) lineOffset,
  265. (float) arrowXMax, bottomY);
  266. g.setColour (diagColour.withAlpha (0.8f));
  267. g.fillPath (path);
  268. }
  269. private:
  270. int arrowXMin, arrowXMax;
  271. int lineYMin, lineOffset;
  272. const DiagnosticMessage::Type diagnosticType;
  273. };
  274. //==============================================================================
  275. void timerCallback() override
  276. {
  277. if (isMouseButtonDownAnywhere())
  278. return;
  279. MouseInputSource mouse = Desktop::getInstance().getMainMouseSource();
  280. Component* underMouse = mouse.getComponentUnderMouse();
  281. if (underMouse != nullptr
  282. && (dynamic_cast<ControlsComponent*> (underMouse) != nullptr
  283. || underMouse->findParentComponentOfClass<ControlsComponent>() != nullptr))
  284. return;
  285. overlay.reset();
  286. if (hasKeyboardFocus (true) && underMouse != nullptr
  287. && (underMouse == this || underMouse->isParentOf (this)))
  288. {
  289. Point<int> mousePos = getLocalPoint (nullptr, mouse.getScreenPosition()).toInt();
  290. CodeDocument::Position start, end;
  291. getDocument().findTokenContaining (getPositionAt (mousePos.x, mousePos.y), start, end);
  292. if (end.getPosition() > start.getPosition())
  293. {
  294. Range<int> selection = optimiseSelection ({ start.getPosition(), end.getPosition() });
  295. String text = getTextInRange (selection).toLowerCase();
  296. if (isIntegerLiteral (text) || isFloatLiteral (text))
  297. overlay.reset (new LiteralHighlightOverlay (*this, selection, mightBeColourValue (text)));
  298. }
  299. }
  300. startTimerHz (10);
  301. }
  302. void hideOverlay()
  303. {
  304. stopTimer();
  305. overlay.reset();
  306. }
  307. void focusLost (FocusChangeType) override
  308. {
  309. if (CompileEngineChildProcess::Ptr childProcess = getChildProcess())
  310. childProcess->flushEditorChanges();
  311. }
  312. void mouseMove (const MouseEvent& e) override
  313. {
  314. if (overlay == nullptr)
  315. startTimer (100);
  316. CppCodeEditorComponent::mouseMove (e);
  317. }
  318. void mouseDrag (const MouseEvent& e) override
  319. {
  320. if (e.getDistanceFromDragStart() > 0)
  321. hideOverlay();
  322. CppCodeEditorComponent::mouseDrag (e);
  323. }
  324. void mouseDown (const MouseEvent& e) override
  325. {
  326. CppCodeEditorComponent::mouseDown (e);
  327. }
  328. void mouseUp (const MouseEvent& e) override
  329. {
  330. CppCodeEditorComponent::mouseUp (e);
  331. }
  332. bool keyPressed (const KeyPress& key) override
  333. {
  334. hideOverlay();
  335. return CppCodeEditorComponent::keyPressed (key);
  336. }
  337. static bool isIntegerLiteral (const String& text) { return CppParserHelpers::parseSingleToken (text) == CPlusPlusCodeTokeniser::tokenType_integer; }
  338. static bool isFloatLiteral (const String& text) { return CppParserHelpers::parseSingleToken (text) == CPlusPlusCodeTokeniser::tokenType_float; }
  339. static bool mightBeColourValue (const String& text)
  340. {
  341. return isIntegerLiteral (text)
  342. && text.trim().startsWith ("0x")
  343. && text.trim().length() > 7;
  344. }
  345. Range<int> optimiseSelection (Range<int> selection)
  346. {
  347. String text (getTextInRange (selection));
  348. if (CharacterFunctions::isDigit (text[0]) || text[0] == '.')
  349. if (getTextInRange (Range<int> (selection.getStart() - 1, selection.getStart())) == "-")
  350. selection.setStart (selection.getStart() - 1);
  351. selection.setStart (selection.getStart() + (text.length() - text.trimStart().length()));
  352. selection.setEnd (selection.getEnd() - (text.length() - text.trimEnd().length()));
  353. return selection;
  354. }
  355. void launch (const String& name)
  356. {
  357. if (CompileEngineChildProcess::Ptr p = getChildProcess())
  358. if (auto* cls = p->getComponentList().globalNamespace.findClass (name))
  359. p->openPreview (*cls);
  360. }
  361. //==============================================================================
  362. class ControlsComponent : public Component,
  363. private ChangeListener
  364. {
  365. public:
  366. ControlsComponent (CodeDocument& doc, const Range<int>& selection,
  367. CompileEngineChildProcess::Ptr cp, bool showColourSelector)
  368. : document (doc),
  369. start (doc, selection.getStart()),
  370. end (doc, selection.getEnd()),
  371. childProcess (cp)
  372. {
  373. slider.setTextBoxStyle (Slider::NoTextBox, true, 0, 0);
  374. slider.setWantsKeyboardFocus (false);
  375. slider.setMouseClickGrabsKeyboardFocus (false);
  376. setWantsKeyboardFocus (false);
  377. setMouseClickGrabsKeyboardFocus (false);
  378. addAndMakeVisible (&slider);
  379. updateRange();
  380. slider.onValueChange = [this] { updateSliderValue(); };
  381. slider.onDragEnd = [this] { updateRange(); };
  382. if (showColourSelector)
  383. {
  384. updateColourSelector();
  385. selector.setWantsKeyboardFocus (false);
  386. selector.setMouseClickGrabsKeyboardFocus (false);
  387. addAndMakeVisible (&selector);
  388. setSize (400, sliderHeight + 400);
  389. selector.addChangeListener (this);
  390. }
  391. else
  392. {
  393. setSize (400, sliderHeight);
  394. }
  395. end.setPositionMaintained (true);
  396. }
  397. void updateRange()
  398. {
  399. double v = getValue();
  400. if (isFloat())
  401. slider.setRange (v - 10, v + 10);
  402. else
  403. slider.setRange (v - 100, v + 100);
  404. slider.setValue (v, dontSendNotification);
  405. }
  406. private:
  407. Slider slider;
  408. ColourSelector selector;
  409. CodeDocument& document;
  410. CodeDocument::Position start, end;
  411. CompileEngineChildProcess::Ptr childProcess;
  412. static const int sliderHeight = 26;
  413. void paint (Graphics& g) override
  414. {
  415. g.setColour (LiteralHighlightOverlay::getBackgroundColour());
  416. g.fillRoundedRectangle (getLocalBounds().toFloat(), 8.0f);
  417. }
  418. void updateSliderValue()
  419. {
  420. const String oldText (document.getTextBetween (start, end));
  421. const String newText (CppParserHelpers::getReplacementStringInSameFormat (oldText, slider.getValue()));
  422. if (oldText != newText)
  423. document.replaceSection (start.getPosition(), end.getPosition(), newText);
  424. if (childProcess != nullptr)
  425. childProcess->flushEditorChanges();
  426. updateColourSelector();
  427. }
  428. void changeListenerCallback (ChangeBroadcaster*) override
  429. {
  430. setNewColour (selector.getCurrentColour());
  431. }
  432. void updateColourSelector()
  433. {
  434. selector.setCurrentColour (getCurrentColour());
  435. }
  436. Colour getCurrentColour() const
  437. {
  438. int64 val;
  439. if (CppParserHelpers::parseInt (document.getTextBetween (start, end), val))
  440. return Colour ((uint32) val);
  441. return Colours::white;
  442. }
  443. void setNewColour (const Colour& c)
  444. {
  445. const String oldText (document.getTextBetween (start, end));
  446. const String newText (CppParserHelpers::getReplacementStringInSameFormat (oldText, (int64) c.getARGB()));
  447. if (oldText != newText)
  448. document.replaceSection (start.getPosition(), end.getPosition(), newText);
  449. if (childProcess != nullptr)
  450. childProcess->flushEditorChanges();
  451. }
  452. void resized() override
  453. {
  454. Rectangle<int> r (getLocalBounds());
  455. slider.setBounds (r.removeFromTop (sliderHeight));
  456. r.removeFromTop (10);
  457. if (selector.isVisible())
  458. selector.setBounds (r);
  459. }
  460. double getValue() const
  461. {
  462. const String text (document.getTextBetween (start, end));
  463. if (text.containsChar ('.'))
  464. {
  465. double f;
  466. if (CppParserHelpers::parseFloat (text, f))
  467. return f;
  468. }
  469. else
  470. {
  471. int64 val;
  472. if (CppParserHelpers::parseInt (text, val))
  473. return (double) val;
  474. }
  475. jassertfalse;
  476. return 0;
  477. }
  478. bool isFloat() const
  479. {
  480. return document.getTextBetween (start, end).containsChar ('.');
  481. }
  482. };
  483. //==============================================================================
  484. struct LiteralHighlightOverlay : public Component,
  485. private CodeDocument::Listener
  486. {
  487. LiteralHighlightOverlay (LiveBuildCodeEditor& e, Range<int> section, bool showColourSelector)
  488. : owner (e),
  489. start (e.getDocument(), section.getStart()),
  490. end (e.getDocument(), section.getEnd()),
  491. controls (e.getDocument(), section, e.getChildProcess(), showColourSelector)
  492. {
  493. if (e.hasKeyboardFocus (true))
  494. previouslyFocused = Component::getCurrentlyFocusedComponent();
  495. start.setPositionMaintained (true);
  496. end.setPositionMaintained (true);
  497. setInterceptsMouseClicks (false, false);
  498. if (Component* parent = owner.findParentComponentOfClass<ProjectContentComponent>())
  499. parent->addAndMakeVisible (controls);
  500. else
  501. jassertfalse;
  502. owner.addAndMakeVisible (this);
  503. toBack();
  504. updatePosition();
  505. owner.getDocument().addListener (this);
  506. }
  507. ~LiteralHighlightOverlay() override
  508. {
  509. if (auto* p = getParentComponent())
  510. {
  511. p->removeChildComponent (this);
  512. if (previouslyFocused != nullptr && ! previouslyFocused->hasKeyboardFocus (true))
  513. previouslyFocused->grabKeyboardFocus();
  514. }
  515. owner.getDocument().removeListener (this);
  516. }
  517. void paint (Graphics& g) override
  518. {
  519. g.setColour (getBackgroundColour());
  520. Rectangle<int> r (getLocalBounds());
  521. g.fillRect (r.removeFromTop (borderSize));
  522. g.fillRect (r.removeFromLeft (borderSize));
  523. g.fillRect (r.removeFromRight (borderSize));
  524. }
  525. void updatePosition()
  526. {
  527. Rectangle<int> area = owner.getCharacterBounds (start)
  528. .getUnion (owner.getCharacterBounds (end.movedBy (-1)))
  529. .expanded (borderSize)
  530. .withTrimmedBottom (borderSize);
  531. setBounds (getParentComponent()->getLocalArea (&owner, area));
  532. area.setPosition (area.getX() - controls.getWidth() / 2, area.getBottom());
  533. area.setSize (controls.getWidth(), controls.getHeight());
  534. controls.setBounds (controls.getParentComponent()->getLocalArea (&owner, area));
  535. }
  536. void codeDocumentTextInserted (const String&, int) override { updatePosition(); }
  537. void codeDocumentTextDeleted (int, int) override { updatePosition(); }
  538. LiveBuildCodeEditor& owner;
  539. CodeDocument::Position start, end;
  540. ControlsComponent controls;
  541. Component::SafePointer<Component> previouslyFocused;
  542. static const int borderSize = 4;
  543. static Colour getBackgroundColour() { return Colour (0xcb5c7879); }
  544. };
  545. std::unique_ptr<LiteralHighlightOverlay> overlay;
  546. };
  547. //==============================================================================
  548. class LiveBuildCodeEditorDocument : public SourceCodeDocument
  549. {
  550. public:
  551. LiveBuildCodeEditorDocument (Project* projectToUse, const File& file)
  552. : SourceCodeDocument (projectToUse, file)
  553. {
  554. if (projectToUse != nullptr)
  555. if (CompileEngineChildProcess::Ptr childProcess = getChildProcess())
  556. childProcess->editorOpened (file, getCodeDocument());
  557. }
  558. struct Type : public SourceCodeDocument::Type
  559. {
  560. Document* openFile (Project* proj, const File& file) override
  561. {
  562. return new LiveBuildCodeEditorDocument (proj, file);
  563. }
  564. };
  565. Component* createEditor() override
  566. {
  567. SourceCodeEditor* e = nullptr;
  568. if (fileNeedsCppSyntaxHighlighting (getFile()))
  569. e = new SourceCodeEditor (this, new LiveBuildCodeEditor (*this, getCodeDocument()));
  570. else
  571. e = new SourceCodeEditor (this, getCodeDocument());
  572. applyLastState (*(e->editor));
  573. return e;
  574. }
  575. // override save() to make a few more attempts at saving if it fails, since on Windows
  576. // the compiler can interfere with things saving..
  577. bool save() override
  578. {
  579. for (int i = 5; --i >= 0;)
  580. {
  581. if (SourceCodeDocument::save()) // should already re-try for up to half a second
  582. return true;
  583. Thread::sleep (100);
  584. }
  585. return false;
  586. }
  587. CompileEngineChildProcess::Ptr getChildProcess() const
  588. {
  589. return ProjucerApplication::getApp().childProcessCache->getExisting (*project);
  590. }
  591. };