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