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.

744 lines
25KB

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