From c309a5b2445ccabc626ce2784e7bcd2869957a51 Mon Sep 17 00:00:00 2001 From: jules Date: Thu, 21 Sep 2017 19:48:19 +0100 Subject: [PATCH] Avoided forcing TextEditor to be always opaque --- .../widgets/juce_TextEditor.cpp | 5040 ++++++++--------- .../juce_gui_basics/widgets/juce_TextEditor.h | 1548 ++--- 2 files changed, 3288 insertions(+), 3300 deletions(-) diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp index 3e9ee97234..f3490c35b7 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp @@ -1,2526 +1,2514 @@ -/* - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2017 - ROLI Ltd. - - JUCE is an open source library subject to commercial or open-source - licensing. - - By using JUCE, you agree to the terms of both the JUCE 5 End-User License - Agreement and JUCE 5 Privacy Policy (both updated and effective as of the - 27th April 2017). - - End User License Agreement: www.juce.com/juce-5-licence - Privacy Policy: www.juce.com/juce-5-privacy-policy - - Or: You may also use this code under the terms of the GPL v3 (see - www.gnu.org/licenses). - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace juce -{ - -// a word or space that can't be broken down any further -struct TextAtom -{ - //============================================================================== - String atomText; - float width; - int numChars; - - //============================================================================== - bool isWhitespace() const noexcept { return CharacterFunctions::isWhitespace (atomText[0]); } - bool isNewLine() const noexcept { return atomText[0] == '\r' || atomText[0] == '\n'; } - - String getText (const juce_wchar passwordCharacter) const - { - if (passwordCharacter == 0) - return atomText; - - return String::repeatedString (String::charToString (passwordCharacter), - atomText.length()); - } - - String getTrimmedText (const juce_wchar passwordCharacter) const - { - if (passwordCharacter == 0) - return atomText.substring (0, numChars); - - if (isNewLine()) - return {}; - - return String::repeatedString (String::charToString (passwordCharacter), numChars); - } - - JUCE_LEAK_DETECTOR (TextAtom) -}; - -//============================================================================== -// a run of text with a single font and colour -class TextEditor::UniformTextSection -{ -public: - UniformTextSection (const String& text, const Font& f, Colour col, juce_wchar passwordChar) - : font (f), colour (col) - { - initialiseAtoms (text, passwordChar); - } - - UniformTextSection (const UniformTextSection& other) - : font (other.font), colour (other.colour) - { - atoms.addCopiesOf (other.atoms); - } - - void append (UniformTextSection& other, const juce_wchar passwordChar) - { - if (other.atoms.size() > 0) - { - int i = 0; - - if (auto* lastAtom = atoms.getLast()) - { - if (! CharacterFunctions::isWhitespace (lastAtom->atomText.getLastCharacter())) - { - auto* first = other.atoms.getUnchecked(0); - - if (! CharacterFunctions::isWhitespace (first->atomText[0])) - { - lastAtom->atomText += first->atomText; - lastAtom->numChars = (uint16) (lastAtom->numChars + first->numChars); - lastAtom->width = font.getStringWidthFloat (lastAtom->getText (passwordChar)); - delete first; - ++i; - } - } - } - - atoms.ensureStorageAllocated (atoms.size() + other.atoms.size() - i); - - while (i < other.atoms.size()) - { - atoms.add (other.atoms.getUnchecked(i)); - ++i; - } - - other.atoms.clear (false); - } - } - - UniformTextSection* split (const int indexToBreakAt, const juce_wchar passwordChar) - { - UniformTextSection* const section2 = new UniformTextSection (String(), font, colour, passwordChar); - int index = 0; - - for (int i = 0; i < atoms.size(); ++i) - { - auto* atom = atoms.getUnchecked(i); - auto nextIndex = index + atom->numChars; - - if (index == indexToBreakAt) - { - for (int j = i; j < atoms.size(); ++j) - section2->atoms.add (atoms.getUnchecked (j)); - - atoms.removeRange (i, atoms.size(), false); - break; - } - else if (indexToBreakAt >= index && indexToBreakAt < nextIndex) - { - auto* secondAtom = new TextAtom(); - - secondAtom->atomText = atom->atomText.substring (indexToBreakAt - index); - secondAtom->width = font.getStringWidthFloat (secondAtom->getText (passwordChar)); - secondAtom->numChars = (uint16) secondAtom->atomText.length(); - - section2->atoms.add (secondAtom); - - atom->atomText = atom->atomText.substring (0, indexToBreakAt - index); - atom->width = font.getStringWidthFloat (atom->getText (passwordChar)); - atom->numChars = (uint16) (indexToBreakAt - index); - - for (int j = i + 1; j < atoms.size(); ++j) - section2->atoms.add (atoms.getUnchecked (j)); - - atoms.removeRange (i + 1, atoms.size(), false); - break; - } - - index = nextIndex; - } - - return section2; - } - - void appendAllText (MemoryOutputStream& mo) const - { - for (int i = 0; i < atoms.size(); ++i) - mo << atoms.getUnchecked(i)->atomText; - } - - void appendSubstring (MemoryOutputStream& mo, const Range range) const - { - int index = 0; - - for (auto* atom : atoms) - { - auto nextIndex = index + atom->numChars; - - if (range.getStart() < nextIndex) - { - if (range.getEnd() <= index) - break; - - auto r = (range - index).getIntersectionWith (Range (0, (int) atom->numChars)); - - if (! r.isEmpty()) - mo << atom->atomText.substring (r.getStart(), r.getEnd()); - } - - index = nextIndex; - } - } - - int getTotalLength() const noexcept - { - int total = 0; - - for (auto* atom : atoms) - total += atom->numChars; - - return total; - } - - void setFont (const Font& newFont, const juce_wchar passwordChar) - { - if (font != newFont) - { - font = newFont; - - for (auto* atom : atoms) - atom->width = newFont.getStringWidthFloat (atom->getText (passwordChar)); - } - } - - //============================================================================== - Font font; - Colour colour; - OwnedArray atoms; - -private: - void initialiseAtoms (const String& textToParse, const juce_wchar passwordChar) - { - auto text = textToParse.getCharPointer(); - - while (! text.isEmpty()) - { - size_t numChars = 0; - auto start = text; - - // create a whitespace atom unless it starts with non-ws - if (text.isWhitespace() && *text != '\r' && *text != '\n') - { - do - { - ++text; - ++numChars; - } - while (text.isWhitespace() && *text != '\r' && *text != '\n'); - } - else - { - if (*text == '\r') - { - ++text; - ++numChars; - - if (*text == '\n') - { - ++start; - ++text; - } - } - else if (*text == '\n') - { - ++text; - ++numChars; - } - else - { - while (! (text.isEmpty() || text.isWhitespace())) - { - ++text; - ++numChars; - } - } - } - - auto* atom = atoms.add (new TextAtom()); - - atom->atomText = String (start, numChars); - atom->width = font.getStringWidthFloat (atom->getText (passwordChar)); - atom->numChars = (uint16) numChars; - } - } - - UniformTextSection& operator= (const UniformTextSection&); - JUCE_LEAK_DETECTOR (UniformTextSection) -}; - -//============================================================================== -class TextEditor::Iterator -{ -public: - Iterator (const OwnedArray& sectionList, - float wrapWidth, juce_wchar passwordChar, float spacing) - : sections (sectionList), - wordWrapWidth (wrapWidth), - passwordCharacter (passwordChar), - lineSpacing (spacing) - { - jassert (wordWrapWidth > 0); - - if (sections.size() > 0) - { - currentSection = sections.getUnchecked (sectionIndex); - - if (currentSection != nullptr) - beginNewLine(); - } - } - - Iterator (const Iterator& other) - : indexInText (other.indexInText), - lineY (other.lineY), - lineHeight (other.lineHeight), - maxDescent (other.maxDescent), - atomX (other.atomX), - atomRight (other.atomRight), - atom (other.atom), - currentSection (other.currentSection), - sections (other.sections), - sectionIndex (other.sectionIndex), - atomIndex (other.atomIndex), - wordWrapWidth (other.wordWrapWidth), - passwordCharacter (other.passwordCharacter), - lineSpacing (other.lineSpacing), - tempAtom (other.tempAtom) - { - } - - //============================================================================== - bool next() - { - if (atom == &tempAtom) - { - const int numRemaining = tempAtom.atomText.length() - tempAtom.numChars; - - if (numRemaining > 0) - { - tempAtom.atomText = tempAtom.atomText.substring (tempAtom.numChars); - - atomX = 0; - - if (tempAtom.numChars > 0) - lineY += lineHeight * lineSpacing; - - indexInText += tempAtom.numChars; - - GlyphArrangement g; - g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f); - - int split; - for (split = 0; split < g.getNumGlyphs(); ++split) - if (shouldWrap (g.getGlyph (split).getRight())) - break; - - if (split > 0 && split <= numRemaining) - { - tempAtom.numChars = (uint16) split; - tempAtom.width = g.getGlyph (split - 1).getRight(); - atomRight = atomX + tempAtom.width; - return true; - } - } - } - - if (sectionIndex >= sections.size()) - { - moveToEndOfLastAtom(); - return false; - } - - bool forceNewLine = false; - - if (atomIndex >= currentSection->atoms.size() - 1) - { - if (atomIndex >= currentSection->atoms.size()) - { - if (++sectionIndex >= sections.size()) - { - moveToEndOfLastAtom(); - return false; - } - - atomIndex = 0; - currentSection = sections.getUnchecked (sectionIndex); - } - else - { - auto* lastAtom = currentSection->atoms.getUnchecked (atomIndex); - - if (! lastAtom->isWhitespace()) - { - // handle the case where the last atom in a section is actually part of the same - // word as the first atom of the next section... - float right = atomRight + lastAtom->width; - float lineHeight2 = lineHeight; - float maxDescent2 = maxDescent; - - for (int section = sectionIndex + 1; section < sections.size(); ++section) - { - auto* s = sections.getUnchecked (section); - - if (s->atoms.size() == 0) - break; - - auto* nextAtom = s->atoms.getUnchecked (0); - - if (nextAtom->isWhitespace()) - break; - - right += nextAtom->width; - - lineHeight2 = jmax (lineHeight2, s->font.getHeight()); - maxDescent2 = jmax (maxDescent2, s->font.getDescent()); - - if (shouldWrap (right)) - { - lineHeight = lineHeight2; - maxDescent = maxDescent2; - - forceNewLine = true; - break; - } - - if (s->atoms.size() > 1) - break; - } - } - } - } - - if (atom != nullptr) - { - atomX = atomRight; - indexInText += atom->numChars; - - if (atom->isNewLine()) - beginNewLine(); - } - - atom = currentSection->atoms.getUnchecked (atomIndex); - atomRight = atomX + atom->width; - ++atomIndex; - - if (shouldWrap (atomRight) || forceNewLine) - { - if (atom->isWhitespace()) - { - // leave whitespace at the end of a line, but truncate it to avoid scrolling - atomRight = jmin (atomRight, wordWrapWidth); - } - else - { - atomRight = atom->width; - - if (shouldWrap (atomRight)) // atom too big to fit on a line, so break it up.. - { - tempAtom = *atom; - tempAtom.width = 0; - tempAtom.numChars = 0; - atom = &tempAtom; - - if (atomX > 0) - beginNewLine(); - - return next(); - } - - beginNewLine(); - return true; - } - } - - return true; - } - - void beginNewLine() - { - atomX = 0; - lineY += lineHeight * lineSpacing; - - int tempSectionIndex = sectionIndex; - int tempAtomIndex = atomIndex; - auto* section = sections.getUnchecked (tempSectionIndex); - - lineHeight = section->font.getHeight(); - maxDescent = section->font.getDescent(); - - float x = (atom != nullptr) ? atom->width : 0; - - while (! shouldWrap (x)) - { - if (tempSectionIndex >= sections.size()) - break; - - bool checkSize = false; - - if (tempAtomIndex >= section->atoms.size()) - { - if (++tempSectionIndex >= sections.size()) - break; - - tempAtomIndex = 0; - section = sections.getUnchecked (tempSectionIndex); - checkSize = true; - } - - auto* nextAtom = section->atoms.getUnchecked (tempAtomIndex); - - if (nextAtom == nullptr) - break; - - x += nextAtom->width; - - if (shouldWrap (x) || nextAtom->isNewLine()) - break; - - if (checkSize) - { - lineHeight = jmax (lineHeight, section->font.getHeight()); - maxDescent = jmax (maxDescent, section->font.getDescent()); - } - - ++tempAtomIndex; - } - } - - //============================================================================== - void draw (Graphics& g, const UniformTextSection*& lastSection) const - { - if (passwordCharacter != 0 || ! atom->isWhitespace()) - { - if (lastSection != currentSection) - { - lastSection = currentSection; - g.setColour (currentSection->colour); - g.setFont (currentSection->font); - } - - jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty()); - - GlyphArrangement ga; - ga.addLineOfText (currentSection->font, - atom->getTrimmedText (passwordCharacter), - atomX, (float) roundToInt (lineY + lineHeight - maxDescent)); - ga.draw (g); - } - } - - void addSelection (RectangleList& area, const Range selected) const - { - const float startX = indexToX (selected.getStart()); - const float endX = indexToX (selected.getEnd()); - - area.add (startX, lineY, endX - startX, lineHeight * lineSpacing); - } - - void drawUnderline (Graphics& g, const Range underline, const Colour colour) const - { - const int startX = roundToInt (indexToX (underline.getStart())); - const int endX = roundToInt (indexToX (underline.getEnd())); - const int baselineY = roundToInt (lineY + currentSection->font.getAscent() + 0.5f); - - Graphics::ScopedSaveState state (g); - g.reduceClipRegion (Rectangle (startX, baselineY, endX - startX, 1)); - g.fillCheckerBoard (Rectangle (endX, baselineY + 1), 3, 1, colour, Colours::transparentBlack); - } - - void drawSelectedText (Graphics& g, - const Range selected, - const Colour selectedTextColour) const - { - if (passwordCharacter != 0 || ! atom->isWhitespace()) - { - GlyphArrangement ga; - ga.addLineOfText (currentSection->font, - atom->getTrimmedText (passwordCharacter), - atomX, (float) roundToInt (lineY + lineHeight - maxDescent)); - - if (selected.getEnd() < indexInText + atom->numChars) - { - GlyphArrangement ga2 (ga); - ga2.removeRangeOfGlyphs (0, selected.getEnd() - indexInText); - ga.removeRangeOfGlyphs (selected.getEnd() - indexInText, -1); - - g.setColour (currentSection->colour); - ga2.draw (g); - } - - if (selected.getStart() > indexInText) - { - GlyphArrangement ga2 (ga); - ga2.removeRangeOfGlyphs (selected.getStart() - indexInText, -1); - ga.removeRangeOfGlyphs (0, selected.getStart() - indexInText); - - g.setColour (currentSection->colour); - ga2.draw (g); - } - - g.setColour (selectedTextColour); - ga.draw (g); - } - } - - //============================================================================== - float indexToX (const int indexToFind) const - { - if (indexToFind <= indexInText) - return atomX; - - if (indexToFind >= indexInText + atom->numChars) - return atomRight; - - GlyphArrangement g; - g.addLineOfText (currentSection->font, - atom->getText (passwordCharacter), - atomX, 0.0f); - - if (indexToFind - indexInText >= g.getNumGlyphs()) - return atomRight; - - return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft()); - } - - int xToIndex (const float xToFind) const - { - if (xToFind <= atomX || atom->isNewLine()) - return indexInText; - - if (xToFind >= atomRight) - return indexInText + atom->numChars; - - GlyphArrangement g; - g.addLineOfText (currentSection->font, - atom->getText (passwordCharacter), - atomX, 0.0f); - - const int numGlyphs = g.getNumGlyphs(); - - int j; - for (j = 0; j < numGlyphs; ++j) - { - auto& pg = g.getGlyph(j); - - if ((pg.getLeft() + pg.getRight()) / 2 > xToFind) - break; - } - - return indexInText + j; - } - - //============================================================================== - bool getCharPosition (const int index, float& cx, float& cy, float& lineHeightFound) - { - while (next()) - { - if (indexInText + atom->numChars > index) - { - cx = indexToX (index); - cy = lineY; - lineHeightFound = lineHeight; - return true; - } - } - - cx = atomX; - cy = lineY; - lineHeightFound = lineHeight; - return false; - } - - //============================================================================== - int indexInText = 0; - float lineY = 0, lineHeight = 0, maxDescent = 0; - float atomX = 0, atomRight = 0; - const TextAtom* atom = nullptr; - const UniformTextSection* currentSection = nullptr; - -private: - const OwnedArray& sections; - int sectionIndex = 0, atomIndex = 0; - const float wordWrapWidth; - const juce_wchar passwordCharacter; - const float lineSpacing; - TextAtom tempAtom; - - Iterator& operator= (const Iterator&) = delete; - - void moveToEndOfLastAtom() - { - if (atom != nullptr) - { - atomX = atomRight; - - if (atom->isNewLine()) - { - atomX = 0.0f; - lineY += lineHeight * lineSpacing; - } - } - } - - bool shouldWrap (const float x) const noexcept - { - return (x - 0.0001f) >= wordWrapWidth; - } - - JUCE_LEAK_DETECTOR (Iterator) -}; - - -//============================================================================== -class TextEditor::InsertAction : public UndoableAction -{ -public: - InsertAction (TextEditor& ed, - const String& newText, - const int insertPos, - const Font& newFont, - const Colour newColour, - const int oldCaret, - const int newCaret) - : owner (ed), - text (newText), - insertIndex (insertPos), - oldCaretPos (oldCaret), - newCaretPos (newCaret), - font (newFont), - colour (newColour) - { - } - - bool perform() override - { - owner.insert (text, insertIndex, font, colour, 0, newCaretPos); - return true; - } - - bool undo() override - { - owner.remove (Range (insertIndex, insertIndex + text.length()), 0, oldCaretPos); - return true; - } - - int getSizeInUnits() override - { - return text.length() + 16; - } - -private: - TextEditor& owner; - const String text; - const int insertIndex, oldCaretPos, newCaretPos; - const Font font; - const Colour colour; - - JUCE_DECLARE_NON_COPYABLE (InsertAction) -}; - -//============================================================================== -class TextEditor::RemoveAction : public UndoableAction -{ -public: - RemoveAction (TextEditor& ed, - const Range rangeToRemove, - const int oldCaret, - const int newCaret, - const Array& oldSections) - : owner (ed), - range (rangeToRemove), - oldCaretPos (oldCaret), - newCaretPos (newCaret) - { - removedSections.addArray (oldSections); - } - - bool perform() override - { - owner.remove (range, 0, newCaretPos); - return true; - } - - bool undo() override - { - owner.reinsert (range.getStart(), removedSections); - owner.moveCaretTo (oldCaretPos, false); - return true; - } - - int getSizeInUnits() override - { - int n = 16; - - for (auto* s : removedSections) - n += s->getTotalLength(); - - return n; - } - -private: - TextEditor& owner; - const Range range; - const int oldCaretPos, newCaretPos; - OwnedArray removedSections; - - JUCE_DECLARE_NON_COPYABLE (RemoveAction) -}; - -//============================================================================== -class TextEditor::TextHolderComponent : public Component, - public Timer, - public Value::Listener -{ -public: - TextHolderComponent (TextEditor& ed) : owner (ed) - { - setWantsKeyboardFocus (false); - setInterceptsMouseClicks (false, true); - setMouseCursor (MouseCursor::ParentCursor); - - owner.getTextValue().addListener (this); - } - - ~TextHolderComponent() - { - owner.getTextValue().removeListener (this); - } - - void paint (Graphics& g) override - { - owner.drawContent (g); - } - - void restartTimer() - { - startTimer (350); - } - - void timerCallback() override - { - owner.timerCallbackInt(); - } - - void valueChanged (Value&) override - { - owner.textWasChangedByValue(); - } - -private: - TextEditor& owner; - - JUCE_DECLARE_NON_COPYABLE (TextHolderComponent) -}; - -//============================================================================== -class TextEditorViewport : public Viewport -{ -public: - TextEditorViewport (TextEditor& ed) : owner (ed) {} - - void visibleAreaChanged (const Rectangle&) override - { - if (! rentrant) // it's rare, but possible to get into a feedback loop as the viewport's scrollbars - // appear and disappear, causing the wrap width to change. - { - const float wordWrapWidth = owner.getWordWrapWidth(); - - if (wordWrapWidth != lastWordWrapWidth) - { - lastWordWrapWidth = wordWrapWidth; - - rentrant = true; - owner.updateTextHolderSize(); - rentrant = false; - } - } - } - -private: - TextEditor& owner; - float lastWordWrapWidth = 0; - bool rentrant = false; - - JUCE_DECLARE_NON_COPYABLE (TextEditorViewport) -}; - -//============================================================================== -namespace TextEditorDefs -{ - const int textChangeMessageId = 0x10003001; - const int returnKeyMessageId = 0x10003002; - const int escapeKeyMessageId = 0x10003003; - const int focusLossMessageId = 0x10003004; - - const int maxActionsPerTransaction = 100; - - static int getCharacterCategory (const juce_wchar character) - { - return CharacterFunctions::isLetterOrDigit (character) - ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1); - } -} - -//============================================================================== -TextEditor::TextEditor (const String& name, - const juce_wchar passwordChar) - : Component (name), - passwordCharacter (passwordChar), - keyboardType (TextInputTarget::textKeyboard), - dragType (notDragging) -{ - setOpaque (true); - setMouseCursor (MouseCursor::IBeamCursor); - - addAndMakeVisible (viewport = new TextEditorViewport (*this)); - viewport->setViewedComponent (textHolder = new TextHolderComponent (*this)); - viewport->setWantsKeyboardFocus (false); - viewport->setScrollBarsShown (false, false); - - setWantsKeyboardFocus (true); - recreateCaret(); -} - -TextEditor::~TextEditor() -{ - if (wasFocused) - if (auto* peer = getPeer()) - peer->dismissPendingTextInput(); - - textValue.removeListener (textHolder); - textValue.referTo (Value()); - - viewport = nullptr; - textHolder = nullptr; -} - -//============================================================================== -void TextEditor::newTransaction() -{ - lastTransactionTime = Time::getApproximateMillisecondCounter(); - undoManager.beginNewTransaction(); -} - -bool TextEditor::undoOrRedo (const bool shouldUndo) -{ - if (! isReadOnly()) - { - newTransaction(); - - if (shouldUndo ? undoManager.undo() - : undoManager.redo()) - { - scrollToMakeSureCursorIsVisible(); - repaint(); - textChanged(); - return true; - } - } - - return false; -} - -bool TextEditor::undo() { return undoOrRedo (true); } -bool TextEditor::redo() { return undoOrRedo (false); } - -//============================================================================== -void TextEditor::setMultiLine (const bool shouldBeMultiLine, - const bool shouldWordWrap) -{ - if (multiline != shouldBeMultiLine - || wordWrap != (shouldWordWrap && shouldBeMultiLine)) - { - multiline = shouldBeMultiLine; - wordWrap = shouldWordWrap && shouldBeMultiLine; - - viewport->setScrollBarsShown (scrollbarVisible && multiline, - scrollbarVisible && multiline); - viewport->setViewPosition (0, 0); - resized(); - scrollToMakeSureCursorIsVisible(); - } -} - -bool TextEditor::isMultiLine() const -{ - return multiline; -} - -void TextEditor::setScrollbarsShown (bool shown) -{ - if (scrollbarVisible != shown) - { - scrollbarVisible = shown; - shown = shown && isMultiLine(); - viewport->setScrollBarsShown (shown, shown); - } -} - -void TextEditor::setReadOnly (const bool shouldBeReadOnly) -{ - if (readOnly != shouldBeReadOnly) - { - readOnly = shouldBeReadOnly; - enablementChanged(); - } -} - -bool TextEditor::isReadOnly() const noexcept -{ - return readOnly || ! isEnabled(); -} - -bool TextEditor::isTextInputActive() const -{ - return ! isReadOnly(); -} - -void TextEditor::setReturnKeyStartsNewLine (const bool shouldStartNewLine) -{ - returnKeyStartsNewLine = shouldStartNewLine; -} - -void TextEditor::setTabKeyUsedAsCharacter (const bool shouldTabKeyBeUsed) -{ - tabKeyUsed = shouldTabKeyBeUsed; -} - -void TextEditor::setPopupMenuEnabled (const bool b) -{ - popupMenuEnabled = b; -} - -void TextEditor::setSelectAllWhenFocused (const bool b) -{ - selectAllTextWhenFocused = b; -} - -//============================================================================== -void TextEditor::setFont (const Font& newFont) -{ - currentFont = newFont; - scrollToMakeSureCursorIsVisible(); -} - -void TextEditor::applyFontToAllText (const Font& newFont, bool changeCurrentFont) -{ - if (changeCurrentFont) - currentFont = newFont; - - auto overallColour = findColour (textColourId); - - for (auto* uts : sections) - { - uts->setFont (newFont, passwordCharacter); - uts->colour = overallColour; - } - - coalesceSimilarSections(); - updateTextHolderSize(); - scrollToMakeSureCursorIsVisible(); - repaint(); -} - -void TextEditor::applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour) -{ - for (auto* uts : sections) - uts->colour = newColour; - - if (changeCurrentTextColour) - setColour (TextEditor::textColourId, newColour); - else - repaint(); -} - -void TextEditor::colourChanged() -{ - setOpaque (findColour (backgroundColourId).isOpaque()); - repaint(); -} - -void TextEditor::lookAndFeelChanged() -{ - colourChanged(); - - caret = nullptr; - recreateCaret(); - repaint(); -} - -void TextEditor::parentHierarchyChanged() -{ - lookAndFeelChanged(); -} - -void TextEditor::enablementChanged() -{ - recreateCaret(); - repaint(); -} - -void TextEditor::setCaretVisible (const bool shouldCaretBeVisible) -{ - if (caretVisible != shouldCaretBeVisible) - { - caretVisible = shouldCaretBeVisible; - recreateCaret(); - } -} - -void TextEditor::recreateCaret() -{ - if (isCaretVisible()) - { - if (caret == nullptr) - { - textHolder->addChildComponent (caret = getLookAndFeel().createCaretComponent (this)); - updateCaretPosition(); - } - } - else - { - caret = nullptr; - } -} - -void TextEditor::updateCaretPosition() -{ - if (caret != nullptr) - caret->setCaretPosition (getCaretRectangle().translated (leftIndent, topIndent)); -} - -TextEditor::LengthAndCharacterRestriction::LengthAndCharacterRestriction (int maxLen, const String& chars) - : allowedCharacters (chars), maxLength (maxLen) -{} - -String TextEditor::LengthAndCharacterRestriction::filterNewText (TextEditor& ed, const String& newInput) -{ - String t (newInput); - - if (allowedCharacters.isNotEmpty()) - t = t.retainCharacters (allowedCharacters); - - if (maxLength > 0) - t = t.substring (0, maxLength - (ed.getTotalNumChars() - ed.getHighlightedRegion().getLength())); - - return t; -} - -void TextEditor::setInputFilter (InputFilter* newFilter, bool takeOwnership) -{ - inputFilter.set (newFilter, takeOwnership); -} - -void TextEditor::setInputRestrictions (const int maxLen, const String& chars) -{ - setInputFilter (new LengthAndCharacterRestriction (maxLen, chars), true); -} - -void TextEditor::setTextToShowWhenEmpty (const String& text, Colour colourToUse) -{ - textToShowWhenEmpty = text; - colourForTextWhenEmpty = colourToUse; -} - -void TextEditor::setPasswordCharacter (const juce_wchar newPasswordCharacter) -{ - if (passwordCharacter != newPasswordCharacter) - { - passwordCharacter = newPasswordCharacter; - applyFontToAllText (currentFont); - } -} - -void TextEditor::setScrollBarThickness (const int newThicknessPixels) -{ - viewport->setScrollBarThickness (newThicknessPixels); -} - -//============================================================================== -void TextEditor::clear() -{ - clearInternal (nullptr); - updateTextHolderSize(); - undoManager.clearUndoHistory(); -} - -void TextEditor::setText (const String& newText, - const bool sendTextChangeMessage) -{ - const int newLength = newText.length(); - - if (newLength != getTotalNumChars() || getText() != newText) - { - if (! sendTextChangeMessage) - textValue.removeListener (textHolder); - - textValue = newText; - - auto oldCursorPos = caretPosition; - const bool cursorWasAtEnd = oldCursorPos >= getTotalNumChars(); - - clearInternal (nullptr); - insert (newText, 0, currentFont, findColour (textColourId), 0, caretPosition); - - // if you're adding text with line-feeds to a single-line text editor, it - // ain't gonna look right! - jassert (multiline || ! newText.containsAnyOf ("\r\n")); - - if (cursorWasAtEnd && ! isMultiLine()) - oldCursorPos = getTotalNumChars(); - - moveCaretTo (oldCursorPos, false); - - if (sendTextChangeMessage) - textChanged(); - else - textValue.addListener (textHolder); - - updateTextHolderSize(); - scrollToMakeSureCursorIsVisible(); - undoManager.clearUndoHistory(); - - repaint(); - } -} - -//============================================================================== -void TextEditor::updateValueFromText() -{ - if (valueTextNeedsUpdating) - { - valueTextNeedsUpdating = false; - textValue = getText(); - } -} - -Value& TextEditor::getTextValue() -{ - updateValueFromText(); - return textValue; -} - -void TextEditor::textWasChangedByValue() -{ - if (textValue.getValueSource().getReferenceCount() > 1) - setText (textValue.getValue()); -} - -//============================================================================== -void TextEditor::textChanged() -{ - updateTextHolderSize(); - - if (listeners.size() > 0) - postCommandMessage (TextEditorDefs::textChangeMessageId); - - if (textValue.getValueSource().getReferenceCount() > 1) - { - valueTextNeedsUpdating = false; - textValue = getText(); - } -} - -void TextEditor::returnPressed() { postCommandMessage (TextEditorDefs::returnKeyMessageId); } -void TextEditor::escapePressed() { postCommandMessage (TextEditorDefs::escapeKeyMessageId); } - -void TextEditor::addListener (Listener* l) { listeners.add (l); } -void TextEditor::removeListener (Listener* l) { listeners.remove (l); } - -//============================================================================== -void TextEditor::timerCallbackInt() -{ - if (hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent()) - wasFocused = true; - - auto now = Time::getApproximateMillisecondCounter(); - - if (now > lastTransactionTime + 200) - newTransaction(); -} - -void TextEditor::repaintText (const Range range) -{ - if (! range.isEmpty()) - { - float x = 0, y = 0, lh = currentFont.getHeight(); - - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0) - { - Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); - - i.getCharPosition (range.getStart(), x, y, lh); - - auto y1 = (int) y; - int y2; - - if (range.getEnd() >= getTotalNumChars()) - { - y2 = textHolder->getHeight(); - } - else - { - i.getCharPosition (range.getEnd(), x, y, lh); - y2 = (int) (y + lh * 2.0f); - } - - textHolder->repaint (0, y1, textHolder->getWidth(), y2 - y1); - } - } -} - -//============================================================================== -void TextEditor::moveCaret (int newCaretPos) -{ - if (newCaretPos < 0) - newCaretPos = 0; - else - newCaretPos = jmin (newCaretPos, getTotalNumChars()); - - if (newCaretPos != getCaretPosition()) - { - caretPosition = newCaretPos; - textHolder->restartTimer(); - scrollToMakeSureCursorIsVisible(); - updateCaretPosition(); - } -} - -int TextEditor::getCaretPosition() const -{ - return caretPosition; -} - -void TextEditor::setCaretPosition (const int newIndex) -{ - moveCaretTo (newIndex, false); -} - -void TextEditor::moveCaretToEnd() -{ - moveCaretTo (std::numeric_limits::max(), false); -} - -void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX, - const int desiredCaretY) - -{ - updateCaretPosition(); - auto caretPos = getCaretRectangle(); - - int vx = caretPos.getX() - desiredCaretX; - int vy = caretPos.getY() - desiredCaretY; - - if (desiredCaretX < jmax (1, proportionOfWidth (0.05f))) - vx += desiredCaretX - proportionOfWidth (0.2f); - else if (desiredCaretX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10))) - vx += desiredCaretX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth(); - - vx = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vx); - - if (! isMultiLine()) - { - vy = viewport->getViewPositionY(); - } - else - { - vy = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vy); - - if (desiredCaretY < 0) - vy = jmax (0, desiredCaretY + vy); - else if (desiredCaretY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretPos.getHeight())) - vy += desiredCaretY + 2 + caretPos.getHeight() + topIndent - viewport->getMaximumVisibleHeight(); - } - - viewport->setViewPosition (vx, vy); -} - -Rectangle TextEditor::getCaretRectangle() -{ - float cursorX, cursorY; - float cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value) - getCharPosition (caretPosition, cursorX, cursorY, cursorHeight); - - return { roundToInt (cursorX), roundToInt (cursorY), 2, roundToInt (cursorHeight) }; -} - -//============================================================================== -enum { rightEdgeSpace = 2 }; - -float TextEditor::getWordWrapWidth() const -{ - return wordWrap ? (float) (viewport->getMaximumVisibleWidth() - (leftIndent + rightEdgeSpace + 1)) - : std::numeric_limits::max(); -} - -void TextEditor::updateTextHolderSize() -{ - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0) - { - float maxWidth = 0.0f; - - Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); - - while (i.next()) - maxWidth = jmax (maxWidth, i.atomRight); - - const int w = leftIndent + roundToInt (maxWidth); - const int h = topIndent + roundToInt (jmax (i.lineY + i.lineHeight, - currentFont.getHeight())); - - textHolder->setSize (w + rightEdgeSpace, h + 1); // (allows a bit of space for the cursor to be at the right-hand-edge) - } -} - -int TextEditor::getTextWidth() const { return textHolder->getWidth(); } -int TextEditor::getTextHeight() const { return textHolder->getHeight(); } - -void TextEditor::setIndents (const int newLeftIndent, const int newTopIndent) -{ - leftIndent = newLeftIndent; - topIndent = newTopIndent; -} - -void TextEditor::setBorder (const BorderSize& border) -{ - borderSize = border; - resized(); -} - -BorderSize TextEditor::getBorder() const -{ - return borderSize; -} - -void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor) -{ - keepCaretOnScreen = shouldScrollToShowCursor; -} - -void TextEditor::scrollToMakeSureCursorIsVisible() -{ - updateCaretPosition(); - - if (keepCaretOnScreen) - { - auto viewPos = viewport->getViewPosition(); - auto caretRect = getCaretRectangle(); - auto relativeCursor = caretRect.getPosition() - viewPos; - - if (relativeCursor.x < jmax (1, proportionOfWidth (0.05f))) - { - viewPos.x += relativeCursor.x - proportionOfWidth (0.2f); - } - else if (relativeCursor.x > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10))) - { - viewPos.x += relativeCursor.x + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth(); - } - - viewPos.x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), viewPos.x); - - if (! isMultiLine()) - { - viewPos.y = (getHeight() - textHolder->getHeight() - topIndent) / -2; - } - else if (relativeCursor.y < 0) - { - viewPos.y = jmax (0, relativeCursor.y + viewPos.y); - } - else if (relativeCursor.y > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretRect.getHeight())) - { - viewPos.y += relativeCursor.y + 2 + caretRect.getHeight() + topIndent - viewport->getMaximumVisibleHeight(); - } - - viewport->setViewPosition (viewPos); - } -} - -void TextEditor::moveCaretTo (const int newPosition, const bool isSelecting) -{ - if (isSelecting) - { - moveCaret (newPosition); - - auto oldSelection = selection; - - if (dragType == notDragging) - { - if (std::abs (getCaretPosition() - selection.getStart()) < std::abs (getCaretPosition() - selection.getEnd())) - dragType = draggingSelectionStart; - else - dragType = draggingSelectionEnd; - } - - if (dragType == draggingSelectionStart) - { - if (getCaretPosition() >= selection.getEnd()) - dragType = draggingSelectionEnd; - - selection = Range::between (getCaretPosition(), selection.getEnd()); - } - else - { - if (getCaretPosition() < selection.getStart()) - dragType = draggingSelectionStart; - - selection = Range::between (getCaretPosition(), selection.getStart()); - } - - repaintText (selection.getUnionWith (oldSelection)); - } - else - { - dragType = notDragging; - - repaintText (selection); - - moveCaret (newPosition); - selection = Range::emptyRange (getCaretPosition()); - } -} - -int TextEditor::getTextIndexAt (const int x, const int y) -{ - return indexAtPosition ((float) (x + viewport->getViewPositionX() - leftIndent - borderSize.getLeft()), - (float) (y + viewport->getViewPositionY() - topIndent - borderSize.getTop())); -} - -void TextEditor::insertTextAtCaret (const String& t) -{ - String newText (inputFilter != nullptr ? inputFilter->filterNewText (*this, t) : t); - - if (isMultiLine()) - newText = newText.replace ("\r\n", "\n"); - else - newText = newText.replaceCharacters ("\r\n", " "); - - const int insertIndex = selection.getStart(); - const int newCaretPos = insertIndex + newText.length(); - - remove (selection, getUndoManager(), - newText.isNotEmpty() ? newCaretPos - 1 : newCaretPos); - - insert (newText, insertIndex, currentFont, findColour (textColourId), - getUndoManager(), newCaretPos); - - textChanged(); -} - -void TextEditor::setHighlightedRegion (const Range& newSelection) -{ - moveCaretTo (newSelection.getStart(), false); - moveCaretTo (newSelection.getEnd(), true); -} - -//============================================================================== -void TextEditor::copy() -{ - if (passwordCharacter == 0) - { - auto selectedText = getHighlightedText(); - - if (selectedText.isNotEmpty()) - SystemClipboard::copyTextToClipboard (selectedText); - } -} - -void TextEditor::paste() -{ - if (! isReadOnly()) - { - auto clip = SystemClipboard::getTextFromClipboard(); - - if (clip.isNotEmpty()) - insertTextAtCaret (clip); - } -} - -void TextEditor::cut() -{ - if (! isReadOnly()) - { - moveCaret (selection.getEnd()); - insertTextAtCaret (String()); - } -} - -//============================================================================== -void TextEditor::drawContent (Graphics& g) -{ - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0) - { - g.setOrigin (leftIndent, topIndent); - auto clip = g.getClipBounds(); - Colour selectedTextColour; - - Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); - - if (! selection.isEmpty()) - { - Iterator i2 (i); - RectangleList selectionArea; - - while (i2.next() && i2.lineY < clip.getBottom()) - { - if (i2.lineY + i2.lineHeight >= clip.getY() - && selection.intersects (Range (i2.indexInText, i2.indexInText + i2.atom->numChars))) - { - i2.addSelection (selectionArea, selection); - } - } - - g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f)); - g.fillRectList (selectionArea); - - selectedTextColour = findColour (highlightedTextColourId); - } - - const UniformTextSection* lastSection = nullptr; - - while (i.next() && i.lineY < clip.getBottom()) - { - if (i.lineY + i.lineHeight >= clip.getY()) - { - if (selection.intersects (Range (i.indexInText, i.indexInText + i.atom->numChars))) - { - i.drawSelectedText (g, selection, selectedTextColour); - lastSection = nullptr; - } - else - { - i.draw (g, lastSection); - } - } - } - - for (int j = underlinedSections.size(); --j >= 0;) - { - const Range underlinedSection = underlinedSections.getReference (j); - - Iterator i2 (sections, wordWrapWidth, passwordCharacter, lineSpacing); - - while (i2.next() && i2.lineY < clip.getBottom()) - { - if (i2.lineY + i2.lineHeight >= clip.getY() - && underlinedSection.intersects (Range (i2.indexInText, i2.indexInText + i2.atom->numChars))) - { - i2.drawUnderline (g, underlinedSection, findColour (textColourId)); - } - } - } - } -} - -void TextEditor::paint (Graphics& g) -{ - getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this); -} - -void TextEditor::paintOverChildren (Graphics& g) -{ - if (textToShowWhenEmpty.isNotEmpty() - && (! hasKeyboardFocus (false)) - && getTotalNumChars() == 0) - { - g.setColour (colourForTextWhenEmpty); - g.setFont (getFont()); - - if (isMultiLine()) - g.drawText (textToShowWhenEmpty, getLocalBounds(), - Justification::centred, true); - else - g.drawText (textToShowWhenEmpty, - leftIndent, 0, viewport->getWidth() - leftIndent, getHeight(), - Justification::centredLeft, true); - } - - getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this); -} - -//============================================================================== -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); - newTransaction(); - - if (wasFocused || ! selectAllTextWhenFocused) - { - if (! (popupMenuEnabled && e.mods.isPopupMenu())) - { - moveCaretTo (getTextIndexAt (e.x, e.y), - e.mods.isShiftDown()); - } - else - { - PopupMenu m; - m.setLookAndFeel (&getLookAndFeel()); - addPopupMenuItems (m, &e); - - m.showMenuAsync (PopupMenu::Options(), - ModalCallbackFunction::forComponent (textEditorMenuCallback, this)); - } - } -} - -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) -{ - newTransaction(); - textHolder->restartTimer(); - - if (wasFocused || ! selectAllTextWhenFocused) - if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu())) - moveCaret (getTextIndexAt (e.x, e.y)); - - wasFocused = true; -} - -void TextEditor::mouseDoubleClick (const MouseEvent& e) -{ - int tokenEnd = getTextIndexAt (e.x, e.y); - int tokenStart = 0; - - if (e.getNumberOfClicks() > 3) - { - tokenEnd = getTotalNumChars(); - } - else - { - auto t = getText(); - auto totalLength = getTotalNumChars(); - - while (tokenEnd < totalLength) - { - // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale) - const juce_wchar c = t [tokenEnd]; - if (CharacterFunctions::isLetterOrDigit (c) || c > 128) - ++tokenEnd; - else - break; - } - - tokenStart = tokenEnd; - - while (tokenStart > 0) - { - // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale) - const juce_wchar c = t [tokenStart - 1]; - if (CharacterFunctions::isLetterOrDigit (c) || c > 128) - --tokenStart; - else - break; - } - - if (e.getNumberOfClicks() > 2) - { - while (tokenEnd < totalLength) - { - const juce_wchar c = t [tokenEnd]; - if (c != '\r' && c != '\n') - ++tokenEnd; - else - break; - } - - while (tokenStart > 0) - { - const juce_wchar c = t [tokenStart - 1]; - if (c != '\r' && c != '\n') - --tokenStart; - else - break; - } - } - } - - moveCaretTo (tokenEnd, false); - moveCaretTo (tokenStart, true); -} - -void TextEditor::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel) -{ - if (! viewport->useMouseWheelMoveIfNeeded (e, wheel)) - Component::mouseWheelMove (e, wheel); -} - -//============================================================================== -bool TextEditor::moveCaretWithTransaction (const int newPos, const bool selecting) -{ - newTransaction(); - moveCaretTo (newPos, selecting); - return true; -} - -bool TextEditor::moveCaretLeft (bool moveInWholeWordSteps, bool selecting) -{ - int pos = getCaretPosition(); - - if (moveInWholeWordSteps) - pos = findWordBreakBefore (pos); - else - --pos; - - return moveCaretWithTransaction (pos, selecting); -} - -bool TextEditor::moveCaretRight (bool moveInWholeWordSteps, bool selecting) -{ - int pos = getCaretPosition(); - - if (moveInWholeWordSteps) - pos = findWordBreakAfter (pos); - else - ++pos; - - return moveCaretWithTransaction (pos, selecting); -} - -bool TextEditor::moveCaretUp (bool selecting) -{ - if (! isMultiLine()) - return moveCaretToStartOfLine (selecting); - - auto caretPos = getCaretRectangle().toFloat(); - return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - 1.0f), selecting); -} - -bool TextEditor::moveCaretDown (bool selecting) -{ - if (! isMultiLine()) - return moveCaretToEndOfLine (selecting); - - auto caretPos = getCaretRectangle().toFloat(); - return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting); -} - -bool TextEditor::pageUp (bool selecting) -{ - if (! isMultiLine()) - return moveCaretToStartOfLine (selecting); - - auto caretPos = getCaretRectangle().toFloat(); - return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - viewport->getViewHeight()), selecting); -} - -bool TextEditor::pageDown (bool selecting) -{ - if (! isMultiLine()) - return moveCaretToEndOfLine (selecting); - - auto caretPos = getCaretRectangle().toFloat(); - return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + viewport->getViewHeight()), selecting); -} - -void TextEditor::scrollByLines (int deltaLines) -{ - if (auto* scrollbar = viewport->getVerticalScrollBar()) - scrollbar->moveScrollbarInSteps (deltaLines); -} - -bool TextEditor::scrollDown() -{ - scrollByLines (-1); - return true; -} - -bool TextEditor::scrollUp() -{ - scrollByLines (1); - return true; -} - -bool TextEditor::moveCaretToTop (bool selecting) -{ - return moveCaretWithTransaction (0, selecting); -} - -bool TextEditor::moveCaretToStartOfLine (bool selecting) -{ - auto caretPos = getCaretRectangle().toFloat(); - return moveCaretWithTransaction (indexAtPosition (0.0f, caretPos.getY()), selecting); -} - -bool TextEditor::moveCaretToEnd (bool selecting) -{ - return moveCaretWithTransaction (getTotalNumChars(), selecting); -} - -bool TextEditor::moveCaretToEndOfLine (bool selecting) -{ - auto caretPos = getCaretRectangle().toFloat(); - return moveCaretWithTransaction (indexAtPosition ((float) textHolder->getWidth(), caretPos.getY()), selecting); -} - -bool TextEditor::deleteBackwards (bool moveInWholeWordSteps) -{ - if (moveInWholeWordSteps) - moveCaretTo (findWordBreakBefore (getCaretPosition()), true); - else if (selection.isEmpty() && selection.getStart() > 0) - selection = Range (selection.getEnd() - 1, selection.getEnd()); - - cut(); - return true; -} - -bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/) -{ - if (selection.isEmpty() && selection.getStart() < getTotalNumChars()) - selection = Range (selection.getStart(), selection.getStart() + 1); - - cut(); - return true; -} - -bool TextEditor::copyToClipboard() -{ - newTransaction(); - copy(); - return true; -} - -bool TextEditor::cutToClipboard() -{ - newTransaction(); - copy(); - cut(); - return true; -} - -bool TextEditor::pasteFromClipboard() -{ - newTransaction(); - paste(); - return true; -} - -bool TextEditor::selectAll() -{ - newTransaction(); - moveCaretTo (getTotalNumChars(), false); - moveCaretTo (0, true); - return true; -} - -//============================================================================== -void TextEditor::setEscapeAndReturnKeysConsumed (bool shouldBeConsumed) noexcept -{ - consumeEscAndReturnKeys = shouldBeConsumed; -} - -bool TextEditor::keyPressed (const KeyPress& key) -{ - if (isReadOnly() && key != KeyPress ('c', ModifierKeys::commandModifier, 0) - && key != KeyPress ('a', ModifierKeys::commandModifier, 0)) - return false; - - if (! TextEditorKeyMapper::invokeKeyFunction (*this, key)) - { - if (key == KeyPress::returnKey) - { - newTransaction(); - - if (returnKeyStartsNewLine) - insertTextAtCaret ("\n"); - else - { - returnPressed(); - return consumeEscAndReturnKeys; - } - } - else if (key.isKeyCode (KeyPress::escapeKey)) - { - newTransaction(); - moveCaretTo (getCaretPosition(), false); - escapePressed(); - return consumeEscAndReturnKeys; - } - else if (key.getTextCharacter() >= ' ' - || (tabKeyUsed && (key.getTextCharacter() == '\t'))) - { - insertTextAtCaret (String::charToString (key.getTextCharacter())); - - lastTransactionTime = Time::getApproximateMillisecondCounter(); - } - else - { - return false; - } - } - - return true; -} - -bool TextEditor::keyStateChanged (const bool isKeyDown) -{ - if (! isKeyDown) - return false; - - #if JUCE_WINDOWS - if (KeyPress (KeyPress::F4Key, ModifierKeys::altModifier, 0).isCurrentlyDown()) - return false; // We need to explicitly allow alt-F4 to pass through on Windows - #endif - - if ((! consumeEscAndReturnKeys) - && (KeyPress (KeyPress::escapeKey).isCurrentlyDown() - || KeyPress (KeyPress::returnKey).isCurrentlyDown())) - return false; - - // (overridden to avoid forwarding key events to the parent) - return ! ModifierKeys::getCurrentModifiers().isCommandDown(); -} - -//============================================================================== -void TextEditor::focusGained (FocusChangeType) -{ - newTransaction(); - - if (selectAllTextWhenFocused) - { - moveCaretTo (0, false); - moveCaretTo (getTotalNumChars(), true); - } - - repaint(); - updateCaretPosition(); - - if (auto* peer = getPeer()) - if (! isReadOnly()) - peer->textInputRequired (peer->globalToLocal (getScreenPosition()), *this); -} - -void TextEditor::focusLost (FocusChangeType) -{ - newTransaction(); - - wasFocused = false; - textHolder->stopTimer(); - - underlinedSections.clear(); - - if (auto* peer = getPeer()) - peer->dismissPendingTextInput(); - - updateCaretPosition(); - - postCommandMessage (TextEditorDefs::focusLossMessageId); - repaint(); -} - -//============================================================================== -void TextEditor::resized() -{ - viewport->setBoundsInset (borderSize); - viewport->setSingleStepSizes (16, roundToInt (currentFont.getHeight())); - - updateTextHolderSize(); - - if (isMultiLine()) - updateCaretPosition(); - else - scrollToMakeSureCursorIsVisible(); -} - -void TextEditor::handleCommandMessage (const int commandId) -{ - Component::BailOutChecker checker (this); - - switch (commandId) - { - case TextEditorDefs::textChangeMessageId: - listeners.callChecked (checker, &Listener::textEditorTextChanged, (TextEditor&) *this); - break; - - case TextEditorDefs::returnKeyMessageId: - listeners.callChecked (checker, &Listener::textEditorReturnKeyPressed, (TextEditor&) *this); - break; - - case TextEditorDefs::escapeKeyMessageId: - listeners.callChecked (checker, &Listener::textEditorEscapeKeyPressed, (TextEditor&) *this); - break; - - case TextEditorDefs::focusLossMessageId: - updateValueFromText(); - listeners.callChecked (checker, &Listener::textEditorFocusLost, (TextEditor&) *this); - break; - - default: - jassertfalse; - break; - } -} - -void TextEditor::setTemporaryUnderlining (const Array >& newUnderlinedSections) -{ - underlinedSections = newUnderlinedSections; - repaint(); -} - -//============================================================================== -UndoManager* TextEditor::getUndoManager() noexcept -{ - return readOnly ? nullptr : &undoManager; -} - -void TextEditor::clearInternal (UndoManager* const um) -{ - remove (Range (0, getTotalNumChars()), um, caretPosition); -} - -void TextEditor::insert (const String& text, - const int insertIndex, - const Font& font, - const Colour colour, - UndoManager* const um, - const int caretPositionToMoveTo) -{ - if (text.isNotEmpty()) - { - if (um != nullptr) - { - if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction) - newTransaction(); - - um->perform (new InsertAction (*this, text, insertIndex, font, colour, - caretPosition, caretPositionToMoveTo)); - } - else - { - repaintText (Range (insertIndex, getTotalNumChars())); // must do this before and after changing the data, in case - // a line gets moved due to word wrap - - int index = 0; - int nextIndex = 0; - - for (int i = 0; i < sections.size(); ++i) - { - nextIndex = index + sections.getUnchecked (i)->getTotalLength(); - - if (insertIndex == index) - { - sections.insert (i, new UniformTextSection (text, font, colour, passwordCharacter)); - break; - } - else if (insertIndex > index && insertIndex < nextIndex) - { - splitSection (i, insertIndex - index); - sections.insert (i + 1, new UniformTextSection (text, font, colour, passwordCharacter)); - break; - } - - index = nextIndex; - } - - if (nextIndex == insertIndex) - sections.add (new UniformTextSection (text, font, colour, passwordCharacter)); - - coalesceSimilarSections(); - totalNumChars = -1; - valueTextNeedsUpdating = true; - - updateTextHolderSize(); - moveCaretTo (caretPositionToMoveTo, false); - - repaintText (Range (insertIndex, getTotalNumChars())); - } - } -} - -void TextEditor::reinsert (const int insertIndex, const OwnedArray& sectionsToInsert) -{ - int index = 0; - int nextIndex = 0; - - for (int i = 0; i < sections.size(); ++i) - { - nextIndex = index + sections.getUnchecked (i)->getTotalLength(); - - if (insertIndex == index) - { - for (int j = sectionsToInsert.size(); --j >= 0;) - sections.insert (i, new UniformTextSection (*sectionsToInsert.getUnchecked(j))); - - break; - } - else if (insertIndex > index && insertIndex < nextIndex) - { - splitSection (i, insertIndex - index); - - for (int j = sectionsToInsert.size(); --j >= 0;) - sections.insert (i + 1, new UniformTextSection (*sectionsToInsert.getUnchecked(j))); - - break; - } - - index = nextIndex; - } - - if (nextIndex == insertIndex) - { - for (int j = 0; j < sectionsToInsert.size(); ++j) - sections.add (new UniformTextSection (*sectionsToInsert.getUnchecked(j))); - } - - coalesceSimilarSections(); - totalNumChars = -1; - valueTextNeedsUpdating = true; -} - -void TextEditor::remove (Range range, UndoManager* const um, const int caretPositionToMoveTo) -{ - if (! range.isEmpty()) - { - int index = 0; - - for (int i = 0; i < sections.size(); ++i) - { - const int nextIndex = index + sections.getUnchecked(i)->getTotalLength(); - - if (range.getStart() > index && range.getStart() < nextIndex) - { - splitSection (i, range.getStart() - index); - --i; - } - else if (range.getEnd() > index && range.getEnd() < nextIndex) - { - splitSection (i, range.getEnd() - index); - --i; - } - else - { - index = nextIndex; - - if (index > range.getEnd()) - break; - } - } - - index = 0; - - if (um != nullptr) - { - Array removedSections; - - for (int i = 0; i < sections.size(); ++i) - { - if (range.getEnd() <= range.getStart()) - break; - - auto* section = sections.getUnchecked (i); - auto nextIndex = index + section->getTotalLength(); - - if (range.getStart() <= index && range.getEnd() >= nextIndex) - removedSections.add (new UniformTextSection (*section)); - - index = nextIndex; - } - - if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction) - newTransaction(); - - um->perform (new RemoveAction (*this, range, caretPosition, - caretPositionToMoveTo, removedSections)); - } - else - { - auto remainingRange = range; - - for (int i = 0; i < sections.size(); ++i) - { - auto* section = sections.getUnchecked (i); - - const int nextIndex = index + section->getTotalLength(); - - if (remainingRange.getStart() <= index && remainingRange.getEnd() >= nextIndex) - { - sections.remove (i); - remainingRange.setEnd (remainingRange.getEnd() - (nextIndex - index)); - - if (remainingRange.isEmpty()) - break; - - --i; - } - else - { - index = nextIndex; - } - } - - coalesceSimilarSections(); - totalNumChars = -1; - valueTextNeedsUpdating = true; - - moveCaretTo (caretPositionToMoveTo, false); - - repaintText (Range (range.getStart(), getTotalNumChars())); - } - } -} - -//============================================================================== -String TextEditor::getText() const -{ - MemoryOutputStream mo; - mo.preallocate ((size_t) getTotalNumChars()); - - for (int i = 0; i < sections.size(); ++i) - sections.getUnchecked (i)->appendAllText (mo); - - return mo.toUTF8(); -} - -String TextEditor::getTextInRange (const Range& range) const -{ - if (range.isEmpty()) - return {}; - - MemoryOutputStream mo; - mo.preallocate ((size_t) jmin (getTotalNumChars(), range.getLength())); - - int index = 0; - - for (int i = 0; i < sections.size(); ++i) - { - auto* s = sections.getUnchecked (i); - auto nextIndex = index + s->getTotalLength(); - - if (range.getStart() < nextIndex) - { - if (range.getEnd() <= index) - break; - - s->appendSubstring (mo, range - index); - } - - index = nextIndex; - } - - return mo.toUTF8(); -} - -String TextEditor::getHighlightedText() const -{ - return getTextInRange (selection); -} - -int TextEditor::getTotalNumChars() const -{ - if (totalNumChars < 0) - { - totalNumChars = 0; - - for (int i = sections.size(); --i >= 0;) - totalNumChars += sections.getUnchecked (i)->getTotalLength(); - } - - return totalNumChars; -} - -bool TextEditor::isEmpty() const -{ - return getTotalNumChars() == 0; -} - -void TextEditor::getCharPosition (const int index, float& cx, float& cy, float& lineHeight) const -{ - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0 && sections.size() > 0) - { - Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); - - i.getCharPosition (index, cx, cy, lineHeight); - } - else - { - cx = cy = 0; - lineHeight = currentFont.getHeight(); - } -} - -int TextEditor::indexAtPosition (const float x, const float y) -{ - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0) - { - Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); - - while (i.next()) - { - if (i.lineY + i.lineHeight > y) - { - if (i.lineY > y) - return jmax (0, i.indexInText - 1); - - if (i.atomX >= x) - return i.indexInText; - - if (x < i.atomRight) - return i.xToIndex (x); - } - } - } - - return getTotalNumChars(); -} - -//============================================================================== -int TextEditor::findWordBreakAfter (const int position) const -{ - auto t = getTextInRange (Range (position, position + 512)); - auto totalLength = t.length(); - int i = 0; - - while (i < totalLength && CharacterFunctions::isWhitespace (t[i])) - ++i; - - const int type = TextEditorDefs::getCharacterCategory (t[i]); - - while (i < totalLength && type == TextEditorDefs::getCharacterCategory (t[i])) - ++i; - - while (i < totalLength && CharacterFunctions::isWhitespace (t[i])) - ++i; - - return position + i; -} - -int TextEditor::findWordBreakBefore (const int position) const -{ - if (position <= 0) - return 0; - - auto startOfBuffer = jmax (0, position - 512); - auto t = getTextInRange (Range (startOfBuffer, position)); - - int i = position - startOfBuffer; - - while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1])) - --i; - - if (i > 0) - { - auto type = TextEditorDefs::getCharacterCategory (t [i - 1]); - - while (i > 0 && type == TextEditorDefs::getCharacterCategory (t [i - 1])) - --i; - } - - jassert (startOfBuffer + i >= 0); - return startOfBuffer + i; -} - - -//============================================================================== -void TextEditor::splitSection (const int sectionIndex, const int charToSplitAt) -{ - jassert (sections[sectionIndex] != nullptr); - - sections.insert (sectionIndex + 1, - sections.getUnchecked (sectionIndex)->split (charToSplitAt, passwordCharacter)); -} - -void TextEditor::coalesceSimilarSections() -{ - for (int i = 0; i < sections.size() - 1; ++i) - { - auto* s1 = sections.getUnchecked (i); - auto* s2 = sections.getUnchecked (i + 1); - - if (s1->font == s2->font - && s1->colour == s2->colour) - { - s1->append (*s2, passwordCharacter); - sections.remove (i + 1); - --i; - } - } -} - -} // namespace juce +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +// a word or space that can't be broken down any further +struct TextAtom +{ + //============================================================================== + String atomText; + float width; + int numChars; + + //============================================================================== + bool isWhitespace() const noexcept { return CharacterFunctions::isWhitespace (atomText[0]); } + bool isNewLine() const noexcept { return atomText[0] == '\r' || atomText[0] == '\n'; } + + String getText (const juce_wchar passwordCharacter) const + { + if (passwordCharacter == 0) + return atomText; + + return String::repeatedString (String::charToString (passwordCharacter), + atomText.length()); + } + + String getTrimmedText (const juce_wchar passwordCharacter) const + { + if (passwordCharacter == 0) + return atomText.substring (0, numChars); + + if (isNewLine()) + return {}; + + return String::repeatedString (String::charToString (passwordCharacter), numChars); + } + + JUCE_LEAK_DETECTOR (TextAtom) +}; + +//============================================================================== +// a run of text with a single font and colour +class TextEditor::UniformTextSection +{ +public: + UniformTextSection (const String& text, const Font& f, Colour col, juce_wchar passwordChar) + : font (f), colour (col) + { + initialiseAtoms (text, passwordChar); + } + + UniformTextSection (const UniformTextSection& other) + : font (other.font), colour (other.colour) + { + atoms.addCopiesOf (other.atoms); + } + + void append (UniformTextSection& other, const juce_wchar passwordChar) + { + if (other.atoms.size() > 0) + { + int i = 0; + + if (auto* lastAtom = atoms.getLast()) + { + if (! CharacterFunctions::isWhitespace (lastAtom->atomText.getLastCharacter())) + { + auto* first = other.atoms.getUnchecked(0); + + if (! CharacterFunctions::isWhitespace (first->atomText[0])) + { + lastAtom->atomText += first->atomText; + lastAtom->numChars = (uint16) (lastAtom->numChars + first->numChars); + lastAtom->width = font.getStringWidthFloat (lastAtom->getText (passwordChar)); + delete first; + ++i; + } + } + } + + atoms.ensureStorageAllocated (atoms.size() + other.atoms.size() - i); + + while (i < other.atoms.size()) + { + atoms.add (other.atoms.getUnchecked(i)); + ++i; + } + + other.atoms.clear (false); + } + } + + UniformTextSection* split (const int indexToBreakAt, const juce_wchar passwordChar) + { + UniformTextSection* const section2 = new UniformTextSection (String(), font, colour, passwordChar); + int index = 0; + + for (int i = 0; i < atoms.size(); ++i) + { + auto* atom = atoms.getUnchecked(i); + auto nextIndex = index + atom->numChars; + + if (index == indexToBreakAt) + { + for (int j = i; j < atoms.size(); ++j) + section2->atoms.add (atoms.getUnchecked (j)); + + atoms.removeRange (i, atoms.size(), false); + break; + } + else if (indexToBreakAt >= index && indexToBreakAt < nextIndex) + { + auto* secondAtom = new TextAtom(); + + secondAtom->atomText = atom->atomText.substring (indexToBreakAt - index); + secondAtom->width = font.getStringWidthFloat (secondAtom->getText (passwordChar)); + secondAtom->numChars = (uint16) secondAtom->atomText.length(); + + section2->atoms.add (secondAtom); + + atom->atomText = atom->atomText.substring (0, indexToBreakAt - index); + atom->width = font.getStringWidthFloat (atom->getText (passwordChar)); + atom->numChars = (uint16) (indexToBreakAt - index); + + for (int j = i + 1; j < atoms.size(); ++j) + section2->atoms.add (atoms.getUnchecked (j)); + + atoms.removeRange (i + 1, atoms.size(), false); + break; + } + + index = nextIndex; + } + + return section2; + } + + void appendAllText (MemoryOutputStream& mo) const + { + for (int i = 0; i < atoms.size(); ++i) + mo << atoms.getUnchecked(i)->atomText; + } + + void appendSubstring (MemoryOutputStream& mo, const Range range) const + { + int index = 0; + + for (auto* atom : atoms) + { + auto nextIndex = index + atom->numChars; + + if (range.getStart() < nextIndex) + { + if (range.getEnd() <= index) + break; + + auto r = (range - index).getIntersectionWith (Range (0, (int) atom->numChars)); + + if (! r.isEmpty()) + mo << atom->atomText.substring (r.getStart(), r.getEnd()); + } + + index = nextIndex; + } + } + + int getTotalLength() const noexcept + { + int total = 0; + + for (auto* atom : atoms) + total += atom->numChars; + + return total; + } + + void setFont (const Font& newFont, const juce_wchar passwordChar) + { + if (font != newFont) + { + font = newFont; + + for (auto* atom : atoms) + atom->width = newFont.getStringWidthFloat (atom->getText (passwordChar)); + } + } + + //============================================================================== + Font font; + Colour colour; + OwnedArray atoms; + +private: + void initialiseAtoms (const String& textToParse, const juce_wchar passwordChar) + { + auto text = textToParse.getCharPointer(); + + while (! text.isEmpty()) + { + size_t numChars = 0; + auto start = text; + + // create a whitespace atom unless it starts with non-ws + if (text.isWhitespace() && *text != '\r' && *text != '\n') + { + do + { + ++text; + ++numChars; + } + while (text.isWhitespace() && *text != '\r' && *text != '\n'); + } + else + { + if (*text == '\r') + { + ++text; + ++numChars; + + if (*text == '\n') + { + ++start; + ++text; + } + } + else if (*text == '\n') + { + ++text; + ++numChars; + } + else + { + while (! (text.isEmpty() || text.isWhitespace())) + { + ++text; + ++numChars; + } + } + } + + auto* atom = atoms.add (new TextAtom()); + + atom->atomText = String (start, numChars); + atom->width = font.getStringWidthFloat (atom->getText (passwordChar)); + atom->numChars = (uint16) numChars; + } + } + + UniformTextSection& operator= (const UniformTextSection&); + JUCE_LEAK_DETECTOR (UniformTextSection) +}; + +//============================================================================== +class TextEditor::Iterator +{ +public: + Iterator (const OwnedArray& sectionList, + float wrapWidth, juce_wchar passwordChar, float spacing) + : sections (sectionList), + wordWrapWidth (wrapWidth), + passwordCharacter (passwordChar), + lineSpacing (spacing) + { + jassert (wordWrapWidth > 0); + + if (sections.size() > 0) + { + currentSection = sections.getUnchecked (sectionIndex); + + if (currentSection != nullptr) + beginNewLine(); + } + } + + Iterator (const Iterator& other) + : indexInText (other.indexInText), + lineY (other.lineY), + lineHeight (other.lineHeight), + maxDescent (other.maxDescent), + atomX (other.atomX), + atomRight (other.atomRight), + atom (other.atom), + currentSection (other.currentSection), + sections (other.sections), + sectionIndex (other.sectionIndex), + atomIndex (other.atomIndex), + wordWrapWidth (other.wordWrapWidth), + passwordCharacter (other.passwordCharacter), + lineSpacing (other.lineSpacing), + tempAtom (other.tempAtom) + { + } + + //============================================================================== + bool next() + { + if (atom == &tempAtom) + { + const int numRemaining = tempAtom.atomText.length() - tempAtom.numChars; + + if (numRemaining > 0) + { + tempAtom.atomText = tempAtom.atomText.substring (tempAtom.numChars); + + atomX = 0; + + if (tempAtom.numChars > 0) + lineY += lineHeight * lineSpacing; + + indexInText += tempAtom.numChars; + + GlyphArrangement g; + g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f); + + int split; + for (split = 0; split < g.getNumGlyphs(); ++split) + if (shouldWrap (g.getGlyph (split).getRight())) + break; + + if (split > 0 && split <= numRemaining) + { + tempAtom.numChars = (uint16) split; + tempAtom.width = g.getGlyph (split - 1).getRight(); + atomRight = atomX + tempAtom.width; + return true; + } + } + } + + if (sectionIndex >= sections.size()) + { + moveToEndOfLastAtom(); + return false; + } + + bool forceNewLine = false; + + if (atomIndex >= currentSection->atoms.size() - 1) + { + if (atomIndex >= currentSection->atoms.size()) + { + if (++sectionIndex >= sections.size()) + { + moveToEndOfLastAtom(); + return false; + } + + atomIndex = 0; + currentSection = sections.getUnchecked (sectionIndex); + } + else + { + auto* lastAtom = currentSection->atoms.getUnchecked (atomIndex); + + if (! lastAtom->isWhitespace()) + { + // handle the case where the last atom in a section is actually part of the same + // word as the first atom of the next section... + float right = atomRight + lastAtom->width; + float lineHeight2 = lineHeight; + float maxDescent2 = maxDescent; + + for (int section = sectionIndex + 1; section < sections.size(); ++section) + { + auto* s = sections.getUnchecked (section); + + if (s->atoms.size() == 0) + break; + + auto* nextAtom = s->atoms.getUnchecked (0); + + if (nextAtom->isWhitespace()) + break; + + right += nextAtom->width; + + lineHeight2 = jmax (lineHeight2, s->font.getHeight()); + maxDescent2 = jmax (maxDescent2, s->font.getDescent()); + + if (shouldWrap (right)) + { + lineHeight = lineHeight2; + maxDescent = maxDescent2; + + forceNewLine = true; + break; + } + + if (s->atoms.size() > 1) + break; + } + } + } + } + + if (atom != nullptr) + { + atomX = atomRight; + indexInText += atom->numChars; + + if (atom->isNewLine()) + beginNewLine(); + } + + atom = currentSection->atoms.getUnchecked (atomIndex); + atomRight = atomX + atom->width; + ++atomIndex; + + if (shouldWrap (atomRight) || forceNewLine) + { + if (atom->isWhitespace()) + { + // leave whitespace at the end of a line, but truncate it to avoid scrolling + atomRight = jmin (atomRight, wordWrapWidth); + } + else + { + atomRight = atom->width; + + if (shouldWrap (atomRight)) // atom too big to fit on a line, so break it up.. + { + tempAtom = *atom; + tempAtom.width = 0; + tempAtom.numChars = 0; + atom = &tempAtom; + + if (atomX > 0) + beginNewLine(); + + return next(); + } + + beginNewLine(); + return true; + } + } + + return true; + } + + void beginNewLine() + { + atomX = 0; + lineY += lineHeight * lineSpacing; + + int tempSectionIndex = sectionIndex; + int tempAtomIndex = atomIndex; + auto* section = sections.getUnchecked (tempSectionIndex); + + lineHeight = section->font.getHeight(); + maxDescent = section->font.getDescent(); + + float x = (atom != nullptr) ? atom->width : 0; + + while (! shouldWrap (x)) + { + if (tempSectionIndex >= sections.size()) + break; + + bool checkSize = false; + + if (tempAtomIndex >= section->atoms.size()) + { + if (++tempSectionIndex >= sections.size()) + break; + + tempAtomIndex = 0; + section = sections.getUnchecked (tempSectionIndex); + checkSize = true; + } + + auto* nextAtom = section->atoms.getUnchecked (tempAtomIndex); + + if (nextAtom == nullptr) + break; + + x += nextAtom->width; + + if (shouldWrap (x) || nextAtom->isNewLine()) + break; + + if (checkSize) + { + lineHeight = jmax (lineHeight, section->font.getHeight()); + maxDescent = jmax (maxDescent, section->font.getDescent()); + } + + ++tempAtomIndex; + } + } + + //============================================================================== + void draw (Graphics& g, const UniformTextSection*& lastSection) const + { + if (passwordCharacter != 0 || ! atom->isWhitespace()) + { + if (lastSection != currentSection) + { + lastSection = currentSection; + g.setColour (currentSection->colour); + g.setFont (currentSection->font); + } + + jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty()); + + GlyphArrangement ga; + ga.addLineOfText (currentSection->font, + atom->getTrimmedText (passwordCharacter), + atomX, (float) roundToInt (lineY + lineHeight - maxDescent)); + ga.draw (g); + } + } + + void addSelection (RectangleList& area, const Range selected) const + { + const float startX = indexToX (selected.getStart()); + const float endX = indexToX (selected.getEnd()); + + area.add (startX, lineY, endX - startX, lineHeight * lineSpacing); + } + + void drawUnderline (Graphics& g, const Range underline, const Colour colour) const + { + const int startX = roundToInt (indexToX (underline.getStart())); + const int endX = roundToInt (indexToX (underline.getEnd())); + const int baselineY = roundToInt (lineY + currentSection->font.getAscent() + 0.5f); + + Graphics::ScopedSaveState state (g); + g.reduceClipRegion (Rectangle (startX, baselineY, endX - startX, 1)); + g.fillCheckerBoard (Rectangle (endX, baselineY + 1), 3, 1, colour, Colours::transparentBlack); + } + + void drawSelectedText (Graphics& g, + const Range selected, + const Colour selectedTextColour) const + { + if (passwordCharacter != 0 || ! atom->isWhitespace()) + { + GlyphArrangement ga; + ga.addLineOfText (currentSection->font, + atom->getTrimmedText (passwordCharacter), + atomX, (float) roundToInt (lineY + lineHeight - maxDescent)); + + if (selected.getEnd() < indexInText + atom->numChars) + { + GlyphArrangement ga2 (ga); + ga2.removeRangeOfGlyphs (0, selected.getEnd() - indexInText); + ga.removeRangeOfGlyphs (selected.getEnd() - indexInText, -1); + + g.setColour (currentSection->colour); + ga2.draw (g); + } + + if (selected.getStart() > indexInText) + { + GlyphArrangement ga2 (ga); + ga2.removeRangeOfGlyphs (selected.getStart() - indexInText, -1); + ga.removeRangeOfGlyphs (0, selected.getStart() - indexInText); + + g.setColour (currentSection->colour); + ga2.draw (g); + } + + g.setColour (selectedTextColour); + ga.draw (g); + } + } + + //============================================================================== + float indexToX (const int indexToFind) const + { + if (indexToFind <= indexInText) + return atomX; + + if (indexToFind >= indexInText + atom->numChars) + return atomRight; + + GlyphArrangement g; + g.addLineOfText (currentSection->font, + atom->getText (passwordCharacter), + atomX, 0.0f); + + if (indexToFind - indexInText >= g.getNumGlyphs()) + return atomRight; + + return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft()); + } + + int xToIndex (const float xToFind) const + { + if (xToFind <= atomX || atom->isNewLine()) + return indexInText; + + if (xToFind >= atomRight) + return indexInText + atom->numChars; + + GlyphArrangement g; + g.addLineOfText (currentSection->font, + atom->getText (passwordCharacter), + atomX, 0.0f); + + const int numGlyphs = g.getNumGlyphs(); + + int j; + for (j = 0; j < numGlyphs; ++j) + { + auto& pg = g.getGlyph(j); + + if ((pg.getLeft() + pg.getRight()) / 2 > xToFind) + break; + } + + return indexInText + j; + } + + //============================================================================== + bool getCharPosition (const int index, float& cx, float& cy, float& lineHeightFound) + { + while (next()) + { + if (indexInText + atom->numChars > index) + { + cx = indexToX (index); + cy = lineY; + lineHeightFound = lineHeight; + return true; + } + } + + cx = atomX; + cy = lineY; + lineHeightFound = lineHeight; + return false; + } + + //============================================================================== + int indexInText = 0; + float lineY = 0, lineHeight = 0, maxDescent = 0; + float atomX = 0, atomRight = 0; + const TextAtom* atom = nullptr; + const UniformTextSection* currentSection = nullptr; + +private: + const OwnedArray& sections; + int sectionIndex = 0, atomIndex = 0; + const float wordWrapWidth; + const juce_wchar passwordCharacter; + const float lineSpacing; + TextAtom tempAtom; + + Iterator& operator= (const Iterator&) = delete; + + void moveToEndOfLastAtom() + { + if (atom != nullptr) + { + atomX = atomRight; + + if (atom->isNewLine()) + { + atomX = 0.0f; + lineY += lineHeight * lineSpacing; + } + } + } + + bool shouldWrap (const float x) const noexcept + { + return (x - 0.0001f) >= wordWrapWidth; + } + + JUCE_LEAK_DETECTOR (Iterator) +}; + + +//============================================================================== +class TextEditor::InsertAction : public UndoableAction +{ +public: + InsertAction (TextEditor& ed, + const String& newText, + const int insertPos, + const Font& newFont, + const Colour newColour, + const int oldCaret, + const int newCaret) + : owner (ed), + text (newText), + insertIndex (insertPos), + oldCaretPos (oldCaret), + newCaretPos (newCaret), + font (newFont), + colour (newColour) + { + } + + bool perform() override + { + owner.insert (text, insertIndex, font, colour, 0, newCaretPos); + return true; + } + + bool undo() override + { + owner.remove (Range (insertIndex, insertIndex + text.length()), 0, oldCaretPos); + return true; + } + + int getSizeInUnits() override + { + return text.length() + 16; + } + +private: + TextEditor& owner; + const String text; + const int insertIndex, oldCaretPos, newCaretPos; + const Font font; + const Colour colour; + + JUCE_DECLARE_NON_COPYABLE (InsertAction) +}; + +//============================================================================== +class TextEditor::RemoveAction : public UndoableAction +{ +public: + RemoveAction (TextEditor& ed, + const Range rangeToRemove, + const int oldCaret, + const int newCaret, + const Array& oldSections) + : owner (ed), + range (rangeToRemove), + oldCaretPos (oldCaret), + newCaretPos (newCaret) + { + removedSections.addArray (oldSections); + } + + bool perform() override + { + owner.remove (range, 0, newCaretPos); + return true; + } + + bool undo() override + { + owner.reinsert (range.getStart(), removedSections); + owner.moveCaretTo (oldCaretPos, false); + return true; + } + + int getSizeInUnits() override + { + int n = 16; + + for (auto* s : removedSections) + n += s->getTotalLength(); + + return n; + } + +private: + TextEditor& owner; + const Range range; + const int oldCaretPos, newCaretPos; + OwnedArray removedSections; + + JUCE_DECLARE_NON_COPYABLE (RemoveAction) +}; + +//============================================================================== +class TextEditor::TextHolderComponent : public Component, + public Timer, + public Value::Listener +{ +public: + TextHolderComponent (TextEditor& ed) : owner (ed) + { + setWantsKeyboardFocus (false); + setInterceptsMouseClicks (false, true); + setMouseCursor (MouseCursor::ParentCursor); + + owner.getTextValue().addListener (this); + } + + ~TextHolderComponent() + { + owner.getTextValue().removeListener (this); + } + + void paint (Graphics& g) override + { + owner.drawContent (g); + } + + void restartTimer() + { + startTimer (350); + } + + void timerCallback() override + { + owner.timerCallbackInt(); + } + + void valueChanged (Value&) override + { + owner.textWasChangedByValue(); + } + +private: + TextEditor& owner; + + JUCE_DECLARE_NON_COPYABLE (TextHolderComponent) +}; + +//============================================================================== +class TextEditorViewport : public Viewport +{ +public: + TextEditorViewport (TextEditor& ed) : owner (ed) {} + + void visibleAreaChanged (const Rectangle&) override + { + if (! rentrant) // it's rare, but possible to get into a feedback loop as the viewport's scrollbars + // appear and disappear, causing the wrap width to change. + { + const float wordWrapWidth = owner.getWordWrapWidth(); + + if (wordWrapWidth != lastWordWrapWidth) + { + lastWordWrapWidth = wordWrapWidth; + + rentrant = true; + owner.updateTextHolderSize(); + rentrant = false; + } + } + } + +private: + TextEditor& owner; + float lastWordWrapWidth = 0; + bool rentrant = false; + + JUCE_DECLARE_NON_COPYABLE (TextEditorViewport) +}; + +//============================================================================== +namespace TextEditorDefs +{ + const int textChangeMessageId = 0x10003001; + const int returnKeyMessageId = 0x10003002; + const int escapeKeyMessageId = 0x10003003; + const int focusLossMessageId = 0x10003004; + + const int maxActionsPerTransaction = 100; + + static int getCharacterCategory (const juce_wchar character) + { + return CharacterFunctions::isLetterOrDigit (character) + ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1); + } +} + +//============================================================================== +TextEditor::TextEditor (const String& name, juce_wchar passwordChar) + : Component (name), + passwordCharacter (passwordChar) +{ + setMouseCursor (MouseCursor::IBeamCursor); + + addAndMakeVisible (viewport = new TextEditorViewport (*this)); + viewport->setViewedComponent (textHolder = new TextHolderComponent (*this)); + viewport->setWantsKeyboardFocus (false); + viewport->setScrollBarsShown (false, false); + + setWantsKeyboardFocus (true); + recreateCaret(); +} + +TextEditor::~TextEditor() +{ + if (wasFocused) + if (auto* peer = getPeer()) + peer->dismissPendingTextInput(); + + textValue.removeListener (textHolder); + textValue.referTo (Value()); + + viewport = nullptr; + textHolder = nullptr; +} + +//============================================================================== +void TextEditor::newTransaction() +{ + lastTransactionTime = Time::getApproximateMillisecondCounter(); + undoManager.beginNewTransaction(); +} + +bool TextEditor::undoOrRedo (const bool shouldUndo) +{ + if (! isReadOnly()) + { + newTransaction(); + + if (shouldUndo ? undoManager.undo() + : undoManager.redo()) + { + scrollToMakeSureCursorIsVisible(); + repaint(); + textChanged(); + return true; + } + } + + return false; +} + +bool TextEditor::undo() { return undoOrRedo (true); } +bool TextEditor::redo() { return undoOrRedo (false); } + +//============================================================================== +void TextEditor::setMultiLine (const bool shouldBeMultiLine, + const bool shouldWordWrap) +{ + if (multiline != shouldBeMultiLine + || wordWrap != (shouldWordWrap && shouldBeMultiLine)) + { + multiline = shouldBeMultiLine; + wordWrap = shouldWordWrap && shouldBeMultiLine; + + viewport->setScrollBarsShown (scrollbarVisible && multiline, + scrollbarVisible && multiline); + viewport->setViewPosition (0, 0); + resized(); + scrollToMakeSureCursorIsVisible(); + } +} + +bool TextEditor::isMultiLine() const +{ + return multiline; +} + +void TextEditor::setScrollbarsShown (bool shown) +{ + if (scrollbarVisible != shown) + { + scrollbarVisible = shown; + shown = shown && isMultiLine(); + viewport->setScrollBarsShown (shown, shown); + } +} + +void TextEditor::setReadOnly (const bool shouldBeReadOnly) +{ + if (readOnly != shouldBeReadOnly) + { + readOnly = shouldBeReadOnly; + enablementChanged(); + } +} + +bool TextEditor::isReadOnly() const noexcept +{ + return readOnly || ! isEnabled(); +} + +bool TextEditor::isTextInputActive() const +{ + return ! isReadOnly(); +} + +void TextEditor::setReturnKeyStartsNewLine (const bool shouldStartNewLine) +{ + returnKeyStartsNewLine = shouldStartNewLine; +} + +void TextEditor::setTabKeyUsedAsCharacter (const bool shouldTabKeyBeUsed) +{ + tabKeyUsed = shouldTabKeyBeUsed; +} + +void TextEditor::setPopupMenuEnabled (const bool b) +{ + popupMenuEnabled = b; +} + +void TextEditor::setSelectAllWhenFocused (const bool b) +{ + selectAllTextWhenFocused = b; +} + +//============================================================================== +void TextEditor::setFont (const Font& newFont) +{ + currentFont = newFont; + scrollToMakeSureCursorIsVisible(); +} + +void TextEditor::applyFontToAllText (const Font& newFont, bool changeCurrentFont) +{ + if (changeCurrentFont) + currentFont = newFont; + + auto overallColour = findColour (textColourId); + + for (auto* uts : sections) + { + uts->setFont (newFont, passwordCharacter); + uts->colour = overallColour; + } + + coalesceSimilarSections(); + updateTextHolderSize(); + scrollToMakeSureCursorIsVisible(); + repaint(); +} + +void TextEditor::applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour) +{ + for (auto* uts : sections) + uts->colour = newColour; + + if (changeCurrentTextColour) + setColour (TextEditor::textColourId, newColour); + else + repaint(); +} + +void TextEditor::lookAndFeelChanged() +{ + caret = nullptr; + recreateCaret(); + repaint(); +} + +void TextEditor::parentHierarchyChanged() +{ + lookAndFeelChanged(); +} + +void TextEditor::enablementChanged() +{ + recreateCaret(); + repaint(); +} + +void TextEditor::setCaretVisible (const bool shouldCaretBeVisible) +{ + if (caretVisible != shouldCaretBeVisible) + { + caretVisible = shouldCaretBeVisible; + recreateCaret(); + } +} + +void TextEditor::recreateCaret() +{ + if (isCaretVisible()) + { + if (caret == nullptr) + { + textHolder->addChildComponent (caret = getLookAndFeel().createCaretComponent (this)); + updateCaretPosition(); + } + } + else + { + caret = nullptr; + } +} + +void TextEditor::updateCaretPosition() +{ + if (caret != nullptr) + caret->setCaretPosition (getCaretRectangle().translated (leftIndent, topIndent)); +} + +TextEditor::LengthAndCharacterRestriction::LengthAndCharacterRestriction (int maxLen, const String& chars) + : allowedCharacters (chars), maxLength (maxLen) +{} + +String TextEditor::LengthAndCharacterRestriction::filterNewText (TextEditor& ed, const String& newInput) +{ + String t (newInput); + + if (allowedCharacters.isNotEmpty()) + t = t.retainCharacters (allowedCharacters); + + if (maxLength > 0) + t = t.substring (0, maxLength - (ed.getTotalNumChars() - ed.getHighlightedRegion().getLength())); + + return t; +} + +void TextEditor::setInputFilter (InputFilter* newFilter, bool takeOwnership) +{ + inputFilter.set (newFilter, takeOwnership); +} + +void TextEditor::setInputRestrictions (const int maxLen, const String& chars) +{ + setInputFilter (new LengthAndCharacterRestriction (maxLen, chars), true); +} + +void TextEditor::setTextToShowWhenEmpty (const String& text, Colour colourToUse) +{ + textToShowWhenEmpty = text; + colourForTextWhenEmpty = colourToUse; +} + +void TextEditor::setPasswordCharacter (const juce_wchar newPasswordCharacter) +{ + if (passwordCharacter != newPasswordCharacter) + { + passwordCharacter = newPasswordCharacter; + applyFontToAllText (currentFont); + } +} + +void TextEditor::setScrollBarThickness (const int newThicknessPixels) +{ + viewport->setScrollBarThickness (newThicknessPixels); +} + +//============================================================================== +void TextEditor::clear() +{ + clearInternal (nullptr); + updateTextHolderSize(); + undoManager.clearUndoHistory(); +} + +void TextEditor::setText (const String& newText, + const bool sendTextChangeMessage) +{ + const int newLength = newText.length(); + + if (newLength != getTotalNumChars() || getText() != newText) + { + if (! sendTextChangeMessage) + textValue.removeListener (textHolder); + + textValue = newText; + + auto oldCursorPos = caretPosition; + const bool cursorWasAtEnd = oldCursorPos >= getTotalNumChars(); + + clearInternal (nullptr); + insert (newText, 0, currentFont, findColour (textColourId), 0, caretPosition); + + // if you're adding text with line-feeds to a single-line text editor, it + // ain't gonna look right! + jassert (multiline || ! newText.containsAnyOf ("\r\n")); + + if (cursorWasAtEnd && ! isMultiLine()) + oldCursorPos = getTotalNumChars(); + + moveCaretTo (oldCursorPos, false); + + if (sendTextChangeMessage) + textChanged(); + else + textValue.addListener (textHolder); + + updateTextHolderSize(); + scrollToMakeSureCursorIsVisible(); + undoManager.clearUndoHistory(); + + repaint(); + } +} + +//============================================================================== +void TextEditor::updateValueFromText() +{ + if (valueTextNeedsUpdating) + { + valueTextNeedsUpdating = false; + textValue = getText(); + } +} + +Value& TextEditor::getTextValue() +{ + updateValueFromText(); + return textValue; +} + +void TextEditor::textWasChangedByValue() +{ + if (textValue.getValueSource().getReferenceCount() > 1) + setText (textValue.getValue()); +} + +//============================================================================== +void TextEditor::textChanged() +{ + updateTextHolderSize(); + + if (listeners.size() > 0) + postCommandMessage (TextEditorDefs::textChangeMessageId); + + if (textValue.getValueSource().getReferenceCount() > 1) + { + valueTextNeedsUpdating = false; + textValue = getText(); + } +} + +void TextEditor::returnPressed() { postCommandMessage (TextEditorDefs::returnKeyMessageId); } +void TextEditor::escapePressed() { postCommandMessage (TextEditorDefs::escapeKeyMessageId); } + +void TextEditor::addListener (Listener* l) { listeners.add (l); } +void TextEditor::removeListener (Listener* l) { listeners.remove (l); } + +//============================================================================== +void TextEditor::timerCallbackInt() +{ + if (hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent()) + wasFocused = true; + + auto now = Time::getApproximateMillisecondCounter(); + + if (now > lastTransactionTime + 200) + newTransaction(); +} + +void TextEditor::repaintText (const Range range) +{ + if (! range.isEmpty()) + { + float x = 0, y = 0, lh = currentFont.getHeight(); + + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0) + { + Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); + + i.getCharPosition (range.getStart(), x, y, lh); + + auto y1 = (int) y; + int y2; + + if (range.getEnd() >= getTotalNumChars()) + { + y2 = textHolder->getHeight(); + } + else + { + i.getCharPosition (range.getEnd(), x, y, lh); + y2 = (int) (y + lh * 2.0f); + } + + textHolder->repaint (0, y1, textHolder->getWidth(), y2 - y1); + } + } +} + +//============================================================================== +void TextEditor::moveCaret (int newCaretPos) +{ + if (newCaretPos < 0) + newCaretPos = 0; + else + newCaretPos = jmin (newCaretPos, getTotalNumChars()); + + if (newCaretPos != getCaretPosition()) + { + caretPosition = newCaretPos; + textHolder->restartTimer(); + scrollToMakeSureCursorIsVisible(); + updateCaretPosition(); + } +} + +int TextEditor::getCaretPosition() const +{ + return caretPosition; +} + +void TextEditor::setCaretPosition (const int newIndex) +{ + moveCaretTo (newIndex, false); +} + +void TextEditor::moveCaretToEnd() +{ + moveCaretTo (std::numeric_limits::max(), false); +} + +void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX, + const int desiredCaretY) + +{ + updateCaretPosition(); + auto caretPos = getCaretRectangle(); + + int vx = caretPos.getX() - desiredCaretX; + int vy = caretPos.getY() - desiredCaretY; + + if (desiredCaretX < jmax (1, proportionOfWidth (0.05f))) + vx += desiredCaretX - proportionOfWidth (0.2f); + else if (desiredCaretX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10))) + vx += desiredCaretX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth(); + + vx = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vx); + + if (! isMultiLine()) + { + vy = viewport->getViewPositionY(); + } + else + { + vy = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vy); + + if (desiredCaretY < 0) + vy = jmax (0, desiredCaretY + vy); + else if (desiredCaretY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretPos.getHeight())) + vy += desiredCaretY + 2 + caretPos.getHeight() + topIndent - viewport->getMaximumVisibleHeight(); + } + + viewport->setViewPosition (vx, vy); +} + +Rectangle TextEditor::getCaretRectangle() +{ + float cursorX, cursorY; + float cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value) + getCharPosition (caretPosition, cursorX, cursorY, cursorHeight); + + return { roundToInt (cursorX), roundToInt (cursorY), 2, roundToInt (cursorHeight) }; +} + +//============================================================================== +enum { rightEdgeSpace = 2 }; + +float TextEditor::getWordWrapWidth() const +{ + return wordWrap ? (float) (viewport->getMaximumVisibleWidth() - (leftIndent + rightEdgeSpace + 1)) + : std::numeric_limits::max(); +} + +void TextEditor::updateTextHolderSize() +{ + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0) + { + float maxWidth = 0.0f; + + Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); + + while (i.next()) + maxWidth = jmax (maxWidth, i.atomRight); + + const int w = leftIndent + roundToInt (maxWidth); + const int h = topIndent + roundToInt (jmax (i.lineY + i.lineHeight, + currentFont.getHeight())); + + textHolder->setSize (w + rightEdgeSpace, h + 1); // (allows a bit of space for the cursor to be at the right-hand-edge) + } +} + +int TextEditor::getTextWidth() const { return textHolder->getWidth(); } +int TextEditor::getTextHeight() const { return textHolder->getHeight(); } + +void TextEditor::setIndents (const int newLeftIndent, const int newTopIndent) +{ + leftIndent = newLeftIndent; + topIndent = newTopIndent; +} + +void TextEditor::setBorder (const BorderSize& border) +{ + borderSize = border; + resized(); +} + +BorderSize TextEditor::getBorder() const +{ + return borderSize; +} + +void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor) +{ + keepCaretOnScreen = shouldScrollToShowCursor; +} + +void TextEditor::scrollToMakeSureCursorIsVisible() +{ + updateCaretPosition(); + + if (keepCaretOnScreen) + { + auto viewPos = viewport->getViewPosition(); + auto caretRect = getCaretRectangle(); + auto relativeCursor = caretRect.getPosition() - viewPos; + + if (relativeCursor.x < jmax (1, proportionOfWidth (0.05f))) + { + viewPos.x += relativeCursor.x - proportionOfWidth (0.2f); + } + else if (relativeCursor.x > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10))) + { + viewPos.x += relativeCursor.x + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth(); + } + + viewPos.x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), viewPos.x); + + if (! isMultiLine()) + { + viewPos.y = (getHeight() - textHolder->getHeight() - topIndent) / -2; + } + else if (relativeCursor.y < 0) + { + viewPos.y = jmax (0, relativeCursor.y + viewPos.y); + } + else if (relativeCursor.y > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretRect.getHeight())) + { + viewPos.y += relativeCursor.y + 2 + caretRect.getHeight() + topIndent - viewport->getMaximumVisibleHeight(); + } + + viewport->setViewPosition (viewPos); + } +} + +void TextEditor::moveCaretTo (const int newPosition, const bool isSelecting) +{ + if (isSelecting) + { + moveCaret (newPosition); + + auto oldSelection = selection; + + if (dragType == notDragging) + { + if (std::abs (getCaretPosition() - selection.getStart()) < std::abs (getCaretPosition() - selection.getEnd())) + dragType = draggingSelectionStart; + else + dragType = draggingSelectionEnd; + } + + if (dragType == draggingSelectionStart) + { + if (getCaretPosition() >= selection.getEnd()) + dragType = draggingSelectionEnd; + + selection = Range::between (getCaretPosition(), selection.getEnd()); + } + else + { + if (getCaretPosition() < selection.getStart()) + dragType = draggingSelectionStart; + + selection = Range::between (getCaretPosition(), selection.getStart()); + } + + repaintText (selection.getUnionWith (oldSelection)); + } + else + { + dragType = notDragging; + + repaintText (selection); + + moveCaret (newPosition); + selection = Range::emptyRange (getCaretPosition()); + } +} + +int TextEditor::getTextIndexAt (const int x, const int y) +{ + return indexAtPosition ((float) (x + viewport->getViewPositionX() - leftIndent - borderSize.getLeft()), + (float) (y + viewport->getViewPositionY() - topIndent - borderSize.getTop())); +} + +void TextEditor::insertTextAtCaret (const String& t) +{ + String newText (inputFilter != nullptr ? inputFilter->filterNewText (*this, t) : t); + + if (isMultiLine()) + newText = newText.replace ("\r\n", "\n"); + else + newText = newText.replaceCharacters ("\r\n", " "); + + const int insertIndex = selection.getStart(); + const int newCaretPos = insertIndex + newText.length(); + + remove (selection, getUndoManager(), + newText.isNotEmpty() ? newCaretPos - 1 : newCaretPos); + + insert (newText, insertIndex, currentFont, findColour (textColourId), + getUndoManager(), newCaretPos); + + textChanged(); +} + +void TextEditor::setHighlightedRegion (const Range& newSelection) +{ + moveCaretTo (newSelection.getStart(), false); + moveCaretTo (newSelection.getEnd(), true); +} + +//============================================================================== +void TextEditor::copy() +{ + if (passwordCharacter == 0) + { + auto selectedText = getHighlightedText(); + + if (selectedText.isNotEmpty()) + SystemClipboard::copyTextToClipboard (selectedText); + } +} + +void TextEditor::paste() +{ + if (! isReadOnly()) + { + auto clip = SystemClipboard::getTextFromClipboard(); + + if (clip.isNotEmpty()) + insertTextAtCaret (clip); + } +} + +void TextEditor::cut() +{ + if (! isReadOnly()) + { + moveCaret (selection.getEnd()); + insertTextAtCaret (String()); + } +} + +//============================================================================== +void TextEditor::drawContent (Graphics& g) +{ + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0) + { + g.setOrigin (leftIndent, topIndent); + auto clip = g.getClipBounds(); + Colour selectedTextColour; + + Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); + + if (! selection.isEmpty()) + { + Iterator i2 (i); + RectangleList selectionArea; + + while (i2.next() && i2.lineY < clip.getBottom()) + { + if (i2.lineY + i2.lineHeight >= clip.getY() + && selection.intersects (Range (i2.indexInText, i2.indexInText + i2.atom->numChars))) + { + i2.addSelection (selectionArea, selection); + } + } + + g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f)); + g.fillRectList (selectionArea); + + selectedTextColour = findColour (highlightedTextColourId); + } + + const UniformTextSection* lastSection = nullptr; + + while (i.next() && i.lineY < clip.getBottom()) + { + if (i.lineY + i.lineHeight >= clip.getY()) + { + if (selection.intersects (Range (i.indexInText, i.indexInText + i.atom->numChars))) + { + i.drawSelectedText (g, selection, selectedTextColour); + lastSection = nullptr; + } + else + { + i.draw (g, lastSection); + } + } + } + + for (int j = underlinedSections.size(); --j >= 0;) + { + const Range underlinedSection = underlinedSections.getReference (j); + + Iterator i2 (sections, wordWrapWidth, passwordCharacter, lineSpacing); + + while (i2.next() && i2.lineY < clip.getBottom()) + { + if (i2.lineY + i2.lineHeight >= clip.getY() + && underlinedSection.intersects (Range (i2.indexInText, i2.indexInText + i2.atom->numChars))) + { + i2.drawUnderline (g, underlinedSection, findColour (textColourId)); + } + } + } + } +} + +void TextEditor::paint (Graphics& g) +{ + getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this); +} + +void TextEditor::paintOverChildren (Graphics& g) +{ + if (textToShowWhenEmpty.isNotEmpty() + && (! hasKeyboardFocus (false)) + && getTotalNumChars() == 0) + { + g.setColour (colourForTextWhenEmpty); + g.setFont (getFont()); + + if (isMultiLine()) + g.drawText (textToShowWhenEmpty, getLocalBounds(), + Justification::centred, true); + else + g.drawText (textToShowWhenEmpty, + leftIndent, 0, viewport->getWidth() - leftIndent, getHeight(), + Justification::centredLeft, true); + } + + getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this); +} + +//============================================================================== +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); + newTransaction(); + + if (wasFocused || ! selectAllTextWhenFocused) + { + if (! (popupMenuEnabled && e.mods.isPopupMenu())) + { + moveCaretTo (getTextIndexAt (e.x, e.y), + e.mods.isShiftDown()); + } + else + { + PopupMenu m; + m.setLookAndFeel (&getLookAndFeel()); + addPopupMenuItems (m, &e); + + m.showMenuAsync (PopupMenu::Options(), + ModalCallbackFunction::forComponent (textEditorMenuCallback, this)); + } + } +} + +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) +{ + newTransaction(); + textHolder->restartTimer(); + + if (wasFocused || ! selectAllTextWhenFocused) + if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu())) + moveCaret (getTextIndexAt (e.x, e.y)); + + wasFocused = true; +} + +void TextEditor::mouseDoubleClick (const MouseEvent& e) +{ + int tokenEnd = getTextIndexAt (e.x, e.y); + int tokenStart = 0; + + if (e.getNumberOfClicks() > 3) + { + tokenEnd = getTotalNumChars(); + } + else + { + auto t = getText(); + auto totalLength = getTotalNumChars(); + + while (tokenEnd < totalLength) + { + // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale) + const juce_wchar c = t [tokenEnd]; + if (CharacterFunctions::isLetterOrDigit (c) || c > 128) + ++tokenEnd; + else + break; + } + + tokenStart = tokenEnd; + + while (tokenStart > 0) + { + // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale) + const juce_wchar c = t [tokenStart - 1]; + if (CharacterFunctions::isLetterOrDigit (c) || c > 128) + --tokenStart; + else + break; + } + + if (e.getNumberOfClicks() > 2) + { + while (tokenEnd < totalLength) + { + const juce_wchar c = t [tokenEnd]; + if (c != '\r' && c != '\n') + ++tokenEnd; + else + break; + } + + while (tokenStart > 0) + { + const juce_wchar c = t [tokenStart - 1]; + if (c != '\r' && c != '\n') + --tokenStart; + else + break; + } + } + } + + moveCaretTo (tokenEnd, false); + moveCaretTo (tokenStart, true); +} + +void TextEditor::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel) +{ + if (! viewport->useMouseWheelMoveIfNeeded (e, wheel)) + Component::mouseWheelMove (e, wheel); +} + +//============================================================================== +bool TextEditor::moveCaretWithTransaction (const int newPos, const bool selecting) +{ + newTransaction(); + moveCaretTo (newPos, selecting); + return true; +} + +bool TextEditor::moveCaretLeft (bool moveInWholeWordSteps, bool selecting) +{ + int pos = getCaretPosition(); + + if (moveInWholeWordSteps) + pos = findWordBreakBefore (pos); + else + --pos; + + return moveCaretWithTransaction (pos, selecting); +} + +bool TextEditor::moveCaretRight (bool moveInWholeWordSteps, bool selecting) +{ + int pos = getCaretPosition(); + + if (moveInWholeWordSteps) + pos = findWordBreakAfter (pos); + else + ++pos; + + return moveCaretWithTransaction (pos, selecting); +} + +bool TextEditor::moveCaretUp (bool selecting) +{ + if (! isMultiLine()) + return moveCaretToStartOfLine (selecting); + + auto caretPos = getCaretRectangle().toFloat(); + return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - 1.0f), selecting); +} + +bool TextEditor::moveCaretDown (bool selecting) +{ + if (! isMultiLine()) + return moveCaretToEndOfLine (selecting); + + auto caretPos = getCaretRectangle().toFloat(); + return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting); +} + +bool TextEditor::pageUp (bool selecting) +{ + if (! isMultiLine()) + return moveCaretToStartOfLine (selecting); + + auto caretPos = getCaretRectangle().toFloat(); + return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - viewport->getViewHeight()), selecting); +} + +bool TextEditor::pageDown (bool selecting) +{ + if (! isMultiLine()) + return moveCaretToEndOfLine (selecting); + + auto caretPos = getCaretRectangle().toFloat(); + return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + viewport->getViewHeight()), selecting); +} + +void TextEditor::scrollByLines (int deltaLines) +{ + if (auto* scrollbar = viewport->getVerticalScrollBar()) + scrollbar->moveScrollbarInSteps (deltaLines); +} + +bool TextEditor::scrollDown() +{ + scrollByLines (-1); + return true; +} + +bool TextEditor::scrollUp() +{ + scrollByLines (1); + return true; +} + +bool TextEditor::moveCaretToTop (bool selecting) +{ + return moveCaretWithTransaction (0, selecting); +} + +bool TextEditor::moveCaretToStartOfLine (bool selecting) +{ + auto caretPos = getCaretRectangle().toFloat(); + return moveCaretWithTransaction (indexAtPosition (0.0f, caretPos.getY()), selecting); +} + +bool TextEditor::moveCaretToEnd (bool selecting) +{ + return moveCaretWithTransaction (getTotalNumChars(), selecting); +} + +bool TextEditor::moveCaretToEndOfLine (bool selecting) +{ + auto caretPos = getCaretRectangle().toFloat(); + return moveCaretWithTransaction (indexAtPosition ((float) textHolder->getWidth(), caretPos.getY()), selecting); +} + +bool TextEditor::deleteBackwards (bool moveInWholeWordSteps) +{ + if (moveInWholeWordSteps) + moveCaretTo (findWordBreakBefore (getCaretPosition()), true); + else if (selection.isEmpty() && selection.getStart() > 0) + selection = Range (selection.getEnd() - 1, selection.getEnd()); + + cut(); + return true; +} + +bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/) +{ + if (selection.isEmpty() && selection.getStart() < getTotalNumChars()) + selection = Range (selection.getStart(), selection.getStart() + 1); + + cut(); + return true; +} + +bool TextEditor::copyToClipboard() +{ + newTransaction(); + copy(); + return true; +} + +bool TextEditor::cutToClipboard() +{ + newTransaction(); + copy(); + cut(); + return true; +} + +bool TextEditor::pasteFromClipboard() +{ + newTransaction(); + paste(); + return true; +} + +bool TextEditor::selectAll() +{ + newTransaction(); + moveCaretTo (getTotalNumChars(), false); + moveCaretTo (0, true); + return true; +} + +//============================================================================== +void TextEditor::setEscapeAndReturnKeysConsumed (bool shouldBeConsumed) noexcept +{ + consumeEscAndReturnKeys = shouldBeConsumed; +} + +bool TextEditor::keyPressed (const KeyPress& key) +{ + if (isReadOnly() && key != KeyPress ('c', ModifierKeys::commandModifier, 0) + && key != KeyPress ('a', ModifierKeys::commandModifier, 0)) + return false; + + if (! TextEditorKeyMapper::invokeKeyFunction (*this, key)) + { + if (key == KeyPress::returnKey) + { + newTransaction(); + + if (returnKeyStartsNewLine) + insertTextAtCaret ("\n"); + else + { + returnPressed(); + return consumeEscAndReturnKeys; + } + } + else if (key.isKeyCode (KeyPress::escapeKey)) + { + newTransaction(); + moveCaretTo (getCaretPosition(), false); + escapePressed(); + return consumeEscAndReturnKeys; + } + else if (key.getTextCharacter() >= ' ' + || (tabKeyUsed && (key.getTextCharacter() == '\t'))) + { + insertTextAtCaret (String::charToString (key.getTextCharacter())); + + lastTransactionTime = Time::getApproximateMillisecondCounter(); + } + else + { + return false; + } + } + + return true; +} + +bool TextEditor::keyStateChanged (const bool isKeyDown) +{ + if (! isKeyDown) + return false; + + #if JUCE_WINDOWS + if (KeyPress (KeyPress::F4Key, ModifierKeys::altModifier, 0).isCurrentlyDown()) + return false; // We need to explicitly allow alt-F4 to pass through on Windows + #endif + + if ((! consumeEscAndReturnKeys) + && (KeyPress (KeyPress::escapeKey).isCurrentlyDown() + || KeyPress (KeyPress::returnKey).isCurrentlyDown())) + return false; + + // (overridden to avoid forwarding key events to the parent) + return ! ModifierKeys::getCurrentModifiers().isCommandDown(); +} + +//============================================================================== +void TextEditor::focusGained (FocusChangeType) +{ + newTransaction(); + + if (selectAllTextWhenFocused) + { + moveCaretTo (0, false); + moveCaretTo (getTotalNumChars(), true); + } + + repaint(); + updateCaretPosition(); + + if (auto* peer = getPeer()) + if (! isReadOnly()) + peer->textInputRequired (peer->globalToLocal (getScreenPosition()), *this); +} + +void TextEditor::focusLost (FocusChangeType) +{ + newTransaction(); + + wasFocused = false; + textHolder->stopTimer(); + + underlinedSections.clear(); + + if (auto* peer = getPeer()) + peer->dismissPendingTextInput(); + + updateCaretPosition(); + + postCommandMessage (TextEditorDefs::focusLossMessageId); + repaint(); +} + +//============================================================================== +void TextEditor::resized() +{ + viewport->setBoundsInset (borderSize); + viewport->setSingleStepSizes (16, roundToInt (currentFont.getHeight())); + + updateTextHolderSize(); + + if (isMultiLine()) + updateCaretPosition(); + else + scrollToMakeSureCursorIsVisible(); +} + +void TextEditor::handleCommandMessage (const int commandId) +{ + Component::BailOutChecker checker (this); + + switch (commandId) + { + case TextEditorDefs::textChangeMessageId: + listeners.callChecked (checker, &Listener::textEditorTextChanged, (TextEditor&) *this); + break; + + case TextEditorDefs::returnKeyMessageId: + listeners.callChecked (checker, &Listener::textEditorReturnKeyPressed, (TextEditor&) *this); + break; + + case TextEditorDefs::escapeKeyMessageId: + listeners.callChecked (checker, &Listener::textEditorEscapeKeyPressed, (TextEditor&) *this); + break; + + case TextEditorDefs::focusLossMessageId: + updateValueFromText(); + listeners.callChecked (checker, &Listener::textEditorFocusLost, (TextEditor&) *this); + break; + + default: + jassertfalse; + break; + } +} + +void TextEditor::setTemporaryUnderlining (const Array >& newUnderlinedSections) +{ + underlinedSections = newUnderlinedSections; + repaint(); +} + +//============================================================================== +UndoManager* TextEditor::getUndoManager() noexcept +{ + return readOnly ? nullptr : &undoManager; +} + +void TextEditor::clearInternal (UndoManager* const um) +{ + remove (Range (0, getTotalNumChars()), um, caretPosition); +} + +void TextEditor::insert (const String& text, + const int insertIndex, + const Font& font, + const Colour colour, + UndoManager* const um, + const int caretPositionToMoveTo) +{ + if (text.isNotEmpty()) + { + if (um != nullptr) + { + if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction) + newTransaction(); + + um->perform (new InsertAction (*this, text, insertIndex, font, colour, + caretPosition, caretPositionToMoveTo)); + } + else + { + repaintText (Range (insertIndex, getTotalNumChars())); // must do this before and after changing the data, in case + // a line gets moved due to word wrap + + int index = 0; + int nextIndex = 0; + + for (int i = 0; i < sections.size(); ++i) + { + nextIndex = index + sections.getUnchecked (i)->getTotalLength(); + + if (insertIndex == index) + { + sections.insert (i, new UniformTextSection (text, font, colour, passwordCharacter)); + break; + } + else if (insertIndex > index && insertIndex < nextIndex) + { + splitSection (i, insertIndex - index); + sections.insert (i + 1, new UniformTextSection (text, font, colour, passwordCharacter)); + break; + } + + index = nextIndex; + } + + if (nextIndex == insertIndex) + sections.add (new UniformTextSection (text, font, colour, passwordCharacter)); + + coalesceSimilarSections(); + totalNumChars = -1; + valueTextNeedsUpdating = true; + + updateTextHolderSize(); + moveCaretTo (caretPositionToMoveTo, false); + + repaintText (Range (insertIndex, getTotalNumChars())); + } + } +} + +void TextEditor::reinsert (const int insertIndex, const OwnedArray& sectionsToInsert) +{ + int index = 0; + int nextIndex = 0; + + for (int i = 0; i < sections.size(); ++i) + { + nextIndex = index + sections.getUnchecked (i)->getTotalLength(); + + if (insertIndex == index) + { + for (int j = sectionsToInsert.size(); --j >= 0;) + sections.insert (i, new UniformTextSection (*sectionsToInsert.getUnchecked(j))); + + break; + } + else if (insertIndex > index && insertIndex < nextIndex) + { + splitSection (i, insertIndex - index); + + for (int j = sectionsToInsert.size(); --j >= 0;) + sections.insert (i + 1, new UniformTextSection (*sectionsToInsert.getUnchecked(j))); + + break; + } + + index = nextIndex; + } + + if (nextIndex == insertIndex) + { + for (int j = 0; j < sectionsToInsert.size(); ++j) + sections.add (new UniformTextSection (*sectionsToInsert.getUnchecked(j))); + } + + coalesceSimilarSections(); + totalNumChars = -1; + valueTextNeedsUpdating = true; +} + +void TextEditor::remove (Range range, UndoManager* const um, const int caretPositionToMoveTo) +{ + if (! range.isEmpty()) + { + int index = 0; + + for (int i = 0; i < sections.size(); ++i) + { + const int nextIndex = index + sections.getUnchecked(i)->getTotalLength(); + + if (range.getStart() > index && range.getStart() < nextIndex) + { + splitSection (i, range.getStart() - index); + --i; + } + else if (range.getEnd() > index && range.getEnd() < nextIndex) + { + splitSection (i, range.getEnd() - index); + --i; + } + else + { + index = nextIndex; + + if (index > range.getEnd()) + break; + } + } + + index = 0; + + if (um != nullptr) + { + Array removedSections; + + for (int i = 0; i < sections.size(); ++i) + { + if (range.getEnd() <= range.getStart()) + break; + + auto* section = sections.getUnchecked (i); + auto nextIndex = index + section->getTotalLength(); + + if (range.getStart() <= index && range.getEnd() >= nextIndex) + removedSections.add (new UniformTextSection (*section)); + + index = nextIndex; + } + + if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction) + newTransaction(); + + um->perform (new RemoveAction (*this, range, caretPosition, + caretPositionToMoveTo, removedSections)); + } + else + { + auto remainingRange = range; + + for (int i = 0; i < sections.size(); ++i) + { + auto* section = sections.getUnchecked (i); + + const int nextIndex = index + section->getTotalLength(); + + if (remainingRange.getStart() <= index && remainingRange.getEnd() >= nextIndex) + { + sections.remove (i); + remainingRange.setEnd (remainingRange.getEnd() - (nextIndex - index)); + + if (remainingRange.isEmpty()) + break; + + --i; + } + else + { + index = nextIndex; + } + } + + coalesceSimilarSections(); + totalNumChars = -1; + valueTextNeedsUpdating = true; + + moveCaretTo (caretPositionToMoveTo, false); + + repaintText (Range (range.getStart(), getTotalNumChars())); + } + } +} + +//============================================================================== +String TextEditor::getText() const +{ + MemoryOutputStream mo; + mo.preallocate ((size_t) getTotalNumChars()); + + for (int i = 0; i < sections.size(); ++i) + sections.getUnchecked (i)->appendAllText (mo); + + return mo.toUTF8(); +} + +String TextEditor::getTextInRange (const Range& range) const +{ + if (range.isEmpty()) + return {}; + + MemoryOutputStream mo; + mo.preallocate ((size_t) jmin (getTotalNumChars(), range.getLength())); + + int index = 0; + + for (int i = 0; i < sections.size(); ++i) + { + auto* s = sections.getUnchecked (i); + auto nextIndex = index + s->getTotalLength(); + + if (range.getStart() < nextIndex) + { + if (range.getEnd() <= index) + break; + + s->appendSubstring (mo, range - index); + } + + index = nextIndex; + } + + return mo.toUTF8(); +} + +String TextEditor::getHighlightedText() const +{ + return getTextInRange (selection); +} + +int TextEditor::getTotalNumChars() const +{ + if (totalNumChars < 0) + { + totalNumChars = 0; + + for (int i = sections.size(); --i >= 0;) + totalNumChars += sections.getUnchecked (i)->getTotalLength(); + } + + return totalNumChars; +} + +bool TextEditor::isEmpty() const +{ + return getTotalNumChars() == 0; +} + +void TextEditor::getCharPosition (const int index, float& cx, float& cy, float& lineHeight) const +{ + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0 && sections.size() > 0) + { + Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); + + i.getCharPosition (index, cx, cy, lineHeight); + } + else + { + cx = cy = 0; + lineHeight = currentFont.getHeight(); + } +} + +int TextEditor::indexAtPosition (const float x, const float y) +{ + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0) + { + Iterator i (sections, wordWrapWidth, passwordCharacter, lineSpacing); + + while (i.next()) + { + if (i.lineY + i.lineHeight > y) + { + if (i.lineY > y) + return jmax (0, i.indexInText - 1); + + if (i.atomX >= x) + return i.indexInText; + + if (x < i.atomRight) + return i.xToIndex (x); + } + } + } + + return getTotalNumChars(); +} + +//============================================================================== +int TextEditor::findWordBreakAfter (const int position) const +{ + auto t = getTextInRange (Range (position, position + 512)); + auto totalLength = t.length(); + int i = 0; + + while (i < totalLength && CharacterFunctions::isWhitespace (t[i])) + ++i; + + const int type = TextEditorDefs::getCharacterCategory (t[i]); + + while (i < totalLength && type == TextEditorDefs::getCharacterCategory (t[i])) + ++i; + + while (i < totalLength && CharacterFunctions::isWhitespace (t[i])) + ++i; + + return position + i; +} + +int TextEditor::findWordBreakBefore (const int position) const +{ + if (position <= 0) + return 0; + + auto startOfBuffer = jmax (0, position - 512); + auto t = getTextInRange (Range (startOfBuffer, position)); + + int i = position - startOfBuffer; + + while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1])) + --i; + + if (i > 0) + { + auto type = TextEditorDefs::getCharacterCategory (t [i - 1]); + + while (i > 0 && type == TextEditorDefs::getCharacterCategory (t [i - 1])) + --i; + } + + jassert (startOfBuffer + i >= 0); + return startOfBuffer + i; +} + + +//============================================================================== +void TextEditor::splitSection (const int sectionIndex, const int charToSplitAt) +{ + jassert (sections[sectionIndex] != nullptr); + + sections.insert (sectionIndex + 1, + sections.getUnchecked (sectionIndex)->split (charToSplitAt, passwordCharacter)); +} + +void TextEditor::coalesceSimilarSections() +{ + for (int i = 0; i < sections.size() - 1; ++i) + { + auto* s1 = sections.getUnchecked (i); + auto* s2 = sections.getUnchecked (i + 1); + + if (s1->font == s2->font + && s1->colour == s2->colour) + { + s1->append (*s2, passwordCharacter); + sections.remove (i + 1); + --i; + } + } +} + +} // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.h b/modules/juce_gui_basics/widgets/juce_TextEditor.h index 9a3960eb24..fff11dcbea 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.h +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.h @@ -1,774 +1,774 @@ -/* - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2017 - ROLI Ltd. - - JUCE is an open source library subject to commercial or open-source - licensing. - - By using JUCE, you agree to the terms of both the JUCE 5 End-User License - Agreement and JUCE 5 Privacy Policy (both updated and effective as of the - 27th April 2017). - - End User License Agreement: www.juce.com/juce-5-licence - Privacy Policy: www.juce.com/juce-5-privacy-policy - - Or: You may also use this code under the terms of the GPL v3 (see - www.gnu.org/licenses). - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace juce -{ - -//============================================================================== -/** - An editable text box. - - A TextEditor can either be in single- or multi-line mode, and supports mixed - fonts and colours. - - @see TextEditor::Listener, Label -*/ -class JUCE_API TextEditor : public Component, - public TextInputTarget, - public SettableTooltipClient -{ -public: - //============================================================================== - /** Creates a new, empty text editor. - - @param componentName the name to pass to the component for it to use as its name - @param passwordCharacter if this is not zero, this character will be used as a replacement - for all characters that are drawn on screen - e.g. to create - a password-style textbox containing circular blobs instead of text, - you could set this value to 0x25cf, which is the unicode character - for a black splodge (not all fonts include this, though), or 0x2022, - which is a bullet (probably the best choice for linux). - */ - explicit TextEditor (const String& componentName = String(), - juce_wchar passwordCharacter = 0); - - /** Destructor. */ - ~TextEditor(); - - //============================================================================== - /** Puts the editor into either multi- or single-line mode. - - By default, the editor will be in single-line mode, so use this if you need a multi-line - editor. - - See also the setReturnKeyStartsNewLine() method, which will also need to be turned - on if you want a multi-line editor with line-breaks. - - @see isMultiLine, setReturnKeyStartsNewLine - */ - void setMultiLine (bool shouldBeMultiLine, - bool shouldWordWrap = true); - - /** Returns true if the editor is in multi-line mode. */ - bool isMultiLine() const; - - //============================================================================== - /** Changes the behaviour of the return key. - - If set to true, the return key will insert a new-line into the text; if false - it will trigger a call to the TextEditor::Listener::textEditorReturnKeyPressed() - method. By default this is set to false, and when true it will only insert - new-lines when in multi-line mode (see setMultiLine()). - */ - void setReturnKeyStartsNewLine (bool shouldStartNewLine); - - /** Returns the value set by setReturnKeyStartsNewLine(). - See setReturnKeyStartsNewLine() for more info. - */ - bool getReturnKeyStartsNewLine() const { return returnKeyStartsNewLine; } - - /** Indicates whether the tab key should be accepted and used to input a tab character, - or whether it gets ignored. - - By default the tab key is ignored, so that it can be used to switch keyboard focus - between components. - */ - void setTabKeyUsedAsCharacter (bool shouldTabKeyBeUsed); - - /** Returns true if the tab key is being used for input. - @see setTabKeyUsedAsCharacter - */ - bool isTabKeyUsedAsCharacter() const { return tabKeyUsed; } - - /** This can be used to change whether escape and return keypress events are - propagated up to the parent component. - The default here is true, meaning that these events are not allowed to reach the - parent, but you may want to allow them through so that they can trigger other - actions, e.g. closing a dialog box, etc. - */ - void setEscapeAndReturnKeysConsumed (bool shouldBeConsumed) noexcept; - - //============================================================================== - /** Changes the editor to read-only mode. - - By default, the text editor is not read-only. If you're making it read-only, you - might also want to call setCaretVisible (false) to get rid of the caret. - - The text can still be highlighted and copied when in read-only mode. - - @see isReadOnly, setCaretVisible - */ - void setReadOnly (bool shouldBeReadOnly); - - /** Returns true if the editor is in read-only mode. */ - bool isReadOnly() const noexcept; - - //============================================================================== - /** Makes the caret visible or invisible. - By default the caret is visible. - @see setCaretColour, setCaretPosition - */ - void setCaretVisible (bool shouldBeVisible); - - /** Returns true if the caret is enabled. - @see setCaretVisible - */ - bool isCaretVisible() const noexcept { return caretVisible && ! isReadOnly(); } - - //============================================================================== - /** Enables/disables a vertical scrollbar. - - (This only applies when in multi-line mode). When the text gets too long to fit - in the component, a scrollbar can appear to allow it to be scrolled. Even when - this is enabled, the scrollbar will be hidden unless it's needed. - - By default the scrollbar is enabled. - */ - void setScrollbarsShown (bool shouldBeEnabled); - - /** Returns true if scrollbars are enabled. - @see setScrollbarsShown - */ - bool areScrollbarsShown() const noexcept { return scrollbarVisible; } - - - /** Changes the password character used to disguise the text. - - @param passwordCharacter if this is not zero, this character will be used as a replacement - for all characters that are drawn on screen - e.g. to create - a password-style textbox containing circular blobs instead of text, - you could set this value to 0x25cf, which is the unicode character - for a black splodge (not all fonts include this, though), or 0x2022, - which is a bullet (probably the best choice for linux). - */ - void setPasswordCharacter (juce_wchar passwordCharacter); - - /** Returns the current password character. - @see setPasswordCharacter - */ - juce_wchar getPasswordCharacter() const noexcept { return passwordCharacter; } - - - //============================================================================== - /** Allows a right-click menu to appear for the editor. - - (This defaults to being enabled). - - If enabled, right-clicking (or command-clicking on the Mac) will pop up a menu - of options such as cut/copy/paste, undo/redo, etc. - */ - void setPopupMenuEnabled (bool menuEnabled); - - /** Returns true if the right-click menu is enabled. - @see setPopupMenuEnabled - */ - bool isPopupMenuEnabled() const noexcept { return popupMenuEnabled; } - - /** Returns true if a popup-menu is currently being displayed. */ - bool isPopupMenuCurrentlyActive() const noexcept { return menuActive; } - - //============================================================================== - /** A set of colour IDs to use to change the colour of various aspects of the editor. - - These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() - methods. - - NB: You can also set the caret colour using CaretComponent::caretColourId - - @see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour - */ - enum ColourIds - { - backgroundColourId = 0x1000200, /**< The colour to use for the text component's background - this can be - transparent if necessary. */ - - textColourId = 0x1000201, /**< The colour that will be used when text is added to the editor. Note - that because the editor can contain multiple colours, calling this - method won't change the colour of existing text - to do that, use - the applyColourToAllText() method */ - - highlightColourId = 0x1000202, /**< The colour with which to fill the background of highlighted sections of - the text - this can be transparent if you don't want to show any - highlighting.*/ - - highlightedTextColourId = 0x1000203, /**< The colour with which to draw the text in highlighted sections. */ - - outlineColourId = 0x1000205, /**< If this is non-transparent, it will be used to draw a box around - the edge of the component. */ - - focusedOutlineColourId = 0x1000206, /**< If this is non-transparent, it will be used to draw a box around - the edge of the component when it has focus. */ - - shadowColourId = 0x1000207, /**< If this is non-transparent, it'll be used to draw an inner shadow - around the edge of the editor. */ - }; - - //============================================================================== - /** Sets the font to use for newly added text. - - This will change the font that will be used next time any text is added or entered - into the editor. It won't change the font of any existing text - to do that, use - applyFontToAllText() instead. - - @see applyFontToAllText - */ - void setFont (const Font& newFont); - - /** Applies a font to all the text in the editor. - - If the changeCurrentFont argument is true then this will also set the - new font as the font to be used for any new text that's added. - - @see setFont - */ - void applyFontToAllText (const Font& newFont, bool changeCurrentFont = true); - - /** Returns the font that's currently being used for new text. - - @see setFont - */ - const Font& getFont() const noexcept { return currentFont; } - - /** Applies a colour to all the text in the editor. - - If the changeCurrentTextColour argument is true then this will also set the - new colour as the colour to be used for any new text that's added. - */ - void applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour = true); - - //============================================================================== - /** If set to true, focusing on the editor will highlight all its text. - - (Set to false by default). - - This is useful for boxes where you expect the user to re-enter all the - text when they focus on the component, rather than editing what's already there. - */ - void setSelectAllWhenFocused (bool shouldSelectAll); - - /** When the text editor is empty, it can be set to display a message. - - This is handy for things like telling the user what to type in the box - the - string is only displayed, it's not taken to actually be the contents of - the editor. - */ - void setTextToShowWhenEmpty (const String& text, Colour colourToUse); - - //============================================================================== - /** Changes the size of the scrollbars that are used. - Handy if you need smaller scrollbars for a small text box. - */ - void setScrollBarThickness (int newThicknessPixels); - - //============================================================================== - /** - Receives callbacks from a TextEditor component when it changes. - - @see TextEditor::addListener - */ - class JUCE_API Listener - { - public: - /** Destructor. */ - virtual ~Listener() {} - - /** Called when the user changes the text in some way. */ - virtual void textEditorTextChanged (TextEditor&) {} - - /** Called when the user presses the return key. */ - virtual void textEditorReturnKeyPressed (TextEditor&) {} - - /** Called when the user presses the escape key. */ - virtual void textEditorEscapeKeyPressed (TextEditor&) {} - - /** Called when the text editor loses focus. */ - virtual void textEditorFocusLost (TextEditor&) {} - }; - - /** Registers a listener to be told when things happen to the text. - @see removeListener - */ - void addListener (Listener* newListener); - - /** Deregisters a listener. - @see addListener - */ - void removeListener (Listener* listenerToRemove); - - //============================================================================== - /** Returns the entire contents of the editor. */ - String getText() const; - - /** Returns a section of the contents of the editor. */ - String getTextInRange (const Range& textRange) const override; - - /** Returns true if there are no characters in the editor. - This is far more efficient than calling getText().isEmpty(). - */ - bool isEmpty() const; - - /** Sets the entire content of the editor. - - This will clear the editor and insert the given text (using the current text colour - and font). You can set the current text colour using - @code setColour (TextEditor::textColourId, ...); - @endcode - - @param newText the text to add - @param sendTextChangeMessage if true, this will cause a change message to - be sent to all the listeners. - @see insertTextAtCaret - */ - void setText (const String& newText, - bool sendTextChangeMessage = true); - - /** Returns a Value object that can be used to get or set the text. - - Bear in mind that this operate quite slowly if your text box contains large - amounts of text, as it needs to dynamically build the string that's involved. - It's best used for small text boxes. - */ - Value& getTextValue(); - - /** Inserts some text at the current caret position. - - If a section of the text is highlighted, it will be replaced by - this string, otherwise it will be inserted. - - To delete a section of text, you can use setHighlightedRegion() to - highlight it, and call insertTextAtCaret (String()). - - @see setCaretPosition, getCaretPosition, setHighlightedRegion - */ - void insertTextAtCaret (const String& textToInsert) override; - - /** Deletes all the text from the editor. */ - void clear(); - - /** Deletes the currently selected region. - This doesn't copy the deleted section to the clipboard - if you need to do that, call copy() first. - @see copy, paste, SystemClipboard - */ - void cut(); - - /** Copies the currently selected region to the clipboard. - @see cut, paste, SystemClipboard - */ - void copy(); - - /** Pastes the contents of the clipboard into the editor at the caret position. - @see cut, copy, SystemClipboard - */ - void paste(); - - //============================================================================== - /** Returns the current index of the caret. - @see setCaretPosition - */ - int getCaretPosition() const; - - /** Moves the caret to be in front of a given character. - @see getCaretPosition, moveCaretToEnd - */ - void setCaretPosition (int newIndex); - - /** Attempts to scroll the text editor so that the caret ends up at - a specified position. - - This won't affect the caret's position within the text, it tries to scroll - the entire editor vertically and horizontally so that the caret is sitting - at the given position (relative to the top-left of this component). - - Depending on the amount of text available, it might not be possible to - scroll far enough for the caret to reach this exact position, but it - will go as far as it can in that direction. - */ - void scrollEditorToPositionCaret (int desiredCaretX, int desiredCaretY); - - /** Get the graphical position of the caret. - - The rectangle returned is relative to the component's top-left corner. - @see scrollEditorToPositionCaret - */ - Rectangle getCaretRectangle() override; - - /** Selects a section of the text. */ - void setHighlightedRegion (const Range& newSelection) override; - - /** Returns the range of characters that are selected. - If nothing is selected, this will return an empty range. - @see setHighlightedRegion - */ - Range getHighlightedRegion() const override { return selection; } - - /** Returns the section of text that is currently selected. */ - String getHighlightedText() const; - - /** Finds the index of the character at a given position. - The coordinates are relative to the component's top-left. - */ - int getTextIndexAt (int x, int y); - - /** Counts the number of characters in the text. - - This is quicker than getting the text as a string if you just need to know - the length. - */ - int getTotalNumChars() const; - - /** Returns the total width of the text, as it is currently laid-out. - - This may be larger than the size of the TextEditor, and can change when - the TextEditor is resized or the text changes. - */ - int getTextWidth() const; - - /** Returns the maximum height of the text, as it is currently laid-out. - - This may be larger than the size of the TextEditor, and can change when - the TextEditor is resized or the text changes. - */ - int getTextHeight() const; - - /** Changes the size of the gap at the top and left-edge of the editor. - By default there's a gap of 4 pixels. - */ - void setIndents (int newLeftIndent, int newTopIndent); - - /** Changes the size of border left around the edge of the component. - @see getBorder - */ - void setBorder (const BorderSize& border); - - /** Returns the size of border around the edge of the component. - @see setBorder - */ - BorderSize getBorder() const; - - /** Used to disable the auto-scrolling which keeps the caret visible. - - If true (the default), the editor will scroll when the caret moves offscreen. If - set to false, it won't. - */ - void setScrollToShowCursor (bool shouldScrollToShowCaret); - - /** Sets the line spacing of the TextEditor. - - The default (and minimum) value is 1.0 and values > 1.0 will increase the line spacing as a - multiple of the line height e.g. for double-spacing call this method with an argument of 2.0. - */ - void setLineSpacing (float newLineSpacing) noexcept { lineSpacing = jmax (1.0f, newLineSpacing); } - - /** Returns the current line spacing of the TextEditor. */ - float getLineSpacing() const noexcept { return lineSpacing; } - - //============================================================================== - void moveCaretToEnd(); - bool moveCaretLeft (bool moveInWholeWordSteps, bool selecting); - bool moveCaretRight (bool moveInWholeWordSteps, bool selecting); - bool moveCaretUp (bool selecting); - bool moveCaretDown (bool selecting); - bool pageUp (bool selecting); - bool pageDown (bool selecting); - bool scrollDown(); - bool scrollUp(); - bool moveCaretToTop (bool selecting); - bool moveCaretToStartOfLine (bool selecting); - bool moveCaretToEnd (bool selecting); - bool moveCaretToEndOfLine (bool selecting); - bool deleteBackwards (bool moveInWholeWordSteps); - bool deleteForwards (bool moveInWholeWordSteps); - bool copyToClipboard(); - bool cutToClipboard(); - bool pasteFromClipboard(); - bool selectAll(); - bool undo(); - bool redo(); - - //============================================================================== - /** 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. - - 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 - 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); - - //============================================================================== - /** Base class for input filters that can be applied to a TextEditor to restrict - the text that can be entered. - */ - class JUCE_API InputFilter - { - public: - InputFilter() {} - virtual ~InputFilter() {} - - /** This method is called whenever text is entered into the editor. - An implementation of this class should should check the input string, - and return an edited version of it that should be used. - */ - virtual String filterNewText (TextEditor&, const String& newInput) = 0; - }; - - /** An input filter for a TextEditor that limits the length of text and/or the - characters that it may contain. - */ - class JUCE_API LengthAndCharacterRestriction : public InputFilter - { - public: - /** Creates a filter that limits the length of text, and/or the characters that it can contain. - @param maxNumChars if this is > 0, it sets a maximum length limit; if <= 0, no - limit is set - @param allowedCharacters if this is non-empty, then only characters that occur in - this string are allowed to be entered into the editor. - */ - LengthAndCharacterRestriction (int maxNumChars, const String& allowedCharacters); - - String filterNewText (TextEditor&, const String&) override; - - private: - String allowedCharacters; - int maxLength; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LengthAndCharacterRestriction) - }; - - /** Sets an input filter that should be applied to this editor. - The filter can be nullptr, to remove any existing filters. - If takeOwnership is true, then the filter will be owned and deleted by the editor - when no longer needed. - */ - void setInputFilter (InputFilter* newFilter, bool takeOwnership); - - /** Returns the current InputFilter, as set by setInputFilter(). */ - InputFilter* getInputFilter() const noexcept { return inputFilter; } - - /** Sets limits on the characters that can be entered. - This is just a shortcut that passes an instance of the LengthAndCharacterRestriction - class to setInputFilter(). - - @param maxTextLength if this is > 0, it sets a maximum length limit; if 0, no - limit is set - @param allowedCharacters if this is non-empty, then only characters that occur in - this string are allowed to be entered into the editor. - */ - void setInputRestrictions (int maxTextLength, - const String& allowedCharacters = String()); - - void setKeyboardType (VirtualKeyboardType type) noexcept { keyboardType = type; } - - //============================================================================== - /** This abstract base class is implemented by LookAndFeel classes to provide - TextEditor drawing functionality. - */ - struct JUCE_API LookAndFeelMethods - { - virtual ~LookAndFeelMethods() {} - - virtual void fillTextEditorBackground (Graphics&, int width, int height, TextEditor&) = 0; - virtual void drawTextEditorOutline (Graphics&, int width, int height, TextEditor&) = 0; - - virtual CaretComponent* createCaretComponent (Component* keyFocusOwner) = 0; - }; - - //============================================================================== - /** @internal */ - void paint (Graphics&) override; - /** @internal */ - void paintOverChildren (Graphics&) override; - /** @internal */ - void mouseDown (const MouseEvent&) override; - /** @internal */ - void mouseUp (const MouseEvent&) override; - /** @internal */ - void mouseDrag (const MouseEvent&) override; - /** @internal */ - void mouseDoubleClick (const MouseEvent&) override; - /** @internal */ - void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override; - /** @internal */ - bool keyPressed (const KeyPress&) override; - /** @internal */ - bool keyStateChanged (bool) override; - /** @internal */ - void focusGained (FocusChangeType) override; - /** @internal */ - void focusLost (FocusChangeType) override; - /** @internal */ - void resized() override; - /** @internal */ - void enablementChanged() override; - /** @internal */ - void colourChanged() override; - /** @internal */ - void lookAndFeelChanged() override; - /** @internal */ - void parentHierarchyChanged() override; - /** @internal */ - bool isTextInputActive() const override; - /** @internal */ - void setTemporaryUnderlining (const Array >&) override; - /** @internal */ - VirtualKeyboardType getKeyboardType() override { return keyboardType; } - -protected: - //============================================================================== - /** Scrolls the minimum distance needed to get the caret into view. */ - void scrollToMakeSureCursorIsVisible(); - - /** Used internally to dispatch a text-change message. */ - void textChanged(); - - /** Begins a new transaction in the UndoManager. */ - void newTransaction(); - - /** Can be overridden to intercept return key presses directly */ - virtual void returnPressed(); - - /** Can be overridden to intercept escape key presses directly */ - virtual void escapePressed(); - -private: - //============================================================================== - class Iterator; - JUCE_PUBLIC_IN_DLL_BUILD (class UniformTextSection) - class TextHolderComponent; - class InsertAction; - class RemoveAction; - friend class InsertAction; - friend class RemoveAction; - - ScopedPointer viewport; - TextHolderComponent* textHolder; - BorderSize borderSize { 1, 1, 1, 3 }; - - bool readOnly = false; - bool caretVisible = true; - bool multiline = false; - bool wordWrap = false; - bool returnKeyStartsNewLine = false; - bool popupMenuEnabled = true; - bool selectAllTextWhenFocused = false; - bool scrollbarVisible = true; - bool wasFocused = false; - bool keepCaretOnScreen = true; - bool tabKeyUsed = false; - bool menuActive = false; - bool valueTextNeedsUpdating = false; - bool consumeEscAndReturnKeys = true; - - UndoManager undoManager; - ScopedPointer caret; - Range selection; - int leftIndent = 4, topIndent = 4; - unsigned int lastTransactionTime = 0; - Font currentFont { 14.0f }; - mutable int totalNumChars = 0; - int caretPosition = 0; - OwnedArray sections; - String textToShowWhenEmpty; - Colour colourForTextWhenEmpty; - juce_wchar passwordCharacter; - OptionalScopedPointer inputFilter; - Value textValue; - VirtualKeyboardType keyboardType; - float lineSpacing = 1.0f; - - enum - { - notDragging, - draggingSelectionStart, - draggingSelectionEnd - } dragType; - - ListenerList listeners; - Array > underlinedSections; - - void moveCaret (int newCaretPos); - void moveCaretTo (int newPosition, bool isSelecting); - void recreateCaret(); - void handleCommandMessage (int) override; - void coalesceSimilarSections(); - void splitSection (int sectionIndex, int charToSplitAt); - void clearInternal (UndoManager*); - void insert (const String&, int insertIndex, const Font&, const Colour, UndoManager*, int newCaretPos); - void reinsert (int insertIndex, const OwnedArray&); - void remove (Range range, UndoManager*, int caretPositionToMoveTo); - void getCharPosition (int index, float& x, float& y, float& lineHeight) const; - void updateCaretPosition(); - void updateValueFromText(); - void textWasChangedByValue(); - int indexAtPosition (float x, float y); - int findWordBreakAfter (int position) const; - int findWordBreakBefore (int position) const; - bool moveCaretWithTransaction (int newPos, bool selecting); - friend class TextHolderComponent; - friend class TextEditorViewport; - void drawContent (Graphics&); - void updateTextHolderSize(); - float getWordWrapWidth() const; - void timerCallbackInt(); - void repaintText (Range); - void scrollByLines (int deltaLines); - bool undoOrRedo (bool shouldUndo); - UndoManager* getUndoManager() noexcept; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextEditor) -}; - -/** This typedef is just for compatibility with old code - newer code should use the TextEditor::Listener class directly. */ -typedef TextEditor::Listener TextEditorListener; - -} // namespace juce +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + An editable text box. + + A TextEditor can either be in single- or multi-line mode, and supports mixed + fonts and colours. + + @see TextEditor::Listener, Label +*/ +class JUCE_API TextEditor : public Component, + public TextInputTarget, + public SettableTooltipClient +{ +public: + //============================================================================== + /** Creates a new, empty text editor. + + @param componentName the name to pass to the component for it to use as its name + @param passwordCharacter if this is not zero, this character will be used as a replacement + for all characters that are drawn on screen - e.g. to create + a password-style textbox containing circular blobs instead of text, + you could set this value to 0x25cf, which is the unicode character + for a black splodge (not all fonts include this, though), or 0x2022, + which is a bullet (probably the best choice for linux). + */ + explicit TextEditor (const String& componentName = String(), + juce_wchar passwordCharacter = 0); + + /** Destructor. */ + ~TextEditor(); + + //============================================================================== + /** Puts the editor into either multi- or single-line mode. + + By default, the editor will be in single-line mode, so use this if you need a multi-line + editor. + + See also the setReturnKeyStartsNewLine() method, which will also need to be turned + on if you want a multi-line editor with line-breaks. + + @see isMultiLine, setReturnKeyStartsNewLine + */ + void setMultiLine (bool shouldBeMultiLine, + bool shouldWordWrap = true); + + /** Returns true if the editor is in multi-line mode. */ + bool isMultiLine() const; + + //============================================================================== + /** Changes the behaviour of the return key. + + If set to true, the return key will insert a new-line into the text; if false + it will trigger a call to the TextEditor::Listener::textEditorReturnKeyPressed() + method. By default this is set to false, and when true it will only insert + new-lines when in multi-line mode (see setMultiLine()). + */ + void setReturnKeyStartsNewLine (bool shouldStartNewLine); + + /** Returns the value set by setReturnKeyStartsNewLine(). + See setReturnKeyStartsNewLine() for more info. + */ + bool getReturnKeyStartsNewLine() const { return returnKeyStartsNewLine; } + + /** Indicates whether the tab key should be accepted and used to input a tab character, + or whether it gets ignored. + + By default the tab key is ignored, so that it can be used to switch keyboard focus + between components. + */ + void setTabKeyUsedAsCharacter (bool shouldTabKeyBeUsed); + + /** Returns true if the tab key is being used for input. + @see setTabKeyUsedAsCharacter + */ + bool isTabKeyUsedAsCharacter() const { return tabKeyUsed; } + + /** This can be used to change whether escape and return keypress events are + propagated up to the parent component. + The default here is true, meaning that these events are not allowed to reach the + parent, but you may want to allow them through so that they can trigger other + actions, e.g. closing a dialog box, etc. + */ + void setEscapeAndReturnKeysConsumed (bool shouldBeConsumed) noexcept; + + //============================================================================== + /** Changes the editor to read-only mode. + + By default, the text editor is not read-only. If you're making it read-only, you + might also want to call setCaretVisible (false) to get rid of the caret. + + The text can still be highlighted and copied when in read-only mode. + + @see isReadOnly, setCaretVisible + */ + void setReadOnly (bool shouldBeReadOnly); + + /** Returns true if the editor is in read-only mode. */ + bool isReadOnly() const noexcept; + + //============================================================================== + /** Makes the caret visible or invisible. + By default the caret is visible. + @see setCaretColour, setCaretPosition + */ + void setCaretVisible (bool shouldBeVisible); + + /** Returns true if the caret is enabled. + @see setCaretVisible + */ + bool isCaretVisible() const noexcept { return caretVisible && ! isReadOnly(); } + + //============================================================================== + /** Enables/disables a vertical scrollbar. + + (This only applies when in multi-line mode). When the text gets too long to fit + in the component, a scrollbar can appear to allow it to be scrolled. Even when + this is enabled, the scrollbar will be hidden unless it's needed. + + By default the scrollbar is enabled. + */ + void setScrollbarsShown (bool shouldBeEnabled); + + /** Returns true if scrollbars are enabled. + @see setScrollbarsShown + */ + bool areScrollbarsShown() const noexcept { return scrollbarVisible; } + + + /** Changes the password character used to disguise the text. + + @param passwordCharacter if this is not zero, this character will be used as a replacement + for all characters that are drawn on screen - e.g. to create + a password-style textbox containing circular blobs instead of text, + you could set this value to 0x25cf, which is the unicode character + for a black splodge (not all fonts include this, though), or 0x2022, + which is a bullet (probably the best choice for linux). + */ + void setPasswordCharacter (juce_wchar passwordCharacter); + + /** Returns the current password character. + @see setPasswordCharacter + */ + juce_wchar getPasswordCharacter() const noexcept { return passwordCharacter; } + + + //============================================================================== + /** Allows a right-click menu to appear for the editor. + + (This defaults to being enabled). + + If enabled, right-clicking (or command-clicking on the Mac) will pop up a menu + of options such as cut/copy/paste, undo/redo, etc. + */ + void setPopupMenuEnabled (bool menuEnabled); + + /** Returns true if the right-click menu is enabled. + @see setPopupMenuEnabled + */ + bool isPopupMenuEnabled() const noexcept { return popupMenuEnabled; } + + /** Returns true if a popup-menu is currently being displayed. */ + bool isPopupMenuCurrentlyActive() const noexcept { return menuActive; } + + //============================================================================== + /** A set of colour IDs to use to change the colour of various aspects of the editor. + + These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() + methods. + + NB: You can also set the caret colour using CaretComponent::caretColourId + + @see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour + */ + enum ColourIds + { + backgroundColourId = 0x1000200, /**< The colour to use for the text component's background - this can be + transparent if necessary. */ + + textColourId = 0x1000201, /**< The colour that will be used when text is added to the editor. Note + that because the editor can contain multiple colours, calling this + method won't change the colour of existing text - to do that, use + the applyColourToAllText() method */ + + highlightColourId = 0x1000202, /**< The colour with which to fill the background of highlighted sections of + the text - this can be transparent if you don't want to show any + highlighting.*/ + + highlightedTextColourId = 0x1000203, /**< The colour with which to draw the text in highlighted sections. */ + + outlineColourId = 0x1000205, /**< If this is non-transparent, it will be used to draw a box around + the edge of the component. */ + + focusedOutlineColourId = 0x1000206, /**< If this is non-transparent, it will be used to draw a box around + the edge of the component when it has focus. */ + + shadowColourId = 0x1000207, /**< If this is non-transparent, it'll be used to draw an inner shadow + around the edge of the editor. */ + }; + + //============================================================================== + /** Sets the font to use for newly added text. + + This will change the font that will be used next time any text is added or entered + into the editor. It won't change the font of any existing text - to do that, use + applyFontToAllText() instead. + + @see applyFontToAllText + */ + void setFont (const Font& newFont); + + /** Applies a font to all the text in the editor. + + If the changeCurrentFont argument is true then this will also set the + new font as the font to be used for any new text that's added. + + @see setFont + */ + void applyFontToAllText (const Font& newFont, bool changeCurrentFont = true); + + /** Returns the font that's currently being used for new text. + + @see setFont + */ + const Font& getFont() const noexcept { return currentFont; } + + /** Applies a colour to all the text in the editor. + + If the changeCurrentTextColour argument is true then this will also set the + new colour as the colour to be used for any new text that's added. + */ + void applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour = true); + + //============================================================================== + /** If set to true, focusing on the editor will highlight all its text. + + (Set to false by default). + + This is useful for boxes where you expect the user to re-enter all the + text when they focus on the component, rather than editing what's already there. + */ + void setSelectAllWhenFocused (bool shouldSelectAll); + + /** When the text editor is empty, it can be set to display a message. + + This is handy for things like telling the user what to type in the box - the + string is only displayed, it's not taken to actually be the contents of + the editor. + */ + void setTextToShowWhenEmpty (const String& text, Colour colourToUse); + + //============================================================================== + /** Changes the size of the scrollbars that are used. + Handy if you need smaller scrollbars for a small text box. + */ + void setScrollBarThickness (int newThicknessPixels); + + //============================================================================== + /** + Receives callbacks from a TextEditor component when it changes. + + @see TextEditor::addListener + */ + class JUCE_API Listener + { + public: + /** Destructor. */ + virtual ~Listener() {} + + /** Called when the user changes the text in some way. */ + virtual void textEditorTextChanged (TextEditor&) {} + + /** Called when the user presses the return key. */ + virtual void textEditorReturnKeyPressed (TextEditor&) {} + + /** Called when the user presses the escape key. */ + virtual void textEditorEscapeKeyPressed (TextEditor&) {} + + /** Called when the text editor loses focus. */ + virtual void textEditorFocusLost (TextEditor&) {} + }; + + /** Registers a listener to be told when things happen to the text. + @see removeListener + */ + void addListener (Listener* newListener); + + /** Deregisters a listener. + @see addListener + */ + void removeListener (Listener* listenerToRemove); + + //============================================================================== + /** Returns the entire contents of the editor. */ + String getText() const; + + /** Returns a section of the contents of the editor. */ + String getTextInRange (const Range& textRange) const override; + + /** Returns true if there are no characters in the editor. + This is far more efficient than calling getText().isEmpty(). + */ + bool isEmpty() const; + + /** Sets the entire content of the editor. + + This will clear the editor and insert the given text (using the current text colour + and font). You can set the current text colour using + @code setColour (TextEditor::textColourId, ...); + @endcode + + @param newText the text to add + @param sendTextChangeMessage if true, this will cause a change message to + be sent to all the listeners. + @see insertTextAtCaret + */ + void setText (const String& newText, + bool sendTextChangeMessage = true); + + /** Returns a Value object that can be used to get or set the text. + + Bear in mind that this operate quite slowly if your text box contains large + amounts of text, as it needs to dynamically build the string that's involved. + It's best used for small text boxes. + */ + Value& getTextValue(); + + /** Inserts some text at the current caret position. + + If a section of the text is highlighted, it will be replaced by + this string, otherwise it will be inserted. + + To delete a section of text, you can use setHighlightedRegion() to + highlight it, and call insertTextAtCaret (String()). + + @see setCaretPosition, getCaretPosition, setHighlightedRegion + */ + void insertTextAtCaret (const String& textToInsert) override; + + /** Deletes all the text from the editor. */ + void clear(); + + /** Deletes the currently selected region. + This doesn't copy the deleted section to the clipboard - if you need to do that, call copy() first. + @see copy, paste, SystemClipboard + */ + void cut(); + + /** Copies the currently selected region to the clipboard. + @see cut, paste, SystemClipboard + */ + void copy(); + + /** Pastes the contents of the clipboard into the editor at the caret position. + @see cut, copy, SystemClipboard + */ + void paste(); + + //============================================================================== + /** Returns the current index of the caret. + @see setCaretPosition + */ + int getCaretPosition() const; + + /** Moves the caret to be in front of a given character. + @see getCaretPosition, moveCaretToEnd + */ + void setCaretPosition (int newIndex); + + /** Attempts to scroll the text editor so that the caret ends up at + a specified position. + + This won't affect the caret's position within the text, it tries to scroll + the entire editor vertically and horizontally so that the caret is sitting + at the given position (relative to the top-left of this component). + + Depending on the amount of text available, it might not be possible to + scroll far enough for the caret to reach this exact position, but it + will go as far as it can in that direction. + */ + void scrollEditorToPositionCaret (int desiredCaretX, int desiredCaretY); + + /** Get the graphical position of the caret. + + The rectangle returned is relative to the component's top-left corner. + @see scrollEditorToPositionCaret + */ + Rectangle getCaretRectangle() override; + + /** Selects a section of the text. */ + void setHighlightedRegion (const Range& newSelection) override; + + /** Returns the range of characters that are selected. + If nothing is selected, this will return an empty range. + @see setHighlightedRegion + */ + Range getHighlightedRegion() const override { return selection; } + + /** Returns the section of text that is currently selected. */ + String getHighlightedText() const; + + /** Finds the index of the character at a given position. + The coordinates are relative to the component's top-left. + */ + int getTextIndexAt (int x, int y); + + /** Counts the number of characters in the text. + + This is quicker than getting the text as a string if you just need to know + the length. + */ + int getTotalNumChars() const; + + /** Returns the total width of the text, as it is currently laid-out. + + This may be larger than the size of the TextEditor, and can change when + the TextEditor is resized or the text changes. + */ + int getTextWidth() const; + + /** Returns the maximum height of the text, as it is currently laid-out. + + This may be larger than the size of the TextEditor, and can change when + the TextEditor is resized or the text changes. + */ + int getTextHeight() const; + + /** Changes the size of the gap at the top and left-edge of the editor. + By default there's a gap of 4 pixels. + */ + void setIndents (int newLeftIndent, int newTopIndent); + + /** Changes the size of border left around the edge of the component. + @see getBorder + */ + void setBorder (const BorderSize& border); + + /** Returns the size of border around the edge of the component. + @see setBorder + */ + BorderSize getBorder() const; + + /** Used to disable the auto-scrolling which keeps the caret visible. + + If true (the default), the editor will scroll when the caret moves offscreen. If + set to false, it won't. + */ + void setScrollToShowCursor (bool shouldScrollToShowCaret); + + /** Sets the line spacing of the TextEditor. + + The default (and minimum) value is 1.0 and values > 1.0 will increase the line spacing as a + multiple of the line height e.g. for double-spacing call this method with an argument of 2.0. + */ + void setLineSpacing (float newLineSpacing) noexcept { lineSpacing = jmax (1.0f, newLineSpacing); } + + /** Returns the current line spacing of the TextEditor. */ + float getLineSpacing() const noexcept { return lineSpacing; } + + //============================================================================== + void moveCaretToEnd(); + bool moveCaretLeft (bool moveInWholeWordSteps, bool selecting); + bool moveCaretRight (bool moveInWholeWordSteps, bool selecting); + bool moveCaretUp (bool selecting); + bool moveCaretDown (bool selecting); + bool pageUp (bool selecting); + bool pageDown (bool selecting); + bool scrollDown(); + bool scrollUp(); + bool moveCaretToTop (bool selecting); + bool moveCaretToStartOfLine (bool selecting); + bool moveCaretToEnd (bool selecting); + bool moveCaretToEndOfLine (bool selecting); + bool deleteBackwards (bool moveInWholeWordSteps); + bool deleteForwards (bool moveInWholeWordSteps); + bool copyToClipboard(); + bool cutToClipboard(); + bool pasteFromClipboard(); + bool selectAll(); + bool undo(); + bool redo(); + + //============================================================================== + /** 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. + + 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 + 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); + + //============================================================================== + /** Base class for input filters that can be applied to a TextEditor to restrict + the text that can be entered. + */ + class JUCE_API InputFilter + { + public: + InputFilter() {} + virtual ~InputFilter() {} + + /** This method is called whenever text is entered into the editor. + An implementation of this class should should check the input string, + and return an edited version of it that should be used. + */ + virtual String filterNewText (TextEditor&, const String& newInput) = 0; + }; + + /** An input filter for a TextEditor that limits the length of text and/or the + characters that it may contain. + */ + class JUCE_API LengthAndCharacterRestriction : public InputFilter + { + public: + /** Creates a filter that limits the length of text, and/or the characters that it can contain. + @param maxNumChars if this is > 0, it sets a maximum length limit; if <= 0, no + limit is set + @param allowedCharacters if this is non-empty, then only characters that occur in + this string are allowed to be entered into the editor. + */ + LengthAndCharacterRestriction (int maxNumChars, const String& allowedCharacters); + + String filterNewText (TextEditor&, const String&) override; + + private: + String allowedCharacters; + int maxLength; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LengthAndCharacterRestriction) + }; + + /** Sets an input filter that should be applied to this editor. + The filter can be nullptr, to remove any existing filters. + If takeOwnership is true, then the filter will be owned and deleted by the editor + when no longer needed. + */ + void setInputFilter (InputFilter* newFilter, bool takeOwnership); + + /** Returns the current InputFilter, as set by setInputFilter(). */ + InputFilter* getInputFilter() const noexcept { return inputFilter; } + + /** Sets limits on the characters that can be entered. + This is just a shortcut that passes an instance of the LengthAndCharacterRestriction + class to setInputFilter(). + + @param maxTextLength if this is > 0, it sets a maximum length limit; if 0, no + limit is set + @param allowedCharacters if this is non-empty, then only characters that occur in + this string are allowed to be entered into the editor. + */ + void setInputRestrictions (int maxTextLength, + const String& allowedCharacters = String()); + + void setKeyboardType (VirtualKeyboardType type) noexcept { keyboardType = type; } + + //============================================================================== + /** This abstract base class is implemented by LookAndFeel classes to provide + TextEditor drawing functionality. + */ + struct JUCE_API LookAndFeelMethods + { + virtual ~LookAndFeelMethods() {} + + virtual void fillTextEditorBackground (Graphics&, int width, int height, TextEditor&) = 0; + virtual void drawTextEditorOutline (Graphics&, int width, int height, TextEditor&) = 0; + + virtual CaretComponent* createCaretComponent (Component* keyFocusOwner) = 0; + }; + + //============================================================================== + /** @internal */ + void paint (Graphics&) override; + /** @internal */ + void paintOverChildren (Graphics&) override; + /** @internal */ + void mouseDown (const MouseEvent&) override; + /** @internal */ + void mouseUp (const MouseEvent&) override; + /** @internal */ + void mouseDrag (const MouseEvent&) override; + /** @internal */ + void mouseDoubleClick (const MouseEvent&) override; + /** @internal */ + void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override; + /** @internal */ + bool keyPressed (const KeyPress&) override; + /** @internal */ + bool keyStateChanged (bool) override; + /** @internal */ + void focusGained (FocusChangeType) override; + /** @internal */ + void focusLost (FocusChangeType) override; + /** @internal */ + void resized() override; + /** @internal */ + void enablementChanged() override; + /** @internal */ + void lookAndFeelChanged() override; + /** @internal */ + void parentHierarchyChanged() override; + /** @internal */ + bool isTextInputActive() const override; + /** @internal */ + void setTemporaryUnderlining (const Array >&) override; + /** @internal */ + VirtualKeyboardType getKeyboardType() override { return keyboardType; } + +protected: + //============================================================================== + /** Scrolls the minimum distance needed to get the caret into view. */ + void scrollToMakeSureCursorIsVisible(); + + /** Used internally to dispatch a text-change message. */ + void textChanged(); + + /** Begins a new transaction in the UndoManager. */ + void newTransaction(); + + /** Can be overridden to intercept return key presses directly */ + virtual void returnPressed(); + + /** Can be overridden to intercept escape key presses directly */ + virtual void escapePressed(); + +private: + //============================================================================== + class Iterator; + JUCE_PUBLIC_IN_DLL_BUILD (class UniformTextSection) + class TextHolderComponent; + class InsertAction; + class RemoveAction; + friend class InsertAction; + friend class RemoveAction; + + ScopedPointer viewport; + TextHolderComponent* textHolder; + BorderSize borderSize { 1, 1, 1, 3 }; + + bool readOnly = false; + bool caretVisible = true; + bool multiline = false; + bool wordWrap = false; + bool returnKeyStartsNewLine = false; + bool popupMenuEnabled = true; + bool selectAllTextWhenFocused = false; + bool scrollbarVisible = true; + bool wasFocused = false; + bool keepCaretOnScreen = true; + bool tabKeyUsed = false; + bool menuActive = false; + bool valueTextNeedsUpdating = false; + bool consumeEscAndReturnKeys = true; + + UndoManager undoManager; + ScopedPointer caret; + Range selection; + int leftIndent = 4, topIndent = 4; + unsigned int lastTransactionTime = 0; + Font currentFont { 14.0f }; + mutable int totalNumChars = 0; + int caretPosition = 0; + OwnedArray sections; + String textToShowWhenEmpty; + Colour colourForTextWhenEmpty; + juce_wchar passwordCharacter; + OptionalScopedPointer inputFilter; + Value textValue; + VirtualKeyboardType keyboardType = TextInputTarget::textKeyboard; + float lineSpacing = 1.0f; + + enum DragType + { + notDragging, + draggingSelectionStart, + draggingSelectionEnd + }; + + DragType dragType = notDragging; + + ListenerList listeners; + Array> underlinedSections; + + void moveCaret (int newCaretPos); + void moveCaretTo (int newPosition, bool isSelecting); + void recreateCaret(); + void handleCommandMessage (int) override; + void coalesceSimilarSections(); + void splitSection (int sectionIndex, int charToSplitAt); + void clearInternal (UndoManager*); + void insert (const String&, int insertIndex, const Font&, const Colour, UndoManager*, int newCaretPos); + void reinsert (int insertIndex, const OwnedArray&); + void remove (Range range, UndoManager*, int caretPositionToMoveTo); + void getCharPosition (int index, float& x, float& y, float& lineHeight) const; + void updateCaretPosition(); + void updateValueFromText(); + void textWasChangedByValue(); + int indexAtPosition (float x, float y); + int findWordBreakAfter (int position) const; + int findWordBreakBefore (int position) const; + bool moveCaretWithTransaction (int newPos, bool selecting); + friend class TextHolderComponent; + friend class TextEditorViewport; + void drawContent (Graphics&); + void updateTextHolderSize(); + float getWordWrapWidth() const; + void timerCallbackInt(); + void repaintText (Range); + void scrollByLines (int deltaLines); + bool undoOrRedo (bool shouldUndo); + UndoManager* getUndoManager() noexcept; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextEditor) +}; + +/** This typedef is just for compatibility with old code - newer code should use the TextEditor::Listener class directly. */ +typedef TextEditor::Listener TextEditorListener; + +} // namespace juce