mirror-chatterino2/src/messages/MessageElement.cpp

703 lines
21 KiB
C++
Raw Normal View History

2018-06-26 14:09:39 +02:00
#include "messages/MessageElement.hpp"
#include "Application.hpp"
2018-06-26 17:20:03 +02:00
#include "debug/Benchmark.hpp"
#include "messages/Emote.hpp"
2018-06-26 14:09:39 +02:00
#include "messages/layouts/MessageLayoutContainer.hpp"
#include "messages/layouts/MessageLayoutElement.hpp"
#include "providers/emoji/Emojis.hpp"
#include "singletons/Emotes.hpp"
2018-06-28 19:46:45 +02:00
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
2018-08-07 01:35:24 +02:00
#include "util/DebugCount.hpp"
namespace chatterino {
2018-08-07 07:55:31 +02:00
MessageElement::MessageElement(MessageElementFlags flags)
2018-07-06 19:23:47 +02:00
: flags_(flags)
{
2018-06-26 17:06:17 +02:00
DebugCount::increase("message elements");
2018-04-06 16:37:30 +02:00
}
MessageElement::~MessageElement()
{
2018-06-26 17:06:17 +02:00
DebugCount::decrease("message elements");
}
2018-07-06 19:23:47 +02:00
MessageElement *MessageElement::setLink(const Link &link)
{
2018-07-06 19:23:47 +02:00
this->link_ = link;
return this;
}
MessageElement *MessageElement::setText(const QString &text)
{
this->text_ = text;
return this;
}
2018-07-06 19:23:47 +02:00
MessageElement *MessageElement::setTooltip(const QString &tooltip)
{
2018-07-06 19:23:47 +02:00
this->tooltip_ = tooltip;
return this;
}
MessageElement *MessageElement::setThumbnail(const ImagePtr &thumbnail)
{
this->thumbnail_ = thumbnail;
return this;
}
MessageElement *MessageElement::setThumbnailType(const ThumbnailType type)
{
this->thumbnailType_ = type;
return this;
}
MessageElement *MessageElement::setTrailingSpace(bool value)
{
this->trailingSpace = value;
return this;
}
const QString &MessageElement::getTooltip() const
{
2018-07-06 19:23:47 +02:00
return this->tooltip_;
}
const ImagePtr &MessageElement::getThumbnail() const
{
return this->thumbnail_;
}
const MessageElement::ThumbnailType &MessageElement::getThumbnailType() const
{
return this->thumbnailType_;
}
const Link &MessageElement::getLink() const
{
2018-07-06 19:23:47 +02:00
return this->link_;
}
bool MessageElement::hasTrailingSpace() const
{
return this->trailingSpace;
}
2018-08-07 07:55:31 +02:00
MessageElementFlags MessageElement::getFlags() const
{
2018-07-06 19:23:47 +02:00
return this->flags_;
}
MessageElement *MessageElement::updateLink()
{
this->linkChanged.invoke();
return this;
}
// Empty
EmptyElement::EmptyElement()
: MessageElement(MessageElementFlag::None)
{
}
void EmptyElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
}
EmptyElement &EmptyElement::instance()
{
static EmptyElement instance;
return instance;
}
// IMAGE
2018-08-07 07:55:31 +02:00
ImageElement::ImageElement(ImagePtr image, MessageElementFlags flags)
: MessageElement(flags)
2018-07-06 19:23:47 +02:00
, image_(image)
{
2018-08-02 14:23:27 +02:00
// this->setTooltip(image->getTooltip());
}
2018-08-06 21:17:03 +02:00
void ImageElement::addToContainer(MessageLayoutContainer &container,
2018-08-07 07:55:31 +02:00
MessageElementFlags flags)
{
2018-10-21 13:43:02 +02:00
if (flags.hasAny(this->getFlags()))
{
2018-08-06 18:25:47 +02:00
auto size = QSize(this->image_->width() * container.getScale(),
this->image_->height() * container.getScale());
2018-08-06 21:17:03 +02:00
container.addElement((new ImageLayoutElement(*this, this->image_, size))
->setLink(this->getLink()));
2018-01-22 22:38:44 +01:00
}
}
CircularImageElement::CircularImageElement(ImagePtr image, int padding,
QColor background,
MessageElementFlags flags)
: MessageElement(flags)
, image_(image)
, padding_(padding)
, background_(background)
{
}
void CircularImageElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
if (flags.hasAny(this->getFlags()))
{
auto imgSize = QSize(this->image_->width(), this->image_->height()) *
container.getScale();
container.addElement((new ImageWithCircleBackgroundLayoutElement(
*this, this->image_, imgSize,
this->background_, this->padding_))
->setLink(this->getLink()));
}
}
// EMOTE
EmoteElement::EmoteElement(const EmotePtr &emote, MessageElementFlags flags,
const MessageColor &textElementColor)
: MessageElement(flags)
2018-08-02 14:23:27 +02:00
, emote_(emote)
{
this->textElement_.reset(new TextElement(
emote->getCopyString(), MessageElementFlag::Misc, textElementColor));
2018-08-02 14:23:27 +02:00
this->setTooltip(emote->tooltip.string);
}
EmotePtr EmoteElement::getEmote() const
{
return this->emote_;
2018-01-22 22:38:44 +01:00
}
2018-08-06 21:17:03 +02:00
void EmoteElement::addToContainer(MessageLayoutContainer &container,
2018-08-07 07:55:31 +02:00
MessageElementFlags flags)
2018-01-22 22:38:44 +01:00
{
2018-10-21 13:43:02 +02:00
if (flags.hasAny(this->getFlags()))
{
if (flags.has(MessageElementFlag::EmoteImages))
{
auto image =
this->emote_->images.getImageOrLoaded(container.getScale());
2018-10-21 13:43:02 +02:00
if (image->isEmpty())
return;
auto emoteScale = getSettings()->emoteScale.getValue();
2018-10-21 10:23:53 +02:00
auto size =
QSize(int(container.getScale() * image->width() * emoteScale),
int(container.getScale() * image->height() * emoteScale));
2018-01-22 22:38:44 +01:00
container.addElement(this->makeImageLayoutElement(image, size)
2018-08-06 21:17:03 +02:00
->setLink(this->getLink()));
2018-10-21 13:43:02 +02:00
}
else
{
if (this->textElement_)
{
2018-08-06 21:17:03 +02:00
this->textElement_->addToContainer(container,
2018-08-07 07:55:31 +02:00
MessageElementFlag::Misc);
2018-01-22 22:38:44 +01:00
}
}
}
}
MessageLayoutElement *EmoteElement::makeImageLayoutElement(
const ImagePtr &image, const QSize &size)
{
return new ImageLayoutElement(*this, image, size);
}
// BADGE
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
: MessageElement(flags)
, emote_(emote)
{
this->setTooltip(emote->tooltip.string);
}
void BadgeElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
if (flags.hasAny(this->getFlags()))
{
auto image =
this->emote_->images.getImageOrLoaded(container.getScale());
if (image->isEmpty())
return;
auto size = QSize(int(container.getScale() * image->width()),
int(container.getScale() * image->height()));
container.addElement(this->makeImageLayoutElement(image, size));
}
}
2019-08-21 01:52:01 +02:00
EmotePtr BadgeElement::getEmote() const
{
return this->emote_;
}
MessageLayoutElement *BadgeElement::makeImageLayoutElement(
const ImagePtr &image, const QSize &size)
{
auto element =
(new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
return element;
}
// MOD BADGE
ModBadgeElement::ModBadgeElement(const EmotePtr &data,
MessageElementFlags flags_)
: BadgeElement(data, flags_)
{
}
MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
const ImagePtr &image, const QSize &size)
{
static const QColor modBadgeBackgroundColor("#34AE0A");
auto element = (new ImageWithBackgroundLayoutElement(
*this, image, size, modBadgeBackgroundColor))
->setLink(this->getLink());
return element;
}
// VIP BADGE
VipBadgeElement::VipBadgeElement(const EmotePtr &data,
MessageElementFlags flags_)
: BadgeElement(data, flags_)
{
}
MessageLayoutElement *VipBadgeElement::makeImageLayoutElement(
const ImagePtr &image, const QSize &size)
{
auto element =
(new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
return element;
}
// FFZ Badge
FfzBadgeElement::FfzBadgeElement(const EmotePtr &data,
MessageElementFlags flags_, QColor color_)
: BadgeElement(data, flags_)
, color(std::move(color_))
{
}
MessageLayoutElement *FfzBadgeElement::makeImageLayoutElement(
const ImagePtr &image, const QSize &size)
{
auto element =
(new ImageWithBackgroundLayoutElement(*this, image, size, this->color))
->setLink(this->getLink());
return element;
}
// TEXT
2018-08-07 07:55:31 +02:00
TextElement::TextElement(const QString &text, MessageElementFlags flags,
2018-07-06 19:23:47 +02:00
const MessageColor &color, FontStyle style)
: MessageElement(flags)
2018-07-06 19:23:47 +02:00
, color_(color)
, style_(style)
{
2018-10-21 13:43:02 +02:00
for (const auto &word : text.split(' '))
{
2018-07-06 19:23:47 +02:00
this->words_.push_back({word, -1});
// fourtf: add logic to store multiple spaces after message
}
}
2018-08-06 21:17:03 +02:00
void TextElement::addToContainer(MessageLayoutContainer &container,
2018-08-07 07:55:31 +02:00
MessageElementFlags flags)
{
auto app = getApp();
2018-10-21 13:43:02 +02:00
if (flags.hasAny(this->getFlags()))
{
2018-08-06 21:17:03 +02:00
QFontMetrics metrics =
app->fonts->getFontMetrics(this->style_, container.getScale());
2018-01-22 22:38:44 +01:00
2018-10-21 13:43:02 +02:00
for (Word &word : this->words_)
{
2018-08-06 21:17:03 +02:00
auto getTextLayoutElement = [&](QString text, int width,
bool hasTrailingSpace) {
auto color = this->color_.getColor(*app->themes);
app->themes->normalizeColor(color);
2018-01-22 22:38:44 +01:00
2018-08-06 21:17:03 +02:00
auto e = (new TextLayoutElement(
*this, text, QSize(width, metrics.height()),
color, this->style_, container.getScale()))
->setLink(this->getLink());
e->setTrailingSpace(hasTrailingSpace);
e->setText(text);
2018-09-30 18:55:41 +02:00
// If URL link was changed,
// Should update it in MessageLayoutElement too!
2018-10-21 13:43:02 +02:00
if (this->getLink().type == Link::Url)
{
static_cast<TextLayoutElement *>(e)->listenToLinkChanges();
}
2018-01-22 22:38:44 +01:00
return e;
};
// fourtf: add again
// if (word.width == -1) {
word.width = metrics.horizontalAdvance(word.text);
// }
2018-01-22 22:38:44 +01:00
// see if the text fits in the current line
2018-10-21 13:43:02 +02:00
if (container.fitsInLine(word.width))
{
2018-08-06 21:17:03 +02:00
container.addElementNoLineBreak(getTextLayoutElement(
word.text, word.width, this->hasTrailingSpace()));
continue;
}
2018-01-22 22:38:44 +01:00
// see if the text fits in the next line
2018-10-21 13:43:02 +02:00
if (!container.atStartOfLine())
{
container.breakLine();
2018-10-21 13:43:02 +02:00
if (container.fitsInLine(word.width))
{
2018-08-06 21:17:03 +02:00
container.addElementNoLineBreak(getTextLayoutElement(
word.text, word.width, this->hasTrailingSpace()));
2018-01-22 22:38:44 +01:00
continue;
2018-01-16 00:56:17 +01:00
}
}
2018-01-22 22:38:44 +01:00
// we done goofed, we need to wrap the text
QString text = word.text;
int textLength = text.length();
int wordStart = 0;
int width = 0;
2018-01-22 22:38:44 +01:00
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
2018-01-22 22:38:44 +01:00
for (int i = 0; i < textLength; i++)
{
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))
{
2018-08-06 21:17:03 +02:00
container.addElementNoLineBreak(getTextLayoutElement(
text.mid(wordStart, i - wordStart), width, false));
2018-01-22 22:38:44 +01:00
container.breakLine();
wordStart = i;
width = charWidth;
2018-10-21 13:43:02 +02:00
if (isSurrogate)
i++;
2018-01-22 22:38:44 +01:00
continue;
}
2018-01-22 22:38:44 +01:00
width += charWidth;
2018-10-21 13:43:02 +02:00
if (isSurrogate)
i++;
2018-01-22 22:38:44 +01:00
}
//add the final piece of wrapped text
container.addElementNoLineBreak(getTextLayoutElement(
2018-08-06 21:17:03 +02:00
text.mid(wordStart), width, this->hasTrailingSpace()));
2018-01-22 22:38:44 +01:00
}
}
}
SingleLineTextElement::SingleLineTextElement(const QString &text,
MessageElementFlags flags,
const MessageColor &color,
FontStyle style)
: MessageElement(flags)
, color_(color)
, style_(style)
{
for (const auto &word : text.split(' '))
{
this->words_.push_back({word, -1});
}
}
void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
auto app = getApp();
if (flags.hasAny(this->getFlags()))
{
QFontMetrics metrics =
app->fonts->getFontMetrics(this->style_, container.getScale());
auto getTextLayoutElement = [&](QString text, int width,
bool hasTrailingSpace) {
auto color = this->color_.getColor(*app->themes);
app->themes->normalizeColor(color);
auto e = (new TextLayoutElement(
*this, text, QSize(width, metrics.height()), color,
this->style_, container.getScale()))
->setLink(this->getLink());
e->setTrailingSpace(hasTrailingSpace);
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;
};
static const auto ellipsis = QStringLiteral("...");
// String to continuously append words onto until we place it in the container
// once we encounter an emote or reach the end of the message text. */
QString currentText;
container.first = FirstWord::Neutral;
for (Word &word : this->words_)
{
auto parsedWords = app->emotes->emojis.parse(word.text);
if (parsedWords.size() == 0)
{
continue; // sanity check
}
auto &parsedWord = parsedWords[0];
if (parsedWord.type() == typeid(QString))
{
int nextWidth =
metrics.horizontalAdvance(currentText + word.text);
// see if the text fits in the current line
if (container.fitsInLine(nextWidth))
{
currentText += (word.text + " ");
}
else
{
// word overflows, try minimum truncation
bool cutSuccess = false;
for (size_t cut = 1; cut < word.text.length(); ++cut)
{
// Cut off n characters and append the ellipsis.
// Try removing characters one by one until the word fits.
QString truncatedWord =
word.text.chopped(cut) + ellipsis;
int newSize = metrics.horizontalAdvance(currentText +
truncatedWord);
if (container.fitsInLine(newSize))
{
currentText += (truncatedWord);
cutSuccess = true;
break;
}
}
if (!cutSuccess)
{
// We weren't able to show any part of the current word, so
// just append the ellipsis.
currentText += ellipsis;
}
break;
}
}
else if (parsedWord.type() == typeid(EmotePtr))
{
auto emote = boost::get<EmotePtr>(parsedWord);
auto image =
emote->images.getImageOrLoaded(container.getScale());
if (!image->isEmpty())
{
auto emoteScale = getSettings()->emoteScale.getValue();
int currentWidth = metrics.horizontalAdvance(currentText);
auto emoteSize = QSize(image->width(), image->height()) *
(emoteScale * container.getScale());
if (!container.fitsInLine(currentWidth + emoteSize.width()))
{
currentText += ellipsis;
break;
}
// Add currently pending text to container, then add the emote after.
container.addElementNoLineBreak(
getTextLayoutElement(currentText, currentWidth, false));
currentText.clear();
container.addElementNoLineBreak(
(new ImageLayoutElement(*this, image, emoteSize))
->setLink(this->getLink()));
}
}
}
// Add the last of the pending message text to the container.
if (!currentText.isEmpty())
{
// Remove trailing space.
currentText = currentText.trimmed();
int width = metrics.horizontalAdvance(currentText);
container.addElementNoLineBreak(
getTextLayoutElement(currentText, width, false));
}
container.breakLine();
}
}
// TIMESTAMP
2018-07-06 19:23:47 +02:00
TimestampElement::TimestampElement(QTime time)
2018-08-07 07:55:31 +02:00
: MessageElement(MessageElementFlag::Timestamp)
2018-07-06 19:23:47 +02:00
, time_(time)
, element_(this->formatTime(time))
{
2018-07-06 19:23:47 +02:00
assert(this->element_ != nullptr);
}
void TimestampElement::addToContainer(MessageLayoutContainer &container,
2018-08-07 07:55:31 +02:00
MessageElementFlags flags)
{
2018-10-21 13:43:02 +02:00
if (flags.hasAny(this->getFlags()))
{
if (getSettings()->timestampFormat != this->format_)
{
this->format_ = getSettings()->timestampFormat.getValue();
2018-07-06 19:23:47 +02:00
this->element_.reset(this->formatTime(this->time_));
2018-01-22 22:38:44 +01:00
}
2018-01-12 23:09:05 +01:00
2018-07-06 19:23:47 +02:00
this->element_->addToContainer(container, flags);
2018-01-22 22:38:44 +01:00
}
}
TextElement *TimestampElement::formatTime(const QTime &time)
{
2018-01-17 18:36:12 +01:00
static QLocale locale("en_US");
QString format = locale.toString(time, getSettings()->timestampFormat);
2018-08-07 07:55:31 +02:00
return new TextElement(format, MessageElementFlag::Timestamp,
MessageColor::System, FontStyle::ChatMedium);
}
// TWITCH MODERATION
TwitchModerationElement::TwitchModerationElement()
2018-08-07 07:55:31 +02:00
: MessageElement(MessageElementFlag::ModeratorTools)
{
}
void TwitchModerationElement::addToContainer(MessageLayoutContainer &container,
2018-08-07 07:55:31 +02:00
MessageElementFlags flags)
{
2018-10-21 13:43:02 +02:00
if (flags.has(MessageElementFlag::ModeratorTools))
{
2018-08-06 21:17:03 +02:00
QSize size(int(container.getScale() * 16),
int(container.getScale() * 16));
2020-02-23 23:07:28 +01:00
auto actions = getCSettings().moderationActions.readOnly();
for (const auto &action : *actions)
2018-10-21 13:43:02 +02:00
{
if (auto image = action.getImage())
{
2018-08-06 21:17:03 +02:00
container.addElement(
(new ImageLayoutElement(*this, image.get(), size))
->setLink(Link(Link::UserAction, action.getAction())));
2018-10-21 13:43:02 +02:00
}
else
{
2018-08-02 14:23:27 +02:00
container.addElement(
2018-08-06 21:17:03 +02:00
(new TextIconLayoutElement(*this, action.getLine1(),
action.getLine2(),
2018-08-02 14:23:27 +02:00
container.getScale(), size))
->setLink(Link(Link::UserAction, action.getAction())));
2018-01-17 16:52:51 +01:00
}
2018-01-17 14:14:31 +01:00
}
}
}
LinebreakElement::LinebreakElement(MessageElementFlags flags)
: MessageElement(flags)
{
}
void LinebreakElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
if (flags.hasAny(this->getFlags()))
{
container.breakLine();
}
}
ScalingImageElement::ScalingImageElement(ImageSet images,
MessageElementFlags flags)
: MessageElement(flags)
, images_(images)
{
}
void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
if (flags.hasAny(this->getFlags()))
{
const auto &image =
this->images_.getImageOrLoaded(container.getScale());
if (image->isEmpty())
return;
auto size = QSize(image->width() * container.getScale(),
image->height() * container.getScale());
container.addElement((new ImageLayoutElement(*this, image, size))
->setLink(this->getLink()));
}
}
ReplyCurveElement::ReplyCurveElement()
: MessageElement(MessageElementFlag::RepliedMessage)
{
}
void ReplyCurveElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
static const int width = 18; // Overall width
static const float thickness = 1.5; // Pen width
static const int radius = 6; // Radius of the top left corner
static const int margin = 2; // Top/Left/Bottom margin
if (flags.hasAny(this->getFlags()))
{
float scale = container.getScale();
container.addElement(
new ReplyCurveLayoutElement(*this, width * scale, thickness * scale,
radius * scale, margin * scale));
}
}
} // namespace chatterino