added text selection

This commit is contained in:
fourtf 2017-09-12 19:06:16 +02:00
parent 8b40393023
commit 81b1a8774b
18 changed files with 585 additions and 239 deletions

View file

@ -100,6 +100,7 @@ void ColorScheme::setColors(double hue, double multiplier)
// Chat // Chat
ChatBackground = getColor(0, 0.1, 1); ChatBackground = getColor(0, 0.1, 1);
ChatBackgroundHighlighted = blendColors(TabSelectedBackground, ChatBackground, 0.8);
ChatHeaderBackground = getColor(0, 0.1, 0.9); ChatHeaderBackground = getColor(0, 0.1, 0.9);
ChatHeaderBorder = getColor(0, 0.1, 0.85); ChatHeaderBorder = getColor(0, 0.1, 0.85);
ChatInputBackground = getColor(0, 0.1, 0.95); ChatInputBackground = getColor(0, 0.1, 0.95);
@ -120,6 +121,15 @@ void ColorScheme::setColors(double hue, double multiplier)
this->updated(); 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) void ColorScheme::normalizeColor(QColor &color)
{ {
if (this->lightTheme) { if (this->lightTheme) {

View file

@ -87,6 +87,7 @@ private:
pajlada::Settings::Setting<double> themeHue; pajlada::Settings::Setting<double> themeHue;
void setColors(double hue, double multiplier); void setColors(double hue, double multiplier);
QColor blendColors(const QColor &color1, const QColor &color2, qreal ratio);
double middleLookupTable[360] = {}; double middleLookupTable[360] = {};
double minLookupTable[360] = {}; double minLookupTable[360] = {};

View file

@ -22,14 +22,14 @@ LazyLoadedImage::LazyLoadedImage(EmoteManager &_emoteManager, WindowManager &_wi
const QString &tooltip, const QMargins &margin, bool isHat) const QString &tooltip, const QMargins &margin, bool isHat)
: emoteManager(_emoteManager) : emoteManager(_emoteManager)
, windowManager(_windowManager) , windowManager(_windowManager)
, _currentPixmap(nullptr) , currentPixmap(nullptr)
, _url(url) , url(url)
, _name(name) , name(name)
, _tooltip(tooltip) , tooltip(tooltip)
, _margin(margin) , margin(margin)
, _ishat(isHat) , ishat(isHat)
, _scale(scale) , scale(scale)
, _isLoading(false) , isLoading(false)
{ {
} }
@ -38,19 +38,19 @@ LazyLoadedImage::LazyLoadedImage(EmoteManager &_emoteManager, WindowManager &_wi
const QString &tooltip, const QMargins &margin, bool isHat) const QString &tooltip, const QMargins &margin, bool isHat)
: emoteManager(_emoteManager) : emoteManager(_emoteManager)
, windowManager(_windowManager) , windowManager(_windowManager)
, _currentPixmap(image) , currentPixmap(image)
, _name(name) , name(name)
, _tooltip(tooltip) , tooltip(tooltip)
, _margin(margin) , margin(margin)
, _ishat(isHat) , ishat(isHat)
, _scale(scale) , scale(scale)
, _isLoading(true) , isLoading(true)
{ {
} }
void LazyLoadedImage::loadImage() void LazyLoadedImage::loadImage()
{ {
util::urlFetch(_url, [=](QNetworkReply &reply) { util::urlFetch(this->url, [=](QNetworkReply &reply) {
QByteArray array = reply.readAll(); QByteArray array = reply.readAll();
QBuffer buffer(&array); QBuffer buffer(&array);
buffer.open(QIODevice::ReadOnly); buffer.open(QIODevice::ReadOnly);
@ -66,19 +66,19 @@ void LazyLoadedImage::loadImage()
if (first) { if (first) {
first = false; first = false;
_currentPixmap = pixmap; this->currentPixmap = pixmap;
} }
FrameData data; FrameData data;
data.duration = std::max(20, reader.nextImageDelay()); data.duration = std::max(20, reader.nextImageDelay());
data.image = pixmap; data.image = pixmap;
_allFrames.push_back(data); this->allFrames.push_back(data);
} }
} }
if (_allFrames.size() > 1) { if (this->allFrames.size() > 1) {
_animated = true; this->animated = true;
this->emoteManager.getGifUpdateSignal().connect([this] { this->emoteManager.getGifUpdateSignal().connect([this] {
gifUpdateTimout(); // gifUpdateTimout(); //
@ -92,18 +92,90 @@ void LazyLoadedImage::loadImage()
void LazyLoadedImage::gifUpdateTimout() void LazyLoadedImage::gifUpdateTimout()
{ {
_currentFrameOffset += GIF_FRAME_LENGTH; this->currentFrameOffset += GIF_FRAME_LENGTH;
while (true) { while (true) {
if (_currentFrameOffset > _allFrames.at(_currentFrame).duration) { if (this->currentFrameOffset > this->allFrames.at(this->currentFrame).duration) {
_currentFrameOffset -= _allFrames.at(_currentFrame).duration; this->currentFrameOffset -= this->allFrames.at(this->currentFrame).duration;
_currentFrame = (_currentFrame + 1) % _allFrames.size(); this->currentFrame = (this->currentFrame + 1) % this->allFrames.size();
} else { } else {
break; 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<int>(getWidth() * this->scale);
}
int LazyLoadedImage::getHeight() const
{
if (this->currentPixmap == nullptr) {
return 16;
}
return this->currentPixmap->height();
}
int LazyLoadedImage::getScaledHeight() const
{
return static_cast<int>(getHeight() * this->scale);
}
} // namespace messages } // namespace messages
} // namespace chatterino } // namespace chatterino

View file

@ -25,66 +25,18 @@ public:
const QString &_tooltip = "", const QMargins &_margin = QMargins(), const QString &_tooltip = "", const QMargins &_margin = QMargins(),
bool isHat = false); bool isHat = false);
const QPixmap *getPixmap() const QPixmap *getPixmap();
{ qreal getScale() const;
if (!_isLoading) { const QString &getUrl() const;
_isLoading = true; const QString &getName() const;
const QString &getTooltip() const;
loadImage(); const QMargins &getMargin() const;
} bool getAnimated() const;
return _currentPixmap; bool isHat() const;
} int getWidth() const;
int getScaledWidth() const;
qreal getScale() const int getHeight() const;
{ int getScaledHeight() 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();
}
private: private:
EmoteManager &emoteManager; EmoteManager &emoteManager;
@ -95,20 +47,20 @@ private:
int duration; int duration;
}; };
QPixmap *_currentPixmap; QPixmap *currentPixmap;
std::vector<FrameData> _allFrames; std::vector<FrameData> allFrames;
int _currentFrame = 0; int currentFrame = 0;
int _currentFrameOffset = 0; int currentFrameOffset = 0;
QString _url; QString url;
QString _name; QString name;
QString _tooltip; QString tooltip;
bool _animated = false; bool animated = false;
QMargins _margin; QMargins margin;
bool _ishat; bool ishat;
qreal _scale; qreal scale;
bool _isLoading; bool isLoading;
void loadImage(); void loadImage();

View file

@ -29,12 +29,12 @@ Message::Message(const QString &text)
} }
*/ */
Message::Message(const QString &text, const std::vector<Word> &words, const bool &highlight) //Message::Message(const QString &text, const std::vector<Word> &words, const bool &highlight)
: text(text) // : text(text)
, highlightTab(highlight) // , highlightTab(highlight)
, words(words) // , words(words)
{ //{
} //}
bool Message::getCanHighlightTab() const bool Message::getCanHighlightTab() const
{ {

View file

@ -24,8 +24,8 @@ class Message
{ {
public: public:
// explicit Message(const QString &text); // explicit Message(const QString &text);
explicit Message(const QString &text, const std::vector<messages::Word> &words, //explicit Message(const QString &text, const std::vector<messages::Word> &words,
const bool &highlight); // const bool &highlight);
bool getCanHighlightTab() const; bool getCanHighlightTab() const;
const QString &getTimeoutUser() const; const QString &getTimeoutUser() const;

View file

@ -7,21 +7,19 @@ namespace chatterino {
namespace messages { namespace messages {
MessageBuilder::MessageBuilder() MessageBuilder::MessageBuilder()
: _words() : message(new Message)
{ {
_parseTime = std::chrono::system_clock::now(); _parseTime = std::chrono::system_clock::now();
linkRegex.setPattern("[[:ascii:]]*\\.[a-zA-Z]+\\/?[[:ascii:]]*");
} }
SharedMessage MessageBuilder::build() 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() void MessageBuilder::appendTimestamp()
@ -59,6 +57,9 @@ void MessageBuilder::appendTimestamp(time_t time)
QString MessageBuilder::matchLink(const QString &string) QString MessageBuilder::matchLink(const QString &string)
{ {
static QRegularExpression linkRegex("[[:ascii:]]*\\.[a-zA-Z]+\\/?[[:ascii:]]*");
static QRegularExpression httpRegex("\\bhttps?://");
auto match = linkRegex.match(string); auto match = linkRegex.match(string);
if (!match.hasMatch()) { if (!match.hasMatch()) {
@ -67,7 +68,7 @@ QString MessageBuilder::matchLink(const QString &string)
QString captured = match.captured(); QString captured = match.captured();
if (!captured.contains(QRegularExpression("\\bhttps?://"))) { if (!captured.contains(httpRegex)) {
captured.insert(0, "http://"); captured.insert(0, "http://");
} }
return captured; return captured;

View file

@ -16,7 +16,7 @@ public:
SharedMessage build(); SharedMessage build();
void appendWord(const Word &word); void appendWord(const Word &&word);
void appendTimestamp(); void appendTimestamp();
void appendTimestamp(std::time_t time); void appendTimestamp(std::time_t time);
void setHighlight(const bool &value); void setHighlight(const bool &value);
@ -27,6 +27,7 @@ public:
QString originalMessage; QString originalMessage;
private: private:
std::shared_ptr<messages::Message> message;
std::vector<Word> _words; std::vector<Word> _words;
bool highlight = false; bool highlight = false;
std::chrono::time_point<std::chrono::system_clock> _parseTime; std::chrono::time_point<std::chrono::system_clock> _parseTime;

View file

@ -6,8 +6,8 @@
#define MARGIN_LEFT 8 #define MARGIN_LEFT 8
#define MARGIN_RIGHT 8 #define MARGIN_RIGHT 8
#define MARGIN_TOP 8 #define MARGIN_TOP 4
#define MARGIN_BOTTOM 8 #define MARGIN_BOTTOM 4
using namespace chatterino::messages; using namespace chatterino::messages;
@ -142,18 +142,12 @@ bool MessageRef::layout(int width, bool enableEmoteMargins)
std::vector<short> &charWidths = word.getCharacterWidthCache(); std::vector<short> &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++) { for (int i = 2; i <= text.length(); i++) {
if ((width = width + charWidths[i - 1]) + MARGIN_LEFT > right) { if ((width = width + charWidths[i - 1]) + MARGIN_LEFT > right) {
QString mid = text.mid(start, i - start - 1); QString mid = text.mid(start, i - start - 1);
_wordParts.push_back(WordPart(word, MARGIN_LEFT, y, width, word.getHeight(), _wordParts.push_back(WordPart(word, MARGIN_LEFT, y, width, word.getHeight(),
lineNumber, mid, mid)); lineNumber, mid, mid, false));
y += metrics.height(); y += metrics.height();
@ -193,6 +187,8 @@ bool MessageRef::layout(int width, bool enableEmoteMargins)
y += lineHeight; y += lineHeight;
lineNumber++;
_wordParts.push_back( _wordParts.push_back(
WordPart(word, MARGIN_LEFT, y - word.getHeight(), lineNumber, word.getCopyText())); 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 = word.getWidth() + MARGIN_LEFT;
x += spaceWidth; x += spaceWidth;
lineNumber++;
} }
} }
@ -214,6 +208,8 @@ bool MessageRef::layout(int width, bool enableEmoteMargins)
_height = y + lineHeight; _height = y + lineHeight;
} }
_height += MARGIN_BOTTOM;
if (sizeChanged) { if (sizeChanged) {
buffer = nullptr; 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. // go through all words and return the first one that contains the point.
for (WordPart &wordPart : _wordParts) { for (WordPart &wordPart : _wordParts) {
if (wordPart.getRect().contains(point)) { if (wordPart.getRect().contains(point)) {
word = wordPart.getWord(); return &wordPart.getWord();
return true;
} }
} }
return false; return nullptr;
} }
int MessageRef::getSelectionIndex(QPoint position) int MessageRef::getSelectionIndex(QPoint position)
@ -259,7 +254,7 @@ int MessageRef::getSelectionIndex(QPoint position)
// find out in which line the cursor is // find out in which line the cursor is
int lineNumber = 0, lineStart = 0, lineEnd = 0; 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]; WordPart &part = _wordParts[i];
if (part.getLineNumber() != 0 && position.y() < part.getY()) { if (part.getLineNumber() != 0 && position.y() < part.getY()) {
@ -267,11 +262,11 @@ int MessageRef::getSelectionIndex(QPoint position)
} }
if (part.getLineNumber() != lineNumber) { if (part.getLineNumber() != lineNumber) {
lineStart = i - 1; lineStart = i;
lineNumber = part.getLineNumber(); lineNumber = part.getLineNumber();
} }
lineEnd = part.getLineNumber() == 0 ? i : i + 1; lineEnd = i + 1;
} }
// count up to the cursor // count up to the cursor
@ -293,15 +288,19 @@ int MessageRef::getSelectionIndex(QPoint position)
// cursor is right of the word part // cursor is right of the word part
if (position.x() > part.getX() + part.getWidth()) { if (position.x() > part.getX() + part.getWidth()) {
index += part.getWord().isImage() ? 2 : part.getText().length() + 1; index += part.getCharacterLength();
continue; continue;
} }
// cursor is over the word part // cursor is over the word part
if (part.getWord().isImage()) { if (part.getWord().isImage()) {
index++; if (position.x() - part.getX() > part.getWidth() / 2) {
index++;
}
} else { } else {
auto text = part.getWord().getText(); // TODO: use word.getCharacterWidthCache();
auto text = part.getText();
int x = part.getX(); int x = part.getX();

View file

@ -28,7 +28,7 @@ public:
std::shared_ptr<QPixmap> buffer = nullptr; std::shared_ptr<QPixmap> buffer = nullptr;
bool updateBuffer = false; bool updateBuffer = false;
bool tryGetWordPart(QPoint point, messages::Word &word); const messages::Word *tryGetWordPart(QPoint point);
int getSelectionIndex(QPoint position); int getSelectionIndex(QPoint position);

View file

@ -6,15 +6,12 @@ namespace messages {
// Image word // Image word
Word::Word(LazyLoadedImage *image, Type type, const QString &copytext, const QString &tooltip, Word::Word(LazyLoadedImage *image, Type type, const QString &copytext, const QString &tooltip,
const Link &link) const Link &link)
: _image(image) : image(image)
, _text()
, _color()
, _isImage(true) , _isImage(true)
, _type(type) , type(type)
, _copyText(copytext) , copyText(copytext)
, _tooltip(tooltip) , tooltip(tooltip)
, _link(link) , link(link)
, _characterWidthCache()
{ {
image->getWidth(); // professional segfault test image->getWidth(); // professional segfault test
} }
@ -22,113 +19,127 @@ Word::Word(LazyLoadedImage *image, Type type, const QString &copytext, const QSt
// Text word // Text word
Word::Word(const QString &text, Type type, const QColor &color, const QString &copytext, Word::Word(const QString &text, Type type, const QColor &color, const QString &copytext,
const QString &tooltip, const Link &link) const QString &tooltip, const Link &link)
: _image(nullptr) : image(nullptr)
, _text(text) , text(text)
, _color(color) , color(color)
, _isImage(false) , _isImage(false)
, _type(type) , type(type)
, _copyText(copytext) , copyText(copytext)
, _tooltip(tooltip) , tooltip(tooltip)
, _link(link) , link(link)
, _characterWidthCache()
{ {
} }
LazyLoadedImage &Word::getImage() const LazyLoadedImage &Word::getImage() const
{ {
return *_image; return *this->image;
} }
const QString &Word::getText() const const QString &Word::getText() const
{ {
return _text; return this->text;
} }
int Word::getWidth() const int Word::getWidth() const
{ {
return _width; return this->width;
} }
int Word::getHeight() const int Word::getHeight() const
{ {
return _height; return this->height;
} }
void Word::setSize(int width, int height) void Word::setSize(int width, int height)
{ {
_width = width; this->width = width;
_height = height; this->height = height;
} }
bool Word::isImage() const bool Word::isImage() const
{ {
return _isImage; return this->_isImage;
} }
bool Word::isText() const bool Word::isText() const
{ {
return !_isImage; return !this->_isImage;
} }
const QString &Word::getCopyText() const const QString &Word::getCopyText() const
{ {
return _copyText; return this->copyText;
} }
bool Word::hasTrailingSpace() const bool Word::hasTrailingSpace() const
{ {
return _hasTrailingSpace; return this->_hasTrailingSpace;
} }
QFont &Word::getFont() const QFont &Word::getFont() const
{ {
return FontManager::getInstance().getFont(_font); return FontManager::getInstance().getFont(this->font);
} }
QFontMetrics &Word::getFontMetrics() const QFontMetrics &Word::getFontMetrics() const
{ {
return FontManager::getInstance().getFontMetrics(_font); return FontManager::getInstance().getFontMetrics(this->font);
} }
Word::Type Word::getType() const Word::Type Word::getType() const
{ {
return _type; return this->type;
} }
const QString &Word::getTooltip() const const QString &Word::getTooltip() const
{ {
return _tooltip; return this->tooltip;
} }
const QColor &Word::getColor() const const QColor &Word::getColor() const
{ {
return _color; return this->color;
} }
const Link &Word::getLink() const const Link &Word::getLink() const
{ {
return _link; return this->link;
} }
int Word::getXOffset() const int Word::getXOffset() const
{ {
return _xOffset; return this->xOffset;
} }
int Word::getYOffset() const int Word::getYOffset() const
{ {
return _yOffset; return this->yOffset;
} }
void Word::setOffset(int xOffset, int yOffset) void Word::setOffset(int xOffset, int yOffset)
{ {
_xOffset = std::max(0, xOffset); this->xOffset = std::max(0, xOffset);
_yOffset = std::max(0, yOffset); this->yOffset = std::max(0, yOffset);
}
int Word::getCharacterLength() const
{
return this->isImage() ? 2 : this->getText().length() + 1;
} }
std::vector<short> &Word::getCharacterWidthCache() const std::vector<short> &Word::getCharacterWidthCache() const
{ {
return _characterWidthCache; // lock not required because there is only one gui thread
// std::lock_guard<std::mutex> 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 } // namespace messages

View file

@ -110,29 +110,30 @@ public:
int getXOffset() const; int getXOffset() const;
int getYOffset() const; int getYOffset() const;
void setOffset(int _xOffset, int _yOffset); void setOffset(int _xOffset, int _yOffset);
int getCharacterLength() const;
std::vector<short> &getCharacterWidthCache() const; std::vector<short> &getCharacterWidthCache() const;
private: private:
LazyLoadedImage *_image; LazyLoadedImage *image;
QString _text; QString text;
QColor _color; QColor color;
bool _isImage; bool _isImage;
Type _type; Type type;
QString _copyText; QString copyText;
QString _tooltip; QString tooltip;
int _width = 16; int width = 16;
int _height = 16; int height = 16;
int _xOffset = 0; int xOffset = 0;
int _yOffset = 0; int yOffset = 0;
bool _hasTrailingSpace; bool _hasTrailingSpace = true;
FontManager::Type _font = FontManager::Medium; FontManager::Type font = FontManager::Medium;
Link _link; Link link;
mutable std::vector<short> _characterWidthCache; mutable std::vector<short> charWidthCache;
}; };
} // namespace messages } // namespace messages

View file

@ -14,7 +14,7 @@ WordPart::WordPart(Word &word, int x, int y, int lineNumber, const QString &copy
, _width(word.getWidth()) , _width(word.getWidth())
, _height(word.getHeight()) , _height(word.getHeight())
, _lineNumber(lineNumber) , _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) , _width(width)
, _height(height) , _height(height)
, _lineNumber(lineNumber) , _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 QRect WordPart::getRect() const
{ {
return QRect(_x, _y, _width, _height); return QRect(_x, _y, _width, _height - 1);
} }
const QString WordPart::getCopyText() const const QString WordPart::getCopyText() const
@ -98,10 +98,17 @@ const QString &WordPart::getText() const
return _text; return _text;
} }
int WordPart::getLineNumber() int WordPart::getLineNumber() const
{ {
return _lineNumber; 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 messages
} // namespace chatterino } // namespace chatterino

View file

@ -30,7 +30,8 @@ public:
const QString getCopyText() const; const QString getCopyText() const;
int hasTrailingSpace() const; int hasTrailingSpace() const;
const QString &getText() const; const QString &getText() const;
int getLineNumber(); int getLineNumber() const;
int getCharacterLength() const;
private: private:
Word &_word; Word &_word;

View file

@ -5,6 +5,8 @@
#include "settingsmanager.hpp" #include "settingsmanager.hpp"
#include "widgets/textinputdialog.hpp" #include "widgets/textinputdialog.hpp"
#include <QApplication>
#include <QClipboard>
#include <QDebug> #include <QDebug>
#include <QFileInfo> #include <QFileInfo>
#include <QFont> #include <QFont>
@ -65,6 +67,9 @@ ChatWidget::ChatWidget(ChannelManager &_channelManager, NotebookPage *parent)
// CTRL+R: Change Channel // CTRL+R: Change Channel
ezShortcut(this, "CTRL+R", &ChatWidget::doChangeChannel); ezShortcut(this, "CTRL+R", &ChatWidget::doChangeChannel);
// CTRL+C: Copy
ezShortcut(this, "CTRL+B", &ChatWidget::doCopy);
this->channelName.getValueChangedSignal().connect( this->channelName.getValueChangedSignal().connect(
std::bind(&ChatWidget::channelNameUpdated, this, std::placeholders::_1)); std::bind(&ChatWidget::channelNameUpdated, this, std::placeholders::_1));
@ -108,8 +113,11 @@ void ChatWidget::setChannel(std::shared_ptr<Channel> _newChannel)
// on message removed // on message removed
this->messageRemovedConnection = 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(); auto snapshot = this->channel->getMessageSnapshot();
@ -296,5 +304,10 @@ void ChatWidget::doOpenStreamlink()
} }
} }
void ChatWidget::doCopy()
{
QApplication::clipboard()->setText(this->view.getSelectedText());
}
} // namespace widgets } // namespace widgets
} // namespace chatterino } // namespace chatterino

View file

@ -115,6 +115,9 @@ public slots:
// Open twitch channel stream through streamlink // Open twitch channel stream through streamlink
void doOpenStreamlink(); void doOpenStreamlink();
// Copy text from chat
void doCopy();
}; };
} // namespace widgets } // namespace widgets

View file

@ -1,8 +1,9 @@
#include "widgets/chatwidgetview.hpp" #include "widgets/chatwidgetview.hpp"
#include "channelmanager.hpp" #include "channelmanager.hpp"
#include "colorscheme.hpp" #include "colorscheme.hpp"
#include "messages/limitedqueuesnapshot.hpp"
#include "messages/message.hpp" #include "messages/message.hpp"
#include "messages/wordpart.hpp" #include "messages/messageref.hpp"
#include "settingsmanager.hpp" #include "settingsmanager.hpp"
#include "ui_accountpopupform.h" #include "ui_accountpopupform.h"
#include "util/distancebetweenpoints.hpp" #include "util/distancebetweenpoints.hpp"
@ -14,8 +15,10 @@
#include <QPainter> #include <QPainter>
#include <math.h> #include <math.h>
#include <algorithm>
#include <chrono> #include <chrono>
#include <functional> #include <functional>
#include <memory>
namespace chatterino { namespace chatterino {
namespace widgets { namespace widgets {
@ -25,10 +28,6 @@ ChatWidgetView::ChatWidgetView(ChatWidget *_chatWidget)
, chatWidget(_chatWidget) , chatWidget(_chatWidget)
, scrollBar(this) , scrollBar(this)
, userPopupWidget(_chatWidget->getChannelRef()) , userPopupWidget(_chatWidget->getChannelRef())
, selectionStart(0, 0)
, selectionEnd(0, 0)
, selectionMin(0, 0)
, selectionMax(0, 0)
{ {
#ifndef Q_OS_MAC #ifndef Q_OS_MAC
this->setAttribute(Qt::WA_OpaquePaintEvent); this->setAttribute(Qt::WA_OpaquePaintEvent);
@ -70,14 +69,14 @@ bool ChatWidgetView::layoutMessages()
// The scrollbar was visible and at the bottom // The scrollbar was visible and at the bottom
this->showingLatestMessages = this->scrollBar.isAtBottom() || !this->scrollBar.isVisible(); 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(); int layoutWidth = this->scrollBar.isVisible() ? width() - this->scrollBar.width() : width();
// layout the visible messages in the view // layout the visible messages in the view
if (messages.getLength() > start) { if (messages.getLength() > start) {
int y = -(messages[start]->getHeight() * (fmod(this->scrollBar.getCurrentValue(), 1))); 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]; auto message = messages[i];
redraw |= message->layout(layoutWidth, true); redraw |= message->layout(layoutWidth, true);
@ -141,6 +140,97 @@ ScrollBar &ChatWidgetView::getScrollBar()
return this->scrollBar; return this->scrollBar;
} }
QString ChatWidgetView::getSelectedText() const
{
messages::LimitedQueueSnapshot<messages::SharedMessageRef> 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 *) void ChatWidgetView::resizeEvent(QResizeEvent *)
{ {
this->scrollBar.resize(this->scrollBar.width(), height()); this->scrollBar.resize(this->scrollBar.width(), height());
@ -151,24 +241,13 @@ void ChatWidgetView::resizeEvent(QResizeEvent *)
this->update(); this->update();
} }
void ChatWidgetView::setSelection(SelectionItem start, SelectionItem end) void ChatWidgetView::setSelection(const SelectionItem &start, const SelectionItem &end)
{ {
// selections // selections
SelectionItem min = selectionStart; this->selection = Selection(start, end);
SelectionItem max = selectionEnd;
if (max.isSmallerThan(min)) { // qDebug() << min.messageIndex << ":" << min.charIndex << " " << max.messageIndex << ":"
std::swap(min, max); // << max.charIndex;
}
this->selectionStart = start;
this->selectionEnd = end;
this->selectionMin = min;
this->selectionMax = max;
qDebug() << min.messageIndex << ":" << min.charIndex << " " << max.messageIndex << ":"
<< max.charIndex;
} }
void ChatWidgetView::paintEvent(QPaintEvent * /*event*/) void ChatWidgetView::paintEvent(QPaintEvent * /*event*/)
@ -212,7 +291,7 @@ void ChatWidgetView::drawMessages(QPainter &painter)
{ {
auto messages = this->chatWidget->getMessagesSnapshot(); auto messages = this->chatWidget->getMessagesSnapshot();
int start = this->scrollBar.getCurrentValue(); size_t start = this->scrollBar.getCurrentValue();
if (start >= messages.getLength()) { if (start >= messages.getLength()) {
return; return;
@ -234,6 +313,8 @@ void ChatWidgetView::drawMessages(QPainter &painter)
updateBuffer = true; updateBuffer = true;
} }
updateBuffer |= this->selecting;
// update messages that have been changed // update messages that have been changed
if (updateBuffer) { if (updateBuffer) {
this->updateMessageBuffer(messageRef, buffer, i); this->updateMessageBuffer(messageRef, buffer, i);
@ -275,17 +356,17 @@ void ChatWidgetView::updateMessageBuffer(messages::MessageRef *messageRef, QPixm
QPainter painter(buffer); QPainter painter(buffer);
// draw background // draw background
// if (this->selectionMin.messageIndex <= messageIndex &&
// this->selectionMax.messageIndex >= messageIndex) {
// painter.fillRect(buffer->rect(), QColor(24, 55, 25));
//} else {
painter.fillRect(buffer->rect(), painter.fillRect(buffer->rect(),
(messageRef->getMessage()->getCanHighlightTab()) (messageRef->getMessage()->getCanHighlightTab())
? this->colorScheme.ChatBackgroundHighlighted ? this->colorScheme.ChatBackgroundHighlighted
: this->colorScheme.ChatBackground); : 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()) { for (messages::WordPart const &wordPart : messageRef->getWordParts()) {
// image // image
if (wordPart.getWord().isImage()) { if (wordPart.getWord().isImage()) {
@ -316,6 +397,155 @@ void ChatWidgetView::updateMessageBuffer(messages::MessageRef *messageRef, QPixm
messageRef->updateBuffer = false; 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<short> &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<short> &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) void ChatWidgetView::wheelEvent(QWheelEvent *event)
{ {
if (this->scrollBar.isVisible()) { if (this->scrollBar.isVisible()) {
@ -340,18 +570,18 @@ void ChatWidgetView::mouseMoveEvent(QMouseEvent *event)
if (this->selecting) { if (this->selecting) {
int index = message->getSelectionIndex(relativePos); int index = message->getSelectionIndex(relativePos);
this->setSelection(this->selectionStart, SelectionItem(messageIndex, index)); this->setSelection(this->selection.start, SelectionItem(messageIndex, index));
this->repaint(); this->repaint();
} }
messages::Word hoverWord; const messages::Word *hoverWord;
if (!message->tryGetWordPart(relativePos, hoverWord)) { if ((hoverWord = message->tryGetWordPart(relativePos)) == nullptr) {
setCursor(Qt::ArrowCursor); setCursor(Qt::ArrowCursor);
return; return;
} }
if (hoverWord.getLink().isValid()) { if (hoverWord->getLink().isValid()) {
setCursor(Qt::PointingHandCursor); setCursor(Qt::PointingHandCursor);
} else { } else {
setCursor(Qt::ArrowCursor); setCursor(Qt::ArrowCursor);
@ -420,13 +650,13 @@ void ChatWidgetView::mouseReleaseEvent(QMouseEvent *event)
return; return;
} }
messages::Word hoverWord; const messages::Word *hoverWord;
if (!message->tryGetWordPart(relativePos, hoverWord)) { if ((hoverWord = message->tryGetWordPart(relativePos)) == nullptr) {
return; return;
} }
auto &link = hoverWord.getLink(); auto &link = hoverWord->getLink();
switch (link.getType()) { switch (link.getType()) {
case messages::Link::UserInfo: { case messages::Link::UserInfo: {
@ -451,7 +681,7 @@ bool ChatWidgetView::tryGetMessageAt(QPoint p, std::shared_ptr<messages::Message
{ {
auto messages = this->chatWidget->getMessagesSnapshot(); auto messages = this->chatWidget->getMessagesSnapshot();
int start = this->scrollBar.getCurrentValue(); size_t start = this->scrollBar.getCurrentValue();
if (start >= messages.getLength()) { if (start >= messages.getLength()) {
return false; return false;
@ -459,7 +689,7 @@ bool ChatWidgetView::tryGetMessageAt(QPoint p, std::shared_ptr<messages::Message
int y = -(messages[start]->getHeight() * (fmod(this->scrollBar.getCurrentValue(), 1))); 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]; auto message = messages[i];
if (p.y() < y + message->getHeight()) { if (p.y() < y + message->getHeight()) {

View file

@ -20,16 +20,58 @@ struct SelectionItem {
int messageIndex; int messageIndex;
int charIndex; int charIndex;
SelectionItem()
{
messageIndex = charIndex = 0;
}
SelectionItem(int _messageIndex, int _charIndex) SelectionItem(int _messageIndex, int _charIndex)
{ {
this->messageIndex = _messageIndex; this->messageIndex = _messageIndex;
this->charIndex = _charIndex; this->charIndex = _charIndex;
} }
bool isSmallerThan(SelectionItem &other) bool isSmallerThan(const SelectionItem &other) const
{ {
return messageIndex < other.messageIndex || return this->messageIndex < other.messageIndex ||
(messageIndex == other.messageIndex && charIndex < other.charIndex); (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 class ChatWidgetView : public BaseWidget
{ {
friend class ChatWidget;
public: public:
explicit ChatWidgetView(ChatWidget *_chatWidget); explicit ChatWidgetView(ChatWidget *_chatWidget);
~ChatWidgetView(); ~ChatWidgetView();
@ -45,6 +89,7 @@ public:
void updateGifEmotes(); void updateGifEmotes();
ScrollBar &getScrollBar(); ScrollBar &getScrollBar();
QString getSelectedText() const;
protected: protected:
virtual void resizeEvent(QResizeEvent *) override; virtual void resizeEvent(QResizeEvent *) override;
@ -67,7 +112,9 @@ private:
void drawMessages(QPainter &painter); void drawMessages(QPainter &painter);
void updateMessageBuffer(messages::MessageRef *messageRef, QPixmap *buffer, int messageIndex); 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<GifEmoteData> gifEmotes; std::vector<GifEmoteData> gifEmotes;
@ -86,10 +133,7 @@ private:
bool isMouseDown = false; bool isMouseDown = false;
QPointF lastPressPosition; QPointF lastPressPosition;
SelectionItem selectionStart; Selection selection;
SelectionItem selectionEnd;
SelectionItem selectionMin;
SelectionItem selectionMax;
bool selecting = false; bool selecting = false;
private slots: private slots: