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.

667 lines
21KB

  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. #include "../Application/jucer_Headers.h"
  20. #include "jucer_SourceCodeEditor.h"
  21. #include "../Application/jucer_Application.h"
  22. //==============================================================================
  23. SourceCodeDocument::SourceCodeDocument (Project* p, const File& f)
  24. : modDetector (f), project (p)
  25. {
  26. }
  27. CodeDocument& SourceCodeDocument::getCodeDocument()
  28. {
  29. if (codeDoc == nullptr)
  30. {
  31. codeDoc.reset (new CodeDocument());
  32. reloadInternal();
  33. codeDoc->clearUndoHistory();
  34. }
  35. return *codeDoc;
  36. }
  37. Component* SourceCodeDocument::createEditor()
  38. {
  39. auto* e = new SourceCodeEditor (this, getCodeDocument());
  40. applyLastState (*(e->editor));
  41. return e;
  42. }
  43. void SourceCodeDocument::reloadFromFile()
  44. {
  45. getCodeDocument();
  46. reloadInternal();
  47. }
  48. void SourceCodeDocument::reloadInternal()
  49. {
  50. jassert (codeDoc != nullptr);
  51. modDetector.updateHash();
  52. auto fileContent = getFile().loadFileAsString();
  53. auto lineFeed = getLineFeedForFile (fileContent);
  54. if (lineFeed.isEmpty())
  55. {
  56. if (project != nullptr)
  57. lineFeed = project->getProjectLineFeed();
  58. else
  59. lineFeed = "\r\n";
  60. }
  61. codeDoc->setNewLineCharacters (lineFeed);
  62. codeDoc->applyChanges (fileContent);
  63. codeDoc->setSavePoint();
  64. }
  65. static bool writeCodeDocToFile (const File& file, CodeDocument& doc)
  66. {
  67. TemporaryFile temp (file);
  68. {
  69. FileOutputStream fo (temp.getFile());
  70. if (! (fo.openedOk() && doc.writeToStream (fo)))
  71. return false;
  72. }
  73. return temp.overwriteTargetFileWithTemporary();
  74. }
  75. bool SourceCodeDocument::save()
  76. {
  77. if (writeCodeDocToFile (getFile(), getCodeDocument()))
  78. {
  79. getCodeDocument().setSavePoint();
  80. modDetector.updateHash();
  81. return true;
  82. }
  83. return false;
  84. }
  85. bool SourceCodeDocument::saveAs()
  86. {
  87. FileChooser fc (TRANS("Save As..."), getFile(), "*");
  88. if (! fc.browseForFileToSave (true))
  89. return true;
  90. return writeCodeDocToFile (fc.getResult(), getCodeDocument());
  91. }
  92. void SourceCodeDocument::updateLastState (CodeEditorComponent& editor)
  93. {
  94. lastState.reset (new CodeEditorComponent::State (editor));
  95. }
  96. void SourceCodeDocument::applyLastState (CodeEditorComponent& editor) const
  97. {
  98. if (lastState != nullptr)
  99. lastState->restoreState (editor);
  100. }
  101. //==============================================================================
  102. SourceCodeEditor::SourceCodeEditor (OpenDocumentManager::Document* doc, CodeDocument& codeDocument)
  103. : DocumentEditorComponent (doc)
  104. {
  105. GenericCodeEditorComponent* ed = nullptr;
  106. auto file = document->getFile();
  107. if (fileNeedsCppSyntaxHighlighting (file))
  108. {
  109. ed = new CppCodeEditorComponent (file, codeDocument);
  110. }
  111. else
  112. {
  113. CodeTokeniser* tokeniser = nullptr;
  114. if (file.hasFileExtension ("xml;svg"))
  115. {
  116. static XmlTokeniser xmlTokeniser;
  117. tokeniser = &xmlTokeniser;
  118. }
  119. if (file.hasFileExtension ("lua"))
  120. {
  121. static LuaTokeniser luaTokeniser;
  122. tokeniser = &luaTokeniser;
  123. }
  124. ed = new GenericCodeEditorComponent (file, codeDocument, tokeniser);
  125. }
  126. setEditor (ed);
  127. }
  128. SourceCodeEditor::SourceCodeEditor (OpenDocumentManager::Document* doc, GenericCodeEditorComponent* ed)
  129. : DocumentEditorComponent (doc)
  130. {
  131. setEditor (ed);
  132. }
  133. SourceCodeEditor::~SourceCodeEditor()
  134. {
  135. if (editor != nullptr)
  136. editor->getDocument().removeListener (this);
  137. getAppSettings().appearance.settings.removeListener (this);
  138. if (auto* doc = dynamic_cast<SourceCodeDocument*> (getDocument()))
  139. doc->updateLastState (*editor);
  140. }
  141. void SourceCodeEditor::setEditor (GenericCodeEditorComponent* newEditor)
  142. {
  143. if (editor != nullptr)
  144. editor->getDocument().removeListener (this);
  145. editor.reset (newEditor);
  146. addAndMakeVisible (newEditor);
  147. editor->setFont (AppearanceSettings::getDefaultCodeFont());
  148. editor->setTabSize (4, true);
  149. updateColourScheme();
  150. getAppSettings().appearance.settings.addListener (this);
  151. editor->getDocument().addListener (this);
  152. }
  153. void SourceCodeEditor::scrollToKeepRangeOnScreen (Range<int> range)
  154. {
  155. auto space = jmin (10, editor->getNumLinesOnScreen() / 3);
  156. const CodeDocument::Position start (editor->getDocument(), range.getStart());
  157. const CodeDocument::Position end (editor->getDocument(), range.getEnd());
  158. editor->scrollToKeepLinesOnScreen ({ start.getLineNumber() - space, end.getLineNumber() + space });
  159. }
  160. void SourceCodeEditor::highlight (Range<int> range, bool cursorAtStart)
  161. {
  162. scrollToKeepRangeOnScreen (range);
  163. if (cursorAtStart)
  164. {
  165. editor->moveCaretTo (CodeDocument::Position (editor->getDocument(), range.getEnd()), false);
  166. editor->moveCaretTo (CodeDocument::Position (editor->getDocument(), range.getStart()), true);
  167. }
  168. else
  169. {
  170. editor->setHighlightedRegion (range);
  171. }
  172. }
  173. void SourceCodeEditor::resized()
  174. {
  175. editor->setBounds (getLocalBounds());
  176. }
  177. void SourceCodeEditor::updateColourScheme()
  178. {
  179. getAppSettings().appearance.applyToCodeEditor (*editor);
  180. }
  181. void SourceCodeEditor::checkSaveState()
  182. {
  183. setEditedState (getDocument()->needsSaving());
  184. }
  185. void SourceCodeEditor::lookAndFeelChanged()
  186. {
  187. updateColourScheme();
  188. }
  189. void SourceCodeEditor::valueTreePropertyChanged (ValueTree&, const Identifier&) { updateColourScheme(); }
  190. void SourceCodeEditor::valueTreeChildAdded (ValueTree&, ValueTree&) { updateColourScheme(); }
  191. void SourceCodeEditor::valueTreeChildRemoved (ValueTree&, ValueTree&, int) { updateColourScheme(); }
  192. void SourceCodeEditor::valueTreeChildOrderChanged (ValueTree&, int, int) { updateColourScheme(); }
  193. void SourceCodeEditor::valueTreeParentChanged (ValueTree&) { updateColourScheme(); }
  194. void SourceCodeEditor::valueTreeRedirected (ValueTree&) { updateColourScheme(); }
  195. void SourceCodeEditor::codeDocumentTextInserted (const String&, int) { checkSaveState(); }
  196. void SourceCodeEditor::codeDocumentTextDeleted (int, int) { checkSaveState(); }
  197. //==============================================================================
  198. GenericCodeEditorComponent::GenericCodeEditorComponent (const File& f, CodeDocument& codeDocument,
  199. CodeTokeniser* tokeniser)
  200. : CodeEditorComponent (codeDocument, tokeniser), file (f)
  201. {
  202. setScrollbarThickness (6);
  203. setCommandManager (&ProjucerApplication::getCommandManager());
  204. }
  205. GenericCodeEditorComponent::~GenericCodeEditorComponent() {}
  206. enum
  207. {
  208. showInFinderID = 0x2fe821e3,
  209. insertComponentID = 0x2fe821e4
  210. };
  211. void GenericCodeEditorComponent::addPopupMenuItems (PopupMenu& menu, const MouseEvent* e)
  212. {
  213. menu.addItem (showInFinderID,
  214. #if JUCE_MAC
  215. "Reveal " + file.getFileName() + " in Finder");
  216. #else
  217. "Reveal " + file.getFileName() + " in Explorer");
  218. #endif
  219. menu.addSeparator();
  220. CodeEditorComponent::addPopupMenuItems (menu, e);
  221. }
  222. void GenericCodeEditorComponent::performPopupMenuAction (int menuItemID)
  223. {
  224. if (menuItemID == showInFinderID)
  225. file.revealToUser();
  226. else
  227. CodeEditorComponent::performPopupMenuAction (menuItemID);
  228. }
  229. void GenericCodeEditorComponent::getAllCommands (Array <CommandID>& commands)
  230. {
  231. CodeEditorComponent::getAllCommands (commands);
  232. const CommandID ids[] = { CommandIDs::showFindPanel,
  233. CommandIDs::findSelection,
  234. CommandIDs::findNext,
  235. CommandIDs::findPrevious };
  236. commands.addArray (ids, numElementsInArray (ids));
  237. }
  238. void GenericCodeEditorComponent::getCommandInfo (const CommandID commandID, ApplicationCommandInfo& result)
  239. {
  240. auto anythingSelected = isHighlightActive();
  241. switch (commandID)
  242. {
  243. case CommandIDs::showFindPanel:
  244. result.setInfo (TRANS ("Find"), TRANS ("Searches for text in the current document."), "Editing", 0);
  245. result.defaultKeypresses.add (KeyPress ('f', ModifierKeys::commandModifier, 0));
  246. break;
  247. case CommandIDs::findSelection:
  248. result.setInfo (TRANS ("Find Selection"), TRANS ("Searches for the currently selected text."), "Editing", 0);
  249. result.setActive (anythingSelected);
  250. result.defaultKeypresses.add (KeyPress ('l', ModifierKeys::commandModifier, 0));
  251. break;
  252. case CommandIDs::findNext:
  253. result.setInfo (TRANS ("Find Next"), TRANS ("Searches for the next occurrence of the current search-term."), "Editing", 0);
  254. result.defaultKeypresses.add (KeyPress ('g', ModifierKeys::commandModifier, 0));
  255. break;
  256. case CommandIDs::findPrevious:
  257. result.setInfo (TRANS ("Find Previous"), TRANS ("Searches for the previous occurrence of the current search-term."), "Editing", 0);
  258. result.defaultKeypresses.add (KeyPress ('g', ModifierKeys::commandModifier | ModifierKeys::shiftModifier, 0));
  259. result.defaultKeypresses.add (KeyPress ('d', ModifierKeys::commandModifier, 0));
  260. break;
  261. default:
  262. CodeEditorComponent::getCommandInfo (commandID, result);
  263. break;
  264. }
  265. }
  266. bool GenericCodeEditorComponent::perform (const InvocationInfo& info)
  267. {
  268. switch (info.commandID)
  269. {
  270. case CommandIDs::showFindPanel: showFindPanel(); return true;
  271. case CommandIDs::findSelection: findSelection(); return true;
  272. case CommandIDs::findNext: findNext (true, true); return true;
  273. case CommandIDs::findPrevious: findNext (false, false); return true;
  274. default: break;
  275. }
  276. return CodeEditorComponent::perform (info);
  277. }
  278. void GenericCodeEditorComponent::addListener (GenericCodeEditorComponent::Listener* listener)
  279. {
  280. listeners.add (listener);
  281. }
  282. void GenericCodeEditorComponent::removeListener (GenericCodeEditorComponent::Listener* listener)
  283. {
  284. listeners.remove (listener);
  285. }
  286. //==============================================================================
  287. class GenericCodeEditorComponent::FindPanel : public Component
  288. {
  289. public:
  290. FindPanel()
  291. {
  292. editor.setColour (CaretComponent::caretColourId, Colours::black);
  293. addAndMakeVisible (editor);
  294. label.setColour (Label::textColourId, Colours::white);
  295. label.attachToComponent (&editor, false);
  296. addAndMakeVisible (caseButton);
  297. caseButton.setColour (ToggleButton::textColourId, Colours::white);
  298. caseButton.setToggleState (isCaseSensitiveSearch(), dontSendNotification);
  299. caseButton.onClick = [this] { setCaseSensitiveSearch (caseButton.getToggleState()); };
  300. findPrev.setConnectedEdges (Button::ConnectedOnRight);
  301. findNext.setConnectedEdges (Button::ConnectedOnLeft);
  302. addAndMakeVisible (findPrev);
  303. addAndMakeVisible (findNext);
  304. setWantsKeyboardFocus (false);
  305. setFocusContainer (true);
  306. findPrev.setWantsKeyboardFocus (false);
  307. findNext.setWantsKeyboardFocus (false);
  308. editor.setText (getSearchString());
  309. editor.onTextChange = [this] { changeSearchString(); };
  310. editor.onReturnKey = [] { ProjucerApplication::getCommandManager().invokeDirectly (CommandIDs::findNext, true); };
  311. editor.onEscapeKey = [this]
  312. {
  313. if (auto* ed = getOwner())
  314. ed->hideFindPanel();
  315. };
  316. }
  317. void setCommandManager (ApplicationCommandManager* cm)
  318. {
  319. findPrev.setCommandToTrigger (cm, CommandIDs::findPrevious, true);
  320. findNext.setCommandToTrigger (cm, CommandIDs::findNext, true);
  321. }
  322. void paint (Graphics& g) override
  323. {
  324. Path outline;
  325. outline.addRoundedRectangle (1.0f, 1.0f, getWidth() - 2.0f, getHeight() - 2.0f, 8.0f);
  326. g.setColour (Colours::black.withAlpha (0.6f));
  327. g.fillPath (outline);
  328. g.setColour (Colours::white.withAlpha (0.8f));
  329. g.strokePath (outline, PathStrokeType (1.0f));
  330. }
  331. void resized() override
  332. {
  333. int y = 30;
  334. editor.setBounds (10, y, getWidth() - 20, 24);
  335. y += 30;
  336. caseButton.setBounds (10, y, getWidth() / 2 - 10, 22);
  337. findNext.setBounds (getWidth() - 40, y, 30, 22);
  338. findPrev.setBounds (getWidth() - 70, y, 30, 22);
  339. }
  340. void changeSearchString()
  341. {
  342. setSearchString (editor.getText());
  343. if (auto* ed = getOwner())
  344. ed->findNext (true, false);
  345. }
  346. GenericCodeEditorComponent* getOwner() const
  347. {
  348. return findParentComponentOfClass <GenericCodeEditorComponent>();
  349. }
  350. TextEditor editor;
  351. Label label { {}, "Find:" };
  352. ToggleButton caseButton { "Case-sensitive" };
  353. TextButton findPrev { "<" },
  354. findNext { ">" };
  355. };
  356. void GenericCodeEditorComponent::resized()
  357. {
  358. CodeEditorComponent::resized();
  359. if (findPanel != nullptr)
  360. {
  361. findPanel->setSize (jmin (260, getWidth() - 32), 100);
  362. findPanel->setTopRightPosition (getWidth() - 16, 8);
  363. }
  364. }
  365. void GenericCodeEditorComponent::showFindPanel()
  366. {
  367. if (findPanel == nullptr)
  368. {
  369. findPanel.reset (new FindPanel());
  370. findPanel->setCommandManager (&ProjucerApplication::getCommandManager());
  371. addAndMakeVisible (findPanel.get());
  372. resized();
  373. }
  374. if (findPanel != nullptr)
  375. {
  376. findPanel->editor.grabKeyboardFocus();
  377. findPanel->editor.selectAll();
  378. }
  379. }
  380. void GenericCodeEditorComponent::hideFindPanel()
  381. {
  382. findPanel.reset();
  383. }
  384. void GenericCodeEditorComponent::findSelection()
  385. {
  386. auto selected = getTextInRange (getHighlightedRegion());
  387. if (selected.isNotEmpty())
  388. {
  389. setSearchString (selected);
  390. findNext (true, true);
  391. }
  392. }
  393. void GenericCodeEditorComponent::findNext (bool forwards, bool skipCurrentSelection)
  394. {
  395. auto highlight = getHighlightedRegion();
  396. const CodeDocument::Position startPos (getDocument(), skipCurrentSelection ? highlight.getEnd()
  397. : highlight.getStart());
  398. auto lineNum = startPos.getLineNumber();
  399. auto linePos = startPos.getIndexInLine();
  400. auto totalLines = getDocument().getNumLines();
  401. auto searchText = getSearchString();
  402. auto caseSensitive = isCaseSensitiveSearch();
  403. for (auto linesToSearch = totalLines; --linesToSearch >= 0;)
  404. {
  405. auto line = getDocument().getLine (lineNum);
  406. int index;
  407. if (forwards)
  408. {
  409. index = caseSensitive ? line.indexOf (linePos, searchText)
  410. : line.indexOfIgnoreCase (linePos, searchText);
  411. }
  412. else
  413. {
  414. if (linePos >= 0)
  415. line = line.substring (0, linePos);
  416. index = caseSensitive ? line.lastIndexOf (searchText)
  417. : line.lastIndexOfIgnoreCase (searchText);
  418. }
  419. if (index >= 0)
  420. {
  421. const CodeDocument::Position p (getDocument(), lineNum, index);
  422. selectRegion (p, p.movedBy (searchText.length()));
  423. break;
  424. }
  425. if (forwards)
  426. {
  427. linePos = 0;
  428. lineNum = (lineNum + 1) % totalLines;
  429. }
  430. else
  431. {
  432. if (--lineNum < 0)
  433. lineNum = totalLines - 1;
  434. linePos = -1;
  435. }
  436. }
  437. }
  438. void GenericCodeEditorComponent::handleEscapeKey()
  439. {
  440. CodeEditorComponent::handleEscapeKey();
  441. hideFindPanel();
  442. }
  443. void GenericCodeEditorComponent::editorViewportPositionChanged()
  444. {
  445. CodeEditorComponent::editorViewportPositionChanged();
  446. listeners.call ([this] (Listener& l) { l.codeEditorViewportMoved (*this); });
  447. }
  448. //==============================================================================
  449. static CPlusPlusCodeTokeniser cppTokeniser;
  450. CppCodeEditorComponent::CppCodeEditorComponent (const File& f, CodeDocument& doc)
  451. : GenericCodeEditorComponent (f, doc, &cppTokeniser)
  452. {
  453. }
  454. CppCodeEditorComponent::~CppCodeEditorComponent() {}
  455. void CppCodeEditorComponent::handleReturnKey()
  456. {
  457. GenericCodeEditorComponent::handleReturnKey();
  458. auto pos = getCaretPos();
  459. String blockIndent, lastLineIndent;
  460. CodeHelpers::getIndentForCurrentBlock (pos, getTabString (getTabSize()), blockIndent, lastLineIndent);
  461. auto remainderOfBrokenLine = pos.getLineText();
  462. auto numLeadingWSChars = CodeHelpers::getLeadingWhitespace (remainderOfBrokenLine).length();
  463. if (numLeadingWSChars > 0)
  464. getDocument().deleteSection (pos, pos.movedBy (numLeadingWSChars));
  465. if (remainderOfBrokenLine.trimStart().startsWithChar ('}'))
  466. insertTextAtCaret (blockIndent);
  467. else
  468. insertTextAtCaret (lastLineIndent);
  469. auto previousLine = pos.movedByLines (-1).getLineText();
  470. auto trimmedPreviousLine = previousLine.trim();
  471. if ((trimmedPreviousLine.startsWith ("if ")
  472. || trimmedPreviousLine.startsWith ("if(")
  473. || trimmedPreviousLine.startsWith ("for ")
  474. || trimmedPreviousLine.startsWith ("for(")
  475. || trimmedPreviousLine.startsWith ("while(")
  476. || trimmedPreviousLine.startsWith ("while "))
  477. && trimmedPreviousLine.endsWithChar (')'))
  478. {
  479. insertTabAtCaret();
  480. }
  481. }
  482. void CppCodeEditorComponent::insertTextAtCaret (const String& newText)
  483. {
  484. if (getHighlightedRegion().isEmpty())
  485. {
  486. auto pos = getCaretPos();
  487. if ((newText == "{" || newText == "}")
  488. && pos.getLineNumber() > 0
  489. && pos.getLineText().trim().isEmpty())
  490. {
  491. moveCaretToStartOfLine (true);
  492. String blockIndent, lastLineIndent;
  493. if (CodeHelpers::getIndentForCurrentBlock (pos, getTabString (getTabSize()), blockIndent, lastLineIndent))
  494. {
  495. GenericCodeEditorComponent::insertTextAtCaret (blockIndent);
  496. if (newText == "{")
  497. insertTabAtCaret();
  498. }
  499. }
  500. }
  501. GenericCodeEditorComponent::insertTextAtCaret (newText);
  502. }
  503. void CppCodeEditorComponent::addPopupMenuItems (PopupMenu& menu, const MouseEvent* e)
  504. {
  505. GenericCodeEditorComponent::addPopupMenuItems (menu, e);
  506. menu.addSeparator();
  507. menu.addItem (insertComponentID, TRANS("Insert code for a new Component class..."));
  508. }
  509. void CppCodeEditorComponent::performPopupMenuAction (int menuItemID)
  510. {
  511. if (menuItemID == insertComponentID)
  512. insertComponentClass();
  513. GenericCodeEditorComponent::performPopupMenuAction (menuItemID);
  514. }
  515. void CppCodeEditorComponent::insertComponentClass()
  516. {
  517. AlertWindow aw (TRANS ("Insert a new Component class"),
  518. TRANS ("Please enter a name for the new class"),
  519. AlertWindow::NoIcon, nullptr);
  520. const char* classNameField = "Class Name";
  521. aw.addTextEditor (classNameField, String(), String(), false);
  522. aw.addButton (TRANS ("Insert Code"), 1, KeyPress (KeyPress::returnKey));
  523. aw.addButton (TRANS ("Cancel"), 0, KeyPress (KeyPress::escapeKey));
  524. while (aw.runModalLoop() != 0)
  525. {
  526. auto className = aw.getTextEditorContents (classNameField).trim();
  527. if (className == CodeHelpers::makeValidIdentifier (className, false, true, false))
  528. {
  529. String code (BinaryData::jucer_InlineComponentTemplate_h);
  530. code = code.replace ("%%component_class%%", className);
  531. insertTextAtCaret (code);
  532. break;
  533. }
  534. }
  535. }