The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2500 lines
72KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. namespace juce
  20. {
  21. // a word or space that can't be broken down any further
  22. struct TextAtom
  23. {
  24. //==============================================================================
  25. String atomText;
  26. float width;
  27. int numChars;
  28. //==============================================================================
  29. bool isWhitespace() const noexcept { return CharacterFunctions::isWhitespace (atomText[0]); }
  30. bool isNewLine() const noexcept { return atomText[0] == '\r' || atomText[0] == '\n'; }
  31. String getText (juce_wchar passwordCharacter) const
  32. {
  33. if (passwordCharacter == 0)
  34. return atomText;
  35. return String::repeatedString (String::charToString (passwordCharacter),
  36. atomText.length());
  37. }
  38. String getTrimmedText (const juce_wchar passwordCharacter) const
  39. {
  40. if (passwordCharacter == 0)
  41. return atomText.substring (0, numChars);
  42. if (isNewLine())
  43. return {};
  44. return String::repeatedString (String::charToString (passwordCharacter), numChars);
  45. }
  46. JUCE_LEAK_DETECTOR (TextAtom)
  47. };
  48. //==============================================================================
  49. // a run of text with a single font and colour
  50. class TextEditor::UniformTextSection
  51. {
  52. public:
  53. UniformTextSection (const String& text, const Font& f, Colour col, juce_wchar passwordChar)
  54. : font (f), colour (col)
  55. {
  56. initialiseAtoms (text, passwordChar);
  57. }
  58. UniformTextSection (const UniformTextSection&) = default;
  59. UniformTextSection (UniformTextSection&&) = default;
  60. UniformTextSection& operator= (const UniformTextSection&) = delete;
  61. void append (UniformTextSection& other, const juce_wchar passwordChar)
  62. {
  63. if (! other.atoms.isEmpty())
  64. {
  65. int i = 0;
  66. if (! atoms.isEmpty())
  67. {
  68. auto& lastAtom = atoms.getReference (atoms.size() - 1);
  69. if (! CharacterFunctions::isWhitespace (lastAtom.atomText.getLastCharacter()))
  70. {
  71. auto& first = other.atoms.getReference(0);
  72. if (! CharacterFunctions::isWhitespace (first.atomText[0]))
  73. {
  74. lastAtom.atomText += first.atomText;
  75. lastAtom.numChars = (uint16) (lastAtom.numChars + first.numChars);
  76. lastAtom.width = font.getStringWidthFloat (lastAtom.getText (passwordChar));
  77. ++i;
  78. }
  79. }
  80. }
  81. atoms.ensureStorageAllocated (atoms.size() + other.atoms.size() - i);
  82. while (i < other.atoms.size())
  83. {
  84. atoms.add (other.atoms.getReference(i));
  85. ++i;
  86. }
  87. }
  88. }
  89. UniformTextSection* split (int indexToBreakAt, juce_wchar passwordChar)
  90. {
  91. auto* section2 = new UniformTextSection (String(), font, colour, passwordChar);
  92. int index = 0;
  93. for (int i = 0; i < atoms.size(); ++i)
  94. {
  95. auto& atom = atoms.getReference(i);
  96. auto nextIndex = index + atom.numChars;
  97. if (index == indexToBreakAt)
  98. {
  99. for (int j = i; j < atoms.size(); ++j)
  100. section2->atoms.add (atoms.getUnchecked (j));
  101. atoms.removeRange (i, atoms.size());
  102. break;
  103. }
  104. if (indexToBreakAt >= index && indexToBreakAt < nextIndex)
  105. {
  106. TextAtom secondAtom;
  107. secondAtom.atomText = atom.atomText.substring (indexToBreakAt - index);
  108. secondAtom.width = font.getStringWidthFloat (secondAtom.getText (passwordChar));
  109. secondAtom.numChars = (uint16) secondAtom.atomText.length();
  110. section2->atoms.add (secondAtom);
  111. atom.atomText = atom.atomText.substring (0, indexToBreakAt - index);
  112. atom.width = font.getStringWidthFloat (atom.getText (passwordChar));
  113. atom.numChars = (uint16) (indexToBreakAt - index);
  114. for (int j = i + 1; j < atoms.size(); ++j)
  115. section2->atoms.add (atoms.getUnchecked (j));
  116. atoms.removeRange (i + 1, atoms.size());
  117. break;
  118. }
  119. index = nextIndex;
  120. }
  121. return section2;
  122. }
  123. void appendAllText (MemoryOutputStream& mo) const
  124. {
  125. for (auto& atom : atoms)
  126. mo << atom.atomText;
  127. }
  128. void appendSubstring (MemoryOutputStream& mo, Range<int> range) const
  129. {
  130. int index = 0;
  131. for (auto& atom : atoms)
  132. {
  133. auto nextIndex = index + atom.numChars;
  134. if (range.getStart() < nextIndex)
  135. {
  136. if (range.getEnd() <= index)
  137. break;
  138. auto r = (range - index).getIntersectionWith ({ 0, (int) atom.numChars });
  139. if (! r.isEmpty())
  140. mo << atom.atomText.substring (r.getStart(), r.getEnd());
  141. }
  142. index = nextIndex;
  143. }
  144. }
  145. int getTotalLength() const noexcept
  146. {
  147. int total = 0;
  148. for (auto& atom : atoms)
  149. total += atom.numChars;
  150. return total;
  151. }
  152. void setFont (const Font& newFont, const juce_wchar passwordChar)
  153. {
  154. if (font != newFont)
  155. {
  156. font = newFont;
  157. for (auto& atom : atoms)
  158. atom.width = newFont.getStringWidthFloat (atom.getText (passwordChar));
  159. }
  160. }
  161. //==============================================================================
  162. Font font;
  163. Colour colour;
  164. Array<TextAtom> atoms;
  165. private:
  166. void initialiseAtoms (const String& textToParse, const juce_wchar passwordChar)
  167. {
  168. auto text = textToParse.getCharPointer();
  169. while (! text.isEmpty())
  170. {
  171. size_t numChars = 0;
  172. auto start = text;
  173. // create a whitespace atom unless it starts with non-ws
  174. if (text.isWhitespace() && *text != '\r' && *text != '\n')
  175. {
  176. do
  177. {
  178. ++text;
  179. ++numChars;
  180. }
  181. while (text.isWhitespace() && *text != '\r' && *text != '\n');
  182. }
  183. else
  184. {
  185. if (*text == '\r')
  186. {
  187. ++text;
  188. ++numChars;
  189. if (*text == '\n')
  190. {
  191. ++start;
  192. ++text;
  193. }
  194. }
  195. else if (*text == '\n')
  196. {
  197. ++text;
  198. ++numChars;
  199. }
  200. else
  201. {
  202. while (! (text.isEmpty() || text.isWhitespace()))
  203. {
  204. ++text;
  205. ++numChars;
  206. }
  207. }
  208. }
  209. TextAtom atom;
  210. atom.atomText = String (start, numChars);
  211. atom.width = font.getStringWidthFloat (atom.getText (passwordChar));
  212. atom.numChars = (uint16) numChars;
  213. atoms.add (atom);
  214. }
  215. }
  216. JUCE_LEAK_DETECTOR (UniformTextSection)
  217. };
  218. //==============================================================================
  219. struct TextEditor::Iterator
  220. {
  221. Iterator (const TextEditor& ed)
  222. : sections (ed.sections),
  223. justification (ed.justification),
  224. justificationWidth (ed.getJustificationWidth()),
  225. wordWrapWidth (ed.getWordWrapWidth()),
  226. passwordCharacter (ed.passwordCharacter),
  227. lineSpacing (ed.lineSpacing)
  228. {
  229. jassert (wordWrapWidth > 0);
  230. if (! sections.isEmpty())
  231. {
  232. currentSection = sections.getUnchecked (sectionIndex);
  233. if (currentSection != nullptr)
  234. beginNewLine();
  235. }
  236. }
  237. Iterator (const Iterator&) = default;
  238. Iterator& operator= (const Iterator&) = delete;
  239. //==============================================================================
  240. bool next()
  241. {
  242. if (atom == &tempAtom)
  243. {
  244. auto numRemaining = tempAtom.atomText.length() - tempAtom.numChars;
  245. if (numRemaining > 0)
  246. {
  247. tempAtom.atomText = tempAtom.atomText.substring (tempAtom.numChars);
  248. if (tempAtom.numChars > 0)
  249. lineY += lineHeight * lineSpacing;
  250. indexInText += tempAtom.numChars;
  251. GlyphArrangement g;
  252. g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f);
  253. int split;
  254. for (split = 0; split < g.getNumGlyphs(); ++split)
  255. if (shouldWrap (g.getGlyph (split).getRight()))
  256. break;
  257. if (split > 0 && split <= numRemaining)
  258. {
  259. tempAtom.numChars = (uint16) split;
  260. tempAtom.width = g.getGlyph (split - 1).getRight();
  261. atomX = getJustificationOffset (tempAtom.width);
  262. atomRight = atomX + tempAtom.width;
  263. return true;
  264. }
  265. }
  266. }
  267. if (sectionIndex >= sections.size())
  268. {
  269. moveToEndOfLastAtom();
  270. return false;
  271. }
  272. bool forceNewLine = false;
  273. if (atomIndex >= currentSection->atoms.size() - 1)
  274. {
  275. if (atomIndex >= currentSection->atoms.size())
  276. {
  277. if (++sectionIndex >= sections.size())
  278. {
  279. moveToEndOfLastAtom();
  280. return false;
  281. }
  282. atomIndex = 0;
  283. currentSection = sections.getUnchecked (sectionIndex);
  284. }
  285. else
  286. {
  287. auto& lastAtom = currentSection->atoms.getReference (atomIndex);
  288. if (! lastAtom.isWhitespace())
  289. {
  290. // handle the case where the last atom in a section is actually part of the same
  291. // word as the first atom of the next section...
  292. float right = atomRight + lastAtom.width;
  293. float lineHeight2 = lineHeight;
  294. float maxDescent2 = maxDescent;
  295. for (int section = sectionIndex + 1; section < sections.size(); ++section)
  296. {
  297. auto* s = sections.getUnchecked (section);
  298. if (s->atoms.size() == 0)
  299. break;
  300. auto& nextAtom = s->atoms.getReference (0);
  301. if (nextAtom.isWhitespace())
  302. break;
  303. right += nextAtom.width;
  304. lineHeight2 = jmax (lineHeight2, s->font.getHeight());
  305. maxDescent2 = jmax (maxDescent2, s->font.getDescent());
  306. if (shouldWrap (right))
  307. {
  308. lineHeight = lineHeight2;
  309. maxDescent = maxDescent2;
  310. forceNewLine = true;
  311. break;
  312. }
  313. if (s->atoms.size() > 1)
  314. break;
  315. }
  316. }
  317. }
  318. }
  319. if (atom != nullptr)
  320. {
  321. atomX = atomRight;
  322. indexInText += atom->numChars;
  323. if (atom->isNewLine())
  324. beginNewLine();
  325. }
  326. atom = &(currentSection->atoms.getReference (atomIndex));
  327. atomRight = atomX + atom->width;
  328. ++atomIndex;
  329. if (shouldWrap (atomRight) || forceNewLine)
  330. {
  331. if (atom->isWhitespace())
  332. {
  333. // leave whitespace at the end of a line, but truncate it to avoid scrolling
  334. atomRight = jmin (atomRight, wordWrapWidth);
  335. }
  336. else
  337. {
  338. if (shouldWrap (atom->width)) // atom too big to fit on a line, so break it up..
  339. {
  340. tempAtom = *atom;
  341. tempAtom.width = 0;
  342. tempAtom.numChars = 0;
  343. atom = &tempAtom;
  344. if (atomX > justificationOffset)
  345. beginNewLine();
  346. return next();
  347. }
  348. beginNewLine();
  349. atomX = justificationOffset;
  350. atomRight = atomX + atom->width;
  351. return true;
  352. }
  353. }
  354. return true;
  355. }
  356. void beginNewLine()
  357. {
  358. lineY += lineHeight * lineSpacing;
  359. float lineWidth = 0;
  360. auto tempSectionIndex = sectionIndex;
  361. auto tempAtomIndex = atomIndex;
  362. auto* section = sections.getUnchecked (tempSectionIndex);
  363. lineHeight = section->font.getHeight();
  364. maxDescent = section->font.getDescent();
  365. float nextLineWidth = (atom != nullptr) ? atom->width : 0.0f;
  366. while (! shouldWrap (nextLineWidth))
  367. {
  368. lineWidth = nextLineWidth;
  369. if (tempSectionIndex >= sections.size())
  370. break;
  371. bool checkSize = false;
  372. if (tempAtomIndex >= section->atoms.size())
  373. {
  374. if (++tempSectionIndex >= sections.size())
  375. break;
  376. tempAtomIndex = 0;
  377. section = sections.getUnchecked (tempSectionIndex);
  378. checkSize = true;
  379. }
  380. if (! isPositiveAndBelow (tempAtomIndex, section->atoms.size()))
  381. break;
  382. auto& nextAtom = section->atoms.getReference (tempAtomIndex);
  383. nextLineWidth += nextAtom.width;
  384. if (shouldWrap (nextLineWidth) || nextAtom.isNewLine())
  385. break;
  386. if (checkSize)
  387. {
  388. lineHeight = jmax (lineHeight, section->font.getHeight());
  389. maxDescent = jmax (maxDescent, section->font.getDescent());
  390. }
  391. ++tempAtomIndex;
  392. }
  393. justificationOffset = getJustificationOffset (lineWidth);
  394. atomX = justificationOffset;
  395. }
  396. float getJustificationOffset (float lineWidth) const
  397. {
  398. if (justification.getOnlyHorizontalFlags() == Justification::horizontallyCentred)
  399. return jmax (0.0f, (justificationWidth - lineWidth) * 0.5f);
  400. if (justification.getOnlyHorizontalFlags() == Justification::right)
  401. return jmax (0.0f, justificationWidth - lineWidth);
  402. return 0;
  403. }
  404. //==============================================================================
  405. void draw (Graphics& g, const UniformTextSection*& lastSection) const
  406. {
  407. if (passwordCharacter != 0 || ! atom->isWhitespace())
  408. {
  409. if (lastSection != currentSection)
  410. {
  411. lastSection = currentSection;
  412. g.setColour (currentSection->colour);
  413. g.setFont (currentSection->font);
  414. }
  415. jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty());
  416. GlyphArrangement ga;
  417. ga.addLineOfText (currentSection->font,
  418. atom->getTrimmedText (passwordCharacter),
  419. atomX, (float) roundToInt (lineY + lineHeight - maxDescent));
  420. ga.draw (g);
  421. }
  422. }
  423. void addSelection (RectangleList<float>& area, Range<int> selected) const
  424. {
  425. auto startX = indexToX (selected.getStart());
  426. auto endX = indexToX (selected.getEnd());
  427. area.add (startX, lineY, endX - startX, lineHeight * lineSpacing);
  428. }
  429. void drawUnderline (Graphics& g, Range<int> underline, Colour colour) const
  430. {
  431. auto startX = roundToInt (indexToX (underline.getStart()));
  432. auto endX = roundToInt (indexToX (underline.getEnd()));
  433. auto baselineY = roundToInt (lineY + currentSection->font.getAscent() + 0.5f);
  434. Graphics::ScopedSaveState state (g);
  435. g.reduceClipRegion ({ startX, baselineY, endX - startX, 1 });
  436. g.fillCheckerBoard ({ endX, baselineY + 1 }, 3, 1, colour, Colours::transparentBlack);
  437. }
  438. void drawSelectedText (Graphics& g, Range<int> selected, Colour selectedTextColour) const
  439. {
  440. if (passwordCharacter != 0 || ! atom->isWhitespace())
  441. {
  442. GlyphArrangement ga;
  443. ga.addLineOfText (currentSection->font,
  444. atom->getTrimmedText (passwordCharacter),
  445. atomX, (float) roundToInt (lineY + lineHeight - maxDescent));
  446. if (selected.getEnd() < indexInText + atom->numChars)
  447. {
  448. GlyphArrangement ga2 (ga);
  449. ga2.removeRangeOfGlyphs (0, selected.getEnd() - indexInText);
  450. ga.removeRangeOfGlyphs (selected.getEnd() - indexInText, -1);
  451. g.setColour (currentSection->colour);
  452. ga2.draw (g);
  453. }
  454. if (selected.getStart() > indexInText)
  455. {
  456. GlyphArrangement ga2 (ga);
  457. ga2.removeRangeOfGlyphs (selected.getStart() - indexInText, -1);
  458. ga.removeRangeOfGlyphs (0, selected.getStart() - indexInText);
  459. g.setColour (currentSection->colour);
  460. ga2.draw (g);
  461. }
  462. g.setColour (selectedTextColour);
  463. ga.draw (g);
  464. }
  465. }
  466. //==============================================================================
  467. float indexToX (int indexToFind) const
  468. {
  469. if (indexToFind <= indexInText)
  470. return atomX;
  471. if (indexToFind >= indexInText + atom->numChars)
  472. return atomRight;
  473. GlyphArrangement g;
  474. g.addLineOfText (currentSection->font,
  475. atom->getText (passwordCharacter),
  476. atomX, 0.0f);
  477. if (indexToFind - indexInText >= g.getNumGlyphs())
  478. return atomRight;
  479. return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft());
  480. }
  481. int xToIndex (float xToFind) const
  482. {
  483. if (xToFind <= atomX || atom->isNewLine())
  484. return indexInText;
  485. if (xToFind >= atomRight)
  486. return indexInText + atom->numChars;
  487. GlyphArrangement g;
  488. g.addLineOfText (currentSection->font,
  489. atom->getText (passwordCharacter),
  490. atomX, 0.0f);
  491. auto numGlyphs = g.getNumGlyphs();
  492. int j;
  493. for (j = 0; j < numGlyphs; ++j)
  494. {
  495. auto& pg = g.getGlyph(j);
  496. if ((pg.getLeft() + pg.getRight()) / 2 > xToFind)
  497. break;
  498. }
  499. return indexInText + j;
  500. }
  501. //==============================================================================
  502. bool getCharPosition (int index, Point<float>& anchor, float& lineHeightFound)
  503. {
  504. while (next())
  505. {
  506. if (indexInText + atom->numChars > index)
  507. {
  508. anchor = { indexToX (index), lineY };
  509. lineHeightFound = lineHeight;
  510. return true;
  511. }
  512. }
  513. anchor = { atomX, lineY };
  514. lineHeightFound = lineHeight;
  515. return false;
  516. }
  517. //==============================================================================
  518. int indexInText = 0;
  519. float lineY = 0, justificationOffset = 0, lineHeight = 0, maxDescent = 0;
  520. float atomX = 0, atomRight = 0;
  521. const TextAtom* atom = nullptr;
  522. const UniformTextSection* currentSection = nullptr;
  523. private:
  524. const OwnedArray<UniformTextSection>& sections;
  525. int sectionIndex = 0, atomIndex = 0;
  526. Justification justification;
  527. const float justificationWidth, wordWrapWidth;
  528. const juce_wchar passwordCharacter;
  529. const float lineSpacing;
  530. TextAtom tempAtom;
  531. void moveToEndOfLastAtom()
  532. {
  533. if (atom != nullptr)
  534. {
  535. atomX = atomRight;
  536. if (atom->isNewLine())
  537. {
  538. atomX = 0.0f;
  539. lineY += lineHeight * lineSpacing;
  540. }
  541. }
  542. }
  543. bool shouldWrap (const float x) const noexcept
  544. {
  545. return (x - 0.0001f) >= wordWrapWidth;
  546. }
  547. JUCE_LEAK_DETECTOR (Iterator)
  548. };
  549. //==============================================================================
  550. struct TextEditor::InsertAction : public UndoableAction
  551. {
  552. InsertAction (TextEditor& ed, const String& newText, int insertPos,
  553. const Font& newFont, Colour newColour, int oldCaret, int newCaret)
  554. : owner (ed),
  555. text (newText),
  556. insertIndex (insertPos),
  557. oldCaretPos (oldCaret),
  558. newCaretPos (newCaret),
  559. font (newFont),
  560. colour (newColour)
  561. {
  562. }
  563. bool perform() override
  564. {
  565. owner.insert (text, insertIndex, font, colour, 0, newCaretPos);
  566. return true;
  567. }
  568. bool undo() override
  569. {
  570. owner.remove ({ insertIndex, insertIndex + text.length() }, 0, oldCaretPos);
  571. return true;
  572. }
  573. int getSizeInUnits() override
  574. {
  575. return text.length() + 16;
  576. }
  577. private:
  578. TextEditor& owner;
  579. const String text;
  580. const int insertIndex, oldCaretPos, newCaretPos;
  581. const Font font;
  582. const Colour colour;
  583. JUCE_DECLARE_NON_COPYABLE (InsertAction)
  584. };
  585. //==============================================================================
  586. struct TextEditor::RemoveAction : public UndoableAction
  587. {
  588. RemoveAction (TextEditor& ed, Range<int> rangeToRemove, int oldCaret, int newCaret,
  589. const Array<UniformTextSection*>& oldSections)
  590. : owner (ed),
  591. range (rangeToRemove),
  592. oldCaretPos (oldCaret),
  593. newCaretPos (newCaret)
  594. {
  595. removedSections.addArray (oldSections);
  596. }
  597. bool perform() override
  598. {
  599. owner.remove (range, 0, newCaretPos);
  600. return true;
  601. }
  602. bool undo() override
  603. {
  604. owner.reinsert (range.getStart(), removedSections);
  605. owner.moveCaretTo (oldCaretPos, false);
  606. return true;
  607. }
  608. int getSizeInUnits() override
  609. {
  610. int n = 16;
  611. for (auto* s : removedSections)
  612. n += s->getTotalLength();
  613. return n;
  614. }
  615. private:
  616. TextEditor& owner;
  617. const Range<int> range;
  618. const int oldCaretPos, newCaretPos;
  619. OwnedArray<UniformTextSection> removedSections;
  620. JUCE_DECLARE_NON_COPYABLE (RemoveAction)
  621. };
  622. //==============================================================================
  623. struct TextEditor::TextHolderComponent : public Component,
  624. public Timer,
  625. public Value::Listener
  626. {
  627. TextHolderComponent (TextEditor& ed) : owner (ed)
  628. {
  629. setWantsKeyboardFocus (false);
  630. setInterceptsMouseClicks (false, true);
  631. setMouseCursor (MouseCursor::ParentCursor);
  632. owner.getTextValue().addListener (this);
  633. }
  634. ~TextHolderComponent()
  635. {
  636. owner.getTextValue().removeListener (this);
  637. }
  638. void paint (Graphics& g) override
  639. {
  640. owner.drawContent (g);
  641. }
  642. void restartTimer()
  643. {
  644. startTimer (350);
  645. }
  646. void timerCallback() override
  647. {
  648. owner.timerCallbackInt();
  649. }
  650. void valueChanged (Value&) override
  651. {
  652. owner.textWasChangedByValue();
  653. }
  654. TextEditor& owner;
  655. JUCE_DECLARE_NON_COPYABLE (TextHolderComponent)
  656. };
  657. //==============================================================================
  658. struct TextEditor::TextEditorViewport : public Viewport
  659. {
  660. TextEditorViewport (TextEditor& ed) : owner (ed) {}
  661. void visibleAreaChanged (const Rectangle<int>&) override
  662. {
  663. if (! rentrant) // it's rare, but possible to get into a feedback loop as the viewport's scrollbars
  664. // appear and disappear, causing the wrap width to change.
  665. {
  666. auto wordWrapWidth = owner.getWordWrapWidth();
  667. if (wordWrapWidth != lastWordWrapWidth)
  668. {
  669. lastWordWrapWidth = wordWrapWidth;
  670. rentrant = true;
  671. owner.updateTextHolderSize();
  672. rentrant = false;
  673. }
  674. }
  675. }
  676. private:
  677. TextEditor& owner;
  678. float lastWordWrapWidth = 0;
  679. bool rentrant = false;
  680. JUCE_DECLARE_NON_COPYABLE (TextEditorViewport)
  681. };
  682. //==============================================================================
  683. namespace TextEditorDefs
  684. {
  685. const int textChangeMessageId = 0x10003001;
  686. const int returnKeyMessageId = 0x10003002;
  687. const int escapeKeyMessageId = 0x10003003;
  688. const int focusLossMessageId = 0x10003004;
  689. const int maxActionsPerTransaction = 100;
  690. static int getCharacterCategory (juce_wchar character) noexcept
  691. {
  692. return CharacterFunctions::isLetterOrDigit (character)
  693. ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1);
  694. }
  695. }
  696. //==============================================================================
  697. TextEditor::TextEditor (const String& name, juce_wchar passwordChar)
  698. : Component (name),
  699. passwordCharacter (passwordChar)
  700. {
  701. setMouseCursor (MouseCursor::IBeamCursor);
  702. addAndMakeVisible (viewport = new TextEditorViewport (*this));
  703. viewport->setViewedComponent (textHolder = new TextHolderComponent (*this));
  704. viewport->setWantsKeyboardFocus (false);
  705. viewport->setScrollBarsShown (false, false);
  706. setWantsKeyboardFocus (true);
  707. recreateCaret();
  708. }
  709. TextEditor::~TextEditor()
  710. {
  711. if (wasFocused)
  712. if (auto* peer = getPeer())
  713. peer->dismissPendingTextInput();
  714. textValue.removeListener (textHolder);
  715. textValue.referTo (Value());
  716. viewport = nullptr;
  717. textHolder = nullptr;
  718. }
  719. //==============================================================================
  720. void TextEditor::newTransaction()
  721. {
  722. lastTransactionTime = Time::getApproximateMillisecondCounter();
  723. undoManager.beginNewTransaction();
  724. }
  725. bool TextEditor::undoOrRedo (const bool shouldUndo)
  726. {
  727. if (! isReadOnly())
  728. {
  729. newTransaction();
  730. if (shouldUndo ? undoManager.undo()
  731. : undoManager.redo())
  732. {
  733. scrollToMakeSureCursorIsVisible();
  734. repaint();
  735. textChanged();
  736. return true;
  737. }
  738. }
  739. return false;
  740. }
  741. bool TextEditor::undo() { return undoOrRedo (true); }
  742. bool TextEditor::redo() { return undoOrRedo (false); }
  743. //==============================================================================
  744. void TextEditor::setMultiLine (const bool shouldBeMultiLine,
  745. const bool shouldWordWrap)
  746. {
  747. if (multiline != shouldBeMultiLine
  748. || wordWrap != (shouldWordWrap && shouldBeMultiLine))
  749. {
  750. multiline = shouldBeMultiLine;
  751. wordWrap = shouldWordWrap && shouldBeMultiLine;
  752. viewport->setScrollBarsShown (scrollbarVisible && multiline,
  753. scrollbarVisible && multiline);
  754. viewport->setViewPosition (0, 0);
  755. resized();
  756. scrollToMakeSureCursorIsVisible();
  757. }
  758. }
  759. bool TextEditor::isMultiLine() const
  760. {
  761. return multiline;
  762. }
  763. void TextEditor::setScrollbarsShown (bool shown)
  764. {
  765. if (scrollbarVisible != shown)
  766. {
  767. scrollbarVisible = shown;
  768. shown = shown && isMultiLine();
  769. viewport->setScrollBarsShown (shown, shown);
  770. }
  771. }
  772. void TextEditor::setReadOnly (bool shouldBeReadOnly)
  773. {
  774. if (readOnly != shouldBeReadOnly)
  775. {
  776. readOnly = shouldBeReadOnly;
  777. enablementChanged();
  778. }
  779. }
  780. bool TextEditor::isReadOnly() const noexcept
  781. {
  782. return readOnly || ! isEnabled();
  783. }
  784. bool TextEditor::isTextInputActive() const
  785. {
  786. return ! isReadOnly();
  787. }
  788. void TextEditor::setReturnKeyStartsNewLine (bool shouldStartNewLine)
  789. {
  790. returnKeyStartsNewLine = shouldStartNewLine;
  791. }
  792. void TextEditor::setTabKeyUsedAsCharacter (bool shouldTabKeyBeUsed)
  793. {
  794. tabKeyUsed = shouldTabKeyBeUsed;
  795. }
  796. void TextEditor::setPopupMenuEnabled (bool b)
  797. {
  798. popupMenuEnabled = b;
  799. }
  800. void TextEditor::setSelectAllWhenFocused (bool b)
  801. {
  802. selectAllTextWhenFocused = b;
  803. }
  804. void TextEditor::setJustification (Justification j)
  805. {
  806. if (justification != j)
  807. {
  808. justification = j;
  809. resized();
  810. }
  811. }
  812. //==============================================================================
  813. void TextEditor::setFont (const Font& newFont)
  814. {
  815. currentFont = newFont;
  816. scrollToMakeSureCursorIsVisible();
  817. }
  818. void TextEditor::applyFontToAllText (const Font& newFont, bool changeCurrentFont)
  819. {
  820. if (changeCurrentFont)
  821. currentFont = newFont;
  822. auto overallColour = findColour (textColourId);
  823. for (auto* uts : sections)
  824. {
  825. uts->setFont (newFont, passwordCharacter);
  826. uts->colour = overallColour;
  827. }
  828. coalesceSimilarSections();
  829. updateTextHolderSize();
  830. scrollToMakeSureCursorIsVisible();
  831. repaint();
  832. }
  833. void TextEditor::applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour)
  834. {
  835. for (auto* uts : sections)
  836. uts->colour = newColour;
  837. if (changeCurrentTextColour)
  838. setColour (TextEditor::textColourId, newColour);
  839. else
  840. repaint();
  841. }
  842. void TextEditor::lookAndFeelChanged()
  843. {
  844. caret = nullptr;
  845. recreateCaret();
  846. repaint();
  847. }
  848. void TextEditor::parentHierarchyChanged()
  849. {
  850. lookAndFeelChanged();
  851. }
  852. void TextEditor::enablementChanged()
  853. {
  854. recreateCaret();
  855. repaint();
  856. }
  857. void TextEditor::setCaretVisible (bool shouldCaretBeVisible)
  858. {
  859. if (caretVisible != shouldCaretBeVisible)
  860. {
  861. caretVisible = shouldCaretBeVisible;
  862. recreateCaret();
  863. }
  864. }
  865. void TextEditor::recreateCaret()
  866. {
  867. if (isCaretVisible())
  868. {
  869. if (caret == nullptr)
  870. {
  871. textHolder->addChildComponent (caret = getLookAndFeel().createCaretComponent (this));
  872. updateCaretPosition();
  873. }
  874. }
  875. else
  876. {
  877. caret = nullptr;
  878. }
  879. }
  880. void TextEditor::updateCaretPosition()
  881. {
  882. if (caret != nullptr)
  883. caret->setCaretPosition (getCaretRectangle().translated (leftIndent, topIndent));
  884. }
  885. TextEditor::LengthAndCharacterRestriction::LengthAndCharacterRestriction (int maxLen, const String& chars)
  886. : allowedCharacters (chars), maxLength (maxLen)
  887. {
  888. }
  889. String TextEditor::LengthAndCharacterRestriction::filterNewText (TextEditor& ed, const String& newInput)
  890. {
  891. String t (newInput);
  892. if (allowedCharacters.isNotEmpty())
  893. t = t.retainCharacters (allowedCharacters);
  894. if (maxLength > 0)
  895. t = t.substring (0, maxLength - (ed.getTotalNumChars() - ed.getHighlightedRegion().getLength()));
  896. return t;
  897. }
  898. void TextEditor::setInputFilter (InputFilter* newFilter, bool takeOwnership)
  899. {
  900. inputFilter.set (newFilter, takeOwnership);
  901. }
  902. void TextEditor::setInputRestrictions (int maxLen, const String& chars)
  903. {
  904. setInputFilter (new LengthAndCharacterRestriction (maxLen, chars), true);
  905. }
  906. void TextEditor::setTextToShowWhenEmpty (const String& text, Colour colourToUse)
  907. {
  908. textToShowWhenEmpty = text;
  909. colourForTextWhenEmpty = colourToUse;
  910. }
  911. void TextEditor::setPasswordCharacter (juce_wchar newPasswordCharacter)
  912. {
  913. if (passwordCharacter != newPasswordCharacter)
  914. {
  915. passwordCharacter = newPasswordCharacter;
  916. applyFontToAllText (currentFont);
  917. }
  918. }
  919. void TextEditor::setScrollBarThickness (int newThicknessPixels)
  920. {
  921. viewport->setScrollBarThickness (newThicknessPixels);
  922. }
  923. //==============================================================================
  924. void TextEditor::clear()
  925. {
  926. clearInternal (nullptr);
  927. updateTextHolderSize();
  928. undoManager.clearUndoHistory();
  929. }
  930. void TextEditor::setText (const String& newText, bool sendTextChangeMessage)
  931. {
  932. auto newLength = newText.length();
  933. if (newLength != getTotalNumChars() || getText() != newText)
  934. {
  935. if (! sendTextChangeMessage)
  936. textValue.removeListener (textHolder);
  937. textValue = newText;
  938. auto oldCursorPos = caretPosition;
  939. bool cursorWasAtEnd = oldCursorPos >= getTotalNumChars();
  940. clearInternal (nullptr);
  941. insert (newText, 0, currentFont, findColour (textColourId), 0, caretPosition);
  942. // if you're adding text with line-feeds to a single-line text editor, it
  943. // ain't gonna look right!
  944. jassert (multiline || ! newText.containsAnyOf ("\r\n"));
  945. if (cursorWasAtEnd && ! isMultiLine())
  946. oldCursorPos = getTotalNumChars();
  947. moveCaretTo (oldCursorPos, false);
  948. if (sendTextChangeMessage)
  949. textChanged();
  950. else
  951. textValue.addListener (textHolder);
  952. updateTextHolderSize();
  953. scrollToMakeSureCursorIsVisible();
  954. undoManager.clearUndoHistory();
  955. repaint();
  956. }
  957. }
  958. //==============================================================================
  959. void TextEditor::updateValueFromText()
  960. {
  961. if (valueTextNeedsUpdating)
  962. {
  963. valueTextNeedsUpdating = false;
  964. textValue = getText();
  965. }
  966. }
  967. Value& TextEditor::getTextValue()
  968. {
  969. updateValueFromText();
  970. return textValue;
  971. }
  972. void TextEditor::textWasChangedByValue()
  973. {
  974. if (textValue.getValueSource().getReferenceCount() > 1)
  975. setText (textValue.getValue());
  976. }
  977. //==============================================================================
  978. void TextEditor::textChanged()
  979. {
  980. updateTextHolderSize();
  981. if (listeners.size() > 0)
  982. postCommandMessage (TextEditorDefs::textChangeMessageId);
  983. if (textValue.getValueSource().getReferenceCount() > 1)
  984. {
  985. valueTextNeedsUpdating = false;
  986. textValue = getText();
  987. }
  988. }
  989. void TextEditor::returnPressed() { postCommandMessage (TextEditorDefs::returnKeyMessageId); }
  990. void TextEditor::escapePressed() { postCommandMessage (TextEditorDefs::escapeKeyMessageId); }
  991. void TextEditor::addListener (Listener* l) { listeners.add (l); }
  992. void TextEditor::removeListener (Listener* l) { listeners.remove (l); }
  993. //==============================================================================
  994. void TextEditor::timerCallbackInt()
  995. {
  996. if (hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent())
  997. wasFocused = true;
  998. auto now = Time::getApproximateMillisecondCounter();
  999. if (now > lastTransactionTime + 200)
  1000. newTransaction();
  1001. }
  1002. void TextEditor::repaintText (Range<int> range)
  1003. {
  1004. if (! range.isEmpty())
  1005. {
  1006. auto lh = currentFont.getHeight();
  1007. auto wordWrapWidth = getWordWrapWidth();
  1008. if (wordWrapWidth > 0)
  1009. {
  1010. Point<float> anchor;
  1011. Iterator i (*this);
  1012. i.getCharPosition (range.getStart(), anchor, lh);
  1013. auto y1 = (int) anchor.y;
  1014. int y2;
  1015. if (range.getEnd() >= getTotalNumChars())
  1016. {
  1017. y2 = textHolder->getHeight();
  1018. }
  1019. else
  1020. {
  1021. i.getCharPosition (range.getEnd(), anchor, lh);
  1022. y2 = (int) (anchor.y + lh * 2.0f);
  1023. }
  1024. textHolder->repaint (0, y1, textHolder->getWidth(), y2 - y1);
  1025. }
  1026. }
  1027. }
  1028. //==============================================================================
  1029. void TextEditor::moveCaret (int newCaretPos)
  1030. {
  1031. if (newCaretPos < 0)
  1032. newCaretPos = 0;
  1033. else
  1034. newCaretPos = jmin (newCaretPos, getTotalNumChars());
  1035. if (newCaretPos != getCaretPosition())
  1036. {
  1037. caretPosition = newCaretPos;
  1038. textHolder->restartTimer();
  1039. scrollToMakeSureCursorIsVisible();
  1040. updateCaretPosition();
  1041. }
  1042. }
  1043. int TextEditor::getCaretPosition() const
  1044. {
  1045. return caretPosition;
  1046. }
  1047. void TextEditor::setCaretPosition (const int newIndex)
  1048. {
  1049. moveCaretTo (newIndex, false);
  1050. }
  1051. void TextEditor::moveCaretToEnd()
  1052. {
  1053. setCaretPosition (std::numeric_limits<int>::max());
  1054. }
  1055. void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX,
  1056. const int desiredCaretY)
  1057. {
  1058. updateCaretPosition();
  1059. auto caretPos = getCaretRectangle();
  1060. auto vx = caretPos.getX() - desiredCaretX;
  1061. auto vy = caretPos.getY() - desiredCaretY;
  1062. if (desiredCaretX < jmax (1, proportionOfWidth (0.05f)))
  1063. vx += desiredCaretX - proportionOfWidth (0.2f);
  1064. else if (desiredCaretX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
  1065. vx += desiredCaretX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
  1066. vx = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vx);
  1067. if (! isMultiLine())
  1068. {
  1069. vy = viewport->getViewPositionY();
  1070. }
  1071. else
  1072. {
  1073. vy = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vy);
  1074. if (desiredCaretY < 0)
  1075. vy = jmax (0, desiredCaretY + vy);
  1076. else if (desiredCaretY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretPos.getHeight()))
  1077. vy += desiredCaretY + 2 + caretPos.getHeight() + topIndent - viewport->getMaximumVisibleHeight();
  1078. }
  1079. viewport->setViewPosition (vx, vy);
  1080. }
  1081. Rectangle<int> TextEditor::getCaretRectangle()
  1082. {
  1083. return getCaretRectangleFloat().getSmallestIntegerContainer();
  1084. }
  1085. Rectangle<float> TextEditor::getCaretRectangleFloat() const
  1086. {
  1087. Point<float> anchor;
  1088. auto cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value)
  1089. getCharPosition (caretPosition, anchor, cursorHeight);
  1090. return { anchor.x, anchor.y, 2.0f, cursorHeight };
  1091. }
  1092. //==============================================================================
  1093. enum { rightEdgeSpace = 2 };
  1094. float TextEditor::getWordWrapWidth() const
  1095. {
  1096. return wordWrap ? getJustificationWidth()
  1097. : std::numeric_limits<float>::max();
  1098. }
  1099. float TextEditor::getJustificationWidth() const
  1100. {
  1101. return (float) (viewport->getMaximumVisibleWidth() - (leftIndent + rightEdgeSpace + 1));
  1102. }
  1103. void TextEditor::updateTextHolderSize()
  1104. {
  1105. if (getWordWrapWidth() > 0)
  1106. {
  1107. float maxWidth = getJustificationWidth();
  1108. Iterator i (*this);
  1109. while (i.next())
  1110. maxWidth = jmax (maxWidth, i.atomRight);
  1111. auto w = leftIndent + roundToInt (maxWidth);
  1112. auto h = topIndent + roundToInt (jmax (i.lineY + i.lineHeight, currentFont.getHeight()));
  1113. textHolder->setSize (w + rightEdgeSpace, h + 1); // (allows a bit of space for the cursor to be at the right-hand-edge)
  1114. }
  1115. }
  1116. int TextEditor::getTextWidth() const { return textHolder->getWidth(); }
  1117. int TextEditor::getTextHeight() const { return textHolder->getHeight(); }
  1118. void TextEditor::setIndents (int newLeftIndent, int newTopIndent)
  1119. {
  1120. leftIndent = newLeftIndent;
  1121. topIndent = newTopIndent;
  1122. }
  1123. void TextEditor::setBorder (const BorderSize<int>& border)
  1124. {
  1125. borderSize = border;
  1126. resized();
  1127. }
  1128. BorderSize<int> TextEditor::getBorder() const
  1129. {
  1130. return borderSize;
  1131. }
  1132. void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor)
  1133. {
  1134. keepCaretOnScreen = shouldScrollToShowCursor;
  1135. }
  1136. void TextEditor::scrollToMakeSureCursorIsVisible()
  1137. {
  1138. updateCaretPosition();
  1139. if (keepCaretOnScreen)
  1140. {
  1141. auto viewPos = viewport->getViewPosition();
  1142. auto caretRect = getCaretRectangle();
  1143. auto relativeCursor = caretRect.getPosition() - viewPos;
  1144. if (relativeCursor.x < jmax (1, proportionOfWidth (0.05f)))
  1145. {
  1146. viewPos.x += relativeCursor.x - proportionOfWidth (0.2f);
  1147. }
  1148. else if (relativeCursor.x > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
  1149. {
  1150. viewPos.x += relativeCursor.x + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
  1151. }
  1152. viewPos.x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), viewPos.x);
  1153. if (! isMultiLine())
  1154. {
  1155. viewPos.y = (getHeight() - textHolder->getHeight() - topIndent) / -2;
  1156. }
  1157. else if (relativeCursor.y < 0)
  1158. {
  1159. viewPos.y = jmax (0, relativeCursor.y + viewPos.y);
  1160. }
  1161. else if (relativeCursor.y > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretRect.getHeight()))
  1162. {
  1163. viewPos.y += relativeCursor.y + 2 + caretRect.getHeight() + topIndent - viewport->getMaximumVisibleHeight();
  1164. }
  1165. viewport->setViewPosition (viewPos);
  1166. }
  1167. }
  1168. void TextEditor::moveCaretTo (const int newPosition, const bool isSelecting)
  1169. {
  1170. if (isSelecting)
  1171. {
  1172. moveCaret (newPosition);
  1173. auto oldSelection = selection;
  1174. if (dragType == notDragging)
  1175. {
  1176. if (std::abs (getCaretPosition() - selection.getStart()) < std::abs (getCaretPosition() - selection.getEnd()))
  1177. dragType = draggingSelectionStart;
  1178. else
  1179. dragType = draggingSelectionEnd;
  1180. }
  1181. if (dragType == draggingSelectionStart)
  1182. {
  1183. if (getCaretPosition() >= selection.getEnd())
  1184. dragType = draggingSelectionEnd;
  1185. selection = Range<int>::between (getCaretPosition(), selection.getEnd());
  1186. }
  1187. else
  1188. {
  1189. if (getCaretPosition() < selection.getStart())
  1190. dragType = draggingSelectionStart;
  1191. selection = Range<int>::between (getCaretPosition(), selection.getStart());
  1192. }
  1193. repaintText (selection.getUnionWith (oldSelection));
  1194. }
  1195. else
  1196. {
  1197. dragType = notDragging;
  1198. repaintText (selection);
  1199. moveCaret (newPosition);
  1200. selection = Range<int>::emptyRange (getCaretPosition());
  1201. }
  1202. }
  1203. int TextEditor::getTextIndexAt (const int x, const int y)
  1204. {
  1205. return indexAtPosition ((float) (x + viewport->getViewPositionX() - leftIndent - borderSize.getLeft()),
  1206. (float) (y + viewport->getViewPositionY() - topIndent - borderSize.getTop()));
  1207. }
  1208. void TextEditor::insertTextAtCaret (const String& t)
  1209. {
  1210. String newText (inputFilter != nullptr ? inputFilter->filterNewText (*this, t) : t);
  1211. if (isMultiLine())
  1212. newText = newText.replace ("\r\n", "\n");
  1213. else
  1214. newText = newText.replaceCharacters ("\r\n", " ");
  1215. const int insertIndex = selection.getStart();
  1216. const int newCaretPos = insertIndex + newText.length();
  1217. remove (selection, getUndoManager(),
  1218. newText.isNotEmpty() ? newCaretPos - 1 : newCaretPos);
  1219. insert (newText, insertIndex, currentFont, findColour (textColourId),
  1220. getUndoManager(), newCaretPos);
  1221. textChanged();
  1222. }
  1223. void TextEditor::setHighlightedRegion (const Range<int>& newSelection)
  1224. {
  1225. moveCaretTo (newSelection.getStart(), false);
  1226. moveCaretTo (newSelection.getEnd(), true);
  1227. }
  1228. //==============================================================================
  1229. void TextEditor::copy()
  1230. {
  1231. if (passwordCharacter == 0)
  1232. {
  1233. auto selectedText = getHighlightedText();
  1234. if (selectedText.isNotEmpty())
  1235. SystemClipboard::copyTextToClipboard (selectedText);
  1236. }
  1237. }
  1238. void TextEditor::paste()
  1239. {
  1240. if (! isReadOnly())
  1241. {
  1242. auto clip = SystemClipboard::getTextFromClipboard();
  1243. if (clip.isNotEmpty())
  1244. insertTextAtCaret (clip);
  1245. }
  1246. }
  1247. void TextEditor::cut()
  1248. {
  1249. if (! isReadOnly())
  1250. {
  1251. moveCaret (selection.getEnd());
  1252. insertTextAtCaret (String());
  1253. }
  1254. }
  1255. //==============================================================================
  1256. void TextEditor::drawContent (Graphics& g)
  1257. {
  1258. if (getWordWrapWidth() > 0)
  1259. {
  1260. g.setOrigin (leftIndent, topIndent);
  1261. auto clip = g.getClipBounds();
  1262. Colour selectedTextColour;
  1263. Iterator i (*this);
  1264. if (! selection.isEmpty())
  1265. {
  1266. Iterator i2 (i);
  1267. RectangleList<float> selectionArea;
  1268. while (i2.next() && i2.lineY < clip.getBottom())
  1269. {
  1270. if (i2.lineY + i2.lineHeight >= clip.getY()
  1271. && selection.intersects ({ i2.indexInText, i2.indexInText + i2.atom->numChars }))
  1272. {
  1273. i2.addSelection (selectionArea, selection);
  1274. }
  1275. }
  1276. g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f));
  1277. g.fillRectList (selectionArea);
  1278. selectedTextColour = findColour (highlightedTextColourId);
  1279. }
  1280. const UniformTextSection* lastSection = nullptr;
  1281. while (i.next() && i.lineY < clip.getBottom())
  1282. {
  1283. if (i.lineY + i.lineHeight >= clip.getY())
  1284. {
  1285. if (selection.intersects ({ i.indexInText, i.indexInText + i.atom->numChars }))
  1286. {
  1287. i.drawSelectedText (g, selection, selectedTextColour);
  1288. lastSection = nullptr;
  1289. }
  1290. else
  1291. {
  1292. i.draw (g, lastSection);
  1293. }
  1294. }
  1295. }
  1296. for (auto& underlinedSection : underlinedSections)
  1297. {
  1298. Iterator i2 (*this);
  1299. while (i2.next() && i2.lineY < clip.getBottom())
  1300. {
  1301. if (i2.lineY + i2.lineHeight >= clip.getY()
  1302. && underlinedSection.intersects ({ i2.indexInText, i2.indexInText + i2.atom->numChars }))
  1303. {
  1304. i2.drawUnderline (g, underlinedSection, findColour (textColourId));
  1305. }
  1306. }
  1307. }
  1308. }
  1309. }
  1310. void TextEditor::paint (Graphics& g)
  1311. {
  1312. getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this);
  1313. }
  1314. void TextEditor::paintOverChildren (Graphics& g)
  1315. {
  1316. if (textToShowWhenEmpty.isNotEmpty()
  1317. && (! hasKeyboardFocus (false))
  1318. && getTotalNumChars() == 0)
  1319. {
  1320. g.setColour (colourForTextWhenEmpty);
  1321. g.setFont (getFont());
  1322. if (isMultiLine())
  1323. g.drawText (textToShowWhenEmpty, getLocalBounds(),
  1324. Justification::centred, true);
  1325. else
  1326. g.drawText (textToShowWhenEmpty,
  1327. leftIndent, 0, viewport->getWidth() - leftIndent, getHeight(),
  1328. Justification::centredLeft, true);
  1329. }
  1330. getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this);
  1331. }
  1332. //==============================================================================
  1333. void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*)
  1334. {
  1335. const bool writable = ! isReadOnly();
  1336. if (passwordCharacter == 0)
  1337. {
  1338. m.addItem (StandardApplicationCommandIDs::cut, TRANS("Cut"), writable);
  1339. m.addItem (StandardApplicationCommandIDs::copy, TRANS("Copy"), ! selection.isEmpty());
  1340. }
  1341. m.addItem (StandardApplicationCommandIDs::paste, TRANS("Paste"), writable);
  1342. m.addItem (StandardApplicationCommandIDs::del, TRANS("Delete"), writable);
  1343. m.addSeparator();
  1344. m.addItem (StandardApplicationCommandIDs::selectAll, TRANS("Select All"));
  1345. m.addSeparator();
  1346. if (getUndoManager() != nullptr)
  1347. {
  1348. m.addItem (StandardApplicationCommandIDs::undo, TRANS("Undo"), undoManager.canUndo());
  1349. m.addItem (StandardApplicationCommandIDs::redo, TRANS("Redo"), undoManager.canRedo());
  1350. }
  1351. }
  1352. void TextEditor::performPopupMenuAction (const int menuItemID)
  1353. {
  1354. switch (menuItemID)
  1355. {
  1356. case StandardApplicationCommandIDs::cut: cutToClipboard(); break;
  1357. case StandardApplicationCommandIDs::copy: copyToClipboard(); break;
  1358. case StandardApplicationCommandIDs::paste: pasteFromClipboard(); break;
  1359. case StandardApplicationCommandIDs::del: cut(); break;
  1360. case StandardApplicationCommandIDs::selectAll: selectAll(); break;
  1361. case StandardApplicationCommandIDs::undo: undo(); break;
  1362. case StandardApplicationCommandIDs::redo: redo(); break;
  1363. default: break;
  1364. }
  1365. }
  1366. static void textEditorMenuCallback (int menuResult, TextEditor* editor)
  1367. {
  1368. if (editor != nullptr && menuResult != 0)
  1369. editor->performPopupMenuAction (menuResult);
  1370. }
  1371. //==============================================================================
  1372. void TextEditor::mouseDown (const MouseEvent& e)
  1373. {
  1374. beginDragAutoRepeat (100);
  1375. newTransaction();
  1376. if (wasFocused || ! selectAllTextWhenFocused)
  1377. {
  1378. if (! (popupMenuEnabled && e.mods.isPopupMenu()))
  1379. {
  1380. moveCaretTo (getTextIndexAt (e.x, e.y),
  1381. e.mods.isShiftDown());
  1382. }
  1383. else
  1384. {
  1385. PopupMenu m;
  1386. m.setLookAndFeel (&getLookAndFeel());
  1387. addPopupMenuItems (m, &e);
  1388. m.showMenuAsync (PopupMenu::Options(),
  1389. ModalCallbackFunction::forComponent (textEditorMenuCallback, this));
  1390. }
  1391. }
  1392. }
  1393. void TextEditor::mouseDrag (const MouseEvent& e)
  1394. {
  1395. if (wasFocused || ! selectAllTextWhenFocused)
  1396. if (! (popupMenuEnabled && e.mods.isPopupMenu()))
  1397. moveCaretTo (getTextIndexAt (e.x, e.y), true);
  1398. }
  1399. void TextEditor::mouseUp (const MouseEvent& e)
  1400. {
  1401. newTransaction();
  1402. textHolder->restartTimer();
  1403. if (wasFocused || ! selectAllTextWhenFocused)
  1404. if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu()))
  1405. moveCaret (getTextIndexAt (e.x, e.y));
  1406. wasFocused = true;
  1407. }
  1408. void TextEditor::mouseDoubleClick (const MouseEvent& e)
  1409. {
  1410. int tokenEnd = getTextIndexAt (e.x, e.y);
  1411. int tokenStart = 0;
  1412. if (e.getNumberOfClicks() > 3)
  1413. {
  1414. tokenEnd = getTotalNumChars();
  1415. }
  1416. else
  1417. {
  1418. auto t = getText();
  1419. auto totalLength = getTotalNumChars();
  1420. while (tokenEnd < totalLength)
  1421. {
  1422. auto c = t[tokenEnd];
  1423. // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
  1424. if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
  1425. ++tokenEnd;
  1426. else
  1427. break;
  1428. }
  1429. tokenStart = tokenEnd;
  1430. while (tokenStart > 0)
  1431. {
  1432. auto c = t[tokenStart - 1];
  1433. // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
  1434. if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
  1435. --tokenStart;
  1436. else
  1437. break;
  1438. }
  1439. if (e.getNumberOfClicks() > 2)
  1440. {
  1441. while (tokenEnd < totalLength)
  1442. {
  1443. auto c = t[tokenEnd];
  1444. if (c != '\r' && c != '\n')
  1445. ++tokenEnd;
  1446. else
  1447. break;
  1448. }
  1449. while (tokenStart > 0)
  1450. {
  1451. auto c = t[tokenStart - 1];
  1452. if (c != '\r' && c != '\n')
  1453. --tokenStart;
  1454. else
  1455. break;
  1456. }
  1457. }
  1458. }
  1459. moveCaretTo (tokenEnd, false);
  1460. moveCaretTo (tokenStart, true);
  1461. }
  1462. void TextEditor::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
  1463. {
  1464. if (! viewport->useMouseWheelMoveIfNeeded (e, wheel))
  1465. Component::mouseWheelMove (e, wheel);
  1466. }
  1467. //==============================================================================
  1468. bool TextEditor::moveCaretWithTransaction (const int newPos, const bool selecting)
  1469. {
  1470. newTransaction();
  1471. moveCaretTo (newPos, selecting);
  1472. return true;
  1473. }
  1474. bool TextEditor::moveCaretLeft (bool moveInWholeWordSteps, bool selecting)
  1475. {
  1476. auto pos = getCaretPosition();
  1477. if (moveInWholeWordSteps)
  1478. pos = findWordBreakBefore (pos);
  1479. else
  1480. --pos;
  1481. return moveCaretWithTransaction (pos, selecting);
  1482. }
  1483. bool TextEditor::moveCaretRight (bool moveInWholeWordSteps, bool selecting)
  1484. {
  1485. auto pos = getCaretPosition();
  1486. if (moveInWholeWordSteps)
  1487. pos = findWordBreakAfter (pos);
  1488. else
  1489. ++pos;
  1490. return moveCaretWithTransaction (pos, selecting);
  1491. }
  1492. bool TextEditor::moveCaretUp (bool selecting)
  1493. {
  1494. if (! isMultiLine())
  1495. return moveCaretToStartOfLine (selecting);
  1496. auto caretPos = getCaretRectangleFloat();
  1497. return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - 1.0f), selecting);
  1498. }
  1499. bool TextEditor::moveCaretDown (bool selecting)
  1500. {
  1501. if (! isMultiLine())
  1502. return moveCaretToEndOfLine (selecting);
  1503. auto caretPos = getCaretRectangleFloat();
  1504. return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting);
  1505. }
  1506. bool TextEditor::pageUp (bool selecting)
  1507. {
  1508. if (! isMultiLine())
  1509. return moveCaretToStartOfLine (selecting);
  1510. auto caretPos = getCaretRectangleFloat();
  1511. return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - viewport->getViewHeight()), selecting);
  1512. }
  1513. bool TextEditor::pageDown (bool selecting)
  1514. {
  1515. if (! isMultiLine())
  1516. return moveCaretToEndOfLine (selecting);
  1517. auto caretPos = getCaretRectangleFloat();
  1518. return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + viewport->getViewHeight()), selecting);
  1519. }
  1520. void TextEditor::scrollByLines (int deltaLines)
  1521. {
  1522. viewport->getVerticalScrollBar().moveScrollbarInSteps (deltaLines);
  1523. }
  1524. bool TextEditor::scrollDown()
  1525. {
  1526. scrollByLines (-1);
  1527. return true;
  1528. }
  1529. bool TextEditor::scrollUp()
  1530. {
  1531. scrollByLines (1);
  1532. return true;
  1533. }
  1534. bool TextEditor::moveCaretToTop (bool selecting)
  1535. {
  1536. return moveCaretWithTransaction (0, selecting);
  1537. }
  1538. bool TextEditor::moveCaretToStartOfLine (bool selecting)
  1539. {
  1540. auto caretPos = getCaretRectangleFloat();
  1541. return moveCaretWithTransaction (indexAtPosition (0.0f, caretPos.getY()), selecting);
  1542. }
  1543. bool TextEditor::moveCaretToEnd (bool selecting)
  1544. {
  1545. return moveCaretWithTransaction (getTotalNumChars(), selecting);
  1546. }
  1547. bool TextEditor::moveCaretToEndOfLine (bool selecting)
  1548. {
  1549. auto caretPos = getCaretRectangleFloat();
  1550. return moveCaretWithTransaction (indexAtPosition ((float) textHolder->getWidth(), caretPos.getY()), selecting);
  1551. }
  1552. bool TextEditor::deleteBackwards (bool moveInWholeWordSteps)
  1553. {
  1554. if (moveInWholeWordSteps)
  1555. moveCaretTo (findWordBreakBefore (getCaretPosition()), true);
  1556. else if (selection.isEmpty() && selection.getStart() > 0)
  1557. selection = { selection.getEnd() - 1, selection.getEnd() };
  1558. cut();
  1559. return true;
  1560. }
  1561. bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/)
  1562. {
  1563. if (selection.isEmpty() && selection.getStart() < getTotalNumChars())
  1564. selection = { selection.getStart(), selection.getStart() + 1 };
  1565. cut();
  1566. return true;
  1567. }
  1568. bool TextEditor::copyToClipboard()
  1569. {
  1570. newTransaction();
  1571. copy();
  1572. return true;
  1573. }
  1574. bool TextEditor::cutToClipboard()
  1575. {
  1576. newTransaction();
  1577. copy();
  1578. cut();
  1579. return true;
  1580. }
  1581. bool TextEditor::pasteFromClipboard()
  1582. {
  1583. newTransaction();
  1584. paste();
  1585. return true;
  1586. }
  1587. bool TextEditor::selectAll()
  1588. {
  1589. newTransaction();
  1590. moveCaretTo (getTotalNumChars(), false);
  1591. moveCaretTo (0, true);
  1592. return true;
  1593. }
  1594. //==============================================================================
  1595. void TextEditor::setEscapeAndReturnKeysConsumed (bool shouldBeConsumed) noexcept
  1596. {
  1597. consumeEscAndReturnKeys = shouldBeConsumed;
  1598. }
  1599. bool TextEditor::keyPressed (const KeyPress& key)
  1600. {
  1601. if (isReadOnly() && key != KeyPress ('c', ModifierKeys::commandModifier, 0)
  1602. && key != KeyPress ('a', ModifierKeys::commandModifier, 0))
  1603. return false;
  1604. if (! TextEditorKeyMapper<TextEditor>::invokeKeyFunction (*this, key))
  1605. {
  1606. if (key == KeyPress::returnKey)
  1607. {
  1608. newTransaction();
  1609. if (returnKeyStartsNewLine)
  1610. {
  1611. insertTextAtCaret ("\n");
  1612. }
  1613. else
  1614. {
  1615. returnPressed();
  1616. return consumeEscAndReturnKeys;
  1617. }
  1618. }
  1619. else if (key.isKeyCode (KeyPress::escapeKey))
  1620. {
  1621. newTransaction();
  1622. moveCaretTo (getCaretPosition(), false);
  1623. escapePressed();
  1624. return consumeEscAndReturnKeys;
  1625. }
  1626. else if (key.getTextCharacter() >= ' '
  1627. || (tabKeyUsed && (key.getTextCharacter() == '\t')))
  1628. {
  1629. insertTextAtCaret (String::charToString (key.getTextCharacter()));
  1630. lastTransactionTime = Time::getApproximateMillisecondCounter();
  1631. }
  1632. else
  1633. {
  1634. return false;
  1635. }
  1636. }
  1637. return true;
  1638. }
  1639. bool TextEditor::keyStateChanged (const bool isKeyDown)
  1640. {
  1641. if (! isKeyDown)
  1642. return false;
  1643. #if JUCE_WINDOWS
  1644. if (KeyPress (KeyPress::F4Key, ModifierKeys::altModifier, 0).isCurrentlyDown())
  1645. return false; // We need to explicitly allow alt-F4 to pass through on Windows
  1646. #endif
  1647. if ((! consumeEscAndReturnKeys)
  1648. && (KeyPress (KeyPress::escapeKey).isCurrentlyDown()
  1649. || KeyPress (KeyPress::returnKey).isCurrentlyDown()))
  1650. return false;
  1651. // (overridden to avoid forwarding key events to the parent)
  1652. return ! ModifierKeys::getCurrentModifiers().isCommandDown();
  1653. }
  1654. //==============================================================================
  1655. void TextEditor::focusGained (FocusChangeType)
  1656. {
  1657. newTransaction();
  1658. if (selectAllTextWhenFocused)
  1659. {
  1660. moveCaretTo (0, false);
  1661. moveCaretTo (getTotalNumChars(), true);
  1662. }
  1663. repaint();
  1664. updateCaretPosition();
  1665. if (auto* peer = getPeer())
  1666. if (! isReadOnly())
  1667. peer->textInputRequired (peer->globalToLocal (getScreenPosition()), *this);
  1668. }
  1669. void TextEditor::focusLost (FocusChangeType)
  1670. {
  1671. newTransaction();
  1672. wasFocused = false;
  1673. textHolder->stopTimer();
  1674. underlinedSections.clear();
  1675. if (auto* peer = getPeer())
  1676. peer->dismissPendingTextInput();
  1677. updateCaretPosition();
  1678. postCommandMessage (TextEditorDefs::focusLossMessageId);
  1679. repaint();
  1680. }
  1681. //==============================================================================
  1682. void TextEditor::resized()
  1683. {
  1684. viewport->setBoundsInset (borderSize);
  1685. viewport->setSingleStepSizes (16, roundToInt (currentFont.getHeight()));
  1686. updateTextHolderSize();
  1687. if (isMultiLine())
  1688. updateCaretPosition();
  1689. else
  1690. scrollToMakeSureCursorIsVisible();
  1691. }
  1692. void TextEditor::handleCommandMessage (const int commandId)
  1693. {
  1694. Component::BailOutChecker checker (this);
  1695. switch (commandId)
  1696. {
  1697. case TextEditorDefs::textChangeMessageId:
  1698. listeners.callChecked (checker, &Listener::textEditorTextChanged, (TextEditor&) *this);
  1699. break;
  1700. case TextEditorDefs::returnKeyMessageId:
  1701. listeners.callChecked (checker, &Listener::textEditorReturnKeyPressed, (TextEditor&) *this);
  1702. break;
  1703. case TextEditorDefs::escapeKeyMessageId:
  1704. listeners.callChecked (checker, &Listener::textEditorEscapeKeyPressed, (TextEditor&) *this);
  1705. break;
  1706. case TextEditorDefs::focusLossMessageId:
  1707. updateValueFromText();
  1708. listeners.callChecked (checker, &Listener::textEditorFocusLost, (TextEditor&) *this);
  1709. break;
  1710. default:
  1711. jassertfalse;
  1712. break;
  1713. }
  1714. }
  1715. void TextEditor::setTemporaryUnderlining (const Array<Range<int>>& newUnderlinedSections)
  1716. {
  1717. underlinedSections = newUnderlinedSections;
  1718. repaint();
  1719. }
  1720. //==============================================================================
  1721. UndoManager* TextEditor::getUndoManager() noexcept
  1722. {
  1723. return readOnly ? nullptr : &undoManager;
  1724. }
  1725. void TextEditor::clearInternal (UndoManager* const um)
  1726. {
  1727. remove ({ 0, getTotalNumChars() }, um, caretPosition);
  1728. }
  1729. void TextEditor::insert (const String& text, int insertIndex, const Font& font,
  1730. Colour colour, UndoManager* um, int caretPositionToMoveTo)
  1731. {
  1732. if (text.isNotEmpty())
  1733. {
  1734. if (um != nullptr)
  1735. {
  1736. if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
  1737. newTransaction();
  1738. um->perform (new InsertAction (*this, text, insertIndex, font, colour,
  1739. caretPosition, caretPositionToMoveTo));
  1740. }
  1741. else
  1742. {
  1743. repaintText ({ insertIndex, getTotalNumChars() }); // must do this before and after changing the data, in case
  1744. // a line gets moved due to word wrap
  1745. int index = 0;
  1746. int nextIndex = 0;
  1747. for (int i = 0; i < sections.size(); ++i)
  1748. {
  1749. nextIndex = index + sections.getUnchecked (i)->getTotalLength();
  1750. if (insertIndex == index)
  1751. {
  1752. sections.insert (i, new UniformTextSection (text, font, colour, passwordCharacter));
  1753. break;
  1754. }
  1755. if (insertIndex > index && insertIndex < nextIndex)
  1756. {
  1757. splitSection (i, insertIndex - index);
  1758. sections.insert (i + 1, new UniformTextSection (text, font, colour, passwordCharacter));
  1759. break;
  1760. }
  1761. index = nextIndex;
  1762. }
  1763. if (nextIndex == insertIndex)
  1764. sections.add (new UniformTextSection (text, font, colour, passwordCharacter));
  1765. coalesceSimilarSections();
  1766. totalNumChars = -1;
  1767. valueTextNeedsUpdating = true;
  1768. updateTextHolderSize();
  1769. moveCaretTo (caretPositionToMoveTo, false);
  1770. repaintText ({ insertIndex, getTotalNumChars() });
  1771. }
  1772. }
  1773. }
  1774. void TextEditor::reinsert (int insertIndex, const OwnedArray<UniformTextSection>& sectionsToInsert)
  1775. {
  1776. int index = 0;
  1777. int nextIndex = 0;
  1778. for (int i = 0; i < sections.size(); ++i)
  1779. {
  1780. nextIndex = index + sections.getUnchecked (i)->getTotalLength();
  1781. if (insertIndex == index)
  1782. {
  1783. for (int j = sectionsToInsert.size(); --j >= 0;)
  1784. sections.insert (i, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
  1785. break;
  1786. }
  1787. if (insertIndex > index && insertIndex < nextIndex)
  1788. {
  1789. splitSection (i, insertIndex - index);
  1790. for (int j = sectionsToInsert.size(); --j >= 0;)
  1791. sections.insert (i + 1, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
  1792. break;
  1793. }
  1794. index = nextIndex;
  1795. }
  1796. if (nextIndex == insertIndex)
  1797. for (auto* s : sectionsToInsert)
  1798. sections.add (new UniformTextSection (*s));
  1799. coalesceSimilarSections();
  1800. totalNumChars = -1;
  1801. valueTextNeedsUpdating = true;
  1802. }
  1803. void TextEditor::remove (Range<int> range, UndoManager* const um, const int caretPositionToMoveTo)
  1804. {
  1805. if (! range.isEmpty())
  1806. {
  1807. int index = 0;
  1808. for (int i = 0; i < sections.size(); ++i)
  1809. {
  1810. auto nextIndex = index + sections.getUnchecked(i)->getTotalLength();
  1811. if (range.getStart() > index && range.getStart() < nextIndex)
  1812. {
  1813. splitSection (i, range.getStart() - index);
  1814. --i;
  1815. }
  1816. else if (range.getEnd() > index && range.getEnd() < nextIndex)
  1817. {
  1818. splitSection (i, range.getEnd() - index);
  1819. --i;
  1820. }
  1821. else
  1822. {
  1823. index = nextIndex;
  1824. if (index > range.getEnd())
  1825. break;
  1826. }
  1827. }
  1828. index = 0;
  1829. if (um != nullptr)
  1830. {
  1831. Array<UniformTextSection*> removedSections;
  1832. for (auto* section : sections)
  1833. {
  1834. if (range.getEnd() <= range.getStart())
  1835. break;
  1836. auto nextIndex = index + section->getTotalLength();
  1837. if (range.getStart() <= index && range.getEnd() >= nextIndex)
  1838. removedSections.add (new UniformTextSection (*section));
  1839. index = nextIndex;
  1840. }
  1841. if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
  1842. newTransaction();
  1843. um->perform (new RemoveAction (*this, range, caretPosition,
  1844. caretPositionToMoveTo, removedSections));
  1845. }
  1846. else
  1847. {
  1848. auto remainingRange = range;
  1849. for (int i = 0; i < sections.size(); ++i)
  1850. {
  1851. auto* section = sections.getUnchecked (i);
  1852. auto nextIndex = index + section->getTotalLength();
  1853. if (remainingRange.getStart() <= index && remainingRange.getEnd() >= nextIndex)
  1854. {
  1855. sections.remove (i);
  1856. remainingRange.setEnd (remainingRange.getEnd() - (nextIndex - index));
  1857. if (remainingRange.isEmpty())
  1858. break;
  1859. --i;
  1860. }
  1861. else
  1862. {
  1863. index = nextIndex;
  1864. }
  1865. }
  1866. coalesceSimilarSections();
  1867. totalNumChars = -1;
  1868. valueTextNeedsUpdating = true;
  1869. moveCaretTo (caretPositionToMoveTo, false);
  1870. repaintText ({ range.getStart(), getTotalNumChars() });
  1871. }
  1872. }
  1873. }
  1874. //==============================================================================
  1875. String TextEditor::getText() const
  1876. {
  1877. MemoryOutputStream mo;
  1878. mo.preallocate ((size_t) getTotalNumChars());
  1879. for (auto* s : sections)
  1880. s->appendAllText (mo);
  1881. return mo.toUTF8();
  1882. }
  1883. String TextEditor::getTextInRange (const Range<int>& range) const
  1884. {
  1885. if (range.isEmpty())
  1886. return {};
  1887. MemoryOutputStream mo;
  1888. mo.preallocate ((size_t) jmin (getTotalNumChars(), range.getLength()));
  1889. int index = 0;
  1890. for (auto* s : sections)
  1891. {
  1892. auto nextIndex = index + s->getTotalLength();
  1893. if (range.getStart() < nextIndex)
  1894. {
  1895. if (range.getEnd() <= index)
  1896. break;
  1897. s->appendSubstring (mo, range - index);
  1898. }
  1899. index = nextIndex;
  1900. }
  1901. return mo.toUTF8();
  1902. }
  1903. String TextEditor::getHighlightedText() const
  1904. {
  1905. return getTextInRange (selection);
  1906. }
  1907. int TextEditor::getTotalNumChars() const
  1908. {
  1909. if (totalNumChars < 0)
  1910. {
  1911. totalNumChars = 0;
  1912. for (auto* s : sections)
  1913. totalNumChars += s->getTotalLength();
  1914. }
  1915. return totalNumChars;
  1916. }
  1917. bool TextEditor::isEmpty() const
  1918. {
  1919. return getTotalNumChars() == 0;
  1920. }
  1921. void TextEditor::getCharPosition (int index, Point<float>& anchor, float& lineHeight) const
  1922. {
  1923. if (getWordWrapWidth() <= 0)
  1924. {
  1925. anchor = {};
  1926. lineHeight = currentFont.getHeight();
  1927. }
  1928. else
  1929. {
  1930. Iterator i (*this);
  1931. if (sections.isEmpty())
  1932. {
  1933. anchor = { i.getJustificationOffset (0), 0 };
  1934. lineHeight = currentFont.getHeight();
  1935. }
  1936. else
  1937. {
  1938. i.getCharPosition (index, anchor, lineHeight);
  1939. }
  1940. }
  1941. }
  1942. int TextEditor::indexAtPosition (const float x, const float y)
  1943. {
  1944. if (getWordWrapWidth() > 0)
  1945. {
  1946. for (Iterator i (*this); i.next();)
  1947. {
  1948. if (y < i.lineY + i.lineHeight)
  1949. {
  1950. if (y < i.lineY)
  1951. return jmax (0, i.indexInText - 1);
  1952. if (x <= i.atomX || i.atom->isNewLine())
  1953. return i.indexInText;
  1954. if (x < i.atomRight)
  1955. return i.xToIndex (x);
  1956. }
  1957. }
  1958. }
  1959. return getTotalNumChars();
  1960. }
  1961. //==============================================================================
  1962. int TextEditor::findWordBreakAfter (const int position) const
  1963. {
  1964. auto t = getTextInRange ({ position, position + 512 });
  1965. auto totalLength = t.length();
  1966. int i = 0;
  1967. while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
  1968. ++i;
  1969. auto type = TextEditorDefs::getCharacterCategory (t[i]);
  1970. while (i < totalLength && type == TextEditorDefs::getCharacterCategory (t[i]))
  1971. ++i;
  1972. while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
  1973. ++i;
  1974. return position + i;
  1975. }
  1976. int TextEditor::findWordBreakBefore (const int position) const
  1977. {
  1978. if (position <= 0)
  1979. return 0;
  1980. auto startOfBuffer = jmax (0, position - 512);
  1981. auto t = getTextInRange ({ startOfBuffer, position });
  1982. int i = position - startOfBuffer;
  1983. while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1]))
  1984. --i;
  1985. if (i > 0)
  1986. {
  1987. auto type = TextEditorDefs::getCharacterCategory (t [i - 1]);
  1988. while (i > 0 && type == TextEditorDefs::getCharacterCategory (t [i - 1]))
  1989. --i;
  1990. }
  1991. jassert (startOfBuffer + i >= 0);
  1992. return startOfBuffer + i;
  1993. }
  1994. //==============================================================================
  1995. void TextEditor::splitSection (const int sectionIndex, const int charToSplitAt)
  1996. {
  1997. jassert (sections[sectionIndex] != nullptr);
  1998. sections.insert (sectionIndex + 1,
  1999. sections.getUnchecked (sectionIndex)->split (charToSplitAt, passwordCharacter));
  2000. }
  2001. void TextEditor::coalesceSimilarSections()
  2002. {
  2003. for (int i = 0; i < sections.size() - 1; ++i)
  2004. {
  2005. auto* s1 = sections.getUnchecked (i);
  2006. auto* s2 = sections.getUnchecked (i + 1);
  2007. if (s1->font == s2->font
  2008. && s1->colour == s2->colour)
  2009. {
  2010. s1->append (*s2, passwordCharacter);
  2011. sections.remove (i + 1);
  2012. --i;
  2013. }
  2014. }
  2015. }
  2016. } // namespace juce