From 81b1a8774bcf76e5365b332a76c7eee9be193068 Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 12 Sep 2017 19:06:16 +0200 Subject: [PATCH] added text selection --- src/colorscheme.cpp | 10 + src/colorscheme.hpp | 1 + src/messages/lazyloadedimage.cpp | 122 +++++++++--- src/messages/lazyloadedimage.hpp | 96 +++------- src/messages/message.cpp | 12 +- src/messages/message.hpp | 4 +- src/messages/messagebuilder.cpp | 15 +- src/messages/messagebuilder.hpp | 3 +- src/messages/messageref.cpp | 41 ++--- src/messages/messageref.hpp | 2 +- src/messages/word.cpp | 85 +++++---- src/messages/word.hpp | 29 +-- src/messages/wordpart.cpp | 15 +- src/messages/wordpart.hpp | 3 +- src/widgets/chatwidget.cpp | 17 +- src/widgets/chatwidget.hpp | 3 + src/widgets/chatwidgetview.cpp | 306 +++++++++++++++++++++++++++---- src/widgets/chatwidgetview.hpp | 60 +++++- 18 files changed, 585 insertions(+), 239 deletions(-) diff --git a/src/colorscheme.cpp b/src/colorscheme.cpp index e83975e01..98b5d7813 100644 --- a/src/colorscheme.cpp +++ b/src/colorscheme.cpp @@ -100,6 +100,7 @@ void ColorScheme::setColors(double hue, double multiplier) // Chat ChatBackground = getColor(0, 0.1, 1); + ChatBackgroundHighlighted = blendColors(TabSelectedBackground, ChatBackground, 0.8); ChatHeaderBackground = getColor(0, 0.1, 0.9); ChatHeaderBorder = getColor(0, 0.1, 0.85); ChatInputBackground = getColor(0, 0.1, 0.95); @@ -120,6 +121,15 @@ void ColorScheme::setColors(double hue, double multiplier) this->updated(); } +QColor ColorScheme::blendColors(const QColor &color1, const QColor &color2, qreal ratio) +{ + int r = color1.red() * (1 - ratio) + color2.red() * ratio; + int g = color1.green() * (1 - ratio) + color2.green() * ratio; + int b = color1.blue() * (1 - ratio) + color2.blue() * ratio; + + return QColor(r, g, b, 255); +} + void ColorScheme::normalizeColor(QColor &color) { if (this->lightTheme) { diff --git a/src/colorscheme.hpp b/src/colorscheme.hpp index 99280e932..5d84389b9 100644 --- a/src/colorscheme.hpp +++ b/src/colorscheme.hpp @@ -87,6 +87,7 @@ private: pajlada::Settings::Setting themeHue; void setColors(double hue, double multiplier); + QColor blendColors(const QColor &color1, const QColor &color2, qreal ratio); double middleLookupTable[360] = {}; double minLookupTable[360] = {}; diff --git a/src/messages/lazyloadedimage.cpp b/src/messages/lazyloadedimage.cpp index 734940a1a..b023edde1 100644 --- a/src/messages/lazyloadedimage.cpp +++ b/src/messages/lazyloadedimage.cpp @@ -22,14 +22,14 @@ LazyLoadedImage::LazyLoadedImage(EmoteManager &_emoteManager, WindowManager &_wi const QString &tooltip, const QMargins &margin, bool isHat) : emoteManager(_emoteManager) , windowManager(_windowManager) - , _currentPixmap(nullptr) - , _url(url) - , _name(name) - , _tooltip(tooltip) - , _margin(margin) - , _ishat(isHat) - , _scale(scale) - , _isLoading(false) + , currentPixmap(nullptr) + , url(url) + , name(name) + , tooltip(tooltip) + , margin(margin) + , ishat(isHat) + , scale(scale) + , isLoading(false) { } @@ -38,19 +38,19 @@ LazyLoadedImage::LazyLoadedImage(EmoteManager &_emoteManager, WindowManager &_wi const QString &tooltip, const QMargins &margin, bool isHat) : emoteManager(_emoteManager) , windowManager(_windowManager) - , _currentPixmap(image) - , _name(name) - , _tooltip(tooltip) - , _margin(margin) - , _ishat(isHat) - , _scale(scale) - , _isLoading(true) + , currentPixmap(image) + , name(name) + , tooltip(tooltip) + , margin(margin) + , ishat(isHat) + , scale(scale) + , isLoading(true) { } void LazyLoadedImage::loadImage() { - util::urlFetch(_url, [=](QNetworkReply &reply) { + util::urlFetch(this->url, [=](QNetworkReply &reply) { QByteArray array = reply.readAll(); QBuffer buffer(&array); buffer.open(QIODevice::ReadOnly); @@ -66,19 +66,19 @@ void LazyLoadedImage::loadImage() if (first) { first = false; - _currentPixmap = pixmap; + this->currentPixmap = pixmap; } FrameData data; data.duration = std::max(20, reader.nextImageDelay()); data.image = pixmap; - _allFrames.push_back(data); + this->allFrames.push_back(data); } } - if (_allFrames.size() > 1) { - _animated = true; + if (this->allFrames.size() > 1) { + this->animated = true; this->emoteManager.getGifUpdateSignal().connect([this] { gifUpdateTimout(); // @@ -92,18 +92,90 @@ void LazyLoadedImage::loadImage() void LazyLoadedImage::gifUpdateTimout() { - _currentFrameOffset += GIF_FRAME_LENGTH; + this->currentFrameOffset += GIF_FRAME_LENGTH; while (true) { - if (_currentFrameOffset > _allFrames.at(_currentFrame).duration) { - _currentFrameOffset -= _allFrames.at(_currentFrame).duration; - _currentFrame = (_currentFrame + 1) % _allFrames.size(); + if (this->currentFrameOffset > this->allFrames.at(this->currentFrame).duration) { + this->currentFrameOffset -= this->allFrames.at(this->currentFrame).duration; + this->currentFrame = (this->currentFrame + 1) % this->allFrames.size(); } else { break; } } - _currentPixmap = _allFrames[_currentFrame].image; + this->currentPixmap = this->allFrames[this->currentFrame].image; } + +const QPixmap *LazyLoadedImage::getPixmap() +{ + if (!this->isLoading) { + this->isLoading = true; + + loadImage(); + } + return this->currentPixmap; +} + +qreal LazyLoadedImage::getScale() const +{ + return this->scale; +} + +const QString &LazyLoadedImage::getUrl() const +{ + return this->url; +} + +const QString &LazyLoadedImage::getName() const +{ + return this->name; +} + +const QString &LazyLoadedImage::getTooltip() const +{ + return this->tooltip; +} + +const QMargins &LazyLoadedImage::getMargin() const +{ + return this->margin; +} + +bool LazyLoadedImage::getAnimated() const +{ + return this->animated; +} + +bool LazyLoadedImage::isHat() const +{ + return this->ishat; +} + +int LazyLoadedImage::getWidth() const +{ + if (this->currentPixmap == nullptr) { + return 16; + } + return this->currentPixmap->width(); +} + +int LazyLoadedImage::getScaledWidth() const +{ + return static_cast(getWidth() * this->scale); +} + +int LazyLoadedImage::getHeight() const +{ + if (this->currentPixmap == nullptr) { + return 16; + } + return this->currentPixmap->height(); +} + +int LazyLoadedImage::getScaledHeight() const +{ + return static_cast(getHeight() * this->scale); +} + } // namespace messages } // namespace chatterino diff --git a/src/messages/lazyloadedimage.hpp b/src/messages/lazyloadedimage.hpp index 6c922c8d4..dc46484aa 100644 --- a/src/messages/lazyloadedimage.hpp +++ b/src/messages/lazyloadedimage.hpp @@ -25,66 +25,18 @@ public: const QString &_tooltip = "", const QMargins &_margin = QMargins(), bool isHat = false); - const QPixmap *getPixmap() - { - if (!_isLoading) { - _isLoading = true; - - loadImage(); - } - return _currentPixmap; - } - - qreal getScale() const - { - return _scale; - } - - const QString &getUrl() const - { - return _url; - } - - const QString &getName() const - { - return _name; - } - - const QString &getTooltip() const - { - return _tooltip; - } - - const QMargins &getMargin() const - { - return _margin; - } - - bool getAnimated() const - { - return _animated; - } - - bool isHat() const - { - return _ishat; - } - - int getWidth() const - { - if (_currentPixmap == nullptr) { - return 16; - } - return _currentPixmap->width(); - } - - int getHeight() const - { - if (_currentPixmap == nullptr) { - return 16; - } - return _currentPixmap->height(); - } + const QPixmap *getPixmap(); + qreal getScale() const; + const QString &getUrl() const; + const QString &getName() const; + const QString &getTooltip() const; + const QMargins &getMargin() const; + bool getAnimated() const; + bool isHat() const; + int getWidth() const; + int getScaledWidth() const; + int getHeight() const; + int getScaledHeight() const; private: EmoteManager &emoteManager; @@ -95,20 +47,20 @@ private: int duration; }; - QPixmap *_currentPixmap; - std::vector _allFrames; - int _currentFrame = 0; - int _currentFrameOffset = 0; + QPixmap *currentPixmap; + std::vector allFrames; + int currentFrame = 0; + int currentFrameOffset = 0; - QString _url; - QString _name; - QString _tooltip; - bool _animated = false; - QMargins _margin; - bool _ishat; - qreal _scale; + QString url; + QString name; + QString tooltip; + bool animated = false; + QMargins margin; + bool ishat; + qreal scale; - bool _isLoading; + bool isLoading; void loadImage(); diff --git a/src/messages/message.cpp b/src/messages/message.cpp index 3d680bad5..99bc9aa4c 100644 --- a/src/messages/message.cpp +++ b/src/messages/message.cpp @@ -29,12 +29,12 @@ Message::Message(const QString &text) } */ -Message::Message(const QString &text, const std::vector &words, const bool &highlight) - : text(text) - , highlightTab(highlight) - , words(words) -{ -} +//Message::Message(const QString &text, const std::vector &words, const bool &highlight) +// : text(text) +// , highlightTab(highlight) +// , words(words) +//{ +//} bool Message::getCanHighlightTab() const { diff --git a/src/messages/message.hpp b/src/messages/message.hpp index 19592ffa7..8eb56d1fd 100644 --- a/src/messages/message.hpp +++ b/src/messages/message.hpp @@ -24,8 +24,8 @@ class Message { public: // explicit Message(const QString &text); - explicit Message(const QString &text, const std::vector &words, - const bool &highlight); + //explicit Message(const QString &text, const std::vector &words, + // const bool &highlight); bool getCanHighlightTab() const; const QString &getTimeoutUser() const; diff --git a/src/messages/messagebuilder.cpp b/src/messages/messagebuilder.cpp index 6fb1a4e65..2a44ae94e 100644 --- a/src/messages/messagebuilder.cpp +++ b/src/messages/messagebuilder.cpp @@ -7,21 +7,19 @@ namespace chatterino { namespace messages { MessageBuilder::MessageBuilder() - : _words() + : message(new Message) { _parseTime = std::chrono::system_clock::now(); - - linkRegex.setPattern("[[:ascii:]]*\\.[a-zA-Z]+\\/?[[:ascii:]]*"); } SharedMessage MessageBuilder::build() { - return SharedMessage(new Message(this->originalMessage, _words, highlight)); + return this->message; } -void MessageBuilder::appendWord(const Word &word) +void MessageBuilder::appendWord(const Word &&word) { - _words.push_back(word); + this->message->getWords().push_back(word); } void MessageBuilder::appendTimestamp() @@ -59,6 +57,9 @@ void MessageBuilder::appendTimestamp(time_t time) QString MessageBuilder::matchLink(const QString &string) { + static QRegularExpression linkRegex("[[:ascii:]]*\\.[a-zA-Z]+\\/?[[:ascii:]]*"); + static QRegularExpression httpRegex("\\bhttps?://"); + auto match = linkRegex.match(string); if (!match.hasMatch()) { @@ -67,7 +68,7 @@ QString MessageBuilder::matchLink(const QString &string) QString captured = match.captured(); - if (!captured.contains(QRegularExpression("\\bhttps?://"))) { + if (!captured.contains(httpRegex)) { captured.insert(0, "http://"); } return captured; diff --git a/src/messages/messagebuilder.hpp b/src/messages/messagebuilder.hpp index 70c93cf2d..f5f236672 100644 --- a/src/messages/messagebuilder.hpp +++ b/src/messages/messagebuilder.hpp @@ -16,7 +16,7 @@ public: SharedMessage build(); - void appendWord(const Word &word); + void appendWord(const Word &&word); void appendTimestamp(); void appendTimestamp(std::time_t time); void setHighlight(const bool &value); @@ -27,6 +27,7 @@ public: QString originalMessage; private: + std::shared_ptr message; std::vector _words; bool highlight = false; std::chrono::time_point _parseTime; diff --git a/src/messages/messageref.cpp b/src/messages/messageref.cpp index 28e5efad8..6334e3b9c 100644 --- a/src/messages/messageref.cpp +++ b/src/messages/messageref.cpp @@ -6,8 +6,8 @@ #define MARGIN_LEFT 8 #define MARGIN_RIGHT 8 -#define MARGIN_TOP 8 -#define MARGIN_BOTTOM 8 +#define MARGIN_TOP 4 +#define MARGIN_BOTTOM 4 using namespace chatterino::messages; @@ -142,18 +142,12 @@ bool MessageRef::layout(int width, bool enableEmoteMargins) std::vector &charWidths = word.getCharacterWidthCache(); - if (charWidths.size() == 0) { - for (int i = 0; i < text.length(); i++) { - charWidths.push_back(metrics.charWidth(text, i)); - } - } - for (int i = 2; i <= text.length(); i++) { if ((width = width + charWidths[i - 1]) + MARGIN_LEFT > right) { QString mid = text.mid(start, i - start - 1); _wordParts.push_back(WordPart(word, MARGIN_LEFT, y, width, word.getHeight(), - lineNumber, mid, mid)); + lineNumber, mid, mid, false)); y += metrics.height(); @@ -193,6 +187,8 @@ bool MessageRef::layout(int width, bool enableEmoteMargins) y += lineHeight; + lineNumber++; + _wordParts.push_back( WordPart(word, MARGIN_LEFT, y - word.getHeight(), lineNumber, word.getCopyText())); @@ -202,8 +198,6 @@ bool MessageRef::layout(int width, bool enableEmoteMargins) x = word.getWidth() + MARGIN_LEFT; x += spaceWidth; - - lineNumber++; } } @@ -214,6 +208,8 @@ bool MessageRef::layout(int width, bool enableEmoteMargins) _height = y + lineHeight; } + _height += MARGIN_BOTTOM; + if (sizeChanged) { buffer = nullptr; } @@ -237,17 +233,16 @@ void MessageRef::alignWordParts(int lineStart, int lineHeight) } } -bool MessageRef::tryGetWordPart(QPoint point, Word &word) +const Word *MessageRef::tryGetWordPart(QPoint point) { // go through all words and return the first one that contains the point. for (WordPart &wordPart : _wordParts) { if (wordPart.getRect().contains(point)) { - word = wordPart.getWord(); - return true; + return &wordPart.getWord(); } } - return false; + return nullptr; } int MessageRef::getSelectionIndex(QPoint position) @@ -259,7 +254,7 @@ int MessageRef::getSelectionIndex(QPoint position) // find out in which line the cursor is int lineNumber = 0, lineStart = 0, lineEnd = 0; - for (int i = 0; i < _wordParts.size(); i++) { + for (size_t i = 0; i < _wordParts.size(); i++) { WordPart &part = _wordParts[i]; if (part.getLineNumber() != 0 && position.y() < part.getY()) { @@ -267,11 +262,11 @@ int MessageRef::getSelectionIndex(QPoint position) } if (part.getLineNumber() != lineNumber) { - lineStart = i - 1; + lineStart = i; lineNumber = part.getLineNumber(); } - lineEnd = part.getLineNumber() == 0 ? i : i + 1; + lineEnd = i + 1; } // count up to the cursor @@ -293,15 +288,19 @@ int MessageRef::getSelectionIndex(QPoint position) // cursor is right of the word part if (position.x() > part.getX() + part.getWidth()) { - index += part.getWord().isImage() ? 2 : part.getText().length() + 1; + index += part.getCharacterLength(); continue; } // cursor is over the word part if (part.getWord().isImage()) { - index++; + if (position.x() - part.getX() > part.getWidth() / 2) { + index++; + } } else { - auto text = part.getWord().getText(); + // TODO: use word.getCharacterWidthCache(); + + auto text = part.getText(); int x = part.getX(); diff --git a/src/messages/messageref.hpp b/src/messages/messageref.hpp index eba5c13e3..c5cfb33a4 100644 --- a/src/messages/messageref.hpp +++ b/src/messages/messageref.hpp @@ -28,7 +28,7 @@ public: std::shared_ptr buffer = nullptr; bool updateBuffer = false; - bool tryGetWordPart(QPoint point, messages::Word &word); + const messages::Word *tryGetWordPart(QPoint point); int getSelectionIndex(QPoint position); diff --git a/src/messages/word.cpp b/src/messages/word.cpp index 7a9a44345..d24cde4d9 100644 --- a/src/messages/word.cpp +++ b/src/messages/word.cpp @@ -6,15 +6,12 @@ namespace messages { // Image word Word::Word(LazyLoadedImage *image, Type type, const QString ©text, const QString &tooltip, const Link &link) - : _image(image) - , _text() - , _color() + : image(image) , _isImage(true) - , _type(type) - , _copyText(copytext) - , _tooltip(tooltip) - , _link(link) - , _characterWidthCache() + , type(type) + , copyText(copytext) + , tooltip(tooltip) + , link(link) { image->getWidth(); // professional segfault test } @@ -22,113 +19,127 @@ Word::Word(LazyLoadedImage *image, Type type, const QString ©text, const QSt // Text word Word::Word(const QString &text, Type type, const QColor &color, const QString ©text, const QString &tooltip, const Link &link) - : _image(nullptr) - , _text(text) - , _color(color) + : image(nullptr) + , text(text) + , color(color) , _isImage(false) - , _type(type) - , _copyText(copytext) - , _tooltip(tooltip) - , _link(link) - , _characterWidthCache() + , type(type) + , copyText(copytext) + , tooltip(tooltip) + , link(link) { } LazyLoadedImage &Word::getImage() const { - return *_image; + return *this->image; } const QString &Word::getText() const { - return _text; + return this->text; } int Word::getWidth() const { - return _width; + return this->width; } int Word::getHeight() const { - return _height; + return this->height; } void Word::setSize(int width, int height) { - _width = width; - _height = height; + this->width = width; + this->height = height; } bool Word::isImage() const { - return _isImage; + return this->_isImage; } bool Word::isText() const { - return !_isImage; + return !this->_isImage; } const QString &Word::getCopyText() const { - return _copyText; + return this->copyText; } bool Word::hasTrailingSpace() const { - return _hasTrailingSpace; + return this->_hasTrailingSpace; } QFont &Word::getFont() const { - return FontManager::getInstance().getFont(_font); + return FontManager::getInstance().getFont(this->font); } QFontMetrics &Word::getFontMetrics() const { - return FontManager::getInstance().getFontMetrics(_font); + return FontManager::getInstance().getFontMetrics(this->font); } Word::Type Word::getType() const { - return _type; + return this->type; } const QString &Word::getTooltip() const { - return _tooltip; + return this->tooltip; } const QColor &Word::getColor() const { - return _color; + return this->color; } const Link &Word::getLink() const { - return _link; + return this->link; } int Word::getXOffset() const { - return _xOffset; + return this->xOffset; } int Word::getYOffset() const { - return _yOffset; + return this->yOffset; } void Word::setOffset(int xOffset, int yOffset) { - _xOffset = std::max(0, xOffset); - _yOffset = std::max(0, yOffset); + this->xOffset = std::max(0, xOffset); + this->yOffset = std::max(0, yOffset); +} + +int Word::getCharacterLength() const +{ + return this->isImage() ? 2 : this->getText().length() + 1; } std::vector &Word::getCharacterWidthCache() const { - return _characterWidthCache; + // lock not required because there is only one gui thread + // std::lock_guard lock(this->charWidthCacheMutex); + + if (this->charWidthCache.size() == 0 && this->isText()) { + for (int i = 0; i < this->getText().length(); i++) { + this->charWidthCache.push_back(this->getFontMetrics().charWidth(this->getText(), i)); + } + } + + // TODO: on font change + return this->charWidthCache; } } // namespace messages diff --git a/src/messages/word.hpp b/src/messages/word.hpp index 0cee04635..889c6bbb5 100644 --- a/src/messages/word.hpp +++ b/src/messages/word.hpp @@ -110,29 +110,30 @@ public: int getXOffset() const; int getYOffset() const; void setOffset(int _xOffset, int _yOffset); + int getCharacterLength() const; std::vector &getCharacterWidthCache() const; private: - LazyLoadedImage *_image; - QString _text; - QColor _color; + LazyLoadedImage *image; + QString text; + QColor color; bool _isImage; - Type _type; - QString _copyText; - QString _tooltip; + Type type; + QString copyText; + QString tooltip; - int _width = 16; - int _height = 16; - int _xOffset = 0; - int _yOffset = 0; + int width = 16; + int height = 16; + int xOffset = 0; + int yOffset = 0; - bool _hasTrailingSpace; - FontManager::Type _font = FontManager::Medium; - Link _link; + bool _hasTrailingSpace = true; + FontManager::Type font = FontManager::Medium; + Link link; - mutable std::vector _characterWidthCache; + mutable std::vector charWidthCache; }; } // namespace messages diff --git a/src/messages/wordpart.cpp b/src/messages/wordpart.cpp index f727b7d05..c19bb94be 100644 --- a/src/messages/wordpart.cpp +++ b/src/messages/wordpart.cpp @@ -14,7 +14,7 @@ WordPart::WordPart(Word &word, int x, int y, int lineNumber, const QString © , _width(word.getWidth()) , _height(word.getHeight()) , _lineNumber(lineNumber) - , _trailingSpace(word.hasTrailingSpace() & allowTrailingSpace) + , _trailingSpace(!word.getCopyText().isEmpty() && word.hasTrailingSpace() & allowTrailingSpace) { } @@ -28,7 +28,7 @@ WordPart::WordPart(Word &word, int x, int y, int width, int height, int lineNumb , _width(width) , _height(height) , _lineNumber(lineNumber) - , _trailingSpace(word.hasTrailingSpace() & allowTrailingSpace) + , _trailingSpace(!word.getCopyText().isEmpty() && word.hasTrailingSpace() & allowTrailingSpace) { } @@ -80,7 +80,7 @@ int WordPart::getBottom() const QRect WordPart::getRect() const { - return QRect(_x, _y, _width, _height); + return QRect(_x, _y, _width, _height - 1); } const QString WordPart::getCopyText() const @@ -98,10 +98,17 @@ const QString &WordPart::getText() const return _text; } -int WordPart::getLineNumber() +int WordPart::getLineNumber() const { return _lineNumber; } +int WordPart::getCharacterLength() const +{ + // return (this->getWord().isImage() ? 1 : this->getText().length()) + (_trailingSpace ? 1 : + // 0); + return this->getWord().isImage() ? 2 : this->getText().length() + 1; +} + } // namespace messages } // namespace chatterino diff --git a/src/messages/wordpart.hpp b/src/messages/wordpart.hpp index 65c6a7e45..bdf3375d6 100644 --- a/src/messages/wordpart.hpp +++ b/src/messages/wordpart.hpp @@ -30,7 +30,8 @@ public: const QString getCopyText() const; int hasTrailingSpace() const; const QString &getText() const; - int getLineNumber(); + int getLineNumber() const; + int getCharacterLength() const; private: Word &_word; diff --git a/src/widgets/chatwidget.cpp b/src/widgets/chatwidget.cpp index 469e48ca2..e0d8ba143 100644 --- a/src/widgets/chatwidget.cpp +++ b/src/widgets/chatwidget.cpp @@ -5,6 +5,8 @@ #include "settingsmanager.hpp" #include "widgets/textinputdialog.hpp" +#include +#include #include #include #include @@ -65,6 +67,9 @@ ChatWidget::ChatWidget(ChannelManager &_channelManager, NotebookPage *parent) // CTRL+R: Change Channel ezShortcut(this, "CTRL+R", &ChatWidget::doChangeChannel); + // CTRL+C: Copy + ezShortcut(this, "CTRL+B", &ChatWidget::doCopy); + this->channelName.getValueChangedSignal().connect( std::bind(&ChatWidget::channelNameUpdated, this, std::placeholders::_1)); @@ -108,8 +113,11 @@ void ChatWidget::setChannel(std::shared_ptr _newChannel) // on message removed this->messageRemovedConnection = - this->channel->messageRemovedFromStart.connect([](SharedMessage &) { - // + this->channel->messageRemovedFromStart.connect([this](SharedMessage &) { + this->view.selection.min.messageIndex--; + this->view.selection.max.messageIndex--; + this->view.selection.start.messageIndex--; + this->view.selection.end.messageIndex--; }); auto snapshot = this->channel->getMessageSnapshot(); @@ -296,5 +304,10 @@ void ChatWidget::doOpenStreamlink() } } +void ChatWidget::doCopy() +{ + QApplication::clipboard()->setText(this->view.getSelectedText()); +} + } // namespace widgets } // namespace chatterino diff --git a/src/widgets/chatwidget.hpp b/src/widgets/chatwidget.hpp index e7112294d..ff11de1bf 100644 --- a/src/widgets/chatwidget.hpp +++ b/src/widgets/chatwidget.hpp @@ -115,6 +115,9 @@ public slots: // Open twitch channel stream through streamlink void doOpenStreamlink(); + + // Copy text from chat + void doCopy(); }; } // namespace widgets diff --git a/src/widgets/chatwidgetview.cpp b/src/widgets/chatwidgetview.cpp index 5b93df0dd..b0b2c210f 100644 --- a/src/widgets/chatwidgetview.cpp +++ b/src/widgets/chatwidgetview.cpp @@ -1,8 +1,9 @@ #include "widgets/chatwidgetview.hpp" #include "channelmanager.hpp" #include "colorscheme.hpp" +#include "messages/limitedqueuesnapshot.hpp" #include "messages/message.hpp" -#include "messages/wordpart.hpp" +#include "messages/messageref.hpp" #include "settingsmanager.hpp" #include "ui_accountpopupform.h" #include "util/distancebetweenpoints.hpp" @@ -14,8 +15,10 @@ #include #include +#include #include #include +#include namespace chatterino { namespace widgets { @@ -25,10 +28,6 @@ ChatWidgetView::ChatWidgetView(ChatWidget *_chatWidget) , chatWidget(_chatWidget) , scrollBar(this) , userPopupWidget(_chatWidget->getChannelRef()) - , selectionStart(0, 0) - , selectionEnd(0, 0) - , selectionMin(0, 0) - , selectionMax(0, 0) { #ifndef Q_OS_MAC this->setAttribute(Qt::WA_OpaquePaintEvent); @@ -70,14 +69,14 @@ bool ChatWidgetView::layoutMessages() // The scrollbar was visible and at the bottom this->showingLatestMessages = this->scrollBar.isAtBottom() || !this->scrollBar.isVisible(); - int start = this->scrollBar.getCurrentValue(); + size_t start = this->scrollBar.getCurrentValue(); int layoutWidth = this->scrollBar.isVisible() ? width() - this->scrollBar.width() : width(); // layout the visible messages in the view if (messages.getLength() > start) { int y = -(messages[start]->getHeight() * (fmod(this->scrollBar.getCurrentValue(), 1))); - for (int i = start; i < messages.getLength(); ++i) { + for (size_t i = start; i < messages.getLength(); ++i) { auto message = messages[i]; redraw |= message->layout(layoutWidth, true); @@ -141,6 +140,97 @@ ScrollBar &ChatWidgetView::getScrollBar() return this->scrollBar; } +QString ChatWidgetView::getSelectedText() const +{ + messages::LimitedQueueSnapshot messages = + this->chatWidget->getMessagesSnapshot(); + + QString text; + bool isSingleMessage = this->selection.isSingleMessage(); + + size_t i = std::max(0, this->selection.min.messageIndex); + + int charIndex = 0; + + bool first = true; + + for (const messages::WordPart &part : messages[i]->getWordParts()) { + int charLength = part.getCharacterLength(); + + if (charIndex + charLength < this->selection.min.charIndex) { + charIndex += charLength; + continue; + } + + if (first) { + first = false; + + if (part.getWord().isText()) { + text += part.getText().mid(this->selection.min.charIndex - charIndex); + } else { + text += part.getCopyText(); + } + } + + if (isSingleMessage && charIndex + charLength >= selection.max.charIndex) { + if (part.getWord().isText()) { + text += part.getText().mid(0, this->selection.max.charIndex - charIndex); + } else { + text += part.getCopyText(); + } + return text; + } + + text += part.getCopyText(); + + if (part.hasTrailingSpace()) { + text += " "; + } + + charIndex += charLength; + } + + text += "\n"; + + for (i++; i < this->selection.max.messageIndex; i++) { + for (const messages::WordPart &part : messages[i]->getWordParts()) { + text += part.getCopyText(); + + if (part.hasTrailingSpace()) { + text += " "; + } + } + text += "\n"; + } + + charIndex = 0; + + for (const messages::WordPart &part : + messages[this->selection.max.messageIndex]->getWordParts()) { + int charLength = part.getCharacterLength(); + + if (charIndex + charLength >= this->selection.max.charIndex) { + if (part.getWord().isText()) { + text += part.getText().mid(0, this->selection.max.charIndex - charIndex); + } else { + text += part.getCopyText(); + } + + return text; + } + + text += part.getCopyText(); + + if (part.hasTrailingSpace()) { + text += " "; + } + + charIndex += charLength; + } + + return text; +} + void ChatWidgetView::resizeEvent(QResizeEvent *) { this->scrollBar.resize(this->scrollBar.width(), height()); @@ -151,24 +241,13 @@ void ChatWidgetView::resizeEvent(QResizeEvent *) this->update(); } -void ChatWidgetView::setSelection(SelectionItem start, SelectionItem end) +void ChatWidgetView::setSelection(const SelectionItem &start, const SelectionItem &end) { // selections - SelectionItem min = selectionStart; - SelectionItem max = selectionEnd; + this->selection = Selection(start, end); - if (max.isSmallerThan(min)) { - std::swap(min, max); - } - - this->selectionStart = start; - this->selectionEnd = end; - - this->selectionMin = min; - this->selectionMax = max; - - qDebug() << min.messageIndex << ":" << min.charIndex << " " << max.messageIndex << ":" - << max.charIndex; + // qDebug() << min.messageIndex << ":" << min.charIndex << " " << max.messageIndex << ":" + // << max.charIndex; } void ChatWidgetView::paintEvent(QPaintEvent * /*event*/) @@ -212,7 +291,7 @@ void ChatWidgetView::drawMessages(QPainter &painter) { auto messages = this->chatWidget->getMessagesSnapshot(); - int start = this->scrollBar.getCurrentValue(); + size_t start = this->scrollBar.getCurrentValue(); if (start >= messages.getLength()) { return; @@ -234,6 +313,8 @@ void ChatWidgetView::drawMessages(QPainter &painter) updateBuffer = true; } + updateBuffer |= this->selecting; + // update messages that have been changed if (updateBuffer) { this->updateMessageBuffer(messageRef, buffer, i); @@ -275,17 +356,17 @@ void ChatWidgetView::updateMessageBuffer(messages::MessageRef *messageRef, QPixm QPainter painter(buffer); // draw background - // if (this->selectionMin.messageIndex <= messageIndex && - // this->selectionMax.messageIndex >= messageIndex) { - // painter.fillRect(buffer->rect(), QColor(24, 55, 25)); - //} else { painter.fillRect(buffer->rect(), (messageRef->getMessage()->getCanHighlightTab()) ? this->colorScheme.ChatBackgroundHighlighted : this->colorScheme.ChatBackground); - //} - // draw messages + // draw selection + if (!selection.isEmpty()) { + drawMessageSelection(painter, messageRef, messageIndex, buffer->height()); + } + + // draw message for (messages::WordPart const &wordPart : messageRef->getWordParts()) { // image if (wordPart.getWord().isImage()) { @@ -316,6 +397,155 @@ void ChatWidgetView::updateMessageBuffer(messages::MessageRef *messageRef, QPixm messageRef->updateBuffer = false; } +void ChatWidgetView::drawMessageSelection(QPainter &painter, messages::MessageRef *messageRef, + int messageIndex, int bufferHeight) +{ + if (this->selection.min.messageIndex > messageIndex || + this->selection.max.messageIndex < messageIndex) { + return; + } + + QColor selectionColor(255, 255, 255, 63); + + int charIndex = 0; + size_t i = 0; + auto &parts = messageRef->getWordParts(); + + int currentLineNumber = 0; + QRect rect; + + if (parts.size() > 0) { + if (selection.min.messageIndex == messageIndex) { + rect.setTop(parts.at(0).getY()); + } + rect.setLeft(parts.at(0).getX()); + } + + // skip until selection start + if (this->selection.min.messageIndex == messageIndex && this->selection.min.charIndex != 0) { + for (; i < parts.size(); i++) { + const messages::WordPart &part = parts.at(i); + auto characterLength = part.getCharacterLength(); + + if (characterLength + charIndex > selection.min.charIndex) { + break; + } + + charIndex += characterLength; + currentLineNumber = part.getLineNumber(); + } + + if (i >= parts.size()) { + return; + } + + // handle word that has a cut of selection + const messages::WordPart &part = parts.at(i); + + // check if selection if single word + int characterLength = part.getCharacterLength(); + bool isSingleWord = charIndex + characterLength > this->selection.max.charIndex && + this->selection.max.messageIndex == messageIndex; + + rect = part.getRect(); + currentLineNumber = part.getLineNumber(); + + if (part.getWord().isText()) { + int offset = this->selection.min.charIndex - charIndex; + + std::vector &characterWidth = part.getWord().getCharacterWidthCache(); + + for (int j = 0; j < offset; j++) { + rect.setLeft(rect.left() + characterWidth[j]); + } + + if (isSingleWord) { + int length = (this->selection.max.charIndex - charIndex) - offset; + + rect.setRight(part.getX()); + + for (int j = 0; j < offset + length; j++) { + rect.setRight(rect.right() + characterWidth[j]); + } + + painter.fillRect(rect, selectionColor); + + return; + } + } else { + if (isSingleWord) { + if (charIndex + 1 != this->selection.max.charIndex) { + rect.setRight(part.getX() + part.getWord().getImage().getScaledWidth()); + } + painter.fillRect(rect, selectionColor); + + return; + } + + if (charIndex != this->selection.min.charIndex) { + rect.setLeft(part.getX() + part.getWord().getImage().getScaledWidth()); + } + } + + i++; + charIndex += characterLength; + } + + // go through lines and draw selection + for (; i < parts.size(); i++) { + const messages::WordPart &part = parts.at(i); + + int charLength = part.getCharacterLength(); + + bool isLastSelectedWord = this->selection.max.messageIndex == messageIndex && + charIndex + charLength > this->selection.max.charIndex; + + if (part.getLineNumber() == currentLineNumber) { + rect.setLeft(std::min(rect.left(), part.getX())); + rect.setTop(std::min(rect.top(), part.getY())); + rect.setRight(std::max(rect.right(), part.getRight())); + rect.setBottom(std::max(rect.bottom(), part.getBottom() - 1)); + } else { + painter.fillRect(rect, selectionColor); + + currentLineNumber = part.getLineNumber(); + + rect = part.getRect(); + } + + if (isLastSelectedWord) { + if (part.getWord().isText()) { + int offset = this->selection.min.charIndex - charIndex; + + std::vector &characterWidth = part.getWord().getCharacterWidthCache(); + + int length = (this->selection.max.charIndex - charIndex) - offset; + + rect.setRight(part.getX()); + + for (int j = 0; j < offset + length; j++) { + rect.setRight(rect.right() + characterWidth[j]); + } + } else { + if (this->selection.max.charIndex == charIndex) { + rect.setRight(part.getX()); + } + } + painter.fillRect(rect, selectionColor); + + return; + } + + charIndex += charLength; + } + + if (this->selection.max.messageIndex != messageIndex) { + rect.setBottom(bufferHeight); + } + + painter.fillRect(rect, selectionColor); +} + void ChatWidgetView::wheelEvent(QWheelEvent *event) { if (this->scrollBar.isVisible()) { @@ -340,18 +570,18 @@ void ChatWidgetView::mouseMoveEvent(QMouseEvent *event) if (this->selecting) { int index = message->getSelectionIndex(relativePos); - this->setSelection(this->selectionStart, SelectionItem(messageIndex, index)); + this->setSelection(this->selection.start, SelectionItem(messageIndex, index)); this->repaint(); } - messages::Word hoverWord; - if (!message->tryGetWordPart(relativePos, hoverWord)) { + const messages::Word *hoverWord; + if ((hoverWord = message->tryGetWordPart(relativePos)) == nullptr) { setCursor(Qt::ArrowCursor); return; } - if (hoverWord.getLink().isValid()) { + if (hoverWord->getLink().isValid()) { setCursor(Qt::PointingHandCursor); } else { setCursor(Qt::ArrowCursor); @@ -420,13 +650,13 @@ void ChatWidgetView::mouseReleaseEvent(QMouseEvent *event) return; } - messages::Word hoverWord; + const messages::Word *hoverWord; - if (!message->tryGetWordPart(relativePos, hoverWord)) { + if ((hoverWord = message->tryGetWordPart(relativePos)) == nullptr) { return; } - auto &link = hoverWord.getLink(); + auto &link = hoverWord->getLink(); switch (link.getType()) { case messages::Link::UserInfo: { @@ -451,7 +681,7 @@ bool ChatWidgetView::tryGetMessageAt(QPoint p, std::shared_ptrchatWidget->getMessagesSnapshot(); - int start = this->scrollBar.getCurrentValue(); + size_t start = this->scrollBar.getCurrentValue(); if (start >= messages.getLength()) { return false; @@ -459,7 +689,7 @@ bool ChatWidgetView::tryGetMessageAt(QPoint p, std::shared_ptrgetHeight() * (fmod(this->scrollBar.getCurrentValue(), 1))); - for (int i = start; i < messages.getLength(); ++i) { + for (size_t i = start; i < messages.getLength(); ++i) { auto message = messages[i]; if (p.y() < y + message->getHeight()) { diff --git a/src/widgets/chatwidgetview.hpp b/src/widgets/chatwidgetview.hpp index 416cb07a5..a9c940138 100644 --- a/src/widgets/chatwidgetview.hpp +++ b/src/widgets/chatwidgetview.hpp @@ -20,16 +20,58 @@ struct SelectionItem { int messageIndex; int charIndex; + SelectionItem() + { + messageIndex = charIndex = 0; + } + SelectionItem(int _messageIndex, int _charIndex) { this->messageIndex = _messageIndex; this->charIndex = _charIndex; } - bool isSmallerThan(SelectionItem &other) + bool isSmallerThan(const SelectionItem &other) const { - return messageIndex < other.messageIndex || - (messageIndex == other.messageIndex && charIndex < other.charIndex); + return this->messageIndex < other.messageIndex || + (this->messageIndex == other.messageIndex && this->charIndex < other.charIndex); + } + + bool equals(const SelectionItem &other) const + { + return this->messageIndex == other.messageIndex && this->charIndex == other.charIndex; + } +}; + +struct Selection { + SelectionItem start; + SelectionItem end; + SelectionItem min; + SelectionItem max; + + Selection() + { + } + + Selection(const SelectionItem &start, const SelectionItem &end) + : start(start) + , end(end) + , min(start) + , max(end) + { + if (max.isSmallerThan(min)) { + std::swap(this->min, this->max); + } + } + + bool isEmpty() const + { + return this->start.equals(this->end); + } + + bool isSingleMessage() const + { + return this->min.messageIndex == this->max.messageIndex; } }; @@ -37,6 +79,8 @@ class ChatWidget; class ChatWidgetView : public BaseWidget { + friend class ChatWidget; + public: explicit ChatWidgetView(ChatWidget *_chatWidget); ~ChatWidgetView(); @@ -45,6 +89,7 @@ public: void updateGifEmotes(); ScrollBar &getScrollBar(); + QString getSelectedText() const; protected: virtual void resizeEvent(QResizeEvent *) override; @@ -67,7 +112,9 @@ private: void drawMessages(QPainter &painter); void updateMessageBuffer(messages::MessageRef *messageRef, QPixmap *buffer, int messageIndex); - void setSelection(SelectionItem start, SelectionItem end); + void drawMessageSelection(QPainter &painter, messages::MessageRef *messageRef, int messageIndex, + int bufferHeight); + void setSelection(const SelectionItem &start, const SelectionItem &end); std::vector gifEmotes; @@ -86,10 +133,7 @@ private: bool isMouseDown = false; QPointF lastPressPosition; - SelectionItem selectionStart; - SelectionItem selectionEnd; - SelectionItem selectionMin; - SelectionItem selectionMax; + Selection selection; bool selecting = false; private slots: