mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Formalize zero-width emote implementation (#4314)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
db97a14cdc
commit
0acbc0d2c3
|
@ -3,6 +3,8 @@
|
||||||
## Unversioned
|
## Unversioned
|
||||||
|
|
||||||
- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
|
- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
|
||||||
|
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
|
||||||
|
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
|
||||||
- Dev: Ignore unhandled BTTV user-events. (#4438)
|
- Dev: Ignore unhandled BTTV user-events. (#4438)
|
||||||
- Dev: Only log debug messages when NDEBUG is not defined. (#4442)
|
- Dev: Only log debug messages when NDEBUG is not defined. (#4442)
|
||||||
- Dev: Cleaned up theme related code. (#4450)
|
- Dev: Cleaned up theme related code. (#4450)
|
||||||
|
|
|
@ -420,6 +420,8 @@ set(SOURCE_FILES
|
||||||
widgets/Scrollbar.hpp
|
widgets/Scrollbar.hpp
|
||||||
widgets/StreamView.cpp
|
widgets/StreamView.cpp
|
||||||
widgets/StreamView.hpp
|
widgets/StreamView.hpp
|
||||||
|
widgets/TooltipEntryWidget.cpp
|
||||||
|
widgets/TooltipEntryWidget.hpp
|
||||||
widgets/TooltipWidget.cpp
|
widgets/TooltipWidget.cpp
|
||||||
widgets/TooltipWidget.hpp
|
widgets/TooltipWidget.hpp
|
||||||
widgets/Window.cpp
|
widgets/Window.cpp
|
||||||
|
|
|
@ -687,6 +687,26 @@ void MessageBuilder::append(std::unique_ptr<MessageElement> element)
|
||||||
this->message().elements.push_back(std::move(element));
|
this->message().elements.push_back(std::move(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MessageBuilder::isEmpty() const
|
||||||
|
{
|
||||||
|
return this->message_->elements.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageElement &MessageBuilder::back()
|
||||||
|
{
|
||||||
|
assert(!this->isEmpty());
|
||||||
|
return *this->message().elements.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<MessageElement> MessageBuilder::releaseBack()
|
||||||
|
{
|
||||||
|
assert(!this->isEmpty());
|
||||||
|
|
||||||
|
auto ptr = std::move(this->message().elements.back());
|
||||||
|
this->message().elements.pop_back();
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
QString MessageBuilder::matchLink(const QString &string)
|
QString MessageBuilder::matchLink(const QString &string)
|
||||||
{
|
{
|
||||||
LinkParser linkParser(string);
|
LinkParser linkParser(string);
|
||||||
|
|
|
@ -123,6 +123,10 @@ protected:
|
||||||
virtual void addTextOrEmoji(EmotePtr emote);
|
virtual void addTextOrEmoji(EmotePtr emote);
|
||||||
virtual void addTextOrEmoji(const QString &value);
|
virtual void addTextOrEmoji(const QString &value);
|
||||||
|
|
||||||
|
bool isEmpty() const;
|
||||||
|
MessageElement &back();
|
||||||
|
std::unique_ptr<MessageElement> releaseBack();
|
||||||
|
|
||||||
MessageColor textColor_ = MessageColor::Text;
|
MessageColor textColor_ = MessageColor::Text;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -15,6 +15,24 @@
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
MessageElement::MessageElement(MessageElementFlags flags)
|
MessageElement::MessageElement(MessageElementFlags flags)
|
||||||
: flags_(flags)
|
: flags_(flags)
|
||||||
{
|
{
|
||||||
|
@ -216,6 +234,168 @@ MessageLayoutElement *EmoteElement::makeImageLayoutElement(
|
||||||
return new ImageLayoutElement(*this, image, size);
|
return new ImageLayoutElement(*this, image, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// BADGE
|
// BADGE
|
||||||
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
|
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
|
||||||
: MessageElement(flags)
|
: MessageElement(flags)
|
||||||
|
|
|
@ -141,9 +141,7 @@ enum class MessageElementFlag : int64_t {
|
||||||
LowercaseLink = (1LL << 29),
|
LowercaseLink = (1LL << 29),
|
||||||
OriginalLink = (1LL << 30),
|
OriginalLink = (1LL << 30),
|
||||||
|
|
||||||
// ZeroWidthEmotes are emotes that are supposed to overlay over any pre-existing emotes
|
// Unused: (1LL << 31)
|
||||||
// e.g. BTTV's SoSnowy during christmas season or 7TV's RainTime
|
|
||||||
ZeroWidthEmote = (1LL << 31),
|
|
||||||
|
|
||||||
// for elements of the message reply
|
// for elements of the message reply
|
||||||
RepliedMessage = (1LL << 32),
|
RepliedMessage = (1LL << 32),
|
||||||
|
@ -321,6 +319,43 @@ private:
|
||||||
EmotePtr emote_;
|
EmotePtr emote_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A LayeredEmoteElement represents multiple Emotes layered on top of each other.
|
||||||
|
// This class takes care of rendering animated and non-animated emotes in the
|
||||||
|
// correct order and aligning them in the right way.
|
||||||
|
class LayeredEmoteElement : public MessageElement
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LayeredEmoteElement(
|
||||||
|
std::vector<EmotePtr> &&emotes, MessageElementFlags flags,
|
||||||
|
const MessageColor &textElementColor = MessageColor::Text);
|
||||||
|
|
||||||
|
void addEmoteLayer(const EmotePtr &emote);
|
||||||
|
|
||||||
|
void addToContainer(MessageLayoutContainer &container,
|
||||||
|
MessageElementFlags flags) override;
|
||||||
|
|
||||||
|
// Returns a concatenation of each emote layer's cleaned copy string
|
||||||
|
QString getCleanCopyString() const;
|
||||||
|
const std::vector<EmotePtr> &getEmotes() const;
|
||||||
|
std::vector<EmotePtr> getUniqueEmotes() const;
|
||||||
|
const std::vector<QString> &getEmoteTooltips() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
MessageLayoutElement *makeImageLayoutElement(
|
||||||
|
const std::vector<ImagePtr> &image, const std::vector<QSize> &sizes,
|
||||||
|
QSize largestSize);
|
||||||
|
|
||||||
|
QString getCopyString() const;
|
||||||
|
void updateTooltips();
|
||||||
|
std::vector<ImagePtr> getLoadedImages(float scale);
|
||||||
|
|
||||||
|
std::vector<EmotePtr> emotes_;
|
||||||
|
std::vector<QString> emoteTooltips_;
|
||||||
|
|
||||||
|
std::unique_ptr<TextElement> textElement_;
|
||||||
|
MessageColor textElementColor_;
|
||||||
|
};
|
||||||
|
|
||||||
class BadgeElement : public MessageElement
|
class BadgeElement : public MessageElement
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -67,10 +67,7 @@ void MessageLayoutContainer::clear()
|
||||||
|
|
||||||
void MessageLayoutContainer::addElement(MessageLayoutElement *element)
|
void MessageLayoutContainer::addElement(MessageLayoutElement *element)
|
||||||
{
|
{
|
||||||
bool isZeroWidth =
|
if (!this->fitsInLine(element->getRect().width()))
|
||||||
element->getFlags().has(MessageElementFlag::ZeroWidthEmote);
|
|
||||||
|
|
||||||
if (!isZeroWidth && !this->fitsInLine(element->getRect().width()))
|
|
||||||
{
|
{
|
||||||
this->breakLine();
|
this->breakLine();
|
||||||
}
|
}
|
||||||
|
@ -175,14 +172,6 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
|
||||||
this->lineHeight_ = std::max(this->lineHeight_, elementLineHeight);
|
this->lineHeight_ = std::max(this->lineHeight_, elementLineHeight);
|
||||||
|
|
||||||
auto xOffset = 0;
|
auto xOffset = 0;
|
||||||
bool isZeroWidthEmote = element->getCreator().getFlags().has(
|
|
||||||
MessageElementFlag::ZeroWidthEmote);
|
|
||||||
|
|
||||||
if (isZeroWidthEmote && !isRTLMode)
|
|
||||||
{
|
|
||||||
xOffset -= element->getRect().width() + this->spaceWidth_;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto yOffset = 0;
|
auto yOffset = 0;
|
||||||
|
|
||||||
if (element->getCreator().getFlags().has(
|
if (element->getCreator().getFlags().has(
|
||||||
|
@ -195,7 +184,7 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
|
||||||
|
|
||||||
if (getSettings()->removeSpacesBetweenEmotes &&
|
if (getSettings()->removeSpacesBetweenEmotes &&
|
||||||
element->getFlags().hasAny({MessageElementFlag::EmoteImages}) &&
|
element->getFlags().hasAny({MessageElementFlag::EmoteImages}) &&
|
||||||
!isZeroWidthEmote && shouldRemoveSpaceBetweenEmotes())
|
shouldRemoveSpaceBetweenEmotes())
|
||||||
{
|
{
|
||||||
// Move cursor one 'space width' to the left (right in case of RTL) to combine hug the previous emote
|
// Move cursor one 'space width' to the left (right in case of RTL) to combine hug the previous emote
|
||||||
if (isRTLMode)
|
if (isRTLMode)
|
||||||
|
@ -230,16 +219,13 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
|
||||||
}
|
}
|
||||||
|
|
||||||
// set current x
|
// set current x
|
||||||
if (!isZeroWidthEmote)
|
if (isRTLMode)
|
||||||
{
|
{
|
||||||
if (isRTLMode)
|
this->currentX_ -= element->getRect().width();
|
||||||
{
|
}
|
||||||
this->currentX_ -= element->getRect().width();
|
else
|
||||||
}
|
{
|
||||||
else
|
this->currentX_ += element->getRect().width();
|
||||||
{
|
|
||||||
this->currentX_ += element->getRect().width();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element->hasTrailingSpace())
|
if (element->hasTrailingSpace())
|
||||||
|
|
|
@ -15,6 +15,14 @@
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
const QChar RTL_EMBED(0x202B);
|
const QChar RTL_EMBED(0x202B);
|
||||||
|
|
||||||
|
void alignRectBottomCenter(QRectF &rect, const QRectF &reference)
|
||||||
|
{
|
||||||
|
QPointF newCenter(reference.center().x(),
|
||||||
|
reference.bottom() - (rect.height() / 2.0));
|
||||||
|
rect.moveCenter(newCenter);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -184,6 +192,133 @@ int ImageLayoutElement::getXFromIndex(int index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// LAYERED IMAGE
|
||||||
|
//
|
||||||
|
|
||||||
|
LayeredImageLayoutElement::LayeredImageLayoutElement(
|
||||||
|
MessageElement &creator, std::vector<ImagePtr> images,
|
||||||
|
std::vector<QSize> sizes, QSize largestSize)
|
||||||
|
: MessageLayoutElement(creator, largestSize)
|
||||||
|
, images_(std::move(images))
|
||||||
|
, sizes_(std::move(sizes))
|
||||||
|
{
|
||||||
|
assert(this->images_.size() == this->sizes_.size());
|
||||||
|
this->trailingSpace = creator.hasTrailingSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayeredImageLayoutElement::addCopyTextToString(QString &str, uint32_t from,
|
||||||
|
uint32_t to) const
|
||||||
|
{
|
||||||
|
const auto *layeredEmoteElement =
|
||||||
|
dynamic_cast<LayeredEmoteElement *>(&this->getCreator());
|
||||||
|
if (layeredEmoteElement)
|
||||||
|
{
|
||||||
|
// cleaning is taken care in call
|
||||||
|
str += layeredEmoteElement->getCleanCopyString();
|
||||||
|
if (this->hasTrailingSpace())
|
||||||
|
{
|
||||||
|
str += " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int LayeredImageLayoutElement::getSelectionIndexCount() const
|
||||||
|
{
|
||||||
|
return this->trailingSpace ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayeredImageLayoutElement::paint(QPainter &painter)
|
||||||
|
{
|
||||||
|
auto fullRect = QRectF(this->getRect());
|
||||||
|
|
||||||
|
for (size_t i = 0; i < this->images_.size(); ++i)
|
||||||
|
{
|
||||||
|
auto &img = this->images_[i];
|
||||||
|
if (img == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pixmap = img->pixmapOrLoad();
|
||||||
|
if (img->animated())
|
||||||
|
{
|
||||||
|
// As soon as we see an animated emote layer, we can stop rendering
|
||||||
|
// the static emotes. The paintAnimated function will render any
|
||||||
|
// static emotes layered on top of the first seen animated emote.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixmap)
|
||||||
|
{
|
||||||
|
// Matching the web chat behavior, we center the emote within the overall
|
||||||
|
// binding box. E.g. small overlay emotes like cvMask will sit in the direct
|
||||||
|
// center of even wide emotes.
|
||||||
|
auto &size = this->sizes_[i];
|
||||||
|
QRectF destRect(0, 0, size.width(), size.height());
|
||||||
|
alignRectBottomCenter(destRect, fullRect);
|
||||||
|
|
||||||
|
painter.drawPixmap(destRect, *pixmap, QRectF());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayeredImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||||
|
{
|
||||||
|
auto fullRect = QRectF(this->getRect());
|
||||||
|
fullRect.moveTop(fullRect.y() + yOffset);
|
||||||
|
bool animatedFlag = false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < this->images_.size(); ++i)
|
||||||
|
{
|
||||||
|
auto &img = this->images_[i];
|
||||||
|
if (img == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a static emote layered on top of an animated emote, we need
|
||||||
|
// to render the static emote again after animating anything below it.
|
||||||
|
if (img->animated() || animatedFlag)
|
||||||
|
{
|
||||||
|
if (auto pixmap = img->pixmapOrLoad())
|
||||||
|
{
|
||||||
|
// Matching the web chat behavior, we center the emote within the overall
|
||||||
|
// binding box. E.g. small overlay emotes like cvMask will sit in the direct
|
||||||
|
// center of even wide emotes.
|
||||||
|
auto &size = this->sizes_[i];
|
||||||
|
QRectF destRect(0, 0, size.width(), size.height());
|
||||||
|
alignRectBottomCenter(destRect, fullRect);
|
||||||
|
|
||||||
|
painter.drawPixmap(destRect, *pixmap, QRectF());
|
||||||
|
animatedFlag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int LayeredImageLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LayeredImageLayoutElement::getXFromIndex(int index)
|
||||||
|
{
|
||||||
|
if (index <= 0)
|
||||||
|
{
|
||||||
|
return this->getRect().left();
|
||||||
|
}
|
||||||
|
else if (index == 1)
|
||||||
|
{
|
||||||
|
// fourtf: remove space width
|
||||||
|
return this->getRect().right();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return this->getRect().right();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// IMAGE WITH BACKGROUND
|
// IMAGE WITH BACKGROUND
|
||||||
//
|
//
|
||||||
|
|
|
@ -83,6 +83,26 @@ protected:
|
||||||
ImagePtr image_;
|
ImagePtr image_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class LayeredImageLayoutElement : public MessageLayoutElement
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LayeredImageLayoutElement(MessageElement &creator,
|
||||||
|
std::vector<ImagePtr> images,
|
||||||
|
std::vector<QSize> sizes, QSize largestSize);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void addCopyTextToString(QString &str, uint32_t from = 0,
|
||||||
|
uint32_t to = UINT32_MAX) const override;
|
||||||
|
int getSelectionIndexCount() const override;
|
||||||
|
void paint(QPainter &painter) override;
|
||||||
|
void paintAnimated(QPainter &painter, int yOffset) override;
|
||||||
|
int getMouseOverIndex(const QPoint &abs) const override;
|
||||||
|
int getXFromIndex(int index) override;
|
||||||
|
|
||||||
|
std::vector<ImagePtr> images_;
|
||||||
|
std::vector<QSize> sizes_;
|
||||||
|
};
|
||||||
|
|
||||||
class ImageWithBackgroundLayoutElement : public ImageLayoutElement
|
class ImageWithBackgroundLayoutElement : public ImageLayoutElement
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -1023,6 +1023,7 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
|
||||||
|
|
||||||
auto flags = MessageElementFlags();
|
auto flags = MessageElementFlags();
|
||||||
auto emote = boost::optional<EmotePtr>{};
|
auto emote = boost::optional<EmotePtr>{};
|
||||||
|
bool zeroWidth = false;
|
||||||
|
|
||||||
// Emote order:
|
// Emote order:
|
||||||
// - FrankerFaceZ Channel
|
// - FrankerFaceZ Channel
|
||||||
|
@ -1044,10 +1045,7 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
|
||||||
(emote = this->twitchChannel->seventvEmote(name)))
|
(emote = this->twitchChannel->seventvEmote(name)))
|
||||||
{
|
{
|
||||||
flags = MessageElementFlag::SevenTVEmote;
|
flags = MessageElementFlag::SevenTVEmote;
|
||||||
if (emote.value()->zeroWidth)
|
zeroWidth = emote.value()->zeroWidth;
|
||||||
{
|
|
||||||
flags.set(MessageElementFlag::ZeroWidthEmote);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if ((emote = globalFfzEmotes.emote(name)))
|
else if ((emote = globalFfzEmotes.emote(name)))
|
||||||
{
|
{
|
||||||
|
@ -1056,23 +1054,45 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
|
||||||
else if ((emote = globalBttvEmotes.emote(name)))
|
else if ((emote = globalBttvEmotes.emote(name)))
|
||||||
{
|
{
|
||||||
flags = MessageElementFlag::BttvEmote;
|
flags = MessageElementFlag::BttvEmote;
|
||||||
|
zeroWidth = zeroWidthEmotes.contains(name.string);
|
||||||
if (zeroWidthEmotes.contains(name.string))
|
|
||||||
{
|
|
||||||
flags.set(MessageElementFlag::ZeroWidthEmote);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if ((emote = globalSeventvEmotes.globalEmote(name)))
|
else if ((emote = globalSeventvEmotes.globalEmote(name)))
|
||||||
{
|
{
|
||||||
flags = MessageElementFlag::SevenTVEmote;
|
flags = MessageElementFlag::SevenTVEmote;
|
||||||
if (emote.value()->zeroWidth)
|
zeroWidth = emote.value()->zeroWidth;
|
||||||
{
|
|
||||||
flags.set(MessageElementFlag::ZeroWidthEmote);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emote)
|
if (emote)
|
||||||
{
|
{
|
||||||
|
if (zeroWidth && getSettings()->enableZeroWidthEmotes &&
|
||||||
|
!this->isEmpty())
|
||||||
|
{
|
||||||
|
// Attempt to merge current zero-width emote into any previous emotes
|
||||||
|
auto asEmote = dynamic_cast<EmoteElement *>(&this->back());
|
||||||
|
if (asEmote)
|
||||||
|
{
|
||||||
|
// Make sure to access asEmote before taking ownership when releasing
|
||||||
|
auto baseEmote = asEmote->getEmote();
|
||||||
|
// Need to remove EmoteElement and replace with LayeredEmoteElement
|
||||||
|
auto baseEmoteElement = this->releaseBack();
|
||||||
|
|
||||||
|
std::vector<EmotePtr> layers = {baseEmote, emote.get()};
|
||||||
|
this->emplace<LayeredEmoteElement>(std::move(layers),
|
||||||
|
baseEmoteElement->getFlags(),
|
||||||
|
this->textColor_);
|
||||||
|
return Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto asLayered = dynamic_cast<LayeredEmoteElement *>(&this->back());
|
||||||
|
if (asLayered)
|
||||||
|
{
|
||||||
|
asLayered->addEmoteLayer(emote.get());
|
||||||
|
return Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No emote to merge with, just show as regular emote
|
||||||
|
}
|
||||||
|
|
||||||
this->emplace<EmoteElement>(emote.get(), flags, this->textColor_);
|
this->emplace<EmoteElement>(emote.get(), flags, this->textColor_);
|
||||||
return Success;
|
return Success;
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,6 +213,7 @@ public:
|
||||||
false};
|
false};
|
||||||
BoolSetting enableEmoteImages = {"/emotes/enableEmoteImages", true};
|
BoolSetting enableEmoteImages = {"/emotes/enableEmoteImages", true};
|
||||||
BoolSetting animateEmotes = {"/emotes/enableGifAnimations", true};
|
BoolSetting animateEmotes = {"/emotes/enableGifAnimations", true};
|
||||||
|
BoolSetting enableZeroWidthEmotes = {"/emotes/enableZeroWidthEmotes", true};
|
||||||
FloatSetting emoteScale = {"/emotes/scale", 1.f};
|
FloatSetting emoteScale = {"/emotes/scale", 1.f};
|
||||||
BoolSetting showUnlistedSevenTVEmotes = {
|
BoolSetting showUnlistedSevenTVEmotes = {
|
||||||
"/emotes/showUnlistedSevenTVEmotes", false};
|
"/emotes/showUnlistedSevenTVEmotes", false};
|
||||||
|
|
119
src/widgets/TooltipEntryWidget.cpp
Normal file
119
src/widgets/TooltipEntryWidget.cpp
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
#include "widgets/TooltipEntryWidget.hpp"
|
||||||
|
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
TooltipEntryWidget::TooltipEntryWidget(QWidget *parent)
|
||||||
|
: TooltipEntryWidget(nullptr, "", 0, 0, parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TooltipEntryWidget::TooltipEntryWidget(ImagePtr image, const QString &text,
|
||||||
|
int customWidth, int customHeight,
|
||||||
|
QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, image_(image)
|
||||||
|
, customImgWidth_(customWidth)
|
||||||
|
, customImgHeight_(customHeight)
|
||||||
|
{
|
||||||
|
auto *layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
this->setLayout(layout);
|
||||||
|
|
||||||
|
this->displayImage_ = new QLabel();
|
||||||
|
this->displayImage_->setAlignment(Qt::AlignHCenter);
|
||||||
|
this->displayImage_->setStyleSheet("background: transparent");
|
||||||
|
this->displayText_ = new QLabel(text);
|
||||||
|
this->displayText_->setAlignment(Qt::AlignHCenter);
|
||||||
|
this->displayText_->setStyleSheet("background: transparent");
|
||||||
|
|
||||||
|
layout->addWidget(this->displayImage_);
|
||||||
|
layout->addWidget(this->displayText_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipEntryWidget::setWordWrap(bool wrap)
|
||||||
|
{
|
||||||
|
this->displayText_->setWordWrap(wrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipEntryWidget::setImageScale(int w, int h)
|
||||||
|
{
|
||||||
|
if (this->customImgWidth_ == w && this->customImgHeight_ == h)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->customImgWidth_ = w;
|
||||||
|
this->customImgHeight_ = h;
|
||||||
|
this->refreshPixmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipEntryWidget::setText(const QString &text)
|
||||||
|
{
|
||||||
|
this->displayText_->setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipEntryWidget::setImage(ImagePtr image)
|
||||||
|
{
|
||||||
|
if (this->image_ == image)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->clearImage();
|
||||||
|
this->image_ = std::move(image);
|
||||||
|
this->refreshPixmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipEntryWidget::clearImage()
|
||||||
|
{
|
||||||
|
this->displayImage_->hide();
|
||||||
|
this->image_ = nullptr;
|
||||||
|
this->setImageScale(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TooltipEntryWidget::refreshPixmap()
|
||||||
|
{
|
||||||
|
if (!this->image_)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pixmap = this->image_->pixmapOrLoad();
|
||||||
|
if (!pixmap)
|
||||||
|
{
|
||||||
|
this->attemptRefresh_ = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->customImgWidth_ > 0 || this->customImgHeight_ > 0)
|
||||||
|
{
|
||||||
|
this->displayImage_->setPixmap(pixmap->scaled(this->customImgWidth_,
|
||||||
|
this->customImgHeight_,
|
||||||
|
Qt::KeepAspectRatio));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->displayImage_->setPixmap(*pixmap);
|
||||||
|
}
|
||||||
|
this->displayImage_->show();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TooltipEntryWidget::animated() const
|
||||||
|
{
|
||||||
|
return this->image_ && this->image_->animated();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TooltipEntryWidget::hasImage() const
|
||||||
|
{
|
||||||
|
return this->image_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TooltipEntryWidget::attemptRefresh() const
|
||||||
|
{
|
||||||
|
return this->attemptRefresh_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
42
src/widgets/TooltipEntryWidget.hpp
Normal file
42
src/widgets/TooltipEntryWidget.hpp
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "messages/Image.hpp"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class TooltipEntryWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
TooltipEntryWidget(QWidget *parent = nullptr);
|
||||||
|
TooltipEntryWidget(ImagePtr image, const QString &text, int customWidth,
|
||||||
|
int customHeight, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setImageScale(int w, int h);
|
||||||
|
void setWordWrap(bool wrap);
|
||||||
|
|
||||||
|
void setText(const QString &text);
|
||||||
|
void setImage(ImagePtr image);
|
||||||
|
void clearImage();
|
||||||
|
bool refreshPixmap();
|
||||||
|
|
||||||
|
bool animated() const;
|
||||||
|
bool hasImage() const;
|
||||||
|
bool attemptRefresh() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel *displayImage_ = nullptr;
|
||||||
|
QLabel *displayText_ = nullptr;
|
||||||
|
|
||||||
|
bool attemptRefresh_ = false;
|
||||||
|
|
||||||
|
ImagePtr image_ = nullptr;
|
||||||
|
int customImgWidth_ = 0;
|
||||||
|
int customImgHeight_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -6,7 +6,9 @@
|
||||||
#include "singletons/WindowManager.hpp"
|
#include "singletons/WindowManager.hpp"
|
||||||
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
// number of columns in grid mode
|
||||||
|
#define GRID_NUM_COLS 3
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
@ -20,8 +22,6 @@ TooltipWidget::TooltipWidget(BaseWidget *parent)
|
||||||
: BaseWindow({BaseWindow::TopMost, BaseWindow::DontFocus,
|
: BaseWindow({BaseWindow::TopMost, BaseWindow::DontFocus,
|
||||||
BaseWindow::DisableLayoutSave},
|
BaseWindow::DisableLayoutSave},
|
||||||
parent)
|
parent)
|
||||||
, displayImage_(new QLabel(this))
|
|
||||||
, displayText_(new QLabel(this))
|
|
||||||
{
|
{
|
||||||
this->setStyleSheet("color: #fff; background: rgba(11, 11, 11, 0.8)");
|
this->setStyleSheet("color: #fff; background: rgba(11, 11, 11, 0.8)");
|
||||||
this->setAttribute(Qt::WA_TranslucentBackground);
|
this->setAttribute(Qt::WA_TranslucentBackground);
|
||||||
|
@ -29,18 +29,10 @@ TooltipWidget::TooltipWidget(BaseWidget *parent)
|
||||||
|
|
||||||
this->setStayInScreenRect(true);
|
this->setStayInScreenRect(true);
|
||||||
|
|
||||||
displayImage_->setAlignment(Qt::AlignHCenter);
|
// Default to using vertical layout
|
||||||
displayImage_->setStyleSheet("background: transparent");
|
this->initializeVLayout();
|
||||||
|
this->setLayout(this->vLayout_);
|
||||||
displayText_->setAlignment(Qt::AlignHCenter);
|
this->currentStyle_ = TooltipStyle::Vertical;
|
||||||
displayText_->setStyleSheet("background: transparent");
|
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(this);
|
|
||||||
layout->setSizeConstraint(QLayout::SetFixedSize);
|
|
||||||
layout->setContentsMargins(10, 5, 10, 5);
|
|
||||||
layout->addWidget(displayImage_);
|
|
||||||
layout->addWidget(displayText_);
|
|
||||||
this->setLayout(layout);
|
|
||||||
|
|
||||||
this->connections_.managedConnect(getFonts()->fontChanged, [this] {
|
this->connections_.managedConnect(getFonts()->fontChanged, [this] {
|
||||||
this->updateFont();
|
this->updateFont();
|
||||||
|
@ -49,24 +41,219 @@ TooltipWidget::TooltipWidget(BaseWidget *parent)
|
||||||
|
|
||||||
auto windows = getApp()->windows;
|
auto windows = getApp()->windows;
|
||||||
this->connections_.managedConnect(windows->gifRepaintRequested, [this] {
|
this->connections_.managedConnect(windows->gifRepaintRequested, [this] {
|
||||||
if (this->image_ && this->image_->animated())
|
for (int i = 0; i < this->visibleEntries_; ++i)
|
||||||
{
|
{
|
||||||
this->refreshPixmap();
|
auto entry = this->entryAt(i);
|
||||||
|
if (entry && entry->animated())
|
||||||
|
{
|
||||||
|
entry->refreshPixmap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this->connections_.managedConnect(windows->miscUpdate, [this] {
|
this->connections_.managedConnect(windows->miscUpdate, [this] {
|
||||||
if (this->image_ && this->attemptRefresh)
|
bool needSizeAdjustment = false;
|
||||||
|
for (int i = 0; i < this->visibleEntries_; ++i)
|
||||||
{
|
{
|
||||||
if (this->refreshPixmap())
|
auto entry = this->entryAt(i);
|
||||||
|
if (entry->hasImage() && entry->attemptRefresh())
|
||||||
{
|
{
|
||||||
this->attemptRefresh = false;
|
bool successfullyUpdated = entry->refreshPixmap();
|
||||||
this->adjustSize();
|
needSizeAdjustment |= successfullyUpdated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needSizeAdjustment)
|
||||||
|
{
|
||||||
|
this->adjustSize();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TooltipWidget::setOne(const TooltipEntry &entry, TooltipStyle style)
|
||||||
|
{
|
||||||
|
this->set({entry}, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipWidget::set(const std::vector<TooltipEntry> &entries,
|
||||||
|
TooltipStyle style)
|
||||||
|
{
|
||||||
|
this->setCurrentStyle(style);
|
||||||
|
|
||||||
|
int delta = entries.size() - this->currentLayoutCount();
|
||||||
|
if (delta > 0)
|
||||||
|
{
|
||||||
|
// Need to add more TooltipEntry instances
|
||||||
|
int base = this->currentLayoutCount();
|
||||||
|
for (int i = 0; i < delta; ++i)
|
||||||
|
{
|
||||||
|
this->addNewEntry(base + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->setVisibleEntries(entries.size());
|
||||||
|
|
||||||
|
for (int i = 0; i < entries.size(); ++i)
|
||||||
|
{
|
||||||
|
if (auto entryWidget = this->entryAt(i))
|
||||||
|
{
|
||||||
|
auto &entry = entries[i];
|
||||||
|
entryWidget->setImage(entry.image);
|
||||||
|
entryWidget->setText(entry.text);
|
||||||
|
entryWidget->setImageScale(entry.customWidth, entry.customHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipWidget::setVisibleEntries(int n)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < this->currentLayoutCount(); ++i)
|
||||||
|
{
|
||||||
|
auto *entry = this->entryAt(i);
|
||||||
|
if (entry == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= n)
|
||||||
|
{
|
||||||
|
entry->hide();
|
||||||
|
entry->clearImage();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entry->show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->visibleEntries_ = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipWidget::addNewEntry(int absoluteIndex)
|
||||||
|
{
|
||||||
|
switch (this->currentStyle_)
|
||||||
|
{
|
||||||
|
case TooltipStyle::Vertical:
|
||||||
|
this->vLayout_->addWidget(new TooltipEntryWidget(),
|
||||||
|
Qt::AlignHCenter);
|
||||||
|
return;
|
||||||
|
case TooltipStyle::Grid:
|
||||||
|
if (absoluteIndex == 0)
|
||||||
|
{
|
||||||
|
// Top row spans all columns
|
||||||
|
this->gLayout_->addWidget(new TooltipEntryWidget(), 0, 0, 1,
|
||||||
|
GRID_NUM_COLS, Qt::AlignCenter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int row = ((absoluteIndex - 1) / GRID_NUM_COLS) + 1;
|
||||||
|
int col = (absoluteIndex - 1) % GRID_NUM_COLS;
|
||||||
|
this->gLayout_->addWidget(new TooltipEntryWidget(), row, col,
|
||||||
|
Qt::AlignHCenter | Qt::AlignBottom);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// May be nullptr
|
||||||
|
QLayout *TooltipWidget::currentLayout() const
|
||||||
|
{
|
||||||
|
switch (this->currentStyle_)
|
||||||
|
{
|
||||||
|
case TooltipStyle::Vertical:
|
||||||
|
return this->vLayout_;
|
||||||
|
case TooltipStyle::Grid:
|
||||||
|
return this->gLayout_;
|
||||||
|
default:
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int TooltipWidget::currentLayoutCount() const
|
||||||
|
{
|
||||||
|
if (auto *layout = this->currentLayout())
|
||||||
|
{
|
||||||
|
return layout->count();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// May be nullptr
|
||||||
|
TooltipEntryWidget *TooltipWidget::entryAt(int n)
|
||||||
|
{
|
||||||
|
if (auto *layout = this->currentLayout())
|
||||||
|
{
|
||||||
|
return dynamic_cast<TooltipEntryWidget *>(layout->itemAt(n)->widget());
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipWidget::setCurrentStyle(TooltipStyle style)
|
||||||
|
{
|
||||||
|
if (this->currentStyle_ == style)
|
||||||
|
{
|
||||||
|
// Nothing to update
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->clearEntries();
|
||||||
|
this->deleteCurrentLayout();
|
||||||
|
|
||||||
|
switch (style)
|
||||||
|
{
|
||||||
|
case TooltipStyle::Vertical:
|
||||||
|
this->initializeVLayout();
|
||||||
|
this->setLayout(this->vLayout_);
|
||||||
|
break;
|
||||||
|
case TooltipStyle::Grid:
|
||||||
|
this->initializeGLayout();
|
||||||
|
this->setLayout(this->gLayout_);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->currentStyle_ = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipWidget::deleteCurrentLayout()
|
||||||
|
{
|
||||||
|
auto *currentLayout = this->layout();
|
||||||
|
delete currentLayout;
|
||||||
|
|
||||||
|
switch (this->currentStyle_)
|
||||||
|
{
|
||||||
|
case TooltipStyle::Vertical:
|
||||||
|
this->vLayout_ = nullptr;
|
||||||
|
break;
|
||||||
|
case TooltipStyle::Grid:
|
||||||
|
this->gLayout_ = nullptr;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipWidget::initializeVLayout()
|
||||||
|
{
|
||||||
|
auto *vLayout = new QVBoxLayout(this);
|
||||||
|
vLayout->setSizeConstraint(QLayout::SetFixedSize);
|
||||||
|
vLayout->setContentsMargins(10, 5, 10, 5);
|
||||||
|
vLayout->setSpacing(10);
|
||||||
|
this->vLayout_ = vLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipWidget::initializeGLayout()
|
||||||
|
{
|
||||||
|
auto *gLayout = new QGridLayout(this);
|
||||||
|
gLayout->setSizeConstraint(QLayout::SetFixedSize);
|
||||||
|
gLayout->setContentsMargins(10, 5, 10, 5);
|
||||||
|
gLayout->setHorizontalSpacing(8);
|
||||||
|
gLayout->setVerticalSpacing(10);
|
||||||
|
this->gLayout_ = gLayout;
|
||||||
|
}
|
||||||
|
|
||||||
void TooltipWidget::themeChangedEvent()
|
void TooltipWidget::themeChangedEvent()
|
||||||
{
|
{
|
||||||
// this->setStyleSheet("color: #fff; background: #000");
|
// this->setStyleSheet("color: #fff; background: #000");
|
||||||
|
@ -90,49 +277,26 @@ void TooltipWidget::updateFont()
|
||||||
getFonts()->getFont(FontStyle::ChatMediumSmall, this->scale()));
|
getFonts()->getFont(FontStyle::ChatMediumSmall, this->scale()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TooltipWidget::setText(QString text)
|
|
||||||
{
|
|
||||||
this->displayText_->setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TooltipWidget::setWordWrap(bool wrap)
|
void TooltipWidget::setWordWrap(bool wrap)
|
||||||
{
|
{
|
||||||
this->displayText_->setWordWrap(wrap);
|
for (int i = 0; i < this->visibleEntries_; ++i)
|
||||||
}
|
|
||||||
|
|
||||||
void TooltipWidget::clearImage()
|
|
||||||
{
|
|
||||||
this->displayImage_->hide();
|
|
||||||
this->image_ = nullptr;
|
|
||||||
this->setImageScale(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TooltipWidget::setImage(ImagePtr image)
|
|
||||||
{
|
|
||||||
if (this->image_ == image)
|
|
||||||
{
|
{
|
||||||
return;
|
auto entry = this->entryAt(i);
|
||||||
|
if (entry)
|
||||||
|
{
|
||||||
|
entry->setWordWrap(wrap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// hide image until loaded and reset scale
|
|
||||||
this->clearImage();
|
|
||||||
this->image_ = std::move(image);
|
|
||||||
this->refreshPixmap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TooltipWidget::setImageScale(int w, int h)
|
void TooltipWidget::clearEntries()
|
||||||
{
|
{
|
||||||
if (this->customImgWidth == w && this->customImgHeight == h)
|
this->setVisibleEntries(0);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this->customImgWidth = w;
|
|
||||||
this->customImgHeight = h;
|
|
||||||
this->refreshPixmap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TooltipWidget::hideEvent(QHideEvent *)
|
void TooltipWidget::hideEvent(QHideEvent *)
|
||||||
{
|
{
|
||||||
this->clearImage();
|
this->clearEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TooltipWidget::showEvent(QShowEvent *)
|
void TooltipWidget::showEvent(QShowEvent *)
|
||||||
|
@ -140,34 +304,6 @@ void TooltipWidget::showEvent(QShowEvent *)
|
||||||
this->adjustSize();
|
this->adjustSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TooltipWidget::refreshPixmap()
|
|
||||||
{
|
|
||||||
if (!this->image_)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto pixmap = this->image_->pixmapOrLoad();
|
|
||||||
if (!pixmap)
|
|
||||||
{
|
|
||||||
this->attemptRefresh = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->customImgWidth > 0 || this->customImgHeight > 0)
|
|
||||||
{
|
|
||||||
this->displayImage_->setPixmap(pixmap->scaled(
|
|
||||||
this->customImgWidth, this->customImgHeight, Qt::KeepAspectRatio));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this->displayImage_->setPixmap(*pixmap);
|
|
||||||
}
|
|
||||||
this->displayImage_->show();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TooltipWidget::changeEvent(QEvent *)
|
void TooltipWidget::changeEvent(QEvent *)
|
||||||
{
|
{
|
||||||
// clear parents event
|
// clear parents event
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "widgets/BaseWindow.hpp"
|
#include "widgets/BaseWindow.hpp"
|
||||||
|
#include "widgets/TooltipEntryWidget.hpp"
|
||||||
|
|
||||||
#include <pajlada/signals/signalholder.hpp>
|
#include <pajlada/signals/signalholder.hpp>
|
||||||
|
#include <QGridLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QLayout>
|
||||||
|
#include <QVBoxLayout>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -11,6 +15,15 @@ namespace chatterino {
|
||||||
class Image;
|
class Image;
|
||||||
using ImagePtr = std::shared_ptr<Image>;
|
using ImagePtr = std::shared_ptr<Image>;
|
||||||
|
|
||||||
|
struct TooltipEntry {
|
||||||
|
ImagePtr image;
|
||||||
|
QString text;
|
||||||
|
int customWidth = 0;
|
||||||
|
int customHeight = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TooltipStyle { Vertical, Grid };
|
||||||
|
|
||||||
class TooltipWidget : public BaseWindow
|
class TooltipWidget : public BaseWindow
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -21,11 +34,13 @@ public:
|
||||||
TooltipWidget(BaseWidget *parent = nullptr);
|
TooltipWidget(BaseWidget *parent = nullptr);
|
||||||
~TooltipWidget() override = default;
|
~TooltipWidget() override = default;
|
||||||
|
|
||||||
void setText(QString text);
|
void setOne(const TooltipEntry &entry,
|
||||||
|
TooltipStyle style = TooltipStyle::Vertical);
|
||||||
|
void set(const std::vector<TooltipEntry> &entries,
|
||||||
|
TooltipStyle style = TooltipStyle::Vertical);
|
||||||
|
|
||||||
void setWordWrap(bool wrap);
|
void setWordWrap(bool wrap);
|
||||||
void clearImage();
|
void clearEntries();
|
||||||
void setImage(ImagePtr image);
|
|
||||||
void setImageScale(int w, int h);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void showEvent(QShowEvent *) override;
|
void showEvent(QShowEvent *) override;
|
||||||
|
@ -39,17 +54,24 @@ protected:
|
||||||
private:
|
private:
|
||||||
void updateFont();
|
void updateFont();
|
||||||
|
|
||||||
// used by WindowManager::gifRepaintRequested signal to progress frames when tooltip image is animated
|
QLayout *currentLayout() const;
|
||||||
bool refreshPixmap();
|
int currentLayoutCount() const;
|
||||||
|
TooltipEntryWidget *entryAt(int n);
|
||||||
|
|
||||||
// set to true when tooltip image did not finish loading yet (pixmapOrLoad returned false)
|
void setVisibleEntries(int n);
|
||||||
bool attemptRefresh{false};
|
void setCurrentStyle(TooltipStyle style);
|
||||||
|
void addNewEntry(int absoluteIndex);
|
||||||
|
|
||||||
|
void deleteCurrentLayout();
|
||||||
|
void initializeVLayout();
|
||||||
|
void initializeGLayout();
|
||||||
|
|
||||||
|
int visibleEntries_ = 0;
|
||||||
|
|
||||||
|
TooltipStyle currentStyle_;
|
||||||
|
QVBoxLayout *vLayout_;
|
||||||
|
QGridLayout *gLayout_;
|
||||||
|
|
||||||
ImagePtr image_ = nullptr;
|
|
||||||
int customImgWidth = 0;
|
|
||||||
int customImgHeight = 0;
|
|
||||||
QLabel *displayImage_;
|
|
||||||
QLabel *displayText_;
|
|
||||||
pajlada::Signals::SignalHolder connections_;
|
pajlada::Signals::SignalHolder connections_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
#define DRAW_WIDTH (this->width())
|
#define DRAW_WIDTH (this->width())
|
||||||
#define SELECTION_RESUME_SCROLLING_MSG_THRESHOLD 3
|
#define SELECTION_RESUME_SCROLLING_MSG_THRESHOLD 3
|
||||||
#define CHAT_HOVER_PAUSE_DURATION 1000
|
#define CHAT_HOVER_PAUSE_DURATION 1000
|
||||||
|
#define TOOLTIP_EMOTE_ENTRIES_LIMIT 7
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
namespace {
|
namespace {
|
||||||
|
@ -1658,10 +1659,12 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
|
||||||
auto element = &hoverLayoutElement->getCreator();
|
auto element = &hoverLayoutElement->getCreator();
|
||||||
bool isLinkValid = hoverLayoutElement->getLink().isValid();
|
bool isLinkValid = hoverLayoutElement->getLink().isValid();
|
||||||
auto emoteElement = dynamic_cast<const EmoteElement *>(element);
|
auto emoteElement = dynamic_cast<const EmoteElement *>(element);
|
||||||
|
auto layeredEmoteElement =
|
||||||
|
dynamic_cast<const LayeredEmoteElement *>(element);
|
||||||
|
bool isNotEmote = emoteElement == nullptr && layeredEmoteElement == nullptr;
|
||||||
|
|
||||||
if (element->getTooltip().isEmpty() ||
|
if (element->getTooltip().isEmpty() ||
|
||||||
(isLinkValid && emoteElement == nullptr &&
|
(isLinkValid && isNotEmote && !getSettings()->linkInfoTooltip))
|
||||||
!getSettings()->linkInfoTooltip))
|
|
||||||
{
|
{
|
||||||
tooltipWidget->hide();
|
tooltipWidget->hide();
|
||||||
}
|
}
|
||||||
|
@ -1669,7 +1672,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
auto badgeElement = dynamic_cast<const BadgeElement *>(element);
|
auto badgeElement = dynamic_cast<const BadgeElement *>(element);
|
||||||
|
|
||||||
if ((badgeElement || emoteElement) &&
|
if ((badgeElement || emoteElement || layeredEmoteElement) &&
|
||||||
getSettings()->emotesTooltipPreview.getValue())
|
getSettings()->emotesTooltipPreview.getValue())
|
||||||
{
|
{
|
||||||
if (event->modifiers() == Qt::ShiftModifier ||
|
if (event->modifiers() == Qt::ShiftModifier ||
|
||||||
|
@ -1677,18 +1680,73 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
if (emoteElement)
|
if (emoteElement)
|
||||||
{
|
{
|
||||||
tooltipWidget->setImage(
|
tooltipWidget->setOne({
|
||||||
emoteElement->getEmote()->images.getImage(3.0));
|
emoteElement->getEmote()->images.getImage(3.0),
|
||||||
|
element->getTooltip(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (layeredEmoteElement)
|
||||||
|
{
|
||||||
|
auto &layeredEmotes = layeredEmoteElement->getEmotes();
|
||||||
|
// Should never be empty but ensure it
|
||||||
|
if (!layeredEmotes.empty())
|
||||||
|
{
|
||||||
|
std::vector<TooltipEntry> entries;
|
||||||
|
entries.reserve(layeredEmotes.size());
|
||||||
|
|
||||||
|
auto &emoteTooltips =
|
||||||
|
layeredEmoteElement->getEmoteTooltips();
|
||||||
|
|
||||||
|
// Someone performing some tomfoolery could put an emote with tens,
|
||||||
|
// if not hundreds of zero-width emotes on a single emote. If the
|
||||||
|
// tooltip may take up more than three rows, truncate everything else.
|
||||||
|
bool truncating = false;
|
||||||
|
size_t upperLimit = layeredEmotes.size();
|
||||||
|
if (layeredEmotes.size() > TOOLTIP_EMOTE_ENTRIES_LIMIT)
|
||||||
|
{
|
||||||
|
upperLimit = TOOLTIP_EMOTE_ENTRIES_LIMIT - 1;
|
||||||
|
truncating = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < upperLimit; ++i)
|
||||||
|
{
|
||||||
|
auto &emote = layeredEmotes[i];
|
||||||
|
if (i == 0)
|
||||||
|
{
|
||||||
|
// First entry gets a large image and full description
|
||||||
|
entries.push_back({emote->images.getImage(3.0),
|
||||||
|
emoteTooltips[i]});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Every other entry gets a small image and just the emote name
|
||||||
|
entries.push_back({emote->images.getImage(1.0),
|
||||||
|
emote->name.string});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (truncating)
|
||||||
|
{
|
||||||
|
entries.push_back({nullptr, "..."});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto style = layeredEmotes.size() > 2
|
||||||
|
? TooltipStyle::Grid
|
||||||
|
: TooltipStyle::Vertical;
|
||||||
|
tooltipWidget->set(entries, style);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (badgeElement)
|
else if (badgeElement)
|
||||||
{
|
{
|
||||||
tooltipWidget->setImage(
|
tooltipWidget->setOne({
|
||||||
badgeElement->getEmote()->images.getImage(3.0));
|
badgeElement->getEmote()->images.getImage(3.0),
|
||||||
|
element->getTooltip(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
tooltipWidget->clearImage();
|
tooltipWidget->clearEntries();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -1711,7 +1769,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
|
||||||
auto thumbnailSize = getSettings()->thumbnailSize;
|
auto thumbnailSize = getSettings()->thumbnailSize;
|
||||||
if (!thumbnailSize)
|
if (!thumbnailSize)
|
||||||
{
|
{
|
||||||
tooltipWidget->clearImage();
|
tooltipWidget->clearEntries();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -1724,19 +1782,23 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
|
||||||
shouldHideThumbnail
|
shouldHideThumbnail
|
||||||
? Image::fromResourcePixmap(getResources().streamerMode)
|
? Image::fromResourcePixmap(getResources().streamerMode)
|
||||||
: element->getThumbnail();
|
: element->getThumbnail();
|
||||||
tooltipWidget->setImage(std::move(thumb));
|
|
||||||
|
|
||||||
if (element->getThumbnailType() ==
|
if (element->getThumbnailType() ==
|
||||||
MessageElement::ThumbnailType::Link_Thumbnail)
|
MessageElement::ThumbnailType::Link_Thumbnail)
|
||||||
{
|
{
|
||||||
tooltipWidget->setImageScale(thumbnailSize, thumbnailSize);
|
tooltipWidget->setOne({std::move(thumb),
|
||||||
|
element->getTooltip(), thumbnailSize,
|
||||||
|
thumbnailSize});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tooltipWidget->setOne({std::move(thumb), ""});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltipWidget->moveTo(this, event->globalPos());
|
tooltipWidget->moveTo(this, event->globalPos());
|
||||||
tooltipWidget->setWordWrap(isLinkValid);
|
tooltipWidget->setWordWrap(isLinkValid);
|
||||||
tooltipWidget->setText(element->getTooltip());
|
|
||||||
tooltipWidget->show();
|
tooltipWidget->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2134,6 +2196,18 @@ void ChannelView::addImageContextMenuItems(
|
||||||
addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags,
|
addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags,
|
||||||
menu);
|
menu);
|
||||||
}
|
}
|
||||||
|
else if (auto layeredElement =
|
||||||
|
dynamic_cast<const LayeredEmoteElement *>(&creator))
|
||||||
|
{
|
||||||
|
// Give each emote its own submenu
|
||||||
|
for (auto &emote : layeredElement->getUniqueEmotes())
|
||||||
|
{
|
||||||
|
auto emoteAction = menu.addAction(emote->name.string);
|
||||||
|
auto emoteMenu = new QMenu(&menu);
|
||||||
|
emoteAction->setMenu(emoteMenu);
|
||||||
|
addEmoteContextMenuItems(*emote, creatorFlags, *emoteMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add seperator
|
// add seperator
|
||||||
|
|
|
@ -374,6 +374,10 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
||||||
layout.addCheckbox("Animate", s.animateEmotes);
|
layout.addCheckbox("Animate", s.animateEmotes);
|
||||||
layout.addCheckbox("Animate only when Chatterino is focused",
|
layout.addCheckbox("Animate only when Chatterino is focused",
|
||||||
s.animationsWhenFocused);
|
s.animationsWhenFocused);
|
||||||
|
layout.addCheckbox(
|
||||||
|
"Enable zero-width emotes", s.enableZeroWidthEmotes,
|
||||||
|
"When disabled, emotes that overlap other emotes, such as BTTV's "
|
||||||
|
"cvMask and 7TV's RainTime, will appear as normal emotes.");
|
||||||
layout.addCheckbox("Enable emote auto-completion by typing :",
|
layout.addCheckbox("Enable emote auto-completion by typing :",
|
||||||
s.emoteCompletionWithColon);
|
s.emoteCompletionWithColon);
|
||||||
layout.addDropdown<float>(
|
layout.addDropdown<float>(
|
||||||
|
|
|
@ -960,8 +960,7 @@ void SplitHeader::enterEvent(QEvent *event)
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *tooltip = TooltipWidget::instance();
|
auto *tooltip = TooltipWidget::instance();
|
||||||
tooltip->clearImage();
|
tooltip->setOne({nullptr, this->tooltipText_});
|
||||||
tooltip->setText(this->tooltipText_);
|
|
||||||
tooltip->setWordWrap(true);
|
tooltip->setWordWrap(true);
|
||||||
tooltip->adjustSize();
|
tooltip->adjustSize();
|
||||||
auto pos = this->mapToGlobal(this->rect().bottomLeft()) +
|
auto pos = this->mapToGlobal(this->rect().bottomLeft()) +
|
||||||
|
|
Loading…
Reference in a new issue