@@ -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; | ||||