Add remaining files
[juce-lv2.git] / juce / source / src / gui / components / controls / juce_TextEditor.cpp
blobbb415404e9a46d2d6217f7f1ee75bf0f7f978d2c
1 /*
2 ==============================================================================
4 This file is part of the JUCE library - "Jules' Utility Class Extensions"
5 Copyright 2004-11 by Raw Material Software Ltd.
7 ------------------------------------------------------------------------------
9 JUCE can be redistributed and/or modified under the terms of the GNU General
10 Public License (Version 2), as published by the Free Software Foundation.
11 A copy of the license is included in the JUCE distribution, or can be found
12 online at www.gnu.org/licenses.
14 JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
15 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 A PARTICULAR PURPOSE. See the GNU General Public License for more details.
18 ------------------------------------------------------------------------------
20 To release a closed-source product which uses JUCE, commercial licenses are
21 available: visit www.rawmaterialsoftware.com/juce for more information.
23 ==============================================================================
26 #include "../../../core/juce_StandardHeader.h"
28 BEGIN_JUCE_NAMESPACE
30 #include "juce_TextEditor.h"
31 #include "../windows/juce_ComponentPeer.h"
32 #include "../../graphics/fonts/juce_GlyphArrangement.h"
33 #include "../../../utilities/juce_SystemClipboard.h"
34 #include "../../../core/juce_Time.h"
35 #include "../../../text/juce_LocalisedStrings.h"
36 #include "../../../io/streams/juce_MemoryOutputStream.h"
37 #include "../lookandfeel/juce_LookAndFeel.h"
38 #include "../keyboard/juce_TextEditorKeyMapper.h"
41 //==============================================================================
42 // a word or space that can't be broken down any further
43 struct TextAtom
45 //==============================================================================
46 String atomText;
47 float width;
48 int numChars;
50 //==============================================================================
51 bool isWhitespace() const { return CharacterFunctions::isWhitespace (atomText[0]); }
52 bool isNewLine() const { return atomText[0] == '\r' || atomText[0] == '\n'; }
54 String getText (const juce_wchar passwordCharacter) const
56 if (passwordCharacter == 0)
57 return atomText;
58 else
59 return String::repeatedString (String::charToString (passwordCharacter),
60 atomText.length());
63 String getTrimmedText (const juce_wchar passwordCharacter) const
65 if (passwordCharacter == 0)
66 return atomText.substring (0, numChars);
67 else if (isNewLine())
68 return String::empty;
69 else
70 return String::repeatedString (String::charToString (passwordCharacter), numChars);
74 //==============================================================================
75 // a run of text with a single font and colour
76 class TextEditor::UniformTextSection
78 public:
79 //==============================================================================
80 UniformTextSection (const String& text,
81 const Font& font_,
82 const Colour& colour_,
83 const juce_wchar passwordCharacter)
84 : font (font_),
85 colour (colour_)
87 initialiseAtoms (text, passwordCharacter);
90 UniformTextSection (const UniformTextSection& other)
91 : font (other.font),
92 colour (other.colour)
94 atoms.ensureStorageAllocated (other.atoms.size());
96 for (int i = 0; i < other.atoms.size(); ++i)
97 atoms.add (new TextAtom (*other.atoms.getUnchecked(i)));
100 ~UniformTextSection()
102 // (no need to delete the atoms, as they're explicitly deleted by the caller)
105 void clear()
107 for (int i = atoms.size(); --i >= 0;)
108 delete getAtom(i);
110 atoms.clear();
113 int getNumAtoms() const
115 return atoms.size();
118 TextAtom* getAtom (const int index) const noexcept
120 return atoms.getUnchecked (index);
123 void append (const UniformTextSection& other, const juce_wchar passwordCharacter)
125 if (other.atoms.size() > 0)
127 TextAtom* const lastAtom = atoms.getLast();
128 int i = 0;
130 if (lastAtom != nullptr)
132 if (! CharacterFunctions::isWhitespace (lastAtom->atomText.getLastCharacter()))
134 TextAtom* const first = other.getAtom(0);
136 if (! CharacterFunctions::isWhitespace (first->atomText[0]))
138 lastAtom->atomText += first->atomText;
139 lastAtom->numChars = (uint16) (lastAtom->numChars + first->numChars);
140 lastAtom->width = font.getStringWidthFloat (lastAtom->getText (passwordCharacter));
141 delete first;
142 ++i;
147 atoms.ensureStorageAllocated (atoms.size() + other.atoms.size() - i);
149 while (i < other.atoms.size())
151 atoms.add (other.getAtom(i));
152 ++i;
157 UniformTextSection* split (const int indexToBreakAt,
158 const juce_wchar passwordCharacter)
160 UniformTextSection* const section2 = new UniformTextSection (String::empty,
161 font, colour,
162 passwordCharacter);
163 int index = 0;
165 for (int i = 0; i < atoms.size(); ++i)
167 TextAtom* const atom = getAtom(i);
169 const int nextIndex = index + atom->numChars;
171 if (index == indexToBreakAt)
173 int j;
174 for (j = i; j < atoms.size(); ++j)
175 section2->atoms.add (getAtom (j));
177 for (j = atoms.size(); --j >= i;)
178 atoms.remove (j);
180 break;
182 else if (indexToBreakAt >= index && indexToBreakAt < nextIndex)
184 TextAtom* const secondAtom = new TextAtom();
186 secondAtom->atomText = atom->atomText.substring (indexToBreakAt - index);
187 secondAtom->width = font.getStringWidthFloat (secondAtom->getText (passwordCharacter));
188 secondAtom->numChars = (uint16) secondAtom->atomText.length();
190 section2->atoms.add (secondAtom);
192 atom->atomText = atom->atomText.substring (0, indexToBreakAt - index);
193 atom->width = font.getStringWidthFloat (atom->getText (passwordCharacter));
194 atom->numChars = (uint16) (indexToBreakAt - index);
196 int j;
197 for (j = i + 1; j < atoms.size(); ++j)
198 section2->atoms.add (getAtom (j));
200 for (j = atoms.size(); --j > i;)
201 atoms.remove (j);
203 break;
206 index = nextIndex;
209 return section2;
212 void appendAllText (MemoryOutputStream& mo) const
214 for (int i = 0; i < atoms.size(); ++i)
215 mo << getAtom(i)->atomText;
218 void appendSubstring (MemoryOutputStream& mo, const Range<int>& range) const
220 int index = 0;
221 for (int i = 0; i < atoms.size(); ++i)
223 const TextAtom* const atom = getAtom (i);
224 const int nextIndex = index + atom->numChars;
226 if (range.getStart() < nextIndex)
228 if (range.getEnd() <= index)
229 break;
231 const Range<int> r ((range - index).getIntersectionWith (Range<int> (0, (int) atom->numChars)));
233 if (! r.isEmpty())
234 mo << atom->atomText.substring (r.getStart(), r.getEnd());
237 index = nextIndex;
241 int getTotalLength() const
243 int total = 0;
245 for (int i = atoms.size(); --i >= 0;)
246 total += getAtom(i)->numChars;
248 return total;
251 void setFont (const Font& newFont,
252 const juce_wchar passwordCharacter)
254 if (font != newFont)
256 font = newFont;
258 for (int i = atoms.size(); --i >= 0;)
260 TextAtom* const atom = atoms.getUnchecked(i);
261 atom->width = newFont.getStringWidthFloat (atom->getText (passwordCharacter));
266 //==============================================================================
267 Font font;
268 Colour colour;
270 private:
271 Array <TextAtom*> atoms;
273 //==============================================================================
274 void initialiseAtoms (const String& textToParse,
275 const juce_wchar passwordCharacter)
277 String::CharPointerType text (textToParse.getCharPointer());
279 while (! text.isEmpty())
281 int numChars = 0;
282 String::CharPointerType start (text);
284 // create a whitespace atom unless it starts with non-ws
285 if (text.isWhitespace() && *text != '\r' && *text != '\n')
289 ++text;
290 ++numChars;
292 while (text.isWhitespace() && *text != '\r' && *text != '\n');
294 else
296 if (*text == '\r')
298 ++text;
299 ++numChars;
301 if (*text == '\n')
303 ++start;
304 ++text;
307 else if (*text == '\n')
309 ++text;
310 ++numChars;
312 else
314 while (! (text.isEmpty() || text.isWhitespace()))
316 ++text;
317 ++numChars;
322 TextAtom* const atom = new TextAtom();
323 atom->atomText = String (start, numChars);
325 atom->width = font.getStringWidthFloat (atom->getText (passwordCharacter));
326 atom->numChars = (uint16) numChars;
328 atoms.add (atom);
332 UniformTextSection& operator= (const UniformTextSection& other);
333 JUCE_LEAK_DETECTOR (UniformTextSection);
336 //==============================================================================
337 class TextEditor::Iterator
339 public:
340 //==============================================================================
341 Iterator (const Array <UniformTextSection*>& sections_,
342 const float wordWrapWidth_,
343 const juce_wchar passwordCharacter_)
344 : indexInText (0),
345 lineY (0),
346 lineHeight (0),
347 maxDescent (0),
348 atomX (0),
349 atomRight (0),
350 atom (0),
351 currentSection (nullptr),
352 sections (sections_),
353 sectionIndex (0),
354 atomIndex (0),
355 wordWrapWidth (wordWrapWidth_),
356 passwordCharacter (passwordCharacter_)
358 jassert (wordWrapWidth_ > 0);
360 if (sections.size() > 0)
362 currentSection = sections.getUnchecked (sectionIndex);
364 if (currentSection != nullptr)
365 beginNewLine();
369 Iterator (const Iterator& other)
370 : indexInText (other.indexInText),
371 lineY (other.lineY),
372 lineHeight (other.lineHeight),
373 maxDescent (other.maxDescent),
374 atomX (other.atomX),
375 atomRight (other.atomRight),
376 atom (other.atom),
377 currentSection (other.currentSection),
378 sections (other.sections),
379 sectionIndex (other.sectionIndex),
380 atomIndex (other.atomIndex),
381 wordWrapWidth (other.wordWrapWidth),
382 passwordCharacter (other.passwordCharacter),
383 tempAtom (other.tempAtom)
387 //==============================================================================
388 bool next()
390 if (atom == &tempAtom)
392 const int numRemaining = tempAtom.atomText.length() - tempAtom.numChars;
394 if (numRemaining > 0)
396 tempAtom.atomText = tempAtom.atomText.substring (tempAtom.numChars);
398 atomX = 0;
400 if (tempAtom.numChars > 0)
401 lineY += lineHeight;
403 indexInText += tempAtom.numChars;
405 GlyphArrangement g;
406 g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f);
408 int split;
409 for (split = 0; split < g.getNumGlyphs(); ++split)
410 if (shouldWrap (g.getGlyph (split).getRight()))
411 break;
413 if (split > 0 && split <= numRemaining)
415 tempAtom.numChars = (uint16) split;
416 tempAtom.width = g.getGlyph (split - 1).getRight();
417 atomRight = atomX + tempAtom.width;
418 return true;
423 bool forceNewLine = false;
425 if (sectionIndex >= sections.size())
427 moveToEndOfLastAtom();
428 return false;
430 else if (atomIndex >= currentSection->getNumAtoms() - 1)
432 if (atomIndex >= currentSection->getNumAtoms())
434 if (++sectionIndex >= sections.size())
436 moveToEndOfLastAtom();
437 return false;
440 atomIndex = 0;
441 currentSection = sections.getUnchecked (sectionIndex);
443 else
445 const TextAtom* const lastAtom = currentSection->getAtom (atomIndex);
447 if (! lastAtom->isWhitespace())
449 // handle the case where the last atom in a section is actually part of the same
450 // word as the first atom of the next section...
451 float right = atomRight + lastAtom->width;
452 float lineHeight2 = lineHeight;
453 float maxDescent2 = maxDescent;
455 for (int section = sectionIndex + 1; section < sections.size(); ++section)
457 const UniformTextSection* const s = sections.getUnchecked (section);
459 if (s->getNumAtoms() == 0)
460 break;
462 const TextAtom* const nextAtom = s->getAtom (0);
464 if (nextAtom->isWhitespace())
465 break;
467 right += nextAtom->width;
469 lineHeight2 = jmax (lineHeight2, s->font.getHeight());
470 maxDescent2 = jmax (maxDescent2, s->font.getDescent());
472 if (shouldWrap (right))
474 lineHeight = lineHeight2;
475 maxDescent = maxDescent2;
477 forceNewLine = true;
478 break;
481 if (s->getNumAtoms() > 1)
482 break;
488 if (atom != nullptr)
490 atomX = atomRight;
491 indexInText += atom->numChars;
493 if (atom->isNewLine())
494 beginNewLine();
497 atom = currentSection->getAtom (atomIndex);
498 atomRight = atomX + atom->width;
499 ++atomIndex;
501 if (shouldWrap (atomRight) || forceNewLine)
503 if (atom->isWhitespace())
505 // leave whitespace at the end of a line, but truncate it to avoid scrolling
506 atomRight = jmin (atomRight, wordWrapWidth);
508 else
510 atomRight = atom->width;
512 if (shouldWrap (atomRight)) // atom too big to fit on a line, so break it up..
514 tempAtom = *atom;
515 tempAtom.width = 0;
516 tempAtom.numChars = 0;
517 atom = &tempAtom;
519 if (atomX > 0)
520 beginNewLine();
522 return next();
525 beginNewLine();
526 return true;
530 return true;
533 void beginNewLine()
535 atomX = 0;
536 lineY += lineHeight;
538 int tempSectionIndex = sectionIndex;
539 int tempAtomIndex = atomIndex;
540 const UniformTextSection* section = sections.getUnchecked (tempSectionIndex);
542 lineHeight = section->font.getHeight();
543 maxDescent = section->font.getDescent();
545 float x = (atom != nullptr) ? atom->width : 0;
547 while (! shouldWrap (x))
549 if (tempSectionIndex >= sections.size())
550 break;
552 bool checkSize = false;
554 if (tempAtomIndex >= section->getNumAtoms())
556 if (++tempSectionIndex >= sections.size())
557 break;
559 tempAtomIndex = 0;
560 section = sections.getUnchecked (tempSectionIndex);
561 checkSize = true;
564 const TextAtom* const nextAtom = section->getAtom (tempAtomIndex);
566 if (nextAtom == nullptr)
567 break;
569 x += nextAtom->width;
571 if (shouldWrap (x) || nextAtom->isNewLine())
572 break;
574 if (checkSize)
576 lineHeight = jmax (lineHeight, section->font.getHeight());
577 maxDescent = jmax (maxDescent, section->font.getDescent());
580 ++tempAtomIndex;
584 //==============================================================================
585 void draw (Graphics& g, const UniformTextSection*& lastSection) const
587 if (passwordCharacter != 0 || ! atom->isWhitespace())
589 if (lastSection != currentSection)
591 lastSection = currentSection;
592 g.setColour (currentSection->colour);
593 g.setFont (currentSection->font);
596 jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty());
598 GlyphArrangement ga;
599 ga.addLineOfText (currentSection->font,
600 atom->getTrimmedText (passwordCharacter),
601 atomX,
602 (float) roundToInt (lineY + lineHeight - maxDescent));
603 ga.draw (g);
607 void drawSelection (Graphics& g, const Range<int>& selection) const
609 const int startX = roundToInt (indexToX (selection.getStart()));
610 const int endX = roundToInt (indexToX (selection.getEnd()));
612 const int y = roundToInt (lineY);
613 const int nextY = roundToInt (lineY + lineHeight);
615 g.fillRect (startX, y, endX - startX, nextY - y);
618 void drawUnderline (Graphics& g, const Range<int>& underline, const Colour& colour) const
620 const int startX = roundToInt (indexToX (underline.getStart()));
621 const int endX = roundToInt (indexToX (underline.getEnd()));
622 const int baselineY = roundToInt (lineY + currentSection->font.getAscent() + 0.5f);
624 Graphics::ScopedSaveState state (g);
625 g.reduceClipRegion (Rectangle<int> (startX, baselineY, endX - startX, 1));
626 g.fillCheckerBoard (Rectangle<int> (0, 0, endX, baselineY + 1), 3, 1, colour, Colours::transparentBlack);
629 void drawSelectedText (Graphics& g,
630 const Range<int>& selection,
631 const Colour& selectedTextColour) const
633 if (passwordCharacter != 0 || ! atom->isWhitespace())
635 GlyphArrangement ga;
636 ga.addLineOfText (currentSection->font,
637 atom->getTrimmedText (passwordCharacter),
638 atomX,
639 (float) roundToInt (lineY + lineHeight - maxDescent));
641 if (selection.getEnd() < indexInText + atom->numChars)
643 GlyphArrangement ga2 (ga);
644 ga2.removeRangeOfGlyphs (0, selection.getEnd() - indexInText);
645 ga.removeRangeOfGlyphs (selection.getEnd() - indexInText, -1);
647 g.setColour (currentSection->colour);
648 ga2.draw (g);
651 if (selection.getStart() > indexInText)
653 GlyphArrangement ga2 (ga);
654 ga2.removeRangeOfGlyphs (selection.getStart() - indexInText, -1);
655 ga.removeRangeOfGlyphs (0, selection.getStart() - indexInText);
657 g.setColour (currentSection->colour);
658 ga2.draw (g);
661 g.setColour (selectedTextColour);
662 ga.draw (g);
666 //==============================================================================
667 float indexToX (const int indexToFind) const
669 if (indexToFind <= indexInText)
670 return atomX;
672 if (indexToFind >= indexInText + atom->numChars)
673 return atomRight;
675 GlyphArrangement g;
676 g.addLineOfText (currentSection->font,
677 atom->getText (passwordCharacter),
678 atomX, 0.0f);
680 if (indexToFind - indexInText >= g.getNumGlyphs())
681 return atomRight;
683 return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft());
686 int xToIndex (const float xToFind) const
688 if (xToFind <= atomX || atom->isNewLine())
689 return indexInText;
691 if (xToFind >= atomRight)
692 return indexInText + atom->numChars;
694 GlyphArrangement g;
695 g.addLineOfText (currentSection->font,
696 atom->getText (passwordCharacter),
697 atomX, 0.0f);
699 int j;
700 for (j = 0; j < g.getNumGlyphs(); ++j)
701 if ((g.getGlyph(j).getLeft() + g.getGlyph(j).getRight()) / 2 > xToFind)
702 break;
704 return indexInText + j;
707 //==============================================================================
708 bool getCharPosition (const int index, float& cx, float& cy, float& lineHeight_)
710 while (next())
712 if (indexInText + atom->numChars > index)
714 cx = indexToX (index);
715 cy = lineY;
716 lineHeight_ = lineHeight;
717 return true;
721 cx = atomX;
722 cy = lineY;
723 lineHeight_ = lineHeight;
724 return false;
727 //==============================================================================
728 int indexInText;
729 float lineY, lineHeight, maxDescent;
730 float atomX, atomRight;
731 const TextAtom* atom;
732 const UniformTextSection* currentSection;
734 private:
735 const Array <UniformTextSection*>& sections;
736 int sectionIndex, atomIndex;
737 const float wordWrapWidth;
738 const juce_wchar passwordCharacter;
739 TextAtom tempAtom;
741 Iterator& operator= (const Iterator&);
743 void moveToEndOfLastAtom()
745 if (atom != nullptr)
747 atomX = atomRight;
749 if (atom->isNewLine())
751 atomX = 0.0f;
752 lineY += lineHeight;
757 bool shouldWrap (const float x) const
759 return (x - 0.0001f) >= wordWrapWidth;
762 JUCE_LEAK_DETECTOR (Iterator);
766 //==============================================================================
767 class TextEditor::InsertAction : public UndoableAction
769 public:
770 InsertAction (TextEditor& owner_,
771 const String& text_,
772 const int insertIndex_,
773 const Font& font_,
774 const Colour& colour_,
775 const int oldCaretPos_,
776 const int newCaretPos_)
777 : owner (owner_),
778 text (text_),
779 insertIndex (insertIndex_),
780 oldCaretPos (oldCaretPos_),
781 newCaretPos (newCaretPos_),
782 font (font_),
783 colour (colour_)
787 bool perform()
789 owner.insert (text, insertIndex, font, colour, 0, newCaretPos);
790 return true;
793 bool undo()
795 owner.remove (Range<int> (insertIndex, insertIndex + text.length()), 0, oldCaretPos);
796 return true;
799 int getSizeInUnits()
801 return text.length() + 16;
804 private:
805 TextEditor& owner;
806 const String text;
807 const int insertIndex, oldCaretPos, newCaretPos;
808 const Font font;
809 const Colour colour;
811 JUCE_DECLARE_NON_COPYABLE (InsertAction);
814 //==============================================================================
815 class TextEditor::RemoveAction : public UndoableAction
817 public:
818 RemoveAction (TextEditor& owner_,
819 const Range<int> range_,
820 const int oldCaretPos_,
821 const int newCaretPos_,
822 const Array <UniformTextSection*>& removedSections_)
823 : owner (owner_),
824 range (range_),
825 oldCaretPos (oldCaretPos_),
826 newCaretPos (newCaretPos_),
827 removedSections (removedSections_)
831 ~RemoveAction()
833 for (int i = removedSections.size(); --i >= 0;)
835 UniformTextSection* const section = removedSections.getUnchecked (i);
836 section->clear();
837 delete section;
841 bool perform()
843 owner.remove (range, 0, newCaretPos);
844 return true;
847 bool undo()
849 owner.reinsert (range.getStart(), removedSections);
850 owner.moveCaretTo (oldCaretPos, false);
851 return true;
854 int getSizeInUnits()
856 int n = 0;
858 for (int i = removedSections.size(); --i >= 0;)
859 n += removedSections.getUnchecked (i)->getTotalLength();
861 return n + 16;
864 private:
865 TextEditor& owner;
866 const Range<int> range;
867 const int oldCaretPos, newCaretPos;
868 Array <UniformTextSection*> removedSections;
870 JUCE_DECLARE_NON_COPYABLE (RemoveAction);
873 //==============================================================================
874 class TextEditor::TextHolderComponent : public Component,
875 public Timer,
876 public ValueListener
878 public:
879 TextHolderComponent (TextEditor& owner_)
880 : owner (owner_)
882 setWantsKeyboardFocus (false);
883 setInterceptsMouseClicks (false, true);
885 owner.getTextValue().addListener (this);
888 ~TextHolderComponent()
890 owner.getTextValue().removeListener (this);
893 void paint (Graphics& g)
895 owner.drawContent (g);
898 void restartTimer()
900 startTimer (350);
903 void timerCallback()
905 owner.timerCallbackInt();
908 const MouseCursor getMouseCursor()
910 return owner.getMouseCursor();
913 void valueChanged (Value&)
915 owner.textWasChangedByValue();
918 private:
919 TextEditor& owner;
921 JUCE_DECLARE_NON_COPYABLE (TextHolderComponent);
924 //==============================================================================
925 class TextEditorViewport : public Viewport
927 public:
928 TextEditorViewport (TextEditor& owner_)
929 : owner (owner_), lastWordWrapWidth (0), rentrant (false)
933 void visibleAreaChanged (const Rectangle<int>&)
935 if (! rentrant) // it's rare, but possible to get into a feedback loop as the viewport's scrollbars
936 // appear and disappear, causing the wrap width to change.
938 const float wordWrapWidth = owner.getWordWrapWidth();
940 if (wordWrapWidth != lastWordWrapWidth)
942 lastWordWrapWidth = wordWrapWidth;
944 rentrant = true;
945 owner.updateTextHolderSize();
946 rentrant = false;
951 private:
952 TextEditor& owner;
953 float lastWordWrapWidth;
954 bool rentrant;
956 JUCE_DECLARE_NON_COPYABLE (TextEditorViewport);
959 //==============================================================================
960 namespace TextEditorDefs
962 const int textChangeMessageId = 0x10003001;
963 const int returnKeyMessageId = 0x10003002;
964 const int escapeKeyMessageId = 0x10003003;
965 const int focusLossMessageId = 0x10003004;
967 const int maxActionsPerTransaction = 100;
969 int getCharacterCategory (const juce_wchar character)
971 return CharacterFunctions::isLetterOrDigit (character)
972 ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1);
976 //==============================================================================
977 TextEditor::TextEditor (const String& name,
978 const juce_wchar passwordCharacter_)
979 : Component (name),
980 borderSize (1, 1, 1, 3),
981 readOnly (false),
982 multiline (false),
983 wordWrap (false),
984 returnKeyStartsNewLine (false),
985 popupMenuEnabled (true),
986 selectAllTextWhenFocused (false),
987 scrollbarVisible (true),
988 wasFocused (false),
989 keepCaretOnScreen (true),
990 tabKeyUsed (false),
991 menuActive (false),
992 valueTextNeedsUpdating (false),
993 maxTextLength (0),
994 leftIndent (4),
995 topIndent (4),
996 lastTransactionTime (0),
997 currentFont (14.0f),
998 totalNumChars (0),
999 caretPosition (0),
1000 passwordCharacter (passwordCharacter_),
1001 dragType (notDragging)
1003 setOpaque (true);
1005 addAndMakeVisible (viewport = new TextEditorViewport (*this));
1006 viewport->setViewedComponent (textHolder = new TextHolderComponent (*this));
1007 viewport->setWantsKeyboardFocus (false);
1008 viewport->setScrollBarsShown (false, false);
1010 setWantsKeyboardFocus (true);
1011 setCaretVisible (true);
1014 TextEditor::~TextEditor()
1016 if (wasFocused)
1018 ComponentPeer* const peer = getPeer();
1019 if (peer != nullptr)
1020 peer->dismissPendingTextInput();
1023 textValue.referTo (Value());
1024 clearInternal (0);
1025 viewport = nullptr;
1026 textHolder = nullptr;
1029 //==============================================================================
1030 void TextEditor::newTransaction()
1032 lastTransactionTime = Time::getApproximateMillisecondCounter();
1033 undoManager.beginNewTransaction();
1036 bool TextEditor::undoOrRedo (const bool shouldUndo)
1038 if (! isReadOnly())
1040 newTransaction();
1042 if (shouldUndo ? undoManager.undo()
1043 : undoManager.redo())
1045 scrollToMakeSureCursorIsVisible();
1046 repaint();
1047 textChanged();
1048 return true;
1052 return false;
1055 bool TextEditor::undo() { return undoOrRedo (true); }
1056 bool TextEditor::redo() { return undoOrRedo (false); }
1058 //==============================================================================
1059 void TextEditor::setMultiLine (const bool shouldBeMultiLine,
1060 const bool shouldWordWrap)
1062 if (multiline != shouldBeMultiLine
1063 || wordWrap != (shouldWordWrap && shouldBeMultiLine))
1065 multiline = shouldBeMultiLine;
1066 wordWrap = shouldWordWrap && shouldBeMultiLine;
1068 viewport->setScrollBarsShown (scrollbarVisible && multiline,
1069 scrollbarVisible && multiline);
1070 viewport->setViewPosition (0, 0);
1071 resized();
1072 scrollToMakeSureCursorIsVisible();
1076 bool TextEditor::isMultiLine() const
1078 return multiline;
1081 void TextEditor::setScrollbarsShown (bool shown)
1083 if (scrollbarVisible != shown)
1085 scrollbarVisible = shown;
1086 shown = shown && isMultiLine();
1087 viewport->setScrollBarsShown (shown, shown);
1091 void TextEditor::setReadOnly (const bool shouldBeReadOnly)
1093 if (readOnly != shouldBeReadOnly)
1095 readOnly = shouldBeReadOnly;
1096 enablementChanged();
1100 bool TextEditor::isReadOnly() const
1102 return readOnly || ! isEnabled();
1105 bool TextEditor::isTextInputActive() const
1107 return ! isReadOnly();
1110 void TextEditor::setReturnKeyStartsNewLine (const bool shouldStartNewLine)
1112 returnKeyStartsNewLine = shouldStartNewLine;
1115 void TextEditor::setTabKeyUsedAsCharacter (const bool shouldTabKeyBeUsed)
1117 tabKeyUsed = shouldTabKeyBeUsed;
1120 void TextEditor::setPopupMenuEnabled (const bool b)
1122 popupMenuEnabled = b;
1125 void TextEditor::setSelectAllWhenFocused (const bool b)
1127 selectAllTextWhenFocused = b;
1130 //==============================================================================
1131 const Font& TextEditor::getFont() const
1133 return currentFont;
1136 void TextEditor::setFont (const Font& newFont)
1138 currentFont = newFont;
1139 scrollToMakeSureCursorIsVisible();
1142 void TextEditor::applyFontToAllText (const Font& newFont)
1144 currentFont = newFont;
1146 const Colour overallColour (findColour (textColourId));
1148 for (int i = sections.size(); --i >= 0;)
1150 UniformTextSection* const uts = sections.getUnchecked (i);
1151 uts->setFont (newFont, passwordCharacter);
1152 uts->colour = overallColour;
1155 coalesceSimilarSections();
1156 updateTextHolderSize();
1157 scrollToMakeSureCursorIsVisible();
1158 repaint();
1161 void TextEditor::colourChanged()
1163 setOpaque (findColour (backgroundColourId).isOpaque());
1164 repaint();
1167 void TextEditor::lookAndFeelChanged()
1169 if (isCaretVisible())
1171 setCaretVisible (false);
1172 setCaretVisible (true);
1176 void TextEditor::setCaretVisible (const bool shouldCaretBeVisible)
1178 if (shouldCaretBeVisible && ! isReadOnly())
1180 if (caret == nullptr)
1181 textHolder->addChildComponent (caret = getLookAndFeel().createCaretComponent (this));
1183 else
1185 caret = nullptr;
1188 setMouseCursor (shouldCaretBeVisible ? MouseCursor::IBeamCursor
1189 : MouseCursor::NormalCursor);
1192 void TextEditor::updateCaretPosition()
1194 if (caret != nullptr)
1195 caret->setCaretPosition (getCaretRectangle().translated (leftIndent, topIndent));
1198 void TextEditor::setInputRestrictions (const int maxLen,
1199 const String& chars)
1201 maxTextLength = jmax (0, maxLen);
1202 allowedCharacters = chars;
1205 void TextEditor::setTextToShowWhenEmpty (const String& text, const Colour& colourToUse)
1207 textToShowWhenEmpty = text;
1208 colourForTextWhenEmpty = colourToUse;
1211 void TextEditor::setPasswordCharacter (const juce_wchar newPasswordCharacter)
1213 if (passwordCharacter != newPasswordCharacter)
1215 passwordCharacter = newPasswordCharacter;
1216 resized();
1217 repaint();
1221 void TextEditor::setScrollBarThickness (const int newThicknessPixels)
1223 viewport->setScrollBarThickness (newThicknessPixels);
1226 void TextEditor::setScrollBarButtonVisibility (const bool buttonsVisible)
1228 viewport->setScrollBarButtonVisibility (buttonsVisible);
1231 //==============================================================================
1232 void TextEditor::clear()
1234 clearInternal (0);
1235 updateTextHolderSize();
1236 undoManager.clearUndoHistory();
1239 void TextEditor::setText (const String& newText,
1240 const bool sendTextChangeMessage)
1242 const int newLength = newText.length();
1244 if (newLength != getTotalNumChars() || getText() != newText)
1246 const int oldCursorPos = caretPosition;
1247 const bool cursorWasAtEnd = oldCursorPos >= getTotalNumChars();
1249 clearInternal (0);
1250 insert (newText, 0, currentFont, findColour (textColourId), 0, caretPosition);
1252 // if you're adding text with line-feeds to a single-line text editor, it
1253 // ain't gonna look right!
1254 jassert (multiline || ! newText.containsAnyOf ("\r\n"));
1256 if (cursorWasAtEnd && ! isMultiLine())
1257 moveCaretTo (getTotalNumChars(), false);
1258 else
1259 moveCaretTo (oldCursorPos, false);
1261 if (sendTextChangeMessage)
1262 textChanged();
1264 updateTextHolderSize();
1265 scrollToMakeSureCursorIsVisible();
1266 undoManager.clearUndoHistory();
1268 repaint();
1272 //==============================================================================
1273 void TextEditor::updateValueFromText()
1275 if (valueTextNeedsUpdating)
1277 valueTextNeedsUpdating = false;
1278 textValue = getText();
1282 Value& TextEditor::getTextValue()
1284 updateValueFromText();
1285 return textValue;
1288 void TextEditor::textWasChangedByValue()
1290 if (textValue.getValueSource().getReferenceCount() > 1)
1291 setText (textValue.getValue());
1294 //==============================================================================
1295 void TextEditor::textChanged()
1297 updateTextHolderSize();
1298 postCommandMessage (TextEditorDefs::textChangeMessageId);
1300 if (textValue.getValueSource().getReferenceCount() > 1)
1302 valueTextNeedsUpdating = false;
1303 textValue = getText();
1307 void TextEditor::returnPressed()
1309 postCommandMessage (TextEditorDefs::returnKeyMessageId);
1312 void TextEditor::escapePressed()
1314 postCommandMessage (TextEditorDefs::escapeKeyMessageId);
1317 void TextEditor::addListener (TextEditorListener* const newListener)
1319 listeners.add (newListener);
1322 void TextEditor::removeListener (TextEditorListener* const listenerToRemove)
1324 listeners.remove (listenerToRemove);
1327 //==============================================================================
1328 void TextEditor::timerCallbackInt()
1330 if (hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent())
1331 wasFocused = true;
1333 const unsigned int now = Time::getApproximateMillisecondCounter();
1335 if (now > lastTransactionTime + 200)
1336 newTransaction();
1339 void TextEditor::repaintText (const Range<int>& range)
1341 if (! range.isEmpty())
1343 float x = 0, y = 0, lh = currentFont.getHeight();
1345 const float wordWrapWidth = getWordWrapWidth();
1347 if (wordWrapWidth > 0)
1349 Iterator i (sections, wordWrapWidth, passwordCharacter);
1351 i.getCharPosition (range.getStart(), x, y, lh);
1353 const int y1 = (int) y;
1354 int y2;
1356 if (range.getEnd() >= getTotalNumChars())
1358 y2 = textHolder->getHeight();
1360 else
1362 i.getCharPosition (range.getEnd(), x, y, lh);
1363 y2 = (int) (y + lh * 2.0f);
1366 textHolder->repaint (0, y1, textHolder->getWidth(), y2 - y1);
1371 //==============================================================================
1372 void TextEditor::moveCaret (int newCaretPos)
1374 if (newCaretPos < 0)
1375 newCaretPos = 0;
1376 else if (newCaretPos > getTotalNumChars())
1377 newCaretPos = getTotalNumChars();
1379 if (newCaretPos != getCaretPosition())
1381 caretPosition = newCaretPos;
1382 textHolder->restartTimer();
1383 scrollToMakeSureCursorIsVisible();
1384 updateCaretPosition();
1388 void TextEditor::setCaretPosition (const int newIndex)
1390 moveCaretTo (newIndex, false);
1393 int TextEditor::getCaretPosition() const
1395 return caretPosition;
1398 void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX,
1399 const int desiredCaretY)
1402 updateCaretPosition();
1404 const Rectangle<int> caretPos (getCaretRectangle());
1406 int vx = caretPos.getX() - desiredCaretX;
1407 int vy = caretPos.getY() - desiredCaretY;
1409 if (desiredCaretX < jmax (1, proportionOfWidth (0.05f)))
1410 vx += desiredCaretX - proportionOfWidth (0.2f);
1411 else if (desiredCaretX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
1412 vx += desiredCaretX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
1414 vx = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vx);
1416 if (! isMultiLine())
1418 vy = viewport->getViewPositionY();
1420 else
1422 vy = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vy);
1424 if (desiredCaretY < 0)
1425 vy = jmax (0, desiredCaretY + vy);
1426 else if (desiredCaretY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretPos.getHeight()))
1427 vy += desiredCaretY + 2 + caretPos.getHeight() + topIndent - viewport->getMaximumVisibleHeight();
1430 viewport->setViewPosition (vx, vy);
1433 const Rectangle<int> TextEditor::getCaretRectangle()
1435 float cursorX, cursorY;
1436 float cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value)
1437 getCharPosition (caretPosition, cursorX, cursorY, cursorHeight);
1439 return Rectangle<int> (roundToInt (cursorX), roundToInt (cursorY), 2, roundToInt (cursorHeight));
1442 //==============================================================================
1443 float TextEditor::getWordWrapWidth() const
1445 return wordWrap ? (float) (viewport->getMaximumVisibleWidth() - leftIndent - leftIndent / 2)
1446 : std::numeric_limits<float>::max();
1449 void TextEditor::updateTextHolderSize()
1451 const float wordWrapWidth = getWordWrapWidth();
1453 if (wordWrapWidth > 0)
1455 float maxWidth = 0.0f;
1457 Iterator i (sections, wordWrapWidth, passwordCharacter);
1459 while (i.next())
1460 maxWidth = jmax (maxWidth, i.atomRight);
1462 const int w = leftIndent + roundToInt (maxWidth);
1463 const int h = topIndent + roundToInt (jmax (i.lineY + i.lineHeight,
1464 currentFont.getHeight()));
1466 textHolder->setSize (w + 2, h + 1); // (the +2 allows a bit of space for the cursor to be at the right-hand-edge)
1470 int TextEditor::getTextWidth() const
1472 return textHolder->getWidth();
1475 int TextEditor::getTextHeight() const
1477 return textHolder->getHeight();
1480 void TextEditor::setIndents (const int newLeftIndent,
1481 const int newTopIndent)
1483 leftIndent = newLeftIndent;
1484 topIndent = newTopIndent;
1487 void TextEditor::setBorder (const BorderSize<int>& border)
1489 borderSize = border;
1490 resized();
1493 const BorderSize<int> TextEditor::getBorder() const
1495 return borderSize;
1498 void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor)
1500 keepCaretOnScreen = shouldScrollToShowCursor;
1503 void TextEditor::scrollToMakeSureCursorIsVisible()
1505 updateCaretPosition();
1507 if (keepCaretOnScreen)
1509 int x = viewport->getViewPositionX();
1510 int y = viewport->getViewPositionY();
1512 const Rectangle<int> caretPos (getCaretRectangle());
1514 const int relativeCursorX = caretPos.getX() - x;
1515 const int relativeCursorY = caretPos.getY() - y;
1517 if (relativeCursorX < jmax (1, proportionOfWidth (0.05f)))
1519 x += relativeCursorX - proportionOfWidth (0.2f);
1521 else if (relativeCursorX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
1523 x += relativeCursorX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
1526 x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), x);
1528 if (! isMultiLine())
1530 y = (getHeight() - textHolder->getHeight() - topIndent) / -2;
1532 else
1534 if (relativeCursorY < 0)
1536 y = jmax (0, relativeCursorY + y);
1538 else if (relativeCursorY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretPos.getHeight()))
1540 y += relativeCursorY + 2 + caretPos.getHeight() + topIndent - viewport->getMaximumVisibleHeight();
1544 viewport->setViewPosition (x, y);
1548 void TextEditor::moveCaretTo (const int newPosition,
1549 const bool isSelecting)
1551 if (isSelecting)
1553 moveCaret (newPosition);
1555 const Range<int> oldSelection (selection);
1557 if (dragType == notDragging)
1559 if (abs (getCaretPosition() - selection.getStart()) < abs (getCaretPosition() - selection.getEnd()))
1560 dragType = draggingSelectionStart;
1561 else
1562 dragType = draggingSelectionEnd;
1565 if (dragType == draggingSelectionStart)
1567 if (getCaretPosition() >= selection.getEnd())
1568 dragType = draggingSelectionEnd;
1570 selection = Range<int>::between (getCaretPosition(), selection.getEnd());
1572 else
1574 if (getCaretPosition() < selection.getStart())
1575 dragType = draggingSelectionStart;
1577 selection = Range<int>::between (getCaretPosition(), selection.getStart());
1580 repaintText (selection.getUnionWith (oldSelection));
1582 else
1584 dragType = notDragging;
1586 repaintText (selection);
1588 moveCaret (newPosition);
1589 selection = Range<int>::emptyRange (getCaretPosition());
1593 int TextEditor::getTextIndexAt (const int x,
1594 const int y)
1596 return indexAtPosition ((float) (x + viewport->getViewPositionX() - leftIndent),
1597 (float) (y + viewport->getViewPositionY() - topIndent));
1600 void TextEditor::insertTextAtCaret (const String& newText_)
1602 String newText (newText_);
1604 if (allowedCharacters.isNotEmpty())
1605 newText = newText.retainCharacters (allowedCharacters);
1607 if (! isMultiLine())
1608 newText = newText.replaceCharacters ("\r\n", " ");
1609 else
1610 newText = newText.replace ("\r\n", "\n");
1612 const int newCaretPos = selection.getStart() + newText.length();
1613 const int insertIndex = selection.getStart();
1615 remove (selection, getUndoManager(),
1616 newText.isNotEmpty() ? newCaretPos - 1 : newCaretPos);
1618 if (maxTextLength > 0)
1619 newText = newText.substring (0, maxTextLength - getTotalNumChars());
1621 if (newText.isNotEmpty())
1622 insert (newText,
1623 insertIndex,
1624 currentFont,
1625 findColour (textColourId),
1626 getUndoManager(),
1627 newCaretPos);
1629 textChanged();
1632 void TextEditor::setHighlightedRegion (const Range<int>& newSelection)
1634 moveCaretTo (newSelection.getStart(), false);
1635 moveCaretTo (newSelection.getEnd(), true);
1638 //==============================================================================
1639 void TextEditor::copy()
1641 if (passwordCharacter == 0)
1643 const String selectedText (getHighlightedText());
1645 if (selectedText.isNotEmpty())
1646 SystemClipboard::copyTextToClipboard (selectedText);
1650 void TextEditor::paste()
1652 if (! isReadOnly())
1654 const String clip (SystemClipboard::getTextFromClipboard());
1656 if (clip.isNotEmpty())
1657 insertTextAtCaret (clip);
1661 void TextEditor::cut()
1663 if (! isReadOnly())
1665 moveCaret (selection.getEnd());
1666 insertTextAtCaret (String::empty);
1670 //==============================================================================
1671 void TextEditor::drawContent (Graphics& g)
1673 const float wordWrapWidth = getWordWrapWidth();
1675 if (wordWrapWidth > 0)
1677 g.setOrigin (leftIndent, topIndent);
1678 const Rectangle<int> clip (g.getClipBounds());
1679 Colour selectedTextColour;
1681 Iterator i (sections, wordWrapWidth, passwordCharacter);
1683 while (i.lineY + 200.0 < clip.getY() && i.next())
1686 if (! selection.isEmpty())
1688 g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f));
1690 selectedTextColour = findColour (highlightedTextColourId);
1692 Iterator i2 (i);
1694 while (i2.next() && i2.lineY < clip.getBottom())
1696 if (i2.lineY + i2.lineHeight >= clip.getY()
1697 && selection.intersects (Range<int> (i2.indexInText, i2.indexInText + i2.atom->numChars)))
1699 i2.drawSelection (g, selection);
1704 const UniformTextSection* lastSection = nullptr;
1706 while (i.next() && i.lineY < clip.getBottom())
1708 if (i.lineY + i.lineHeight >= clip.getY())
1710 if (selection.intersects (Range<int> (i.indexInText, i.indexInText + i.atom->numChars)))
1712 i.drawSelectedText (g, selection, selectedTextColour);
1713 lastSection = nullptr;
1715 else
1717 i.draw (g, lastSection);
1722 for (int j = underlinedSections.size(); --j >= 0;)
1724 const Range<int>& underlinedSection = underlinedSections.getReference (j);
1726 Iterator i2 (sections, wordWrapWidth, passwordCharacter);
1728 while (i2.next() && i2.lineY < clip.getBottom())
1730 if (i2.lineY + i2.lineHeight >= clip.getY()
1731 && underlinedSection.intersects (Range<int> (i2.indexInText, i2.indexInText + i2.atom->numChars)))
1733 i2.drawUnderline (g, underlinedSection, findColour (textColourId));
1740 void TextEditor::paint (Graphics& g)
1742 getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this);
1745 void TextEditor::paintOverChildren (Graphics& g)
1747 if (textToShowWhenEmpty.isNotEmpty()
1748 && (! hasKeyboardFocus (false))
1749 && getTotalNumChars() == 0)
1751 g.setColour (colourForTextWhenEmpty);
1752 g.setFont (getFont());
1754 if (isMultiLine())
1756 g.drawText (textToShowWhenEmpty,
1757 0, 0, getWidth(), getHeight(),
1758 Justification::centred, true);
1760 else
1762 g.drawText (textToShowWhenEmpty,
1763 leftIndent, topIndent,
1764 viewport->getWidth() - leftIndent,
1765 viewport->getHeight() - topIndent,
1766 Justification::centredLeft, true);
1770 getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this);
1773 //==============================================================================
1774 static void textEditorMenuCallback (int menuResult, TextEditor* editor)
1776 if (editor != nullptr && menuResult != 0)
1777 editor->performPopupMenuAction (menuResult);
1780 void TextEditor::mouseDown (const MouseEvent& e)
1782 beginDragAutoRepeat (100);
1783 newTransaction();
1785 if (wasFocused || ! selectAllTextWhenFocused)
1787 if (! (popupMenuEnabled && e.mods.isPopupMenu()))
1789 moveCaretTo (getTextIndexAt (e.x, e.y),
1790 e.mods.isShiftDown());
1792 else
1794 PopupMenu m;
1795 m.setLookAndFeel (&getLookAndFeel());
1796 addPopupMenuItems (m, &e);
1798 m.showMenuAsync (PopupMenu::Options(),
1799 ModalCallbackFunction::forComponent (textEditorMenuCallback, this));
1804 void TextEditor::mouseDrag (const MouseEvent& e)
1806 if (wasFocused || ! selectAllTextWhenFocused)
1808 if (! (popupMenuEnabled && e.mods.isPopupMenu()))
1810 moveCaretTo (getTextIndexAt (e.x, e.y), true);
1815 void TextEditor::mouseUp (const MouseEvent& e)
1817 newTransaction();
1818 textHolder->restartTimer();
1820 if (wasFocused || ! selectAllTextWhenFocused)
1822 if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu()))
1824 moveCaret (getTextIndexAt (e.x, e.y));
1828 wasFocused = true;
1831 void TextEditor::mouseDoubleClick (const MouseEvent& e)
1833 int tokenEnd = getTextIndexAt (e.x, e.y);
1834 int tokenStart = tokenEnd;
1836 if (e.getNumberOfClicks() > 3)
1838 tokenStart = 0;
1839 tokenEnd = getTotalNumChars();
1841 else
1843 const String t (getText());
1844 const int totalLength = getTotalNumChars();
1846 while (tokenEnd < totalLength)
1848 // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
1849 const juce_wchar c = t [tokenEnd];
1850 if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
1851 ++tokenEnd;
1852 else
1853 break;
1856 tokenStart = tokenEnd;
1858 while (tokenStart > 0)
1860 // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
1861 const juce_wchar c = t [tokenStart - 1];
1862 if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
1863 --tokenStart;
1864 else
1865 break;
1868 if (e.getNumberOfClicks() > 2)
1870 while (tokenEnd < totalLength)
1872 const juce_wchar c = t [tokenEnd];
1873 if (c != '\r' && c != '\n')
1874 ++tokenEnd;
1875 else
1876 break;
1879 while (tokenStart > 0)
1881 const juce_wchar c = t [tokenStart - 1];
1882 if (c != '\r' && c != '\n')
1883 --tokenStart;
1884 else
1885 break;
1890 moveCaretTo (tokenEnd, false);
1891 moveCaretTo (tokenStart, true);
1894 void TextEditor::mouseWheelMove (const MouseEvent& e, float wheelIncrementX, float wheelIncrementY)
1896 if (! viewport->useMouseWheelMoveIfNeeded (e, wheelIncrementX, wheelIncrementY))
1897 Component::mouseWheelMove (e, wheelIncrementX, wheelIncrementY);
1900 //==============================================================================
1901 bool TextEditor::moveCaretWithTransation (const int newPos, const bool selecting)
1903 newTransaction();
1904 moveCaretTo (newPos, selecting);
1905 return true;
1908 bool TextEditor::moveCaretLeft (bool moveInWholeWordSteps, bool selecting)
1910 int pos = getCaretPosition();
1912 if (moveInWholeWordSteps)
1913 pos = findWordBreakBefore (pos);
1914 else
1915 --pos;
1917 return moveCaretWithTransation (pos, selecting);
1920 bool TextEditor::moveCaretRight (bool moveInWholeWordSteps, bool selecting)
1922 int pos = getCaretPosition();
1924 if (moveInWholeWordSteps)
1925 pos = findWordBreakAfter (pos);
1926 else
1927 ++pos;
1929 return moveCaretWithTransation (pos, selecting);
1932 bool TextEditor::moveCaretUp (bool selecting)
1934 if (! isMultiLine())
1935 return moveCaretToStartOfLine (selecting);
1937 const Rectangle<float> caretPos (getCaretRectangle().toFloat());
1938 return moveCaretWithTransation (indexAtPosition (caretPos.getX(), caretPos.getY() - 1.0f), selecting);
1941 bool TextEditor::moveCaretDown (bool selecting)
1943 if (! isMultiLine())
1944 return moveCaretToEndOfLine (selecting);
1946 const Rectangle<float> caretPos (getCaretRectangle().toFloat());
1947 return moveCaretWithTransation (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting);
1950 bool TextEditor::pageUp (bool selecting)
1952 if (! isMultiLine())
1953 return moveCaretToStartOfLine (selecting);
1955 const Rectangle<float> caretPos (getCaretRectangle().toFloat());
1956 return moveCaretWithTransation (indexAtPosition (caretPos.getX(), caretPos.getY() - viewport->getViewHeight()), selecting);
1959 bool TextEditor::pageDown (bool selecting)
1961 if (! isMultiLine())
1962 return moveCaretToEndOfLine (selecting);
1964 const Rectangle<float> caretPos (getCaretRectangle().toFloat());
1965 return moveCaretWithTransation (indexAtPosition (caretPos.getX(), caretPos.getBottom() + viewport->getViewHeight()), selecting);
1968 void TextEditor::scrollByLines (int deltaLines)
1970 ScrollBar* scrollbar = viewport->getVerticalScrollBar();
1972 if (scrollbar != nullptr)
1973 scrollbar->moveScrollbarInSteps (deltaLines);
1976 bool TextEditor::scrollDown()
1978 scrollByLines (-1);
1979 return true;
1982 bool TextEditor::scrollUp()
1984 scrollByLines (1);
1985 return true;
1988 bool TextEditor::moveCaretToTop (bool selecting)
1990 return moveCaretWithTransation (0, selecting);
1993 bool TextEditor::moveCaretToStartOfLine (bool selecting)
1995 const Rectangle<float> caretPos (getCaretRectangle().toFloat());
1996 return moveCaretWithTransation (indexAtPosition (0.0f, caretPos.getY()), selecting);
1999 bool TextEditor::moveCaretToEnd (bool selecting)
2001 return moveCaretWithTransation (getTotalNumChars(), selecting);
2004 bool TextEditor::moveCaretToEndOfLine (bool selecting)
2006 const Rectangle<float> caretPos (getCaretRectangle().toFloat());
2007 return moveCaretWithTransation (indexAtPosition ((float) textHolder->getWidth(), caretPos.getY()), selecting);
2010 bool TextEditor::deleteBackwards (bool moveInWholeWordSteps)
2012 if (moveInWholeWordSteps)
2013 moveCaretTo (findWordBreakBefore (getCaretPosition()), true);
2014 else if (selection.isEmpty() && selection.getStart() > 0)
2015 selection.setStart (selection.getEnd() - 1);
2017 cut();
2018 return true;
2021 bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/)
2023 if (selection.isEmpty() && selection.getStart() < getTotalNumChars())
2024 selection.setEnd (selection.getStart() + 1);
2026 cut();
2027 return true;
2030 bool TextEditor::copyToClipboard()
2032 newTransaction();
2033 copy();
2034 return true;
2037 bool TextEditor::cutToClipboard()
2039 newTransaction();
2040 copy();
2041 cut();
2042 return true;
2045 bool TextEditor::pasteFromClipboard()
2047 newTransaction();
2048 paste();
2049 return true;
2052 bool TextEditor::selectAll()
2054 newTransaction();
2055 moveCaretTo (getTotalNumChars(), false);
2056 moveCaretTo (0, true);
2057 return true;
2060 //==============================================================================
2061 bool TextEditor::keyPressed (const KeyPress& key)
2063 if (isReadOnly() && key != KeyPress ('c', ModifierKeys::commandModifier, 0))
2064 return false;
2066 if (! TextEditorKeyMapper<TextEditor>::invokeKeyFunction (*this, key))
2068 if (key == KeyPress::returnKey)
2070 newTransaction();
2072 if (returnKeyStartsNewLine)
2073 insertTextAtCaret ("\n");
2074 else
2075 returnPressed();
2077 else if (key.isKeyCode (KeyPress::escapeKey))
2079 newTransaction();
2080 moveCaretTo (getCaretPosition(), false);
2081 escapePressed();
2083 else if (key.getTextCharacter() >= ' '
2084 || (tabKeyUsed && (key.getTextCharacter() == '\t')))
2086 insertTextAtCaret (String::charToString (key.getTextCharacter()));
2088 lastTransactionTime = Time::getApproximateMillisecondCounter();
2090 else
2092 return false;
2096 return true;
2099 bool TextEditor::keyStateChanged (const bool isKeyDown)
2101 if (! isKeyDown)
2102 return false;
2104 #if JUCE_WINDOWS
2105 if (KeyPress (KeyPress::F4Key, ModifierKeys::altModifier, 0).isCurrentlyDown())
2106 return false; // We need to explicitly allow alt-F4 to pass through on Windows
2107 #endif
2109 // (overridden to avoid forwarding key events to the parent)
2110 return ! ModifierKeys::getCurrentModifiers().isCommandDown();
2113 //==============================================================================
2114 const int baseMenuItemID = 0x7fff0000;
2116 void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*)
2118 const bool writable = ! isReadOnly();
2120 if (passwordCharacter == 0)
2122 m.addItem (baseMenuItemID + 1, TRANS("cut"), writable);
2123 m.addItem (baseMenuItemID + 2, TRANS("copy"), ! selection.isEmpty());
2124 m.addItem (baseMenuItemID + 3, TRANS("paste"), writable);
2127 m.addItem (baseMenuItemID + 4, TRANS("delete"), writable);
2128 m.addSeparator();
2129 m.addItem (baseMenuItemID + 5, TRANS("select all"));
2130 m.addSeparator();
2132 if (getUndoManager() != nullptr)
2134 m.addItem (baseMenuItemID + 6, TRANS("undo"), undoManager.canUndo());
2135 m.addItem (baseMenuItemID + 7, TRANS("redo"), undoManager.canRedo());
2139 void TextEditor::performPopupMenuAction (const int menuItemID)
2141 switch (menuItemID)
2143 case baseMenuItemID + 1: cutToClipboard(); break;
2144 case baseMenuItemID + 2: copyToClipboard(); break;
2145 case baseMenuItemID + 3: pasteFromClipboard(); break;
2146 case baseMenuItemID + 4: cut(); break;
2147 case baseMenuItemID + 5: selectAll(); break;
2148 case baseMenuItemID + 6: undo(); break;
2149 case baseMenuItemID + 7: redo(); break;
2150 default: break;
2154 //==============================================================================
2155 void TextEditor::focusGained (FocusChangeType)
2157 newTransaction();
2159 if (selectAllTextWhenFocused)
2161 moveCaretTo (0, false);
2162 moveCaretTo (getTotalNumChars(), true);
2165 repaint();
2166 updateCaretPosition();
2168 ComponentPeer* const peer = getPeer();
2169 if (peer != nullptr && ! isReadOnly())
2170 peer->textInputRequired (getScreenPosition() - peer->getScreenPosition());
2173 void TextEditor::focusLost (FocusChangeType)
2175 newTransaction();
2177 wasFocused = false;
2178 textHolder->stopTimer();
2180 underlinedSections.clear();
2182 ComponentPeer* const peer = getPeer();
2183 if (peer != nullptr)
2184 peer->dismissPendingTextInput();
2186 updateCaretPosition();
2188 postCommandMessage (TextEditorDefs::focusLossMessageId);
2189 repaint();
2192 //==============================================================================
2193 void TextEditor::resized()
2195 viewport->setBoundsInset (borderSize);
2196 viewport->setSingleStepSizes (16, roundToInt (currentFont.getHeight()));
2198 updateTextHolderSize();
2200 if (! isMultiLine())
2202 scrollToMakeSureCursorIsVisible();
2204 else
2206 updateCaretPosition();
2210 void TextEditor::handleCommandMessage (const int commandId)
2212 Component::BailOutChecker checker (this);
2214 switch (commandId)
2216 case TextEditorDefs::textChangeMessageId:
2217 listeners.callChecked (checker, &TextEditorListener::textEditorTextChanged, (TextEditor&) *this);
2218 break;
2220 case TextEditorDefs::returnKeyMessageId:
2221 listeners.callChecked (checker, &TextEditorListener::textEditorReturnKeyPressed, (TextEditor&) *this);
2222 break;
2224 case TextEditorDefs::escapeKeyMessageId:
2225 listeners.callChecked (checker, &TextEditorListener::textEditorEscapeKeyPressed, (TextEditor&) *this);
2226 break;
2228 case TextEditorDefs::focusLossMessageId:
2229 updateValueFromText();
2230 listeners.callChecked (checker, &TextEditorListener::textEditorFocusLost, (TextEditor&) *this);
2231 break;
2233 default:
2234 jassertfalse;
2235 break;
2239 void TextEditor::enablementChanged()
2241 setMouseCursor (isReadOnly() ? MouseCursor::NormalCursor
2242 : MouseCursor::IBeamCursor);
2243 repaint();
2246 void TextEditor::setTemporaryUnderlining (const Array <Range<int> >& newUnderlinedSections)
2248 underlinedSections = newUnderlinedSections;
2249 repaint();
2252 //==============================================================================
2253 UndoManager* TextEditor::getUndoManager() noexcept
2255 return isReadOnly() ? 0 : &undoManager;
2258 void TextEditor::clearInternal (UndoManager* const um)
2260 remove (Range<int> (0, getTotalNumChars()), um, caretPosition);
2263 void TextEditor::insert (const String& text,
2264 const int insertIndex,
2265 const Font& font,
2266 const Colour& colour,
2267 UndoManager* const um,
2268 const int caretPositionToMoveTo)
2270 if (text.isNotEmpty())
2272 if (um != nullptr)
2274 if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
2275 newTransaction();
2277 um->perform (new InsertAction (*this, text, insertIndex, font, colour,
2278 caretPosition, caretPositionToMoveTo));
2280 else
2282 repaintText (Range<int> (insertIndex, getTotalNumChars())); // must do this before and after changing the data, in case
2283 // a line gets moved due to word wrap
2285 int index = 0;
2286 int nextIndex = 0;
2288 for (int i = 0; i < sections.size(); ++i)
2290 nextIndex = index + sections.getUnchecked (i)->getTotalLength();
2292 if (insertIndex == index)
2294 sections.insert (i, new UniformTextSection (text,
2295 font, colour,
2296 passwordCharacter));
2297 break;
2299 else if (insertIndex > index && insertIndex < nextIndex)
2301 splitSection (i, insertIndex - index);
2302 sections.insert (i + 1, new UniformTextSection (text,
2303 font, colour,
2304 passwordCharacter));
2305 break;
2308 index = nextIndex;
2311 if (nextIndex == insertIndex)
2312 sections.add (new UniformTextSection (text,
2313 font, colour,
2314 passwordCharacter));
2316 coalesceSimilarSections();
2317 totalNumChars = -1;
2318 valueTextNeedsUpdating = true;
2320 updateTextHolderSize();
2321 moveCaretTo (caretPositionToMoveTo, false);
2323 repaintText (Range<int> (insertIndex, getTotalNumChars()));
2328 void TextEditor::reinsert (const int insertIndex,
2329 const Array <UniformTextSection*>& sectionsToInsert)
2331 int index = 0;
2332 int nextIndex = 0;
2334 for (int i = 0; i < sections.size(); ++i)
2336 nextIndex = index + sections.getUnchecked (i)->getTotalLength();
2338 if (insertIndex == index)
2340 for (int j = sectionsToInsert.size(); --j >= 0;)
2341 sections.insert (i, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
2343 break;
2345 else if (insertIndex > index && insertIndex < nextIndex)
2347 splitSection (i, insertIndex - index);
2349 for (int j = sectionsToInsert.size(); --j >= 0;)
2350 sections.insert (i + 1, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
2352 break;
2355 index = nextIndex;
2358 if (nextIndex == insertIndex)
2360 for (int j = 0; j < sectionsToInsert.size(); ++j)
2361 sections.add (new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
2364 coalesceSimilarSections();
2365 totalNumChars = -1;
2366 valueTextNeedsUpdating = true;
2369 void TextEditor::remove (const Range<int>& range,
2370 UndoManager* const um,
2371 const int caretPositionToMoveTo)
2373 if (! range.isEmpty())
2375 int index = 0;
2377 for (int i = 0; i < sections.size(); ++i)
2379 const int nextIndex = index + sections.getUnchecked(i)->getTotalLength();
2381 if (range.getStart() > index && range.getStart() < nextIndex)
2383 splitSection (i, range.getStart() - index);
2384 --i;
2386 else if (range.getEnd() > index && range.getEnd() < nextIndex)
2388 splitSection (i, range.getEnd() - index);
2389 --i;
2391 else
2393 index = nextIndex;
2395 if (index > range.getEnd())
2396 break;
2400 index = 0;
2402 if (um != nullptr)
2404 Array <UniformTextSection*> removedSections;
2406 for (int i = 0; i < sections.size(); ++i)
2408 if (range.getEnd() <= range.getStart())
2409 break;
2411 UniformTextSection* const section = sections.getUnchecked (i);
2413 const int nextIndex = index + section->getTotalLength();
2415 if (range.getStart() <= index && range.getEnd() >= nextIndex)
2416 removedSections.add (new UniformTextSection (*section));
2418 index = nextIndex;
2421 if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
2422 newTransaction();
2424 um->perform (new RemoveAction (*this, range, caretPosition,
2425 caretPositionToMoveTo, removedSections));
2427 else
2429 Range<int> remainingRange (range);
2431 for (int i = 0; i < sections.size(); ++i)
2433 UniformTextSection* const section = sections.getUnchecked (i);
2435 const int nextIndex = index + section->getTotalLength();
2437 if (remainingRange.getStart() <= index && remainingRange.getEnd() >= nextIndex)
2439 sections.remove(i);
2440 section->clear();
2441 delete section;
2443 remainingRange.setEnd (remainingRange.getEnd() - (nextIndex - index));
2444 if (remainingRange.isEmpty())
2445 break;
2447 --i;
2449 else
2451 index = nextIndex;
2455 coalesceSimilarSections();
2456 totalNumChars = -1;
2457 valueTextNeedsUpdating = true;
2459 moveCaretTo (caretPositionToMoveTo, false);
2461 repaintText (Range<int> (range.getStart(), getTotalNumChars()));
2466 //==============================================================================
2467 String TextEditor::getText() const
2469 MemoryOutputStream mo;
2470 mo.preallocate (getTotalNumChars());
2472 for (int i = 0; i < sections.size(); ++i)
2473 sections.getUnchecked (i)->appendAllText (mo);
2475 return mo.toString();
2478 const String TextEditor::getTextInRange (const Range<int>& range) const
2480 if (range.isEmpty())
2481 return String::empty;
2483 MemoryOutputStream mo;
2484 mo.preallocate (jmin (getTotalNumChars(), range.getLength()));
2486 int index = 0;
2488 for (int i = 0; i < sections.size(); ++i)
2490 const UniformTextSection* const s = sections.getUnchecked (i);
2491 const int nextIndex = index + s->getTotalLength();
2493 if (range.getStart() < nextIndex)
2495 if (range.getEnd() <= index)
2496 break;
2498 s->appendSubstring (mo, range - index);
2501 index = nextIndex;
2504 return mo.toString();
2507 String TextEditor::getHighlightedText() const
2509 return getTextInRange (selection);
2512 int TextEditor::getTotalNumChars() const
2514 if (totalNumChars < 0)
2516 totalNumChars = 0;
2518 for (int i = sections.size(); --i >= 0;)
2519 totalNumChars += sections.getUnchecked (i)->getTotalLength();
2522 return totalNumChars;
2525 bool TextEditor::isEmpty() const
2527 return getTotalNumChars() == 0;
2530 void TextEditor::getCharPosition (const int index, float& cx, float& cy, float& lineHeight) const
2532 const float wordWrapWidth = getWordWrapWidth();
2534 if (wordWrapWidth > 0 && sections.size() > 0)
2536 Iterator i (sections, wordWrapWidth, passwordCharacter);
2538 i.getCharPosition (index, cx, cy, lineHeight);
2540 else
2542 cx = cy = 0;
2543 lineHeight = currentFont.getHeight();
2547 int TextEditor::indexAtPosition (const float x, const float y)
2549 const float wordWrapWidth = getWordWrapWidth();
2551 if (wordWrapWidth > 0)
2553 Iterator i (sections, wordWrapWidth, passwordCharacter);
2555 while (i.next())
2557 if (i.lineY + i.lineHeight > y)
2559 if (i.lineY > y)
2560 return jmax (0, i.indexInText - 1);
2562 if (i.atomX >= x)
2563 return i.indexInText;
2565 if (x < i.atomRight)
2566 return i.xToIndex (x);
2571 return getTotalNumChars();
2574 //==============================================================================
2575 int TextEditor::findWordBreakAfter (const int position) const
2577 const String t (getTextInRange (Range<int> (position, position + 512)));
2578 const int totalLength = t.length();
2579 int i = 0;
2581 while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
2582 ++i;
2584 const int type = TextEditorDefs::getCharacterCategory (t[i]);
2586 while (i < totalLength && type == TextEditorDefs::getCharacterCategory (t[i]))
2587 ++i;
2589 while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
2590 ++i;
2592 return position + i;
2595 int TextEditor::findWordBreakBefore (const int position) const
2597 if (position <= 0)
2598 return 0;
2600 const int startOfBuffer = jmax (0, position - 512);
2601 const String t (getTextInRange (Range<int> (startOfBuffer, position)));
2603 int i = position - startOfBuffer;
2605 while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1]))
2606 --i;
2608 if (i > 0)
2610 const int type = TextEditorDefs::getCharacterCategory (t [i - 1]);
2612 while (i > 0 && type == TextEditorDefs::getCharacterCategory (t [i - 1]))
2613 --i;
2616 jassert (startOfBuffer + i >= 0);
2617 return startOfBuffer + i;
2621 //==============================================================================
2622 void TextEditor::splitSection (const int sectionIndex,
2623 const int charToSplitAt)
2625 jassert (sections[sectionIndex] != nullptr);
2627 sections.insert (sectionIndex + 1,
2628 sections.getUnchecked (sectionIndex)->split (charToSplitAt, passwordCharacter));
2631 void TextEditor::coalesceSimilarSections()
2633 for (int i = 0; i < sections.size() - 1; ++i)
2635 UniformTextSection* const s1 = sections.getUnchecked (i);
2636 UniformTextSection* const s2 = sections.getUnchecked (i + 1);
2638 if (s1->font == s2->font
2639 && s1->colour == s2->colour)
2641 s1->append (*s2, passwordCharacter);
2642 sections.remove (i + 1);
2643 delete s2;
2644 --i;
2649 void TextEditor::Listener::textEditorTextChanged (TextEditor&) {}
2650 void TextEditor::Listener::textEditorReturnKeyPressed (TextEditor&) {}
2651 void TextEditor::Listener::textEditorEscapeKeyPressed (TextEditor&) {}
2652 void TextEditor::Listener::textEditorFocusLost (TextEditor&) {}
2654 END_JUCE_NAMESPACE