12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537 |
- /*
- ==============================================================================
- This file is part of the JUCE library.
- Copyright (c) 2017 - ROLI Ltd.
- JUCE is an open source library subject to commercial or open-source
- licensing.
- By using JUCE, you agree to the terms of both the JUCE 5 End-User License
- Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
- 27th April 2017).
- End User License Agreement: www.juce.com/juce-5-licence
- Privacy Policy: www.juce.com/juce-5-privacy-policy
- Or: You may also use this code under the terms of the GPL v3 (see
- www.gnu.org/licenses).
- JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
- EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
- DISCLAIMED.
- ==============================================================================
- */
- namespace juce
- {
- // a word or space that can't be broken down any further
- struct TextAtom
- {
- //==============================================================================
- String atomText;
- float width;
- int numChars;
- //==============================================================================
- bool isWhitespace() const noexcept { return CharacterFunctions::isWhitespace (atomText[0]); }
- bool isNewLine() const noexcept { return atomText[0] == '\r' || atomText[0] == '\n'; }
- String getText (juce_wchar passwordCharacter) const
- {
- if (passwordCharacter == 0)
- return atomText;
- return String::repeatedString (String::charToString (passwordCharacter),
- atomText.length());
- }
- String getTrimmedText (const juce_wchar passwordCharacter) const
- {
- if (passwordCharacter == 0)
- return atomText.substring (0, numChars);
- if (isNewLine())
- return {};
- return String::repeatedString (String::charToString (passwordCharacter), numChars);
- }
- JUCE_LEAK_DETECTOR (TextAtom)
- };
- //==============================================================================
- // a run of text with a single font and colour
- class TextEditor::UniformTextSection
- {
- public:
- UniformTextSection (const String& text, const Font& f, Colour col, juce_wchar passwordCharToUse)
- : font (f), colour (col), passwordChar (passwordCharToUse)
- {
- initialiseAtoms (text);
- }
- UniformTextSection (const UniformTextSection&) = default;
- UniformTextSection (UniformTextSection&&) = default;
- UniformTextSection& operator= (const UniformTextSection&) = delete;
- void append (UniformTextSection& other)
- {
- if (! other.atoms.isEmpty())
- {
- int i = 0;
- if (! atoms.isEmpty())
- {
- auto& lastAtom = atoms.getReference (atoms.size() - 1);
- if (! CharacterFunctions::isWhitespace (lastAtom.atomText.getLastCharacter()))
- {
- auto& first = other.atoms.getReference(0);
- if (! CharacterFunctions::isWhitespace (first.atomText[0]))
- {
- lastAtom.atomText += first.atomText;
- lastAtom.numChars = (uint16) (lastAtom.numChars + first.numChars);
- lastAtom.width = font.getStringWidthFloat (lastAtom.getText (passwordChar));
- ++i;
- }
- }
- }
- atoms.ensureStorageAllocated (atoms.size() + other.atoms.size() - i);
- while (i < other.atoms.size())
- {
- atoms.add (other.atoms.getReference(i));
- ++i;
- }
- }
- }
- UniformTextSection* split (int indexToBreakAt)
- {
- auto* section2 = new UniformTextSection ({}, font, colour, passwordChar);
- int index = 0;
- for (int i = 0; i < atoms.size(); ++i)
- {
- auto& atom = atoms.getReference(i);
- auto nextIndex = index + atom.numChars;
- if (index == indexToBreakAt)
- {
- for (int j = i; j < atoms.size(); ++j)
- section2->atoms.add (atoms.getUnchecked (j));
- atoms.removeRange (i, atoms.size());
- break;
- }
- if (indexToBreakAt >= index && indexToBreakAt < nextIndex)
- {
- TextAtom secondAtom;
- secondAtom.atomText = atom.atomText.substring (indexToBreakAt - index);
- secondAtom.width = font.getStringWidthFloat (secondAtom.getText (passwordChar));
- secondAtom.numChars = (uint16) secondAtom.atomText.length();
- section2->atoms.add (secondAtom);
- atom.atomText = atom.atomText.substring (0, indexToBreakAt - index);
- atom.width = font.getStringWidthFloat (atom.getText (passwordChar));
- atom.numChars = (uint16) (indexToBreakAt - index);
- for (int j = i + 1; j < atoms.size(); ++j)
- section2->atoms.add (atoms.getUnchecked (j));
- atoms.removeRange (i + 1, atoms.size());
- break;
- }
- index = nextIndex;
- }
- return section2;
- }
- void appendAllText (MemoryOutputStream& mo) const
- {
- for (auto& atom : atoms)
- mo << atom.atomText;
- }
- void appendSubstring (MemoryOutputStream& mo, Range<int> range) const
- {
- int index = 0;
- for (auto& atom : atoms)
- {
- auto nextIndex = index + atom.numChars;
- if (range.getStart() < nextIndex)
- {
- if (range.getEnd() <= index)
- break;
- auto r = (range - index).getIntersectionWith ({ 0, (int) atom.numChars });
- if (! r.isEmpty())
- mo << atom.atomText.substring (r.getStart(), r.getEnd());
- }
- index = nextIndex;
- }
- }
- int getTotalLength() const noexcept
- {
- int total = 0;
- for (auto& atom : atoms)
- total += atom.numChars;
- return total;
- }
- void setFont (const Font& newFont, const juce_wchar passwordCharToUse)
- {
- if (font != newFont || passwordChar != passwordCharToUse)
- {
- font = newFont;
- passwordChar = passwordCharToUse;
- for (auto& atom : atoms)
- atom.width = newFont.getStringWidthFloat (atom.getText (passwordChar));
- }
- }
- //==============================================================================
- Font font;
- Colour colour;
- Array<TextAtom> atoms;
- juce_wchar passwordChar;
- private:
- void initialiseAtoms (const String& textToParse)
- {
- auto text = textToParse.getCharPointer();
- while (! text.isEmpty())
- {
- size_t numChars = 0;
- auto start = text;
- // create a whitespace atom unless it starts with non-ws
- if (text.isWhitespace() && *text != '\r' && *text != '\n')
- {
- do
- {
- ++text;
- ++numChars;
- }
- while (text.isWhitespace() && *text != '\r' && *text != '\n');
- }
- else
- {
- if (*text == '\r')
- {
- ++text;
- ++numChars;
- if (*text == '\n')
- {
- ++start;
- ++text;
- }
- }
- else if (*text == '\n')
- {
- ++text;
- ++numChars;
- }
- else
- {
- while (! (text.isEmpty() || text.isWhitespace()))
- {
- ++text;
- ++numChars;
- }
- }
- }
- TextAtom atom;
- atom.atomText = String (start, numChars);
- atom.width = font.getStringWidthFloat (atom.getText (passwordChar));
- atom.numChars = (uint16) numChars;
- atoms.add (atom);
- }
- }
- JUCE_LEAK_DETECTOR (UniformTextSection)
- };
- //==============================================================================
- struct TextEditor::Iterator
- {
- Iterator (const TextEditor& ed)
- : sections (ed.sections),
- justification (ed.justification),
- justificationWidth (ed.getJustificationWidth()),
- wordWrapWidth (ed.getWordWrapWidth()),
- passwordCharacter (ed.passwordCharacter),
- lineSpacing (ed.lineSpacing)
- {
- jassert (wordWrapWidth > 0);
- if (! sections.isEmpty())
- {
- currentSection = sections.getUnchecked (sectionIndex);
- if (currentSection != nullptr)
- beginNewLine();
- }
- }
- Iterator (const Iterator&) = default;
- Iterator& operator= (const Iterator&) = delete;
- //==============================================================================
- bool next()
- {
- if (atom == &tempAtom)
- {
- auto numRemaining = tempAtom.atomText.length() - tempAtom.numChars;
- if (numRemaining > 0)
- {
- tempAtom.atomText = tempAtom.atomText.substring (tempAtom.numChars);
- if (tempAtom.numChars > 0)
- lineY += lineHeight * lineSpacing;
- indexInText += tempAtom.numChars;
- GlyphArrangement g;
- g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f);
- int split;
- for (split = 0; split < g.getNumGlyphs(); ++split)
- if (shouldWrap (g.getGlyph (split).getRight()))
- break;
- if (split > 0 && split <= numRemaining)
- {
- tempAtom.numChars = (uint16) split;
- tempAtom.width = g.getGlyph (split - 1).getRight();
- atomX = getJustificationOffset (tempAtom.width);
- atomRight = atomX + tempAtom.width;
- return true;
- }
- }
- }
- if (sectionIndex >= sections.size())
- {
- moveToEndOfLastAtom();
- return false;
- }
- bool forceNewLine = false;
- if (atomIndex >= currentSection->atoms.size() - 1)
- {
- if (atomIndex >= currentSection->atoms.size())
- {
- if (++sectionIndex >= sections.size())
- {
- moveToEndOfLastAtom();
- return false;
- }
- atomIndex = 0;
- currentSection = sections.getUnchecked (sectionIndex);
- }
- else
- {
- auto& lastAtom = currentSection->atoms.getReference (atomIndex);
- if (! lastAtom.isWhitespace())
- {
- // handle the case where the last atom in a section is actually part of the same
- // word as the first atom of the next section...
- float right = atomRight + lastAtom.width;
- float lineHeight2 = lineHeight;
- float maxDescent2 = maxDescent;
- for (int section = sectionIndex + 1; section < sections.size(); ++section)
- {
- auto* s = sections.getUnchecked (section);
- if (s->atoms.size() == 0)
- break;
- auto& nextAtom = s->atoms.getReference (0);
- if (nextAtom.isWhitespace())
- break;
- right += nextAtom.width;
- lineHeight2 = jmax (lineHeight2, s->font.getHeight());
- maxDescent2 = jmax (maxDescent2, s->font.getDescent());
- if (shouldWrap (right))
- {
- lineHeight = lineHeight2;
- maxDescent = maxDescent2;
- forceNewLine = true;
- break;
- }
- if (s->atoms.size() > 1)
- break;
- }
- }
- }
- }
- if (atom != nullptr)
- {
- atomX = atomRight;
- indexInText += atom->numChars;
- if (atom->isNewLine())
- beginNewLine();
- }
- atom = &(currentSection->atoms.getReference (atomIndex));
- atomRight = atomX + atom->width;
- ++atomIndex;
- if (shouldWrap (atomRight) || forceNewLine)
- {
- if (atom->isWhitespace())
- {
- // leave whitespace at the end of a line, but truncate it to avoid scrolling
- atomRight = jmin (atomRight, wordWrapWidth);
- }
- else
- {
- if (shouldWrap (atom->width)) // atom too big to fit on a line, so break it up..
- {
- tempAtom = *atom;
- tempAtom.width = 0;
- tempAtom.numChars = 0;
- atom = &tempAtom;
- if (atomX > justificationOffset)
- beginNewLine();
- return next();
- }
- beginNewLine();
- atomX = justificationOffset;
- atomRight = atomX + atom->width;
- return true;
- }
- }
- return true;
- }
- void beginNewLine()
- {
- lineY += lineHeight * lineSpacing;
- float lineWidth = 0;
- auto tempSectionIndex = sectionIndex;
- auto tempAtomIndex = atomIndex;
- auto* section = sections.getUnchecked (tempSectionIndex);
- lineHeight = section->font.getHeight();
- maxDescent = section->font.getDescent();
- float nextLineWidth = (atom != nullptr) ? atom->width : 0.0f;
- while (! shouldWrap (nextLineWidth))
- {
- lineWidth = nextLineWidth;
- if (tempSectionIndex >= sections.size())
- break;
- bool checkSize = false;
- if (tempAtomIndex >= section->atoms.size())
- {
- if (++tempSectionIndex >= sections.size())
- break;
- tempAtomIndex = 0;
- section = sections.getUnchecked (tempSectionIndex);
- checkSize = true;
- }
- if (! isPositiveAndBelow (tempAtomIndex, section->atoms.size()))
- break;
- auto& nextAtom = section->atoms.getReference (tempAtomIndex);
- nextLineWidth += nextAtom.width;
- if (shouldWrap (nextLineWidth) || nextAtom.isNewLine())
- break;
- if (checkSize)
- {
- lineHeight = jmax (lineHeight, section->font.getHeight());
- maxDescent = jmax (maxDescent, section->font.getDescent());
- }
- ++tempAtomIndex;
- }
- justificationOffset = getJustificationOffset (lineWidth);
- atomX = justificationOffset;
- }
- float getJustificationOffset (float lineWidth) const
- {
- if (justification.getOnlyHorizontalFlags() == Justification::horizontallyCentred)
- return jmax (0.0f, (justificationWidth - lineWidth) * 0.5f);
- if (justification.getOnlyHorizontalFlags() == Justification::right)
- return jmax (0.0f, justificationWidth - lineWidth);
- return 0;
- }
- //==============================================================================
- void draw (Graphics& g, const UniformTextSection*& lastSection) const
- {
- if (passwordCharacter != 0 || ! atom->isWhitespace())
- {
- if (lastSection != currentSection)
- {
- lastSection = currentSection;
- g.setColour (currentSection->colour);
- g.setFont (currentSection->font);
- }
- jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty());
- GlyphArrangement ga;
- ga.addLineOfText (currentSection->font,
- atom->getTrimmedText (passwordCharacter),
- atomX, (float) roundToInt (lineY + lineHeight - maxDescent));
- ga.draw (g);
- }
- }
- void addSelection (RectangleList<float>& area, Range<int> selected) const
- {
- auto startX = indexToX (selected.getStart());
- auto endX = indexToX (selected.getEnd());
- area.add (startX, lineY, endX - startX, lineHeight * lineSpacing);
- }
- void drawUnderline (Graphics& g, Range<int> underline, Colour colour) const
- {
- auto startX = roundToInt (indexToX (underline.getStart()));
- auto endX = roundToInt (indexToX (underline.getEnd()));
- auto baselineY = roundToInt (lineY + currentSection->font.getAscent() + 0.5f);
- Graphics::ScopedSaveState state (g);
- g.reduceClipRegion ({ startX, baselineY, endX - startX, 1 });
- g.fillCheckerBoard ({ (float) endX, baselineY + 1.0f }, 3.0f, 1.0f, colour, Colours::transparentBlack);
- }
- void drawSelectedText (Graphics& g, Range<int> selected, Colour selectedTextColour) const
- {
- if (passwordCharacter != 0 || ! atom->isWhitespace())
- {
- GlyphArrangement ga;
- ga.addLineOfText (currentSection->font,
- atom->getTrimmedText (passwordCharacter),
- atomX, (float) roundToInt (lineY + lineHeight - maxDescent));
- if (selected.getEnd() < indexInText + atom->numChars)
- {
- GlyphArrangement ga2 (ga);
- ga2.removeRangeOfGlyphs (0, selected.getEnd() - indexInText);
- ga.removeRangeOfGlyphs (selected.getEnd() - indexInText, -1);
- g.setColour (currentSection->colour);
- ga2.draw (g);
- }
- if (selected.getStart() > indexInText)
- {
- GlyphArrangement ga2 (ga);
- ga2.removeRangeOfGlyphs (selected.getStart() - indexInText, -1);
- ga.removeRangeOfGlyphs (0, selected.getStart() - indexInText);
- g.setColour (currentSection->colour);
- ga2.draw (g);
- }
- g.setColour (selectedTextColour);
- ga.draw (g);
- }
- }
- //==============================================================================
- float indexToX (int indexToFind) const
- {
- if (indexToFind <= indexInText)
- return atomX;
- if (indexToFind >= indexInText + atom->numChars)
- return atomRight;
- GlyphArrangement g;
- g.addLineOfText (currentSection->font,
- atom->getText (passwordCharacter),
- atomX, 0.0f);
- if (indexToFind - indexInText >= g.getNumGlyphs())
- return atomRight;
- return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft());
- }
- int xToIndex (float xToFind) const
- {
- if (xToFind <= atomX || atom->isNewLine())
- return indexInText;
- if (xToFind >= atomRight)
- return indexInText + atom->numChars;
- GlyphArrangement g;
- g.addLineOfText (currentSection->font,
- atom->getText (passwordCharacter),
- atomX, 0.0f);
- auto numGlyphs = g.getNumGlyphs();
- int j;
- for (j = 0; j < numGlyphs; ++j)
- {
- auto& pg = g.getGlyph(j);
- if ((pg.getLeft() + pg.getRight()) / 2 > xToFind)
- break;
- }
- return indexInText + j;
- }
- //==============================================================================
- bool getCharPosition (int index, Point<float>& anchor, float& lineHeightFound)
- {
- while (next())
- {
- if (indexInText + atom->numChars > index)
- {
- anchor = { indexToX (index), lineY };
- lineHeightFound = lineHeight;
- return true;
- }
- }
- anchor = { atomX, lineY };
- lineHeightFound = lineHeight;
- return false;
- }
- //==============================================================================
- int indexInText = 0;
- float lineY = 0, justificationOffset = 0, lineHeight = 0, maxDescent = 0;
- float atomX = 0, atomRight = 0;
- const TextAtom* atom = nullptr;
- const UniformTextSection* currentSection = nullptr;
- private:
- const OwnedArray<UniformTextSection>& sections;
- int sectionIndex = 0, atomIndex = 0;
- Justification justification;
- const float justificationWidth, wordWrapWidth;
- const juce_wchar passwordCharacter;
- const float lineSpacing;
- TextAtom tempAtom;
- void moveToEndOfLastAtom()
- {
- if (atom != nullptr)
- {
- atomX = atomRight;
- if (atom->isNewLine())
- {
- atomX = 0.0f;
- lineY += lineHeight * lineSpacing;
- }
- }
- }
- bool shouldWrap (const float x) const noexcept
- {
- return (x - 0.0001f) >= wordWrapWidth;
- }
- JUCE_LEAK_DETECTOR (Iterator)
- };
- //==============================================================================
- struct TextEditor::InsertAction : public UndoableAction
- {
- InsertAction (TextEditor& ed, const String& newText, int insertPos,
- const Font& newFont, Colour newColour, int oldCaret, int newCaret)
- : owner (ed),
- text (newText),
- insertIndex (insertPos),
- oldCaretPos (oldCaret),
- newCaretPos (newCaret),
- font (newFont),
- colour (newColour)
- {
- }
- bool perform() override
- {
- owner.insert (text, insertIndex, font, colour, nullptr, newCaretPos);
- return true;
- }
- bool undo() override
- {
- owner.remove ({ insertIndex, insertIndex + text.length() }, nullptr, oldCaretPos);
- return true;
- }
- int getSizeInUnits() override
- {
- return text.length() + 16;
- }
- private:
- TextEditor& owner;
- const String text;
- const int insertIndex, oldCaretPos, newCaretPos;
- const Font font;
- const Colour colour;
- JUCE_DECLARE_NON_COPYABLE (InsertAction)
- };
- //==============================================================================
- struct TextEditor::RemoveAction : public UndoableAction
- {
- RemoveAction (TextEditor& ed, Range<int> rangeToRemove, int oldCaret, int newCaret,
- const Array<UniformTextSection*>& oldSections)
- : owner (ed),
- range (rangeToRemove),
- oldCaretPos (oldCaret),
- newCaretPos (newCaret)
- {
- removedSections.addArray (oldSections);
- }
- bool perform() override
- {
- owner.remove (range, nullptr, newCaretPos);
- return true;
- }
- bool undo() override
- {
- owner.reinsert (range.getStart(), removedSections);
- owner.moveCaretTo (oldCaretPos, false);
- return true;
- }
- int getSizeInUnits() override
- {
- int n = 16;
- for (auto* s : removedSections)
- n += s->getTotalLength();
- return n;
- }
- private:
- TextEditor& owner;
- const Range<int> range;
- const int oldCaretPos, newCaretPos;
- OwnedArray<UniformTextSection> removedSections;
- JUCE_DECLARE_NON_COPYABLE (RemoveAction)
- };
- //==============================================================================
- struct TextEditor::TextHolderComponent : public Component,
- public Timer,
- public Value::Listener
- {
- TextHolderComponent (TextEditor& ed) : owner (ed)
- {
- setWantsKeyboardFocus (false);
- setInterceptsMouseClicks (false, true);
- setMouseCursor (MouseCursor::ParentCursor);
- owner.getTextValue().addListener (this);
- }
- ~TextHolderComponent() override
- {
- owner.getTextValue().removeListener (this);
- }
- void paint (Graphics& g) override
- {
- owner.drawContent (g);
- }
- void restartTimer()
- {
- startTimer (350);
- }
- void timerCallback() override
- {
- owner.timerCallbackInt();
- }
- void valueChanged (Value&) override
- {
- owner.textWasChangedByValue();
- }
- TextEditor& owner;
- JUCE_DECLARE_NON_COPYABLE (TextHolderComponent)
- };
- //==============================================================================
- struct TextEditor::TextEditorViewport : public Viewport
- {
- TextEditorViewport (TextEditor& ed) : owner (ed) {}
- void visibleAreaChanged (const Rectangle<int>&) override
- {
- if (! rentrant) // it's rare, but possible to get into a feedback loop as the viewport's scrollbars
- // appear and disappear, causing the wrap width to change.
- {
- auto wordWrapWidth = owner.getWordWrapWidth();
- if (wordWrapWidth != lastWordWrapWidth)
- {
- lastWordWrapWidth = wordWrapWidth;
- rentrant = true;
- owner.updateTextHolderSize();
- rentrant = false;
- }
- }
- }
- private:
- TextEditor& owner;
- float lastWordWrapWidth = 0;
- bool rentrant = false;
- JUCE_DECLARE_NON_COPYABLE (TextEditorViewport)
- };
- //==============================================================================
- namespace TextEditorDefs
- {
- const int textChangeMessageId = 0x10003001;
- const int returnKeyMessageId = 0x10003002;
- const int escapeKeyMessageId = 0x10003003;
- const int focusLossMessageId = 0x10003004;
- const int maxActionsPerTransaction = 100;
- static int getCharacterCategory (juce_wchar character) noexcept
- {
- return CharacterFunctions::isLetterOrDigit (character)
- ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1);
- }
- }
- //==============================================================================
- TextEditor::TextEditor (const String& name, juce_wchar passwordChar)
- : Component (name),
- passwordCharacter (passwordChar)
- {
- setMouseCursor (MouseCursor::IBeamCursor);
- viewport.reset (new TextEditorViewport (*this));
- addAndMakeVisible (viewport.get());
- viewport->setViewedComponent (textHolder = new TextHolderComponent (*this));
- viewport->setWantsKeyboardFocus (false);
- viewport->setScrollBarsShown (false, false);
- setWantsKeyboardFocus (true);
- recreateCaret();
- }
- TextEditor::~TextEditor()
- {
- if (wasFocused)
- if (auto* peer = getPeer())
- peer->dismissPendingTextInput();
- textValue.removeListener (textHolder);
- textValue.referTo (Value());
- viewport.reset();
- textHolder = nullptr;
- }
- //==============================================================================
- void TextEditor::newTransaction()
- {
- lastTransactionTime = Time::getApproximateMillisecondCounter();
- undoManager.beginNewTransaction();
- }
- bool TextEditor::undoOrRedo (const bool shouldUndo)
- {
- if (! isReadOnly())
- {
- newTransaction();
- if (shouldUndo ? undoManager.undo()
- : undoManager.redo())
- {
- scrollToMakeSureCursorIsVisible();
- repaint();
- textChanged();
- return true;
- }
- }
- return false;
- }
- bool TextEditor::undo() { return undoOrRedo (true); }
- bool TextEditor::redo() { return undoOrRedo (false); }
- //==============================================================================
- void TextEditor::setMultiLine (const bool shouldBeMultiLine,
- const bool shouldWordWrap)
- {
- if (multiline != shouldBeMultiLine
- || wordWrap != (shouldWordWrap && shouldBeMultiLine))
- {
- multiline = shouldBeMultiLine;
- wordWrap = shouldWordWrap && shouldBeMultiLine;
- viewport->setScrollBarsShown (scrollbarVisible && multiline,
- scrollbarVisible && multiline);
- viewport->setViewPosition (0, 0);
- resized();
- scrollToMakeSureCursorIsVisible();
- }
- }
- bool TextEditor::isMultiLine() const
- {
- return multiline;
- }
- void TextEditor::setScrollbarsShown (bool shown)
- {
- if (scrollbarVisible != shown)
- {
- scrollbarVisible = shown;
- shown = shown && isMultiLine();
- viewport->setScrollBarsShown (shown, shown);
- }
- }
- void TextEditor::setReadOnly (bool shouldBeReadOnly)
- {
- if (readOnly != shouldBeReadOnly)
- {
- readOnly = shouldBeReadOnly;
- enablementChanged();
- }
- }
- bool TextEditor::isReadOnly() const noexcept
- {
- return readOnly || ! isEnabled();
- }
- bool TextEditor::isTextInputActive() const
- {
- return ! isReadOnly();
- }
- void TextEditor::setReturnKeyStartsNewLine (bool shouldStartNewLine)
- {
- returnKeyStartsNewLine = shouldStartNewLine;
- }
- void TextEditor::setTabKeyUsedAsCharacter (bool shouldTabKeyBeUsed)
- {
- tabKeyUsed = shouldTabKeyBeUsed;
- }
- void TextEditor::setPopupMenuEnabled (bool b)
- {
- popupMenuEnabled = b;
- }
- void TextEditor::setSelectAllWhenFocused (bool b)
- {
- selectAllTextWhenFocused = b;
- }
- void TextEditor::setJustification (Justification j)
- {
- if (justification != j)
- {
- justification = j;
- resized();
- }
- }
- //==============================================================================
- void TextEditor::setFont (const Font& newFont)
- {
- currentFont = newFont;
- scrollToMakeSureCursorIsVisible();
- }
- void TextEditor::applyFontToAllText (const Font& newFont, bool changeCurrentFont)
- {
- if (changeCurrentFont)
- currentFont = newFont;
- auto overallColour = findColour (textColourId);
- for (auto* uts : sections)
- {
- uts->setFont (newFont, passwordCharacter);
- uts->colour = overallColour;
- }
- coalesceSimilarSections();
- updateTextHolderSize();
- scrollToMakeSureCursorIsVisible();
- repaint();
- }
- void TextEditor::applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour)
- {
- for (auto* uts : sections)
- uts->colour = newColour;
- if (changeCurrentTextColour)
- setColour (TextEditor::textColourId, newColour);
- else
- repaint();
- }
- void TextEditor::lookAndFeelChanged()
- {
- caret.reset();
- recreateCaret();
- repaint();
- }
- void TextEditor::parentHierarchyChanged()
- {
- lookAndFeelChanged();
- }
- void TextEditor::enablementChanged()
- {
- recreateCaret();
- repaint();
- }
- void TextEditor::setCaretVisible (bool shouldCaretBeVisible)
- {
- if (caretVisible != shouldCaretBeVisible)
- {
- caretVisible = shouldCaretBeVisible;
- recreateCaret();
- }
- }
- void TextEditor::recreateCaret()
- {
- if (isCaretVisible())
- {
- if (caret == nullptr)
- {
- caret.reset (getLookAndFeel().createCaretComponent (this));
- textHolder->addChildComponent (caret.get());
- updateCaretPosition();
- }
- }
- else
- {
- caret.reset();
- }
- }
- void TextEditor::updateCaretPosition()
- {
- if (caret != nullptr)
- caret->setCaretPosition (getCaretRectangle().translated (leftIndent, topIndent));
- }
- TextEditor::LengthAndCharacterRestriction::LengthAndCharacterRestriction (int maxLen, const String& chars)
- : allowedCharacters (chars), maxLength (maxLen)
- {
- }
- String TextEditor::LengthAndCharacterRestriction::filterNewText (TextEditor& ed, const String& newInput)
- {
- String t (newInput);
- if (allowedCharacters.isNotEmpty())
- t = t.retainCharacters (allowedCharacters);
- if (maxLength > 0)
- t = t.substring (0, maxLength - (ed.getTotalNumChars() - ed.getHighlightedRegion().getLength()));
- return t;
- }
- void TextEditor::setInputFilter (InputFilter* newFilter, bool takeOwnership)
- {
- inputFilter.set (newFilter, takeOwnership);
- }
- void TextEditor::setInputRestrictions (int maxLen, const String& chars)
- {
- setInputFilter (new LengthAndCharacterRestriction (maxLen, chars), true);
- }
- void TextEditor::setTextToShowWhenEmpty (const String& text, Colour colourToUse)
- {
- textToShowWhenEmpty = text;
- colourForTextWhenEmpty = colourToUse;
- }
- void TextEditor::setPasswordCharacter (juce_wchar newPasswordCharacter)
- {
- if (passwordCharacter != newPasswordCharacter)
- {
- passwordCharacter = newPasswordCharacter;
- applyFontToAllText (currentFont);
- }
- }
- void TextEditor::setScrollBarThickness (int newThicknessPixels)
- {
- viewport->setScrollBarThickness (newThicknessPixels);
- }
- //==============================================================================
- void TextEditor::clear()
- {
- clearInternal (nullptr);
- updateTextHolderSize();
- undoManager.clearUndoHistory();
- }
- void TextEditor::setText (const String& newText, bool sendTextChangeMessage)
- {
- auto newLength = newText.length();
- if (newLength != getTotalNumChars() || getText() != newText)
- {
- if (! sendTextChangeMessage)
- textValue.removeListener (textHolder);
- textValue = newText;
- auto oldCursorPos = caretPosition;
- bool cursorWasAtEnd = oldCursorPos >= getTotalNumChars();
- clearInternal (nullptr);
- insert (newText, 0, currentFont, findColour (textColourId), nullptr, caretPosition);
- // if you're adding text with line-feeds to a single-line text editor, it
- // ain't gonna look right!
- jassert (multiline || ! newText.containsAnyOf ("\r\n"));
- if (cursorWasAtEnd && ! isMultiLine())
- oldCursorPos = getTotalNumChars();
- moveCaretTo (oldCursorPos, false);
- if (sendTextChangeMessage)
- textChanged();
- else
- textValue.addListener (textHolder);
- updateTextHolderSize();
- scrollToMakeSureCursorIsVisible();
- undoManager.clearUndoHistory();
- repaint();
- }
- }
- //==============================================================================
- void TextEditor::updateValueFromText()
- {
- if (valueTextNeedsUpdating)
- {
- valueTextNeedsUpdating = false;
- textValue = getText();
- }
- }
- Value& TextEditor::getTextValue()
- {
- updateValueFromText();
- return textValue;
- }
- void TextEditor::textWasChangedByValue()
- {
- if (textValue.getValueSource().getReferenceCount() > 1)
- setText (textValue.getValue());
- }
- //==============================================================================
- void TextEditor::textChanged()
- {
- updateTextHolderSize();
- if (listeners.size() != 0 || onTextChange != nullptr)
- postCommandMessage (TextEditorDefs::textChangeMessageId);
- if (textValue.getValueSource().getReferenceCount() > 1)
- {
- valueTextNeedsUpdating = false;
- textValue = getText();
- }
- }
- void TextEditor::returnPressed() { postCommandMessage (TextEditorDefs::returnKeyMessageId); }
- void TextEditor::escapePressed() { postCommandMessage (TextEditorDefs::escapeKeyMessageId); }
- void TextEditor::addListener (Listener* l) { listeners.add (l); }
- void TextEditor::removeListener (Listener* l) { listeners.remove (l); }
- //==============================================================================
- void TextEditor::timerCallbackInt()
- {
- checkFocus();
- auto now = Time::getApproximateMillisecondCounter();
- if (now > lastTransactionTime + 200)
- newTransaction();
- }
- void TextEditor::checkFocus()
- {
- if (! wasFocused && hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent())
- {
- wasFocused = true;
- if (auto* peer = getPeer())
- if (! isReadOnly())
- peer->textInputRequired (peer->globalToLocal (getScreenPosition()), *this);
- }
- }
- void TextEditor::repaintText (Range<int> range)
- {
- if (! range.isEmpty())
- {
- auto lh = currentFont.getHeight();
- auto wordWrapWidth = getWordWrapWidth();
- if (wordWrapWidth > 0)
- {
- Point<float> anchor;
- Iterator i (*this);
- i.getCharPosition (range.getStart(), anchor, lh);
- auto y1 = (int) anchor.y;
- int y2;
- if (range.getEnd() >= getTotalNumChars())
- {
- y2 = textHolder->getHeight();
- }
- else
- {
- i.getCharPosition (range.getEnd(), anchor, lh);
- y2 = (int) (anchor.y + lh * 2.0f);
- }
- textHolder->repaint (0, y1, textHolder->getWidth(), y2 - y1);
- }
- }
- }
- //==============================================================================
- void TextEditor::moveCaret (int newCaretPos)
- {
- if (newCaretPos < 0)
- newCaretPos = 0;
- else
- newCaretPos = jmin (newCaretPos, getTotalNumChars());
- if (newCaretPos != getCaretPosition())
- {
- caretPosition = newCaretPos;
- textHolder->restartTimer();
- scrollToMakeSureCursorIsVisible();
- updateCaretPosition();
- }
- }
- int TextEditor::getCaretPosition() const
- {
- return caretPosition;
- }
- void TextEditor::setCaretPosition (const int newIndex)
- {
- moveCaretTo (newIndex, false);
- }
- void TextEditor::moveCaretToEnd()
- {
- setCaretPosition (std::numeric_limits<int>::max());
- }
- void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX,
- const int desiredCaretY)
- {
- updateCaretPosition();
- auto caretPos = getCaretRectangle();
- auto vx = caretPos.getX() - desiredCaretX;
- auto vy = caretPos.getY() - desiredCaretY;
- if (desiredCaretX < jmax (1, proportionOfWidth (0.05f)))
- vx += desiredCaretX - proportionOfWidth (0.2f);
- else if (desiredCaretX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
- vx += desiredCaretX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
- vx = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vx);
- if (! isMultiLine())
- {
- vy = viewport->getViewPositionY();
- }
- else
- {
- vy = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vy);
- if (desiredCaretY < 0)
- vy = jmax (0, desiredCaretY + vy);
- else if (desiredCaretY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretPos.getHeight()))
- vy += desiredCaretY + 2 + caretPos.getHeight() + topIndent - viewport->getMaximumVisibleHeight();
- }
- viewport->setViewPosition (vx, vy);
- }
- Rectangle<int> TextEditor::getCaretRectangle()
- {
- return getCaretRectangleFloat().getSmallestIntegerContainer();
- }
- Rectangle<float> TextEditor::getCaretRectangleFloat() const
- {
- Point<float> anchor;
- auto cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value)
- getCharPosition (caretPosition, anchor, cursorHeight);
- return { anchor.x, anchor.y, 2.0f, cursorHeight };
- }
- //==============================================================================
- enum { rightEdgeSpace = 2 };
- float TextEditor::getWordWrapWidth() const
- {
- return wordWrap ? getJustificationWidth()
- : std::numeric_limits<float>::max();
- }
- float TextEditor::getJustificationWidth() const
- {
- return (float) (viewport->getMaximumVisibleWidth() - (leftIndent + rightEdgeSpace + 1));
- }
- void TextEditor::updateTextHolderSize()
- {
- if (getWordWrapWidth() > 0)
- {
- float maxWidth = getJustificationWidth();
- Iterator i (*this);
- while (i.next())
- maxWidth = jmax (maxWidth, i.atomRight);
- auto w = leftIndent + roundToInt (maxWidth);
- auto h = topIndent + roundToInt (jmax (i.lineY + i.lineHeight, currentFont.getHeight()));
- textHolder->setSize (w + rightEdgeSpace, h + 1); // (allows a bit of space for the cursor to be at the right-hand-edge)
- }
- }
- int TextEditor::getTextWidth() const { return textHolder->getWidth(); }
- int TextEditor::getTextHeight() const { return textHolder->getHeight(); }
- void TextEditor::setIndents (int newLeftIndent, int newTopIndent)
- {
- leftIndent = newLeftIndent;
- topIndent = newTopIndent;
- }
- void TextEditor::setBorder (BorderSize<int> border)
- {
- borderSize = border;
- resized();
- }
- BorderSize<int> TextEditor::getBorder() const
- {
- return borderSize;
- }
- void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor)
- {
- keepCaretOnScreen = shouldScrollToShowCursor;
- }
- void TextEditor::scrollToMakeSureCursorIsVisible()
- {
- updateCaretPosition();
- if (keepCaretOnScreen)
- {
- auto viewPos = viewport->getViewPosition();
- auto caretRect = getCaretRectangle();
- auto relativeCursor = caretRect.getPosition() - viewPos;
- if (relativeCursor.x < jmax (1, proportionOfWidth (0.05f)))
- {
- viewPos.x += relativeCursor.x - proportionOfWidth (0.2f);
- }
- else if (relativeCursor.x > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
- {
- viewPos.x += relativeCursor.x + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
- }
- viewPos.x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), viewPos.x);
- if (! isMultiLine())
- {
- viewPos.y = (getHeight() - textHolder->getHeight() - topIndent) / -2;
- }
- else if (relativeCursor.y < 0)
- {
- viewPos.y = jmax (0, relativeCursor.y + viewPos.y);
- }
- else if (relativeCursor.y > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - caretRect.getHeight()))
- {
- viewPos.y += relativeCursor.y + 2 + caretRect.getHeight() + topIndent - viewport->getMaximumVisibleHeight();
- }
- viewport->setViewPosition (viewPos);
- }
- }
- void TextEditor::moveCaretTo (const int newPosition, const bool isSelecting)
- {
- if (isSelecting)
- {
- moveCaret (newPosition);
- auto oldSelection = selection;
- if (dragType == notDragging)
- {
- if (std::abs (getCaretPosition() - selection.getStart()) < std::abs (getCaretPosition() - selection.getEnd()))
- dragType = draggingSelectionStart;
- else
- dragType = draggingSelectionEnd;
- }
- if (dragType == draggingSelectionStart)
- {
- if (getCaretPosition() >= selection.getEnd())
- dragType = draggingSelectionEnd;
- selection = Range<int>::between (getCaretPosition(), selection.getEnd());
- }
- else
- {
- if (getCaretPosition() < selection.getStart())
- dragType = draggingSelectionStart;
- selection = Range<int>::between (getCaretPosition(), selection.getStart());
- }
- repaintText (selection.getUnionWith (oldSelection));
- }
- else
- {
- dragType = notDragging;
- repaintText (selection);
- moveCaret (newPosition);
- selection = Range<int>::emptyRange (getCaretPosition());
- }
- }
- int TextEditor::getTextIndexAt (const int x, const int y)
- {
- return indexAtPosition ((float) (x + viewport->getViewPositionX() - leftIndent - borderSize.getLeft()),
- (float) (y + viewport->getViewPositionY() - topIndent - borderSize.getTop()));
- }
- void TextEditor::insertTextAtCaret (const String& t)
- {
- String newText (inputFilter != nullptr ? inputFilter->filterNewText (*this, t) : t);
- if (isMultiLine())
- newText = newText.replace ("\r\n", "\n");
- else
- newText = newText.replaceCharacters ("\r\n", " ");
- const int insertIndex = selection.getStart();
- const int newCaretPos = insertIndex + newText.length();
- remove (selection, getUndoManager(),
- newText.isNotEmpty() ? newCaretPos - 1 : newCaretPos);
- insert (newText, insertIndex, currentFont, findColour (textColourId),
- getUndoManager(), newCaretPos);
- textChanged();
- }
- void TextEditor::setHighlightedRegion (const Range<int>& newSelection)
- {
- moveCaretTo (newSelection.getStart(), false);
- moveCaretTo (newSelection.getEnd(), true);
- }
- //==============================================================================
- void TextEditor::copy()
- {
- if (passwordCharacter == 0)
- {
- auto selectedText = getHighlightedText();
- if (selectedText.isNotEmpty())
- SystemClipboard::copyTextToClipboard (selectedText);
- }
- }
- void TextEditor::paste()
- {
- if (! isReadOnly())
- {
- auto clip = SystemClipboard::getTextFromClipboard();
- if (clip.isNotEmpty())
- insertTextAtCaret (clip);
- }
- }
- void TextEditor::cut()
- {
- if (! isReadOnly())
- {
- moveCaret (selection.getEnd());
- insertTextAtCaret (String());
- }
- }
- //==============================================================================
- void TextEditor::drawContent (Graphics& g)
- {
- if (getWordWrapWidth() > 0)
- {
- g.setOrigin (leftIndent, topIndent);
- auto clip = g.getClipBounds();
- Colour selectedTextColour;
- Iterator i (*this);
- if (! selection.isEmpty())
- {
- Iterator i2 (i);
- RectangleList<float> selectionArea;
- while (i2.next() && i2.lineY < clip.getBottom())
- {
- if (i2.lineY + i2.lineHeight >= clip.getY()
- && selection.intersects ({ i2.indexInText, i2.indexInText + i2.atom->numChars }))
- {
- i2.addSelection (selectionArea, selection);
- }
- }
- g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f));
- g.fillRectList (selectionArea);
- selectedTextColour = findColour (highlightedTextColourId);
- }
- const UniformTextSection* lastSection = nullptr;
- while (i.next() && i.lineY < clip.getBottom())
- {
- if (i.lineY + i.lineHeight >= clip.getY())
- {
- if (selection.intersects ({ i.indexInText, i.indexInText + i.atom->numChars }))
- {
- i.drawSelectedText (g, selection, selectedTextColour);
- lastSection = nullptr;
- }
- else
- {
- i.draw (g, lastSection);
- }
- }
- }
- for (auto& underlinedSection : underlinedSections)
- {
- Iterator i2 (*this);
- while (i2.next() && i2.lineY < clip.getBottom())
- {
- if (i2.lineY + i2.lineHeight >= clip.getY()
- && underlinedSection.intersects ({ i2.indexInText, i2.indexInText + i2.atom->numChars }))
- {
- i2.drawUnderline (g, underlinedSection, findColour (textColourId));
- }
- }
- }
- }
- }
- void TextEditor::paint (Graphics& g)
- {
- getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this);
- }
- void TextEditor::paintOverChildren (Graphics& g)
- {
- if (textToShowWhenEmpty.isNotEmpty()
- && (! hasKeyboardFocus (false))
- && getTotalNumChars() == 0)
- {
- g.setColour (colourForTextWhenEmpty);
- g.setFont (getFont());
- if (isMultiLine())
- g.drawText (textToShowWhenEmpty, getLocalBounds(),
- Justification::centred, true);
- else
- g.drawText (textToShowWhenEmpty,
- leftIndent, 0, viewport->getWidth() - leftIndent, getHeight(),
- Justification::centredLeft, true);
- }
- getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this);
- }
- //==============================================================================
- void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*)
- {
- const bool writable = ! isReadOnly();
- if (passwordCharacter == 0)
- {
- m.addItem (StandardApplicationCommandIDs::cut, TRANS("Cut"), writable);
- m.addItem (StandardApplicationCommandIDs::copy, TRANS("Copy"), ! selection.isEmpty());
- }
- m.addItem (StandardApplicationCommandIDs::paste, TRANS("Paste"), writable);
- m.addItem (StandardApplicationCommandIDs::del, TRANS("Delete"), writable);
- m.addSeparator();
- m.addItem (StandardApplicationCommandIDs::selectAll, TRANS("Select All"));
- m.addSeparator();
- if (getUndoManager() != nullptr)
- {
- m.addItem (StandardApplicationCommandIDs::undo, TRANS("Undo"), undoManager.canUndo());
- m.addItem (StandardApplicationCommandIDs::redo, TRANS("Redo"), undoManager.canRedo());
- }
- }
- void TextEditor::performPopupMenuAction (const int menuItemID)
- {
- switch (menuItemID)
- {
- case StandardApplicationCommandIDs::cut: cutToClipboard(); break;
- case StandardApplicationCommandIDs::copy: copyToClipboard(); break;
- case StandardApplicationCommandIDs::paste: pasteFromClipboard(); break;
- case StandardApplicationCommandIDs::del: cut(); break;
- case StandardApplicationCommandIDs::selectAll: selectAll(); break;
- case StandardApplicationCommandIDs::undo: undo(); break;
- case StandardApplicationCommandIDs::redo: redo(); break;
- default: break;
- }
- }
- //==============================================================================
- void TextEditor::mouseDown (const MouseEvent& e)
- {
- beginDragAutoRepeat (100);
- newTransaction();
- if (wasFocused || ! selectAllTextWhenFocused)
- {
- if (! (popupMenuEnabled && e.mods.isPopupMenu()))
- {
- moveCaretTo (getTextIndexAt (e.x, e.y),
- e.mods.isShiftDown());
- }
- else
- {
- PopupMenu m;
- m.setLookAndFeel (&getLookAndFeel());
- addPopupMenuItems (m, &e);
- menuActive = true;
- SafePointer<TextEditor> safeThis (this);
- m.showMenuAsync (PopupMenu::Options(),
- [safeThis] (int menuResult)
- {
- if (auto* editor = safeThis.getComponent())
- {
- editor->menuActive = false;
- if (menuResult != 0)
- editor->performPopupMenuAction (menuResult);
- }
- });
- }
- }
- }
- void TextEditor::mouseDrag (const MouseEvent& e)
- {
- if (wasFocused || ! selectAllTextWhenFocused)
- if (! (popupMenuEnabled && e.mods.isPopupMenu()))
- moveCaretTo (getTextIndexAt (e.x, e.y), true);
- }
- void TextEditor::mouseUp (const MouseEvent& e)
- {
- newTransaction();
- textHolder->restartTimer();
- if (wasFocused || ! selectAllTextWhenFocused)
- if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu()))
- moveCaret (getTextIndexAt (e.x, e.y));
- wasFocused = true;
- }
- void TextEditor::mouseDoubleClick (const MouseEvent& e)
- {
- int tokenEnd = getTextIndexAt (e.x, e.y);
- int tokenStart = 0;
- if (e.getNumberOfClicks() > 3)
- {
- tokenEnd = getTotalNumChars();
- }
- else
- {
- auto t = getText();
- auto totalLength = getTotalNumChars();
- while (tokenEnd < totalLength)
- {
- auto c = t[tokenEnd];
- // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
- if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
- ++tokenEnd;
- else
- break;
- }
- tokenStart = tokenEnd;
- while (tokenStart > 0)
- {
- auto c = t[tokenStart - 1];
- // (note the slight bodge here - it's because iswalnum only checks for alphabetic chars in the current locale)
- if (CharacterFunctions::isLetterOrDigit (c) || c > 128)
- --tokenStart;
- else
- break;
- }
- if (e.getNumberOfClicks() > 2)
- {
- while (tokenEnd < totalLength)
- {
- auto c = t[tokenEnd];
- if (c != '\r' && c != '\n')
- ++tokenEnd;
- else
- break;
- }
- while (tokenStart > 0)
- {
- auto c = t[tokenStart - 1];
- if (c != '\r' && c != '\n')
- --tokenStart;
- else
- break;
- }
- }
- }
- moveCaretTo (tokenEnd, false);
- moveCaretTo (tokenStart, true);
- }
- void TextEditor::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
- {
- if (! viewport->useMouseWheelMoveIfNeeded (e, wheel))
- Component::mouseWheelMove (e, wheel);
- }
- //==============================================================================
- bool TextEditor::moveCaretWithTransaction (const int newPos, const bool selecting)
- {
- newTransaction();
- moveCaretTo (newPos, selecting);
- return true;
- }
- bool TextEditor::moveCaretLeft (bool moveInWholeWordSteps, bool selecting)
- {
- auto pos = getCaretPosition();
- if (moveInWholeWordSteps)
- pos = findWordBreakBefore (pos);
- else
- --pos;
- return moveCaretWithTransaction (pos, selecting);
- }
- bool TextEditor::moveCaretRight (bool moveInWholeWordSteps, bool selecting)
- {
- auto pos = getCaretPosition();
- if (moveInWholeWordSteps)
- pos = findWordBreakAfter (pos);
- else
- ++pos;
- return moveCaretWithTransaction (pos, selecting);
- }
- bool TextEditor::moveCaretUp (bool selecting)
- {
- if (! isMultiLine())
- return moveCaretToStartOfLine (selecting);
- auto caretPos = getCaretRectangleFloat();
- return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - 1.0f), selecting);
- }
- bool TextEditor::moveCaretDown (bool selecting)
- {
- if (! isMultiLine())
- return moveCaretToEndOfLine (selecting);
- auto caretPos = getCaretRectangleFloat();
- return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting);
- }
- bool TextEditor::pageUp (bool selecting)
- {
- if (! isMultiLine())
- return moveCaretToStartOfLine (selecting);
- auto caretPos = getCaretRectangleFloat();
- return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - viewport->getViewHeight()), selecting);
- }
- bool TextEditor::pageDown (bool selecting)
- {
- if (! isMultiLine())
- return moveCaretToEndOfLine (selecting);
- auto caretPos = getCaretRectangleFloat();
- return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + viewport->getViewHeight()), selecting);
- }
- void TextEditor::scrollByLines (int deltaLines)
- {
- viewport->getVerticalScrollBar().moveScrollbarInSteps (deltaLines);
- }
- bool TextEditor::scrollDown()
- {
- scrollByLines (-1);
- return true;
- }
- bool TextEditor::scrollUp()
- {
- scrollByLines (1);
- return true;
- }
- bool TextEditor::moveCaretToTop (bool selecting)
- {
- return moveCaretWithTransaction (0, selecting);
- }
- bool TextEditor::moveCaretToStartOfLine (bool selecting)
- {
- auto caretPos = getCaretRectangleFloat();
- return moveCaretWithTransaction (indexAtPosition (0.0f, caretPos.getY()), selecting);
- }
- bool TextEditor::moveCaretToEnd (bool selecting)
- {
- return moveCaretWithTransaction (getTotalNumChars(), selecting);
- }
- bool TextEditor::moveCaretToEndOfLine (bool selecting)
- {
- auto caretPos = getCaretRectangleFloat();
- return moveCaretWithTransaction (indexAtPosition ((float) textHolder->getWidth(), caretPos.getY()), selecting);
- }
- bool TextEditor::deleteBackwards (bool moveInWholeWordSteps)
- {
- if (moveInWholeWordSteps)
- moveCaretTo (findWordBreakBefore (getCaretPosition()), true);
- else if (selection.isEmpty() && selection.getStart() > 0)
- selection = { selection.getEnd() - 1, selection.getEnd() };
- cut();
- return true;
- }
- bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/)
- {
- if (selection.isEmpty() && selection.getStart() < getTotalNumChars())
- selection = { selection.getStart(), selection.getStart() + 1 };
- cut();
- return true;
- }
- bool TextEditor::copyToClipboard()
- {
- newTransaction();
- copy();
- return true;
- }
- bool TextEditor::cutToClipboard()
- {
- newTransaction();
- copy();
- cut();
- return true;
- }
- bool TextEditor::pasteFromClipboard()
- {
- newTransaction();
- paste();
- return true;
- }
- bool TextEditor::selectAll()
- {
- newTransaction();
- moveCaretTo (getTotalNumChars(), false);
- moveCaretTo (0, true);
- return true;
- }
- //==============================================================================
- void TextEditor::setEscapeAndReturnKeysConsumed (bool shouldBeConsumed) noexcept
- {
- consumeEscAndReturnKeys = shouldBeConsumed;
- }
- bool TextEditor::keyPressed (const KeyPress& key)
- {
- if (isReadOnly() && key != KeyPress ('c', ModifierKeys::commandModifier, 0)
- && key != KeyPress ('a', ModifierKeys::commandModifier, 0))
- return false;
- if (! TextEditorKeyMapper<TextEditor>::invokeKeyFunction (*this, key))
- {
- if (key == KeyPress::returnKey)
- {
- newTransaction();
- if (returnKeyStartsNewLine)
- {
- insertTextAtCaret ("\n");
- }
- else
- {
- returnPressed();
- return consumeEscAndReturnKeys;
- }
- }
- else if (key.isKeyCode (KeyPress::escapeKey))
- {
- newTransaction();
- moveCaretTo (getCaretPosition(), false);
- escapePressed();
- return consumeEscAndReturnKeys;
- }
- else if (key.getTextCharacter() >= ' '
- || (tabKeyUsed && (key.getTextCharacter() == '\t')))
- {
- insertTextAtCaret (String::charToString (key.getTextCharacter()));
- lastTransactionTime = Time::getApproximateMillisecondCounter();
- }
- else
- {
- return false;
- }
- }
- return true;
- }
- bool TextEditor::keyStateChanged (const bool isKeyDown)
- {
- if (! isKeyDown)
- return false;
- #if JUCE_WINDOWS
- if (KeyPress (KeyPress::F4Key, ModifierKeys::altModifier, 0).isCurrentlyDown())
- return false; // We need to explicitly allow alt-F4 to pass through on Windows
- #endif
- if ((! consumeEscAndReturnKeys)
- && (KeyPress (KeyPress::escapeKey).isCurrentlyDown()
- || KeyPress (KeyPress::returnKey).isCurrentlyDown()))
- return false;
- // (overridden to avoid forwarding key events to the parent)
- return ! ModifierKeys::currentModifiers.isCommandDown();
- }
- //==============================================================================
- void TextEditor::focusGained (FocusChangeType)
- {
- newTransaction();
- if (selectAllTextWhenFocused)
- {
- moveCaretTo (0, false);
- moveCaretTo (getTotalNumChars(), true);
- }
- checkFocus();
- repaint();
- updateCaretPosition();
- }
- void TextEditor::focusLost (FocusChangeType)
- {
- newTransaction();
- wasFocused = false;
- textHolder->stopTimer();
- underlinedSections.clear();
- if (auto* peer = getPeer())
- peer->dismissPendingTextInput();
- updateCaretPosition();
- postCommandMessage (TextEditorDefs::focusLossMessageId);
- repaint();
- }
- //==============================================================================
- void TextEditor::resized()
- {
- viewport->setBoundsInset (borderSize);
- viewport->setSingleStepSizes (16, roundToInt (currentFont.getHeight()));
- updateTextHolderSize();
- if (isMultiLine())
- updateCaretPosition();
- else
- scrollToMakeSureCursorIsVisible();
- }
- void TextEditor::handleCommandMessage (const int commandId)
- {
- Component::BailOutChecker checker (this);
- switch (commandId)
- {
- case TextEditorDefs::textChangeMessageId:
- listeners.callChecked (checker, [this] (Listener& l) { l.textEditorTextChanged (*this); });
- if (! checker.shouldBailOut() && onTextChange != nullptr)
- onTextChange();
- break;
- case TextEditorDefs::returnKeyMessageId:
- listeners.callChecked (checker, [this] (Listener& l) { l.textEditorReturnKeyPressed (*this); });
- if (! checker.shouldBailOut() && onReturnKey != nullptr)
- onReturnKey();
- break;
- case TextEditorDefs::escapeKeyMessageId:
- listeners.callChecked (checker, [this] (Listener& l) { l.textEditorEscapeKeyPressed (*this); });
- if (! checker.shouldBailOut() && onEscapeKey != nullptr)
- onEscapeKey();
- break;
- case TextEditorDefs::focusLossMessageId:
- updateValueFromText();
- listeners.callChecked (checker, [this] (Listener& l) { l.textEditorFocusLost (*this); });
- if (! checker.shouldBailOut() && onFocusLost != nullptr)
- onFocusLost();
- break;
- default:
- jassertfalse;
- break;
- }
- }
- void TextEditor::setTemporaryUnderlining (const Array<Range<int>>& newUnderlinedSections)
- {
- underlinedSections = newUnderlinedSections;
- repaint();
- }
- //==============================================================================
- UndoManager* TextEditor::getUndoManager() noexcept
- {
- return readOnly ? nullptr : &undoManager;
- }
- void TextEditor::clearInternal (UndoManager* const um)
- {
- remove ({ 0, getTotalNumChars() }, um, caretPosition);
- }
- void TextEditor::insert (const String& text, int insertIndex, const Font& font,
- Colour colour, UndoManager* um, int caretPositionToMoveTo)
- {
- if (text.isNotEmpty())
- {
- if (um != nullptr)
- {
- if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
- newTransaction();
- um->perform (new InsertAction (*this, text, insertIndex, font, colour,
- caretPosition, caretPositionToMoveTo));
- }
- else
- {
- repaintText ({ insertIndex, getTotalNumChars() }); // must do this before and after changing the data, in case
- // a line gets moved due to word wrap
- int index = 0;
- int nextIndex = 0;
- for (int i = 0; i < sections.size(); ++i)
- {
- nextIndex = index + sections.getUnchecked (i)->getTotalLength();
- if (insertIndex == index)
- {
- sections.insert (i, new UniformTextSection (text, font, colour, passwordCharacter));
- break;
- }
- if (insertIndex > index && insertIndex < nextIndex)
- {
- splitSection (i, insertIndex - index);
- sections.insert (i + 1, new UniformTextSection (text, font, colour, passwordCharacter));
- break;
- }
- index = nextIndex;
- }
- if (nextIndex == insertIndex)
- sections.add (new UniformTextSection (text, font, colour, passwordCharacter));
- coalesceSimilarSections();
- totalNumChars = -1;
- valueTextNeedsUpdating = true;
- updateTextHolderSize();
- moveCaretTo (caretPositionToMoveTo, false);
- repaintText ({ insertIndex, getTotalNumChars() });
- }
- }
- }
- void TextEditor::reinsert (int insertIndex, const OwnedArray<UniformTextSection>& sectionsToInsert)
- {
- int index = 0;
- int nextIndex = 0;
- for (int i = 0; i < sections.size(); ++i)
- {
- nextIndex = index + sections.getUnchecked (i)->getTotalLength();
- if (insertIndex == index)
- {
- for (int j = sectionsToInsert.size(); --j >= 0;)
- sections.insert (i, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
- break;
- }
- if (insertIndex > index && insertIndex < nextIndex)
- {
- splitSection (i, insertIndex - index);
- for (int j = sectionsToInsert.size(); --j >= 0;)
- sections.insert (i + 1, new UniformTextSection (*sectionsToInsert.getUnchecked(j)));
- break;
- }
- index = nextIndex;
- }
- if (nextIndex == insertIndex)
- for (auto* s : sectionsToInsert)
- sections.add (new UniformTextSection (*s));
- coalesceSimilarSections();
- totalNumChars = -1;
- valueTextNeedsUpdating = true;
- }
- void TextEditor::remove (Range<int> range, UndoManager* const um, const int caretPositionToMoveTo)
- {
- if (! range.isEmpty())
- {
- int index = 0;
- for (int i = 0; i < sections.size(); ++i)
- {
- auto nextIndex = index + sections.getUnchecked(i)->getTotalLength();
- if (range.getStart() > index && range.getStart() < nextIndex)
- {
- splitSection (i, range.getStart() - index);
- --i;
- }
- else if (range.getEnd() > index && range.getEnd() < nextIndex)
- {
- splitSection (i, range.getEnd() - index);
- --i;
- }
- else
- {
- index = nextIndex;
- if (index > range.getEnd())
- break;
- }
- }
- index = 0;
- if (um != nullptr)
- {
- Array<UniformTextSection*> removedSections;
- for (auto* section : sections)
- {
- if (range.getEnd() <= range.getStart())
- break;
- auto nextIndex = index + section->getTotalLength();
- if (range.getStart() <= index && range.getEnd() >= nextIndex)
- removedSections.add (new UniformTextSection (*section));
- index = nextIndex;
- }
- if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction)
- newTransaction();
- um->perform (new RemoveAction (*this, range, caretPosition,
- caretPositionToMoveTo, removedSections));
- }
- else
- {
- auto remainingRange = range;
- for (int i = 0; i < sections.size(); ++i)
- {
- auto* section = sections.getUnchecked (i);
- auto nextIndex = index + section->getTotalLength();
- if (remainingRange.getStart() <= index && remainingRange.getEnd() >= nextIndex)
- {
- sections.remove (i);
- remainingRange.setEnd (remainingRange.getEnd() - (nextIndex - index));
- if (remainingRange.isEmpty())
- break;
- --i;
- }
- else
- {
- index = nextIndex;
- }
- }
- coalesceSimilarSections();
- totalNumChars = -1;
- valueTextNeedsUpdating = true;
- moveCaretTo (caretPositionToMoveTo, false);
- repaintText ({ range.getStart(), getTotalNumChars() });
- }
- }
- }
- //==============================================================================
- String TextEditor::getText() const
- {
- MemoryOutputStream mo;
- mo.preallocate ((size_t) getTotalNumChars());
- for (auto* s : sections)
- s->appendAllText (mo);
- return mo.toUTF8();
- }
- String TextEditor::getTextInRange (const Range<int>& range) const
- {
- if (range.isEmpty())
- return {};
- MemoryOutputStream mo;
- mo.preallocate ((size_t) jmin (getTotalNumChars(), range.getLength()));
- int index = 0;
- for (auto* s : sections)
- {
- auto nextIndex = index + s->getTotalLength();
- if (range.getStart() < nextIndex)
- {
- if (range.getEnd() <= index)
- break;
- s->appendSubstring (mo, range - index);
- }
- index = nextIndex;
- }
- return mo.toUTF8();
- }
- String TextEditor::getHighlightedText() const
- {
- return getTextInRange (selection);
- }
- int TextEditor::getTotalNumChars() const
- {
- if (totalNumChars < 0)
- {
- totalNumChars = 0;
- for (auto* s : sections)
- totalNumChars += s->getTotalLength();
- }
- return totalNumChars;
- }
- bool TextEditor::isEmpty() const
- {
- return getTotalNumChars() == 0;
- }
- void TextEditor::getCharPosition (int index, Point<float>& anchor, float& lineHeight) const
- {
- if (getWordWrapWidth() <= 0)
- {
- anchor = {};
- lineHeight = currentFont.getHeight();
- }
- else
- {
- Iterator i (*this);
- if (sections.isEmpty())
- {
- anchor = { i.getJustificationOffset (0), 0 };
- lineHeight = currentFont.getHeight();
- }
- else
- {
- i.getCharPosition (index, anchor, lineHeight);
- }
- }
- }
- int TextEditor::indexAtPosition (const float x, const float y)
- {
- if (getWordWrapWidth() > 0)
- {
- for (Iterator i (*this); i.next();)
- {
- if (y < i.lineY + i.lineHeight)
- {
- if (y < i.lineY)
- return jmax (0, i.indexInText - 1);
- if (x <= i.atomX || i.atom->isNewLine())
- return i.indexInText;
- if (x < i.atomRight)
- return i.xToIndex (x);
- }
- }
- }
- return getTotalNumChars();
- }
- //==============================================================================
- int TextEditor::findWordBreakAfter (const int position) const
- {
- auto t = getTextInRange ({ position, position + 512 });
- auto totalLength = t.length();
- int i = 0;
- while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
- ++i;
- auto type = TextEditorDefs::getCharacterCategory (t[i]);
- while (i < totalLength && type == TextEditorDefs::getCharacterCategory (t[i]))
- ++i;
- while (i < totalLength && CharacterFunctions::isWhitespace (t[i]))
- ++i;
- return position + i;
- }
- int TextEditor::findWordBreakBefore (const int position) const
- {
- if (position <= 0)
- return 0;
- auto startOfBuffer = jmax (0, position - 512);
- auto t = getTextInRange ({ startOfBuffer, position });
- int i = position - startOfBuffer;
- while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1]))
- --i;
- if (i > 0)
- {
- auto type = TextEditorDefs::getCharacterCategory (t [i - 1]);
- while (i > 0 && type == TextEditorDefs::getCharacterCategory (t [i - 1]))
- --i;
- }
- jassert (startOfBuffer + i >= 0);
- return startOfBuffer + i;
- }
- //==============================================================================
- void TextEditor::splitSection (const int sectionIndex, const int charToSplitAt)
- {
- jassert (sections[sectionIndex] != nullptr);
- sections.insert (sectionIndex + 1,
- sections.getUnchecked (sectionIndex)->split (charToSplitAt));
- }
- void TextEditor::coalesceSimilarSections()
- {
- for (int i = 0; i < sections.size() - 1; ++i)
- {
- auto* s1 = sections.getUnchecked (i);
- auto* s2 = sections.getUnchecked (i + 1);
- if (s1->font == s2->font
- && s1->colour == s2->colour)
- {
- s1->append (*s2);
- sections.remove (i + 1);
- --i;
- }
- }
- }
- } // namespace juce
|