[irc] Partially fix IRC colors (#1594)

Doesn't fix #1379 but it is a big step forward.

Needs some "real life" testing, but should be good.
This commit is contained in:
pajlada
2020-07-04 09:15:59 -04:00
committed by GitHub
parent 0f9a612c55
commit e4af009fda
16 changed files with 1033 additions and 403 deletions
+1 -1
View File
@@ -34,7 +34,6 @@ struct MessageParseArgs {
};
class MessageBuilder
{
public:
MessageBuilder();
@@ -70,4 +69,5 @@ public:
private:
std::shared_ptr<Message> message_;
};
} // namespace chatterino
+205
View File
@@ -1,6 +1,7 @@
#include "messages/MessageElement.hpp"
#include "Application.hpp"
#include "common/IrcColors.hpp"
#include "debug/Benchmark.hpp"
#include "messages/Emote.hpp"
#include "messages/layouts/MessageLayoutContainer.hpp"
@@ -11,6 +12,14 @@
namespace chatterino {
namespace {
QRegularExpression IRC_COLOR_PARSE_REGEX(
"\u0003(\\d{1,2})?(,(\\d{1,2}))?",
QRegularExpression::UseUnicodePropertiesOption);
} // namespace
MessageElement::MessageElement(MessageElementFlags flags)
: flags_(flags)
{
@@ -428,4 +437,200 @@ 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<Segment> 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 = match.captured(1).toInt(nullptr);
}
else
{
fg = -1;
}
if (!match.captured(3).isEmpty())
{
bg = match.captured(3).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<Segment> segments,
int width, bool hasTrailingSpace) {
std::vector<PajSegment> 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<TextLayoutElement *>(e)->listenToLinkChanges();
}
return e;
};
// fourtf: add again
// if (word.width == -1) {
word.width = metrics.width(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;
}
}
// we done goofed, we need to wrap the text
QString text = word.text;
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++) //
{
auto isSurrogate = text.size() > i + 1 &&
QChar::isHighSurrogate(text[i].unicode());
auto charWidth = isSurrogate ? metrics.width(text.mid(i, 2))
: metrics.width(text[i]);
if (!container.fitsInLine(width + charWidth)) //
{
container.addElementNoLineBreak(getTextLayoutElement(
text.mid(wordStart, i - wordStart), {}, width, false));
container.breakLine();
wordStart = i;
width = charWidth;
if (isSurrogate)
i++;
continue;
}
width += charWidth;
if (isSurrogate)
i++;
}
container.addElement(getTextLayoutElement(
text.mid(wordStart), {}, width, this->hasTrailingSpace()));
container.breakLine();
}
}
}
} // namespace chatterino
+30
View File
@@ -291,4 +291,34 @@ 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<Segment> segments;
};
std::vector<Word> words_;
};
} // namespace chatterino
+399
View File
@@ -0,0 +1,399 @@
#include "messages/SharedMessageBuilder.hpp"
#include "Application.hpp"
#include "controllers/ignores/IgnorePhrase.hpp"
#include "messages/Message.hpp"
#include "messages/MessageElement.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "singletons/Settings.hpp"
#include "singletons/WindowManager.hpp"
namespace chatterino {
namespace {
QUrl getFallbackHighlightSound()
{
QString path = getSettings()->pathHighlightSound;
bool fileExists = QFileInfo::exists(path) && QFileInfo(path).isFile();
// Use fallback sound when checkbox is not checked
// or custom file doesn't exist
if (getSettings()->customHighlightSound && fileExists)
{
return QUrl::fromLocalFile(path);
}
else
{
return QUrl("qrc:/sounds/ping2.wav");
}
}
} // namespace
SharedMessageBuilder::SharedMessageBuilder(
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
const MessageParseArgs &_args)
: channel(_channel)
, ircMessage(_ircMessage)
, args(_args)
, tags(this->ircMessage->tags())
, originalMessage_(_ircMessage->content())
, action_(_ircMessage->isAction())
{
}
SharedMessageBuilder::SharedMessageBuilder(
Channel *_channel, const Communi::IrcMessage *_ircMessage,
const MessageParseArgs &_args, QString content, bool isAction)
: channel(_channel)
, ircMessage(_ircMessage)
, args(_args)
, tags(this->ircMessage->tags())
, originalMessage_(content)
, action_(isAction)
{
}
namespace {
QColor getRandomColor(const QString &v)
{
int colorSeed = 0;
for (const auto &c : v)
{
colorSeed += c.digitValue();
}
const auto colorIndex = colorSeed % TWITCH_USERNAME_COLORS.size();
return TWITCH_USERNAME_COLORS[colorIndex];
}
} // namespace
void SharedMessageBuilder::parse()
{
this->parseUsernameColor();
this->parseUsername();
this->message().flags.set(MessageFlag::Collapsed);
}
bool SharedMessageBuilder::isIgnored() const
{
// TODO(pajlada): Do we need to check if the phrase is valid first?
auto phrases = getCSettings().ignoredMessages.readOnly();
for (const auto &phrase : *phrases)
{
if (phrase.isBlock() && phrase.isMatch(this->originalMessage_))
{
qDebug() << "Blocking message because it contains ignored phrase"
<< phrase.getPattern();
return true;
}
}
return false;
}
void SharedMessageBuilder::parseUsernameColor()
{
if (getSettings()->colorizeNicknames)
{
this->usernameColor_ = getRandomColor(this->ircMessage->nick());
}
}
void SharedMessageBuilder::parseUsername()
{
// username
this->userName = this->ircMessage->nick();
this->message().loginName = this->userName;
}
void SharedMessageBuilder::parseHighlights()
{
auto app = getApp();
if (this->message().flags.has(MessageFlag::Subscription) &&
getSettings()->enableSubHighlight)
{
if (getSettings()->enableSubHighlightTaskbar)
{
this->highlightAlert_ = true;
}
if (getSettings()->enableSubHighlightSound)
{
this->highlightSound_ = true;
// Use custom sound if set, otherwise use fallback
if (!getSettings()->subHighlightSoundUrl.getValue().isEmpty())
{
this->highlightSoundUrl_ =
QUrl(getSettings()->subHighlightSoundUrl.getValue());
}
else
{
this->highlightSoundUrl_ = getFallbackHighlightSound();
}
}
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor =
ColorProvider::instance().color(ColorType::Subscription);
// This message was a subscription.
// Don't check for any other highlight phrases.
return;
}
// XXX: Non-common term in SharedMessageBuilder
auto currentUser = app->accounts->twitch.getCurrent();
QString currentUsername = currentUser->getUserName();
if (getCSettings().isBlacklistedUser(this->ircMessage->nick()))
{
// Do nothing. We ignore highlights from this user.
return;
}
// Highlight because it's a whisper
if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight)
{
if (getSettings()->enableWhisperHighlightTaskbar)
{
this->highlightAlert_ = true;
}
if (getSettings()->enableWhisperHighlightSound)
{
this->highlightSound_ = true;
// Use custom sound if set, otherwise use fallback
if (!getSettings()->whisperHighlightSoundUrl.getValue().isEmpty())
{
this->highlightSoundUrl_ =
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
}
else
{
this->highlightSoundUrl_ = getFallbackHighlightSound();
}
}
this->message().highlightColor =
ColorProvider::instance().color(ColorType::Whisper);
/*
* Do _NOT_ return yet, we might want to apply phrase/user name
* highlights (which override whisper color/sound).
*/
}
// Highlight because of sender
auto userHighlights = getCSettings().highlightedUsers.readOnly();
for (const HighlightPhrase &userHighlight : *userHighlights)
{
if (!userHighlight.isMatch(this->ircMessage->nick()))
{
continue;
}
qDebug() << "Highlight because user" << this->ircMessage->nick()
<< "sent a message";
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor = userHighlight.getColor();
if (userHighlight.hasAlert())
{
this->highlightAlert_ = true;
}
if (userHighlight.hasSound())
{
this->highlightSound_ = true;
// Use custom sound if set, otherwise use the fallback sound
if (userHighlight.hasCustomSound())
{
this->highlightSoundUrl_ = userHighlight.getSoundUrl();
}
else
{
this->highlightSoundUrl_ = getFallbackHighlightSound();
}
}
if (this->highlightAlert_ && this->highlightSound_)
{
/*
* User name highlights "beat" highlight phrases: If a message has
* all attributes (color, taskbar flashing, sound) set, highlight
* phrases will not be checked.
*/
return;
}
}
if (this->ircMessage->nick() == currentUsername)
{
// Do nothing. Highlights cannot be triggered by yourself
return;
}
// TODO: This vector should only be rebuilt upon highlights being changed
// fourtf: should be implemented in the HighlightsController
std::vector<HighlightPhrase> activeHighlights =
getSettings()->highlightedMessages.cloneVector();
if (getSettings()->enableSelfHighlight && currentUsername.size() > 0)
{
HighlightPhrase selfHighlight(
currentUsername, getSettings()->enableSelfHighlightTaskbar,
getSettings()->enableSelfHighlightSound, false, false,
getSettings()->selfHighlightSoundUrl.getValue(),
ColorProvider::instance().color(ColorType::SelfHighlight));
activeHighlights.emplace_back(std::move(selfHighlight));
}
// Highlight because of message
for (const HighlightPhrase &highlight : activeHighlights)
{
if (!highlight.isMatch(this->originalMessage_))
{
continue;
}
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor = highlight.getColor();
if (highlight.hasAlert())
{
this->highlightAlert_ = true;
}
// Only set highlightSound_ if it hasn't been set by username
// highlights already.
if (highlight.hasSound() && !this->highlightSound_)
{
this->highlightSound_ = true;
// Use custom sound if set, otherwise use fallback sound
if (highlight.hasCustomSound())
{
this->highlightSoundUrl_ = highlight.getSoundUrl();
}
else
{
this->highlightSoundUrl_ = getFallbackHighlightSound();
}
}
if (this->highlightAlert_ && this->highlightSound_)
{
/*
* Break once no further attributes (taskbar, sound) can be
* applied.
*/
break;
}
}
}
void SharedMessageBuilder::addTextOrEmoji(EmotePtr emote)
{
this->emplace<EmoteElement>(emote, MessageElementFlag::EmojiAll);
}
void SharedMessageBuilder::addTextOrEmoji(const QString &string_)
{
auto string = QString(string_);
// Actually just text
auto linkString = this->matchLink(string);
auto link = Link();
auto textColor = this->action_ ? MessageColor(this->usernameColor_)
: MessageColor(MessageColor::Text);
if (linkString.isEmpty())
{
if (string.startsWith('@'))
{
this->emplace<TextElement>(string, MessageElementFlag::BoldUsername,
textColor, FontStyle::ChatMediumBold);
this->emplace<TextElement>(
string, MessageElementFlag::NonBoldUsername, textColor);
}
else
{
this->emplace<TextElement>(string, MessageElementFlag::Text,
textColor);
}
}
else
{
this->addLink(string, linkString);
}
}
void SharedMessageBuilder::appendChannelName()
{
QString channelName("#" + this->channel->getName());
Link link(Link::Url, this->channel->getName() + "\n" + this->message().id);
this->emplace<TextElement>(channelName, MessageElementFlag::ChannelName,
MessageColor::System) //
->setLink(link);
}
inline QMediaPlayer *getPlayer()
{
if (isGuiThread())
{
static auto player = new QMediaPlayer;
return player;
}
else
{
return nullptr;
}
}
void SharedMessageBuilder::triggerHighlights()
{
static QUrl currentPlayerUrl;
if (getCSettings().isMutedChannel(this->channel->getName()))
{
// Do nothing. Pings are muted in this channel.
return;
}
bool hasFocus = (QApplication::focusWidget() != nullptr);
bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
if (this->highlightSound_ && resolveFocus)
{
if (auto player = getPlayer())
{
// update the media player url if necessary
if (currentPlayerUrl != this->highlightSoundUrl_)
{
player->setMedia(this->highlightSoundUrl_);
currentPlayerUrl = this->highlightSoundUrl_;
}
player->play();
}
}
if (this->highlightAlert_)
{
getApp()->windows->sendAlert();
}
}
} // namespace chatterino
+69
View File
@@ -0,0 +1,69 @@
#include "messages/MessageBuilder.hpp"
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include <IrcMessage>
#include <QColor>
namespace chatterino {
class SharedMessageBuilder : public MessageBuilder
{
public:
SharedMessageBuilder() = delete;
explicit SharedMessageBuilder(Channel *_channel,
const Communi::IrcPrivateMessage *_ircMessage,
const MessageParseArgs &_args);
explicit SharedMessageBuilder(Channel *_channel,
const Communi::IrcMessage *_ircMessage,
const MessageParseArgs &_args,
QString content, bool isAction);
QString userName;
[[nodiscard]] virtual bool isIgnored() const;
// triggerHighlights triggers any alerts or sounds parsed by parseHighlights
virtual void triggerHighlights();
virtual MessagePtr build() = 0;
protected:
virtual void parse();
virtual void parseUsernameColor();
virtual void parseUsername();
virtual Outcome tryAppendEmote(const EmoteName &name)
{
return Failure;
}
// parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function
virtual void parseHighlights();
virtual void addTextOrEmoji(EmotePtr emote);
virtual void addTextOrEmoji(const QString &value);
void appendChannelName();
Channel *channel;
const Communi::IrcMessage *ircMessage;
MessageParseArgs args;
const QVariantMap tags;
QString originalMessage_;
const bool action_{};
QColor usernameColor_;
bool highlightAlert_ = false;
bool highlightSound_ = false;
QUrl highlightSoundUrl_;
};
} // namespace chatterino
@@ -395,4 +395,41 @@ int TextIconLayoutElement::getXFromIndex(int index)
}
}
//
// TEXT
//
MultiColorTextLayoutElement::MultiColorTextLayoutElement(
MessageElement &_creator, QString &_text, const QSize &_size,
std::vector<PajSegment> 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_)
{
// qDebug() << "Draw segment:" << segment.text;
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.width(segment.text);
}
}
} // namespace chatterino
+21 -1
View File
@@ -107,7 +107,6 @@ protected:
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
private:
QColor color_;
FontStyle style_;
float scale_;
@@ -138,4 +137,25 @@ 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<PajSegment> segments,
FontStyle style_, float scale_);
protected:
void paint(QPainter &painter) override;
private:
std::vector<PajSegment> segments_;
};
} // namespace chatterino