mirror-chatterino2/src/messages/layouts/MessageLayoutContainer.cpp

636 lines
18 KiB
C++

#include "MessageLayoutContainer.hpp"
#include "Application.hpp"
#include "messages/Message.hpp"
#include "messages/MessageElement.hpp"
#include "messages/Selection.hpp"
#include "messages/layouts/MessageLayoutElement.hpp"
#include "singletons/Fonts.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include <QDebug>
#include <QPainter>
#define COMPACT_EMOTES_OFFSET 6
#define MAX_UNCOLLAPSED_LINES \
(getSettings()->collpseMessagesMinLines.getValue())
namespace chatterino {
int MessageLayoutContainer::getHeight() const
{
return this->height_;
}
int MessageLayoutContainer::getWidth() const
{
return this->width_;
}
float MessageLayoutContainer::getScale() const
{
return this->scale_;
}
// methods
void MessageLayoutContainer::begin(int width, float scale, MessageFlags flags)
{
this->clear();
this->width_ = width;
this->scale_ = scale;
this->flags_ = flags;
auto mediumFontMetrics =
getApp()->fonts->getFontMetrics(FontStyle::ChatMedium, scale);
this->textLineHeight_ = mediumFontMetrics.height();
this->spaceWidth_ = mediumFontMetrics.width(' ');
this->dotdotdotWidth_ = mediumFontMetrics.width("...");
this->canAddMessages_ = true;
this->isCollapsed_ = false;
}
void MessageLayoutContainer::clear()
{
this->elements_.clear();
this->lines_.clear();
this->height_ = 0;
this->line_ = 0;
this->currentX_ = 0;
this->currentY_ = 0;
this->lineStart_ = 0;
this->lineHeight_ = 0;
this->charIndex_ = 0;
}
void MessageLayoutContainer::addElement(MessageLayoutElement *element)
{
if (!this->fitsInLine(element->getRect().width()))
{
this->breakLine();
}
this->_addElement(element);
}
void MessageLayoutContainer::addElementNoLineBreak(
MessageLayoutElement *element)
{
this->_addElement(element);
}
bool MessageLayoutContainer::canAddElements()
{
return this->canAddMessages_;
}
void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
bool forceAdd)
{
if (!this->canAddElements() && !forceAdd)
{
delete element;
return;
}
// top margin
if (this->elements_.size() == 0)
{
this->currentY_ = int(this->margin.top * this->scale_);
}
int newLineHeight = element->getRect().height();
// compact emote offset
bool isCompactEmote =
getSettings()->compactEmotes &&
!this->flags_.has(MessageFlag::DisableCompactEmotes) &&
element->getCreator().getFlags().has(MessageElementFlag::EmoteImages);
if (isCompactEmote)
{
newLineHeight -= COMPACT_EMOTES_OFFSET * this->scale_;
}
// update line height
this->lineHeight_ = std::max(this->lineHeight_, newLineHeight);
// set move element
element->setPosition(
QPoint(this->currentX_, this->currentY_ - element->getRect().height()));
// add element
this->elements_.push_back(std::unique_ptr<MessageLayoutElement>(element));
// set current x
this->currentX_ += element->getRect().width();
if (element->hasTrailingSpace())
{
this->currentX_ += this->spaceWidth_;
}
}
void MessageLayoutContainer::breakLine()
{
int xOffset = 0;
if (this->flags_.has(MessageFlag::Centered) && this->elements_.size() > 0)
{
xOffset = (width_ - this->elements_.at(0)->getRect().left() -
this->elements_.at(this->elements_.size() - 1)
->getRect()
.right()) /
2;
}
for (size_t i = lineStart_; i < this->elements_.size(); i++)
{
MessageLayoutElement *element = this->elements_.at(i).get();
bool isCompactEmote =
getSettings()->compactEmotes &&
!this->flags_.has(MessageFlag::DisableCompactEmotes) &&
element->getCreator().getFlags().has(
MessageElementFlag::EmoteImages);
int yExtra = 0;
if (isCompactEmote)
{
yExtra = (COMPACT_EMOTES_OFFSET / 2) * this->scale_;
}
// if (element->getCreator().getFlags() &
// MessageElementFlag::Badges)
// {
if (element->getRect().height() < this->textLineHeight_)
{
// yExtra -= (this->textLineHeight_ - element->getRect().height()) /
// 2;
}
element->setPosition(
QPoint(element->getRect().x() + xOffset +
int(this->margin.left * this->scale_),
element->getRect().y() + this->lineHeight_ + yExtra));
}
if (this->lines_.size() != 0)
{
this->lines_.back().endIndex = this->lineStart_;
this->lines_.back().endCharIndex = this->charIndex_;
}
this->lines_.push_back(
{(int)lineStart_, 0, this->charIndex_, 0,
QRect(-100000, this->currentY_, 200000, lineHeight_)});
for (int i = this->lineStart_; i < this->elements_.size(); i++)
{
this->charIndex_ += this->elements_[i]->getSelectionIndexCount();
}
this->lineStart_ = this->elements_.size();
// this->currentX = (int)(this->scale * 8);
if (this->canCollapse() && line_ + 1 >= MAX_UNCOLLAPSED_LINES)
{
this->canAddMessages_ = false;
return;
}
this->currentX_ = 0;
this->currentY_ += this->lineHeight_;
this->height_ = this->currentY_ + int(this->margin.bottom * this->scale_);
this->lineHeight_ = 0;
this->line_++;
}
bool MessageLayoutContainer::atStartOfLine()
{
return this->lineStart_ == this->elements_.size();
}
bool MessageLayoutContainer::fitsInLine(int _width)
{
return this->currentX_ + _width <=
(this->width_ - int(this->margin.left * this->scale_) -
int(this->margin.right * this->scale_) -
(this->line_ + 1 == MAX_UNCOLLAPSED_LINES ? this->dotdotdotWidth_
: 0));
}
void MessageLayoutContainer::end()
{
if (!this->canAddElements())
{
static TextElement dotdotdot("...", MessageElementFlag::Collapsed,
MessageColor::Link);
static QString dotdotdotText("...");
auto *element = new TextLayoutElement(
dotdotdot, dotdotdotText,
QSize(this->dotdotdotWidth_, this->textLineHeight_),
QColor("#00D80A"), FontStyle::ChatMediumBold, this->scale_);
// getApp()->themes->messages.textColors.system
this->_addElement(element, true);
this->isCollapsed_ = true;
}
if (!this->atStartOfLine())
{
this->breakLine();
}
this->height_ += this->lineHeight_;
if (this->lines_.size() != 0)
{
this->lines_[0].rect.setTop(-100000);
this->lines_.back().rect.setBottom(100000);
this->lines_.back().endIndex = this->elements_.size();
this->lines_.back().endCharIndex = this->charIndex_;
}
}
bool MessageLayoutContainer::canCollapse()
{
return getSettings()->collpseMessagesMinLines.getValue() > 0 &&
this->flags_.has(MessageFlag::Collapsed);
}
bool MessageLayoutContainer::isCollapsed()
{
return this->isCollapsed_;
}
MessageLayoutElement *MessageLayoutContainer::getElementAt(QPoint point)
{
for (std::unique_ptr<MessageLayoutElement> &element : this->elements_)
{
if (element->getRect().contains(point))
{
return element.get();
}
}
return nullptr;
}
// painting
void MessageLayoutContainer::paintElements(QPainter &painter)
{
for (const std::unique_ptr<MessageLayoutElement> &element : this->elements_)
{
#ifdef FOURTF
painter.setPen(QColor(0, 255, 0));
painter.drawRect(element->getRect());
#endif
element->paint(painter);
}
}
void MessageLayoutContainer::paintAnimatedElements(QPainter &painter,
int yOffset)
{
for (const std::unique_ptr<MessageLayoutElement> &element : this->elements_)
{
element->paintAnimated(painter, yOffset);
}
}
void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex,
Selection &selection, int yOffset)
{
auto app = getApp();
QColor selectionColor = app->themes->messages.selection;
// don't draw anything
if (selection.selectionMin.messageIndex > messageIndex ||
selection.selectionMax.messageIndex < messageIndex)
{
return;
}
// fully selected
if (selection.selectionMin.messageIndex < messageIndex &&
selection.selectionMax.messageIndex > messageIndex)
{
for (Line &line : this->lines_)
{
QRect rect = line.rect;
rect.setTop(std::max(0, rect.top()) + yOffset);
rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset);
rect.setLeft(this->elements_[line.startIndex]->getRect().left());
rect.setRight(
this->elements_[line.endIndex - 1]->getRect().right());
painter.fillRect(rect, selectionColor);
}
return;
}
int lineIndex = 0;
int index = 0;
// start in this message
if (selection.selectionMin.messageIndex == messageIndex)
{
for (; lineIndex < this->lines_.size(); lineIndex++)
{
Line &line = this->lines_[lineIndex];
index = line.startCharIndex;
bool returnAfter = false;
bool breakAfter = false;
int x = this->elements_[line.startIndex]->getRect().left();
int r = this->elements_[line.endIndex - 1]->getRect().right();
if (line.endCharIndex <= selection.selectionMin.charIndex)
{
continue;
}
for (int i = line.startIndex; i < line.endIndex; i++)
{
int c = this->elements_[i]->getSelectionIndexCount();
if (index + c > selection.selectionMin.charIndex)
{
x = this->elements_[i]->getXFromIndex(
selection.selectionMin.charIndex - index);
// ends in same line
if (selection.selectionMax.messageIndex == messageIndex &&
line.endCharIndex >
/*=*/selection.selectionMax.charIndex) //
{
returnAfter = true;
index = line.startCharIndex;
for (int i = line.startIndex; i < line.endIndex; i++)
{
int c =
this->elements_[i]->getSelectionIndexCount();
if (index + c > selection.selectionMax.charIndex)
{
r = this->elements_[i]->getXFromIndex(
selection.selectionMax.charIndex - index);
break;
}
index += c;
}
}
// ends in same line end
if (selection.selectionMax.messageIndex != messageIndex)
{
int lineIndex2 = lineIndex + 1;
for (; lineIndex2 < this->lines_.size(); lineIndex2++)
{
Line &line2 = this->lines_[lineIndex2];
QRect rect = line2.rect;
rect.setTop(std::max(0, rect.top()) + yOffset);
rect.setBottom(
std::min(this->height_, rect.bottom()) +
yOffset);
rect.setLeft(this->elements_[line2.startIndex]
->getRect()
.left());
rect.setRight(this->elements_[line2.endIndex - 1]
->getRect()
.right());
painter.fillRect(rect, selectionColor);
}
returnAfter = true;
}
else
{
lineIndex++;
breakAfter = true;
}
break;
}
index += c;
}
QRect rect = line.rect;
rect.setTop(std::max(0, rect.top()) + yOffset);
rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset);
rect.setLeft(x);
rect.setRight(r);
painter.fillRect(rect, selectionColor);
if (returnAfter)
{
return;
}
if (breakAfter)
{
break;
}
}
}
// start in this message
for (; lineIndex < this->lines_.size(); lineIndex++)
{
Line &line = this->lines_[lineIndex];
index = line.startCharIndex;
// just draw the garbage
if (line.endCharIndex < /*=*/selection.selectionMax.charIndex)
{
QRect rect = line.rect;
rect.setTop(std::max(0, rect.top()) + yOffset);
rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset);
rect.setLeft(this->elements_[line.startIndex]->getRect().left());
rect.setRight(
this->elements_[line.endIndex - 1]->getRect().right());
painter.fillRect(rect, selectionColor);
continue;
}
int r = this->elements_[line.endIndex - 1]->getRect().right();
for (int i = line.startIndex; i < line.endIndex; i++)
{
int c = this->elements_[i]->getSelectionIndexCount();
if (index + c > selection.selectionMax.charIndex)
{
r = this->elements_[i]->getXFromIndex(
selection.selectionMax.charIndex - index);
break;
}
index += c;
}
QRect rect = line.rect;
rect.setTop(std::max(0, rect.top()) + yOffset);
rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset);
rect.setLeft(this->elements_[line.startIndex]->getRect().left());
rect.setRight(r);
painter.fillRect(rect, selectionColor);
break;
}
}
// selection
int MessageLayoutContainer::getSelectionIndex(QPoint point)
{
if (this->elements_.size() == 0)
{
return 0;
}
auto line = this->lines_.begin();
for (; line != this->lines_.end(); line++)
{
if (line->rect.contains(point))
{
break;
}
}
int lineStart = line == this->lines_.end() ? this->lines_.back().startIndex
: line->startIndex;
if (line != this->lines_.end())
{
line++;
}
int lineEnd =
line == this->lines_.end() ? this->elements_.size() : line->startIndex;
int index = 0;
for (int i = 0; i < lineEnd; i++)
{
auto &&element = this->elements_[i];
// end of line
if (i == lineEnd)
{
break;
}
// before line
if (i < lineStart)
{
index += element->getSelectionIndexCount();
continue;
}
// this is the word
auto rightMargin = element->hasTrailingSpace() ? this->spaceWidth_ : 0;
if (point.x() <= element->getRect().right() + rightMargin)
{
index += element->getMouseOverIndex(point);
break;
}
index += element->getSelectionIndexCount();
}
return index;
}
// fourtf: no idea if this is acurate LOL
int MessageLayoutContainer::getLastCharacterIndex() const
{
if (this->lines_.size() == 0)
{
return 0;
}
return this->lines_.back().endCharIndex;
}
int MessageLayoutContainer::getFirstMessageCharacterIndex() const
{
static FlagsEnum<MessageElementFlag> flags;
flags.set(MessageElementFlag::Username);
flags.set(MessageElementFlag::Timestamp);
flags.set(MessageElementFlag::Badges);
// Get the index of the first character of the real message
// (no badges/timestamps/username)
int index = 0;
for (auto &element : this->elements_)
{
if (element->getFlags().hasAny(flags))
{
index += element->getSelectionIndexCount();
}
else
{
break;
}
}
return index;
}
void MessageLayoutContainer::addSelectionText(QString &str, int from, int to,
CopyMode copymode)
{
int index = 0;
bool first = true;
for (auto &element : this->elements_)
{
if (copymode == CopyMode::OnlyTextAndEmotes)
{
if (element->getCreator().getFlags().hasAny(
{MessageElementFlag::Timestamp,
MessageElementFlag::Username, MessageElementFlag::Badges}))
continue;
}
auto indexCount = element->getSelectionIndexCount();
if (first)
{
if (index + indexCount > from)
{
element->addCopyTextToString(str, from - index, to - index);
first = false;
if (index + indexCount > to)
{
break;
}
}
}
else
{
if (index + indexCount > to)
{
element->addCopyTextToString(str, 0, to - index);
break;
}
else
{
element->addCopyTextToString(str);
}
}
index += indexCount;
}
}
} // namespace chatterino