diff --git a/CHANGELOG.md b/CHANGELOG.md index d10d3ddde..3905d9393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Minor: Removed duplicate setting for toggling `Channel Point Redeemed Message` highlights (#3296) - Minor: Added support for opening channels from twitch.tv/popout links. (#3309) - Minor: Clean up chat messages of special line characters prior to sending. (#3312) +- Minor: IRC now parses/displays links like Twitch chat. (#3334) - Minor: Added button & label for copying login name of user instead of display name in the user info popout. (#3335) - Bugfix: Fixed colored usernames sometimes not working. (#3170) - Bugfix: Restored ability to send duplicate `/me` messages. (#3166) diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index e1edbcc0e..b369d18b9 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -1,7 +1,6 @@ #include "messages/MessageElement.hpp" #include "Application.hpp" -#include "common/IrcColors.hpp" #include "debug/Benchmark.hpp" #include "messages/Emote.hpp" #include "messages/layouts/MessageLayoutContainer.hpp" @@ -12,14 +11,6 @@ namespace chatterino { -namespace { - - QRegularExpression IRC_COLOR_PARSE_REGEX( - "(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)", - QRegularExpression::UseUnicodePropertiesOption); - -} // namespace - MessageElement::MessageElement(MessageElementFlags flags) : flags_(flags) { @@ -472,252 +463,6 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container, } } -// TEXT -// IrcTextElement gets its color from the color code in the message, and can change from character to character. -// This differs from the TextElement -IrcTextElement::IrcTextElement(const QString &fullText, - MessageElementFlags flags, FontStyle style) - : MessageElement(flags) - , style_(style) -{ - assert(IRC_COLOR_PARSE_REGEX.isValid()); - - // Default pen colors. -1 = default theme colors - int fg = -1, bg = -1; - - // Split up the message in words (space separated) - // Each word contains one or more colored segments. - // The color of that segment is "global", as in it can be decided by the word before it. - for (const auto &text : fullText.split(' ')) - { - std::vector segments; - - int pos = 0; - int lastPos = 0; - - auto i = IRC_COLOR_PARSE_REGEX.globalMatch(text); - - while (i.hasNext()) - { - auto match = i.next(); - - if (lastPos != match.capturedStart() && match.capturedStart() != 0) - { - auto seg = Segment{}; - seg.text = text.mid(lastPos, match.capturedStart() - lastPos); - seg.fg = fg; - seg.bg = bg; - segments.emplace_back(seg); - lastPos = match.capturedStart() + match.capturedLength(); - } - if (!match.captured(1).isEmpty()) - { - fg = -1; - bg = -1; - } - - if (!match.captured(2).isEmpty()) - { - fg = match.captured(2).toInt(nullptr); - } - else - { - fg = -1; - } - if (!match.captured(4).isEmpty()) - { - bg = match.captured(4).toInt(nullptr); - } - else if (fg == -1) - { - bg = -1; - } - - lastPos = match.capturedStart() + match.capturedLength(); - } - - auto seg = Segment{}; - seg.text = text.mid(lastPos); - seg.fg = fg; - seg.bg = bg; - segments.emplace_back(seg); - - QString n(text); - - n.replace(IRC_COLOR_PARSE_REGEX, ""); - - Word w{ - n, - -1, - segments, - }; - this->words_.emplace_back(w); - } -} - -void IrcTextElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) -{ - auto app = getApp(); - - MessageColor defaultColorType = MessageColor::Text; - auto defaultColor = defaultColorType.getColor(*app->themes); - if (flags.hasAny(this->getFlags())) - { - QFontMetrics metrics = - app->fonts->getFontMetrics(this->style_, container.getScale()); - - for (auto &word : this->words_) - { - auto getTextLayoutElement = [&](QString text, - std::vector segments, - int width, bool hasTrailingSpace) { - std::vector xd{}; - - for (const auto &segment : segments) - { - QColor color = defaultColor; - if (segment.fg >= 0 && segment.fg <= 98) - { - color = IRC_COLORS[segment.fg]; - } - app->themes->normalizeColor(color); - xd.emplace_back(PajSegment{segment.text, color}); - } - - auto e = (new MultiColorTextLayoutElement( - *this, text, QSize(width, metrics.height()), xd, - this->style_, container.getScale())) - ->setLink(this->getLink()); - e->setTrailingSpace(true); - e->setText(text); - - // If URL link was changed, - // Should update it in MessageLayoutElement too! - if (this->getLink().type == Link::Url) - { - static_cast(e)->listenToLinkChanges(); - } - return e; - }; - - // fourtf: add again - // if (word.width == -1) { - word.width = metrics.horizontalAdvance(word.text); - // } - - // see if the text fits in the current line - if (container.fitsInLine(word.width)) - { - container.addElementNoLineBreak( - getTextLayoutElement(word.text, word.segments, word.width, - this->hasTrailingSpace())); - continue; - } - - // see if the text fits in the next line - if (!container.atStartOfLine()) - { - container.breakLine(); - - if (container.fitsInLine(word.width)) - { - container.addElementNoLineBreak(getTextLayoutElement( - word.text, word.segments, word.width, - this->hasTrailingSpace())); - continue; - } - } - - // The word does not fit on a new line, we need to wrap it - QString text = word.text; - std::vector segments = word.segments; - int textLength = text.length(); - int wordStart = 0; - int width = 0; - - // QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1 - - // XXX(pajlada): NOT TESTED - for (int i = 0; i < textLength; i++) - { - if (!container.canAddElements()) - { - // The container does not allow any more elements to be added, stop here - break; - } - - auto isSurrogate = text.size() > i + 1 && - QChar::isHighSurrogate(text[i].unicode()); - - auto charWidth = isSurrogate - ? metrics.horizontalAdvance(text.mid(i, 2)) - : metrics.horizontalAdvance(text[i]); - - if (!container.fitsInLine(width + charWidth)) - { - std::vector pieceSegments; - int charactersLeft = i - wordStart; - - for (auto segmentIt = segments.begin(); - segmentIt != segments.end();) - { - auto &segment = *segmentIt; - if (charactersLeft >= segment.text.length()) - { - // Entire segment fits in this piece - pieceSegments.push_back(segment); - charactersLeft -= segment.text.length(); - segmentIt = segments.erase(segmentIt); - - assert(charactersLeft >= 0); - - if (charactersLeft == 0) - { - break; - } - } - else - { - // Only part of the segment fits in this piece - // We create a new segment with the characters that fit, and modify the segment we checked to only contain the characters we didn't consume - Segment segmentThatFitsInPiece{ - segment.text.left(charactersLeft), segment.fg, - segment.bg}; - pieceSegments.emplace_back(segmentThatFitsInPiece); - segment.text = segment.text.mid(charactersLeft); - - break; - } - } - - container.addElementNoLineBreak( - getTextLayoutElement(text.mid(wordStart, i - wordStart), - pieceSegments, width, false)); - container.breakLine(); - - wordStart = i; - width = charWidth; - - if (isSurrogate) - i++; - continue; - } - - width += charWidth; - - if (isSurrogate) - i++; - } - - // Add last remaining text & segments - container.addElementNoLineBreak( - getTextLayoutElement(text.mid(wordStart), segments, width, - this->hasTrailingSpace())); - } - } -} - LinebreakElement::LinebreakElement(MessageElementFlags flags) : MessageElement(flags) { diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 295157e17..5739aada2 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -333,36 +333,6 @@ public: MessageElementFlags flags) override; }; -// contains a full message string that's split into words on space and parses IRC colors that are then put into segments -// these segments are later passed to "MultiColorTextLayoutElement" elements to be rendered :) -class IrcTextElement : public MessageElement -{ -public: - IrcTextElement(const QString &text, MessageElementFlags flags, - FontStyle style = FontStyle::ChatMedium); - ~IrcTextElement() override = default; - - void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; - -private: - FontStyle style_; - - struct Segment { - QString text; - int fg = -1; - int bg = -1; - }; - - struct Word { - QString text; - int width = -1; - std::vector segments; - }; - - std::vector words_; -}; - // Forces a linebreak class LinebreakElement : public MessageElement { diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index 923d0b3c2..89e13b485 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -405,40 +405,4 @@ int TextIconLayoutElement::getXFromIndex(int index) } } -// -// TEXT -// - -MultiColorTextLayoutElement::MultiColorTextLayoutElement( - MessageElement &_creator, QString &_text, const QSize &_size, - std::vector segments, FontStyle _style, float _scale) - : TextLayoutElement(_creator, _text, _size, QColor{}, _style, _scale) - , segments_(segments) -{ - this->setText(_text); -} - -void MultiColorTextLayoutElement::paint(QPainter &painter) -{ - auto app = getApp(); - - painter.setPen(this->color_); - - painter.setFont(app->fonts->getFont(this->style_, this->scale_)); - - int xOffset = 0; - - auto metrics = app->fonts->getFontMetrics(this->style_, this->scale_); - - for (const auto &segment : this->segments_) - { - painter.setPen(segment.color); - painter.drawText(QRectF(this->getRect().x() + xOffset, - this->getRect().y(), 10000, 10000), - segment.text, - QTextOption(Qt::AlignLeft | Qt::AlignTop)); - xOffset += metrics.horizontalAdvance(segment.text); - } -} - } // namespace chatterino diff --git a/src/messages/layouts/MessageLayoutElement.hpp b/src/messages/layouts/MessageLayoutElement.hpp index 2695eabfe..8cdc6a948 100644 --- a/src/messages/layouts/MessageLayoutElement.hpp +++ b/src/messages/layouts/MessageLayoutElement.hpp @@ -140,25 +140,4 @@ private: QString line2; }; -struct PajSegment { - QString text; - QColor color; -}; - -// TEXT -class MultiColorTextLayoutElement : public TextLayoutElement -{ -public: - MultiColorTextLayoutElement(MessageElement &creator_, QString &text, - const QSize &size, - std::vector segments, - FontStyle style_, float scale_); - -protected: - void paint(QPainter &painter) override; - -private: - std::vector segments_; -}; - } // namespace chatterino diff --git a/src/providers/irc/IrcMessageBuilder.cpp b/src/providers/irc/IrcMessageBuilder.cpp index 574e2aca9..1c37f3621 100644 --- a/src/providers/irc/IrcMessageBuilder.cpp +++ b/src/providers/irc/IrcMessageBuilder.cpp @@ -1,6 +1,7 @@ #include "providers/irc/IrcMessageBuilder.hpp" #include "Application.hpp" +#include "common/IrcColors.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/ignores/IgnoreController.hpp" #include "controllers/ignores/IgnorePhrase.hpp" @@ -15,6 +16,14 @@ #include "util/IrcHelpers.hpp" #include "widgets/Window.hpp" +namespace { + +QRegularExpression IRC_COLOR_PARSE_REGEX( + "(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)", + QRegularExpression::UseUnicodePropertiesOption); + +} // namespace + namespace chatterino { IrcMessageBuilder::IrcMessageBuilder( @@ -68,7 +77,107 @@ MessagePtr IrcMessageBuilder::build() void IrcMessageBuilder::addWords(const QStringList &words) { - this->emplace(words.join(' '), MessageElementFlag::Text); + MessageColor defaultColorType = MessageColor::Text; + auto defaultColor = defaultColorType.getColor(*getApp()->themes); + QColor textColor = defaultColor; + int fg = -1; + int bg = -1; + + for (auto word : words) + { + if (word.isEmpty()) + { + continue; + } + + auto string = QString(word); + + // Actually just text + auto linkString = this->matchLink(string); + auto link = Link(); + + if (!linkString.isEmpty()) + { + this->addLink(string, linkString); + continue; + } + + // Does the word contain a color changer? If so, split on it. + // Add color indicators, then combine into the same word with the color being changed + + auto i = IRC_COLOR_PARSE_REGEX.globalMatch(string); + + if (!i.hasNext()) + { + this->emplace(string, MessageElementFlag::Text, + textColor); + continue; + } + + int pos = 0; + int lastPos = 0; + + while (i.hasNext()) + { + auto match = i.next(); + + if (lastPos != match.capturedStart() && match.capturedStart() != 0) + { + if (fg >= 0 && fg <= 98) + { + textColor = IRC_COLORS[fg]; + getApp()->themes->normalizeColor(textColor); + } + else + { + textColor = defaultColor; + } + this->emplace( + string.mid(lastPos, match.capturedStart() - lastPos), + MessageElementFlag::Text, textColor) + ->setTrailingSpace(false); + lastPos = match.capturedStart() + match.capturedLength(); + } + if (!match.captured(1).isEmpty()) + { + fg = -1; + bg = -1; + } + + if (!match.captured(2).isEmpty()) + { + fg = match.captured(2).toInt(nullptr); + } + else + { + fg = -1; + } + if (!match.captured(4).isEmpty()) + { + bg = match.captured(4).toInt(nullptr); + } + else if (fg == -1) + { + bg = -1; + } + + lastPos = match.capturedStart() + match.capturedLength(); + } + + if (fg >= 0 && fg <= 98) + { + textColor = IRC_COLORS[fg]; + getApp()->themes->normalizeColor(textColor); + } + else + { + textColor = defaultColor; + } + this->emplace(string.mid(lastPos), + MessageElementFlag::Text, textColor); + } + + this->message().elements.back()->setTrailingSpace(false); } void IrcMessageBuilder::appendUsername()