1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 // a word or space that can't be broken down any further
30 struct TextAtom
31 {
32     //==============================================================================
33     String atomText;
34     float width;
35     int numChars;
36 
37     //==============================================================================
isWhitespacejuce::TextAtom38     bool isWhitespace() const noexcept       { return CharacterFunctions::isWhitespace (atomText[0]); }
isNewLinejuce::TextAtom39     bool isNewLine() const noexcept          { return atomText[0] == '\r' || atomText[0] == '\n'; }
40 
getTextjuce::TextAtom41     String getText (juce_wchar passwordCharacter) const
42     {
43         if (passwordCharacter == 0)
44             return atomText;
45 
46         return String::repeatedString (String::charToString (passwordCharacter),
47                                        atomText.length());
48     }
49 
getTrimmedTextjuce::TextAtom50     String getTrimmedText (const juce_wchar passwordCharacter) const
51     {
52         if (passwordCharacter == 0)
53             return atomText.substring (0, numChars);
54 
55         if (isNewLine())
56             return {};
57 
58         return String::repeatedString (String::charToString (passwordCharacter), numChars);
59     }
60 
61     JUCE_LEAK_DETECTOR (TextAtom)
62 };
63 
64 //==============================================================================
65 // a run of text with a single font and colour
66 class TextEditor::UniformTextSection
67 {
68 public:
UniformTextSection(const String & text,const Font & f,Colour col,juce_wchar passwordCharToUse)69     UniformTextSection (const String& text, const Font& f, Colour col, juce_wchar passwordCharToUse)
70         : font (f), colour (col), passwordChar (passwordCharToUse)
71     {
72         initialiseAtoms (text);
73     }
74 
75     UniformTextSection (const UniformTextSection&) = default;
76     UniformTextSection (UniformTextSection&&) = default;
77 
78     UniformTextSection& operator= (const UniformTextSection&) = delete;
79 
append(UniformTextSection & other)80     void append (UniformTextSection& other)
81     {
82         if (! other.atoms.isEmpty())
83         {
84             int i = 0;
85 
86             if (! atoms.isEmpty())
87             {
88                 auto& lastAtom = atoms.getReference (atoms.size() - 1);
89 
90                 if (! CharacterFunctions::isWhitespace (lastAtom.atomText.getLastCharacter()))
91                 {
92                     auto& first = other.atoms.getReference(0);
93 
94                     if (! CharacterFunctions::isWhitespace (first.atomText[0]))
95                     {
96                         lastAtom.atomText += first.atomText;
97                         lastAtom.numChars = (uint16) (lastAtom.numChars + first.numChars);
98                         lastAtom.width = font.getStringWidthFloat (lastAtom.getText (passwordChar));
99                         ++i;
100                     }
101                 }
102             }
103 
104             atoms.ensureStorageAllocated (atoms.size() + other.atoms.size() - i);
105 
106             while (i < other.atoms.size())
107             {
108                 atoms.add (other.atoms.getReference(i));
109                 ++i;
110             }
111         }
112     }
113 
split(int indexToBreakAt)114     UniformTextSection* split (int indexToBreakAt)
115     {
116         auto* section2 = new UniformTextSection ({}, font, colour, passwordChar);
117         int index = 0;
118 
119         for (int i = 0; i < atoms.size(); ++i)
120         {
121             auto& atom = atoms.getReference(i);
122             auto nextIndex = index + atom.numChars;
123 
124             if (index == indexToBreakAt)
125             {
126                 for (int j = i; j < atoms.size(); ++j)
127                     section2->atoms.add (atoms.getUnchecked (j));
128 
129                 atoms.removeRange (i, atoms.size());
130                 break;
131             }
132 
133             if (indexToBreakAt >= index && indexToBreakAt < nextIndex)
134             {
135                 TextAtom secondAtom;
136                 secondAtom.atomText = atom.atomText.substring (indexToBreakAt - index);
137                 secondAtom.width = font.getStringWidthFloat (secondAtom.getText (passwordChar));
138                 secondAtom.numChars = (uint16) secondAtom.atomText.length();
139 
140                 section2->atoms.add (secondAtom);
141 
142                 atom.atomText = atom.atomText.substring (0, indexToBreakAt - index);
143                 atom.width = font.getStringWidthFloat (atom.getText (passwordChar));
144                 atom.numChars = (uint16) (indexToBreakAt - index);
145 
146                 for (int j = i + 1; j < atoms.size(); ++j)
147                     section2->atoms.add (atoms.getUnchecked (j));
148 
149                 atoms.removeRange (i + 1, atoms.size());
150                 break;
151             }
152 
153             index = nextIndex;
154         }
155 
156         return section2;
157     }
158 
appendAllText(MemoryOutputStream & mo) const159     void appendAllText (MemoryOutputStream& mo) const
160     {
161         for (auto& atom : atoms)
162             mo << atom.atomText;
163     }
164 
appendSubstring(MemoryOutputStream & mo,Range<int> range) const165     void appendSubstring (MemoryOutputStream& mo, Range<int> range) const
166     {
167         int index = 0;
168 
169         for (auto& atom : atoms)
170         {
171             auto nextIndex = index + atom.numChars;
172 
173             if (range.getStart() < nextIndex)
174             {
175                 if (range.getEnd() <= index)
176                     break;
177 
178                 auto r = (range - index).getIntersectionWith ({ 0, (int) atom.numChars });
179 
180                 if (! r.isEmpty())
181                     mo << atom.atomText.substring (r.getStart(), r.getEnd());
182             }
183 
184             index = nextIndex;
185         }
186     }
187 
getTotalLength() const188     int getTotalLength() const noexcept
189     {
190         int total = 0;
191 
192         for (auto& atom : atoms)
193             total += atom.numChars;
194 
195         return total;
196     }
197 
setFont(const Font & newFont,const juce_wchar passwordCharToUse)198     void setFont (const Font& newFont, const juce_wchar passwordCharToUse)
199     {
200         if (font != newFont || passwordChar != passwordCharToUse)
201         {
202             font = newFont;
203             passwordChar = passwordCharToUse;
204 
205             for (auto& atom : atoms)
206                 atom.width = newFont.getStringWidthFloat (atom.getText (passwordChar));
207         }
208     }
209 
210     //==============================================================================
211     Font font;
212     Colour colour;
213     Array<TextAtom> atoms;
214     juce_wchar passwordChar;
215 
216 private:
initialiseAtoms(const String & textToParse)217     void initialiseAtoms (const String& textToParse)
218     {
219         auto text = textToParse.getCharPointer();
220 
221         while (! text.isEmpty())
222         {
223             size_t numChars = 0;
224             auto start = text;
225 
226             // create a whitespace atom unless it starts with non-ws
227             if (text.isWhitespace() && *text != '\r' && *text != '\n')
228             {
229                 do
230                 {
231                     ++text;
232                     ++numChars;
233                 }
234                 while (text.isWhitespace() && *text != '\r' && *text != '\n');
235             }
236             else
237             {
238                 if (*text == '\r')
239                 {
240                     ++text;
241                     ++numChars;
242 
243                     if (*text == '\n')
244                     {
245                         ++start;
246                         ++text;
247                     }
248                 }
249                 else if (*text == '\n')
250                 {
251                     ++text;
252                     ++numChars;
253                 }
254                 else
255                 {
256                     while (! (text.isEmpty() || text.isWhitespace()))
257                     {
258                         ++text;
259                         ++numChars;
260                     }
261                 }
262             }
263 
264             TextAtom atom;
265             atom.atomText = String (start, numChars);
266             atom.width = font.getStringWidthFloat (atom.getText (passwordChar));
267             atom.numChars = (uint16) numChars;
268             atoms.add (atom);
269         }
270     }
271 
272     JUCE_LEAK_DETECTOR (UniformTextSection)
273 };
274 
275 //==============================================================================
276 struct TextEditor::Iterator
277 {
Iteratorjuce::TextEditor::Iterator278     Iterator (const TextEditor& ed)
279       : sections (ed.sections),
280         justification (ed.justification),
281         bottomRight (ed.getMaximumWidth(), ed.getMaximumHeight()),
282         wordWrapWidth (ed.getWordWrapWidth()),
283         passwordCharacter (ed.passwordCharacter),
284         lineSpacing (ed.lineSpacing),
285         underlineWhitespace (ed.underlineWhitespace)
286     {
287         jassert (wordWrapWidth > 0);
288 
289         if (! sections.isEmpty())
290         {
291             currentSection = sections.getUnchecked (sectionIndex);
292 
293             if (currentSection != nullptr)
294                 beginNewLine();
295         }
296 
297         lineHeight = ed.currentFont.getHeight();
298     }
299 
300     Iterator (const Iterator&) = default;
301     Iterator& operator= (const Iterator&) = delete;
302 
303     //==============================================================================
nextjuce::TextEditor::Iterator304     bool next()
305     {
306         if (atom == &tempAtom)
307         {
308             auto numRemaining = tempAtom.atomText.length() - tempAtom.numChars;
309 
310             if (numRemaining > 0)
311             {
312                 tempAtom.atomText = tempAtom.atomText.substring (tempAtom.numChars);
313 
314                 if (tempAtom.numChars > 0)
315                     lineY += lineHeight * lineSpacing;
316 
317                 indexInText += tempAtom.numChars;
318 
319                 GlyphArrangement g;
320                 g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f);
321 
322                 int split;
323                 for (split = 0; split < g.getNumGlyphs(); ++split)
324                     if (shouldWrap (g.getGlyph (split).getRight()))
325                         break;
326 
327                 if (split > 0 && split <= numRemaining)
328                 {
329                     tempAtom.numChars = (uint16) split;
330                     tempAtom.width = g.getGlyph (split - 1).getRight();
331                     atomX = getJustificationOffsetX (tempAtom.width);
332                     atomRight = atomX + tempAtom.width;
333                     return true;
334                 }
335             }
336         }
337 
338         if (sectionIndex >= sections.size())
339         {
340             moveToEndOfLastAtom();
341             return false;
342         }
343 
344         bool forceNewLine = false;
345 
346         if (atomIndex >= currentSection->atoms.size() - 1)
347         {
348             if (atomIndex >= currentSection->atoms.size())
349             {
350                 if (++sectionIndex >= sections.size())
351                 {
352                     moveToEndOfLastAtom();
353                     return false;
354                 }
355 
356                 atomIndex = 0;
357                 currentSection = sections.getUnchecked (sectionIndex);
358             }
359             else
360             {
361                 auto& lastAtom = currentSection->atoms.getReference (atomIndex);
362 
363                 if (! lastAtom.isWhitespace())
364                 {
365                     // handle the case where the last atom in a section is actually part of the same
366                     // word as the first atom of the next section...
367                     float right = atomRight + lastAtom.width;
368                     float lineHeight2 = lineHeight;
369                     float maxDescent2 = maxDescent;
370 
371                     for (int section = sectionIndex + 1; section < sections.size(); ++section)
372                     {
373                         auto* s = sections.getUnchecked (section);
374 
375                         if (s->atoms.size() == 0)
376                             break;
377 
378                         auto& nextAtom = s->atoms.getReference (0);
379 
380                         if (nextAtom.isWhitespace())
381                             break;
382 
383                         right += nextAtom.width;
384 
385                         lineHeight2 = jmax (lineHeight2, s->font.getHeight());
386                         maxDescent2 = jmax (maxDescent2, s->font.getDescent());
387 
388                         if (shouldWrap (right))
389                         {
390                             lineHeight = lineHeight2;
391                             maxDescent = maxDescent2;
392 
393                             forceNewLine = true;
394                             break;
395                         }
396 
397                         if (s->atoms.size() > 1)
398                             break;
399                     }
400                 }
401             }
402         }
403 
404         if (atom != nullptr)
405         {
406             atomX = atomRight;
407             indexInText += atom->numChars;
408 
409             if (atom->isNewLine())
410                 beginNewLine();
411         }
412 
413         atom = &(currentSection->atoms.getReference (atomIndex));
414         atomRight = atomX + atom->width;
415         ++atomIndex;
416 
417         if (shouldWrap (atomRight) || forceNewLine)
418         {
419             if (atom->isWhitespace())
420             {
421                 // leave whitespace at the end of a line, but truncate it to avoid scrolling
422                 atomRight = jmin (atomRight, wordWrapWidth);
423             }
424             else
425             {
426                 if (shouldWrap (atom->width))  // atom too big to fit on a line, so break it up..
427                 {
428                     tempAtom = *atom;
429                     tempAtom.width = 0;
430                     tempAtom.numChars = 0;
431                     atom = &tempAtom;
432 
433                     if (atomX > justificationOffsetX)
434                         beginNewLine();
435 
436                     return next();
437                 }
438 
439                 beginNewLine();
440                 atomX = justificationOffsetX;
441                 atomRight = atomX + atom->width;
442                 return true;
443             }
444         }
445 
446         return true;
447     }
448 
beginNewLinejuce::TextEditor::Iterator449     void beginNewLine()
450     {
451         lineY += lineHeight * lineSpacing;
452         float lineWidth = 0;
453 
454         auto tempSectionIndex = sectionIndex;
455         auto tempAtomIndex = atomIndex;
456         auto* section = sections.getUnchecked (tempSectionIndex);
457 
458         lineHeight = section->font.getHeight();
459         maxDescent = section->font.getDescent();
460 
461         float nextLineWidth = (atom != nullptr) ? atom->width : 0.0f;
462 
463         while (! shouldWrap (nextLineWidth))
464         {
465             lineWidth = nextLineWidth;
466 
467             if (tempSectionIndex >= sections.size())
468                 break;
469 
470             bool checkSize = false;
471 
472             if (tempAtomIndex >= section->atoms.size())
473             {
474                 if (++tempSectionIndex >= sections.size())
475                     break;
476 
477                 tempAtomIndex = 0;
478                 section = sections.getUnchecked (tempSectionIndex);
479                 checkSize = true;
480             }
481 
482             if (! isPositiveAndBelow (tempAtomIndex, section->atoms.size()))
483                 break;
484 
485             auto& nextAtom = section->atoms.getReference (tempAtomIndex);
486             nextLineWidth += nextAtom.width;
487 
488             if (shouldWrap (nextLineWidth) || nextAtom.isNewLine())
489                 break;
490 
491             if (checkSize)
492             {
493                 lineHeight = jmax (lineHeight, section->font.getHeight());
494                 maxDescent = jmax (maxDescent, section->font.getDescent());
495             }
496 
497             ++tempAtomIndex;
498         }
499 
500         justificationOffsetX = getJustificationOffsetX (lineWidth);
501         atomX = justificationOffsetX;
502     }
503 
getJustificationOffsetXjuce::TextEditor::Iterator504     float getJustificationOffsetX (float lineWidth) const
505     {
506         if (justification.testFlags (Justification::horizontallyCentred))    return jmax (0.0f, (bottomRight.x - lineWidth) * 0.5f);
507         if (justification.testFlags (Justification::right))                  return jmax (0.0f, bottomRight.x - lineWidth);
508 
509         return 0;
510     }
511 
512     //==============================================================================
drawjuce::TextEditor::Iterator513     void draw (Graphics& g, const UniformTextSection*& lastSection, AffineTransform transform) const
514     {
515         if (passwordCharacter != 0 || (underlineWhitespace || ! atom->isWhitespace()))
516         {
517             if (lastSection != currentSection)
518             {
519                 lastSection = currentSection;
520                 g.setColour (currentSection->colour);
521                 g.setFont (currentSection->font);
522             }
523 
524             jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty());
525 
526             GlyphArrangement ga;
527             ga.addLineOfText (currentSection->font,
528                               atom->getTrimmedText (passwordCharacter),
529                               atomX, (float) roundToInt (lineY + lineHeight - maxDescent));
530             ga.draw (g, transform);
531         }
532     }
533 
addSelectionjuce::TextEditor::Iterator534     void addSelection (RectangleList<float>& area, Range<int> selected) const
535     {
536         auto startX = indexToX (selected.getStart());
537         auto endX   = indexToX (selected.getEnd());
538 
539         area.add (startX, lineY, endX - startX, lineHeight * lineSpacing);
540     }
541 
drawUnderlinejuce::TextEditor::Iterator542     void drawUnderline (Graphics& g, Range<int> underline, Colour colour, AffineTransform transform) const
543     {
544         auto startX    = roundToInt (indexToX (underline.getStart()));
545         auto endX      = roundToInt (indexToX (underline.getEnd()));
546         auto baselineY = roundToInt (lineY + currentSection->font.getAscent() + 0.5f);
547 
548         Graphics::ScopedSaveState state (g);
549         g.addTransform (transform);
550         g.reduceClipRegion ({ startX, baselineY, endX - startX, 1 });
551         g.fillCheckerBoard ({ (float) endX, (float) baselineY + 1.0f }, 3.0f, 1.0f, colour, Colours::transparentBlack);
552     }
553 
drawSelectedTextjuce::TextEditor::Iterator554     void drawSelectedText (Graphics& g, Range<int> selected, Colour selectedTextColour, AffineTransform transform) const
555     {
556         if (passwordCharacter != 0 || ! atom->isWhitespace())
557         {
558             GlyphArrangement ga;
559             ga.addLineOfText (currentSection->font,
560                               atom->getTrimmedText (passwordCharacter),
561                               atomX, (float) roundToInt (lineY + lineHeight - maxDescent));
562 
563             if (selected.getEnd() < indexInText + atom->numChars)
564             {
565                 GlyphArrangement ga2 (ga);
566                 ga2.removeRangeOfGlyphs (0, selected.getEnd() - indexInText);
567                 ga.removeRangeOfGlyphs (selected.getEnd() - indexInText, -1);
568 
569                 g.setColour (currentSection->colour);
570                 ga2.draw (g, transform);
571             }
572 
573             if (selected.getStart() > indexInText)
574             {
575                 GlyphArrangement ga2 (ga);
576                 ga2.removeRangeOfGlyphs (selected.getStart() - indexInText, -1);
577                 ga.removeRangeOfGlyphs (0, selected.getStart() - indexInText);
578 
579                 g.setColour (currentSection->colour);
580                 ga2.draw (g, transform);
581             }
582 
583             g.setColour (selectedTextColour);
584             ga.draw (g, transform);
585         }
586     }
587 
588     //==============================================================================
indexToXjuce::TextEditor::Iterator589     float indexToX (int indexToFind) const
590     {
591         if (indexToFind <= indexInText)
592             return atomX;
593 
594         if (indexToFind >= indexInText + atom->numChars)
595             return atomRight;
596 
597         GlyphArrangement g;
598         g.addLineOfText (currentSection->font,
599                          atom->getText (passwordCharacter),
600                          atomX, 0.0f);
601 
602         if (indexToFind - indexInText >= g.getNumGlyphs())
603             return atomRight;
604 
605         return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft());
606     }
607 
xToIndexjuce::TextEditor::Iterator608     int xToIndex (float xToFind) const
609     {
610         if (xToFind <= atomX || atom->isNewLine())
611             return indexInText;
612 
613         if (xToFind >= atomRight)
614             return indexInText + atom->numChars;
615 
616         GlyphArrangement g;
617         g.addLineOfText (currentSection->font,
618                          atom->getText (passwordCharacter),
619                          atomX, 0.0f);
620 
621         auto numGlyphs = g.getNumGlyphs();
622 
623         int j;
624         for (j = 0; j < numGlyphs; ++j)
625         {
626             auto& pg = g.getGlyph(j);
627 
628             if ((pg.getLeft() + pg.getRight()) / 2 > xToFind)
629                 break;
630         }
631 
632         return indexInText + j;
633     }
634 
635     //==============================================================================
getCharPositionjuce::TextEditor::Iterator636     bool getCharPosition (int index, Point<float>& anchor, float& lineHeightFound)
637     {
638         while (next())
639         {
640             if (indexInText + atom->numChars > index)
641             {
642                 anchor = { indexToX (index), lineY };
643                 lineHeightFound = lineHeight;
644                 return true;
645             }
646         }
647 
648         anchor = { atomX, lineY };
649         lineHeightFound = lineHeight;
650         return false;
651     }
652 
getYOffsetjuce::TextEditor::Iterator653     float getYOffset()
654     {
655         if (justification.testFlags (Justification::top) || lineY >= bottomRight.y)
656             return 0;
657 
658         while (next())
659         {
660             if (lineY >= bottomRight.y)
661                 return 0;
662         }
663 
664         auto bottom = jmax (0.0f, bottomRight.y - lineY - lineHeight);
665 
666         if (justification.testFlags (Justification::bottom))
667             return bottom;
668 
669         return bottom * 0.5f;
670     }
671 
672     //==============================================================================
673     int indexInText = 0;
674     float lineY = 0, lineHeight = 0, maxDescent = 0;
675     float atomX = 0, atomRight = 0;
676     const TextAtom* atom = nullptr;
677 
678 private:
679     const OwnedArray<UniformTextSection>& sections;
680     const UniformTextSection* currentSection = nullptr;
681     int sectionIndex = 0, atomIndex = 0;
682     Justification justification;
683     float justificationOffsetX = 0;
684     const Point<float> bottomRight;
685     const float wordWrapWidth;
686     const juce_wchar passwordCharacter;
687     const float lineSpacing;
688     const bool underlineWhitespace;
689     TextAtom tempAtom;
690 
moveToEndOfLastAtomjuce::TextEditor::Iterator691     void moveToEndOfLastAtom()
692     {
693         if (atom != nullptr)
694         {
695             atomX = atomRight;
696 
697             if (atom->isNewLine())
698             {
699                 atomX = getJustificationOffsetX (0);
700                 lineY += lineHeight * lineSpacing;
701             }
702         }
703     }
704 
shouldWrapjuce::TextEditor::Iterator705     bool shouldWrap (const float x) const noexcept
706     {
707         return (x - 0.0001f) >= wordWrapWidth;
708     }
709 
710     JUCE_LEAK_DETECTOR (Iterator)
711 };
712 
713 
714 //==============================================================================
715 struct TextEditor::InsertAction  : public UndoableAction
716 {
InsertActionjuce::TextEditor::InsertAction717     InsertAction (TextEditor& ed, const String& newText, int insertPos,
718                   const Font& newFont, Colour newColour, int oldCaret, int newCaret)
719         : owner (ed),
720           text (newText),
721           insertIndex (insertPos),
722           oldCaretPos (oldCaret),
723           newCaretPos (newCaret),
724           font (newFont),
725           colour (newColour)
726     {
727     }
728 
performjuce::TextEditor::InsertAction729     bool perform() override
730     {
731         owner.insert (text, insertIndex, font, colour, nullptr, newCaretPos);
732         return true;
733     }
734 
undojuce::TextEditor::InsertAction735     bool undo() override
736     {
737         owner.remove ({ insertIndex, insertIndex + text.length() }, nullptr, oldCaretPos);
738         return true;
739     }
740 
getSizeInUnitsjuce::TextEditor::InsertAction741     int getSizeInUnits() override
742     {
743         return text.length() + 16;
744     }
745 
746 private:
747     TextEditor& owner;
748     const String text;
749     const int insertIndex, oldCaretPos, newCaretPos;
750     const Font font;
751     const Colour colour;
752 
753     JUCE_DECLARE_NON_COPYABLE (InsertAction)
754 };
755 
756 //==============================================================================
757 struct TextEditor::RemoveAction  : public UndoableAction
758 {
RemoveActionjuce::TextEditor::RemoveAction759     RemoveAction (TextEditor& ed, Range<int> rangeToRemove, int oldCaret, int newCaret,
760                   const Array<UniformTextSection*>& oldSections)
761         : owner (ed),
762           range (rangeToRemove),
763           oldCaretPos (oldCaret),
764           newCaretPos (newCaret)
765     {
766         removedSections.addArray (oldSections);
767     }
768 
performjuce::TextEditor::RemoveAction769     bool perform() override
770     {
771         owner.remove (range, nullptr, newCaretPos);
772         return true;
773     }
774 
undojuce::TextEditor::RemoveAction775     bool undo() override
776     {
777         owner.reinsert (range.getStart(), removedSections);
778         owner.moveCaretTo (oldCaretPos, false);
779         return true;
780     }
781 
getSizeInUnitsjuce::TextEditor::RemoveAction782     int getSizeInUnits() override
783     {
784         int n = 16;
785 
786         for (auto* s : removedSections)
787             n += s->getTotalLength();
788 
789         return n;
790     }
791 
792 private:
793     TextEditor& owner;
794     const Range<int> range;
795     const int oldCaretPos, newCaretPos;
796     OwnedArray<UniformTextSection> removedSections;
797 
798     JUCE_DECLARE_NON_COPYABLE (RemoveAction)
799 };
800 
801 //==============================================================================
802 struct TextEditor::TextHolderComponent  : public Component,
803                                           public Timer,
804                                           public Value::Listener
805 {
TextHolderComponentjuce::TextEditor::TextHolderComponent806     TextHolderComponent (TextEditor& ed)  : owner (ed)
807     {
808         setWantsKeyboardFocus (false);
809         setInterceptsMouseClicks (false, true);
810         setMouseCursor (MouseCursor::ParentCursor);
811 
812         owner.getTextValue().addListener (this);
813     }
814 
~TextHolderComponentjuce::TextEditor::TextHolderComponent815     ~TextHolderComponent() override
816     {
817         owner.getTextValue().removeListener (this);
818     }
819 
paintjuce::TextEditor::TextHolderComponent820     void paint (Graphics& g) override
821     {
822         owner.drawContent (g);
823     }
824 
restartTimerjuce::TextEditor::TextHolderComponent825     void restartTimer()
826     {
827         startTimer (350);
828     }
829 
timerCallbackjuce::TextEditor::TextHolderComponent830     void timerCallback() override
831     {
832         owner.timerCallbackInt();
833     }
834 
valueChangedjuce::TextEditor::TextHolderComponent835     void valueChanged (Value&) override
836     {
837         owner.textWasChangedByValue();
838     }
839 
840     TextEditor& owner;
841 
842     JUCE_DECLARE_NON_COPYABLE (TextHolderComponent)
843 };
844 
845 //==============================================================================
846 struct TextEditor::TextEditorViewport  : public Viewport
847 {
TextEditorViewportjuce::TextEditor::TextEditorViewport848     TextEditorViewport (TextEditor& ed) : owner (ed) {}
849 
visibleAreaChangedjuce::TextEditor::TextEditorViewport850     void visibleAreaChanged (const Rectangle<int>&) override
851     {
852         if (! reentrant) // it's rare, but possible to get into a feedback loop as the viewport's scrollbars
853                          // appear and disappear, causing the wrap width to change.
854         {
855             auto wordWrapWidth = owner.getWordWrapWidth();
856 
857             if (wordWrapWidth != lastWordWrapWidth)
858             {
859                 lastWordWrapWidth = wordWrapWidth;
860 
861                 ScopedValueSetter<bool> svs (reentrant, true);
862                 owner.checkLayout();
863             }
864         }
865     }
866 
867 private:
868     TextEditor& owner;
869     float lastWordWrapWidth = 0;
870     bool reentrant = false;
871 
872     JUCE_DECLARE_NON_COPYABLE (TextEditorViewport)
873 };
874 
875 //==============================================================================
876 namespace TextEditorDefs
877 {
878     const int textChangeMessageId = 0x10003001;
879     const int returnKeyMessageId  = 0x10003002;
880     const int escapeKeyMessageId  = 0x10003003;
881     const int focusLossMessageId  = 0x10003004;
882 
883     const int maxActionsPerTransaction = 100;
884 
getCharacterCategory(juce_wchar character)885     static int getCharacterCategory (juce_wchar character) noexcept
886     {
887         return CharacterFunctions::isLetterOrDigit (character)
888                     ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1);
889     }
890 }
891 
892 //==============================================================================
TextEditor(const String & name,juce_wchar passwordChar)893 TextEditor::TextEditor (const String& name, juce_wchar passwordChar)
894     : Component (name),
895       passwordCharacter (passwordChar)
896 {
897     setMouseCursor (MouseCursor::IBeamCursor);
898 
899     viewport.reset (new TextEditorViewport (*this));
900     addAndMakeVisible (viewport.get());
901     viewport->setViewedComponent (textHolder = new TextHolderComponent (*this));
902     viewport->setWantsKeyboardFocus (false);
903     viewport->setScrollBarsShown (false, false);
904 
905     setWantsKeyboardFocus (true);
906     recreateCaret();
907 }
908 
~TextEditor()909 TextEditor::~TextEditor()
910 {
911     if (wasFocused)
912         if (auto* peer = getPeer())
913             peer->dismissPendingTextInput();
914 
915     textValue.removeListener (textHolder);
916     textValue.referTo (Value());
917 
918     viewport.reset();
919     textHolder = nullptr;
920 }
921 
922 //==============================================================================
newTransaction()923 void TextEditor::newTransaction()
924 {
925     lastTransactionTime = Time::getApproximateMillisecondCounter();
926     undoManager.beginNewTransaction();
927 }
928 
undoOrRedo(const bool shouldUndo)929 bool TextEditor::undoOrRedo (const bool shouldUndo)
930 {
931     if (! isReadOnly())
932     {
933         newTransaction();
934 
935         if (shouldUndo ? undoManager.undo()
936                        : undoManager.redo())
937         {
938             scrollToMakeSureCursorIsVisible();
939             repaint();
940             textChanged();
941             return true;
942         }
943     }
944 
945     return false;
946 }
947 
undo()948 bool TextEditor::undo()     { return undoOrRedo (true); }
redo()949 bool TextEditor::redo()     { return undoOrRedo (false); }
950 
951 //==============================================================================
setMultiLine(const bool shouldBeMultiLine,const bool shouldWordWrap)952 void TextEditor::setMultiLine (const bool shouldBeMultiLine,
953                                const bool shouldWordWrap)
954 {
955     if (multiline != shouldBeMultiLine
956          || wordWrap != (shouldWordWrap && shouldBeMultiLine))
957     {
958         multiline = shouldBeMultiLine;
959         wordWrap = shouldWordWrap && shouldBeMultiLine;
960 
961         checkLayout();
962 
963         viewport->setViewPosition (0, 0);
964         resized();
965         scrollToMakeSureCursorIsVisible();
966     }
967 }
968 
isMultiLine() const969 bool TextEditor::isMultiLine() const
970 {
971     return multiline;
972 }
973 
setScrollbarsShown(bool shown)974 void TextEditor::setScrollbarsShown (bool shown)
975 {
976     if (scrollbarVisible != shown)
977     {
978         scrollbarVisible = shown;
979         checkLayout();
980     }
981 }
982 
setReadOnly(bool shouldBeReadOnly)983 void TextEditor::setReadOnly (bool shouldBeReadOnly)
984 {
985     if (readOnly != shouldBeReadOnly)
986     {
987         readOnly = shouldBeReadOnly;
988         enablementChanged();
989     }
990 }
991 
isReadOnly() const992 bool TextEditor::isReadOnly() const noexcept
993 {
994     return readOnly || ! isEnabled();
995 }
996 
isTextInputActive() const997 bool TextEditor::isTextInputActive() const
998 {
999     return ! isReadOnly();
1000 }
1001 
setReturnKeyStartsNewLine(bool shouldStartNewLine)1002 void TextEditor::setReturnKeyStartsNewLine (bool shouldStartNewLine)
1003 {
1004     returnKeyStartsNewLine = shouldStartNewLine;
1005 }
1006 
setTabKeyUsedAsCharacter(bool shouldTabKeyBeUsed)1007 void TextEditor::setTabKeyUsedAsCharacter (bool shouldTabKeyBeUsed)
1008 {
1009     tabKeyUsed = shouldTabKeyBeUsed;
1010 }
1011 
setPopupMenuEnabled(bool b)1012 void TextEditor::setPopupMenuEnabled (bool b)
1013 {
1014     popupMenuEnabled = b;
1015 }
1016 
setSelectAllWhenFocused(bool b)1017 void TextEditor::setSelectAllWhenFocused (bool b)
1018 {
1019     selectAllTextWhenFocused = b;
1020 }
1021 
setJustification(Justification j)1022 void TextEditor::setJustification (Justification j)
1023 {
1024     if (justification != j)
1025     {
1026         justification = j;
1027 
1028         resized();
1029         repaint();
1030     }
1031 }
1032 
1033 //==============================================================================
setFont(const Font & newFont)1034 void TextEditor::setFont (const Font& newFont)
1035 {
1036     currentFont = newFont;
1037     scrollToMakeSureCursorIsVisible();
1038 }
1039 
applyFontToAllText(const Font & newFont,bool changeCurrentFont)1040 void TextEditor::applyFontToAllText (const Font& newFont, bool changeCurrentFont)
1041 {
1042     if (changeCurrentFont)
1043         currentFont = newFont;
1044 
1045     auto overallColour = findColour (textColourId);
1046 
1047     for (auto* uts : sections)
1048     {
1049         uts->setFont (newFont, passwordCharacter);
1050         uts->colour = overallColour;
1051     }
1052 
1053     coalesceSimilarSections();
1054     checkLayout();
1055     scrollToMakeSureCursorIsVisible();
1056     repaint();
1057 }
1058 
applyColourToAllText(const Colour & newColour,bool changeCurrentTextColour)1059 void TextEditor::applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour)
1060 {
1061     for (auto* uts : sections)
1062         uts->colour = newColour;
1063 
1064     if (changeCurrentTextColour)
1065         setColour (TextEditor::textColourId, newColour);
1066     else
1067         repaint();
1068 }
1069 
lookAndFeelChanged()1070 void TextEditor::lookAndFeelChanged()
1071 {
1072     caret.reset();
1073     recreateCaret();
1074     repaint();
1075 }
1076 
parentHierarchyChanged()1077 void TextEditor::parentHierarchyChanged()
1078 {
1079     lookAndFeelChanged();
1080 }
1081 
enablementChanged()1082 void TextEditor::enablementChanged()
1083 {
1084     recreateCaret();
1085     repaint();
1086 }
1087 
setCaretVisible(bool shouldCaretBeVisible)1088 void TextEditor::setCaretVisible (bool shouldCaretBeVisible)
1089 {
1090     if (caretVisible != shouldCaretBeVisible)
1091     {
1092         caretVisible = shouldCaretBeVisible;
1093         recreateCaret();
1094     }
1095 }
1096 
recreateCaret()1097 void TextEditor::recreateCaret()
1098 {
1099     if (isCaretVisible())
1100     {
1101         if (caret == nullptr)
1102         {
1103             caret.reset (getLookAndFeel().createCaretComponent (this));
1104             textHolder->addChildComponent (caret.get());
1105             updateCaretPosition();
1106         }
1107     }
1108     else
1109     {
1110         caret.reset();
1111     }
1112 }
1113 
updateCaretPosition()1114 void TextEditor::updateCaretPosition()
1115 {
1116     if (caret != nullptr
1117         && getWidth() > 0 && getHeight() > 0)
1118     {
1119         Iterator i (*this);
1120         caret->setCaretPosition (getCaretRectangle().translated (leftIndent,
1121                                                                  topIndent + roundToInt (i.getYOffset())));
1122     }
1123 }
1124 
LengthAndCharacterRestriction(int maxLen,const String & chars)1125 TextEditor::LengthAndCharacterRestriction::LengthAndCharacterRestriction (int maxLen, const String& chars)
1126     : allowedCharacters (chars), maxLength (maxLen)
1127 {
1128 }
1129 
filterNewText(TextEditor & ed,const String & newInput)1130 String TextEditor::LengthAndCharacterRestriction::filterNewText (TextEditor& ed, const String& newInput)
1131 {
1132     String t (newInput);
1133 
1134     if (allowedCharacters.isNotEmpty())
1135         t = t.retainCharacters (allowedCharacters);
1136 
1137     if (maxLength > 0)
1138         t = t.substring (0, maxLength - (ed.getTotalNumChars() - ed.getHighlightedRegion().getLength()));
1139 
1140     return t;
1141 }
1142 
setInputFilter(InputFilter * newFilter,bool takeOwnership)1143 void TextEditor::setInputFilter (InputFilter* newFilter, bool takeOwnership)
1144 {
1145     inputFilter.set (newFilter, takeOwnership);
1146 }
1147 
setInputRestrictions(int maxLen,const String & chars)1148 void TextEditor::setInputRestrictions (int maxLen, const String& chars)
1149 {
1150     setInputFilter (new LengthAndCharacterRestriction (maxLen, chars), true);
1151 }
1152 
setTextToShowWhenEmpty(const String & text,Colour colourToUse)1153 void TextEditor::setTextToShowWhenEmpty (const String& text, Colour colourToUse)
1154 {
1155     textToShowWhenEmpty = text;
1156     colourForTextWhenEmpty = colourToUse;
1157 }
1158 
setPasswordCharacter(juce_wchar newPasswordCharacter)1159 void TextEditor::setPasswordCharacter (juce_wchar newPasswordCharacter)
1160 {
1161     if (passwordCharacter != newPasswordCharacter)
1162     {
1163         passwordCharacter = newPasswordCharacter;
1164         applyFontToAllText (currentFont);
1165     }
1166 }
1167 
setScrollBarThickness(int newThicknessPixels)1168 void TextEditor::setScrollBarThickness (int newThicknessPixels)
1169 {
1170     viewport->setScrollBarThickness (newThicknessPixels);
1171 }
1172 
1173 //==============================================================================
clear()1174 void TextEditor::clear()
1175 {
1176     clearInternal (nullptr);
1177     checkLayout();
1178     undoManager.clearUndoHistory();
1179 }
1180 
setText(const String & newText,bool sendTextChangeMessage)1181 void TextEditor::setText (const String& newText, bool sendTextChangeMessage)
1182 {
1183     auto newLength = newText.length();
1184 
1185     if (newLength != getTotalNumChars() || getText() != newText)
1186     {
1187         if (! sendTextChangeMessage)
1188             textValue.removeListener (textHolder);
1189 
1190         textValue = newText;
1191 
1192         auto oldCursorPos = caretPosition;
1193         bool cursorWasAtEnd = oldCursorPos >= getTotalNumChars();
1194 
1195         clearInternal (nullptr);
1196         insert (newText, 0, currentFont, findColour (textColourId), nullptr, caretPosition);
1197 
1198         // if you're adding text with line-feeds to a single-line text editor, it
1199         // ain't gonna look right!
1200         jassert (multiline || ! newText.containsAnyOf ("\r\n"));
1201 
1202         if (cursorWasAtEnd && ! isMultiLine())
1203             oldCursorPos = getTotalNumChars();
1204 
1205         moveCaretTo (oldCursorPos, false);
1206 
1207         if (sendTextChangeMessage)
1208             textChanged();
1209         else
1210             textValue.addListener (textHolder);
1211 
1212         checkLayout();
1213         scrollToMakeSureCursorIsVisible();
1214         undoManager.clearUndoHistory();
1215 
1216         repaint();
1217     }
1218 }
1219 
1220 //==============================================================================
updateValueFromText()1221 void TextEditor::updateValueFromText()
1222 {
1223     if (valueTextNeedsUpdating)
1224     {
1225         valueTextNeedsUpdating = false;
1226         textValue = getText();
1227     }
1228 }
1229 
getTextValue()1230 Value& TextEditor::getTextValue()
1231 {
1232     updateValueFromText();
1233     return textValue;
1234 }
1235 
textWasChangedByValue()1236 void TextEditor::textWasChangedByValue()
1237 {
1238     if (textValue.getValueSource().getReferenceCount() > 1)
1239         setText (textValue.getValue());
1240 }
1241 
1242 //==============================================================================
textChanged()1243 void TextEditor::textChanged()
1244 {
1245     checkLayout();
1246 
1247     if (listeners.size() != 0 || onTextChange != nullptr)
1248         postCommandMessage (TextEditorDefs::textChangeMessageId);
1249 
1250     if (textValue.getValueSource().getReferenceCount() > 1)
1251     {
1252         valueTextNeedsUpdating = false;
1253         textValue = getText();
1254     }
1255 }
1256 
returnPressed()1257 void TextEditor::returnPressed()    { postCommandMessage (TextEditorDefs::returnKeyMessageId); }
escapePressed()1258 void TextEditor::escapePressed()    { postCommandMessage (TextEditorDefs::escapeKeyMessageId); }
1259 
addListener(Listener * l)1260 void TextEditor::addListener (Listener* l)      { listeners.add (l); }
removeListener(Listener * l)1261 void TextEditor::removeListener (Listener* l)   { listeners.remove (l); }
1262 
1263 //==============================================================================
timerCallbackInt()1264 void TextEditor::timerCallbackInt()
1265 {
1266     checkFocus();
1267 
1268     auto now = Time::getApproximateMillisecondCounter();
1269 
1270     if (now > lastTransactionTime + 200)
1271         newTransaction();
1272 }
1273 
checkFocus()1274 void TextEditor::checkFocus()
1275 {
1276     if (! wasFocused && hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent())
1277     {
1278         wasFocused = true;
1279 
1280         if (auto* peer = getPeer())
1281             if (! isReadOnly())
1282                 peer->textInputRequired (peer->globalToLocal (getScreenPosition()), *this);
1283     }
1284 }
1285 
repaintText(Range<int> range)1286 void TextEditor::repaintText (Range<int> range)
1287 {
1288     if (! range.isEmpty())
1289     {
1290         if (range.getEnd() >= getTotalNumChars())
1291         {
1292             textHolder->repaint();
1293             return;
1294         }
1295 
1296         Iterator i (*this);
1297 
1298         Point<float> anchor;
1299         auto lh = currentFont.getHeight();
1300         i.getCharPosition (range.getStart(), anchor, lh);
1301 
1302         auto y1 = std::trunc (anchor.y);
1303         int y2 = 0;
1304 
1305         if (range.getEnd() >= getTotalNumChars())
1306         {
1307             y2 = textHolder->getHeight();
1308         }
1309         else
1310         {
1311             i.getCharPosition (range.getEnd(), anchor, lh);
1312             y2 = (int) (anchor.y + lh * 2.0f);
1313         }
1314 
1315         auto offset = i.getYOffset();
1316         textHolder->repaint (0, roundToInt (y1 + offset), textHolder->getWidth(), roundToInt ((float) y2 - y1 + offset));
1317     }
1318 }
1319 
1320 //==============================================================================
moveCaret(int newCaretPos)1321 void TextEditor::moveCaret (int newCaretPos)
1322 {
1323     if (newCaretPos < 0)
1324         newCaretPos = 0;
1325     else
1326         newCaretPos = jmin (newCaretPos, getTotalNumChars());
1327 
1328     if (newCaretPos != getCaretPosition())
1329     {
1330         caretPosition = newCaretPos;
1331         textHolder->restartTimer();
1332         scrollToMakeSureCursorIsVisible();
1333         updateCaretPosition();
1334     }
1335 }
1336 
getCaretPosition() const1337 int TextEditor::getCaretPosition() const
1338 {
1339     return caretPosition;
1340 }
1341 
setCaretPosition(const int newIndex)1342 void TextEditor::setCaretPosition (const int newIndex)
1343 {
1344     moveCaretTo (newIndex, false);
1345 }
1346 
moveCaretToEnd()1347 void TextEditor::moveCaretToEnd()
1348 {
1349     setCaretPosition (std::numeric_limits<int>::max());
1350 }
1351 
scrollEditorToPositionCaret(const int desiredCaretX,const int desiredCaretY)1352 void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX,
1353                                               const int desiredCaretY)
1354 
1355 {
1356     updateCaretPosition();
1357     auto caretPos = getCaretRectangle();
1358 
1359     auto vx = caretPos.getX() - desiredCaretX;
1360     auto vy = caretPos.getY() - desiredCaretY;
1361 
1362     if (desiredCaretX < jmax (1, proportionOfWidth (0.05f)))
1363         vx += desiredCaretX - proportionOfWidth (0.2f);
1364     else if (desiredCaretX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
1365         vx += desiredCaretX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
1366 
1367     vx = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vx);
1368 
1369     if (! isMultiLine())
1370     {
1371         vy = viewport->getViewPositionY();
1372     }
1373     else
1374     {
1375         vy = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vy);
1376 
1377         if (desiredCaretY < 0)
1378             vy = jmax (0, desiredCaretY + vy);
1379         else if (desiredCaretY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretPos.getHeight()))
1380             vy += desiredCaretY + 2 + caretPos.getHeight() + topIndent - viewport->getMaximumVisibleHeight();
1381     }
1382 
1383     viewport->setViewPosition (vx, vy);
1384 }
1385 
getCaretRectangle()1386 Rectangle<int> TextEditor::getCaretRectangle()
1387 {
1388     return getCaretRectangleFloat().getSmallestIntegerContainer();
1389 }
1390 
getCaretRectangleFloat() const1391 Rectangle<float> TextEditor::getCaretRectangleFloat() const
1392 {
1393     Point<float> anchor;
1394     auto cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value)
1395     getCharPosition (caretPosition, anchor, cursorHeight);
1396 
1397     return { anchor.x, anchor.y, 2.0f, cursorHeight };
1398 }
1399 
1400 //==============================================================================
1401 // Extra space for the cursor at the right-hand-edge
1402 constexpr int rightEdgeSpace = 2;
1403 
getWordWrapWidth() const1404 float TextEditor::getWordWrapWidth() const
1405 {
1406     return wordWrap ? getMaximumWidth()
1407                     : std::numeric_limits<float>::max();
1408 }
1409 
getMaximumWidth() const1410 float TextEditor::getMaximumWidth() const
1411 {
1412     return (float) (viewport->getMaximumVisibleWidth() - (leftIndent + rightEdgeSpace + 1));
1413 }
1414 
getMaximumHeight() const1415 float TextEditor::getMaximumHeight() const
1416 {
1417     return (float) (viewport->getMaximumVisibleHeight() - topIndent);
1418 }
1419 
checkLayout()1420 void TextEditor::checkLayout()
1421 {
1422     if (getWordWrapWidth() > 0)
1423     {
1424         auto maxWidth = getMaximumWidth();
1425         Iterator i (*this);
1426 
1427         while (i.next())
1428             maxWidth = jmax (maxWidth, i.atomRight);
1429 
1430         auto textRight = roundToInt (maxWidth);
1431         auto textBottom = roundToInt (i.lineY + i.lineHeight + i.getYOffset());
1432 
1433         if (i.atom != nullptr && i.atom->isNewLine())
1434             textBottom += (int) i.lineHeight;
1435 
1436         updateTextHolderSize (textRight, textBottom);
1437         updateScrollbarVisibility (textRight, textBottom);
1438     }
1439 }
1440 
updateTextHolderSize(int textRight,int textBottom)1441 void TextEditor::updateTextHolderSize (int textRight, int textBottom)
1442 {
1443     auto w = leftIndent + jmax (roundToInt (getMaximumWidth()), textRight);
1444     auto h = topIndent + textBottom;
1445 
1446     textHolder->setSize (w + rightEdgeSpace, h + 1);
1447 }
1448 
updateScrollbarVisibility(int textRight,int textBottom)1449 void TextEditor::updateScrollbarVisibility (int textRight, int textBottom)
1450 {
1451     if (scrollbarVisible && multiline)
1452     {
1453         auto horizontalVisible = (leftIndent + textRight) > (viewport->getMaximumVisibleWidth() - viewport->getScrollBarThickness());
1454         auto verticalVisible = (topIndent + textBottom) > (viewport->getMaximumVisibleHeight() + 1);
1455 
1456         viewport->setScrollBarsShown (verticalVisible, horizontalVisible);
1457     }
1458     else
1459     {
1460         viewport->setScrollBarsShown (false, false);
1461     }
1462 }
1463 
getTextWidth() const1464 int TextEditor::getTextWidth() const    { return textHolder->getWidth(); }
getTextHeight() const1465 int TextEditor::getTextHeight() const   { return textHolder->getHeight(); }
1466 
setIndents(int newLeftIndent,int newTopIndent)1467 void TextEditor::setIndents (int newLeftIndent, int newTopIndent)
1468 {
1469     if (leftIndent != newLeftIndent || topIndent != newTopIndent)
1470     {
1471         leftIndent = newLeftIndent;
1472         topIndent  = newTopIndent;
1473 
1474         resized();
1475         repaint();
1476     }
1477 }
1478 
setBorder(BorderSize<int> border)1479 void TextEditor::setBorder (BorderSize<int> border)
1480 {
1481     borderSize = border;
1482     resized();
1483 }
1484 
getBorder() const1485 BorderSize<int> TextEditor::getBorder() const
1486 {
1487     return borderSize;
1488 }
1489 
setScrollToShowCursor(const bool shouldScrollToShowCursor)1490 void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor)
1491 {
1492     keepCaretOnScreen = shouldScrollToShowCursor;
1493 }
1494 
scrollToMakeSureCursorIsVisible()1495 void TextEditor::scrollToMakeSureCursorIsVisible()
1496 {
1497     updateCaretPosition();
1498 
1499     if (keepCaretOnScreen)
1500     {
1501         auto viewPos = viewport->getViewPosition();
1502         auto caretRect = getCaretRectangle();
1503         auto relativeCursor = caretRect.getPosition() - viewPos;
1504 
1505         if (relativeCursor.x < jmax (1, proportionOfWidth (0.05f)))
1506         {
1507             viewPos.x += relativeCursor.x - proportionOfWidth (0.2f);
1508         }
1509         else if (relativeCursor.x > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
1510         {
1511             viewPos.x += relativeCursor.x + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
1512         }
1513 
1514         viewPos.x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), viewPos.x);
1515 
1516         if (! isMultiLine())
1517         {
1518             viewPos.y = (getHeight() - textHolder->getHeight() - topIndent) / -2;
1519         }
1520         else if (relativeCursor.y < 0)
1521         {
1522             viewPos.y = jmax (0, relativeCursor.y + viewPos.y);
1523         }
1524         else if (relativeCursor.y > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretRect.getHeight()))
1525         {
1526             viewPos.y += relativeCursor.y + 2 + caretRect.getHeight() + topIndent - viewport->getMaximumVisibleHeight();
1527         }
1528 
1529         viewport->setViewPosition (viewPos);
1530     }
1531 }
1532 
moveCaretTo(const int newPosition,const bool isSelecting)1533 void TextEditor::moveCaretTo (const int newPosition, const bool isSelecting)
1534 {
1535     if (isSelecting)
1536     {
1537         moveCaret (newPosition);
1538 
1539         auto oldSelection = selection;
1540 
1541         if (dragType == notDragging)
1542         {
1543             if (std::abs (getCaretPosition() - selection.getStart()) < std::abs (getCaretPosition() - selection.getEnd()))
1544                 dragType = draggingSelectionStart;
1545             else
1546                 dragType = draggingSelectionEnd;
1547         }
1548 
1549         if (dragType == draggingSelectionStart)
1550         {
1551             if (getCaretPosition() >= selection.getEnd())
1552                 dragType = draggingSelectionEnd;
1553 
1554             selection = Range<int>::between (getCaretPosition(), selection.getEnd());
1555         }
1556         else
1557         {
1558             if (getCaretPosition() < selection.getStart())
1559                 dragType = draggingSelectionStart;
1560 
1561             selection = Range<int>::between (getCaretPosition(), selection.getStart());
1562         }
1563 
1564         repaintText (selection.getUnionWith (oldSelection));
1565     }
1566     else
1567     {
1568         dragType = notDragging;
1569 
1570         repaintText (selection);
1571 
1572         moveCaret (newPosition);
1573         selection = Range<int>::emptyRange (getCaretPosition());
1574     }
1575 }
1576 
getTextIndexAt(const int x,const int y)1577 int TextEditor::getTextIndexAt (const int x, const int y)
1578 {
1579     Iterator i (*this);
1580 
1581     return indexAtPosition ((float) (x + viewport->getViewPositionX() - leftIndent - borderSize.getLeft()),
1582                             (float) (y + viewport->getViewPositionY() - topIndent  - borderSize.getTop()) - i.getYOffset());
1583 }
1584 
insertTextAtCaret(const String & t)1585 void TextEditor::insertTextAtCaret (const String& t)
1586 {
1587     String newText (inputFilter != nullptr ? inputFilter->filterNewText (*this, t) : t);
1588 
1589     if (isMultiLine())
1590         newText = newText.replace ("\r\n", "\n");
1591     else
1592         newText = newText.replaceCharacters ("\r\n", "  ");
1593 
1594     const int insertIndex = selection.getStart();
1595     const int newCaretPos = insertIndex + newText.length();
1596 
1597     remove (selection, getUndoManager(),
1598             newText.isNotEmpty() ? newCaretPos - 1 : newCaretPos);
1599 
1600     insert (newText, insertIndex, currentFont, findColour (textColourId),
1601             getUndoManager(), newCaretPos);
1602 
1603     textChanged();
1604 }
1605 
setHighlightedRegion(const Range<int> & newSelection)1606 void TextEditor::setHighlightedRegion (const Range<int>& newSelection)
1607 {
1608     moveCaretTo (newSelection.getStart(), false);
1609     moveCaretTo (newSelection.getEnd(), true);
1610 }
1611 
1612 //==============================================================================
copy()1613 void TextEditor::copy()
1614 {
1615     if (passwordCharacter == 0)
1616     {
1617         auto selectedText = getHighlightedText();
1618 
1619         if (selectedText.isNotEmpty())
1620             SystemClipboard::copyTextToClipboard (selectedText);
1621     }
1622 }
1623 
paste()1624 void TextEditor::paste()
1625 {
1626     if (! isReadOnly())
1627     {
1628         auto clip = SystemClipboard::getTextFromClipboard();
1629 
1630         if (clip.isNotEmpty())
1631             insertTextAtCaret (clip);
1632     }
1633 }
1634 
cut()1635 void TextEditor::cut()
1636 {
1637     if (! isReadOnly())
1638     {
1639         moveCaret (selection.getEnd());
1640         insertTextAtCaret (String());
1641     }
1642 }
1643 
1644 //==============================================================================
drawContent(Graphics & g)1645 void TextEditor::drawContent (Graphics& g)
1646 {
1647     if (getWordWrapWidth() > 0)
1648     {
1649         g.setOrigin (leftIndent, topIndent);
1650         auto clip = g.getClipBounds();
1651 
1652         auto yOffset = Iterator (*this).getYOffset();
1653 
1654         AffineTransform transform;
1655 
1656         if (yOffset > 0)
1657         {
1658             transform = AffineTransform::translation (0.0f, yOffset);
1659             clip.setY (roundToInt ((float) clip.getY() - yOffset));
1660         }
1661 
1662         Iterator i (*this);
1663         Colour selectedTextColour;
1664 
1665         if (! selection.isEmpty())
1666         {
1667             Iterator i2 (i);
1668             RectangleList<float> selectionArea;
1669 
1670             while (i2.next() && i2.lineY < (float) clip.getBottom())
1671             {
1672                 if (i2.lineY + i2.lineHeight >= (float) clip.getY()
1673                     && selection.intersects ({ i2.indexInText, i2.indexInText + i2.atom->numChars }))
1674                 {
1675                     i2.addSelection (selectionArea, selection);
1676                 }
1677             }
1678 
1679             selectedTextColour = findColour (highlightedTextColourId);
1680 
1681             g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f));
1682             g.fillPath (selectionArea.toPath(), transform);
1683         }
1684 
1685         const UniformTextSection* lastSection = nullptr;
1686 
1687         while (i.next() && i.lineY < (float) clip.getBottom())
1688         {
1689             if (i.lineY + i.lineHeight >= (float) clip.getY())
1690             {
1691                 if (selection.intersects ({ i.indexInText, i.indexInText + i.atom->numChars }))
1692                 {
1693                     i.drawSelectedText (g, selection, selectedTextColour, transform);
1694                     lastSection = nullptr;
1695                 }
1696                 else
1697                 {
1698                     i.draw (g, lastSection, transform);
1699                 }
1700             }
1701         }
1702 
1703         for (auto& underlinedSection : underlinedSections)
1704         {
1705             Iterator i2 (*this);
1706 
1707             while (i2.next() && i2.lineY < (float) clip.getBottom())
1708             {
1709                 if (i2.lineY + i2.lineHeight >= (float) clip.getY()
1710                       && underlinedSection.intersects ({ i2.indexInText, i2.indexInText + i2.atom->numChars }))
1711                 {
1712                     i2.drawUnderline (g, underlinedSection, findColour (textColourId), transform);
1713                 }
1714             }
1715         }
1716     }
1717 }
1718 
paint(Graphics & g)1719 void TextEditor::paint (Graphics& g)
1720 {
1721     getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this);
1722 }
1723 
paintOverChildren(Graphics & g)1724 void TextEditor::paintOverChildren (Graphics& g)
1725 {
1726     if (textToShowWhenEmpty.isNotEmpty()
1727          && (! hasKeyboardFocus (false))
1728          && getTotalNumChars() == 0)
1729     {
1730         g.setColour (colourForTextWhenEmpty);
1731         g.setFont (getFont());
1732 
1733         g.drawText (textToShowWhenEmpty,
1734                     leftIndent, topIndent,
1735                     viewport->getWidth() - leftIndent, getHeight() - topIndent,
1736                     justification, true);
1737     }
1738 
1739     getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this);
1740 }
1741 
1742 //==============================================================================
addPopupMenuItems(PopupMenu & m,const MouseEvent *)1743 void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*)
1744 {
1745     const bool writable = ! isReadOnly();
1746 
1747     if (passwordCharacter == 0)
1748     {
1749         m.addItem (StandardApplicationCommandIDs::cut,   TRANS("Cut"), writable);
1750         m.addItem (StandardApplicationCommandIDs::copy,  TRANS("Copy"), ! selection.isEmpty());
1751     }
1752 
1753     m.addItem (StandardApplicationCommandIDs::paste,     TRANS("Paste"), writable);
1754     m.addItem (StandardApplicationCommandIDs::del,       TRANS("Delete"), writable);
1755     m.addSeparator();
1756     m.addItem (StandardApplicationCommandIDs::selectAll, TRANS("Select All"));
1757     m.addSeparator();
1758 
1759     if (getUndoManager() != nullptr)
1760     {
1761         m.addItem (StandardApplicationCommandIDs::undo, TRANS("Undo"), undoManager.canUndo());
1762         m.addItem (StandardApplicationCommandIDs::redo, TRANS("Redo"), undoManager.canRedo());
1763     }
1764 }
1765 
performPopupMenuAction(const int menuItemID)1766 void TextEditor::performPopupMenuAction (const int menuItemID)
1767 {
1768     switch (menuItemID)
1769     {
1770         case StandardApplicationCommandIDs::cut:        cutToClipboard(); break;
1771         case StandardApplicationCommandIDs::copy:       copyToClipboard(); break;
1772         case StandardApplicationCommandIDs::paste:      pasteFromClipboard(); break;
1773         case StandardApplicationCommandIDs::del:        cut(); break;
1774         case StandardApplicationCommandIDs::selectAll:  selectAll(); break;
1775         case StandardApplicationCommandIDs::undo:       undo(); break;
1776         case StandardApplicationCommandIDs::redo:       redo(); break;
1777         default: break;
1778     }
1779 }
1780 
1781 //==============================================================================
mouseDown(const MouseEvent & e)1782 void TextEditor::mouseDown (const MouseEvent& e)
1783 {
1784     beginDragAutoRepeat (100);
1785     newTransaction();
1786 
1787     if (wasFocused || ! selectAllTextWhenFocused)
1788     {
1789         if (! (popupMenuEnabled && e.mods.isPopupMenu()))
1790         {
1791             moveCaretTo (getTextIndexAt (e.x, e.y),
1792                          e.mods.isShiftDown());
1793         }
1794         else
1795         {
1796             PopupMenu m;
1797             m.setLookAndFeel (&getLookAndFeel());
1798             addPopupMenuItems (m, &e);
1799 
1800             menuActive = true;
1801 
1802             SafePointer<TextEditor> safeThis (this);
1803 
1804             m.showMenuAsync (PopupMenu::Options(),
1805                              [safeThis] (int menuResult)
1806                              {
1807                                  if (auto* editor = safeThis.getComponent())
1808                                  {
1809                                      editor->menuActive = false;
1810 
1811                                      if (menuResult != 0)
1812                                          editor->performPopupMenuAction (menuResult);
1813                                  }
1814                              });
1815         }
1816     }
1817 }
1818 
mouseDrag(const MouseEvent & e)1819 void TextEditor::mouseDrag (const MouseEvent& e)
1820 {
1821     if (wasFocused || ! selectAllTextWhenFocused)
1822         if (! (popupMenuEnabled && e.mods.isPopupMenu()))
1823             moveCaretTo (getTextIndexAt (e.x, e.y), true);
1824 }
1825 
mouseUp(const MouseEvent & e)1826 void TextEditor::mouseUp (const MouseEvent& e)
1827 {
1828     newTransaction();
1829     textHolder->restartTimer();
1830 
1831     if (wasFocused || ! selectAllTextWhenFocused)
1832         if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu()))
1833             moveCaret (getTextIndexAt (e.x, e.y));
1834 
1835     wasFocused = true;
1836 }
1837 
mouseDoubleClick(const MouseEvent & e)1838 void TextEditor::mouseDoubleClick (const MouseEvent& e)
1839 {
1840     int tokenEnd = getTextIndexAt (e.x, e.y);
1841     int tokenStart = 0;
1842 
1843     if (e.getNumberOfClicks() > 3)
1844     {
1845         tokenEnd = getTotalNumChars();
1846     }
1847     else
1848     {
1849         auto t = getText();
1850         auto totalLength = getTotalNumChars();
1851 
1852         while (tokenEnd < totalLength)
1853         {
1854             auto c = t[tokenEnd];
1855 
1856             // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
1857             if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
1858                 ++tokenEnd;
1859             else
1860                 break;
1861         }
1862 
1863         tokenStart = tokenEnd;
1864 
1865         while (tokenStart > 0)
1866         {
1867             auto c = t[tokenStart - 1];
1868 
1869             // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
1870             if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
1871                 --tokenStart;
1872             else
1873                 break;
1874         }
1875 
1876         if (e.getNumberOfClicks() > 2)
1877         {
1878             while (tokenEnd < totalLength)
1879             {
1880                 auto c = t[tokenEnd];
1881 
1882                 if (c != '\r' && c != '\n')
1883                     ++tokenEnd;
1884                 else
1885                     break;
1886             }
1887 
1888             while (tokenStart > 0)
1889             {
1890                 auto c = t[tokenStart - 1];
1891 
1892                 if (c != '\r' && c != '\n')
1893                     --tokenStart;
1894                 else
1895                     break;
1896             }
1897         }
1898     }
1899 
1900     moveCaretTo (tokenEnd, false);
1901     moveCaretTo (tokenStart, true);
1902 }
1903 
mouseWheelMove(const MouseEvent & e,const MouseWheelDetails & wheel)1904 void TextEditor::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
1905 {
1906     if (! viewport->useMouseWheelMoveIfNeeded (e, wheel))
1907         Component::mouseWheelMove (e, wheel);
1908 }
1909 
1910 //==============================================================================
moveCaretWithTransaction(const int newPos,const bool selecting)1911 bool TextEditor::moveCaretWithTransaction (const int newPos, const bool selecting)
1912 {
1913     newTransaction();
1914     moveCaretTo (newPos, selecting);
1915     return true;
1916 }
1917 
moveCaretLeft(bool moveInWholeWordSteps,bool selecting)1918 bool TextEditor::moveCaretLeft (bool moveInWholeWordSteps, bool selecting)
1919 {
1920     auto pos = getCaretPosition();
1921 
1922     if (moveInWholeWordSteps)
1923         pos = findWordBreakBefore (pos);
1924     else
1925         --pos;
1926 
1927     return moveCaretWithTransaction (pos, selecting);
1928 }
1929 
moveCaretRight(bool moveInWholeWordSteps,bool selecting)1930 bool TextEditor::moveCaretRight (bool moveInWholeWordSteps, bool selecting)
1931 {
1932     auto pos = getCaretPosition();
1933 
1934     if (moveInWholeWordSteps)
1935         pos = findWordBreakAfter (pos);
1936     else
1937         ++pos;
1938 
1939     return moveCaretWithTransaction (pos, selecting);
1940 }
1941 
moveCaretUp(bool selecting)1942 bool TextEditor::moveCaretUp (bool selecting)
1943 {
1944     if (! isMultiLine())
1945         return moveCaretToStartOfLine (selecting);
1946 
1947     auto caretPos = getCaretRectangleFloat();
1948     return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - 1.0f), selecting);
1949 }
1950 
moveCaretDown(bool selecting)1951 bool TextEditor::moveCaretDown (bool selecting)
1952 {
1953     if (! isMultiLine())
1954         return moveCaretToEndOfLine (selecting);
1955 
1956     auto caretPos = getCaretRectangleFloat();
1957     return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting);
1958 }
1959 
pageUp(bool selecting)1960 bool TextEditor::pageUp (bool selecting)
1961 {
1962     if (! isMultiLine())
1963         return moveCaretToStartOfLine (selecting);
1964 
1965     auto caretPos = getCaretRectangleFloat();
1966     return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - (float) viewport->getViewHeight()), selecting);
1967 }
1968 
pageDown(bool selecting)1969 bool TextEditor::pageDown (bool selecting)
1970 {
1971     if (! isMultiLine())
1972         return moveCaretToEndOfLine (selecting);
1973 
1974     auto caretPos = getCaretRectangleFloat();
1975     return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + (float) viewport->getViewHeight()), selecting);
1976 }
1977 
scrollByLines(int deltaLines)1978 void TextEditor::scrollByLines (int deltaLines)
1979 {
1980     viewport->getVerticalScrollBar().moveScrollbarInSteps (deltaLines);
1981 }
1982 
scrollDown()1983 bool TextEditor::scrollDown()
1984 {
1985     scrollByLines (-1);
1986     return true;
1987 }
1988 
scrollUp()1989 bool TextEditor::scrollUp()
1990 {
1991     scrollByLines (1);
1992     return true;
1993 }
1994 
moveCaretToTop(bool selecting)1995 bool TextEditor::moveCaretToTop (bool selecting)
1996 {
1997     return moveCaretWithTransaction (0, selecting);
1998 }
1999 
moveCaretToStartOfLine(bool selecting)2000 bool TextEditor::moveCaretToStartOfLine (bool selecting)
2001 {
2002     auto caretPos = getCaretRectangleFloat();
2003     return moveCaretWithTransaction (indexAtPosition (0.0f, caretPos.getY()), selecting);
2004 }
2005 
moveCaretToEnd(bool selecting)2006 bool TextEditor::moveCaretToEnd (bool selecting)
2007 {
2008     return moveCaretWithTransaction (getTotalNumChars(), selecting);
2009 }
2010 
moveCaretToEndOfLine(bool selecting)2011 bool TextEditor::moveCaretToEndOfLine (bool selecting)
2012 {
2013     auto caretPos = getCaretRectangleFloat();
2014     return moveCaretWithTransaction (indexAtPosition ((float) textHolder->getWidth(), caretPos.getY()), selecting);
2015 }
2016 
deleteBackwards(bool moveInWholeWordSteps)2017 bool TextEditor::deleteBackwards (bool moveInWholeWordSteps)
2018 {
2019     if (moveInWholeWordSteps)
2020         moveCaretTo (findWordBreakBefore (getCaretPosition()), true);
2021     else if (selection.isEmpty() && selection.getStart() > 0)
2022         selection = { selection.getEnd() - 1, selection.getEnd() };
2023 
2024     cut();
2025     return true;
2026 }
2027 
deleteForwards(bool)2028 bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/)
2029 {
2030     if (selection.isEmpty() && selection.getStart() < getTotalNumChars())
2031         selection = { selection.getStart(), selection.getStart() + 1 };
2032 
2033     cut();
2034     return true;
2035 }
2036 
copyToClipboard()2037 bool TextEditor::copyToClipboard()
2038 {
2039     newTransaction();
2040     copy();
2041     return true;
2042 }
2043 
cutToClipboard()2044 bool TextEditor::cutToClipboard()
2045 {
2046     newTransaction();
2047     copy();
2048     cut();
2049     return true;
2050 }
2051 
pasteFromClipboard()2052 bool TextEditor::pasteFromClipboard()
2053 {
2054     newTransaction();
2055     paste();
2056     return true;
2057 }
2058 
selectAll()2059 bool TextEditor::selectAll()
2060 {
2061     newTransaction();
2062     moveCaretTo (getTotalNumChars(), false);
2063     moveCaretTo (0, true);
2064     return true;
2065 }
2066 
2067 //==============================================================================
setEscapeAndReturnKeysConsumed(bool shouldBeConsumed)2068 void TextEditor::setEscapeAndReturnKeysConsumed (bool shouldBeConsumed) noexcept
2069 {
2070     consumeEscAndReturnKeys = shouldBeConsumed;
2071 }
2072 
keyPressed(const KeyPress & key)2073 bool TextEditor::keyPressed (const KeyPress& key)
2074 {
2075     if (isReadOnly() && key != KeyPress ('c', ModifierKeys::commandModifier, 0)
2076                      && key != KeyPress ('a', ModifierKeys::commandModifier, 0))
2077         return false;
2078 
2079     if (! TextEditorKeyMapper<TextEditor>::invokeKeyFunction (*this, key))
2080     {
2081         if (key == KeyPress::returnKey)
2082         {
2083             newTransaction();
2084 
2085             if (returnKeyStartsNewLine)
2086             {
2087                 insertTextAtCaret ("\n");
2088             }
2089             else
2090             {
2091                 returnPressed();
2092                 return consumeEscAndReturnKeys;
2093             }
2094         }
2095         else if (key.isKeyCode (KeyPress::escapeKey))
2096         {
2097             newTransaction();
2098             moveCaretTo (getCaretPosition(), false);
2099             escapePressed();
2100             return consumeEscAndReturnKeys;
2101         }
2102         else if (key.getTextCharacter() >= ' '
2103                   || (tabKeyUsed && (key.getTextCharacter() == '\t')))
2104         {
2105             insertTextAtCaret (String::charToString (key.getTextCharacter()));
2106 
2107             lastTransactionTime = Time::getApproximateMillisecondCounter();
2108         }
2109         else
2110         {
2111             return false;
2112         }
2113     }
2114 
2115     return true;
2116 }
2117 
keyStateChanged(const bool isKeyDown)2118 bool TextEditor::keyStateChanged (const bool isKeyDown)
2119 {
2120     if (! isKeyDown)
2121         return false;
2122 
2123    #if JUCE_WINDOWS
2124     if (KeyPress (KeyPress::F4Key, ModifierKeys::altModifier, 0).isCurrentlyDown())
2125         return false;  // We need to explicitly allow alt-F4 to pass through on Windows
2126    #endif
2127 
2128     if ((! consumeEscAndReturnKeys)
2129          && (KeyPress (KeyPress::escapeKey).isCurrentlyDown()
2130           || KeyPress (KeyPress::returnKey).isCurrentlyDown()))
2131         return false;
2132 
2133     // (overridden to avoid forwarding key events to the parent)
2134     return ! ModifierKeys::currentModifiers.isCommandDown();
2135 }
2136 
2137 //==============================================================================
focusGained(FocusChangeType cause)2138 void TextEditor::focusGained (FocusChangeType cause)
2139 {
2140     newTransaction();
2141 
2142     if (selectAllTextWhenFocused)
2143     {
2144         moveCaretTo (0, false);
2145         moveCaretTo (getTotalNumChars(), true);
2146     }
2147 
2148     checkFocus();
2149 
2150     if (cause == FocusChangeType::focusChangedByMouseClick && selectAllTextWhenFocused)
2151         wasFocused = false;
2152 
2153     repaint();
2154     updateCaretPosition();
2155 }
2156 
focusLost(FocusChangeType)2157 void TextEditor::focusLost (FocusChangeType)
2158 {
2159     newTransaction();
2160 
2161     wasFocused = false;
2162     textHolder->stopTimer();
2163 
2164     underlinedSections.clear();
2165 
2166     if (auto* peer = getPeer())
2167         peer->dismissPendingTextInput();
2168 
2169     updateCaretPosition();
2170 
2171     postCommandMessage (TextEditorDefs::focusLossMessageId);
2172     repaint();
2173 }
2174 
2175 //==============================================================================
resized()2176 void TextEditor::resized()
2177 {
2178     viewport->setBoundsInset (borderSize);
2179     viewport->setSingleStepSizes (16, roundToInt (currentFont.getHeight()));
2180 
2181     checkLayout();
2182 
2183     if (isMultiLine())
2184         updateCaretPosition();
2185     else
2186         scrollToMakeSureCursorIsVisible();
2187 }
2188 
handleCommandMessage(const int commandId)2189 void TextEditor::handleCommandMessage (const int commandId)
2190 {
2191     Component::BailOutChecker checker (this);
2192 
2193     switch (commandId)
2194     {
2195     case TextEditorDefs::textChangeMessageId:
2196         listeners.callChecked (checker, [this] (Listener& l) { l.textEditorTextChanged (*this); });
2197 
2198         if (! checker.shouldBailOut() && onTextChange != nullptr)
2199             onTextChange();
2200 
2201         break;
2202 
2203     case TextEditorDefs::returnKeyMessageId:
2204         listeners.callChecked (checker, [this] (Listener& l) { l.textEditorReturnKeyPressed (*this); });
2205 
2206         if (! checker.shouldBailOut() && onReturnKey != nullptr)
2207             onReturnKey();
2208 
2209         break;
2210 
2211     case TextEditorDefs::escapeKeyMessageId:
2212         listeners.callChecked (checker, [this] (Listener& l) { l.textEditorEscapeKeyPressed (*this); });
2213 
2214         if (! checker.shouldBailOut() && onEscapeKey != nullptr)
2215             onEscapeKey();
2216 
2217         break;
2218 
2219     case TextEditorDefs::focusLossMessageId:
2220         updateValueFromText();
2221         listeners.callChecked (checker, [this] (Listener& l) { l.textEditorFocusLost (*this); });
2222 
2223         if (! checker.shouldBailOut() && onFocusLost != nullptr)
2224             onFocusLost();
2225 
2226         break;
2227 
2228     default:
2229         jassertfalse;
2230         break;
2231     }
2232 }
2233 
setTemporaryUnderlining(const Array<Range<int>> & newUnderlinedSections)2234 void TextEditor::setTemporaryUnderlining (const Array<Range<int>>& newUnderlinedSections)
2235 {
2236     underlinedSections = newUnderlinedSections;
2237     repaint();
2238 }
2239 
2240 //==============================================================================
getUndoManager()2241 UndoManager* TextEditor::getUndoManager() noexcept
2242 {
2243     return readOnly ? nullptr : &undoManager;
2244 }
2245 
clearInternal(UndoManager * const um)2246 void TextEditor::clearInternal (UndoManager* const um)
2247 {
2248     remove ({ 0, getTotalNumChars() }, um, caretPosition);
2249 }
2250 
insert(const String & text,int insertIndex,const Font & font,Colour colour,UndoManager * um,int caretPositionToMoveTo)2251 void TextEditor::insert (const String& text, int insertIndex, const Font& font,
2252                          Colour colour, UndoManager* um, int caretPositionToMoveTo)
2253 {
2254     if (text.isNotEmpty())
2255     {
2256         if (um != nullptr)
2257         {
2258             if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
2259                 newTransaction();
2260 
2261             um->perform (new InsertAction (*this, text, insertIndex, font, colour,
2262                                            caretPosition, caretPositionToMoveTo));
2263         }
2264         else
2265         {
2266             repaintText ({ insertIndex, getTotalNumChars() }); // must do this before and after changing the data, in case
2267                                                                // a line gets moved due to word wrap
2268 
2269             int index = 0;
2270             int nextIndex = 0;
2271 
2272             for (int i = 0; i < sections.size(); ++i)
2273             {
2274                 nextIndex = index + sections.getUnchecked (i)->getTotalLength();
2275 
2276                 if (insertIndex == index)
2277                 {
2278                     sections.insert (i, new UniformTextSection (text, font, colour, passwordCharacter));
2279                     break;
2280                 }
2281 
2282                 if (insertIndex > index && insertIndex < nextIndex)
2283                 {
2284                     splitSection (i, insertIndex - index);
2285                     sections.insert (i + 1, new UniformTextSection (text, font, colour, passwordCharacter));
2286                     break;
2287                 }
2288 
2289                 index = nextIndex;
2290             }
2291 
2292             if (nextIndex == insertIndex)
2293                 sections.add (new UniformTextSection (text, font, colour, passwordCharacter));
2294 
2295             coalesceSimilarSections();
2296             totalNumChars = -1;
2297             valueTextNeedsUpdating = true;
2298 
2299             checkLayout();
2300             moveCaretTo (caretPositionToMoveTo, false);
2301 
2302             repaintText ({ insertIndex, getTotalNumChars() });
2303         }
2304     }
2305 }
2306 
reinsert(int insertIndex,const OwnedArray<UniformTextSection> & sectionsToInsert)2307 void TextEditor::reinsert (int insertIndex, const OwnedArray<UniformTextSection>& sectionsToInsert)
2308 {
2309     int index = 0;
2310     int nextIndex = 0;
2311 
2312     for (int i = 0; i < sections.size(); ++i)
2313     {
2314         nextIndex = index + sections.getUnchecked (i)->getTotalLength();
2315 
2316         if (insertIndex == index)
2317         {
2318             for (int j = sectionsToInsert.size(); --j >= 0;)
2319                 sections.insert (i, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
2320 
2321             break;
2322         }
2323 
2324         if (insertIndex > index && insertIndex < nextIndex)
2325         {
2326             splitSection (i, insertIndex - index);
2327 
2328             for (int j = sectionsToInsert.size(); --j >= 0;)
2329                 sections.insert (i + 1, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
2330 
2331             break;
2332         }
2333 
2334         index = nextIndex;
2335     }
2336 
2337     if (nextIndex == insertIndex)
2338         for (auto* s : sectionsToInsert)
2339             sections.add (new UniformTextSection (*s));
2340 
2341     coalesceSimilarSections();
2342     totalNumChars = -1;
2343     valueTextNeedsUpdating = true;
2344 }
2345 
remove(Range<int> range,UndoManager * const um,const int caretPositionToMoveTo)2346 void TextEditor::remove (Range<int> range, UndoManager* const um, const int caretPositionToMoveTo)
2347 {
2348     if (! range.isEmpty())
2349     {
2350         int index = 0;
2351 
2352         for (int i = 0; i < sections.size(); ++i)
2353         {
2354             auto nextIndex = index + sections.getUnchecked(i)->getTotalLength();
2355 
2356             if (range.getStart() > index && range.getStart() < nextIndex)
2357             {
2358                 splitSection (i, range.getStart() - index);
2359                 --i;
2360             }
2361             else if (range.getEnd() > index && range.getEnd() < nextIndex)
2362             {
2363                 splitSection (i, range.getEnd() - index);
2364                 --i;
2365             }
2366             else
2367             {
2368                 index = nextIndex;
2369 
2370                 if (index > range.getEnd())
2371                     break;
2372             }
2373         }
2374 
2375         index = 0;
2376 
2377         if (um != nullptr)
2378         {
2379             Array<UniformTextSection*> removedSections;
2380 
2381             for (auto* section : sections)
2382             {
2383                 if (range.getEnd() <= range.getStart())
2384                     break;
2385 
2386                 auto nextIndex = index + section->getTotalLength();
2387 
2388                 if (range.getStart() <= index && range.getEnd() >= nextIndex)
2389                     removedSections.add (new UniformTextSection (*section));
2390 
2391                 index = nextIndex;
2392             }
2393 
2394             if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
2395                 newTransaction();
2396 
2397             um->perform (new RemoveAction (*this, range, caretPosition,
2398                                            caretPositionToMoveTo, removedSections));
2399         }
2400         else
2401         {
2402             auto remainingRange = range;
2403 
2404             for (int i = 0; i < sections.size(); ++i)
2405             {
2406                 auto* section = sections.getUnchecked (i);
2407                 auto nextIndex = index + section->getTotalLength();
2408 
2409                 if (remainingRange.getStart() <= index && remainingRange.getEnd() >= nextIndex)
2410                 {
2411                     sections.remove (i);
2412                     remainingRange.setEnd (remainingRange.getEnd() - (nextIndex - index));
2413 
2414                     if (remainingRange.isEmpty())
2415                         break;
2416 
2417                     --i;
2418                 }
2419                 else
2420                 {
2421                     index = nextIndex;
2422                 }
2423             }
2424 
2425             coalesceSimilarSections();
2426             totalNumChars = -1;
2427             valueTextNeedsUpdating = true;
2428 
2429             moveCaretTo (caretPositionToMoveTo, false);
2430 
2431             repaintText ({ range.getStart(), getTotalNumChars() });
2432         }
2433     }
2434 }
2435 
2436 //==============================================================================
getText() const2437 String TextEditor::getText() const
2438 {
2439     MemoryOutputStream mo;
2440     mo.preallocate ((size_t) getTotalNumChars());
2441 
2442     for (auto* s : sections)
2443         s->appendAllText (mo);
2444 
2445     return mo.toUTF8();
2446 }
2447 
getTextInRange(const Range<int> & range) const2448 String TextEditor::getTextInRange (const Range<int>& range) const
2449 {
2450     if (range.isEmpty())
2451         return {};
2452 
2453     MemoryOutputStream mo;
2454     mo.preallocate ((size_t) jmin (getTotalNumChars(), range.getLength()));
2455 
2456     int index = 0;
2457 
2458     for (auto* s : sections)
2459     {
2460         auto nextIndex = index + s->getTotalLength();
2461 
2462         if (range.getStart() < nextIndex)
2463         {
2464             if (range.getEnd() <= index)
2465                 break;
2466 
2467             s->appendSubstring (mo, range - index);
2468         }
2469 
2470         index = nextIndex;
2471     }
2472 
2473     return mo.toUTF8();
2474 }
2475 
getHighlightedText() const2476 String TextEditor::getHighlightedText() const
2477 {
2478     return getTextInRange (selection);
2479 }
2480 
getTotalNumChars() const2481 int TextEditor::getTotalNumChars() const
2482 {
2483     if (totalNumChars < 0)
2484     {
2485         totalNumChars = 0;
2486 
2487         for (auto* s : sections)
2488             totalNumChars += s->getTotalLength();
2489     }
2490 
2491     return totalNumChars;
2492 }
2493 
isEmpty() const2494 bool TextEditor::isEmpty() const
2495 {
2496     return getTotalNumChars() == 0;
2497 }
2498 
getCharPosition(int index,Point<float> & anchor,float & lineHeight) const2499 void TextEditor::getCharPosition (int index, Point<float>& anchor, float& lineHeight) const
2500 {
2501     if (getWordWrapWidth() <= 0)
2502     {
2503         anchor = {};
2504         lineHeight = currentFont.getHeight();
2505     }
2506     else
2507     {
2508         Iterator i (*this);
2509 
2510         if (sections.isEmpty())
2511         {
2512             anchor = { i.getJustificationOffsetX (0), 0 };
2513             lineHeight = currentFont.getHeight();
2514         }
2515         else
2516         {
2517             i.getCharPosition (index, anchor, lineHeight);
2518         }
2519     }
2520 }
2521 
indexAtPosition(const float x,const float y)2522 int TextEditor::indexAtPosition (const float x, const float y)
2523 {
2524     if (getWordWrapWidth() > 0)
2525     {
2526         for (Iterator i (*this); i.next();)
2527         {
2528             if (y < i.lineY + i.lineHeight)
2529             {
2530                 if (y < i.lineY)
2531                     return jmax (0, i.indexInText - 1);
2532 
2533                 if (x <= i.atomX || i.atom->isNewLine())
2534                     return i.indexInText;
2535 
2536                 if (x < i.atomRight)
2537                     return i.xToIndex (x);
2538             }
2539         }
2540     }
2541 
2542     return getTotalNumChars();
2543 }
2544 
2545 //==============================================================================
findWordBreakAfter(const int position) const2546 int TextEditor::findWordBreakAfter (const int position) const
2547 {
2548     auto t = getTextInRange ({ position, position + 512 });
2549     auto totalLength = t.length();
2550     int i = 0;
2551 
2552     while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
2553         ++i;
2554 
2555     auto type = TextEditorDefs::getCharacterCategory (t[i]);
2556 
2557     while (i < totalLength && type == TextEditorDefs::getCharacterCategory (t[i]))
2558         ++i;
2559 
2560     while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
2561         ++i;
2562 
2563     return position + i;
2564 }
2565 
findWordBreakBefore(const int position) const2566 int TextEditor::findWordBreakBefore (const int position) const
2567 {
2568     if (position <= 0)
2569         return 0;
2570 
2571     auto startOfBuffer = jmax (0, position - 512);
2572     auto t = getTextInRange ({ startOfBuffer, position });
2573 
2574     int i = position - startOfBuffer;
2575 
2576     while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1]))
2577         --i;
2578 
2579     if (i > 0)
2580     {
2581         auto type = TextEditorDefs::getCharacterCategory (t [i - 1]);
2582 
2583         while (i > 0 && type == TextEditorDefs::getCharacterCategory (t [i - 1]))
2584             --i;
2585     }
2586 
2587     jassert (startOfBuffer + i >= 0);
2588     return startOfBuffer + i;
2589 }
2590 
2591 
2592 //==============================================================================
splitSection(const int sectionIndex,const int charToSplitAt)2593 void TextEditor::splitSection (const int sectionIndex, const int charToSplitAt)
2594 {
2595     jassert (sections[sectionIndex] != nullptr);
2596 
2597     sections.insert (sectionIndex + 1,
2598                      sections.getUnchecked (sectionIndex)->split (charToSplitAt));
2599 }
2600 
coalesceSimilarSections()2601 void TextEditor::coalesceSimilarSections()
2602 {
2603     for (int i = 0; i < sections.size() - 1; ++i)
2604     {
2605         auto* s1 = sections.getUnchecked (i);
2606         auto* s2 = sections.getUnchecked (i + 1);
2607 
2608         if (s1->font == s2->font
2609              && s1->colour == s2->colour)
2610         {
2611             s1->append (*s2);
2612             sections.remove (i + 1);
2613             --i;
2614         }
2615     }
2616 }
2617 
2618 } // namespace juce
2619