1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00
JUCE/modules/juce_gui_basics/widgets/juce_TextEditor.cpp

2388 lines
74 KiB
C++

/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
struct TextEditor::InsertAction final : public UndoableAction
{
InsertAction (TextEditor& ed, const String& newText, int insertPos,
const Font& newFont, Colour newColour, int oldCaret, int newCaret)
: owner (ed),
text (newText),
insertIndex (insertPos),
oldCaretPos (oldCaret),
newCaretPos (newCaret),
font (newFont),
colour (newColour)
{
}
bool perform() override
{
owner.insert (text, insertIndex, font, colour, nullptr, newCaretPos);
return true;
}
bool undo() override
{
owner.remove ({ insertIndex, insertIndex + text.length() }, nullptr, oldCaretPos);
return true;
}
int getSizeInUnits() override
{
return text.length() + 16;
}
private:
TextEditor& owner;
const String text;
const int insertIndex, oldCaretPos, newCaretPos;
const Font font;
const Colour colour;
JUCE_DECLARE_NON_COPYABLE (InsertAction)
};
//==============================================================================
struct TextEditor::RemoveAction final : public UndoableAction
{
RemoveAction (TextEditor& ed, Range<int> rangeToRemove, int oldCaret, int newCaret)
: owner (ed),
range (rangeToRemove),
oldCaretPos (oldCaret),
newCaretPos (newCaret)
{
}
bool perform() override
{
owner.remove (range, nullptr, newCaretPos, &removedText);
return true;
}
bool undo() override
{
owner.reinsert (removedText);
owner.moveCaretTo (oldCaretPos, false);
return true;
}
int getSizeInUnits() override
{
return std::accumulate (removedText.texts.begin(),
removedText.texts.end(),
0,
[] (auto& sum, auto& value)
{
return sum + (int) value.getNumBytesAsUTF8();
});
}
private:
TextEditor& owner;
const Range<int> range;
const int oldCaretPos, newCaretPos;
TextEditorStorageChunks removedText;
JUCE_DECLARE_NON_COPYABLE (RemoveAction)
};
//==============================================================================
struct TextEditor::TextHolderComponent final : public Component,
public Timer,
public Value::Listener
{
TextHolderComponent (TextEditor& ed) : owner (ed)
{
setWantsKeyboardFocus (false);
setInterceptsMouseClicks (false, true);
setMouseCursor (MouseCursor::ParentCursor);
owner.getTextValue().addListener (this);
}
~TextHolderComponent() override
{
owner.getTextValue().removeListener (this);
}
void paint (Graphics& g) override
{
owner.drawContent (g);
}
void restartTimer()
{
startTimer (350);
}
void timerCallback() override
{
owner.timerCallbackInt();
}
void valueChanged (Value&) override
{
owner.textWasChangedByValue();
}
TextEditor& owner;
private:
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
{
return createIgnoredAccessibilityHandler (*this);
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextHolderComponent)
};
//==============================================================================
struct TextEditor::TextEditorViewport final : public Viewport
{
TextEditorViewport (TextEditor& ed) : owner (ed) {}
void visibleAreaChanged (const Rectangle<int>&) override
{
if (! reentrant) // it's rare, but possible to get into a feedback loop as the viewport's scrollbars
// appear and disappear, causing the wrap width to change.
{
auto wordWrapWidth = owner.getWordWrapWidth();
owner.updateBaseShapedTextOptions();
if (wordWrapWidth != lastWordWrapWidth)
{
lastWordWrapWidth = wordWrapWidth;
ScopedValueSetter<bool> svs (reentrant, true);
owner.checkLayout();
}
}
}
private:
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
{
return createIgnoredAccessibilityHandler (*this);
}
TextEditor& owner;
int lastWordWrapWidth = 0;
bool reentrant = false;
JUCE_DECLARE_NON_COPYABLE (TextEditorViewport)
};
//==============================================================================
namespace TextEditorDefs
{
const int textChangeMessageId = 0x10003001;
const int returnKeyMessageId = 0x10003002;
const int escapeKeyMessageId = 0x10003003;
const int focusLossMessageId = 0x10003004;
const int maxActionsPerTransaction = 100;
static int getCharacterCategory (juce_wchar character) noexcept
{
return CharacterFunctions::isLetterOrDigit (character)
? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1);
}
}
//==============================================================================
TextEditor::TextEditor (const String& name, juce_wchar passwordChar)
: Component (name),
passwordCharacter (passwordChar),
textStorage { std::make_unique<TextEditorStorage>() },
caretState { this }
{
setMouseCursor (MouseCursor::IBeamCursor);
viewport.reset (new TextEditorViewport (*this));
addAndMakeVisible (viewport.get());
viewport->setViewedComponent (textHolder = new TextHolderComponent (*this));
viewport->setWantsKeyboardFocus (false);
viewport->setScrollBarsShown (false, false);
setWantsKeyboardFocus (true);
recreateCaret();
}
TextEditor::~TextEditor()
{
if (auto* peer = getPeer())
peer->refreshTextInputTarget();
textValue.removeListener (textHolder);
textValue.referTo (Value());
viewport.reset();
textHolder = nullptr;
}
//==============================================================================
void TextEditor::newTransaction()
{
lastTransactionTime = Time::getApproximateMillisecondCounter();
undoManager.beginNewTransaction();
}
bool TextEditor::undoOrRedo (const bool shouldUndo)
{
if (! isReadOnly())
{
newTransaction();
if (shouldUndo ? undoManager.undo()
: undoManager.redo())
{
repaint();
textChanged();
scrollToMakeSureCursorIsVisible();
return true;
}
}
return false;
}
bool TextEditor::undo() { return undoOrRedo (true); }
bool TextEditor::redo() { return undoOrRedo (false); }
//==============================================================================
void TextEditor::setMultiLine (const bool shouldBeMultiLine,
const bool shouldWordWrap)
{
if (multiline != shouldBeMultiLine
|| wordWrap != (shouldWordWrap && shouldBeMultiLine))
{
multiline = shouldBeMultiLine;
wordWrap = shouldWordWrap && shouldBeMultiLine;
updateBaseShapedTextOptions();
checkLayout();
viewport->setViewPosition (0, 0);
resized();
scrollToMakeSureCursorIsVisible();
}
}
bool TextEditor::isMultiLine() const
{
return multiline;
}
void TextEditor::setScrollbarsShown (bool shown)
{
if (scrollbarVisible != shown)
{
scrollbarVisible = shown;
checkLayout();
}
}
void TextEditor::setReadOnly (bool shouldBeReadOnly)
{
if (readOnly != shouldBeReadOnly)
{
readOnly = shouldBeReadOnly;
enablementChanged();
invalidateAccessibilityHandler();
if (auto* peer = getPeer())
peer->refreshTextInputTarget();
}
}
void TextEditor::setClicksOutsideDismissVirtualKeyboard (bool newValue)
{
clicksOutsideDismissVirtualKeyboard = newValue;
}
bool TextEditor::isReadOnly() const noexcept
{
return readOnly || ! isEnabled();
}
bool TextEditor::isTextInputActive() const
{
return ! isReadOnly() && (! clicksOutsideDismissVirtualKeyboard || globalMouseListener.lastMouseDownInEditor());
}
void TextEditor::setReturnKeyStartsNewLine (bool shouldStartNewLine)
{
returnKeyStartsNewLine = shouldStartNewLine;
}
void TextEditor::setTabKeyUsedAsCharacter (bool shouldTabKeyBeUsed)
{
tabKeyUsed = shouldTabKeyBeUsed;
}
void TextEditor::setPopupMenuEnabled (bool b)
{
popupMenuEnabled = b;
}
void TextEditor::setSelectAllWhenFocused (bool b)
{
selectAllTextWhenFocused = b;
}
void TextEditor::setJustification (Justification j)
{
if (justification != j)
{
justification = j;
resized();
repaint();
}
}
//==============================================================================
void TextEditor::setFont (const Font& newFont)
{
currentFont = newFont;
scrollToMakeSureCursorIsVisible();
}
void TextEditor::applyFontToAllText (const Font& newFont, bool changeCurrentFont)
{
if (changeCurrentFont)
currentFont = newFont;
textStorage->setFontForAllText (newFont);
const auto overallColour = findColour (textColourId);
textStorage->setColourForAllText (overallColour);
checkLayout();
scrollToMakeSureCursorIsVisible();
repaint();
}
void TextEditor::applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour)
{
textStorage->setColourForAllText (newColour);
if (changeCurrentTextColour)
setColour (TextEditor::textColourId, newColour);
else
repaint();
}
void TextEditor::lookAndFeelChanged()
{
caret.reset();
recreateCaret();
}
void TextEditor::parentHierarchyChanged()
{
lookAndFeelChanged();
}
void TextEditor::enablementChanged()
{
recreateCaret();
repaint();
}
void TextEditor::setCaretVisible (bool shouldCaretBeVisible)
{
if (caretVisible != shouldCaretBeVisible)
{
caretVisible = shouldCaretBeVisible;
recreateCaret();
}
}
void TextEditor::recreateCaret()
{
if (isCaretVisible())
{
if (caret == nullptr)
{
caret.reset (getLookAndFeel().createCaretComponent (this));
textHolder->addChildComponent (caret.get());
updateCaretPosition();
}
}
else
{
caret.reset();
}
}
void TextEditor::updateCaretPosition()
{
if (caret != nullptr
&& getWidth() > 0 && getHeight() > 0)
{
caret->setCaretPosition (getCaretRectangle().translated (leftIndent,
topIndent + roundToInt (getYOffset())) - getTextOffset());
if (auto* handler = getAccessibilityHandler())
handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged);
}
}
TextEditor::LengthAndCharacterRestriction::LengthAndCharacterRestriction (int maxLen, const String& chars)
: allowedCharacters (chars), maxLength (maxLen)
{
}
String TextEditor::LengthAndCharacterRestriction::filterNewText (TextEditor& ed, const String& newInput)
{
String t (newInput);
if (allowedCharacters.isNotEmpty())
t = t.retainCharacters (allowedCharacters);
if (maxLength > 0)
t = t.substring (0, maxLength - (ed.getTotalNumChars() - ed.getHighlightedRegion().getLength()));
return t;
}
void TextEditor::setInputFilter (InputFilter* newFilter, bool takeOwnership)
{
inputFilter.set (newFilter, takeOwnership);
}
void TextEditor::setInputRestrictions (int maxLen, const String& chars)
{
setInputFilter (new LengthAndCharacterRestriction (maxLen, chars), true);
}
void TextEditor::setTextToShowWhenEmpty (const String& text, Colour colourToUse)
{
textToShowWhenEmpty = text;
colourForTextWhenEmpty = colourToUse;
}
void TextEditor::setPasswordCharacter (juce_wchar newPasswordCharacter)
{
if (passwordCharacter != newPasswordCharacter)
{
passwordCharacter = newPasswordCharacter;
applyFontToAllText (currentFont);
updateBaseShapedTextOptions();
}
}
void TextEditor::setScrollBarThickness (int newThicknessPixels)
{
viewport->setScrollBarThickness (newThicknessPixels);
}
//==============================================================================
void TextEditor::clear()
{
clearInternal (nullptr);
checkLayout();
undoManager.clearUndoHistory();
repaint();
}
void TextEditor::setText (const String& newText, bool sendTextChangeMessage)
{
auto newLength = newText.length();
if (newLength != getTotalNumChars() || getText() != newText)
{
if (! sendTextChangeMessage)
textValue.removeListener (textHolder);
textValue = newText;
auto oldCursorPos = caretState.getPosition();
auto cursorWasAtEnd = oldCursorPos >= getTotalNumChars();
clearInternal (nullptr);
insert (newText, 0, currentFont, findColour (textColourId), nullptr, caretState.getPosition());
// if you're adding text with line-feeds to a single-line text editor, it
// ain't gonna look right!
jassert (multiline || ! newText.containsAnyOf ("\r\n"));
if (cursorWasAtEnd && ! isMultiLine())
oldCursorPos = getTotalNumChars();
moveCaretTo (oldCursorPos, false);
if (sendTextChangeMessage)
textChanged();
else
textValue.addListener (textHolder);
checkLayout();
scrollToMakeSureCursorIsVisible();
undoManager.clearUndoHistory();
repaint();
}
}
//==============================================================================
void TextEditor::updateValueFromText()
{
if (valueTextNeedsUpdating)
{
valueTextNeedsUpdating = false;
textValue = getText();
}
}
Value& TextEditor::getTextValue()
{
updateValueFromText();
return textValue;
}
void TextEditor::textWasChangedByValue()
{
if (textValue.getValueSource().getReferenceCount() > 1)
setText (textValue.getValue());
}
//==============================================================================
void TextEditor::textChanged()
{
checkLayout();
if (listeners.size() != 0 || onTextChange != nullptr)
postCommandMessage (TextEditorDefs::textChangeMessageId);
if (textValue.getValueSource().getReferenceCount() > 1)
{
valueTextNeedsUpdating = false;
textValue = getText();
}
if (auto* handler = getAccessibilityHandler())
handler->notifyAccessibilityEvent (AccessibilityEvent::textChanged);
}
void TextEditor::setSelection (Range<int> newSelection) noexcept
{
if (newSelection != selection)
{
selection = newSelection;
if (auto* handler = getAccessibilityHandler())
handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged);
}
}
void TextEditor::returnPressed() { postCommandMessage (TextEditorDefs::returnKeyMessageId); }
void TextEditor::escapePressed() { postCommandMessage (TextEditorDefs::escapeKeyMessageId); }
void TextEditor::addListener (Listener* l) { listeners.add (l); }
void TextEditor::removeListener (Listener* l) { listeners.remove (l); }
//==============================================================================
void TextEditor::timerCallbackInt()
{
checkFocus();
auto now = Time::getApproximateMillisecondCounter();
if (now > lastTransactionTime + 200)
newTransaction();
}
void TextEditor::checkFocus()
{
if (! wasFocused && hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent())
wasFocused = true;
}
void TextEditor::repaintText (Range<int> range)
{
if (! range.isEmpty())
{
if (range.getEnd() >= getTotalNumChars())
{
textHolder->repaint();
return;
}
const auto [anchor, lh] = getCursorEdge (caretState.withPosition (range.getStart())
.withPreferredEdge (Edge::trailing));
auto y1 = std::trunc (anchor.y);
int y2 = 0;
if (range.getEnd() >= getTotalNumChars())
{
y2 = textHolder->getHeight();
}
else
{
const auto info = getCursorEdge (caretState.withPosition (range.getEnd())
.withPreferredEdge (Edge::leading));
y2 = (int) (info.first.y + lh * 2.0f);
}
const auto offset = getYOffset();
textHolder->repaint (0,
(int) std::floor (y1 + offset),
textHolder->getWidth(),
(int) std::ceil ((float) y2 - y1 + offset));
}
}
//==============================================================================
void TextEditor::moveCaret (const int newCaretPos)
{
const auto clamped = std::clamp (newCaretPos, 0, getTotalNumChars());
if (clamped == getCaretPosition())
return;
caretState.setPosition (clamped);
if (hasKeyboardFocus (false))
textHolder->restartTimer();
scrollToMakeSureCursorIsVisible();
updateCaretPosition();
if (auto* handler = getAccessibilityHandler())
handler->notifyAccessibilityEvent (AccessibilityEvent::textChanged);
}
int TextEditor::getCaretPosition() const
{
return caretState.getPosition();
}
void TextEditor::setCaretPosition (const int newIndex)
{
moveCaretTo (newIndex, false);
}
void TextEditor::moveCaretToEnd()
{
setCaretPosition (std::numeric_limits<int>::max());
}
void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX,
const int desiredCaretY)
{
updateCaretPosition();
auto caretRect = getCaretRectangle().translated (leftIndent, topIndent);
auto vx = caretRect.getX() - desiredCaretX;
auto vy = caretRect.getY() - desiredCaretY;
if (desiredCaretX < jmax (1, proportionOfWidth (0.05f)))
vx += desiredCaretX - proportionOfWidth (0.2f);
else if (desiredCaretX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
vx += desiredCaretX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
vx = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vx);
if (! isMultiLine())
{
vy = viewport->getViewPositionY();
}
else
{
vy = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vy);
if (desiredCaretY < 0)
vy = jmax (0, desiredCaretY + vy);
else if (desiredCaretY > jmax (0, viewport->getMaximumVisibleHeight() - caretRect.getHeight()))
vy += desiredCaretY + 2 + caretRect.getHeight() - viewport->getMaximumVisibleHeight();
}
viewport->setViewPosition (vx, vy);
}
Rectangle<int> TextEditor::getCaretRectangleForCharIndex (int index) const
{
const auto [anchor, cursorHeight] = getCursorEdge (caretState.withPosition (index));
Rectangle<float> caretRectangle { anchor.x, anchor.y, 2.0f, cursorHeight };
return caretRectangle.getSmallestIntegerContainer() + getTextOffset();
}
Point<int> TextEditor::getTextOffset() const
{
return { getLeftIndent() + borderSize.getLeft() - viewport->getViewPositionX(),
roundToInt ((float) getTopIndent() + (float) borderSize.getTop() + getYOffset()) - viewport->getViewPositionY() };
}
template <typename T>
detail::RangedValues<T> TextEditor::getGlyphRanges (const detail::RangedValues<T>& textRanges) const
{
detail::RangedValues<T> glyphRanges;
std::vector<Range<int64>> glyphRangesStorage;
detail::Ranges::Operations ops;
for (const auto [range, value, paragraph] : makeIntersectingRangedValues (&textRanges,
textStorage.get()))
{
paragraph->getShapedText().getGlyphRanges (range - paragraph->getRange().getStart(),
glyphRangesStorage);
for (const auto& glyphRange : glyphRangesStorage)
{
glyphRanges.set (glyphRange + paragraph->getStartingGlyph(), value, ops);
ops.clear();
}
}
return glyphRanges;
}
bool TextEditor::isTextStorageHeightGreaterEqualThan (float value) const
{
float height = 0.0;
for (auto paragraphItem : *textStorage)
{
height += paragraphItem.value->getHeight();
if (height >= value)
return true;
}
return false;
}
float TextEditor::getTextStorageHeight() const
{
const auto textHeight = std::accumulate (textStorage->begin(), textStorage->end(), 0.0f, [&] (auto acc, auto item)
{
return acc + item.value->getHeight();
});
if (! textStorage->isEmpty() && ! textStorage->back().value->getText().endsWith ("\n"))
return textHeight;
return textHeight + getLineSpacing() * textStorage->getLastFont().value_or (currentFont).getHeight();
}
float TextEditor::getYOffset() const
{
const auto bottomY = getMaximumTextHeight();
if (justification.testFlags (Justification::top) || isTextStorageHeightGreaterEqualThan ((float) bottomY))
return 0;
auto bottom = jmax (0.0f, (float) bottomY - getTextStorageHeight());
if (justification.testFlags (Justification::bottom))
return bottom;
return bottom * 0.5f;
}
Range<int64> TextEditor::getLineRangeForIndex (int index)
{
jassert (index >= 0);
const auto indexInText = (int64) index;
if (textStorage->isEmpty())
return { indexInText, indexInText };
if (const auto paragraph = textStorage->getParagraphContainingCodepointIndex (indexInText))
{
const auto& shapedText = paragraph->value->getShapedText();
auto r = *shapedText.getLineTextRanges().find (indexInText - paragraph->range.getStart())
+ paragraph->range.getStart();
if (r.getEnd() != paragraph->range.getEnd())
return r;
constexpr juce_wchar cr = 0x0d;
constexpr juce_wchar lf = 0x0a;
const auto startIt = shapedText.getText().begin();
auto endIt = shapedText.getText().end();
for (int i = 0; i < 2; ++i)
{
if (endIt == startIt)
break;
auto newEnd = endIt - 1;
if (*newEnd != cr && *newEnd != lf)
break;
r.setEnd (std::max (r.getStart(), r.getEnd() - 1));
endIt = newEnd;
}
return r;
}
const auto& lastParagraphItem = textStorage->back();
if (lastParagraphItem.value->getText().endsWith ("\n"))
return Range<int64>::withStartAndLength (lastParagraphItem.range.getEnd(), 0);
return lastParagraphItem.value->getShapedText().getLineTextRanges().getRanges().back()
+ lastParagraphItem.range.getStart();
}
std::pair<Point<float>, float> TextEditor::getTextSelectionEdge (int index, Edge edge) const
{
jassert (0 <= index && index < getTotalNumChars());
const auto textRange = Range<int64>::withStartAndLength ((int64) index, 1);
const auto paragraphIt = std::find_if (textStorage->begin(),
textStorage->end(),
[&] (const auto& p)
{
return p.range.contains (textRange.getStart());
});
jassert (paragraphIt != textStorage->end());
auto& paragraph = paragraphIt->value;
const auto& shapedText = paragraph->getShapedText();
const auto glyphRange = std::invoke ([&]
{
std::vector<Range<int64>> g;
shapedText.getGlyphRanges (textRange - paragraph->getRange().getStart(), g);
jassert (! g.empty());
return g.front();
});
const auto glyphsBounds = shapedText.getGlyphsBounds (glyphRange).getRectangle (0);
const auto ltr = shapedText.isLtr (glyphRange.getStart());
const auto anchorX = std::invoke ([&]
{
if (edge == Edge::leading)
return ltr ? glyphsBounds.getX() : glyphsBounds.getRight();
return ltr ? glyphsBounds.getRight() : glyphsBounds.getX();
});
const auto lineMetrics = shapedText.getLineMetricsForGlyphRange().find (glyphRange.getStart())->value;
const auto anchorY = lineMetrics.anchor.getY();
return { { anchorX, anchorY -lineMetrics.maxAscent + paragraph->getTop() },
lineMetrics.maxAscent + lineMetrics.maxDescent };
}
void TextEditor::updateBaseShapedTextOptions()
{
textStorage->setBaseShapedTextOptions (detail::ShapedText::Options{}
.withMaxWidth ((float) getWordWrapWidth())
.withTrailingWhitespacesShouldFit (true)
.withJustification (getJustificationType().getOnlyHorizontalFlags()),
passwordCharacter);
}
static auto asInt64Range (Range<int> r)
{
return Range<int64> { (int64) r.getStart(), (int64) r.getEnd() };
}
RectangleList<int> TextEditor::getTextBounds (Range<int> textRange) const
{
RectangleList<int> boundingBox;
detail::RangedValues<int> mask;
detail::Ranges::Operations ops;
mask.set (asInt64Range (textRange), 0, ops);
for (auto [_1, paragraph, _2] : makeIntersectingRangedValues (textStorage.get(), &mask))
{
ignoreUnused (_1, _2);
auto& shapedText = paragraph->getShapedText();
std::vector<Range<int64>> glyphRanges;
shapedText.getGlyphRanges (asInt64Range (textRange) - paragraph->getRange().getStart(),
glyphRanges);
for (const auto& glyphRange : glyphRanges)
for (const auto& bounds : shapedText.getGlyphsBounds (glyphRange))
boundingBox.add (bounds.withY (bounds.getY() + paragraph->getTop()).getSmallestIntegerContainer());
}
boundingBox.offsetAll (getTextOffset());
return boundingBox;
}
//==============================================================================
// Extra space for the cursor at the right-hand-edge
constexpr int rightEdgeSpace = 2;
int TextEditor::getWordWrapWidth() const
{
return wordWrap ? getMaximumTextWidth()
: std::numeric_limits<int>::max();
}
int TextEditor::getMaximumTextWidth() const
{
return jmax (1, viewport->getMaximumVisibleWidth() - leftIndent - rightEdgeSpace);
}
int TextEditor::getMaximumTextHeight() const
{
return jmax (1, viewport->getMaximumVisibleHeight() - topIndent);
}
void TextEditor::checkLayout()
{
if (getWordWrapWidth() > 0)
{
const auto textBottom = topIndent
+ (int) std::ceil (getYOffset() + getTextStorageHeight());
const auto maxTextWidth = std::accumulate (textStorage->begin(), textStorage->end(), 0.0f, [&](auto pMax, auto paragraph)
{
auto& shapedText = paragraph.value->getShapedText();
const auto paragraphWidth = std::accumulate (shapedText.getLineMetricsForGlyphRange().begin(),
shapedText.getLineMetricsForGlyphRange().end(),
0.0f,
[&] (auto lMax, auto line)
{
return std::max (lMax,
line.value.effectiveLineLength);
});
return std::max (pMax, paragraphWidth);
});
const auto textRight = std::max (viewport->getMaximumVisibleWidth(),
(int) std::ceil (maxTextWidth) + leftIndent + rightEdgeSpace);
textHolder->setSize (textRight, std::max (textBottom, viewport->getHeight()));
viewport->setScrollBarsShown (scrollbarVisible && multiline && textBottom > viewport->getMaximumVisibleHeight(),
scrollbarVisible && multiline && ! wordWrap && textRight > viewport->getMaximumVisibleWidth());
}
}
int TextEditor::getTextWidth() const { return textHolder->getWidth(); }
int TextEditor::getTextHeight() const { return textHolder->getHeight(); }
void TextEditor::setIndents (int newLeftIndent, int newTopIndent)
{
if (leftIndent != newLeftIndent || topIndent != newTopIndent)
{
leftIndent = newLeftIndent;
topIndent = newTopIndent;
resized();
repaint();
}
}
void TextEditor::setBorder (BorderSize<int> border)
{
borderSize = border;
resized();
}
BorderSize<int> TextEditor::getBorder() const
{
return borderSize;
}
void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor)
{
keepCaretOnScreen = shouldScrollToShowCursor;
}
void TextEditor::scrollToMakeSureCursorIsVisible()
{
updateCaretPosition();
if (keepCaretOnScreen)
{
auto viewPos = viewport->getViewPosition();
auto caretRect = getCaretRectangle().translated (leftIndent, topIndent) - getTextOffset();
auto relativeCursor = caretRect.getPosition() - viewPos;
if (relativeCursor.x < jmax (1, proportionOfWidth (0.05f)))
{
viewPos.x += relativeCursor.x - proportionOfWidth (0.2f);
}
else if (relativeCursor.x > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
{
viewPos.x += relativeCursor.x + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
}
viewPos.x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), viewPos.x);
if (! isMultiLine())
{
viewPos.y = (getHeight() - textHolder->getHeight() - topIndent) / -2;
}
else if (relativeCursor.y < 0)
{
viewPos.y = jmax (0, relativeCursor.y + viewPos.y);
}
else if (relativeCursor.y > jmax (0, viewport->getMaximumVisibleHeight() - caretRect.getHeight()))
{
viewPos.y += relativeCursor.y + 2 + caretRect.getHeight() - viewport->getMaximumVisibleHeight();
}
viewport->setViewPosition (viewPos);
}
}
void TextEditor::moveCaretTo (const int newPosition, const bool isSelecting)
{
if (isSelecting)
{
moveCaret (newPosition);
auto oldSelection = selection;
if (dragType == notDragging)
{
if (std::abs (getCaretPosition() - selection.getStart()) < std::abs (getCaretPosition() - selection.getEnd()))
dragType = draggingSelectionStart;
else
dragType = draggingSelectionEnd;
}
if (dragType == draggingSelectionStart)
{
if (getCaretPosition() >= selection.getEnd())
dragType = draggingSelectionEnd;
setSelection (Range<int>::between (getCaretPosition(), selection.getEnd()));
}
else
{
if (getCaretPosition() < selection.getStart())
dragType = draggingSelectionStart;
setSelection (Range<int>::between (getCaretPosition(), selection.getStart()));
}
repaintText (selection.getUnionWith (oldSelection));
}
else
{
dragType = notDragging;
repaintText (selection);
moveCaret (newPosition);
setSelection (Range<int>::emptyRange (getCaretPosition()));
}
}
int TextEditor::getTextIndexAt (const int x, const int y) const
{
const auto offset = getTextOffset();
return indexAtPosition ((float) (x - offset.x),
(float) (y - offset.y));
}
int TextEditor::getTextIndexAt (const Point<int> pt) const
{
return getTextIndexAt (pt.x, pt.y);
}
int TextEditor::getCharIndexForPoint (const Point<int> point) const
{
return getTextIndexAt (isMultiLine() ? point : getTextBounds ({ 0, getTotalNumChars() }).getBounds().getConstrainedPoint (point));
}
void TextEditor::insertTextAtCaret (const String& t)
{
const auto filtered = inputFilter != nullptr ? inputFilter->filterNewText (*this, t) : t;
const auto newText = isMultiLine() ? filtered.replace ("\r\n", "\n")
: filtered.replaceCharacters ("\r\n", " ");
const auto insertIndex = selection.getStart();
const auto newCaretPos = insertIndex + newText.length();
remove (selection, getUndoManager(),
newText.isNotEmpty() ? newCaretPos - 1 : newCaretPos);
insert (newText, insertIndex, currentFont, findColour (textColourId),
getUndoManager(), newCaretPos);
textChanged();
}
void TextEditor::setHighlightedRegion (const Range<int>& newSelection)
{
if (newSelection == getHighlightedRegion())
return;
const auto cursorAtStart = newSelection.getEnd() == getHighlightedRegion().getStart()
|| newSelection.getEnd() == getHighlightedRegion().getEnd();
moveCaretTo (cursorAtStart ? newSelection.getEnd() : newSelection.getStart(), false);
moveCaretTo (cursorAtStart ? newSelection.getStart() : newSelection.getEnd(), true);
}
//==============================================================================
void TextEditor::copy()
{
if (passwordCharacter == 0)
{
auto selectedText = getHighlightedText();
if (selectedText.isNotEmpty())
SystemClipboard::copyTextToClipboard (selectedText);
}
}
void TextEditor::paste()
{
if (! isReadOnly())
{
auto clip = SystemClipboard::getTextFromClipboard();
if (clip.isNotEmpty())
insertTextAtCaret (clip);
}
}
void TextEditor::cut()
{
if (! isReadOnly())
{
moveCaret (selection.getEnd());
insertTextAtCaret (String());
}
}
static void drawUnderline (Graphics& g,
Span<const detail::ShapedGlyph> glyphs,
Span<const Point<float>> positions,
const Font& font,
const AffineTransform& transform,
bool underlineWhitespace)
{
const auto lineThickness = font.getDescent() * 0.3f;
const auto getLeft = [&] (const auto& iter)
{
return *(positions.begin() + std::distance (glyphs.begin(), iter));
};
const auto getRight = [&] (const auto& iter)
{
if (iter == glyphs.end())
return positions.back() + glyphs.back().advance;
const auto p = *(positions.begin() + std::distance (glyphs.begin(), iter));
return p + iter->advance;
};
for (auto it = glyphs.begin(), end = glyphs.end(); it != end;)
{
const auto adjacent = std::adjacent_find (it,
end,
[] (const auto& a, const auto& b)
{
return a.isWhitespace() != b.isWhitespace();
});
if (! it->isWhitespace() || underlineWhitespace)
{
const auto left = getLeft (it);
const auto right = getRight (adjacent);
Path p;
p.addRectangle (left.x, left.y + lineThickness * 2.0f, right.x - left.x, lineThickness);
g.fillPath (p, transform);
}
it = adjacent + (adjacent == end ? 0 : 1);;
}
}
struct UseClip
{
bool clipAtBegin = false;
bool clipAtEnd = false;
};
// Glyphs can reach beyond the anchor - advance defined rectangle. We shouldn't use a clip unless
// we need to partially paint a ligature.
static UseClip getDrawableGlyphs (Span<const detail::ShapedGlyph> glyphs,
Span<const Point<float>> positions,
std::vector<uint16_t>& glyphIdsOut,
std::vector<Point<float>>& positionsOut)
{
jassert (! glyphs.empty() && glyphs.size() == positions.size());
glyphIdsOut.clear();
positionsOut.clear();
UseClip useClip;
const auto& firstGlyph = glyphs.front();
if (firstGlyph.isPlaceholderForLigature())
{
useClip.clipAtBegin = true;
glyphIdsOut.push_back ((uint16_t) firstGlyph.glyphId);
positionsOut.push_back (positions[0] - (float) firstGlyph.getDistanceFromLigature() * firstGlyph.advance);
}
int remainingLigaturePlaceholders = 0;
for (const auto [index, glyph] : enumerate (glyphs, size_t{}))
{
if (glyph.isLigature())
remainingLigaturePlaceholders += glyph.getNumTrailingLigaturePlaceholders();
else
remainingLigaturePlaceholders = std::max (0, remainingLigaturePlaceholders - 1);
if (! glyph.isPlaceholderForLigature())
{
glyphIdsOut.push_back ((uint16_t) glyph.glyphId);
positionsOut.push_back (positions[index]);
}
}
useClip.clipAtEnd = remainingLigaturePlaceholders > 0;
return useClip;
}
//==============================================================================
void TextEditor::drawContent (Graphics& g)
{
using namespace detail;
g.setOrigin (leftIndent, topIndent);
float yOffset = getYOffset();
Graphics::ScopedSaveState ss (g);
detail::Ranges::Operations ops;
const auto glyphColours = getGlyphRanges (textStorage->getColours());
const auto selectedTextRanges = std::invoke ([&]
{
detail::RangedValues<int8_t> rv;
rv.set (asInt64Range (selection), 1, ops);
return getGlyphRanges (rv);
});
const auto textSelectionMask = std::invoke ([&]
{
ops.clear();
detail::RangedValues<int8_t> rv;
rv.set ({ 0, textStorage->getTotalNumGlyphs() }, 0, ops);
for (const auto item : selectedTextRanges)
rv.set (item.range, item.value, ops);
return rv;
});
const auto underlining = std::invoke ([&]
{
ops.clear();
detail::RangedValues<int> rv;
rv.set ({ 0, textStorage->getTotalNumChars() }, 0, ops);
for (const auto& underlined : underlinedSections)
rv.set (asInt64Range (underlined), 1, ops);
return getGlyphRanges (rv);
});
const auto drawSelection = [&] (Span<const detail::ShapedGlyph> glyphs,
Span<const Point<float>> positions,
Font font,
Range<int64>,
LineMetrics,
int)
{
g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f));
g.fillRect ({ positions.front().translated (0.0f, -font.getAscent() + yOffset),
positions.back().translated (glyphs.back().advance.getX(), font.getDescent() + yOffset) });
};
std::vector<uint16_t> glyphIds;
std::vector<Point<float>> positionsForGlyphIds;
const auto drawGlyphRuns = [&] (Span<const detail::ShapedGlyph> glyphs,
Span<const Point<float>> positions,
Font font,
Range<int64>,
LineMetrics,
Colour colour,
int isSelected,
int hasTemporaryUnderlining)
{
auto& context = g.getInternalContext();
if (context.getFont() != font)
context.setFont (font);
const auto transform = AffineTransform::translation (0.0f, yOffset);
g.setColour (isSelected ? findColour (highlightedTextColourId) : colour);
glyphIds.clear();
positionsForGlyphIds.clear();
const auto useClip = getDrawableGlyphs (glyphs, positions, glyphIds, positionsForGlyphIds);
{
// Graphics::ScopedSaveState doesn't restore the clipping regions
context.saveState();
const ScopeGuard restoreGraphicsContext { [&context] { context.restoreState(); } };
if (useClip.clipAtBegin || useClip.clipAtEnd)
{
const auto componentBoundsInDrawBasis = getLocalBounds().toFloat().transformedBy (transform.inverted());
// We don't really want to constrain the vertical clip, so we add/subtract a little extra,
// because clipping right at the line 0 will still result in a visible clip border with
// the below code.
const auto clipTop = componentBoundsInDrawBasis.getY() - 10.0f;
const auto clipBottom = componentBoundsInDrawBasis.getBottom() + 10.0f;
const auto clipX = useClip.clipAtBegin ? positions.front().getX() : 0.0f;
const auto clipRight = useClip.clipAtEnd ? positions.back().getX() + glyphs.back().advance.getX()
: (float) getRight();
const Rectangle<float> clipRect { { clipX, clipTop }, { clipRight, clipBottom } };
Path clipPath;
clipPath.addRectangle (clipRect);
context.clipToPath (clipPath, transform);
}
context.drawGlyphs (glyphIds, positionsForGlyphIds, transform);
}
if (font.isUnderlined())
drawUnderline (g, glyphs, positions, font, transform, isWhitespaceUnderlined());
if (hasTemporaryUnderlining)
{
const auto startX = roundToInt (positions.front().getX());
const auto endX = roundToInt (positions.back().getX() + glyphs.back().advance.getX());
auto baselineY = roundToInt (positions.front().getY() + 0.5f);
Graphics::ScopedSaveState state (g);
g.addTransform (transform);
g.reduceClipRegion ({ startX, baselineY, endX - startX, 1 });
g.fillCheckerBoard ({ (float) endX, (float) baselineY + 1.0f },
3.0f,
1.0f,
colour,
Colours::transparentBlack);
}
};
const auto clip = std::invoke ([&]
{
auto c = g.getClipBounds();
c.setY (roundToInt ((float) c.getY() - yOffset));
return c;
});
for (auto [range, paragraph] : *textStorage)
{
ignoreUnused (range);
const auto glyphsRange = Range<int64>::withStartAndLength (paragraph->getStartingGlyph(),
paragraph->getNumGlyphs());
const auto top = paragraph->getTop();
const auto bottom = top + paragraph->getHeight();
if ((float) clip.getY() <= bottom && top <= (float) clip.getBottom())
{
paragraph->getShapedText().accessTogetherWith (drawSelection,
selectedTextRanges.getIntersectionsStartingAtZeroWith (glyphsRange));
paragraph->getShapedText().accessTogetherWith (drawGlyphRuns,
glyphColours,
textSelectionMask.getIntersectionsStartingAtZeroWith (glyphsRange),
underlining.getIntersectionsStartingAtZeroWith (glyphsRange));
}
yOffset += paragraph->getHeight();
}
}
void TextEditor::paint (Graphics& g)
{
getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this);
}
void TextEditor::paintOverChildren (Graphics& g)
{
if (textToShowWhenEmpty.isNotEmpty()
&& (! hasKeyboardFocus (false))
&& getTotalNumChars() == 0)
{
g.setColour (colourForTextWhenEmpty);
g.setFont (getFont());
Rectangle<int> textBounds (leftIndent,
topIndent,
viewport->getWidth() - leftIndent,
getHeight() - topIndent);
if (! textBounds.isEmpty())
g.drawText (textToShowWhenEmpty, textBounds, justification, true);
}
getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this);
}
//==============================================================================
void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*)
{
const bool writable = ! isReadOnly();
if (passwordCharacter == 0)
{
m.addItem (StandardApplicationCommandIDs::cut, TRANS ("Cut"), writable);
m.addItem (StandardApplicationCommandIDs::copy, TRANS ("Copy"), ! selection.isEmpty());
}
m.addItem (StandardApplicationCommandIDs::paste, TRANS ("Paste"), writable);
m.addItem (StandardApplicationCommandIDs::del, TRANS ("Delete"), writable);
m.addSeparator();
m.addItem (StandardApplicationCommandIDs::selectAll, TRANS ("Select All"));
m.addSeparator();
if (getUndoManager() != nullptr)
{
m.addItem (StandardApplicationCommandIDs::undo, TRANS ("Undo"), undoManager.canUndo());
m.addItem (StandardApplicationCommandIDs::redo, TRANS ("Redo"), undoManager.canRedo());
}
}
void TextEditor::performPopupMenuAction (const int menuItemID)
{
switch (menuItemID)
{
case StandardApplicationCommandIDs::cut: cutToClipboard(); break;
case StandardApplicationCommandIDs::copy: copyToClipboard(); break;
case StandardApplicationCommandIDs::paste: pasteFromClipboard(); break;
case StandardApplicationCommandIDs::del: cut(); break;
case StandardApplicationCommandIDs::selectAll: selectAll(); break;
case StandardApplicationCommandIDs::undo: undo(); break;
case StandardApplicationCommandIDs::redo: redo(); break;
default: break;
}
}
//==============================================================================
void TextEditor::mouseDown (const MouseEvent& e)
{
beginDragAutoRepeat (100);
newTransaction();
if (wasFocused || ! selectAllTextWhenFocused)
{
if (! (popupMenuEnabled && e.mods.isPopupMenu()))
{
caretState.setPreferredEdge (Edge::leading);
moveCaretTo (getTextIndexAt (e.getPosition()), e.mods.isShiftDown());
if (auto* peer = getPeer())
peer->closeInputMethodContext();
}
else
{
PopupMenu m;
m.setLookAndFeel (&getLookAndFeel());
addPopupMenuItems (m, &e);
menuActive = true;
m.showMenuAsync (PopupMenu::Options(),
[safeThis = SafePointer<TextEditor> { this }] (int menuResult)
{
if (auto* editor = safeThis.getComponent())
{
editor->menuActive = false;
if (menuResult != 0)
editor->performPopupMenuAction (menuResult);
}
});
}
}
}
void TextEditor::mouseDrag (const MouseEvent& e)
{
if (wasFocused || ! selectAllTextWhenFocused)
{
if (! (popupMenuEnabled && e.mods.isPopupMenu()))
{
caretState.setPreferredEdge (Edge::leading);
moveCaretTo (getTextIndexAt (e.getPosition()), true);
}
}
}
void TextEditor::mouseUp (const MouseEvent& e)
{
newTransaction();
textHolder->restartTimer();
if (wasFocused || ! selectAllTextWhenFocused)
if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu()))
moveCaret (getTextIndexAt (e.getPosition()));
wasFocused = true;
}
void TextEditor::mouseDoubleClick (const MouseEvent& e)
{
int tokenEnd = getTextIndexAt (e.getPosition());
int tokenStart = 0;
if (e.getNumberOfClicks() > 3)
{
tokenEnd = getTotalNumChars();
}
else
{
auto t = getText();
auto totalLength = getTotalNumChars();
while (tokenEnd < totalLength)
{
auto c = t[tokenEnd];
// (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
++tokenEnd;
else
break;
}
tokenStart = tokenEnd;
while (tokenStart > 0)
{
auto c = t[tokenStart - 1];
// (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
--tokenStart;
else
break;
}
if (e.getNumberOfClicks() > 2)
{
while (tokenEnd < totalLength)
{
auto c = t[tokenEnd];
if (c != '\r' && c != '\n')
++tokenEnd;
else
break;
}
while (tokenStart > 0)
{
auto c = t[tokenStart - 1];
if (c != '\r' && c != '\n')
--tokenStart;
else
break;
}
}
}
moveCaretTo (tokenEnd, false);
moveCaretTo (tokenStart, true);
}
void TextEditor::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
{
if (! viewport->useMouseWheelMoveIfNeeded (e, wheel))
Component::mouseWheelMove (e, wheel);
}
//==============================================================================
bool TextEditor::moveCaretWithTransaction (const int newPos, const bool selecting)
{
newTransaction();
moveCaretTo (newPos, selecting);
if (auto* peer = getPeer())
peer->closeInputMethodContext();
return true;
}
bool TextEditor::moveCaretLeft (bool moveInWholeWordSteps, bool selecting)
{
auto pos = getCaretPosition();
if (moveInWholeWordSteps)
pos = findWordBreakBefore (pos);
else
--pos;
return moveCaretWithTransaction (pos, selecting);
}
bool TextEditor::moveCaretRight (bool moveInWholeWordSteps, bool selecting)
{
auto pos = getCaretPosition();
if (moveInWholeWordSteps)
pos = findWordBreakAfter (pos);
else
++pos;
return moveCaretWithTransaction (pos, selecting);
}
TextEditor::Edge TextEditor::getEdgeTypeCloserToPosition (int indexInText, Point<float> pos) const
{
const auto testCaret = caretState.withPosition (indexInText);
const auto leading = getCursorEdge (testCaret.withPreferredEdge (Edge::leading)).first.getDistanceFrom (pos);
const auto trailing = getCursorEdge (testCaret.withPreferredEdge (Edge::trailing)).first.getDistanceFrom (pos);
if (leading < trailing)
return Edge::leading;
return Edge::trailing;
}
bool TextEditor::moveCaretUp (bool selecting)
{
if (! isMultiLine())
return moveCaretToStartOfLine (selecting);
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
const auto newY = caretPos.getY() - 1.0f;
if (newY < 0.0f)
return moveCaretToStartOfLine (selecting);
const Point<float> testPosition { caretPos.getX(), newY };
const auto newIndex = indexAtPosition (testPosition.getX(), testPosition.getY());
const auto edgeToUse = getEdgeTypeCloserToPosition (newIndex, testPosition);
caretState.setPreferredEdge (edgeToUse);
return moveCaretWithTransaction (newIndex, selecting);
}
bool TextEditor::moveCaretDown (bool selecting)
{
if (! isMultiLine())
return moveCaretToEndOfLine (selecting);
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
const Point<float> testPosition { caretPos.getX(), caretPos.getBottom() + 1.0f };
const auto newIndex = indexAtPosition (testPosition.getX(), testPosition.getY());
const auto edgeToUse = getEdgeTypeCloserToPosition (newIndex, testPosition);
caretState.setPreferredEdge (edgeToUse);
return moveCaretWithTransaction (newIndex, selecting);
}
bool TextEditor::pageUp (bool selecting)
{
if (! isMultiLine())
return moveCaretToStartOfLine (selecting);
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - (float) viewport->getViewHeight()), selecting);
}
bool TextEditor::pageDown (bool selecting)
{
if (! isMultiLine())
return moveCaretToEndOfLine (selecting);
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + (float) viewport->getViewHeight()), selecting);
}
void TextEditor::scrollByLines (int deltaLines)
{
viewport->getVerticalScrollBar().moveScrollbarInSteps (deltaLines);
}
bool TextEditor::scrollDown()
{
scrollByLines (-1);
return true;
}
bool TextEditor::scrollUp()
{
scrollByLines (1);
return true;
}
bool TextEditor::moveCaretToTop (bool selecting)
{
return moveCaretWithTransaction (0, selecting);
}
bool TextEditor::moveCaretToStartOfLine (bool selecting)
{
const auto lineRange = getLineRangeForIndex (caretState.getVisualIndex());
caretState.setPreferredEdge (Edge::leading);
return moveCaretWithTransaction ((int) lineRange.getStart(), selecting);
}
bool TextEditor::moveCaretToEnd (bool selecting)
{
return moveCaretWithTransaction (getTotalNumChars(), selecting);
}
bool TextEditor::moveCaretToEndOfLine (bool selecting)
{
const auto lineRange = getLineRangeForIndex (caretState.getVisualIndex());
caretState.setPreferredEdge (Edge::trailing);
return moveCaretWithTransaction ((int) lineRange.getEnd(), selecting);
}
bool TextEditor::deleteBackwards (bool moveInWholeWordSteps)
{
if (moveInWholeWordSteps)
moveCaretTo (findWordBreakBefore (getCaretPosition()), true);
else if (selection.isEmpty() && selection.getStart() > 0)
setSelection ({ selection.getEnd() - 1, selection.getEnd() });
cut();
return true;
}
bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/)
{
if (selection.isEmpty() && selection.getStart() < getTotalNumChars())
setSelection ({ selection.getStart(), selection.getStart() + 1 });
cut();
return true;
}
bool TextEditor::copyToClipboard()
{
newTransaction();
copy();
return true;
}
bool TextEditor::cutToClipboard()
{
newTransaction();
copy();
cut();
return true;
}
bool TextEditor::pasteFromClipboard()
{
newTransaction();
paste();
return true;
}
bool TextEditor::selectAll()
{
newTransaction();
moveCaretTo (getTotalNumChars(), false);
moveCaretTo (0, true);
return true;
}
//==============================================================================
void TextEditor::setEscapeAndReturnKeysConsumed (bool shouldBeConsumed) noexcept
{
consumeEscAndReturnKeys = shouldBeConsumed;
}
bool TextEditor::keyPressed (const KeyPress& key)
{
if (isReadOnly() && key != KeyPress ('c', ModifierKeys::commandModifier, 0)
&& key != KeyPress ('a', ModifierKeys::commandModifier, 0))
return false;
if (! TextEditorKeyMapper<TextEditor>::invokeKeyFunction (*this, key))
{
if (key == KeyPress::returnKey)
{
newTransaction();
if (returnKeyStartsNewLine)
{
insertTextAtCaret ("\n");
}
else
{
returnPressed();
return consumeEscAndReturnKeys;
}
}
else if (key.isKeyCode (KeyPress::escapeKey))
{
newTransaction();
moveCaretTo (getCaretPosition(), false);
escapePressed();
return consumeEscAndReturnKeys;
}
else if (key.getTextCharacter() >= ' '
|| (tabKeyUsed && (key.getTextCharacter() == '\t')))
{
insertTextAtCaret (String::charToString (key.getTextCharacter()));
lastTransactionTime = Time::getApproximateMillisecondCounter();
}
else
{
return false;
}
}
return true;
}
bool TextEditor::keyStateChanged (const bool isKeyDown)
{
if (! isKeyDown)
return false;
#if JUCE_WINDOWS
if (KeyPress (KeyPress::F4Key, ModifierKeys::altModifier, 0).isCurrentlyDown())
return false; // We need to explicitly allow alt-F4 to pass through on Windows
#endif
if ((! consumeEscAndReturnKeys)
&& (KeyPress (KeyPress::escapeKey).isCurrentlyDown()
|| KeyPress (KeyPress::returnKey).isCurrentlyDown()))
return false;
// (overridden to avoid forwarding key events to the parent)
return ! ModifierKeys::currentModifiers.isCommandDown();
}
//==============================================================================
void TextEditor::focusGained (FocusChangeType cause)
{
newTransaction();
if (selectAllTextWhenFocused)
{
moveCaretTo (0, false);
moveCaretTo (getTotalNumChars(), true);
}
checkFocus();
if (cause == FocusChangeType::focusChangedByMouseClick && selectAllTextWhenFocused)
wasFocused = false;
repaint();
updateCaretPosition();
}
void TextEditor::focusLost (FocusChangeType)
{
newTransaction();
wasFocused = false;
textHolder->stopTimer();
underlinedSections.clear();
updateCaretPosition();
postCommandMessage (TextEditorDefs::focusLossMessageId);
repaint();
}
//==============================================================================
void TextEditor::resized()
{
viewport->setBoundsInset (borderSize);
viewport->setSingleStepSizes (16, roundToInt (currentFont.getHeight()));
updateBaseShapedTextOptions();
checkLayout();
if (isMultiLine())
updateCaretPosition();
else
scrollToMakeSureCursorIsVisible();
}
void TextEditor::handleCommandMessage (const int commandId)
{
Component::BailOutChecker checker (this);
switch (commandId)
{
case TextEditorDefs::textChangeMessageId:
listeners.callChecked (checker, [this] (Listener& l) { l.textEditorTextChanged (*this); });
if (! checker.shouldBailOut())
NullCheckedInvocation::invoke (onTextChange);
break;
case TextEditorDefs::returnKeyMessageId:
listeners.callChecked (checker, [this] (Listener& l) { l.textEditorReturnKeyPressed (*this); });
if (! checker.shouldBailOut())
NullCheckedInvocation::invoke (onReturnKey);
break;
case TextEditorDefs::escapeKeyMessageId:
listeners.callChecked (checker, [this] (Listener& l) { l.textEditorEscapeKeyPressed (*this); });
if (! checker.shouldBailOut())
NullCheckedInvocation::invoke (onEscapeKey);
break;
case TextEditorDefs::focusLossMessageId:
updateValueFromText();
listeners.callChecked (checker, [this] (Listener& l) { l.textEditorFocusLost (*this); });
if (! checker.shouldBailOut())
NullCheckedInvocation::invoke (onFocusLost);
break;
default:
jassertfalse;
break;
}
}
void TextEditor::setTemporaryUnderlining (const Array<Range<int>>& newUnderlinedSections)
{
underlinedSections = newUnderlinedSections;
repaint();
}
TextInputTarget::VirtualKeyboardType TextEditor::getKeyboardType()
{
return passwordCharacter != 0 ? passwordKeyboard : keyboardType;
}
//==============================================================================
UndoManager* TextEditor::getUndoManager() noexcept
{
return readOnly ? nullptr : &undoManager;
}
void TextEditor::clearInternal (UndoManager* const um)
{
remove ({ 0, getTotalNumChars() }, um, getCaretPosition());
}
void TextEditor::insert (const String& text, int insertIndex, const Font& font,
Colour colour, UndoManager* um, int caretPositionToMoveTo)
{
if (text.isNotEmpty())
{
if (um != nullptr)
{
if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
newTransaction();
um->perform (new InsertAction (*this, text, insertIndex, font, colour,
getCaretPosition(), caretPositionToMoveTo));
}
else
{
textStorage->set ({ insertIndex, insertIndex }, text, font, colour);
caretState.updateEdge();
repaintText ({ insertIndex, getTotalNumChars() }); // must do this before and after changing the data, in case
// a line gets moved due to word wrap
totalNumChars = -1;
valueTextNeedsUpdating = true;
checkLayout();
moveCaretTo (caretPositionToMoveTo, false);
repaintText ({ insertIndex, getTotalNumChars() });
}
}
}
void TextEditor::reinsert (const TextEditorStorageChunks& chunks)
{
textStorage->addChunks (chunks);
totalNumChars = -1;
valueTextNeedsUpdating = true;
}
void TextEditor::remove (Range<int> range,
UndoManager* const um,
const int caretPositionToMoveTo,
TextEditorStorageChunks* removedOut)
{
using namespace detail;
if (! range.isEmpty())
{
if (um != nullptr)
{
if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
newTransaction();
um->perform (new RemoveAction (*this, range, caretState.getPosition(),
caretPositionToMoveTo));
}
else
{
textStorage->remove (asInt64Range (range), removedOut);
caretState.updateEdge();
totalNumChars = -1;
valueTextNeedsUpdating = true;
checkLayout();
moveCaretTo (caretPositionToMoveTo, false);
repaintText ({ range.getStart(), getTotalNumChars() });
}
}
}
//==============================================================================
String TextEditor::getText() const
{
return textStorage->getText();
}
String TextEditor::getTextInRange (const Range<int>& range) const
{
return textStorage->getTextInRange (asInt64Range (range));
}
String TextEditor::getHighlightedText() const
{
return getTextInRange (selection);
}
int TextEditor::getTotalNumChars() const
{
return (int) textStorage->getTotalNumChars();
}
bool TextEditor::isEmpty() const
{
return getTotalNumChars() == 0;
}
std::pair<Point<float>, float> TextEditor::getCursorEdge (const CaretState& tempCaret) const
{
const auto visualIndex = tempCaret.getVisualIndex();
jassert (0 <= visualIndex && visualIndex <= getTotalNumChars());
if (getWordWrapWidth() <= 0)
return { {}, currentFont.getHeight() };
const auto getJustificationOffsetX = [&]
{
const auto bottomRightX = (float) getMaximumTextWidth();
if (justification.testFlags (Justification::horizontallyCentred)) return jmax (0.0f, bottomRightX * 0.5f);
if (justification.testFlags (Justification::right)) return jmax (0.0f, bottomRightX);
return 0.0f;
};
if (textStorage->isEmpty())
return { { getJustificationOffsetX(), 0.0f }, currentFont.getHeight() };
return getTextSelectionEdge (visualIndex, tempCaret.getEdge());
}
int TextEditor::indexAtPosition (float x, float y) const
{
y = std::max (0.0f, y);
if (getWordWrapWidth() <= 0)
return getTotalNumChars();
auto paragraphIt = textStorage->begin();
float paragraphTop = 0.0f;
while (paragraphIt != textStorage->end())
{
auto& paragraph = paragraphIt->value;
const auto paragraphBottom = paragraphTop + paragraph->getHeight();
if (paragraphTop <= y && y < paragraphBottom)
break;
if (y < paragraphTop)
return {};
paragraphTop = paragraphBottom;
++paragraphIt;
}
if (paragraphIt == textStorage->end())
return getTotalNumChars();
auto& shapedText = paragraphIt->value->getShapedText();
return (int) (shapedText.getTextIndexForCaret ({ x, y - paragraphTop }) + paragraphIt->range.getStart());
}
//==============================================================================
int TextEditor::findWordBreakAfter (const int position) const
{
auto t = getTextInRange ({ position, position + 512 });
auto totalLength = t.length();
int i = 0;
while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
++i;
auto type = TextEditorDefs::getCharacterCategory (t[i]);
while (i < totalLength && type == TextEditorDefs::getCharacterCategory (t[i]))
++i;
while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
++i;
return position + i;
}
int TextEditor::findWordBreakBefore (const int position) const
{
if (position <= 0)
return 0;
auto startOfBuffer = jmax (0, position - 512);
auto t = getTextInRange ({ startOfBuffer, position });
int i = position - startOfBuffer;
while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1]))
--i;
if (i > 0)
{
auto type = TextEditorDefs::getCharacterCategory (t [i - 1]);
while (i > 0 && type == TextEditorDefs::getCharacterCategory (t [i - 1]))
--i;
}
jassert (startOfBuffer + i >= 0);
return startOfBuffer + i;
}
//==============================================================================
TextEditor::CaretState::CaretState (const TextEditor* ownerIn)
: owner { *ownerIn }
{
updateEdge();
}
void TextEditor::CaretState::setPosition (int newPosition)
{
if (std::exchange (position, newPosition) != newPosition)
updateEdge();
}
void TextEditor::CaretState::setPreferredEdge (TextEditor::Edge newEdge)
{
if (std::exchange (preferredEdge, newEdge) != newEdge)
updateEdge();
}
int TextEditor::CaretState::getVisualIndex() const
{
if (edge == Edge::leading)
return position;
return position - 1;
}
TextEditor::CaretState TextEditor::CaretState::withPosition (int newPosition) const
{
auto copy = *this;
copy.setPosition (newPosition);
return copy;
}
TextEditor::CaretState TextEditor::CaretState::withPreferredEdge (Edge newEdge) const
{
auto copy = *this;
copy.setPreferredEdge (newEdge);
return copy;
}
void TextEditor::CaretState::updateEdge()
{
jassert (0 <= position && position <= owner.getTotalNumChars());
if (position == 0)
{
edge = Edge::leading;
}
else if (owner.getText()[position - 1] == '\n')
{
edge = Edge::leading;
}
else if (position == owner.getTotalNumChars())
{
edge = Edge::trailing;
}
else
{
edge = preferredEdge;
}
}
//==============================================================================
class TextEditor::EditorAccessibilityHandler final : public AccessibilityHandler
{
public:
explicit EditorAccessibilityHandler (TextEditor& textEditorToWrap)
: AccessibilityHandler (textEditorToWrap,
textEditorToWrap.isReadOnly() ? AccessibilityRole::staticText : AccessibilityRole::editableText,
{},
{ std::make_unique<TextEditorTextInterface> (textEditorToWrap) }),
textEditor (textEditorToWrap)
{
}
String getHelp() const override { return textEditor.getTooltip(); }
private:
class TextEditorTextInterface final : public AccessibilityTextInterface
{
public:
explicit TextEditorTextInterface (TextEditor& editor)
: textEditor (editor)
{
}
bool isDisplayingProtectedText() const override { return textEditor.getPasswordCharacter() != 0; }
bool isReadOnly() const override { return textEditor.isReadOnly(); }
int getTotalNumCharacters() const override { return textEditor.getText().length(); }
Range<int> getSelection() const override { return textEditor.getHighlightedRegion(); }
void setSelection (Range<int> r) override
{
textEditor.setHighlightedRegion (r);
}
String getText (Range<int> r) const override
{
if (isDisplayingProtectedText())
return String::repeatedString (String::charToString (textEditor.getPasswordCharacter()),
getTotalNumCharacters());
return textEditor.getTextInRange (r);
}
void setText (const String& newText) override
{
textEditor.setText (newText);
}
int getTextInsertionOffset() const override { return textEditor.getCaretPosition(); }
RectangleList<int> getTextBounds (Range<int> textRange) const override
{
auto localRects = textEditor.getTextBounds (textRange);
RectangleList<int> globalRects;
std::for_each (localRects.begin(), localRects.end(),
[&] (const Rectangle<int>& r) { globalRects.add (textEditor.localAreaToGlobal (r)); });
return globalRects;
}
int getOffsetAtPoint (Point<int> point) const override
{
return textEditor.getTextIndexAt (textEditor.getLocalPoint (nullptr, point));
}
private:
TextEditor& textEditor;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextEditorTextInterface)
};
TextEditor& textEditor;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EditorAccessibilityHandler)
};
std::unique_ptr<AccessibilityHandler> TextEditor::createAccessibilityHandler()
{
return std::make_unique<EditorAccessibilityHandler> (*this);
}
} // namespace juce