2018-06-26 14:09:39 +02:00
|
|
|
#include "messages/MessageElement.hpp"
|
|
|
|
|
|
|
|
#include "Application.hpp"
|
2022-12-31 15:41:01 +01:00
|
|
|
#include "controllers/moderationactions/ModerationAction.hpp"
|
2018-06-26 17:20:03 +02:00
|
|
|
#include "debug/Benchmark.hpp"
|
2018-08-11 22:23:06 +02:00
|
|
|
#include "messages/Emote.hpp"
|
2022-12-31 15:41:01 +01:00
|
|
|
#include "messages/Image.hpp"
|
2018-06-26 14:09:39 +02:00
|
|
|
#include "messages/layouts/MessageLayoutContainer.hpp"
|
|
|
|
#include "messages/layouts/MessageLayoutElement.hpp"
|
2022-07-31 12:45:25 +02:00
|
|
|
#include "providers/emoji/Emojis.hpp"
|
|
|
|
#include "singletons/Emotes.hpp"
|
2018-06-28 19:46:45 +02:00
|
|
|
#include "singletons/Settings.hpp"
|
2018-08-11 22:23:06 +02:00
|
|
|
#include "singletons/Theme.hpp"
|
2018-08-07 01:35:24 +02:00
|
|
|
#include "util/DebugCount.hpp"
|
2018-01-11 20:16:25 +01:00
|
|
|
|
|
|
|
namespace chatterino {
|
|
|
|
|
2023-03-18 17:30:08 +01:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
// Computes the bounding box for the given vector of images
|
|
|
|
QSize getBoundingBoxSize(const std::vector<ImagePtr> &images)
|
|
|
|
{
|
|
|
|
int width = 0;
|
|
|
|
int height = 0;
|
|
|
|
for (const auto &img : images)
|
|
|
|
{
|
|
|
|
width = std::max(width, img->width());
|
|
|
|
height = std::max(height, img->height());
|
|
|
|
}
|
|
|
|
|
|
|
|
return QSize(width, height);
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
2018-08-07 07:55:31 +02:00
|
|
|
MessageElement::MessageElement(MessageElementFlags flags)
|
2018-07-06 19:23:47 +02:00
|
|
|
: flags_(flags)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
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-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
MessageElement *MessageElement::setLink(const Link &link)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
this->link_ = link;
|
2018-01-11 20:16:25 +01:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2018-09-30 18:18:30 +02:00
|
|
|
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-01-11 20:16:25 +01:00
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
this->tooltip_ = tooltip;
|
2018-01-11 20:16:25 +01:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2020-05-10 12:11:10 +02:00
|
|
|
MessageElement *MessageElement::setThumbnail(const ImagePtr &thumbnail)
|
|
|
|
{
|
|
|
|
this->thumbnail_ = thumbnail;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
MessageElement *MessageElement::setThumbnailType(const ThumbnailType type)
|
|
|
|
{
|
|
|
|
this->thumbnailType_ = type;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2018-01-11 20:16:25 +01:00
|
|
|
MessageElement *MessageElement::setTrailingSpace(bool value)
|
|
|
|
{
|
|
|
|
this->trailingSpace = value;
|
2018-01-12 18:42:13 +01:00
|
|
|
return this;
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const QString &MessageElement::getTooltip() const
|
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
return this->tooltip_;
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
2020-05-10 12:11:10 +02:00
|
|
|
const ImagePtr &MessageElement::getThumbnail() const
|
|
|
|
{
|
|
|
|
return this->thumbnail_;
|
|
|
|
}
|
|
|
|
|
|
|
|
const MessageElement::ThumbnailType &MessageElement::getThumbnailType() const
|
|
|
|
{
|
|
|
|
return this->thumbnailType_;
|
|
|
|
}
|
|
|
|
|
2018-01-11 20:16:25 +01:00
|
|
|
const Link &MessageElement::getLink() const
|
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
return this->link_;
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
bool MessageElement::hasTrailingSpace() const
|
|
|
|
{
|
|
|
|
return this->trailingSpace;
|
|
|
|
}
|
|
|
|
|
2018-08-07 07:55:31 +02:00
|
|
|
MessageElementFlags MessageElement::getFlags() const
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
return this->flags_;
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
2018-09-06 16:11:25 +02:00
|
|
|
MessageElement *MessageElement::updateLink()
|
|
|
|
{
|
|
|
|
this->linkChanged.invoke();
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2018-12-04 21:07:55 +01:00
|
|
|
// Empty
|
|
|
|
EmptyElement::EmptyElement()
|
|
|
|
: MessageElement(MessageElementFlag::None)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void EmptyElement::addToContainer(MessageLayoutContainer &container,
|
|
|
|
MessageElementFlags flags)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
EmptyElement &EmptyElement::instance()
|
|
|
|
{
|
|
|
|
static EmptyElement instance;
|
|
|
|
return instance;
|
|
|
|
}
|
|
|
|
|
2018-01-11 20:16:25 +01:00
|
|
|
// IMAGE
|
2018-08-07 07:55:31 +02:00
|
|
|
ImageElement::ImageElement(ImagePtr image, MessageElementFlags flags)
|
2018-01-11 20:16:25 +01:00
|
|
|
: MessageElement(flags)
|
2018-07-06 19:23:47 +02:00
|
|
|
, image_(image)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
2018-08-02 14:23:27 +02:00
|
|
|
// this->setTooltip(image->getTooltip());
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
2018-08-06 21:17:03 +02:00
|
|
|
void ImageElement::addToContainer(MessageLayoutContainer &container,
|
2018-08-07 07:55:31 +02:00
|
|
|
MessageElementFlags flags)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
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-01-11 20:16:25 +01:00
|
|
|
|
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
|
|
|
}
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
2022-07-31 12:45:25 +02: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()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-11 20:16:25 +01:00
|
|
|
// EMOTE
|
2021-10-31 11:37:06 +01:00
|
|
|
EmoteElement::EmoteElement(const EmotePtr &emote, MessageElementFlags flags,
|
|
|
|
const MessageColor &textElementColor)
|
2018-01-11 20:16:25 +01:00
|
|
|
: MessageElement(flags)
|
2018-08-02 14:23:27 +02:00
|
|
|
, emote_(emote)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
2021-10-31 11:37:06 +01:00
|
|
|
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-01-11 20:16:25 +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))
|
|
|
|
{
|
2019-08-21 01:44:19 +02:00
|
|
|
auto image =
|
|
|
|
this->emote_->images.getImageOrLoaded(container.getScale());
|
2018-10-21 13:43:02 +02:00
|
|
|
if (image->isEmpty())
|
|
|
|
return;
|
2018-01-11 20:16:25 +01:00
|
|
|
|
2019-06-22 14:34:54 +02:00
|
|
|
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
|
|
|
|
2019-09-08 11:30:06 +02: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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
2019-09-08 11:30:06 +02:00
|
|
|
MessageLayoutElement *EmoteElement::makeImageLayoutElement(
|
|
|
|
const ImagePtr &image, const QSize &size)
|
|
|
|
{
|
|
|
|
return new ImageLayoutElement(*this, image, size);
|
|
|
|
}
|
|
|
|
|
2023-03-18 17:30:08 +01:00
|
|
|
LayeredEmoteElement::LayeredEmoteElement(std::vector<EmotePtr> &&emotes,
|
|
|
|
MessageElementFlags flags,
|
|
|
|
const MessageColor &textElementColor)
|
|
|
|
: MessageElement(flags)
|
|
|
|
, emotes_(std::move(emotes))
|
|
|
|
, textElementColor_(textElementColor)
|
|
|
|
{
|
|
|
|
this->updateTooltips();
|
|
|
|
}
|
|
|
|
|
|
|
|
void LayeredEmoteElement::addEmoteLayer(const EmotePtr &emote)
|
|
|
|
{
|
|
|
|
this->emotes_.push_back(emote);
|
|
|
|
this->updateTooltips();
|
|
|
|
}
|
|
|
|
|
|
|
|
void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container,
|
|
|
|
MessageElementFlags flags)
|
|
|
|
{
|
|
|
|
if (flags.hasAny(this->getFlags()))
|
|
|
|
{
|
|
|
|
if (flags.has(MessageElementFlag::EmoteImages))
|
|
|
|
{
|
|
|
|
auto images = this->getLoadedImages(container.getScale());
|
|
|
|
if (images.empty())
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto emoteScale = getSettings()->emoteScale.getValue();
|
|
|
|
float overallScale = emoteScale * container.getScale();
|
|
|
|
|
|
|
|
auto largestSize = getBoundingBoxSize(images) * overallScale;
|
|
|
|
std::vector<QSize> individualSizes;
|
|
|
|
individualSizes.reserve(this->emotes_.size());
|
|
|
|
for (auto img : images)
|
|
|
|
{
|
|
|
|
individualSizes.push_back(QSize(img->width(), img->height()) *
|
|
|
|
overallScale);
|
|
|
|
}
|
|
|
|
|
|
|
|
container.addElement(this->makeImageLayoutElement(
|
|
|
|
images, individualSizes, largestSize)
|
|
|
|
->setLink(this->getLink()));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (this->textElement_)
|
|
|
|
{
|
|
|
|
this->textElement_->addToContainer(container,
|
|
|
|
MessageElementFlag::Misc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<ImagePtr> LayeredEmoteElement::getLoadedImages(float scale)
|
|
|
|
{
|
|
|
|
std::vector<ImagePtr> res;
|
|
|
|
res.reserve(this->emotes_.size());
|
|
|
|
|
|
|
|
for (auto emote : this->emotes_)
|
|
|
|
{
|
|
|
|
auto image = emote->images.getImageOrLoaded(scale);
|
|
|
|
if (image->isEmpty())
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
res.push_back(image);
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
MessageLayoutElement *LayeredEmoteElement::makeImageLayoutElement(
|
|
|
|
const std::vector<ImagePtr> &images, const std::vector<QSize> &sizes,
|
|
|
|
QSize largestSize)
|
|
|
|
{
|
|
|
|
return new LayeredImageLayoutElement(*this, images, sizes, largestSize);
|
|
|
|
}
|
|
|
|
|
|
|
|
void LayeredEmoteElement::updateTooltips()
|
|
|
|
{
|
|
|
|
if (!this->emotes_.empty())
|
|
|
|
{
|
|
|
|
QString copyStr = this->getCopyString();
|
|
|
|
this->textElement_.reset(new TextElement(
|
|
|
|
copyStr, MessageElementFlag::Misc, this->textElementColor_));
|
|
|
|
this->setTooltip(copyStr);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<QString> result;
|
|
|
|
result.reserve(this->emotes_.size());
|
|
|
|
|
|
|
|
for (auto &emote : this->emotes_)
|
|
|
|
{
|
|
|
|
result.push_back(emote->tooltip.string);
|
|
|
|
}
|
|
|
|
|
|
|
|
this->emoteTooltips_ = std::move(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::vector<QString> &LayeredEmoteElement::getEmoteTooltips() const
|
|
|
|
{
|
|
|
|
return this->emoteTooltips_;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString LayeredEmoteElement::getCleanCopyString() const
|
|
|
|
{
|
|
|
|
QString result;
|
|
|
|
for (size_t i = 0; i < this->emotes_.size(); ++i)
|
|
|
|
{
|
|
|
|
if (i != 0)
|
|
|
|
{
|
|
|
|
result += " ";
|
|
|
|
}
|
|
|
|
result +=
|
|
|
|
TwitchEmotes::cleanUpEmoteCode(this->emotes_[i]->getCopyString());
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString LayeredEmoteElement::getCopyString() const
|
|
|
|
{
|
|
|
|
QString result;
|
|
|
|
for (size_t i = 0; i < this->emotes_.size(); ++i)
|
|
|
|
{
|
|
|
|
if (i != 0)
|
|
|
|
{
|
|
|
|
result += " ";
|
|
|
|
}
|
|
|
|
result += this->emotes_[i]->getCopyString();
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::vector<EmotePtr> &LayeredEmoteElement::getEmotes() const
|
|
|
|
{
|
|
|
|
return this->emotes_;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<EmotePtr> LayeredEmoteElement::getUniqueEmotes() const
|
|
|
|
{
|
|
|
|
// Functor for std::copy_if that keeps track of seen elements
|
|
|
|
struct NotDuplicate {
|
|
|
|
bool operator()(const EmotePtr &element)
|
|
|
|
{
|
|
|
|
return seen.insert(element).second;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
std::set<EmotePtr> seen;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Get unique emotes while maintaining relative layering order
|
|
|
|
NotDuplicate dup;
|
|
|
|
std::vector<EmotePtr> unique;
|
|
|
|
std::copy_if(this->emotes_.begin(), this->emotes_.end(),
|
|
|
|
std::back_insert_iterator(unique), dup);
|
|
|
|
|
|
|
|
return unique;
|
|
|
|
}
|
|
|
|
|
2019-06-22 14:34:54 +02:00
|
|
|
// 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()))
|
|
|
|
{
|
2019-08-21 01:44:19 +02:00
|
|
|
auto image =
|
|
|
|
this->emote_->images.getImageOrLoaded(container.getScale());
|
2019-06-22 14:34:54 +02:00
|
|
|
if (image->isEmpty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
auto size = QSize(int(container.getScale() * image->width()),
|
|
|
|
int(container.getScale() * image->height()));
|
|
|
|
|
2020-03-14 13:06:24 +01:00
|
|
|
container.addElement(this->makeImageLayoutElement(image, size));
|
2019-06-22 14:34:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-21 01:52:01 +02:00
|
|
|
EmotePtr BadgeElement::getEmote() const
|
|
|
|
{
|
|
|
|
return this->emote_;
|
|
|
|
}
|
|
|
|
|
2020-03-14 13:06:24 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-04-17 14:42:30 +02:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2020-10-25 10:36:00 +01:00
|
|
|
// FFZ Badge
|
|
|
|
FfzBadgeElement::FfzBadgeElement(const EmotePtr &data,
|
2022-06-27 20:36:58 +02:00
|
|
|
MessageElementFlags flags_, QColor color_)
|
2020-10-25 10:36:00 +01:00
|
|
|
: BadgeElement(data, flags_)
|
2022-06-27 20:36:58 +02:00
|
|
|
, color(std::move(color_))
|
2020-10-25 10:36:00 +01:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
MessageLayoutElement *FfzBadgeElement::makeImageLayoutElement(
|
|
|
|
const ImagePtr &image, const QSize &size)
|
|
|
|
{
|
|
|
|
auto element =
|
|
|
|
(new ImageWithBackgroundLayoutElement(*this, image, size, this->color))
|
|
|
|
->setLink(this->getLink());
|
|
|
|
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
|
2018-01-11 20:16:25 +01:00
|
|
|
// 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)
|
2018-01-11 20:16:25 +01:00
|
|
|
: MessageElement(flags)
|
2018-07-06 19:23:47 +02:00
|
|
|
, color_(color)
|
|
|
|
, style_(style)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
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});
|
2018-04-03 02:55:32 +02:00
|
|
|
// fourtf: add logic to store multiple spaces after message
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-06 21:17:03 +02:00
|
|
|
void TextElement::addToContainer(MessageLayoutContainer &container,
|
2018-08-07 07:55:31 +02:00
|
|
|
MessageElementFlags flags)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
2018-04-27 22:11:19 +02:00
|
|
|
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,
|
2019-05-08 08:51:14 +02:00
|
|
|
bool hasTrailingSpace) {
|
2018-10-21 13:03:26 +02:00
|
|
|
auto color = this->color_.getColor(*app->themes);
|
2018-04-27 22:11:19 +02:00
|
|
|
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()))
|
2018-09-30 20:02:07 +02:00
|
|
|
->setLink(this->getLink());
|
2019-05-08 08:51:14 +02:00
|
|
|
e->setTrailingSpace(hasTrailingSpace);
|
2018-09-30 20:02:07 +02:00
|
|
|
e->setText(text);
|
2018-09-06 16:11:25 +02:00
|
|
|
|
2018-09-30 18:55:41 +02:00
|
|
|
// If URL link was changed,
|
2018-09-06 16:11:25 +02:00
|
|
|
// Should update it in MessageLayoutElement too!
|
2018-10-21 13:43:02 +02:00
|
|
|
if (this->getLink().type == Link::Url)
|
|
|
|
{
|
2018-10-23 10:32:13 +02:00
|
|
|
static_cast<TextLayoutElement *>(e)->listenToLinkChanges();
|
2018-09-06 16:11:25 +02:00
|
|
|
}
|
2018-01-22 22:38:44 +01:00
|
|
|
return e;
|
|
|
|
};
|
|
|
|
|
2018-01-23 22:00:58 +01:00
|
|
|
// fourtf: add again
|
|
|
|
// if (word.width == -1) {
|
2021-03-13 14:47:02 +01:00
|
|
|
word.width = metrics.horizontalAdvance(word.text);
|
2018-01-23 22:00:58 +01:00
|
|
|
// }
|
2018-01-11 20:16:25 +01:00
|
|
|
|
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()));
|
2018-01-11 20:16:25 +01:00
|
|
|
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())
|
|
|
|
{
|
2018-01-11 20:16:25 +01:00
|
|
|
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-11 20:16:25 +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;
|
2018-10-21 13:03:26 +02:00
|
|
|
int width = 0;
|
2018-01-22 22:38:44 +01:00
|
|
|
|
2018-10-21 13:03:26 +02:00
|
|
|
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
|
2018-01-22 22:38:44 +01:00
|
|
|
|
2020-11-08 12:02:19 +01:00
|
|
|
for (int i = 0; i < textLength; i++)
|
2018-10-21 13:03:26 +02:00
|
|
|
{
|
|
|
|
auto isSurrogate = text.size() > i + 1 &&
|
|
|
|
QChar::isHighSurrogate(text[i].unicode());
|
|
|
|
|
2021-03-13 14:47:02 +01:00
|
|
|
auto charWidth = isSurrogate
|
|
|
|
? metrics.horizontalAdvance(text.mid(i, 2))
|
|
|
|
: metrics.horizontalAdvance(text[i]);
|
2018-10-21 13:03:26 +02:00
|
|
|
|
2020-11-08 12:02:19 +01:00
|
|
|
if (!container.fitsInLine(width + charWidth))
|
2018-10-21 13:03:26 +02:00
|
|
|
{
|
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;
|
2018-10-21 13:03:26 +02:00
|
|
|
width = charWidth;
|
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (isSurrogate)
|
|
|
|
i++;
|
2018-01-22 22:38:44 +01:00
|
|
|
continue;
|
|
|
|
}
|
2018-10-21 13:03:26 +02:00
|
|
|
|
2018-01-22 22:38:44 +01:00
|
|
|
width += charWidth;
|
2018-10-21 13:03:26 +02:00
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (isSurrogate)
|
|
|
|
i++;
|
2018-01-22 22:38:44 +01:00
|
|
|
}
|
2020-07-19 12:16:58 +02: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
|
|
|
}
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-31 12:45:25 +02: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("...");
|
2022-10-22 12:46:20 +02:00
|
|
|
|
|
|
|
// 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;
|
2022-07-31 12:45:25 +02:00
|
|
|
|
2022-11-10 21:36:19 +01:00
|
|
|
container.first = FirstWord::Neutral;
|
2022-07-31 12:45:25 +02:00
|
|
|
for (Word &word : this->words_)
|
|
|
|
{
|
|
|
|
auto parsedWords = app->emotes->emojis.parse(word.text);
|
|
|
|
if (parsedWords.size() == 0)
|
|
|
|
{
|
|
|
|
continue; // sanity check
|
|
|
|
}
|
|
|
|
|
|
|
|
auto &parsedWord = parsedWords[0];
|
2022-10-22 12:46:20 +02:00
|
|
|
if (parsedWord.type() == typeid(QString))
|
2022-07-31 12:45:25 +02:00
|
|
|
{
|
2022-10-22 12:46:20 +02:00
|
|
|
int nextWidth =
|
|
|
|
metrics.horizontalAdvance(currentText + word.text);
|
2022-07-31 12:45:25 +02:00
|
|
|
|
|
|
|
// see if the text fits in the current line
|
2022-10-22 12:46:20 +02:00
|
|
|
if (container.fitsInLine(nextWidth))
|
2022-07-31 12:45:25 +02:00
|
|
|
{
|
2022-10-22 12:46:20 +02:00
|
|
|
currentText += (word.text + " ");
|
2022-07-31 12:45:25 +02:00
|
|
|
}
|
|
|
|
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;
|
2022-10-22 12:46:20 +02:00
|
|
|
int newSize = metrics.horizontalAdvance(currentText +
|
|
|
|
truncatedWord);
|
2022-07-31 12:45:25 +02:00
|
|
|
if (container.fitsInLine(newSize))
|
|
|
|
{
|
2022-10-22 12:46:20 +02:00
|
|
|
currentText += (truncatedWord);
|
|
|
|
|
2022-07-31 12:45:25 +02:00
|
|
|
cutSuccess = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!cutSuccess)
|
|
|
|
{
|
|
|
|
// We weren't able to show any part of the current word, so
|
|
|
|
// just append the ellipsis.
|
2022-10-22 12:46:20 +02:00
|
|
|
currentText += ellipsis;
|
2022-07-31 12:45:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-10-22 12:46:20 +02:00
|
|
|
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));
|
2022-07-31 12:45:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
container.breakLine();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-11 20:16:25 +01:00
|
|
|
// 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-01-11 20:16:25 +01:00
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
assert(this->element_ != nullptr);
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void TimestampElement::addToContainer(MessageLayoutContainer &container,
|
2018-08-07 07:55:31 +02:00
|
|
|
MessageElementFlags flags)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
2018-10-21 13:43:02 +02:00
|
|
|
if (flags.hasAny(this->getFlags()))
|
|
|
|
{
|
|
|
|
if (getSettings()->timestampFormat != this->format_)
|
|
|
|
{
|
2018-08-12 12:56:28 +02:00
|
|
|
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
|
|
|
}
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
TextElement *TimestampElement::formatTime(const QTime &time)
|
|
|
|
{
|
2018-01-17 18:36:12 +01:00
|
|
|
static QLocale locale("en_US");
|
|
|
|
|
2018-08-12 12:56:28 +02:00
|
|
|
QString format = locale.toString(time, getSettings()->timestampFormat);
|
2018-01-11 20:16:25 +01:00
|
|
|
|
2018-08-07 07:55:31 +02:00
|
|
|
return new TextElement(format, MessageElementFlag::Timestamp,
|
|
|
|
MessageColor::System, FontStyle::ChatMedium);
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// TWITCH MODERATION
|
|
|
|
TwitchModerationElement::TwitchModerationElement()
|
2018-08-07 07:55:31 +02:00
|
|
|
: MessageElement(MessageElementFlag::ModeratorTools)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void TwitchModerationElement::addToContainer(MessageLayoutContainer &container,
|
2018-08-07 07:55:31 +02:00
|
|
|
MessageElementFlags flags)
|
2018-01-11 20:16:25 +01:00
|
|
|
{
|
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
|
|
|
}
|
|
|
|
}
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
|
|
|
|
2020-08-08 15:37:22 +02: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()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-31 12:45:25 +02:00
|
|
|
ReplyCurveElement::ReplyCurveElement()
|
|
|
|
: MessageElement(MessageElementFlag::RepliedMessage)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void ReplyCurveElement::addToContainer(MessageLayoutContainer &container,
|
|
|
|
MessageElementFlags flags)
|
|
|
|
{
|
2022-11-02 09:19:44 +01:00
|
|
|
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
|
|
|
|
|
2022-07-31 12:45:25 +02:00
|
|
|
if (flags.hasAny(this->getFlags()))
|
|
|
|
{
|
2022-11-02 09:19:44 +01:00
|
|
|
float scale = container.getScale();
|
|
|
|
container.addElement(
|
|
|
|
new ReplyCurveLayoutElement(*this, width * scale, thickness * scale,
|
|
|
|
radius * scale, margin * scale));
|
2022-07-31 12:45:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-11 20:16:25 +01:00
|
|
|
} // namespace chatterino
|