VST3: fetch midi mappings all at once, use it for note/sound-off
[carla.git] / source / modules / juce_gui_basics / widgets / juce_TextEditor.cpp
blob5c2055be2b6a523618946b2678974b39e32f1f85
1 /*
2 ==============================================================================
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
10 By using JUCE, you agree to the terms of both the JUCE 7 End-User License
11 Agreement and JUCE Privacy Policy.
13 End User License Agreement: www.juce.com/juce-7-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
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.
23 ==============================================================================
26 namespace juce
29 // a word or space that can't be broken down any further
30 struct TextAtom
32 //==============================================================================
33 String atomText;
34 float width;
35 int numChars;
37 //==============================================================================
38 bool isWhitespace() const noexcept { return CharacterFunctions::isWhitespace (atomText[0]); }
39 bool isNewLine() const noexcept { return atomText[0] == '\r' || atomText[0] == '\n'; }
41 String getText (juce_wchar passwordCharacter) const
43 if (passwordCharacter == 0)
44 return atomText;
46 return String::repeatedString (String::charToString (passwordCharacter),
47 atomText.length());
50 String getTrimmedText (const juce_wchar passwordCharacter) const
52 if (passwordCharacter == 0)
53 return atomText.substring (0, numChars);
55 if (isNewLine())
56 return {};
58 return String::repeatedString (String::charToString (passwordCharacter), numChars);
61 JUCE_LEAK_DETECTOR (TextAtom)
64 //==============================================================================
65 // a run of text with a single font and colour
66 class TextEditor::UniformTextSection
68 public:
69 UniformTextSection (const String& text, const Font& f, Colour col, juce_wchar passwordCharToUse)
70 : font (f), colour (col), passwordChar (passwordCharToUse)
72 initialiseAtoms (text);
75 UniformTextSection (const UniformTextSection&) = default;
76 UniformTextSection (UniformTextSection&&) = default;
78 UniformTextSection& operator= (const UniformTextSection&) = delete;
80 void append (UniformTextSection& other)
82 if (! other.atoms.isEmpty())
84 int i = 0;
86 if (! atoms.isEmpty())
88 auto& lastAtom = atoms.getReference (atoms.size() - 1);
90 if (! CharacterFunctions::isWhitespace (lastAtom.atomText.getLastCharacter()))
92 auto& first = other.atoms.getReference(0);
94 if (! CharacterFunctions::isWhitespace (first.atomText[0]))
96 lastAtom.atomText += first.atomText;
97 lastAtom.numChars = (uint16) (lastAtom.numChars + first.numChars);
98 lastAtom.width = font.getStringWidthFloat (lastAtom.getText (passwordChar));
99 ++i;
104 atoms.ensureStorageAllocated (atoms.size() + other.atoms.size() - i);
106 while (i < other.atoms.size())
108 atoms.add (other.atoms.getReference(i));
109 ++i;
114 UniformTextSection* split (int indexToBreakAt)
116 auto* section2 = new UniformTextSection ({}, font, colour, passwordChar);
117 int index = 0;
119 for (int i = 0; i < atoms.size(); ++i)
121 auto& atom = atoms.getReference(i);
122 auto nextIndex = index + atom.numChars;
124 if (index == indexToBreakAt)
126 for (int j = i; j < atoms.size(); ++j)
127 section2->atoms.add (atoms.getUnchecked (j));
129 atoms.removeRange (i, atoms.size());
130 break;
133 if (indexToBreakAt >= index && indexToBreakAt < nextIndex)
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();
140 section2->atoms.add (secondAtom);
142 atom.atomText = atom.atomText.substring (0, indexToBreakAt - index);
143 atom.width = font.getStringWidthFloat (atom.getText (passwordChar));
144 atom.numChars = (uint16) (indexToBreakAt - index);
146 for (int j = i + 1; j < atoms.size(); ++j)
147 section2->atoms.add (atoms.getUnchecked (j));
149 atoms.removeRange (i + 1, atoms.size());
150 break;
153 index = nextIndex;
156 return section2;
159 void appendAllText (MemoryOutputStream& mo) const
161 for (auto& atom : atoms)
162 mo << atom.atomText;
165 void appendSubstring (MemoryOutputStream& mo, Range<int> range) const
167 int index = 0;
169 for (auto& atom : atoms)
171 auto nextIndex = index + atom.numChars;
173 if (range.getStart() < nextIndex)
175 if (range.getEnd() <= index)
176 break;
178 auto r = (range - index).getIntersectionWith ({ 0, (int) atom.numChars });
180 if (! r.isEmpty())
181 mo << atom.atomText.substring (r.getStart(), r.getEnd());
184 index = nextIndex;
188 int getTotalLength() const noexcept
190 int total = 0;
192 for (auto& atom : atoms)
193 total += atom.numChars;
195 return total;
198 void setFont (const Font& newFont, const juce_wchar passwordCharToUse)
200 if (font != newFont || passwordChar != passwordCharToUse)
202 font = newFont;
203 passwordChar = passwordCharToUse;
205 for (auto& atom : atoms)
206 atom.width = newFont.getStringWidthFloat (atom.getText (passwordChar));
210 //==============================================================================
211 Font font;
212 Colour colour;
213 Array<TextAtom> atoms;
214 juce_wchar passwordChar;
216 private:
217 void initialiseAtoms (const String& textToParse)
219 auto text = textToParse.getCharPointer();
221 while (! text.isEmpty())
223 size_t numChars = 0;
224 auto start = text;
226 // create a whitespace atom unless it starts with non-ws
227 if (text.isWhitespace() && *text != '\r' && *text != '\n')
231 ++text;
232 ++numChars;
234 while (text.isWhitespace() && *text != '\r' && *text != '\n');
236 else
238 if (*text == '\r')
240 ++text;
241 ++numChars;
243 if (*text == '\n')
245 ++start;
246 ++text;
249 else if (*text == '\n')
251 ++text;
252 ++numChars;
254 else
256 while (! (text.isEmpty() || text.isWhitespace()))
258 ++text;
259 ++numChars;
264 TextAtom atom;
265 atom.atomText = String (start, numChars);
266 atom.width = (atom.isNewLine() ? 0.0f : font.getStringWidthFloat (atom.getText (passwordChar)));
267 atom.numChars = (uint16) numChars;
268 atoms.add (atom);
272 JUCE_LEAK_DETECTOR (UniformTextSection)
275 //==============================================================================
276 struct TextEditor::Iterator
278 Iterator (const TextEditor& ed)
279 : sections (ed.sections),
280 justification (ed.justification),
281 bottomRight ((float) ed.getMaximumTextWidth(), (float) ed.getMaximumTextHeight()),
282 wordWrapWidth ((float) ed.getWordWrapWidth()),
283 passwordCharacter (ed.passwordCharacter),
284 lineSpacing (ed.lineSpacing),
285 underlineWhitespace (ed.underlineWhitespace)
287 jassert (wordWrapWidth > 0);
289 if (! sections.isEmpty())
291 currentSection = sections.getUnchecked (sectionIndex);
293 if (currentSection != nullptr)
294 beginNewLine();
297 lineHeight = ed.currentFont.getHeight();
300 Iterator (const Iterator&) = default;
301 Iterator& operator= (const Iterator&) = delete;
303 //==============================================================================
304 bool next()
306 if (atom == &longAtom && chunkLongAtom (true))
307 return true;
309 if (sectionIndex >= sections.size())
311 moveToEndOfLastAtom();
312 return false;
315 bool forceNewLine = false;
317 if (atomIndex >= currentSection->atoms.size() - 1)
319 if (atomIndex >= currentSection->atoms.size())
321 if (++sectionIndex >= sections.size())
323 moveToEndOfLastAtom();
324 return false;
327 atomIndex = 0;
328 currentSection = sections.getUnchecked (sectionIndex);
330 else
332 auto& lastAtom = currentSection->atoms.getReference (atomIndex);
334 if (! lastAtom.isWhitespace())
336 // handle the case where the last atom in a section is actually part of the same
337 // word as the first atom of the next section...
338 float right = atomRight + lastAtom.width;
339 float lineHeight2 = lineHeight;
340 float maxDescent2 = maxDescent;
342 for (int section = sectionIndex + 1; section < sections.size(); ++section)
344 auto* s = sections.getUnchecked (section);
346 if (s->atoms.size() == 0)
347 break;
349 auto& nextAtom = s->atoms.getReference (0);
351 if (nextAtom.isWhitespace())
352 break;
354 right += nextAtom.width;
356 lineHeight2 = jmax (lineHeight2, s->font.getHeight());
357 maxDescent2 = jmax (maxDescent2, s->font.getDescent());
359 if (shouldWrap (right))
361 lineHeight = lineHeight2;
362 maxDescent = maxDescent2;
364 forceNewLine = true;
365 break;
368 if (s->atoms.size() > 1)
369 break;
375 bool isInPreviousAtom = false;
377 if (atom != nullptr)
379 atomX = atomRight;
380 indexInText += atom->numChars;
382 if (atom->isNewLine())
383 beginNewLine();
384 else
385 isInPreviousAtom = true;
388 atom = &(currentSection->atoms.getReference (atomIndex));
389 atomRight = atomX + atom->width;
390 ++atomIndex;
392 if (shouldWrap (atomRight) || forceNewLine)
394 if (atom->isWhitespace())
396 // leave whitespace at the end of a line, but truncate it to avoid scrolling
397 atomRight = jmin (atomRight, wordWrapWidth);
399 else if (shouldWrap (atom->width)) // atom too big to fit on a line, so break it up..
401 longAtom = *atom;
402 longAtom.numChars = 0;
403 atom = &longAtom;
404 chunkLongAtom (isInPreviousAtom);
406 else
408 beginNewLine();
409 atomRight = atomX + atom->width;
413 return true;
416 void beginNewLine()
418 lineY += lineHeight * lineSpacing;
419 float lineWidth = 0;
421 auto tempSectionIndex = sectionIndex;
422 auto tempAtomIndex = atomIndex;
423 auto* section = sections.getUnchecked (tempSectionIndex);
425 lineHeight = section->font.getHeight();
426 maxDescent = section->font.getDescent();
428 float nextLineWidth = (atom != nullptr) ? atom->width : 0.0f;
430 while (! shouldWrap (nextLineWidth))
432 lineWidth = nextLineWidth;
434 if (tempSectionIndex >= sections.size())
435 break;
437 bool checkSize = false;
439 if (tempAtomIndex >= section->atoms.size())
441 if (++tempSectionIndex >= sections.size())
442 break;
444 tempAtomIndex = 0;
445 section = sections.getUnchecked (tempSectionIndex);
446 checkSize = true;
449 if (! isPositiveAndBelow (tempAtomIndex, section->atoms.size()))
450 break;
452 auto& nextAtom = section->atoms.getReference (tempAtomIndex);
453 nextLineWidth += nextAtom.width;
455 if (shouldWrap (nextLineWidth) || nextAtom.isNewLine())
456 break;
458 if (checkSize)
460 lineHeight = jmax (lineHeight, section->font.getHeight());
461 maxDescent = jmax (maxDescent, section->font.getDescent());
464 ++tempAtomIndex;
467 atomX = getJustificationOffsetX (lineWidth);
470 float getJustificationOffsetX (float lineWidth) const
472 if (justification.testFlags (Justification::horizontallyCentred)) return jmax (0.0f, (bottomRight.x - lineWidth) * 0.5f);
473 if (justification.testFlags (Justification::right)) return jmax (0.0f, bottomRight.x - lineWidth);
475 return 0;
478 //==============================================================================
479 void draw (Graphics& g, const UniformTextSection*& lastSection, AffineTransform transform) const
481 if (atom == nullptr)
482 return;
484 if (passwordCharacter != 0 || (underlineWhitespace || ! atom->isWhitespace()))
486 if (lastSection != currentSection)
488 lastSection = currentSection;
489 g.setColour (currentSection->colour);
490 g.setFont (currentSection->font);
493 jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty());
495 GlyphArrangement ga;
496 ga.addLineOfText (currentSection->font,
497 atom->getTrimmedText (passwordCharacter),
498 atomX, (float) roundToInt (lineY + lineHeight - maxDescent));
499 ga.draw (g, transform);
503 void drawUnderline (Graphics& g, Range<int> underline, Colour colour, AffineTransform transform) const
505 auto startX = roundToInt (indexToX (underline.getStart()));
506 auto endX = roundToInt (indexToX (underline.getEnd()));
507 auto baselineY = roundToInt (lineY + currentSection->font.getAscent() + 0.5f);
509 Graphics::ScopedSaveState state (g);
510 g.addTransform (transform);
511 g.reduceClipRegion ({ startX, baselineY, endX - startX, 1 });
512 g.fillCheckerBoard ({ (float) endX, (float) baselineY + 1.0f }, 3.0f, 1.0f, colour, Colours::transparentBlack);
515 void drawSelectedText (Graphics& g, Range<int> selected, Colour selectedTextColour, AffineTransform transform) const
517 if (atom == nullptr)
518 return;
520 if (passwordCharacter != 0 || ! atom->isWhitespace())
522 GlyphArrangement ga;
523 ga.addLineOfText (currentSection->font,
524 atom->getTrimmedText (passwordCharacter),
525 atomX, (float) roundToInt (lineY + lineHeight - maxDescent));
527 if (selected.getEnd() < indexInText + atom->numChars)
529 GlyphArrangement ga2 (ga);
530 ga2.removeRangeOfGlyphs (0, selected.getEnd() - indexInText);
531 ga.removeRangeOfGlyphs (selected.getEnd() - indexInText, -1);
533 g.setColour (currentSection->colour);
534 ga2.draw (g, transform);
537 if (selected.getStart() > indexInText)
539 GlyphArrangement ga2 (ga);
540 ga2.removeRangeOfGlyphs (selected.getStart() - indexInText, -1);
541 ga.removeRangeOfGlyphs (0, selected.getStart() - indexInText);
543 g.setColour (currentSection->colour);
544 ga2.draw (g, transform);
547 g.setColour (selectedTextColour);
548 ga.draw (g, transform);
552 //==============================================================================
553 float indexToX (int indexToFind) const
555 if (indexToFind <= indexInText || atom == nullptr)
556 return atomX;
558 if (indexToFind >= indexInText + atom->numChars)
559 return atomRight;
561 GlyphArrangement g;
562 g.addLineOfText (currentSection->font,
563 atom->getText (passwordCharacter),
564 atomX, 0.0f);
566 if (indexToFind - indexInText >= g.getNumGlyphs())
567 return atomRight;
569 return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft());
572 int xToIndex (float xToFind) const
574 if (xToFind <= atomX || atom == nullptr || atom->isNewLine())
575 return indexInText;
577 if (xToFind >= atomRight)
578 return indexInText + atom->numChars;
580 GlyphArrangement g;
581 g.addLineOfText (currentSection->font,
582 atom->getText (passwordCharacter),
583 atomX, 0.0f);
585 auto numGlyphs = g.getNumGlyphs();
587 int j;
588 for (j = 0; j < numGlyphs; ++j)
590 auto& pg = g.getGlyph(j);
592 if ((pg.getLeft() + pg.getRight()) / 2 > xToFind)
593 break;
596 return indexInText + j;
599 //==============================================================================
600 bool getCharPosition (int index, Point<float>& anchor, float& lineHeightFound)
602 while (next())
604 if (indexInText + atom->numChars > index)
606 anchor = { indexToX (index), lineY };
607 lineHeightFound = lineHeight;
608 return true;
612 anchor = { atomX, lineY };
613 lineHeightFound = lineHeight;
614 return false;
617 float getYOffset()
619 if (justification.testFlags (Justification::top) || lineY >= bottomRight.y)
620 return 0;
622 while (next())
624 if (lineY >= bottomRight.y)
625 return 0;
628 auto bottom = jmax (0.0f, bottomRight.y - lineY - lineHeight);
630 if (justification.testFlags (Justification::bottom))
631 return bottom;
633 return bottom * 0.5f;
636 int getTotalTextHeight()
638 while (next()) {}
640 auto height = lineY + lineHeight + getYOffset();
642 if (atom != nullptr && atom->isNewLine())
643 height += lineHeight;
645 return roundToInt (height);
648 int getTextRight()
650 float maxWidth = 0.0f;
652 while (next())
653 maxWidth = jmax (maxWidth, atomRight);
655 return roundToInt (maxWidth);
658 Rectangle<int> getTextBounds (Range<int> range) const
660 auto startX = indexToX (range.getStart());
661 auto endX = indexToX (range.getEnd());
663 return Rectangle<float> (startX, lineY, endX - startX, lineHeight * lineSpacing).getSmallestIntegerContainer();
666 //==============================================================================
667 int indexInText = 0;
668 float lineY = 0, lineHeight = 0, maxDescent = 0;
669 float atomX = 0, atomRight = 0;
670 const TextAtom* atom = nullptr;
672 private:
673 const OwnedArray<UniformTextSection>& sections;
674 const UniformTextSection* currentSection = nullptr;
675 int sectionIndex = 0, atomIndex = 0;
676 Justification justification;
677 const Point<float> bottomRight;
678 const float wordWrapWidth;
679 const juce_wchar passwordCharacter;
680 const float lineSpacing;
681 const bool underlineWhitespace;
682 TextAtom longAtom;
684 bool chunkLongAtom (bool shouldStartNewLine)
686 const auto numRemaining = longAtom.atomText.length() - longAtom.numChars;
688 if (numRemaining <= 0)
689 return false;
691 longAtom.atomText = longAtom.atomText.substring (longAtom.numChars);
692 indexInText += longAtom.numChars;
694 GlyphArrangement g;
695 g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f);
697 int split;
698 for (split = 0; split < g.getNumGlyphs(); ++split)
699 if (shouldWrap (g.getGlyph (split).getRight()))
700 break;
702 const auto numChars = jmax (1, split);
703 longAtom.numChars = (uint16) numChars;
704 longAtom.width = g.getGlyph (numChars - 1).getRight();
706 atomX = getJustificationOffsetX (longAtom.width);
708 if (shouldStartNewLine)
710 if (split == numRemaining)
711 beginNewLine();
712 else
713 lineY += lineHeight * lineSpacing;
716 atomRight = atomX + longAtom.width;
717 return true;
720 void moveToEndOfLastAtom()
722 if (atom != nullptr)
724 atomX = atomRight;
726 if (atom->isNewLine())
728 atomX = getJustificationOffsetX (0);
729 lineY += lineHeight * lineSpacing;
734 bool shouldWrap (const float x) const noexcept
736 return (x - 0.0001f) >= wordWrapWidth;
739 JUCE_LEAK_DETECTOR (Iterator)
743 //==============================================================================
744 struct TextEditor::InsertAction : public UndoableAction
746 InsertAction (TextEditor& ed, const String& newText, int insertPos,
747 const Font& newFont, Colour newColour, int oldCaret, int newCaret)
748 : owner (ed),
749 text (newText),
750 insertIndex (insertPos),
751 oldCaretPos (oldCaret),
752 newCaretPos (newCaret),
753 font (newFont),
754 colour (newColour)
758 bool perform() override
760 owner.insert (text, insertIndex, font, colour, nullptr, newCaretPos);
761 return true;
764 bool undo() override
766 owner.remove ({ insertIndex, insertIndex + text.length() }, nullptr, oldCaretPos);
767 return true;
770 int getSizeInUnits() override
772 return text.length() + 16;
775 private:
776 TextEditor& owner;
777 const String text;
778 const int insertIndex, oldCaretPos, newCaretPos;
779 const Font font;
780 const Colour colour;
782 JUCE_DECLARE_NON_COPYABLE (InsertAction)
785 //==============================================================================
786 struct TextEditor::RemoveAction : public UndoableAction
788 RemoveAction (TextEditor& ed, Range<int> rangeToRemove, int oldCaret, int newCaret,
789 const Array<UniformTextSection*>& oldSections)
790 : owner (ed),
791 range (rangeToRemove),
792 oldCaretPos (oldCaret),
793 newCaretPos (newCaret)
795 removedSections.addArray (oldSections);
798 bool perform() override
800 owner.remove (range, nullptr, newCaretPos);
801 return true;
804 bool undo() override
806 owner.reinsert (range.getStart(), removedSections);
807 owner.moveCaretTo (oldCaretPos, false);
808 return true;
811 int getSizeInUnits() override
813 int n = 16;
815 for (auto* s : removedSections)
816 n += s->getTotalLength();
818 return n;
821 private:
822 TextEditor& owner;
823 const Range<int> range;
824 const int oldCaretPos, newCaretPos;
825 OwnedArray<UniformTextSection> removedSections;
827 JUCE_DECLARE_NON_COPYABLE (RemoveAction)
830 //==============================================================================
831 struct TextEditor::TextHolderComponent : public Component,
832 public Timer,
833 public Value::Listener
835 TextHolderComponent (TextEditor& ed) : owner (ed)
837 setWantsKeyboardFocus (false);
838 setInterceptsMouseClicks (false, true);
839 setMouseCursor (MouseCursor::ParentCursor);
841 owner.getTextValue().addListener (this);
844 ~TextHolderComponent() override
846 owner.getTextValue().removeListener (this);
849 void paint (Graphics& g) override
851 owner.drawContent (g);
854 void restartTimer()
856 startTimer (350);
859 void timerCallback() override
861 owner.timerCallbackInt();
864 void valueChanged (Value&) override
866 owner.textWasChangedByValue();
869 TextEditor& owner;
871 private:
872 std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
874 return createIgnoredAccessibilityHandler (*this);
877 JUCE_DECLARE_NON_COPYABLE (TextHolderComponent)
880 //==============================================================================
881 struct TextEditor::TextEditorViewport : public Viewport
883 TextEditorViewport (TextEditor& ed) : owner (ed) {}
885 void visibleAreaChanged (const Rectangle<int>&) override
887 if (! reentrant) // it's rare, but possible to get into a feedback loop as the viewport's scrollbars
888 // appear and disappear, causing the wrap width to change.
890 auto wordWrapWidth = owner.getWordWrapWidth();
892 if (wordWrapWidth != lastWordWrapWidth)
894 lastWordWrapWidth = wordWrapWidth;
896 ScopedValueSetter<bool> svs (reentrant, true);
897 owner.checkLayout();
902 private:
903 std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
905 return createIgnoredAccessibilityHandler (*this);
908 TextEditor& owner;
909 int lastWordWrapWidth = 0;
910 bool reentrant = false;
912 JUCE_DECLARE_NON_COPYABLE (TextEditorViewport)
915 //==============================================================================
916 namespace TextEditorDefs
918 const int textChangeMessageId = 0x10003001;
919 const int returnKeyMessageId = 0x10003002;
920 const int escapeKeyMessageId = 0x10003003;
921 const int focusLossMessageId = 0x10003004;
923 const int maxActionsPerTransaction = 100;
925 static int getCharacterCategory (juce_wchar character) noexcept
927 return CharacterFunctions::isLetterOrDigit (character)
928 ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1);
932 //==============================================================================
933 TextEditor::TextEditor (const String& name, juce_wchar passwordChar)
934 : Component (name),
935 passwordCharacter (passwordChar)
937 setMouseCursor (MouseCursor::IBeamCursor);
939 viewport.reset (new TextEditorViewport (*this));
940 addAndMakeVisible (viewport.get());
941 viewport->setViewedComponent (textHolder = new TextHolderComponent (*this));
942 viewport->setWantsKeyboardFocus (false);
943 viewport->setScrollBarsShown (false, false);
945 setWantsKeyboardFocus (true);
946 recreateCaret();
948 juce::Desktop::getInstance().addGlobalMouseListener (this);
951 TextEditor::~TextEditor()
953 juce::Desktop::getInstance().removeGlobalMouseListener (this);
955 textValue.removeListener (textHolder);
956 textValue.referTo (Value());
958 viewport.reset();
959 textHolder = nullptr;
962 //==============================================================================
963 void TextEditor::newTransaction()
965 lastTransactionTime = Time::getApproximateMillisecondCounter();
966 undoManager.beginNewTransaction();
969 bool TextEditor::undoOrRedo (const bool shouldUndo)
971 if (! isReadOnly())
973 newTransaction();
975 if (shouldUndo ? undoManager.undo()
976 : undoManager.redo())
978 repaint();
979 textChanged();
980 scrollToMakeSureCursorIsVisible();
982 return true;
986 return false;
989 bool TextEditor::undo() { return undoOrRedo (true); }
990 bool TextEditor::redo() { return undoOrRedo (false); }
992 //==============================================================================
993 void TextEditor::setMultiLine (const bool shouldBeMultiLine,
994 const bool shouldWordWrap)
996 if (multiline != shouldBeMultiLine
997 || wordWrap != (shouldWordWrap && shouldBeMultiLine))
999 multiline = shouldBeMultiLine;
1000 wordWrap = shouldWordWrap && shouldBeMultiLine;
1002 checkLayout();
1004 viewport->setViewPosition (0, 0);
1005 resized();
1006 scrollToMakeSureCursorIsVisible();
1010 bool TextEditor::isMultiLine() const
1012 return multiline;
1015 void TextEditor::setScrollbarsShown (bool shown)
1017 if (scrollbarVisible != shown)
1019 scrollbarVisible = shown;
1020 checkLayout();
1024 void TextEditor::setReadOnly (bool shouldBeReadOnly)
1026 if (readOnly != shouldBeReadOnly)
1028 readOnly = shouldBeReadOnly;
1029 enablementChanged();
1030 invalidateAccessibilityHandler();
1032 if (auto* peer = getPeer())
1033 peer->refreshTextInputTarget();
1037 void TextEditor::setClicksOutsideDismissVirtualKeyboard (bool newValue)
1039 clicksOutsideDismissVirtualKeyboard = newValue;
1042 bool TextEditor::isReadOnly() const noexcept
1044 return readOnly || ! isEnabled();
1047 bool TextEditor::isTextInputActive() const
1049 return ! isReadOnly() && (! clicksOutsideDismissVirtualKeyboard || mouseDownInEditor);
1052 void TextEditor::setReturnKeyStartsNewLine (bool shouldStartNewLine)
1054 returnKeyStartsNewLine = shouldStartNewLine;
1057 void TextEditor::setTabKeyUsedAsCharacter (bool shouldTabKeyBeUsed)
1059 tabKeyUsed = shouldTabKeyBeUsed;
1062 void TextEditor::setPopupMenuEnabled (bool b)
1064 popupMenuEnabled = b;
1067 void TextEditor::setSelectAllWhenFocused (bool b)
1069 selectAllTextWhenFocused = b;
1072 void TextEditor::setJustification (Justification j)
1074 if (justification != j)
1076 justification = j;
1078 resized();
1079 repaint();
1083 //==============================================================================
1084 void TextEditor::setFont (const Font& newFont)
1086 currentFont = newFont;
1087 scrollToMakeSureCursorIsVisible();
1090 void TextEditor::applyFontToAllText (const Font& newFont, bool changeCurrentFont)
1092 if (changeCurrentFont)
1093 currentFont = newFont;
1095 auto overallColour = findColour (textColourId);
1097 for (auto* uts : sections)
1099 uts->setFont (newFont, passwordCharacter);
1100 uts->colour = overallColour;
1103 coalesceSimilarSections();
1104 checkLayout();
1105 scrollToMakeSureCursorIsVisible();
1106 repaint();
1109 void TextEditor::applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour)
1111 for (auto* uts : sections)
1112 uts->colour = newColour;
1114 if (changeCurrentTextColour)
1115 setColour (TextEditor::textColourId, newColour);
1116 else
1117 repaint();
1120 void TextEditor::lookAndFeelChanged()
1122 caret.reset();
1123 recreateCaret();
1124 repaint();
1127 void TextEditor::parentHierarchyChanged()
1129 lookAndFeelChanged();
1132 void TextEditor::enablementChanged()
1134 recreateCaret();
1135 repaint();
1138 void TextEditor::setCaretVisible (bool shouldCaretBeVisible)
1140 if (caretVisible != shouldCaretBeVisible)
1142 caretVisible = shouldCaretBeVisible;
1143 recreateCaret();
1147 void TextEditor::recreateCaret()
1149 if (isCaretVisible())
1151 if (caret == nullptr)
1153 caret.reset (getLookAndFeel().createCaretComponent (this));
1154 textHolder->addChildComponent (caret.get());
1155 updateCaretPosition();
1158 else
1160 caret.reset();
1164 void TextEditor::updateCaretPosition()
1166 if (caret != nullptr
1167 && getWidth() > 0 && getHeight() > 0)
1169 Iterator i (*this);
1170 caret->setCaretPosition (getCaretRectangle().translated (leftIndent,
1171 topIndent + roundToInt (i.getYOffset())));
1173 if (auto* handler = getAccessibilityHandler())
1174 handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged);
1178 TextEditor::LengthAndCharacterRestriction::LengthAndCharacterRestriction (int maxLen, const String& chars)
1179 : allowedCharacters (chars), maxLength (maxLen)
1183 String TextEditor::LengthAndCharacterRestriction::filterNewText (TextEditor& ed, const String& newInput)
1185 String t (newInput);
1187 if (allowedCharacters.isNotEmpty())
1188 t = t.retainCharacters (allowedCharacters);
1190 if (maxLength > 0)
1191 t = t.substring (0, maxLength - (ed.getTotalNumChars() - ed.getHighlightedRegion().getLength()));
1193 return t;
1196 void TextEditor::setInputFilter (InputFilter* newFilter, bool takeOwnership)
1198 inputFilter.set (newFilter, takeOwnership);
1201 void TextEditor::setInputRestrictions (int maxLen, const String& chars)
1203 setInputFilter (new LengthAndCharacterRestriction (maxLen, chars), true);
1206 void TextEditor::setTextToShowWhenEmpty (const String& text, Colour colourToUse)
1208 textToShowWhenEmpty = text;
1209 colourForTextWhenEmpty = colourToUse;
1212 void TextEditor::setPasswordCharacter (juce_wchar newPasswordCharacter)
1214 if (passwordCharacter != newPasswordCharacter)
1216 passwordCharacter = newPasswordCharacter;
1217 applyFontToAllText (currentFont);
1221 void TextEditor::setScrollBarThickness (int newThicknessPixels)
1223 viewport->setScrollBarThickness (newThicknessPixels);
1226 //==============================================================================
1227 void TextEditor::clear()
1229 clearInternal (nullptr);
1230 checkLayout();
1231 undoManager.clearUndoHistory();
1234 void TextEditor::setText (const String& newText, bool sendTextChangeMessage)
1236 auto newLength = newText.length();
1238 if (newLength != getTotalNumChars() || getText() != newText)
1240 if (! sendTextChangeMessage)
1241 textValue.removeListener (textHolder);
1243 textValue = newText;
1245 auto oldCursorPos = caretPosition;
1246 bool cursorWasAtEnd = oldCursorPos >= getTotalNumChars();
1248 clearInternal (nullptr);
1249 insert (newText, 0, currentFont, findColour (textColourId), nullptr, caretPosition);
1251 // if you're adding text with line-feeds to a single-line text editor, it
1252 // ain't gonna look right!
1253 jassert (multiline || ! newText.containsAnyOf ("\r\n"));
1255 if (cursorWasAtEnd && ! isMultiLine())
1256 oldCursorPos = getTotalNumChars();
1258 moveCaretTo (oldCursorPos, false);
1260 if (sendTextChangeMessage)
1261 textChanged();
1262 else
1263 textValue.addListener (textHolder);
1265 checkLayout();
1266 scrollToMakeSureCursorIsVisible();
1267 undoManager.clearUndoHistory();
1269 repaint();
1273 //==============================================================================
1274 void TextEditor::updateValueFromText()
1276 if (valueTextNeedsUpdating)
1278 valueTextNeedsUpdating = false;
1279 textValue = getText();
1283 Value& TextEditor::getTextValue()
1285 updateValueFromText();
1286 return textValue;
1289 void TextEditor::textWasChangedByValue()
1291 if (textValue.getValueSource().getReferenceCount() > 1)
1292 setText (textValue.getValue());
1295 //==============================================================================
1296 void TextEditor::textChanged()
1298 checkLayout();
1300 if (listeners.size() != 0 || onTextChange != nullptr)
1301 postCommandMessage (TextEditorDefs::textChangeMessageId);
1303 if (textValue.getValueSource().getReferenceCount() > 1)
1305 valueTextNeedsUpdating = false;
1306 textValue = getText();
1309 if (auto* handler = getAccessibilityHandler())
1310 handler->notifyAccessibilityEvent (AccessibilityEvent::textChanged);
1313 void TextEditor::setSelection (Range<int> newSelection) noexcept
1315 if (newSelection != selection)
1317 selection = newSelection;
1319 if (auto* handler = getAccessibilityHandler())
1320 handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged);
1324 void TextEditor::returnPressed() { postCommandMessage (TextEditorDefs::returnKeyMessageId); }
1325 void TextEditor::escapePressed() { postCommandMessage (TextEditorDefs::escapeKeyMessageId); }
1327 void TextEditor::addListener (Listener* l) { listeners.add (l); }
1328 void TextEditor::removeListener (Listener* l) { listeners.remove (l); }
1330 //==============================================================================
1331 void TextEditor::timerCallbackInt()
1333 checkFocus();
1335 auto now = Time::getApproximateMillisecondCounter();
1337 if (now > lastTransactionTime + 200)
1338 newTransaction();
1341 void TextEditor::checkFocus()
1343 if (! wasFocused && hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent())
1344 wasFocused = true;
1347 void TextEditor::repaintText (Range<int> range)
1349 if (! range.isEmpty())
1351 if (range.getEnd() >= getTotalNumChars())
1353 textHolder->repaint();
1354 return;
1357 Iterator i (*this);
1359 Point<float> anchor;
1360 auto lh = currentFont.getHeight();
1361 i.getCharPosition (range.getStart(), anchor, lh);
1363 auto y1 = std::trunc (anchor.y);
1364 int y2 = 0;
1366 if (range.getEnd() >= getTotalNumChars())
1368 y2 = textHolder->getHeight();
1370 else
1372 i.getCharPosition (range.getEnd(), anchor, lh);
1373 y2 = (int) (anchor.y + lh * 2.0f);
1376 auto offset = i.getYOffset();
1377 textHolder->repaint (0, roundToInt (y1 + offset), textHolder->getWidth(), roundToInt ((float) y2 - y1 + offset));
1381 //==============================================================================
1382 void TextEditor::moveCaret (int newCaretPos)
1384 if (newCaretPos < 0)
1385 newCaretPos = 0;
1386 else
1387 newCaretPos = jmin (newCaretPos, getTotalNumChars());
1389 if (newCaretPos != getCaretPosition())
1391 caretPosition = newCaretPos;
1393 if (hasKeyboardFocus (false))
1394 textHolder->restartTimer();
1396 scrollToMakeSureCursorIsVisible();
1397 updateCaretPosition();
1399 if (auto* handler = getAccessibilityHandler())
1400 handler->notifyAccessibilityEvent (AccessibilityEvent::textChanged);
1404 int TextEditor::getCaretPosition() const
1406 return caretPosition;
1409 void TextEditor::setCaretPosition (const int newIndex)
1411 moveCaretTo (newIndex, false);
1414 void TextEditor::moveCaretToEnd()
1416 setCaretPosition (std::numeric_limits<int>::max());
1419 void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX,
1420 const int desiredCaretY)
1423 updateCaretPosition();
1424 auto caretRect = getCaretRectangle().translated (leftIndent, topIndent);
1426 auto vx = caretRect.getX() - desiredCaretX;
1427 auto vy = caretRect.getY() - desiredCaretY;
1429 if (desiredCaretX < jmax (1, proportionOfWidth (0.05f)))
1430 vx += desiredCaretX - proportionOfWidth (0.2f);
1431 else if (desiredCaretX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
1432 vx += desiredCaretX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
1434 vx = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vx);
1436 if (! isMultiLine())
1438 vy = viewport->getViewPositionY();
1440 else
1442 vy = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vy);
1444 if (desiredCaretY < 0)
1445 vy = jmax (0, desiredCaretY + vy);
1446 else if (desiredCaretY > jmax (0, viewport->getMaximumVisibleHeight() - caretRect.getHeight()))
1447 vy += desiredCaretY + 2 + caretRect.getHeight() - viewport->getMaximumVisibleHeight();
1450 viewport->setViewPosition (vx, vy);
1453 Rectangle<int> TextEditor::getCaretRectangle()
1455 return getCaretRectangleFloat().getSmallestIntegerContainer();
1458 Rectangle<float> TextEditor::getCaretRectangleFloat() const
1460 Point<float> anchor;
1461 auto cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value)
1462 getCharPosition (caretPosition, anchor, cursorHeight);
1464 return { anchor.x, anchor.y, 2.0f, cursorHeight };
1467 Point<int> TextEditor::getTextOffset() const noexcept
1469 Iterator i (*this);
1470 auto yOffset = i.getYOffset();
1472 return { getLeftIndent() + borderSize.getLeft() - viewport->getViewPositionX(),
1473 roundToInt ((float) getTopIndent() + (float) borderSize.getTop() + yOffset) - viewport->getViewPositionY() };
1476 RectangleList<int> TextEditor::getTextBounds (Range<int> textRange)
1478 RectangleList<int> boundingBox;
1479 Iterator i (*this);
1481 while (i.next())
1483 if (textRange.intersects ({ i.indexInText,
1484 i.indexInText + i.atom->numChars }))
1486 boundingBox.add (i.getTextBounds (textRange));
1490 boundingBox.offsetAll (getTextOffset());
1491 return boundingBox;
1494 //==============================================================================
1495 // Extra space for the cursor at the right-hand-edge
1496 constexpr int rightEdgeSpace = 2;
1498 int TextEditor::getWordWrapWidth() const
1500 return wordWrap ? getMaximumTextWidth()
1501 : std::numeric_limits<int>::max();
1504 int TextEditor::getMaximumTextWidth() const
1506 return jmax (1, viewport->getMaximumVisibleWidth() - leftIndent - rightEdgeSpace);
1509 int TextEditor::getMaximumTextHeight() const
1511 return jmax (1, viewport->getMaximumVisibleHeight() - topIndent);
1514 void TextEditor::checkLayout()
1516 if (getWordWrapWidth() > 0)
1518 const auto textBottom = Iterator (*this).getTotalTextHeight() + topIndent;
1519 const auto textRight = jmax (viewport->getMaximumVisibleWidth(),
1520 Iterator (*this).getTextRight() + leftIndent + rightEdgeSpace);
1522 textHolder->setSize (textRight, textBottom);
1523 viewport->setScrollBarsShown (scrollbarVisible && multiline && textBottom > viewport->getMaximumVisibleHeight(),
1524 scrollbarVisible && multiline && ! wordWrap && textRight > viewport->getMaximumVisibleWidth());
1528 int TextEditor::getTextWidth() const { return textHolder->getWidth(); }
1529 int TextEditor::getTextHeight() const { return textHolder->getHeight(); }
1531 void TextEditor::setIndents (int newLeftIndent, int newTopIndent)
1533 if (leftIndent != newLeftIndent || topIndent != newTopIndent)
1535 leftIndent = newLeftIndent;
1536 topIndent = newTopIndent;
1538 resized();
1539 repaint();
1543 void TextEditor::setBorder (BorderSize<int> border)
1545 borderSize = border;
1546 resized();
1549 BorderSize<int> TextEditor::getBorder() const
1551 return borderSize;
1554 void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor)
1556 keepCaretOnScreen = shouldScrollToShowCursor;
1559 void TextEditor::scrollToMakeSureCursorIsVisible()
1561 updateCaretPosition();
1563 if (keepCaretOnScreen)
1565 auto viewPos = viewport->getViewPosition();
1566 auto caretRect = getCaretRectangle().translated (leftIndent, topIndent);
1567 auto relativeCursor = caretRect.getPosition() - viewPos;
1569 if (relativeCursor.x < jmax (1, proportionOfWidth (0.05f)))
1571 viewPos.x += relativeCursor.x - proportionOfWidth (0.2f);
1573 else if (relativeCursor.x > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
1575 viewPos.x += relativeCursor.x + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
1578 viewPos.x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), viewPos.x);
1580 if (! isMultiLine())
1582 viewPos.y = (getHeight() - textHolder->getHeight() - topIndent) / -2;
1584 else if (relativeCursor.y < 0)
1586 viewPos.y = jmax (0, relativeCursor.y + viewPos.y);
1588 else if (relativeCursor.y > jmax (0, viewport->getMaximumVisibleHeight() - caretRect.getHeight()))
1590 viewPos.y += relativeCursor.y + 2 + caretRect.getHeight() - viewport->getMaximumVisibleHeight();
1593 viewport->setViewPosition (viewPos);
1597 void TextEditor::moveCaretTo (const int newPosition, const bool isSelecting)
1599 if (isSelecting)
1601 moveCaret (newPosition);
1603 auto oldSelection = selection;
1605 if (dragType == notDragging)
1607 if (std::abs (getCaretPosition() - selection.getStart()) < std::abs (getCaretPosition() - selection.getEnd()))
1608 dragType = draggingSelectionStart;
1609 else
1610 dragType = draggingSelectionEnd;
1613 if (dragType == draggingSelectionStart)
1615 if (getCaretPosition() >= selection.getEnd())
1616 dragType = draggingSelectionEnd;
1618 setSelection (Range<int>::between (getCaretPosition(), selection.getEnd()));
1620 else
1622 if (getCaretPosition() < selection.getStart())
1623 dragType = draggingSelectionStart;
1625 setSelection (Range<int>::between (getCaretPosition(), selection.getStart()));
1628 repaintText (selection.getUnionWith (oldSelection));
1630 else
1632 dragType = notDragging;
1634 repaintText (selection);
1636 moveCaret (newPosition);
1637 setSelection (Range<int>::emptyRange (getCaretPosition()));
1641 int TextEditor::getTextIndexAt (const int x, const int y) const
1643 const auto offset = getTextOffset();
1645 return indexAtPosition ((float) (x - offset.x),
1646 (float) (y - offset.y));
1649 void TextEditor::insertTextAtCaret (const String& t)
1651 String newText (inputFilter != nullptr ? inputFilter->filterNewText (*this, t) : t);
1653 if (isMultiLine())
1654 newText = newText.replace ("\r\n", "\n");
1655 else
1656 newText = newText.replaceCharacters ("\r\n", " ");
1658 const int insertIndex = selection.getStart();
1659 const int newCaretPos = insertIndex + newText.length();
1661 remove (selection, getUndoManager(),
1662 newText.isNotEmpty() ? newCaretPos - 1 : newCaretPos);
1664 insert (newText, insertIndex, currentFont, findColour (textColourId),
1665 getUndoManager(), newCaretPos);
1667 textChanged();
1670 void TextEditor::setHighlightedRegion (const Range<int>& newSelection)
1672 moveCaretTo (newSelection.getStart(), false);
1673 moveCaretTo (newSelection.getEnd(), true);
1676 //==============================================================================
1677 void TextEditor::copy()
1679 if (passwordCharacter == 0)
1681 auto selectedText = getHighlightedText();
1683 if (selectedText.isNotEmpty())
1684 SystemClipboard::copyTextToClipboard (selectedText);
1688 void TextEditor::paste()
1690 if (! isReadOnly())
1692 auto clip = SystemClipboard::getTextFromClipboard();
1694 if (clip.isNotEmpty())
1695 insertTextAtCaret (clip);
1699 void TextEditor::cut()
1701 if (! isReadOnly())
1703 moveCaret (selection.getEnd());
1704 insertTextAtCaret (String());
1708 //==============================================================================
1709 void TextEditor::drawContent (Graphics& g)
1711 if (getWordWrapWidth() > 0)
1713 g.setOrigin (leftIndent, topIndent);
1714 auto clip = g.getClipBounds();
1716 auto yOffset = Iterator (*this).getYOffset();
1718 AffineTransform transform;
1720 if (yOffset > 0)
1722 transform = AffineTransform::translation (0.0f, yOffset);
1723 clip.setY (roundToInt ((float) clip.getY() - yOffset));
1726 Iterator i (*this);
1727 Colour selectedTextColour;
1729 if (! selection.isEmpty())
1731 selectedTextColour = findColour (highlightedTextColourId);
1733 g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f));
1735 auto boundingBox = getTextBounds (selection);
1736 boundingBox.offsetAll (-getTextOffset());
1738 g.fillPath (boundingBox.toPath(), transform);
1741 const UniformTextSection* lastSection = nullptr;
1743 while (i.next() && i.lineY < (float) clip.getBottom())
1745 if (i.lineY + i.lineHeight >= (float) clip.getY())
1747 if (selection.intersects ({ i.indexInText, i.indexInText + i.atom->numChars }))
1749 i.drawSelectedText (g, selection, selectedTextColour, transform);
1750 lastSection = nullptr;
1752 else
1754 i.draw (g, lastSection, transform);
1759 for (auto& underlinedSection : underlinedSections)
1761 Iterator i2 (*this);
1763 while (i2.next() && i2.lineY < (float) clip.getBottom())
1765 if (i2.lineY + i2.lineHeight >= (float) clip.getY()
1766 && underlinedSection.intersects ({ i2.indexInText, i2.indexInText + i2.atom->numChars }))
1768 i2.drawUnderline (g, underlinedSection, findColour (textColourId), transform);
1775 void TextEditor::paint (Graphics& g)
1777 getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this);
1780 void TextEditor::paintOverChildren (Graphics& g)
1782 if (textToShowWhenEmpty.isNotEmpty()
1783 && (! hasKeyboardFocus (false))
1784 && getTotalNumChars() == 0)
1786 g.setColour (colourForTextWhenEmpty);
1787 g.setFont (getFont());
1789 Rectangle<int> textBounds (leftIndent,
1790 topIndent,
1791 viewport->getWidth() - leftIndent,
1792 getHeight() - topIndent);
1794 if (! textBounds.isEmpty())
1795 g.drawText (textToShowWhenEmpty, textBounds, justification, true);
1798 getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this);
1801 //==============================================================================
1802 void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*)
1804 const bool writable = ! isReadOnly();
1806 if (passwordCharacter == 0)
1808 m.addItem (StandardApplicationCommandIDs::cut, TRANS("Cut"), writable);
1809 m.addItem (StandardApplicationCommandIDs::copy, TRANS("Copy"), ! selection.isEmpty());
1812 m.addItem (StandardApplicationCommandIDs::paste, TRANS("Paste"), writable);
1813 m.addItem (StandardApplicationCommandIDs::del, TRANS("Delete"), writable);
1814 m.addSeparator();
1815 m.addItem (StandardApplicationCommandIDs::selectAll, TRANS("Select All"));
1816 m.addSeparator();
1818 if (getUndoManager() != nullptr)
1820 m.addItem (StandardApplicationCommandIDs::undo, TRANS("Undo"), undoManager.canUndo());
1821 m.addItem (StandardApplicationCommandIDs::redo, TRANS("Redo"), undoManager.canRedo());
1825 void TextEditor::performPopupMenuAction (const int menuItemID)
1827 switch (menuItemID)
1829 case StandardApplicationCommandIDs::cut: cutToClipboard(); break;
1830 case StandardApplicationCommandIDs::copy: copyToClipboard(); break;
1831 case StandardApplicationCommandIDs::paste: pasteFromClipboard(); break;
1832 case StandardApplicationCommandIDs::del: cut(); break;
1833 case StandardApplicationCommandIDs::selectAll: selectAll(); break;
1834 case StandardApplicationCommandIDs::undo: undo(); break;
1835 case StandardApplicationCommandIDs::redo: redo(); break;
1836 default: break;
1840 //==============================================================================
1841 void TextEditor::mouseDown (const MouseEvent& e)
1843 mouseDownInEditor = e.originalComponent == this;
1845 if (! mouseDownInEditor)
1846 return;
1848 beginDragAutoRepeat (100);
1849 newTransaction();
1851 if (wasFocused || ! selectAllTextWhenFocused)
1853 if (! (popupMenuEnabled && e.mods.isPopupMenu()))
1855 moveCaretTo (getTextIndexAt (e.x, e.y),
1856 e.mods.isShiftDown());
1858 if (auto* peer = getPeer())
1859 peer->closeInputMethodContext();
1861 else
1863 PopupMenu m;
1864 m.setLookAndFeel (&getLookAndFeel());
1865 addPopupMenuItems (m, &e);
1867 menuActive = true;
1869 m.showMenuAsync (PopupMenu::Options(),
1870 [safeThis = SafePointer<TextEditor> { this }] (int menuResult)
1872 if (auto* editor = safeThis.getComponent())
1874 editor->menuActive = false;
1876 if (menuResult != 0)
1877 editor->performPopupMenuAction (menuResult);
1884 void TextEditor::mouseDrag (const MouseEvent& e)
1886 if (! mouseDownInEditor)
1887 return;
1889 if (wasFocused || ! selectAllTextWhenFocused)
1890 if (! (popupMenuEnabled && e.mods.isPopupMenu()))
1891 moveCaretTo (getTextIndexAt (e.x, e.y), true);
1894 void TextEditor::mouseUp (const MouseEvent& e)
1896 if (! mouseDownInEditor)
1897 return;
1899 newTransaction();
1900 textHolder->restartTimer();
1902 if (wasFocused || ! selectAllTextWhenFocused)
1903 if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu()))
1904 moveCaret (getTextIndexAt (e.x, e.y));
1906 wasFocused = true;
1909 void TextEditor::mouseDoubleClick (const MouseEvent& e)
1911 if (! mouseDownInEditor)
1912 return;
1914 int tokenEnd = getTextIndexAt (e.x, e.y);
1915 int tokenStart = 0;
1917 if (e.getNumberOfClicks() > 3)
1919 tokenEnd = getTotalNumChars();
1921 else
1923 auto t = getText();
1924 auto totalLength = getTotalNumChars();
1926 while (tokenEnd < totalLength)
1928 auto c = t[tokenEnd];
1930 // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
1931 if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
1932 ++tokenEnd;
1933 else
1934 break;
1937 tokenStart = tokenEnd;
1939 while (tokenStart > 0)
1941 auto c = t[tokenStart - 1];
1943 // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
1944 if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
1945 --tokenStart;
1946 else
1947 break;
1950 if (e.getNumberOfClicks() > 2)
1952 while (tokenEnd < totalLength)
1954 auto c = t[tokenEnd];
1956 if (c != '\r' && c != '\n')
1957 ++tokenEnd;
1958 else
1959 break;
1962 while (tokenStart > 0)
1964 auto c = t[tokenStart - 1];
1966 if (c != '\r' && c != '\n')
1967 --tokenStart;
1968 else
1969 break;
1974 moveCaretTo (tokenEnd, false);
1975 moveCaretTo (tokenStart, true);
1978 void TextEditor::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
1980 if (! mouseDownInEditor)
1981 return;
1983 if (! viewport->useMouseWheelMoveIfNeeded (e, wheel))
1984 Component::mouseWheelMove (e, wheel);
1987 //==============================================================================
1988 bool TextEditor::moveCaretWithTransaction (const int newPos, const bool selecting)
1990 newTransaction();
1991 moveCaretTo (newPos, selecting);
1993 if (auto* peer = getPeer())
1994 peer->closeInputMethodContext();
1996 return true;
1999 bool TextEditor::moveCaretLeft (bool moveInWholeWordSteps, bool selecting)
2001 auto pos = getCaretPosition();
2003 if (moveInWholeWordSteps)
2004 pos = findWordBreakBefore (pos);
2005 else
2006 --pos;
2008 return moveCaretWithTransaction (pos, selecting);
2011 bool TextEditor::moveCaretRight (bool moveInWholeWordSteps, bool selecting)
2013 auto pos = getCaretPosition();
2015 if (moveInWholeWordSteps)
2016 pos = findWordBreakAfter (pos);
2017 else
2018 ++pos;
2020 return moveCaretWithTransaction (pos, selecting);
2023 bool TextEditor::moveCaretUp (bool selecting)
2025 if (! isMultiLine())
2026 return moveCaretToStartOfLine (selecting);
2028 auto caretPos = getCaretRectangleFloat();
2029 return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - 1.0f), selecting);
2032 bool TextEditor::moveCaretDown (bool selecting)
2034 if (! isMultiLine())
2035 return moveCaretToEndOfLine (selecting);
2037 auto caretPos = getCaretRectangleFloat();
2038 return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting);
2041 bool TextEditor::pageUp (bool selecting)
2043 if (! isMultiLine())
2044 return moveCaretToStartOfLine (selecting);
2046 auto caretPos = getCaretRectangleFloat();
2047 return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - (float) viewport->getViewHeight()), selecting);
2050 bool TextEditor::pageDown (bool selecting)
2052 if (! isMultiLine())
2053 return moveCaretToEndOfLine (selecting);
2055 auto caretPos = getCaretRectangleFloat();
2056 return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + (float) viewport->getViewHeight()), selecting);
2059 void TextEditor::scrollByLines (int deltaLines)
2061 viewport->getVerticalScrollBar().moveScrollbarInSteps (deltaLines);
2064 bool TextEditor::scrollDown()
2066 scrollByLines (-1);
2067 return true;
2070 bool TextEditor::scrollUp()
2072 scrollByLines (1);
2073 return true;
2076 bool TextEditor::moveCaretToTop (bool selecting)
2078 return moveCaretWithTransaction (0, selecting);
2081 bool TextEditor::moveCaretToStartOfLine (bool selecting)
2083 auto caretPos = getCaretRectangleFloat();
2084 return moveCaretWithTransaction (indexAtPosition (0.0f, caretPos.getY()), selecting);
2087 bool TextEditor::moveCaretToEnd (bool selecting)
2089 return moveCaretWithTransaction (getTotalNumChars(), selecting);
2092 bool TextEditor::moveCaretToEndOfLine (bool selecting)
2094 auto caretPos = getCaretRectangleFloat();
2095 return moveCaretWithTransaction (indexAtPosition ((float) textHolder->getWidth(), caretPos.getY()), selecting);
2098 bool TextEditor::deleteBackwards (bool moveInWholeWordSteps)
2100 if (moveInWholeWordSteps)
2101 moveCaretTo (findWordBreakBefore (getCaretPosition()), true);
2102 else if (selection.isEmpty() && selection.getStart() > 0)
2103 setSelection ({ selection.getEnd() - 1, selection.getEnd() });
2105 cut();
2106 return true;
2109 bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/)
2111 if (selection.isEmpty() && selection.getStart() < getTotalNumChars())
2112 setSelection ({ selection.getStart(), selection.getStart() + 1 });
2114 cut();
2115 return true;
2118 bool TextEditor::copyToClipboard()
2120 newTransaction();
2121 copy();
2122 return true;
2125 bool TextEditor::cutToClipboard()
2127 newTransaction();
2128 copy();
2129 cut();
2130 return true;
2133 bool TextEditor::pasteFromClipboard()
2135 newTransaction();
2136 paste();
2137 return true;
2140 bool TextEditor::selectAll()
2142 newTransaction();
2143 moveCaretTo (getTotalNumChars(), false);
2144 moveCaretTo (0, true);
2145 return true;
2148 //==============================================================================
2149 void TextEditor::setEscapeAndReturnKeysConsumed (bool shouldBeConsumed) noexcept
2151 consumeEscAndReturnKeys = shouldBeConsumed;
2154 bool TextEditor::keyPressed (const KeyPress& key)
2156 if (isReadOnly() && key != KeyPress ('c', ModifierKeys::commandModifier, 0)
2157 && key != KeyPress ('a', ModifierKeys::commandModifier, 0))
2158 return false;
2160 if (! TextEditorKeyMapper<TextEditor>::invokeKeyFunction (*this, key))
2162 if (key == KeyPress::returnKey)
2164 newTransaction();
2166 if (returnKeyStartsNewLine)
2168 insertTextAtCaret ("\n");
2170 else
2172 returnPressed();
2173 return consumeEscAndReturnKeys;
2176 else if (key.isKeyCode (KeyPress::escapeKey))
2178 newTransaction();
2179 moveCaretTo (getCaretPosition(), false);
2180 escapePressed();
2181 return consumeEscAndReturnKeys;
2183 else if (key.getTextCharacter() >= ' '
2184 || (tabKeyUsed && (key.getTextCharacter() == '\t')))
2186 insertTextAtCaret (String::charToString (key.getTextCharacter()));
2188 lastTransactionTime = Time::getApproximateMillisecondCounter();
2190 else
2192 return false;
2196 return true;
2199 bool TextEditor::keyStateChanged (const bool isKeyDown)
2201 if (! isKeyDown)
2202 return false;
2204 #if JUCE_WINDOWS
2205 if (KeyPress (KeyPress::F4Key, ModifierKeys::altModifier, 0).isCurrentlyDown())
2206 return false; // We need to explicitly allow alt-F4 to pass through on Windows
2207 #endif
2209 if ((! consumeEscAndReturnKeys)
2210 && (KeyPress (KeyPress::escapeKey).isCurrentlyDown()
2211 || KeyPress (KeyPress::returnKey).isCurrentlyDown()))
2212 return false;
2214 // (overridden to avoid forwarding key events to the parent)
2215 return ! ModifierKeys::currentModifiers.isCommandDown();
2218 //==============================================================================
2219 void TextEditor::focusGained (FocusChangeType cause)
2221 newTransaction();
2223 if (selectAllTextWhenFocused)
2225 moveCaretTo (0, false);
2226 moveCaretTo (getTotalNumChars(), true);
2229 checkFocus();
2231 if (cause == FocusChangeType::focusChangedByMouseClick && selectAllTextWhenFocused)
2232 wasFocused = false;
2234 repaint();
2235 updateCaretPosition();
2238 void TextEditor::focusLost (FocusChangeType)
2240 newTransaction();
2242 wasFocused = false;
2243 textHolder->stopTimer();
2245 underlinedSections.clear();
2247 updateCaretPosition();
2249 postCommandMessage (TextEditorDefs::focusLossMessageId);
2250 repaint();
2253 //==============================================================================
2254 void TextEditor::resized()
2256 viewport->setBoundsInset (borderSize);
2257 viewport->setSingleStepSizes (16, roundToInt (currentFont.getHeight()));
2259 checkLayout();
2261 if (isMultiLine())
2262 updateCaretPosition();
2263 else
2264 scrollToMakeSureCursorIsVisible();
2267 void TextEditor::handleCommandMessage (const int commandId)
2269 Component::BailOutChecker checker (this);
2271 switch (commandId)
2273 case TextEditorDefs::textChangeMessageId:
2274 listeners.callChecked (checker, [this] (Listener& l) { l.textEditorTextChanged (*this); });
2276 if (! checker.shouldBailOut() && onTextChange != nullptr)
2277 onTextChange();
2279 break;
2281 case TextEditorDefs::returnKeyMessageId:
2282 listeners.callChecked (checker, [this] (Listener& l) { l.textEditorReturnKeyPressed (*this); });
2284 if (! checker.shouldBailOut() && onReturnKey != nullptr)
2285 onReturnKey();
2287 break;
2289 case TextEditorDefs::escapeKeyMessageId:
2290 listeners.callChecked (checker, [this] (Listener& l) { l.textEditorEscapeKeyPressed (*this); });
2292 if (! checker.shouldBailOut() && onEscapeKey != nullptr)
2293 onEscapeKey();
2295 break;
2297 case TextEditorDefs::focusLossMessageId:
2298 updateValueFromText();
2299 listeners.callChecked (checker, [this] (Listener& l) { l.textEditorFocusLost (*this); });
2301 if (! checker.shouldBailOut() && onFocusLost != nullptr)
2302 onFocusLost();
2304 break;
2306 default:
2307 jassertfalse;
2308 break;
2312 void TextEditor::setTemporaryUnderlining (const Array<Range<int>>& newUnderlinedSections)
2314 underlinedSections = newUnderlinedSections;
2315 repaint();
2318 //==============================================================================
2319 UndoManager* TextEditor::getUndoManager() noexcept
2321 return readOnly ? nullptr : &undoManager;
2324 void TextEditor::clearInternal (UndoManager* const um)
2326 remove ({ 0, getTotalNumChars() }, um, caretPosition);
2329 void TextEditor::insert (const String& text, int insertIndex, const Font& font,
2330 Colour colour, UndoManager* um, int caretPositionToMoveTo)
2332 if (text.isNotEmpty())
2334 if (um != nullptr)
2336 if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
2337 newTransaction();
2339 um->perform (new InsertAction (*this, text, insertIndex, font, colour,
2340 caretPosition, caretPositionToMoveTo));
2342 else
2344 repaintText ({ insertIndex, getTotalNumChars() }); // must do this before and after changing the data, in case
2345 // a line gets moved due to word wrap
2347 int index = 0;
2348 int nextIndex = 0;
2350 for (int i = 0; i < sections.size(); ++i)
2352 nextIndex = index + sections.getUnchecked (i)->getTotalLength();
2354 if (insertIndex == index)
2356 sections.insert (i, new UniformTextSection (text, font, colour, passwordCharacter));
2357 break;
2360 if (insertIndex > index && insertIndex < nextIndex)
2362 splitSection (i, insertIndex - index);
2363 sections.insert (i + 1, new UniformTextSection (text, font, colour, passwordCharacter));
2364 break;
2367 index = nextIndex;
2370 if (nextIndex == insertIndex)
2371 sections.add (new UniformTextSection (text, font, colour, passwordCharacter));
2373 coalesceSimilarSections();
2374 totalNumChars = -1;
2375 valueTextNeedsUpdating = true;
2377 checkLayout();
2378 moveCaretTo (caretPositionToMoveTo, false);
2380 repaintText ({ insertIndex, getTotalNumChars() });
2385 void TextEditor::reinsert (int insertIndex, const OwnedArray<UniformTextSection>& sectionsToInsert)
2387 int index = 0;
2388 int nextIndex = 0;
2390 for (int i = 0; i < sections.size(); ++i)
2392 nextIndex = index + sections.getUnchecked (i)->getTotalLength();
2394 if (insertIndex == index)
2396 for (int j = sectionsToInsert.size(); --j >= 0;)
2397 sections.insert (i, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
2399 break;
2402 if (insertIndex > index && insertIndex < nextIndex)
2404 splitSection (i, insertIndex - index);
2406 for (int j = sectionsToInsert.size(); --j >= 0;)
2407 sections.insert (i + 1, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
2409 break;
2412 index = nextIndex;
2415 if (nextIndex == insertIndex)
2416 for (auto* s : sectionsToInsert)
2417 sections.add (new UniformTextSection (*s));
2419 coalesceSimilarSections();
2420 totalNumChars = -1;
2421 valueTextNeedsUpdating = true;
2424 void TextEditor::remove (Range<int> range, UndoManager* const um, const int caretPositionToMoveTo)
2426 if (! range.isEmpty())
2428 int index = 0;
2430 for (int i = 0; i < sections.size(); ++i)
2432 auto nextIndex = index + sections.getUnchecked(i)->getTotalLength();
2434 if (range.getStart() > index && range.getStart() < nextIndex)
2436 splitSection (i, range.getStart() - index);
2437 --i;
2439 else if (range.getEnd() > index && range.getEnd() < nextIndex)
2441 splitSection (i, range.getEnd() - index);
2442 --i;
2444 else
2446 index = nextIndex;
2448 if (index > range.getEnd())
2449 break;
2453 index = 0;
2455 if (um != nullptr)
2457 Array<UniformTextSection*> removedSections;
2459 for (auto* section : sections)
2461 if (range.getEnd() <= range.getStart())
2462 break;
2464 auto nextIndex = index + section->getTotalLength();
2466 if (range.getStart() <= index && range.getEnd() >= nextIndex)
2467 removedSections.add (new UniformTextSection (*section));
2469 index = nextIndex;
2472 if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
2473 newTransaction();
2475 um->perform (new RemoveAction (*this, range, caretPosition,
2476 caretPositionToMoveTo, removedSections));
2478 else
2480 auto remainingRange = range;
2482 for (int i = 0; i < sections.size(); ++i)
2484 auto* section = sections.getUnchecked (i);
2485 auto nextIndex = index + section->getTotalLength();
2487 if (remainingRange.getStart() <= index && remainingRange.getEnd() >= nextIndex)
2489 sections.remove (i);
2490 remainingRange.setEnd (remainingRange.getEnd() - (nextIndex - index));
2492 if (remainingRange.isEmpty())
2493 break;
2495 --i;
2497 else
2499 index = nextIndex;
2503 coalesceSimilarSections();
2504 totalNumChars = -1;
2505 valueTextNeedsUpdating = true;
2507 checkLayout();
2508 moveCaretTo (caretPositionToMoveTo, false);
2510 repaintText ({ range.getStart(), getTotalNumChars() });
2515 //==============================================================================
2516 String TextEditor::getText() const
2518 MemoryOutputStream mo;
2519 mo.preallocate ((size_t) getTotalNumChars());
2521 for (auto* s : sections)
2522 s->appendAllText (mo);
2524 return mo.toUTF8();
2527 String TextEditor::getTextInRange (const Range<int>& range) const
2529 if (range.isEmpty())
2530 return {};
2532 MemoryOutputStream mo;
2533 mo.preallocate ((size_t) jmin (getTotalNumChars(), range.getLength()));
2535 int index = 0;
2537 for (auto* s : sections)
2539 auto nextIndex = index + s->getTotalLength();
2541 if (range.getStart() < nextIndex)
2543 if (range.getEnd() <= index)
2544 break;
2546 s->appendSubstring (mo, range - index);
2549 index = nextIndex;
2552 return mo.toUTF8();
2555 String TextEditor::getHighlightedText() const
2557 return getTextInRange (selection);
2560 int TextEditor::getTotalNumChars() const
2562 if (totalNumChars < 0)
2564 totalNumChars = 0;
2566 for (auto* s : sections)
2567 totalNumChars += s->getTotalLength();
2570 return totalNumChars;
2573 bool TextEditor::isEmpty() const
2575 return getTotalNumChars() == 0;
2578 void TextEditor::getCharPosition (int index, Point<float>& anchor, float& lineHeight) const
2580 if (getWordWrapWidth() <= 0)
2582 anchor = {};
2583 lineHeight = currentFont.getHeight();
2585 else
2587 Iterator i (*this);
2589 if (sections.isEmpty())
2591 anchor = { i.getJustificationOffsetX (0), 0 };
2592 lineHeight = currentFont.getHeight();
2594 else
2596 i.getCharPosition (index, anchor, lineHeight);
2601 int TextEditor::indexAtPosition (const float x, const float y) const
2603 if (getWordWrapWidth() > 0)
2605 for (Iterator i (*this); i.next();)
2607 if (y < i.lineY + i.lineHeight)
2609 if (y < i.lineY)
2610 return jmax (0, i.indexInText - 1);
2612 if (x <= i.atomX || i.atom->isNewLine())
2613 return i.indexInText;
2615 if (x < i.atomRight)
2616 return i.xToIndex (x);
2621 return getTotalNumChars();
2624 //==============================================================================
2625 int TextEditor::findWordBreakAfter (const int position) const
2627 auto t = getTextInRange ({ position, position + 512 });
2628 auto totalLength = t.length();
2629 int i = 0;
2631 while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
2632 ++i;
2634 auto type = TextEditorDefs::getCharacterCategory (t[i]);
2636 while (i < totalLength && type == TextEditorDefs::getCharacterCategory (t[i]))
2637 ++i;
2639 while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
2640 ++i;
2642 return position + i;
2645 int TextEditor::findWordBreakBefore (const int position) const
2647 if (position <= 0)
2648 return 0;
2650 auto startOfBuffer = jmax (0, position - 512);
2651 auto t = getTextInRange ({ startOfBuffer, position });
2653 int i = position - startOfBuffer;
2655 while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1]))
2656 --i;
2658 if (i > 0)
2660 auto type = TextEditorDefs::getCharacterCategory (t [i - 1]);
2662 while (i > 0 && type == TextEditorDefs::getCharacterCategory (t [i - 1]))
2663 --i;
2666 jassert (startOfBuffer + i >= 0);
2667 return startOfBuffer + i;
2671 //==============================================================================
2672 void TextEditor::splitSection (const int sectionIndex, const int charToSplitAt)
2674 jassert (sections[sectionIndex] != nullptr);
2676 sections.insert (sectionIndex + 1,
2677 sections.getUnchecked (sectionIndex)->split (charToSplitAt));
2680 void TextEditor::coalesceSimilarSections()
2682 for (int i = 0; i < sections.size() - 1; ++i)
2684 auto* s1 = sections.getUnchecked (i);
2685 auto* s2 = sections.getUnchecked (i + 1);
2687 if (s1->font == s2->font
2688 && s1->colour == s2->colour)
2690 s1->append (*s2);
2691 sections.remove (i + 1);
2692 --i;
2697 //==============================================================================
2698 class TextEditor::EditorAccessibilityHandler : public AccessibilityHandler
2700 public:
2701 explicit EditorAccessibilityHandler (TextEditor& textEditorToWrap)
2702 : AccessibilityHandler (textEditorToWrap,
2703 textEditorToWrap.isReadOnly() ? AccessibilityRole::staticText : AccessibilityRole::editableText,
2705 { std::make_unique<TextEditorTextInterface> (textEditorToWrap) }),
2706 textEditor (textEditorToWrap)
2710 String getHelp() const override { return textEditor.getTooltip(); }
2712 private:
2713 class TextEditorTextInterface : public AccessibilityTextInterface
2715 public:
2716 explicit TextEditorTextInterface (TextEditor& editor)
2717 : textEditor (editor)
2721 bool isDisplayingProtectedText() const override { return textEditor.getPasswordCharacter() != 0; }
2722 bool isReadOnly() const override { return textEditor.isReadOnly(); }
2724 int getTotalNumCharacters() const override { return textEditor.getText().length(); }
2725 Range<int> getSelection() const override { return textEditor.getHighlightedRegion(); }
2727 void setSelection (Range<int> r) override
2729 if (r == textEditor.getHighlightedRegion())
2730 return;
2732 if (r.isEmpty())
2734 textEditor.setCaretPosition (r.getStart());
2736 else
2738 const auto cursorAtStart = r.getEnd() == textEditor.getHighlightedRegion().getStart()
2739 || r.getEnd() == textEditor.getHighlightedRegion().getEnd();
2740 textEditor.moveCaretTo (cursorAtStart ? r.getEnd() : r.getStart(), false);
2741 textEditor.moveCaretTo (cursorAtStart ? r.getStart() : r.getEnd(), true);
2745 String getText (Range<int> r) const override
2747 if (isDisplayingProtectedText())
2748 return String::repeatedString (String::charToString (textEditor.getPasswordCharacter()),
2749 getTotalNumCharacters());
2751 return textEditor.getTextInRange (r);
2754 void setText (const String& newText) override
2756 textEditor.setText (newText);
2759 int getTextInsertionOffset() const override { return textEditor.getCaretPosition(); }
2761 RectangleList<int> getTextBounds (Range<int> textRange) const override
2763 auto localRects = textEditor.getTextBounds (textRange);
2764 RectangleList<int> globalRects;
2766 std::for_each (localRects.begin(), localRects.end(),
2767 [&] (const Rectangle<int>& r) { globalRects.add (textEditor.localAreaToGlobal (r)); });
2769 return globalRects;
2772 int getOffsetAtPoint (Point<int> point) const override
2774 auto localPoint = textEditor.getLocalPoint (nullptr, point);
2775 return textEditor.getTextIndexAt (localPoint.x, localPoint.y);
2778 private:
2779 TextEditor& textEditor;
2781 //==============================================================================
2782 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextEditorTextInterface)
2785 TextEditor& textEditor;
2787 //==============================================================================
2788 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EditorAccessibilityHandler)
2791 std::unique_ptr<AccessibilityHandler> TextEditor::createAccessibilityHandler()
2793 return std::make_unique<EditorAccessibilityHandler> (*this);
2796 } // namespace juce