| @@ -183,8 +183,8 @@ public: | |||
| } | |||
| 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.addCommandItem (commandManager, StandardApplicationCommandIDs::cut); | |||
| menu.addCommandItem (commandManager, StandardApplicationCommandIDs::copy); | |||
| @@ -43,8 +43,6 @@ namespace CommandIDs | |||
| static const int showAppearanceSettings = 0x200077; | |||
| static const int saveAll = 0x200080; | |||
| static const int undo = 0x200090; | |||
| static const int redo = 0x2000a0; | |||
| static const int closeWindow = 0x201001; | |||
| static const int closeAllDocuments = 0x201000; | |||
| @@ -28,7 +28,7 @@ | |||
| #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_, | |||
| CodeDocument& codeDocument, | |||
| CodeTokeniser* const codeTokeniser) | |||
| : DocumentEditorComponent (document_), | |||
| editor (codeDocument, codeTokeniser) | |||
| CodeDocument& codeDocument) | |||
| : DocumentEditorComponent (document_) | |||
| { | |||
| addAndMakeVisible (&editor); | |||
| addAndMakeVisible (editor = createEditor (codeDocument)); | |||
| #if JUCE_MAC | |||
| Font font (13.0f); | |||
| font.setTypefaceName ("Menlo"); | |||
| #else | |||
| Font font (10.0f); | |||
| Font font (12.0f); | |||
| font.setTypefaceName (Font::getDefaultMonospacedFontName()); | |||
| #endif | |||
| editor.setFont (font); | |||
| editor->setFont (font); | |||
| editor.setTabSize (4, true); | |||
| editor->setTabSize (4, true); | |||
| updateColourScheme(); | |||
| getAppSettings().appearance.settings.addListener (this); | |||
| @@ -56,36 +54,25 @@ SourceCodeEditor::~SourceCodeEditor() | |||
| 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::valueTreeChildAdded (ValueTree&, ValueTree&) { updateColourScheme(); } | |||
| void SourceCodeEditor::valueTreeChildRemoved (ValueTree&, ValueTree&) { updateColourScheme(); } | |||
| void SourceCodeEditor::valueTreeChildOrderChanged (ValueTree&) { updateColourScheme(); } | |||
| void SourceCodeEditor::valueTreeParentChanged (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 | |||
| { | |||
| public: | |||
| //============================================================================== | |||
| SourceCodeEditor (OpenDocumentManager::Document* document, | |||
| CodeDocument& codeDocument, | |||
| CodeTokeniser* const codeTokeniser); | |||
| CodeDocument& codeDocument); | |||
| ~SourceCodeEditor(); | |||
| static SourceCodeEditor* createFor (OpenDocumentManager::Document* document, | |||
| CodeDocument& codeDocument); | |||
| static CodeTokeniser* getTokeniserFor (const File& file); | |||
| private: | |||
| ScopedPointer<CodeEditorComponent> editor; | |||
| //============================================================================== | |||
| CodeEditorComponent* createEditor (CodeDocument&); | |||
| void resized(); | |||
| CodeEditorComponent editor; | |||
| private: | |||
| void valueTreePropertyChanged (ValueTree&, const Identifier&); | |||
| void valueTreeChildAdded (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__ | |||
| @@ -77,6 +77,12 @@ namespace StandardApplicationCommandIDs | |||
| /** The command ID that should be used to send a "Deselect all" command. */ | |||
| 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) | |||
| { | |||
| if (editor != nullptr && menuResult != 0) | |||
| editor->performPopupMenuAction (menuResult); | |||
| } | |||
| //============================================================================== | |||
| void TextEditor::mouseDown (const MouseEvent& e) | |||
| { | |||
| beginDragAutoRepeat (100); | |||
| @@ -1788,12 +1827,8 @@ void TextEditor::mouseDown (const MouseEvent& e) | |||
| void TextEditor::mouseDrag (const MouseEvent& e) | |||
| { | |||
| if (wasFocused || ! selectAllTextWhenFocused) | |||
| { | |||
| if (! (popupMenuEnabled && e.mods.isPopupMenu())) | |||
| { | |||
| moveCaretTo (getTextIndexAt (e.x, e.y), true); | |||
| } | |||
| } | |||
| } | |||
| void TextEditor::mouseUp (const MouseEvent& e) | |||
| @@ -1802,12 +1837,8 @@ void TextEditor::mouseUp (const MouseEvent& e) | |||
| textHolder->restartTimer(); | |||
| if (wasFocused || ! selectAllTextWhenFocused) | |||
| { | |||
| if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu())) | |||
| { | |||
| moveCaret (getTextIndexAt (e.x, e.y)); | |||
| } | |||
| } | |||
| wasFocused = true; | |||
| } | |||
| @@ -2094,47 +2125,6 @@ bool TextEditor::keyStateChanged (const bool isKeyDown) | |||
| 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) | |||
| { | |||
| @@ -566,9 +566,8 @@ public: | |||
| When the menu has been shown, performPopupMenuAction() will be called to | |||
| 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 | |||
| a pointer to the info about it, or may be null if the menu is being triggered | |||
| @@ -26,8 +26,7 @@ | |||
| class CodeEditorComponent::CodeEditorLine | |||
| { | |||
| public: | |||
| CodeEditorLine() noexcept | |||
| : highlightColumnStart (0), highlightColumnEnd (0) | |||
| CodeEditorLine() noexcept : highlightColumnStart (0), highlightColumnEnd (0) | |||
| { | |||
| } | |||
| @@ -196,7 +195,7 @@ private: | |||
| for (;;) | |||
| { | |||
| int tabPos = t.text.indexOfChar ('\t'); | |||
| const int tabPos = t.text.indexOfChar ('\t'); | |||
| if (tabPos < 0) | |||
| 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 | |||
| { | |||
| @@ -640,6 +659,11 @@ CodeDocument::Position CodeEditorComponent::getPositionAt (int x, int y) | |||
| //============================================================================== | |||
| void CodeEditorComponent::insertTextAtCaret (const String& newText) | |||
| { | |||
| insertText (newText); | |||
| } | |||
| void CodeEditorComponent::insertText (const String& newText) | |||
| { | |||
| document.deleteSection (selectionStart, selectionEnd); | |||
| @@ -661,17 +685,89 @@ void CodeEditorComponent::insertTabAtCaret() | |||
| { | |||
| const int caretCol = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine()); | |||
| const int spacesNeeded = spacesPerTab - (caretCol % spacesPerTab); | |||
| insertTextAtCaret (String::repeatedString (" ", spacesNeeded)); | |||
| insertText (String::repeatedString (" ", spacesNeeded)); | |||
| } | |||
| 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() | |||
| { | |||
| insertTextAtCaret (String::empty); | |||
| insertText (String::empty); | |||
| } | |||
| bool CodeEditorComponent::copyToClipboard() | |||
| @@ -700,7 +796,7 @@ bool CodeEditorComponent::pasteFromClipboard() | |||
| const String clip (SystemClipboard::getTextFromClipboard()); | |||
| if (clip.isNotEmpty()) | |||
| insertTextAtCaret (clip); | |||
| insertText (clip); | |||
| newTransaction(); | |||
| return true; | |||
| @@ -814,26 +910,6 @@ bool CodeEditorComponent::moveCaretToTop (const bool selecting) | |||
| 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) | |||
| { | |||
| newTransaction(); | |||
| @@ -956,21 +1032,28 @@ bool CodeEditorComponent::keyPressed (const KeyPress& key) | |||
| { | |||
| if (key == KeyPress::tabKey || key.getTextCharacter() == '\t') | |||
| { | |||
| insertTabAtCaret(); | |||
| handleTabKey(); | |||
| } | |||
| else if (key == KeyPress::returnKey) | |||
| { | |||
| newTransaction(); | |||
| insertTextAtCaret (document.getNewLineCharacters()); | |||
| handleReturnKey(); | |||
| } | |||
| else if (key.isKeyCode (KeyPress::escapeKey)) | |||
| { | |||
| newTransaction(); | |||
| handleEscapeKey(); | |||
| } | |||
| else if (key.getTextCharacter() >= ' ') | |||
| { | |||
| insertTextAtCaret (String::charToString (key.getTextCharacter())); | |||
| } | |||
| else if (key == KeyPress ('[', ModifierKeys::commandModifier, 0)) | |||
| { | |||
| unindentSelection(); | |||
| } | |||
| else if (key == KeyPress (']', ModifierKeys::commandModifier, 0)) | |||
| { | |||
| indentSelection(); | |||
| } | |||
| else | |||
| { | |||
| return false; | |||
| @@ -980,26 +1063,90 @@ bool CodeEditorComponent::keyPressed (const KeyPress& key) | |||
| 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) | |||
| { | |||
| newTransaction(); | |||
| 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 | |||
| { | |||
| /*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) | |||
| { | |||
| TokenType& tt = types.getReference(i); | |||
| if (tt.name == name) | |||
| { | |||
| tt.colour = colour; | |||
| @@ -1191,57 +1339,57 @@ void CodeEditorComponent::updateCachedIterators (int maxLineNum) | |||
| if (cachedIterators.size() == 0) | |||
| 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 (;;) | |||
| { | |||
| codeTokeniser->readNextToken (*t); | |||
| CodeDocument::Iterator* const last = cachedIterators.getLast(); | |||
| if (t->getLine() >= targetLine) | |||
| if (last->getLine() >= maxLineNum) | |||
| 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) | |||
| { | |||
| 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, | |||
| public TextInputTarget, | |||
| public Timer, | |||
| public ScrollBar::Listener, | |||
| public CodeDocument::Listener, | |||
| public AsyncUpdater | |||
| private Timer, | |||
| private ScrollBar::Listener, | |||
| private CodeDocument::Listener, | |||
| private AsyncUpdater | |||
| { | |||
| public: | |||
| //============================================================================== | |||
| /** 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 | |||
| 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 deleteBackwards (bool moveInWholeWordSteps); | |||
| bool deleteForwards (bool moveInWholeWordSteps); | |||
| bool deleteWhitespaceBackwardsToTabStop(); | |||
| bool copyToClipboard(); | |||
| bool cutToClipboard(); | |||
| bool pasteFromClipboard(); | |||
| @@ -145,6 +146,9 @@ public: | |||
| void insertTextAtCaret (const String& textToInsert); | |||
| void insertTabAtCaret(); | |||
| void indentSelection(); | |||
| void unindentSelection(); | |||
| //============================================================================== | |||
| Range<int> getHighlightedRegion() const; | |||
| void setHighlightedRegion (const Range<int>& newRange); | |||
| @@ -230,11 +234,53 @@ public: | |||
| 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 */ | |||
| void paint (Graphics&); | |||
| /** @internal */ | |||
| void resized(); | |||
| /** @internal */ | |||
| bool keyPressed (const KeyPress&); | |||
| /** @internal */ | |||
| void mouseDown (const MouseEvent&); | |||
| @@ -251,14 +297,6 @@ public: | |||
| /** @internal */ | |||
| void focusLost (FocusChangeType); | |||
| /** @internal */ | |||
| void timerCallback(); | |||
| /** @internal */ | |||
| void scrollBarMoved (ScrollBar*, double); | |||
| /** @internal */ | |||
| void handleAsyncUpdate(); | |||
| /** @internal */ | |||
| void codeDocumentChanged (const CodeDocument::Position&, const CodeDocument::Position&); | |||
| /** @internal */ | |||
| bool isTextInputActive() const; | |||
| /** @internal */ | |||
| void setTemporaryUnderlining (const Array <Range<int> >&); | |||
| @@ -307,16 +345,24 @@ private: | |||
| void clearCachedIterators (int firstLineToBeInvalid); | |||
| void updateCachedIterators (int maxLineNum); | |||
| 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); | |||
| int getGutterSize() const noexcept; | |||
| //============================================================================== | |||
| void insertText (const String& textToInsert); | |||
| void updateCaretPosition(); | |||
| void updateScrollBars(); | |||
| void scrollToLineInternal (int line); | |||
| void scrollToColumnInternal (double column); | |||
| void newTransaction(); | |||
| void cut(); | |||
| void indentSelectedLines (int spacesToAdd); | |||
| int indexToColumn (int line, int index) const noexcept; | |||
| int columnToIndex (int line, int column) const noexcept; | |||