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