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.

2793 lines
81KB

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