Added support for Twitch's Chat Replies (#3722)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -23,6 +23,8 @@ public:
|
||||
JumpToChannel,
|
||||
Reconnect,
|
||||
CopyToClipboard,
|
||||
ReplyToMessage,
|
||||
ViewThread,
|
||||
};
|
||||
|
||||
Link();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
namespace chatterino {
|
||||
class MessageElement;
|
||||
class MessageThread;
|
||||
|
||||
enum class MessageFlag : uint32_t {
|
||||
None = 0,
|
||||
@@ -40,6 +41,7 @@ enum class MessageFlag : uint32_t {
|
||||
RedeemedChannelPointReward = (1 << 21),
|
||||
ShowInMentions = (1 << 22),
|
||||
FirstMessage = (1 << 23),
|
||||
ReplyMessage = (1 << 24),
|
||||
};
|
||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||
|
||||
@@ -68,6 +70,10 @@ struct Message : boost::noncopyable {
|
||||
std::vector<Badge> badges;
|
||||
std::unordered_map<QString, QString> badgeInfos;
|
||||
std::shared_ptr<QColor> highlightColor;
|
||||
// Each reply holds a reference to the thread. When every reply is dropped,
|
||||
// the reply thread will be cleaned up by the TwitchChannel.
|
||||
// The root of the thread does not have replyThread set.
|
||||
std::shared_ptr<MessageThread> replyThread;
|
||||
uint32_t count = 1;
|
||||
std::vector<std::unique_ptr<MessageElement>> elements;
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "messages/Emote.hpp"
|
||||
#include "messages/layouts/MessageLayoutContainer.hpp"
|
||||
#include "messages/layouts/MessageLayoutElement.hpp"
|
||||
#include "providers/emoji/Emojis.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
#include "util/DebugCount.hpp"
|
||||
@@ -132,6 +134,31 @@ void ImageElement::addToContainer(MessageLayoutContainer &container,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -395,6 +422,137 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
||||
}
|
||||
}
|
||||
|
||||
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("...");
|
||||
auto addEllipsis = [&]() {
|
||||
int ellipsisSize = metrics.horizontalAdvance(ellipsis);
|
||||
container.addElementNoLineBreak(
|
||||
getTextLayoutElement(ellipsis, ellipsisSize, false));
|
||||
};
|
||||
|
||||
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(EmotePtr))
|
||||
{
|
||||
auto emote = boost::get<EmotePtr>(parsedWord);
|
||||
auto image =
|
||||
emote->images.getImageOrLoaded(container.getScale());
|
||||
if (!image->isEmpty())
|
||||
{
|
||||
auto emoteScale = getSettings()->emoteScale.getValue();
|
||||
|
||||
auto size = QSize(image->width(), image->height()) *
|
||||
(emoteScale * container.getScale());
|
||||
|
||||
if (!container.fitsInLine(size.width()))
|
||||
{
|
||||
addEllipsis();
|
||||
break;
|
||||
}
|
||||
|
||||
container.addElementNoLineBreak(
|
||||
(new ImageLayoutElement(*this, image, size))
|
||||
->setLink(this->getLink()));
|
||||
}
|
||||
}
|
||||
else if (parsedWord.type() == typeid(QString))
|
||||
{
|
||||
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.width, this->hasTrailingSpace()));
|
||||
}
|
||||
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(truncatedWord);
|
||||
if (container.fitsInLine(newSize))
|
||||
{
|
||||
container.addElementNoLineBreak(
|
||||
getTextLayoutElement(truncatedWord, newSize,
|
||||
false));
|
||||
cutSuccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cutSuccess)
|
||||
{
|
||||
// We weren't able to show any part of the current word, so
|
||||
// just append the ellipsis.
|
||||
addEllipsis();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
container.breakLine();
|
||||
}
|
||||
}
|
||||
|
||||
// TIMESTAMP
|
||||
TimestampElement::TimestampElement(QTime time)
|
||||
: MessageElement(MessageElementFlag::Timestamp)
|
||||
@@ -502,4 +660,24 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
|
||||
}
|
||||
}
|
||||
|
||||
ReplyCurveElement::ReplyCurveElement()
|
||||
: MessageElement(MessageElementFlag::RepliedMessage)
|
||||
// these values nicely align with a single badge
|
||||
, neededMargin_(3)
|
||||
, size_(18, 14)
|
||||
{
|
||||
}
|
||||
|
||||
void ReplyCurveElement::addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags)
|
||||
{
|
||||
if (flags.hasAny(this->getFlags()))
|
||||
{
|
||||
QSize boxSize = this->size_ * container.getScale();
|
||||
container.addElement(new ReplyCurveLayoutElement(
|
||||
*this, boxSize, 1.5 * container.getScale(),
|
||||
this->neededMargin_ * container.getScale()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -126,6 +126,12 @@ enum class MessageElementFlag : int64_t {
|
||||
// e.g. BTTV's SoSnowy during christmas season
|
||||
ZeroWidthEmote = (1LL << 31),
|
||||
|
||||
// for elements of the message reply
|
||||
RepliedMessage = (1LL << 32),
|
||||
|
||||
// for the reply button element
|
||||
ReplyButton = (1LL << 33),
|
||||
|
||||
Default = Timestamp | Badges | Username | BitsStatic | FfzEmoteImage |
|
||||
BttvEmoteImage | TwitchEmoteImage | BitsAmount | Text |
|
||||
AlwaysShow,
|
||||
@@ -209,6 +215,22 @@ private:
|
||||
ImagePtr image_;
|
||||
};
|
||||
|
||||
// contains a image with a circular background color
|
||||
class CircularImageElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
CircularImageElement(ImagePtr image, int padding, QColor background,
|
||||
MessageElementFlags flags);
|
||||
|
||||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
private:
|
||||
ImagePtr image_;
|
||||
int padding_;
|
||||
QColor background_;
|
||||
};
|
||||
|
||||
// contains a text, it will split it into words
|
||||
class TextElement : public MessageElement
|
||||
{
|
||||
@@ -232,6 +254,29 @@ private:
|
||||
std::vector<Word> words_;
|
||||
};
|
||||
|
||||
// contains a text that will be truncated to one line
|
||||
class SingleLineTextElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
SingleLineTextElement(const QString &text, MessageElementFlags flags,
|
||||
const MessageColor &color = MessageColor::Text,
|
||||
FontStyle style = FontStyle::ChatMedium);
|
||||
~SingleLineTextElement() override = default;
|
||||
|
||||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
private:
|
||||
MessageColor color_;
|
||||
FontStyle style_;
|
||||
|
||||
struct Word {
|
||||
QString text;
|
||||
int width = -1;
|
||||
};
|
||||
std::vector<Word> words_;
|
||||
};
|
||||
|
||||
// contains emote data and will pick the emote based on :
|
||||
// a) are images for the emote type enabled
|
||||
// b) which size it wants
|
||||
@@ -355,4 +400,18 @@ public:
|
||||
private:
|
||||
ImageSet images_;
|
||||
};
|
||||
|
||||
class ReplyCurveElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
ReplyCurveElement();
|
||||
|
||||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
private:
|
||||
int neededMargin_;
|
||||
QSize size_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "MessageThread.hpp"
|
||||
|
||||
#include "messages/Message.hpp"
|
||||
#include "util/DebugCount.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
MessageThread::MessageThread(std::shared_ptr<const Message> rootMessage)
|
||||
: rootMessageId_(rootMessage->id)
|
||||
, rootMessage_(std::move(rootMessage))
|
||||
{
|
||||
DebugCount::increase("message threads");
|
||||
}
|
||||
|
||||
MessageThread::~MessageThread()
|
||||
{
|
||||
DebugCount::decrease("message threads");
|
||||
}
|
||||
|
||||
void MessageThread::addToThread(const std::shared_ptr<const Message> &message)
|
||||
{
|
||||
this->replies_.emplace_back(message);
|
||||
}
|
||||
|
||||
void MessageThread::addToThread(const std::weak_ptr<const Message> &message)
|
||||
{
|
||||
this->replies_.push_back(message);
|
||||
}
|
||||
|
||||
size_t MessageThread::liveCount() const
|
||||
{
|
||||
size_t count = 0;
|
||||
for (auto reply : this->replies_)
|
||||
{
|
||||
if (!reply.expired())
|
||||
{
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
size_t MessageThread::liveCount(
|
||||
const std::shared_ptr<const Message> &exclude) const
|
||||
{
|
||||
size_t count = 0;
|
||||
for (auto reply : this->replies_)
|
||||
{
|
||||
if (!reply.expired() && reply.lock() != exclude)
|
||||
{
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
struct Message;
|
||||
|
||||
class MessageThread
|
||||
{
|
||||
public:
|
||||
MessageThread(std::shared_ptr<const Message> rootMessage);
|
||||
~MessageThread();
|
||||
|
||||
void addToThread(const std::shared_ptr<const Message> &message);
|
||||
void addToThread(const std::weak_ptr<const Message> &message);
|
||||
|
||||
/// Returns the number of live reply references
|
||||
size_t liveCount() const;
|
||||
|
||||
/// Returns the number of live reply references
|
||||
size_t liveCount(const std::shared_ptr<const Message> &exclude) const;
|
||||
|
||||
const QString &rootId() const
|
||||
{
|
||||
return rootMessageId_;
|
||||
}
|
||||
|
||||
const std::shared_ptr<const Message> &root() const
|
||||
{
|
||||
return rootMessage_;
|
||||
}
|
||||
|
||||
const std::vector<std::weak_ptr<const Message>> &replies() const
|
||||
{
|
||||
return replies_;
|
||||
}
|
||||
|
||||
private:
|
||||
const QString rootMessageId_;
|
||||
const std::shared_ptr<const Message> rootMessage_;
|
||||
std::vector<std::weak_ptr<const Message>> replies_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -55,6 +55,11 @@ const Message *MessageLayout::getMessage()
|
||||
return this->message_.get();
|
||||
}
|
||||
|
||||
const MessagePtr &MessageLayout::getMessagePtr() const
|
||||
{
|
||||
return this->message_;
|
||||
}
|
||||
|
||||
// Height
|
||||
int MessageLayout::getHeight() const
|
||||
{
|
||||
@@ -147,6 +152,12 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this->renderReplies_ &&
|
||||
element->getFlags().has(MessageElementFlag::RepliedMessage))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
element->addToContainer(*this->container_, flags);
|
||||
}
|
||||
|
||||
@@ -414,4 +425,26 @@ void MessageLayout::addSelectionText(QString &str, int from, int to,
|
||||
this->container_->addSelectionText(str, from, to, copymode);
|
||||
}
|
||||
|
||||
bool MessageLayout::isReplyable() const
|
||||
{
|
||||
if (this->message_->loginName.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->message_->flags.hasAny(
|
||||
{MessageFlag::System, MessageFlag::Subscription,
|
||||
MessageFlag::Timeout, MessageFlag::Whisper}))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MessageLayout::setRenderReplies(bool render)
|
||||
{
|
||||
this->renderReplies_ = render;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -37,6 +37,7 @@ public:
|
||||
~MessageLayout();
|
||||
|
||||
const Message *getMessage();
|
||||
const MessagePtr &getMessagePtr() const;
|
||||
|
||||
int getHeight() const;
|
||||
|
||||
@@ -62,6 +63,8 @@ public:
|
||||
|
||||
// Misc
|
||||
bool isDisabled() const;
|
||||
bool isReplyable() const;
|
||||
void setRenderReplies(bool render);
|
||||
|
||||
private:
|
||||
// variables
|
||||
@@ -69,6 +72,7 @@ private:
|
||||
std::shared_ptr<MessageLayoutContainer> container_;
|
||||
std::shared_ptr<QPixmap> buffer_{};
|
||||
bool bufferValid_ = false;
|
||||
bool renderReplies_ = true;
|
||||
|
||||
int height_ = 0;
|
||||
|
||||
|
||||
@@ -661,6 +661,14 @@ void MessageLayoutContainer::addSelectionText(QString &str, int from, int to,
|
||||
|
||||
for (auto &element : this->elements_)
|
||||
{
|
||||
if (copymode != CopyMode::Everything &&
|
||||
element->getCreator().getFlags().has(
|
||||
MessageElementFlag::RepliedMessage))
|
||||
{
|
||||
// Don't include the message being replied to
|
||||
continue;
|
||||
}
|
||||
|
||||
if (copymode == CopyMode::OnlyTextAndEmotes)
|
||||
{
|
||||
if (element->getCreator().getFlags().hasAny(
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <QDebug>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
@@ -205,6 +206,44 @@ void ImageWithBackgroundLayoutElement::paint(QPainter &painter)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// IMAGE WITH CIRCLE BACKGROUND
|
||||
//
|
||||
ImageWithCircleBackgroundLayoutElement::ImageWithCircleBackgroundLayoutElement(
|
||||
MessageElement &creator, ImagePtr image, const QSize &imageSize,
|
||||
QColor color, int padding)
|
||||
: ImageLayoutElement(creator, image,
|
||||
imageSize + QSize(padding, padding) * 2)
|
||||
, color_(color)
|
||||
, imageSize_(imageSize)
|
||||
, padding_(padding)
|
||||
{
|
||||
}
|
||||
|
||||
void ImageWithCircleBackgroundLayoutElement::paint(QPainter &painter)
|
||||
{
|
||||
if (this->image_ == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto pixmap = this->image_->pixmapOrLoad();
|
||||
if (pixmap && !this->image_->animated())
|
||||
{
|
||||
QRectF boxRect(this->getRect());
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QBrush(this->color_, Qt::SolidPattern));
|
||||
painter.drawEllipse(boxRect);
|
||||
|
||||
QRectF imgRect;
|
||||
imgRect.setTopLeft(boxRect.topLeft());
|
||||
imgRect.setSize(this->imageSize_);
|
||||
imgRect.translate(this->padding_, this->padding_);
|
||||
|
||||
painter.drawPixmap(imgRect, *pixmap, QRectF());
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// TEXT
|
||||
//
|
||||
@@ -402,4 +441,67 @@ int TextIconLayoutElement::getXFromIndex(int index)
|
||||
}
|
||||
}
|
||||
|
||||
ReplyCurveLayoutElement::ReplyCurveLayoutElement(MessageElement &creator,
|
||||
const QSize &size,
|
||||
float thickness,
|
||||
float neededMargin)
|
||||
: MessageLayoutElement(creator, size)
|
||||
, pen_(QColor("#888"), thickness, Qt::SolidLine, Qt::RoundCap)
|
||||
, neededMargin_(neededMargin)
|
||||
{
|
||||
}
|
||||
|
||||
void ReplyCurveLayoutElement::paint(QPainter &painter)
|
||||
{
|
||||
QRectF paintRect(this->getRect());
|
||||
QPainterPath bezierPath;
|
||||
|
||||
qreal top = paintRect.top() + paintRect.height() * 0.25; // 25% from top
|
||||
qreal left = paintRect.left() + this->neededMargin_;
|
||||
qreal bottom = paintRect.bottom() - this->neededMargin_;
|
||||
QPointF startPoint(left, bottom);
|
||||
QPointF controlPoint(left, top);
|
||||
QPointF endPoint(paintRect.right(), top);
|
||||
|
||||
// Create curve path
|
||||
bezierPath.moveTo(startPoint);
|
||||
bezierPath.quadTo(controlPoint, endPoint);
|
||||
|
||||
// Render curve
|
||||
painter.setPen(this->pen_);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.drawPath(bezierPath);
|
||||
}
|
||||
|
||||
void ReplyCurveLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||
{
|
||||
}
|
||||
|
||||
int ReplyCurveLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ReplyCurveLayoutElement::getXFromIndex(int index)
|
||||
{
|
||||
if (index <= 0)
|
||||
{
|
||||
return this->getRect().left();
|
||||
}
|
||||
else
|
||||
{
|
||||
return this->getRect().right();
|
||||
}
|
||||
}
|
||||
|
||||
void ReplyCurveLayoutElement::addCopyTextToString(QString &str, int from,
|
||||
int to) const
|
||||
{
|
||||
}
|
||||
|
||||
int ReplyCurveLayoutElement::getSelectionIndexCount() const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPen>
|
||||
#include <QPoint>
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
@@ -93,6 +94,23 @@ private:
|
||||
QColor color_;
|
||||
};
|
||||
|
||||
class ImageWithCircleBackgroundLayoutElement : public ImageLayoutElement
|
||||
{
|
||||
public:
|
||||
ImageWithCircleBackgroundLayoutElement(MessageElement &creator,
|
||||
ImagePtr image,
|
||||
const QSize &imageSize, QColor color,
|
||||
int padding);
|
||||
|
||||
protected:
|
||||
void paint(QPainter &painter) override;
|
||||
|
||||
private:
|
||||
const QColor color_;
|
||||
const QSize imageSize_;
|
||||
const int padding_;
|
||||
};
|
||||
|
||||
// TEXT
|
||||
class TextLayoutElement : public MessageLayoutElement
|
||||
{
|
||||
@@ -142,4 +160,24 @@ private:
|
||||
QString line2;
|
||||
};
|
||||
|
||||
class ReplyCurveLayoutElement : public MessageLayoutElement
|
||||
{
|
||||
public:
|
||||
ReplyCurveLayoutElement(MessageElement &creator, const QSize &size,
|
||||
float thickness, float lMargin);
|
||||
|
||||
protected:
|
||||
void paint(QPainter &painter) override;
|
||||
void paintAnimated(QPainter &painter, int yOffset) override;
|
||||
int getMouseOverIndex(const QPoint &abs) const override;
|
||||
int getXFromIndex(int index) override;
|
||||
void addCopyTextToString(QString &str, int from = 0,
|
||||
int to = INT_MAX) const override;
|
||||
int getSelectionIndexCount() const override;
|
||||
|
||||
private:
|
||||
const QPen pen_;
|
||||
const float neededMargin_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
Reference in New Issue
Block a user