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
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
23 ==============================================================================
29 // a word or space that can't be broken down any further
32 //==============================================================================
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)
46 return String::repeatedString (String::charToString (passwordCharacter
),
50 String
getTrimmedText (const juce_wchar passwordCharacter
) const
52 if (passwordCharacter
== 0)
53 return atomText
.substring (0, numChars
);
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
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())
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
));
104 atoms
.ensureStorageAllocated (atoms
.size() + other
.atoms
.size() - i
);
106 while (i
< other
.atoms
.size())
108 atoms
.add (other
.atoms
.getReference(i
));
114 UniformTextSection
* split (int indexToBreakAt
)
116 auto* section2
= new UniformTextSection ({}, font
, colour
, passwordChar
);
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());
133 if (indexToBreakAt
>= index
&& indexToBreakAt
< nextIndex
)
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());
159 void appendAllText (MemoryOutputStream
& mo
) const
161 for (auto& atom
: atoms
)
165 void appendSubstring (MemoryOutputStream
& mo
, Range
<int> range
) const
169 for (auto& atom
: atoms
)
171 auto nextIndex
= index
+ atom
.numChars
;
173 if (range
.getStart() < nextIndex
)
175 if (range
.getEnd() <= index
)
178 auto r
= (range
- index
).getIntersectionWith ({ 0, (int) atom
.numChars
});
181 mo
<< atom
.atomText
.substring (r
.getStart(), r
.getEnd());
188 int getTotalLength() const noexcept
192 for (auto& atom
: atoms
)
193 total
+= atom
.numChars
;
198 void setFont (const Font
& newFont
, const juce_wchar passwordCharToUse
)
200 if (font
!= newFont
|| passwordChar
!= passwordCharToUse
)
203 passwordChar
= passwordCharToUse
;
205 for (auto& atom
: atoms
)
206 atom
.width
= newFont
.getStringWidthFloat (atom
.getText (passwordChar
));
210 //==============================================================================
213 Array
<TextAtom
> atoms
;
214 juce_wchar passwordChar
;
217 void initialiseAtoms (const String
& textToParse
)
219 auto text
= textToParse
.getCharPointer();
221 while (! text
.isEmpty())
226 // create a whitespace atom unless it starts with non-ws
227 if (text
.isWhitespace() && *text
!= '\r' && *text
!= '\n')
234 while (text
.isWhitespace() && *text
!= '\r' && *text
!= '\n');
249 else if (*text
== '\n')
256 while (! (text
.isEmpty() || text
.isWhitespace()))
265 atom
.atomText
= String (start
, numChars
);
266 atom
.width
= (atom
.isNewLine() ? 0.0f
: font
.getStringWidthFloat (atom
.getText (passwordChar
)));
267 atom
.numChars
= (uint16
) numChars
;
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)
297 lineHeight
= ed
.currentFont
.getHeight();
300 Iterator (const Iterator
&) = default;
301 Iterator
& operator= (const Iterator
&) = delete;
303 //==============================================================================
306 if (atom
== &longAtom
&& chunkLongAtom (true))
309 if (sectionIndex
>= sections
.size())
311 moveToEndOfLastAtom();
315 bool forceNewLine
= false;
317 if (atomIndex
>= currentSection
->atoms
.size() - 1)
319 if (atomIndex
>= currentSection
->atoms
.size())
321 if (++sectionIndex
>= sections
.size())
323 moveToEndOfLastAtom();
328 currentSection
= sections
.getUnchecked (sectionIndex
);
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)
349 auto& nextAtom
= s
->atoms
.getReference (0);
351 if (nextAtom
.isWhitespace())
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
;
368 if (s
->atoms
.size() > 1)
375 bool isInPreviousAtom
= false;
380 indexInText
+= atom
->numChars
;
382 if (atom
->isNewLine())
385 isInPreviousAtom
= true;
388 atom
= &(currentSection
->atoms
.getReference (atomIndex
));
389 atomRight
= atomX
+ atom
->width
;
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..
402 longAtom
.numChars
= 0;
404 chunkLongAtom (isInPreviousAtom
);
409 atomRight
= atomX
+ atom
->width
;
418 lineY
+= lineHeight
* lineSpacing
;
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())
437 bool checkSize
= false;
439 if (tempAtomIndex
>= section
->atoms
.size())
441 if (++tempSectionIndex
>= sections
.size())
445 section
= sections
.getUnchecked (tempSectionIndex
);
449 if (! isPositiveAndBelow (tempAtomIndex
, section
->atoms
.size()))
452 auto& nextAtom
= section
->atoms
.getReference (tempAtomIndex
);
453 nextLineWidth
+= nextAtom
.width
;
455 if (shouldWrap (nextLineWidth
) || nextAtom
.isNewLine())
460 lineHeight
= jmax (lineHeight
, section
->font
.getHeight());
461 maxDescent
= jmax (maxDescent
, section
->font
.getDescent());
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
);
478 //==============================================================================
479 void draw (Graphics
& g
, const UniformTextSection
*& lastSection
, AffineTransform transform
) const
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());
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
520 if (passwordCharacter
!= 0 || ! atom
->isWhitespace())
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)
558 if (indexToFind
>= indexInText
+ atom
->numChars
)
562 g
.addLineOfText (currentSection
->font
,
563 atom
->getText (passwordCharacter
),
566 if (indexToFind
- indexInText
>= g
.getNumGlyphs())
569 return jmin (atomRight
, g
.getGlyph (indexToFind
- indexInText
).getLeft());
572 int xToIndex (float xToFind
) const
574 if (xToFind
<= atomX
|| atom
== nullptr || atom
->isNewLine())
577 if (xToFind
>= atomRight
)
578 return indexInText
+ atom
->numChars
;
581 g
.addLineOfText (currentSection
->font
,
582 atom
->getText (passwordCharacter
),
585 auto numGlyphs
= g
.getNumGlyphs();
588 for (j
= 0; j
< numGlyphs
; ++j
)
590 auto& pg
= g
.getGlyph(j
);
592 if ((pg
.getLeft() + pg
.getRight()) / 2 > xToFind
)
596 return indexInText
+ j
;
599 //==============================================================================
600 bool getCharPosition (int index
, Point
<float>& anchor
, float& lineHeightFound
)
604 if (indexInText
+ atom
->numChars
> index
)
606 anchor
= { indexToX (index
), lineY
};
607 lineHeightFound
= lineHeight
;
612 anchor
= { atomX
, lineY
};
613 lineHeightFound
= lineHeight
;
619 if (justification
.testFlags (Justification::top
) || lineY
>= bottomRight
.y
)
624 if (lineY
>= bottomRight
.y
)
628 auto bottom
= jmax (0.0f
, bottomRight
.y
- lineY
- lineHeight
);
630 if (justification
.testFlags (Justification::bottom
))
633 return bottom
* 0.5f
;
636 int getTotalTextHeight()
640 auto height
= lineY
+ lineHeight
+ getYOffset();
642 if (atom
!= nullptr && atom
->isNewLine())
643 height
+= lineHeight
;
645 return roundToInt (height
);
650 float maxWidth
= 0.0f
;
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 //==============================================================================
668 float lineY
= 0, lineHeight
= 0, maxDescent
= 0;
669 float atomX
= 0, atomRight
= 0;
670 const TextAtom
* atom
= nullptr;
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
;
684 bool chunkLongAtom (bool shouldStartNewLine
)
686 const auto numRemaining
= longAtom
.atomText
.length() - longAtom
.numChars
;
688 if (numRemaining
<= 0)
691 longAtom
.atomText
= longAtom
.atomText
.substring (longAtom
.numChars
);
692 indexInText
+= longAtom
.numChars
;
695 g
.addLineOfText (currentSection
->font
, atom
->getText (passwordCharacter
), 0.0f
, 0.0f
);
698 for (split
= 0; split
< g
.getNumGlyphs(); ++split
)
699 if (shouldWrap (g
.getGlyph (split
).getRight()))
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
)
713 lineY
+= lineHeight
* lineSpacing
;
716 atomRight
= atomX
+ longAtom
.width
;
720 void moveToEndOfLastAtom()
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
)
750 insertIndex (insertPos
),
751 oldCaretPos (oldCaret
),
752 newCaretPos (newCaret
),
758 bool perform() override
760 owner
.insert (text
, insertIndex
, font
, colour
, nullptr, newCaretPos
);
766 owner
.remove ({ insertIndex
, insertIndex
+ text
.length() }, nullptr, oldCaretPos
);
770 int getSizeInUnits() override
772 return text
.length() + 16;
778 const int insertIndex
, oldCaretPos
, newCaretPos
;
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
)
791 range (rangeToRemove
),
792 oldCaretPos (oldCaret
),
793 newCaretPos (newCaret
)
795 removedSections
.addArray (oldSections
);
798 bool perform() override
800 owner
.remove (range
, nullptr, newCaretPos
);
806 owner
.reinsert (range
.getStart(), removedSections
);
807 owner
.moveCaretTo (oldCaretPos
, false);
811 int getSizeInUnits() override
815 for (auto* s
: removedSections
)
816 n
+= s
->getTotalLength();
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
,
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
);
859 void timerCallback() override
861 owner
.timerCallbackInt();
864 void valueChanged (Value
&) override
866 owner
.textWasChangedByValue();
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);
903 std::unique_ptr
<AccessibilityHandler
> createAccessibilityHandler() override
905 return createIgnoredAccessibilityHandler (*this);
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
)
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);
948 juce::Desktop::getInstance().addGlobalMouseListener (this);
951 TextEditor::~TextEditor()
953 juce::Desktop::getInstance().removeGlobalMouseListener (this);
955 textValue
.removeListener (textHolder
);
956 textValue
.referTo (Value());
959 textHolder
= nullptr;
962 //==============================================================================
963 void TextEditor::newTransaction()
965 lastTransactionTime
= Time::getApproximateMillisecondCounter();
966 undoManager
.beginNewTransaction();
969 bool TextEditor::undoOrRedo (const bool shouldUndo
)
975 if (shouldUndo
? undoManager
.undo()
976 : undoManager
.redo())
980 scrollToMakeSureCursorIsVisible();
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
;
1004 viewport
->setViewPosition (0, 0);
1006 scrollToMakeSureCursorIsVisible();
1010 bool TextEditor::isMultiLine() const
1015 void TextEditor::setScrollbarsShown (bool shown
)
1017 if (scrollbarVisible
!= shown
)
1019 scrollbarVisible
= shown
;
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
)
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();
1105 scrollToMakeSureCursorIsVisible();
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
);
1120 void TextEditor::lookAndFeelChanged()
1127 void TextEditor::parentHierarchyChanged()
1129 lookAndFeelChanged();
1132 void TextEditor::enablementChanged()
1138 void TextEditor::setCaretVisible (bool shouldCaretBeVisible
)
1140 if (caretVisible
!= shouldCaretBeVisible
)
1142 caretVisible
= shouldCaretBeVisible
;
1147 void TextEditor::recreateCaret()
1149 if (isCaretVisible())
1151 if (caret
== nullptr)
1153 caret
.reset (getLookAndFeel().createCaretComponent (this));
1154 textHolder
->addChildComponent (caret
.get());
1155 updateCaretPosition();
1164 void TextEditor::updateCaretPosition()
1166 if (caret
!= nullptr
1167 && getWidth() > 0 && getHeight() > 0)
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
);
1191 t
= t
.substring (0, maxLength
- (ed
.getTotalNumChars() - ed
.getHighlightedRegion().getLength()));
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);
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
)
1263 textValue
.addListener (textHolder
);
1266 scrollToMakeSureCursorIsVisible();
1267 undoManager
.clearUndoHistory();
1273 //==============================================================================
1274 void TextEditor::updateValueFromText()
1276 if (valueTextNeedsUpdating
)
1278 valueTextNeedsUpdating
= false;
1279 textValue
= getText();
1283 Value
& TextEditor::getTextValue()
1285 updateValueFromText();
1289 void TextEditor::textWasChangedByValue()
1291 if (textValue
.getValueSource().getReferenceCount() > 1)
1292 setText (textValue
.getValue());
1295 //==============================================================================
1296 void TextEditor::textChanged()
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()
1335 auto now
= Time::getApproximateMillisecondCounter();
1337 if (now
> lastTransactionTime
+ 200)
1341 void TextEditor::checkFocus()
1343 if (! wasFocused
&& hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent())
1347 void TextEditor::repaintText (Range
<int> range
)
1349 if (! range
.isEmpty())
1351 if (range
.getEnd() >= getTotalNumChars())
1353 textHolder
->repaint();
1359 Point
<float> anchor
;
1360 auto lh
= currentFont
.getHeight();
1361 i
.getCharPosition (range
.getStart(), anchor
, lh
);
1363 auto y1
= std::trunc (anchor
.y
);
1366 if (range
.getEnd() >= getTotalNumChars())
1368 y2
= textHolder
->getHeight();
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)
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();
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
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
;
1483 if (textRange
.intersects ({ i
.indexInText
,
1484 i
.indexInText
+ i
.atom
->numChars
}))
1486 boundingBox
.add (i
.getTextBounds (textRange
));
1490 boundingBox
.offsetAll (getTextOffset());
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
;
1543 void TextEditor::setBorder (BorderSize
<int> border
)
1545 borderSize
= border
;
1549 BorderSize
<int> TextEditor::getBorder() const
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
)
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
;
1610 dragType
= draggingSelectionEnd
;
1613 if (dragType
== draggingSelectionStart
)
1615 if (getCaretPosition() >= selection
.getEnd())
1616 dragType
= draggingSelectionEnd
;
1618 setSelection (Range
<int>::between (getCaretPosition(), selection
.getEnd()));
1622 if (getCaretPosition() < selection
.getStart())
1623 dragType
= draggingSelectionStart
;
1625 setSelection (Range
<int>::between (getCaretPosition(), selection
.getStart()));
1628 repaintText (selection
.getUnionWith (oldSelection
));
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
);
1654 newText
= newText
.replace ("\r\n", "\n");
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
);
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()
1692 auto clip
= SystemClipboard::getTextFromClipboard();
1694 if (clip
.isNotEmpty())
1695 insertTextAtCaret (clip
);
1699 void TextEditor::cut()
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
;
1722 transform
= AffineTransform::translation (0.0f
, yOffset
);
1723 clip
.setY (roundToInt ((float) clip
.getY() - yOffset
));
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;
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
,
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
);
1815 m
.addItem (StandardApplicationCommandIDs::selectAll
, TRANS("Select All"));
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
)
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;
1840 //==============================================================================
1841 void TextEditor::mouseDown (const MouseEvent
& e
)
1843 mouseDownInEditor
= e
.originalComponent
== this;
1845 if (! mouseDownInEditor
)
1848 beginDragAutoRepeat (100);
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();
1864 m
.setLookAndFeel (&getLookAndFeel());
1865 addPopupMenuItems (m
, &e
);
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
)
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
)
1900 textHolder
->restartTimer();
1902 if (wasFocused
|| ! selectAllTextWhenFocused
)
1903 if (e
.mouseWasClicked() && ! (popupMenuEnabled
&& e
.mods
.isPopupMenu()))
1904 moveCaret (getTextIndexAt (e
.x
, e
.y
));
1909 void TextEditor::mouseDoubleClick (const MouseEvent
& e
)
1911 if (! mouseDownInEditor
)
1914 int tokenEnd
= getTextIndexAt (e
.x
, e
.y
);
1917 if (e
.getNumberOfClicks() > 3)
1919 tokenEnd
= getTotalNumChars();
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)
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)
1950 if (e
.getNumberOfClicks() > 2)
1952 while (tokenEnd
< totalLength
)
1954 auto c
= t
[tokenEnd
];
1956 if (c
!= '\r' && c
!= '\n')
1962 while (tokenStart
> 0)
1964 auto c
= t
[tokenStart
- 1];
1966 if (c
!= '\r' && c
!= '\n')
1974 moveCaretTo (tokenEnd
, false);
1975 moveCaretTo (tokenStart
, true);
1978 void TextEditor::mouseWheelMove (const MouseEvent
& e
, const MouseWheelDetails
& wheel
)
1980 if (! mouseDownInEditor
)
1983 if (! viewport
->useMouseWheelMoveIfNeeded (e
, wheel
))
1984 Component::mouseWheelMove (e
, wheel
);
1987 //==============================================================================
1988 bool TextEditor::moveCaretWithTransaction (const int newPos
, const bool selecting
)
1991 moveCaretTo (newPos
, selecting
);
1993 if (auto* peer
= getPeer())
1994 peer
->closeInputMethodContext();
1999 bool TextEditor::moveCaretLeft (bool moveInWholeWordSteps
, bool selecting
)
2001 auto pos
= getCaretPosition();
2003 if (moveInWholeWordSteps
)
2004 pos
= findWordBreakBefore (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
);
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()
2070 bool TextEditor::scrollUp()
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() });
2109 bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/)
2111 if (selection
.isEmpty() && selection
.getStart() < getTotalNumChars())
2112 setSelection ({ selection
.getStart(), selection
.getStart() + 1 });
2118 bool TextEditor::copyToClipboard()
2125 bool TextEditor::cutToClipboard()
2133 bool TextEditor::pasteFromClipboard()
2140 bool TextEditor::selectAll()
2143 moveCaretTo (getTotalNumChars(), false);
2144 moveCaretTo (0, 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))
2160 if (! TextEditorKeyMapper
<TextEditor
>::invokeKeyFunction (*this, key
))
2162 if (key
== KeyPress::returnKey
)
2166 if (returnKeyStartsNewLine
)
2168 insertTextAtCaret ("\n");
2173 return consumeEscAndReturnKeys
;
2176 else if (key
.isKeyCode (KeyPress::escapeKey
))
2179 moveCaretTo (getCaretPosition(), false);
2181 return consumeEscAndReturnKeys
;
2183 else if (key
.getTextCharacter() >= ' '
2184 || (tabKeyUsed
&& (key
.getTextCharacter() == '\t')))
2186 insertTextAtCaret (String::charToString (key
.getTextCharacter()));
2188 lastTransactionTime
= Time::getApproximateMillisecondCounter();
2199 bool TextEditor::keyStateChanged (const bool isKeyDown
)
2205 if (KeyPress (KeyPress::F4Key
, ModifierKeys::altModifier
, 0).isCurrentlyDown())
2206 return false; // We need to explicitly allow alt-F4 to pass through on Windows
2209 if ((! consumeEscAndReturnKeys
)
2210 && (KeyPress (KeyPress::escapeKey
).isCurrentlyDown()
2211 || KeyPress (KeyPress::returnKey
).isCurrentlyDown()))
2214 // (overridden to avoid forwarding key events to the parent)
2215 return ! ModifierKeys::currentModifiers
.isCommandDown();
2218 //==============================================================================
2219 void TextEditor::focusGained (FocusChangeType cause
)
2223 if (selectAllTextWhenFocused
)
2225 moveCaretTo (0, false);
2226 moveCaretTo (getTotalNumChars(), true);
2231 if (cause
== FocusChangeType::focusChangedByMouseClick
&& selectAllTextWhenFocused
)
2235 updateCaretPosition();
2238 void TextEditor::focusLost (FocusChangeType
)
2243 textHolder
->stopTimer();
2245 underlinedSections
.clear();
2247 updateCaretPosition();
2249 postCommandMessage (TextEditorDefs::focusLossMessageId
);
2253 //==============================================================================
2254 void TextEditor::resized()
2256 viewport
->setBoundsInset (borderSize
);
2257 viewport
->setSingleStepSizes (16, roundToInt (currentFont
.getHeight()));
2262 updateCaretPosition();
2264 scrollToMakeSureCursorIsVisible();
2267 void TextEditor::handleCommandMessage (const int commandId
)
2269 Component::BailOutChecker
checker (this);
2273 case TextEditorDefs::textChangeMessageId
:
2274 listeners
.callChecked (checker
, [this] (Listener
& l
) { l
.textEditorTextChanged (*this); });
2276 if (! checker
.shouldBailOut() && onTextChange
!= nullptr)
2281 case TextEditorDefs::returnKeyMessageId
:
2282 listeners
.callChecked (checker
, [this] (Listener
& l
) { l
.textEditorReturnKeyPressed (*this); });
2284 if (! checker
.shouldBailOut() && onReturnKey
!= nullptr)
2289 case TextEditorDefs::escapeKeyMessageId
:
2290 listeners
.callChecked (checker
, [this] (Listener
& l
) { l
.textEditorEscapeKeyPressed (*this); });
2292 if (! checker
.shouldBailOut() && onEscapeKey
!= nullptr)
2297 case TextEditorDefs::focusLossMessageId
:
2298 updateValueFromText();
2299 listeners
.callChecked (checker
, [this] (Listener
& l
) { l
.textEditorFocusLost (*this); });
2301 if (! checker
.shouldBailOut() && onFocusLost
!= nullptr)
2312 void TextEditor::setTemporaryUnderlining (const Array
<Range
<int>>& newUnderlinedSections
)
2314 underlinedSections
= newUnderlinedSections
;
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())
2336 if (um
->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction
)
2339 um
->perform (new InsertAction (*this, text
, insertIndex
, font
, colour
,
2340 caretPosition
, caretPositionToMoveTo
));
2344 repaintText ({ insertIndex
, getTotalNumChars() }); // must do this before and after changing the data, in case
2345 // a line gets moved due to word wrap
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
));
2360 if (insertIndex
> index
&& insertIndex
< nextIndex
)
2362 splitSection (i
, insertIndex
- index
);
2363 sections
.insert (i
+ 1, new UniformTextSection (text
, font
, colour
, passwordCharacter
));
2370 if (nextIndex
== insertIndex
)
2371 sections
.add (new UniformTextSection (text
, font
, colour
, passwordCharacter
));
2373 coalesceSimilarSections();
2375 valueTextNeedsUpdating
= true;
2378 moveCaretTo (caretPositionToMoveTo
, false);
2380 repaintText ({ insertIndex
, getTotalNumChars() });
2385 void TextEditor::reinsert (int insertIndex
, const OwnedArray
<UniformTextSection
>& sectionsToInsert
)
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
)));
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
)));
2415 if (nextIndex
== insertIndex
)
2416 for (auto* s
: sectionsToInsert
)
2417 sections
.add (new UniformTextSection (*s
));
2419 coalesceSimilarSections();
2421 valueTextNeedsUpdating
= true;
2424 void TextEditor::remove (Range
<int> range
, UndoManager
* const um
, const int caretPositionToMoveTo
)
2426 if (! range
.isEmpty())
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
);
2439 else if (range
.getEnd() > index
&& range
.getEnd() < nextIndex
)
2441 splitSection (i
, range
.getEnd() - index
);
2448 if (index
> range
.getEnd())
2457 Array
<UniformTextSection
*> removedSections
;
2459 for (auto* section
: sections
)
2461 if (range
.getEnd() <= range
.getStart())
2464 auto nextIndex
= index
+ section
->getTotalLength();
2466 if (range
.getStart() <= index
&& range
.getEnd() >= nextIndex
)
2467 removedSections
.add (new UniformTextSection (*section
));
2472 if (um
->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction
)
2475 um
->perform (new RemoveAction (*this, range
, caretPosition
,
2476 caretPositionToMoveTo
, removedSections
));
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())
2503 coalesceSimilarSections();
2505 valueTextNeedsUpdating
= true;
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
);
2527 String
TextEditor::getTextInRange (const Range
<int>& range
) const
2529 if (range
.isEmpty())
2532 MemoryOutputStream mo
;
2533 mo
.preallocate ((size_t) jmin (getTotalNumChars(), range
.getLength()));
2537 for (auto* s
: sections
)
2539 auto nextIndex
= index
+ s
->getTotalLength();
2541 if (range
.getStart() < nextIndex
)
2543 if (range
.getEnd() <= index
)
2546 s
->appendSubstring (mo
, range
- index
);
2555 String
TextEditor::getHighlightedText() const
2557 return getTextInRange (selection
);
2560 int TextEditor::getTotalNumChars() const
2562 if (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)
2583 lineHeight
= currentFont
.getHeight();
2589 if (sections
.isEmpty())
2591 anchor
= { i
.getJustificationOffsetX (0), 0 };
2592 lineHeight
= currentFont
.getHeight();
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
)
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();
2631 while (i
< totalLength
&& CharacterFunctions::isWhitespace (t
[i
]))
2634 auto type
= TextEditorDefs::getCharacterCategory (t
[i
]);
2636 while (i
< totalLength
&& type
== TextEditorDefs::getCharacterCategory (t
[i
]))
2639 while (i
< totalLength
&& CharacterFunctions::isWhitespace (t
[i
]))
2642 return position
+ i
;
2645 int TextEditor::findWordBreakBefore (const int position
) const
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]))
2660 auto type
= TextEditorDefs::getCharacterCategory (t
[i
- 1]);
2662 while (i
> 0 && type
== TextEditorDefs::getCharacterCategory (t
[i
- 1]))
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
)
2691 sections
.remove (i
+ 1);
2697 //==============================================================================
2698 class TextEditor::EditorAccessibilityHandler
: public AccessibilityHandler
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(); }
2713 class TextEditorTextInterface
: public AccessibilityTextInterface
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())
2734 textEditor
.setCaretPosition (r
.getStart());
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
)); });
2772 int getOffsetAtPoint (Point
<int> point
) const override
2774 auto localPoint
= textEditor
.getLocalPoint (nullptr, point
);
2775 return textEditor
.getTextIndexAt (localPoint
.x
, localPoint
.y
);
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);