| @@ -183,8 +183,8 @@ public: | |||||
| } | } | ||||
| else if (topLevelMenuIndex == 1) // "Edit" menu | else if (topLevelMenuIndex == 1) // "Edit" menu | ||||
| { | { | ||||
| menu.addCommandItem (commandManager, CommandIDs::undo); | |||||
| menu.addCommandItem (commandManager, CommandIDs::redo); | |||||
| menu.addCommandItem (commandManager, StandardApplicationCommandIDs::undo); | |||||
| menu.addCommandItem (commandManager, StandardApplicationCommandIDs::redo); | |||||
| menu.addSeparator(); | menu.addSeparator(); | ||||
| menu.addCommandItem (commandManager, StandardApplicationCommandIDs::cut); | menu.addCommandItem (commandManager, StandardApplicationCommandIDs::cut); | ||||
| menu.addCommandItem (commandManager, StandardApplicationCommandIDs::copy); | menu.addCommandItem (commandManager, StandardApplicationCommandIDs::copy); | ||||
| @@ -43,8 +43,6 @@ namespace CommandIDs | |||||
| static const int showAppearanceSettings = 0x200077; | static const int showAppearanceSettings = 0x200077; | ||||
| static const int saveAll = 0x200080; | static const int saveAll = 0x200080; | ||||
| static const int undo = 0x200090; | |||||
| static const int redo = 0x2000a0; | |||||
| static const int closeWindow = 0x201001; | static const int closeWindow = 0x201001; | ||||
| static const int closeAllDocuments = 0x201000; | static const int closeAllDocuments = 0x201000; | ||||
| @@ -28,7 +28,7 @@ | |||||
| #include "../Code Editor/jucer_SourceCodeEditor.h" | #include "../Code Editor/jucer_SourceCodeEditor.h" | ||||
| //============================================================================== | //============================================================================== | ||||
| Component* SourceCodeDocument::createEditor() { return SourceCodeEditor::createFor (this, codeDoc); } | |||||
| Component* SourceCodeDocument::createEditor() { return new SourceCodeEditor (this, codeDoc); } | |||||
| //============================================================================== | //============================================================================== | ||||
| @@ -29,23 +29,21 @@ | |||||
| //============================================================================== | //============================================================================== | ||||
| SourceCodeEditor::SourceCodeEditor (OpenDocumentManager::Document* document_, | SourceCodeEditor::SourceCodeEditor (OpenDocumentManager::Document* document_, | ||||
| CodeDocument& codeDocument, | |||||
| CodeTokeniser* const codeTokeniser) | |||||
| : DocumentEditorComponent (document_), | |||||
| editor (codeDocument, codeTokeniser) | |||||
| CodeDocument& codeDocument) | |||||
| : DocumentEditorComponent (document_) | |||||
| { | { | ||||
| addAndMakeVisible (&editor); | |||||
| addAndMakeVisible (editor = createEditor (codeDocument)); | |||||
| #if JUCE_MAC | #if JUCE_MAC | ||||
| Font font (13.0f); | Font font (13.0f); | ||||
| font.setTypefaceName ("Menlo"); | font.setTypefaceName ("Menlo"); | ||||
| #else | #else | ||||
| Font font (10.0f); | |||||
| Font font (12.0f); | |||||
| font.setTypefaceName (Font::getDefaultMonospacedFontName()); | font.setTypefaceName (Font::getDefaultMonospacedFontName()); | ||||
| #endif | #endif | ||||
| editor.setFont (font); | |||||
| editor->setFont (font); | |||||
| editor.setTabSize (4, true); | |||||
| editor->setTabSize (4, true); | |||||
| updateColourScheme(); | updateColourScheme(); | ||||
| getAppSettings().appearance.settings.addListener (this); | getAppSettings().appearance.settings.addListener (this); | ||||
| @@ -56,36 +54,25 @@ SourceCodeEditor::~SourceCodeEditor() | |||||
| getAppSettings().appearance.settings.removeListener (this); | getAppSettings().appearance.settings.removeListener (this); | ||||
| } | } | ||||
| void SourceCodeEditor::resized() | |||||
| CodeEditorComponent* SourceCodeEditor::createEditor (CodeDocument& codeDocument) | |||||
| { | { | ||||
| editor.setBounds (getLocalBounds()); | |||||
| } | |||||
| if (document->getFile().hasFileExtension (sourceOrHeaderFileExtensions)) | |||||
| return new CppCodeEditorComponent (codeDocument); | |||||
| CodeTokeniser* SourceCodeEditor::getTokeniserFor (const File& file) | |||||
| { | |||||
| if (file.hasFileExtension (sourceOrHeaderFileExtensions)) | |||||
| { | |||||
| static CPlusPlusCodeTokeniser cppTokeniser; | |||||
| return &cppTokeniser; | |||||
| } | |||||
| return nullptr; | |||||
| return new CodeEditorComponent (codeDocument, nullptr); | |||||
| } | } | ||||
| SourceCodeEditor* SourceCodeEditor::createFor (OpenDocumentManager::Document* document, | |||||
| CodeDocument& codeDocument) | |||||
| //============================================================================== | |||||
| void SourceCodeEditor::resized() | |||||
| { | { | ||||
| return new SourceCodeEditor (document, codeDocument, getTokeniserFor (document->getFile())); | |||||
| editor->setBounds (getLocalBounds()); | |||||
| } | } | ||||
| void SourceCodeEditor::updateColourScheme() { getAppSettings().appearance.applyToCodeEditor (*editor); } | |||||
| void SourceCodeEditor::valueTreePropertyChanged (ValueTree&, const Identifier&) { updateColourScheme(); } | void SourceCodeEditor::valueTreePropertyChanged (ValueTree&, const Identifier&) { updateColourScheme(); } | ||||
| void SourceCodeEditor::valueTreeChildAdded (ValueTree&, ValueTree&) { updateColourScheme(); } | void SourceCodeEditor::valueTreeChildAdded (ValueTree&, ValueTree&) { updateColourScheme(); } | ||||
| void SourceCodeEditor::valueTreeChildRemoved (ValueTree&, ValueTree&) { updateColourScheme(); } | void SourceCodeEditor::valueTreeChildRemoved (ValueTree&, ValueTree&) { updateColourScheme(); } | ||||
| void SourceCodeEditor::valueTreeChildOrderChanged (ValueTree&) { updateColourScheme(); } | void SourceCodeEditor::valueTreeChildOrderChanged (ValueTree&) { updateColourScheme(); } | ||||
| void SourceCodeEditor::valueTreeParentChanged (ValueTree&) { updateColourScheme(); } | void SourceCodeEditor::valueTreeParentChanged (ValueTree&) { updateColourScheme(); } | ||||
| void SourceCodeEditor::valueTreeRedirected (ValueTree&) { updateColourScheme(); } | void SourceCodeEditor::valueTreeRedirected (ValueTree&) { updateColourScheme(); } | ||||
| void SourceCodeEditor::updateColourScheme() | |||||
| { | |||||
| getAppSettings().appearance.applyToCodeEditor (editor); | |||||
| } | |||||
| @@ -34,24 +34,16 @@ class SourceCodeEditor : public DocumentEditorComponent, | |||||
| private ValueTree::Listener | private ValueTree::Listener | ||||
| { | { | ||||
| public: | public: | ||||
| //============================================================================== | |||||
| SourceCodeEditor (OpenDocumentManager::Document* document, | SourceCodeEditor (OpenDocumentManager::Document* document, | ||||
| CodeDocument& codeDocument, | |||||
| CodeTokeniser* const codeTokeniser); | |||||
| CodeDocument& codeDocument); | |||||
| ~SourceCodeEditor(); | ~SourceCodeEditor(); | ||||
| static SourceCodeEditor* createFor (OpenDocumentManager::Document* document, | |||||
| CodeDocument& codeDocument); | |||||
| static CodeTokeniser* getTokeniserFor (const File& file); | |||||
| private: | |||||
| ScopedPointer<CodeEditorComponent> editor; | |||||
| //============================================================================== | |||||
| CodeEditorComponent* createEditor (CodeDocument&); | |||||
| void resized(); | void resized(); | ||||
| CodeEditorComponent editor; | |||||
| private: | |||||
| void valueTreePropertyChanged (ValueTree&, const Identifier&); | void valueTreePropertyChanged (ValueTree&, const Identifier&); | ||||
| void valueTreeChildAdded (ValueTree&, ValueTree&); | void valueTreeChildAdded (ValueTree&, ValueTree&); | ||||
| void valueTreeChildRemoved (ValueTree&, ValueTree&); | void valueTreeChildRemoved (ValueTree&, ValueTree&); | ||||
| @@ -65,4 +57,88 @@ private: | |||||
| }; | }; | ||||
| //============================================================================== | |||||
| class CppCodeEditorComponent : public CodeEditorComponent | |||||
| { | |||||
| public: | |||||
| CppCodeEditorComponent (CodeDocument& codeDocument) | |||||
| : CodeEditorComponent (codeDocument, getCppTokeniser()) | |||||
| { | |||||
| } | |||||
| void handleReturnKey() | |||||
| { | |||||
| CodeEditorComponent::handleReturnKey(); | |||||
| const CodeDocument::Position pos (getCaretPos()); | |||||
| if (pos.getLineNumber() > 0 && pos.getLineText().trim().isEmpty()) | |||||
| { | |||||
| String indent (getIndentForCurrentBlock (pos)); | |||||
| const String previousLine (pos.movedByLines (-1).getLineText()); | |||||
| const String trimmedPreviousLine (previousLine.trim()); | |||||
| const String leadingWhitespace (getLeadingWhitespace (previousLine)); | |||||
| insertTextAtCaret (leadingWhitespace); | |||||
| if (trimmedPreviousLine.endsWithChar ('{') | |||||
| || ((trimmedPreviousLine.startsWith ("if ") | |||||
| || trimmedPreviousLine.startsWith ("for ") | |||||
| || trimmedPreviousLine.startsWith ("while ")) | |||||
| && trimmedPreviousLine.endsWithChar (')'))) | |||||
| insertTabAtCaret(); | |||||
| } | |||||
| } | |||||
| void insertTextAtCaret (const String& newText) | |||||
| { | |||||
| if (getHighlightedRegion().isEmpty()) | |||||
| { | |||||
| const CodeDocument::Position pos (getCaretPos()); | |||||
| if ((newText == "{" || newText == "}") && pos.getLineNumber() > 0) | |||||
| { | |||||
| moveCaretToStartOfLine (true); | |||||
| CodeEditorComponent::insertTextAtCaret (getIndentForCurrentBlock (pos)); | |||||
| if (newText == "{") | |||||
| insertTabAtCaret(); | |||||
| } | |||||
| } | |||||
| CodeEditorComponent::insertTextAtCaret (newText); | |||||
| } | |||||
| private: | |||||
| static CPlusPlusCodeTokeniser* getCppTokeniser() | |||||
| { | |||||
| static CPlusPlusCodeTokeniser cppTokeniser; | |||||
| return &cppTokeniser; | |||||
| } | |||||
| static String getLeadingWhitespace (String line) | |||||
| { | |||||
| line = line.removeCharacters ("\r\n"); | |||||
| const String::CharPointerType endOfLeadingWS (line.getCharPointer().findEndOfWhitespace()); | |||||
| return String (line.getCharPointer(), endOfLeadingWS); | |||||
| } | |||||
| static String getIndentForCurrentBlock (CodeDocument::Position pos) | |||||
| { | |||||
| while (pos.getLineNumber() > 0) | |||||
| { | |||||
| pos = pos.movedByLines (-1); | |||||
| const String line (pos.getLineText()); | |||||
| const String trimmedLine (line.trimStart()); | |||||
| if (trimmedLine.startsWithChar ('{')) | |||||
| return getLeadingWhitespace (line); | |||||
| } | |||||
| return String::empty; | |||||
| } | |||||
| }; | |||||
| #endif // __JUCER_SOURCECODEEDITOR_JUCEHEADER__ | #endif // __JUCER_SOURCECODEEDITOR_JUCEHEADER__ | ||||
| @@ -77,6 +77,12 @@ namespace StandardApplicationCommandIDs | |||||
| /** The command ID that should be used to send a "Deselect all" command. */ | /** The command ID that should be used to send a "Deselect all" command. */ | ||||
| static const CommandID deselectAll = 0x1007; | static const CommandID deselectAll = 0x1007; | ||||
| /** The command ID that should be used to send a "undo" command. */ | |||||
| static const CommandID undo = 0x1008; | |||||
| /** The command ID that should be used to send a "redo" command. */ | |||||
| static const CommandID redo = 0x1009; | |||||
| } | } | ||||
| @@ -1755,12 +1755,51 @@ void TextEditor::paintOverChildren (Graphics& g) | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*) | |||||
| { | |||||
| const bool writable = ! isReadOnly(); | |||||
| if (passwordCharacter == 0) | |||||
| { | |||||
| m.addItem (StandardApplicationCommandIDs::cut, TRANS("Cut"), writable); | |||||
| m.addItem (StandardApplicationCommandIDs::copy, TRANS("Copy"), ! selection.isEmpty()); | |||||
| m.addItem (StandardApplicationCommandIDs::paste, TRANS("Paste"), writable); | |||||
| } | |||||
| m.addItem (StandardApplicationCommandIDs::del, TRANS("Delete"), writable); | |||||
| m.addSeparator(); | |||||
| m.addItem (StandardApplicationCommandIDs::selectAll, TRANS("Select All")); | |||||
| m.addSeparator(); | |||||
| if (getUndoManager() != nullptr) | |||||
| { | |||||
| m.addItem (StandardApplicationCommandIDs::undo, TRANS("Undo"), undoManager.canUndo()); | |||||
| m.addItem (StandardApplicationCommandIDs::redo, TRANS("Redo"), undoManager.canRedo()); | |||||
| } | |||||
| } | |||||
| void TextEditor::performPopupMenuAction (const int menuItemID) | |||||
| { | |||||
| switch (menuItemID) | |||||
| { | |||||
| case StandardApplicationCommandIDs::cut: cutToClipboard(); break; | |||||
| case StandardApplicationCommandIDs::copy: copyToClipboard(); break; | |||||
| case StandardApplicationCommandIDs::paste: pasteFromClipboard(); break; | |||||
| case StandardApplicationCommandIDs::del: cut(); break; | |||||
| case StandardApplicationCommandIDs::selectAll: selectAll(); break; | |||||
| case StandardApplicationCommandIDs::undo: undo(); break; | |||||
| case StandardApplicationCommandIDs::redo: redo(); break; | |||||
| default: break; | |||||
| } | |||||
| } | |||||
| static void textEditorMenuCallback (int menuResult, TextEditor* editor) | static void textEditorMenuCallback (int menuResult, TextEditor* editor) | ||||
| { | { | ||||
| if (editor != nullptr && menuResult != 0) | if (editor != nullptr && menuResult != 0) | ||||
| editor->performPopupMenuAction (menuResult); | editor->performPopupMenuAction (menuResult); | ||||
| } | } | ||||
| //============================================================================== | |||||
| void TextEditor::mouseDown (const MouseEvent& e) | void TextEditor::mouseDown (const MouseEvent& e) | ||||
| { | { | ||||
| beginDragAutoRepeat (100); | beginDragAutoRepeat (100); | ||||
| @@ -1788,12 +1827,8 @@ void TextEditor::mouseDown (const MouseEvent& e) | |||||
| void TextEditor::mouseDrag (const MouseEvent& e) | void TextEditor::mouseDrag (const MouseEvent& e) | ||||
| { | { | ||||
| if (wasFocused || ! selectAllTextWhenFocused) | if (wasFocused || ! selectAllTextWhenFocused) | ||||
| { | |||||
| if (! (popupMenuEnabled && e.mods.isPopupMenu())) | if (! (popupMenuEnabled && e.mods.isPopupMenu())) | ||||
| { | |||||
| moveCaretTo (getTextIndexAt (e.x, e.y), true); | moveCaretTo (getTextIndexAt (e.x, e.y), true); | ||||
| } | |||||
| } | |||||
| } | } | ||||
| void TextEditor::mouseUp (const MouseEvent& e) | void TextEditor::mouseUp (const MouseEvent& e) | ||||
| @@ -1802,12 +1837,8 @@ void TextEditor::mouseUp (const MouseEvent& e) | |||||
| textHolder->restartTimer(); | textHolder->restartTimer(); | ||||
| if (wasFocused || ! selectAllTextWhenFocused) | if (wasFocused || ! selectAllTextWhenFocused) | ||||
| { | |||||
| if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu())) | if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu())) | ||||
| { | |||||
| moveCaret (getTextIndexAt (e.x, e.y)); | moveCaret (getTextIndexAt (e.x, e.y)); | ||||
| } | |||||
| } | |||||
| wasFocused = true; | wasFocused = true; | ||||
| } | } | ||||
| @@ -2094,47 +2125,6 @@ bool TextEditor::keyStateChanged (const bool isKeyDown) | |||||
| return ! ModifierKeys::getCurrentModifiers().isCommandDown(); | return ! ModifierKeys::getCurrentModifiers().isCommandDown(); | ||||
| } | } | ||||
| //============================================================================== | |||||
| const int baseMenuItemID = 0x7fff0000; | |||||
| void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*) | |||||
| { | |||||
| const bool writable = ! isReadOnly(); | |||||
| if (passwordCharacter == 0) | |||||
| { | |||||
| m.addItem (baseMenuItemID + 1, TRANS("cut"), writable); | |||||
| m.addItem (baseMenuItemID + 2, TRANS("copy"), ! selection.isEmpty()); | |||||
| m.addItem (baseMenuItemID + 3, TRANS("paste"), writable); | |||||
| } | |||||
| m.addItem (baseMenuItemID + 4, TRANS("delete"), writable); | |||||
| m.addSeparator(); | |||||
| m.addItem (baseMenuItemID + 5, TRANS("select all")); | |||||
| m.addSeparator(); | |||||
| if (getUndoManager() != nullptr) | |||||
| { | |||||
| m.addItem (baseMenuItemID + 6, TRANS("undo"), undoManager.canUndo()); | |||||
| m.addItem (baseMenuItemID + 7, TRANS("redo"), undoManager.canRedo()); | |||||
| } | |||||
| } | |||||
| void TextEditor::performPopupMenuAction (const int menuItemID) | |||||
| { | |||||
| switch (menuItemID) | |||||
| { | |||||
| case baseMenuItemID + 1: cutToClipboard(); break; | |||||
| case baseMenuItemID + 2: copyToClipboard(); break; | |||||
| case baseMenuItemID + 3: pasteFromClipboard(); break; | |||||
| case baseMenuItemID + 4: cut(); break; | |||||
| case baseMenuItemID + 5: selectAll(); break; | |||||
| case baseMenuItemID + 6: undo(); break; | |||||
| case baseMenuItemID + 7: redo(); break; | |||||
| default: break; | |||||
| } | |||||
| } | |||||
| //============================================================================== | //============================================================================== | ||||
| void TextEditor::focusGained (FocusChangeType) | void TextEditor::focusGained (FocusChangeType) | ||||
| { | { | ||||
| @@ -566,9 +566,8 @@ public: | |||||
| When the menu has been shown, performPopupMenuAction() will be called to | When the menu has been shown, performPopupMenuAction() will be called to | ||||
| perform the item that the user has chosen. | perform the item that the user has chosen. | ||||
| The default menu items will be added using item IDs in the range | |||||
| 0x7fff0000 - 0x7fff1000, so you should avoid those values for your own | |||||
| menu IDs. | |||||
| The default menu items will be added using item IDs from the | |||||
| StandardApplicationCommandIDs namespace. | |||||
| If this was triggered by a mouse-click, the mouseClickEvent parameter will be | If this was triggered by a mouse-click, the mouseClickEvent parameter will be | ||||
| a pointer to the info about it, or may be null if the menu is being triggered | a pointer to the info about it, or may be null if the menu is being triggered | ||||
| @@ -26,8 +26,7 @@ | |||||
| class CodeEditorComponent::CodeEditorLine | class CodeEditorComponent::CodeEditorLine | ||||
| { | { | ||||
| public: | public: | ||||
| CodeEditorLine() noexcept | |||||
| : highlightColumnStart (0), highlightColumnEnd (0) | |||||
| CodeEditorLine() noexcept : highlightColumnStart (0), highlightColumnEnd (0) | |||||
| { | { | ||||
| } | } | ||||
| @@ -196,7 +195,7 @@ private: | |||||
| for (;;) | for (;;) | ||||
| { | { | ||||
| int tabPos = t.text.indexOfChar ('\t'); | |||||
| const int tabPos = t.text.indexOfChar ('\t'); | |||||
| if (tabPos < 0) | if (tabPos < 0) | ||||
| break; | break; | ||||
| @@ -226,6 +225,26 @@ private: | |||||
| } | } | ||||
| }; | }; | ||||
| namespace CodeEditorHelpers | |||||
| { | |||||
| static int findFirstNonWhitespaceChar (const String& line) noexcept | |||||
| { | |||||
| String::CharPointerType t (line.getCharPointer()); | |||||
| int i = 0; | |||||
| while (! t.isEmpty()) | |||||
| { | |||||
| if (! t.isWhitespace()) | |||||
| return i; | |||||
| ++t; | |||||
| ++i; | |||||
| } | |||||
| return 0; | |||||
| } | |||||
| } | |||||
| //============================================================================== | //============================================================================== | ||||
| class CodeEditorComponent::GutterComponent : public Component | class CodeEditorComponent::GutterComponent : public Component | ||||
| { | { | ||||
| @@ -640,6 +659,11 @@ CodeDocument::Position CodeEditorComponent::getPositionAt (int x, int y) | |||||
| //============================================================================== | //============================================================================== | ||||
| void CodeEditorComponent::insertTextAtCaret (const String& newText) | void CodeEditorComponent::insertTextAtCaret (const String& newText) | ||||
| { | |||||
| insertText (newText); | |||||
| } | |||||
| void CodeEditorComponent::insertText (const String& newText) | |||||
| { | { | ||||
| document.deleteSection (selectionStart, selectionEnd); | document.deleteSection (selectionStart, selectionEnd); | ||||
| @@ -661,17 +685,89 @@ void CodeEditorComponent::insertTabAtCaret() | |||||
| { | { | ||||
| const int caretCol = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine()); | const int caretCol = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine()); | ||||
| const int spacesNeeded = spacesPerTab - (caretCol % spacesPerTab); | const int spacesNeeded = spacesPerTab - (caretCol % spacesPerTab); | ||||
| insertTextAtCaret (String::repeatedString (" ", spacesNeeded)); | |||||
| insertText (String::repeatedString (" ", spacesNeeded)); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| insertTextAtCaret ("\t"); | |||||
| insertText ("\t"); | |||||
| } | |||||
| } | |||||
| bool CodeEditorComponent::deleteWhitespaceBackwardsToTabStop() | |||||
| { | |||||
| if (! getHighlightedRegion().isEmpty()) | |||||
| return false; | |||||
| for (;;) | |||||
| { | |||||
| const int currentColumn = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine()); | |||||
| if (currentColumn <= 0 || (currentColumn % spacesPerTab) == 0) | |||||
| break; | |||||
| moveCaretLeft (false, true); | |||||
| } | |||||
| const String selected (getTextInRange (getHighlightedRegion())); | |||||
| if (selected.isNotEmpty() && selected.trim().isEmpty()) | |||||
| { | |||||
| cut(); | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| void CodeEditorComponent::indentSelection() { indentSelectedLines ( spacesPerTab); } | |||||
| void CodeEditorComponent::unindentSelection() { indentSelectedLines (-spacesPerTab); } | |||||
| void CodeEditorComponent::indentSelectedLines (const int spacesToAdd) | |||||
| { | |||||
| newTransaction(); | |||||
| CodeDocument::Position oldSelectionStart (selectionStart), oldSelectionEnd (selectionEnd), oldCaret (caretPos); | |||||
| oldSelectionStart.setPositionMaintained (true); | |||||
| oldSelectionEnd.setPositionMaintained (true); | |||||
| oldCaret.setPositionMaintained (true); | |||||
| const int lineStart = selectionStart.getLineNumber(); | |||||
| int lineEnd = selectionEnd.getLineNumber(); | |||||
| if (lineEnd > lineStart && selectionEnd.getIndexInLine() == 0) | |||||
| --lineEnd; | |||||
| for (int line = lineStart; line <= lineEnd; ++line) | |||||
| { | |||||
| const String lineText (document.getLine (line)); | |||||
| const int nonWhitespaceStart = CodeEditorHelpers::findFirstNonWhitespaceChar (lineText); | |||||
| if (nonWhitespaceStart > 0 || lineText.trimStart().isNotEmpty()) | |||||
| { | |||||
| const CodeDocument::Position wsStart (&document, line, 0); | |||||
| const CodeDocument::Position wsEnd (&document, line, nonWhitespaceStart); | |||||
| const int numLeadingSpaces = indexToColumn (line, wsEnd.getIndexInLine()); | |||||
| const int newNumLeadingSpaces = jmax (0, numLeadingSpaces + spacesToAdd); | |||||
| if (newNumLeadingSpaces != numLeadingSpaces) | |||||
| { | |||||
| document.deleteSection (wsStart, wsEnd); | |||||
| document.insertText (wsStart, String::repeatedString (useSpacesForTabs ? " " : "\t", | |||||
| useSpacesForTabs ? newNumLeadingSpaces | |||||
| : (newNumLeadingSpaces / spacesPerTab))); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| selectionStart = oldSelectionStart; | |||||
| selectionEnd = oldSelectionEnd; | |||||
| caretPos = oldCaret; | |||||
| } | } | ||||
| void CodeEditorComponent::cut() | void CodeEditorComponent::cut() | ||||
| { | { | ||||
| insertTextAtCaret (String::empty); | |||||
| insertText (String::empty); | |||||
| } | } | ||||
| bool CodeEditorComponent::copyToClipboard() | bool CodeEditorComponent::copyToClipboard() | ||||
| @@ -700,7 +796,7 @@ bool CodeEditorComponent::pasteFromClipboard() | |||||
| const String clip (SystemClipboard::getTextFromClipboard()); | const String clip (SystemClipboard::getTextFromClipboard()); | ||||
| if (clip.isNotEmpty()) | if (clip.isNotEmpty()) | ||||
| insertTextAtCaret (clip); | |||||
| insertText (clip); | |||||
| newTransaction(); | newTransaction(); | ||||
| return true; | return true; | ||||
| @@ -814,26 +910,6 @@ bool CodeEditorComponent::moveCaretToTop (const bool selecting) | |||||
| return true; | return true; | ||||
| } | } | ||||
| namespace CodeEditorHelpers | |||||
| { | |||||
| static int findFirstNonWhitespaceChar (const String& line) noexcept | |||||
| { | |||||
| String::CharPointerType t (line.getCharPointer()); | |||||
| int i = 0; | |||||
| while (! t.isEmpty()) | |||||
| { | |||||
| if (! t.isWhitespace()) | |||||
| return i; | |||||
| ++t; | |||||
| ++i; | |||||
| } | |||||
| return 0; | |||||
| } | |||||
| } | |||||
| bool CodeEditorComponent::moveCaretToStartOfLine (const bool selecting) | bool CodeEditorComponent::moveCaretToStartOfLine (const bool selecting) | ||||
| { | { | ||||
| newTransaction(); | newTransaction(); | ||||
| @@ -956,21 +1032,28 @@ bool CodeEditorComponent::keyPressed (const KeyPress& key) | |||||
| { | { | ||||
| if (key == KeyPress::tabKey || key.getTextCharacter() == '\t') | if (key == KeyPress::tabKey || key.getTextCharacter() == '\t') | ||||
| { | { | ||||
| insertTabAtCaret(); | |||||
| handleTabKey(); | |||||
| } | } | ||||
| else if (key == KeyPress::returnKey) | else if (key == KeyPress::returnKey) | ||||
| { | { | ||||
| newTransaction(); | |||||
| insertTextAtCaret (document.getNewLineCharacters()); | |||||
| handleReturnKey(); | |||||
| } | } | ||||
| else if (key.isKeyCode (KeyPress::escapeKey)) | else if (key.isKeyCode (KeyPress::escapeKey)) | ||||
| { | { | ||||
| newTransaction(); | |||||
| handleEscapeKey(); | |||||
| } | } | ||||
| else if (key.getTextCharacter() >= ' ') | else if (key.getTextCharacter() >= ' ') | ||||
| { | { | ||||
| insertTextAtCaret (String::charToString (key.getTextCharacter())); | insertTextAtCaret (String::charToString (key.getTextCharacter())); | ||||
| } | } | ||||
| else if (key == KeyPress ('[', ModifierKeys::commandModifier, 0)) | |||||
| { | |||||
| unindentSelection(); | |||||
| } | |||||
| else if (key == KeyPress (']', ModifierKeys::commandModifier, 0)) | |||||
| { | |||||
| indentSelection(); | |||||
| } | |||||
| else | else | ||||
| { | { | ||||
| return false; | return false; | ||||
| @@ -980,26 +1063,90 @@ bool CodeEditorComponent::keyPressed (const KeyPress& key) | |||||
| return true; | return true; | ||||
| } | } | ||||
| void CodeEditorComponent::handleReturnKey() | |||||
| { | |||||
| insertText (document.getNewLineCharacters()); | |||||
| } | |||||
| void CodeEditorComponent::handleTabKey() | |||||
| { | |||||
| insertTabAtCaret(); | |||||
| } | |||||
| void CodeEditorComponent::handleEscapeKey() | |||||
| { | |||||
| newTransaction(); | |||||
| } | |||||
| //============================================================================== | |||||
| void CodeEditorComponent::addPopupMenuItems (PopupMenu& m, const MouseEvent*) | |||||
| { | |||||
| m.addItem (StandardApplicationCommandIDs::cut, TRANS("Cut")); | |||||
| m.addItem (StandardApplicationCommandIDs::copy, TRANS("Copy"), ! getHighlightedRegion().isEmpty()); | |||||
| m.addItem (StandardApplicationCommandIDs::paste, TRANS("Paste")); | |||||
| m.addItem (StandardApplicationCommandIDs::del, TRANS("Delete")); | |||||
| m.addSeparator(); | |||||
| m.addItem (StandardApplicationCommandIDs::selectAll, TRANS("Select All")); | |||||
| m.addSeparator(); | |||||
| m.addItem (StandardApplicationCommandIDs::undo, TRANS("Undo"), document.getUndoManager().canUndo()); | |||||
| m.addItem (StandardApplicationCommandIDs::redo, TRANS("Redo"), document.getUndoManager().canRedo()); | |||||
| } | |||||
| void CodeEditorComponent::performPopupMenuAction (const int menuItemID) | |||||
| { | |||||
| switch (menuItemID) | |||||
| { | |||||
| case StandardApplicationCommandIDs::cut: cutToClipboard(); break; | |||||
| case StandardApplicationCommandIDs::copy: copyToClipboard(); break; | |||||
| case StandardApplicationCommandIDs::paste: pasteFromClipboard(); break; | |||||
| case StandardApplicationCommandIDs::del: cut(); break; | |||||
| case StandardApplicationCommandIDs::selectAll: selectAll(); break; | |||||
| case StandardApplicationCommandIDs::undo: undo(); break; | |||||
| case StandardApplicationCommandIDs::redo: redo(); break; | |||||
| default: break; | |||||
| } | |||||
| } | |||||
| static void codeEditorMenuCallback (int menuResult, CodeEditorComponent* editor) | |||||
| { | |||||
| if (editor != nullptr && menuResult != 0) | |||||
| editor->performPopupMenuAction (menuResult); | |||||
| } | |||||
| //============================================================================== | |||||
| void CodeEditorComponent::mouseDown (const MouseEvent& e) | void CodeEditorComponent::mouseDown (const MouseEvent& e) | ||||
| { | { | ||||
| newTransaction(); | newTransaction(); | ||||
| dragType = notDragging; | dragType = notDragging; | ||||
| if (! e.mods.isPopupMenu()) | |||||
| if (e.mods.isPopupMenu()) | |||||
| { | { | ||||
| beginDragAutoRepeat (100); | |||||
| moveCaretTo (getPositionAt (e.x, e.y), e.mods.isShiftDown()); | |||||
| if (getHighlightedRegion().isEmpty()) | |||||
| { | |||||
| const CodeDocument::Position pos (getPositionAt (e.x, e.y)); | |||||
| const String line (pos.getLineText()); | |||||
| const int index = pos.getIndexInLine(); | |||||
| const int lineLen = line.length(); | |||||
| if (index > 0 && index < lineLen - 2) | |||||
| { | |||||
| moveCaretTo (pos, false); | |||||
| moveCaretLeft (true, false); | |||||
| moveCaretRight (true, true); | |||||
| } | |||||
| } | |||||
| PopupMenu m; | |||||
| m.setLookAndFeel (&getLookAndFeel()); | |||||
| addPopupMenuItems (m, &e); | |||||
| m.showMenuAsync (PopupMenu::Options(), | |||||
| ModalCallbackFunction::forComponent (codeEditorMenuCallback, this)); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| /*PopupMenu m; | |||||
| addPopupMenuItems (m, &e); | |||||
| const int result = m.show(); | |||||
| if (result != 0) | |||||
| performPopupMenuAction (result); | |||||
| */ | |||||
| beginDragAutoRepeat (100); | |||||
| moveCaretTo (getPositionAt (e.x, e.y), e.mods.isShiftDown()); | |||||
| } | } | ||||
| } | } | ||||
| @@ -1147,6 +1294,7 @@ void CodeEditorComponent::ColourScheme::set (const String& name, const Colour& c | |||||
| for (int i = 0; i < types.size(); ++i) | for (int i = 0; i < types.size(); ++i) | ||||
| { | { | ||||
| TokenType& tt = types.getReference(i); | TokenType& tt = types.getReference(i); | ||||
| if (tt.name == name) | if (tt.name == name) | ||||
| { | { | ||||
| tt.colour = colour; | tt.colour = colour; | ||||
| @@ -1191,57 +1339,57 @@ void CodeEditorComponent::updateCachedIterators (int maxLineNum) | |||||
| if (cachedIterators.size() == 0) | if (cachedIterators.size() == 0) | ||||
| cachedIterators.add (new CodeDocument::Iterator (&document)); | cachedIterators.add (new CodeDocument::Iterator (&document)); | ||||
| if (codeTokeniser == nullptr) | |||||
| return; | |||||
| for (;;) | |||||
| if (codeTokeniser != nullptr) | |||||
| { | { | ||||
| CodeDocument::Iterator* last = cachedIterators.getLast(); | |||||
| if (last->getLine() >= maxLineNum) | |||||
| break; | |||||
| CodeDocument::Iterator* t = new CodeDocument::Iterator (*last); | |||||
| cachedIterators.add (t); | |||||
| const int targetLine = last->getLine() + linesBetweenCachedSources; | |||||
| for (;;) | for (;;) | ||||
| { | { | ||||
| codeTokeniser->readNextToken (*t); | |||||
| CodeDocument::Iterator* const last = cachedIterators.getLast(); | |||||
| if (t->getLine() >= targetLine) | |||||
| if (last->getLine() >= maxLineNum) | |||||
| break; | break; | ||||
| if (t->isEOF()) | |||||
| return; | |||||
| CodeDocument::Iterator* t = new CodeDocument::Iterator (*last); | |||||
| cachedIterators.add (t); | |||||
| const int targetLine = last->getLine() + linesBetweenCachedSources; | |||||
| for (;;) | |||||
| { | |||||
| codeTokeniser->readNextToken (*t); | |||||
| if (t->getLine() >= targetLine) | |||||
| break; | |||||
| if (t->isEOF()) | |||||
| return; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| void CodeEditorComponent::getIteratorForPosition (int position, CodeDocument::Iterator& source) | void CodeEditorComponent::getIteratorForPosition (int position, CodeDocument::Iterator& source) | ||||
| { | { | ||||
| if (codeTokeniser == nullptr) | |||||
| return; | |||||
| for (int i = cachedIterators.size(); --i >= 0;) | |||||
| if (codeTokeniser != nullptr) | |||||
| { | { | ||||
| CodeDocument::Iterator* t = cachedIterators.getUnchecked (i); | |||||
| if (t->getPosition() <= position) | |||||
| for (int i = cachedIterators.size(); --i >= 0;) | |||||
| { | { | ||||
| source = *t; | |||||
| break; | |||||
| CodeDocument::Iterator* t = cachedIterators.getUnchecked (i); | |||||
| if (t->getPosition() <= position) | |||||
| { | |||||
| source = *t; | |||||
| break; | |||||
| } | |||||
| } | } | ||||
| } | |||||
| while (source.getPosition() < position) | |||||
| { | |||||
| const CodeDocument::Iterator original (source); | |||||
| codeTokeniser->readNextToken (source); | |||||
| if (source.getPosition() > position || source.isEOF()) | |||||
| while (source.getPosition() < position) | |||||
| { | { | ||||
| source = original; | |||||
| break; | |||||
| const CodeDocument::Iterator original (source); | |||||
| codeTokeniser->readNextToken (source); | |||||
| if (source.getPosition() > position || source.isEOF()) | |||||
| { | |||||
| source = original; | |||||
| break; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -38,16 +38,16 @@ class CodeTokeniser; | |||||
| */ | */ | ||||
| class JUCE_API CodeEditorComponent : public Component, | class JUCE_API CodeEditorComponent : public Component, | ||||
| public TextInputTarget, | public TextInputTarget, | ||||
| public Timer, | |||||
| public ScrollBar::Listener, | |||||
| public CodeDocument::Listener, | |||||
| public AsyncUpdater | |||||
| private Timer, | |||||
| private ScrollBar::Listener, | |||||
| private CodeDocument::Listener, | |||||
| private AsyncUpdater | |||||
| { | { | ||||
| public: | public: | ||||
| //============================================================================== | //============================================================================== | ||||
| /** Creates an editor for a document. | /** Creates an editor for a document. | ||||
| The tokeniser object is optional - pass 0 to disable syntax highlighting. | |||||
| The tokeniser object is optional - pass nullptr to disable syntax highlighting. | |||||
| The object that you pass in is not owned or deleted by the editor - you must | The object that you pass in is not owned or deleted by the editor - you must | ||||
| make sure that it doesn't get deleted while this component is still using it. | make sure that it doesn't get deleted while this component is still using it. | ||||
| @@ -128,6 +128,7 @@ public: | |||||
| bool moveCaretToEndOfLine (bool selecting); | bool moveCaretToEndOfLine (bool selecting); | ||||
| bool deleteBackwards (bool moveInWholeWordSteps); | bool deleteBackwards (bool moveInWholeWordSteps); | ||||
| bool deleteForwards (bool moveInWholeWordSteps); | bool deleteForwards (bool moveInWholeWordSteps); | ||||
| bool deleteWhitespaceBackwardsToTabStop(); | |||||
| bool copyToClipboard(); | bool copyToClipboard(); | ||||
| bool cutToClipboard(); | bool cutToClipboard(); | ||||
| bool pasteFromClipboard(); | bool pasteFromClipboard(); | ||||
| @@ -145,6 +146,9 @@ public: | |||||
| void insertTextAtCaret (const String& textToInsert); | void insertTextAtCaret (const String& textToInsert); | ||||
| void insertTabAtCaret(); | void insertTabAtCaret(); | ||||
| void indentSelection(); | |||||
| void unindentSelection(); | |||||
| //============================================================================== | //============================================================================== | ||||
| Range<int> getHighlightedRegion() const; | Range<int> getHighlightedRegion() const; | ||||
| void setHighlightedRegion (const Range<int>& newRange); | void setHighlightedRegion (const Range<int>& newRange); | ||||
| @@ -230,11 +234,53 @@ public: | |||||
| int getScrollbarThickness() const noexcept { return scrollbarThickness; } | int getScrollbarThickness() const noexcept { return scrollbarThickness; } | ||||
| //============================================================================== | //============================================================================== | ||||
| /** @internal */ | |||||
| void resized(); | |||||
| /** Called when the return key is pressed - this can be overridden for custom behaviour. */ | |||||
| virtual void handleReturnKey(); | |||||
| /** Called when the tab key is pressed - this can be overridden for custom behaviour. */ | |||||
| virtual void handleTabKey(); | |||||
| /** Called when the escape key is pressed - this can be overridden for custom behaviour. */ | |||||
| virtual void handleEscapeKey(); | |||||
| //============================================================================== | |||||
| /** This adds the items to the popup menu. | |||||
| By default it adds the cut/copy/paste items, but you can override this if | |||||
| you need to replace these with your own items. | |||||
| If you want to add your own items to the existing ones, you can override this, | |||||
| call the base class's addPopupMenuItems() method, then append your own items. | |||||
| When the menu has been shown, performPopupMenuAction() will be called to | |||||
| perform the item that the user has chosen. | |||||
| If this was triggered by a mouse-click, the mouseClickEvent parameter will be | |||||
| a pointer to the info about it, or may be null if the menu is being triggered | |||||
| by some other means. | |||||
| @see performPopupMenuAction, setPopupMenuEnabled, isPopupMenuEnabled | |||||
| */ | |||||
| virtual void addPopupMenuItems (PopupMenu& menuToAddTo, | |||||
| const MouseEvent* mouseClickEvent); | |||||
| /** This is called to perform one of the items that was shown on the popup menu. | |||||
| If you've overridden addPopupMenuItems(), you should also override this | |||||
| to perform the actions that you've added. | |||||
| If you've overridden addPopupMenuItems() but have still left the default items | |||||
| on the menu, remember to call the superclass's performPopupMenuAction() | |||||
| so that it can perform the default actions if that's what the user clicked on. | |||||
| @see addPopupMenuItems, setPopupMenuEnabled, isPopupMenuEnabled | |||||
| */ | |||||
| virtual void performPopupMenuAction (int menuItemID); | |||||
| //============================================================================== | |||||
| /** @internal */ | /** @internal */ | ||||
| void paint (Graphics&); | void paint (Graphics&); | ||||
| /** @internal */ | /** @internal */ | ||||
| void resized(); | |||||
| /** @internal */ | |||||
| bool keyPressed (const KeyPress&); | bool keyPressed (const KeyPress&); | ||||
| /** @internal */ | /** @internal */ | ||||
| void mouseDown (const MouseEvent&); | void mouseDown (const MouseEvent&); | ||||
| @@ -251,14 +297,6 @@ public: | |||||
| /** @internal */ | /** @internal */ | ||||
| void focusLost (FocusChangeType); | void focusLost (FocusChangeType); | ||||
| /** @internal */ | /** @internal */ | ||||
| void timerCallback(); | |||||
| /** @internal */ | |||||
| void scrollBarMoved (ScrollBar*, double); | |||||
| /** @internal */ | |||||
| void handleAsyncUpdate(); | |||||
| /** @internal */ | |||||
| void codeDocumentChanged (const CodeDocument::Position&, const CodeDocument::Position&); | |||||
| /** @internal */ | |||||
| bool isTextInputActive() const; | bool isTextInputActive() const; | ||||
| /** @internal */ | /** @internal */ | ||||
| void setTemporaryUnderlining (const Array <Range<int> >&); | void setTemporaryUnderlining (const Array <Range<int> >&); | ||||
| @@ -307,16 +345,24 @@ private: | |||||
| void clearCachedIterators (int firstLineToBeInvalid); | void clearCachedIterators (int firstLineToBeInvalid); | ||||
| void updateCachedIterators (int maxLineNum); | void updateCachedIterators (int maxLineNum); | ||||
| void getIteratorForPosition (int position, CodeDocument::Iterator& result); | void getIteratorForPosition (int position, CodeDocument::Iterator& result); | ||||
| void timerCallback(); | |||||
| void scrollBarMoved (ScrollBar*, double); | |||||
| void handleAsyncUpdate(); | |||||
| void codeDocumentChanged (const CodeDocument::Position&, const CodeDocument::Position&); | |||||
| void moveLineDelta (int delta, bool selecting); | void moveLineDelta (int delta, bool selecting); | ||||
| int getGutterSize() const noexcept; | int getGutterSize() const noexcept; | ||||
| //============================================================================== | //============================================================================== | ||||
| void insertText (const String& textToInsert); | |||||
| void updateCaretPosition(); | void updateCaretPosition(); | ||||
| void updateScrollBars(); | void updateScrollBars(); | ||||
| void scrollToLineInternal (int line); | void scrollToLineInternal (int line); | ||||
| void scrollToColumnInternal (double column); | void scrollToColumnInternal (double column); | ||||
| void newTransaction(); | void newTransaction(); | ||||
| void cut(); | void cut(); | ||||
| void indentSelectedLines (int spacesToAdd); | |||||
| int indexToColumn (int line, int index) const noexcept; | int indexToColumn (int line, int index) const noexcept; | ||||
| int columnToIndex (int line, int column) const noexcept; | int columnToIndex (int line, int column) const noexcept; | ||||