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.

659 lines
20KB

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