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.

737 lines
25KB

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