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.

691 lines
22KB

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