From 801e7b8a1ee85d12d69827dcba02a7010ba9aebd Mon Sep 17 00:00:00 2001 From: fourtf Date: Sun, 15 Jan 2017 16:38:30 +0100 Subject: [PATCH] updated messages --- chatterino.pro | 10 +- chatwidgetheader.cpp | 137 ++++++++++++++++++++++++++-- chatwidgetheader.h | 42 +++++++-- chatwidgetheaderbutton.cpp | 99 ++++++++++++++++++++ chatwidgetheaderbutton.h | 47 ++++++++++ chatwidgetheaderbuttonlabel.cpp | 21 +++++ chatwidgetheaderbuttonlabel.h | 23 +++++ chatwidgetview.cpp | 9 +- emotes.cpp | 2 + emotes.h | 14 +++ fonts.cpp | 4 +- fonts.h | 14 +++ images/button_ban.png | Bin 0 -> 8261 bytes images/button_timeout.png | Bin 0 -> 8431 bytes images/tool_moreCollapser_off16.png | Bin 292 -> 279 bytes ircmanager.cpp | 2 - lazyloadedimage.cpp | 2 + link.h | 2 + main.cpp | 3 +- message.cpp | 137 ++++++++++++++++++++-------- message.h | 18 +--- notebooktab.cpp | 62 ++++--------- notebooktab.h | 65 ++++++++++--- resources.cpp | 22 +++-- resources.h | 15 +++ resources.qrc | 2 + windows.cpp | 10 ++ windows.h | 35 +++++++ word.cpp | 6 +- word.h | 17 +++- wordpart.cpp | 15 ++- wordpart.h | 11 ++- 32 files changed, 698 insertions(+), 148 deletions(-) create mode 100644 chatwidgetheaderbutton.cpp create mode 100644 chatwidgetheaderbutton.h create mode 100644 chatwidgetheaderbuttonlabel.cpp create mode 100644 chatwidgetheaderbuttonlabel.h create mode 100644 images/button_ban.png create mode 100644 images/button_timeout.png create mode 100644 windows.cpp create mode 100644 windows.h diff --git a/chatterino.pro b/chatterino.pro index 535ef437..9331902d 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -61,7 +61,10 @@ SOURCES += main.cpp\ appsettings.cpp \ emojis.cpp \ wordpart.cpp \ - resources.cpp + resources.cpp \ + windows.cpp \ + chatwidgetheaderbutton.cpp \ + chatwidgetheaderbuttonlabel.cpp HEADERS += mainwindow.h \ chatwidget.h \ @@ -96,7 +99,10 @@ HEADERS += mainwindow.h \ emojis.h \ wordpart.h \ common.h \ - resources.h + resources.h \ + windows.h \ + chatwidgetheaderbutton.h \ + chatwidgetheaderbuttonlabel.h PRECOMPILED_HEADER = diff --git a/chatwidgetheader.cpp b/chatwidgetheader.cpp index 7abf9227..d2cd7101 100644 --- a/chatwidgetheader.cpp +++ b/chatwidgetheader.cpp @@ -1,16 +1,82 @@ #include "chatwidgetheader.h" -#include "QByteArray" -#include "QDrag" -#include "QMimeData" -#include "QPainter" #include "chatwidget.h" #include "colorscheme.h" #include "notebookpage.h" +#include +#include +#include +#include + ChatWidgetHeader::ChatWidgetHeader() : QWidget() + , m_dragStart() + , m_dragging(false) + , leftLabel() + , middleLabel() + , rightLabel() + , leftMenu(this) + , rightMenu(this) { setFixedHeight(32); + + updateColors(); + + setLayout(&hbox); + hbox.setMargin(0); + hbox.addWidget(&leftLabel); + hbox.addWidget(&middleLabel, 1); + hbox.addWidget(&rightLabel); + + // left + leftLabel.label().setTextFormat(Qt::RichText); + leftLabel.label().setText( + ""); + + QObject::connect(&leftLabel, ChatWidgetHeaderButton::clicked, this, + leftButtonClicked); + + leftMenu.addAction("Add new split", this, ChatWidgetHeader::menuAddSplit, + QKeySequence(tr("Ctrl+T"))); + leftMenu.addAction("Close split", this, ChatWidgetHeader::menuCloseSplit, + QKeySequence(tr("Ctrl+W"))); + leftMenu.addAction("Move split", this, ChatWidgetHeader::menuMoveSplit); + leftMenu.addSeparator(); + leftMenu.addAction("Change channel", this, + ChatWidgetHeader::menuChangeChannel, + QKeySequence(tr("Ctrl+R"))); + leftMenu.addAction("Clear chat", this, ChatWidgetHeader::menuClearChat); + leftMenu.addAction("Open channel", this, ChatWidgetHeader::menuOpenChannel); + leftMenu.addAction("Open pop-out player", this, + ChatWidgetHeader::menuPopupPlayer); + leftMenu.addSeparator(); + leftMenu.addAction("Reload channel emotes", this, + ChatWidgetHeader::menuReloadChannelEmotes); + leftMenu.addAction("Manual reconnect", this, + ChatWidgetHeader::menuManualReconnect); + leftMenu.addSeparator(); + leftMenu.addAction("Show changelog", this, + ChatWidgetHeader::menuShowChangelog); + + // middle + middleLabel.setAlignment(Qt::AlignCenter); + middleLabel.setText("textString"); + + // right + rightLabel.setMinimumWidth(height()); + rightLabel.label().setTextFormat(Qt::RichText); + rightLabel.label().setText("ayy"); +} + +void +ChatWidgetHeader::updateColors() +{ + QPalette palette; + palette.setColor(QPalette::Foreground, ColorScheme::instance().Text); + + leftLabel.setPalette(palette); + middleLabel.setPalette(palette); + rightLabel.setPalette(palette); } void @@ -26,17 +92,17 @@ ChatWidgetHeader::paintEvent(QPaintEvent *) void ChatWidgetHeader::mousePressEvent(QMouseEvent *event) { - dragging = true; + m_dragging = true; - dragStart = event->pos(); + m_dragStart = event->pos(); } void ChatWidgetHeader::mouseMoveEvent(QMouseEvent *event) { - if (dragging) { - if (std::abs(dragStart.x() - event->pos().x()) > 12 || - std::abs(dragStart.y() - event->pos().y()) > 12) { + if (m_dragging) { + if (std::abs(m_dragStart.x() - event->pos().x()) > 12 || + std::abs(m_dragStart.y() - event->pos().y()) > 12) { auto chatWidget = getChatWidget(); auto page = static_cast(chatWidget->parentWidget()); @@ -72,3 +138,56 @@ ChatWidgetHeader::getChatWidget() { return static_cast(parentWidget()); } + +void +ChatWidgetHeader::leftButtonClicked() +{ + leftMenu.move(leftLabel.mapToGlobal(QPoint(0, leftLabel.height()))); + leftMenu.show(); +} + +void +ChatWidgetHeader::rightButtonClicked() +{ +} + +void +ChatWidgetHeader::menuAddSplit() +{ +} +void +ChatWidgetHeader::menuCloseSplit() +{ +} +void +ChatWidgetHeader::menuMoveSplit() +{ +} +void +ChatWidgetHeader::menuChangeChannel() +{ +} +void +ChatWidgetHeader::menuClearChat() +{ +} +void +ChatWidgetHeader::menuOpenChannel() +{ +} +void +ChatWidgetHeader::menuPopupPlayer() +{ +} +void +ChatWidgetHeader::menuReloadChannelEmotes() +{ +} +void +ChatWidgetHeader::menuManualReconnect() +{ +} +void +ChatWidgetHeader::menuShowChangelog() +{ +} diff --git a/chatwidgetheader.h b/chatwidgetheader.h index 05c19615..3bedf36c 100644 --- a/chatwidgetheader.h +++ b/chatwidgetheader.h @@ -1,10 +1,16 @@ #ifndef CHATWIDGETHEADER_H #define CHATWIDGETHEADER_H -#include "QMouseEvent" -#include "QPaintEvent" -#include "QPoint" -#include "QWidget" +#include "chatwidgetheaderbutton.h" + +#include +#include +#include +#include +#include +#include +#include +#include class ChatWidget; @@ -15,6 +21,7 @@ class ChatWidgetHeader : public QWidget public: ChatWidgetHeader(); ChatWidget *getChatWidget(); + void updateColors(); protected: void paintEvent(QPaintEvent *); @@ -22,8 +29,31 @@ protected: void mouseMoveEvent(QMouseEvent *event); private: - QPoint dragStart; - bool dragging = false; + QPoint m_dragStart; + bool m_dragging; + + QHBoxLayout hbox; + + ChatWidgetHeaderButton leftLabel; + QLabel middleLabel; + ChatWidgetHeaderButton rightLabel; + + QMenu leftMenu; + QMenu rightMenu; + + void leftButtonClicked(); + void rightButtonClicked(); + + void menuAddSplit(); + void menuCloseSplit(); + void menuMoveSplit(); + void menuChangeChannel(); + void menuClearChat(); + void menuOpenChannel(); + void menuPopupPlayer(); + void menuReloadChannelEmotes(); + void menuManualReconnect(); + void menuShowChangelog(); }; #endif // CHATWIDGETHEADER_H diff --git a/chatwidgetheaderbutton.cpp b/chatwidgetheaderbutton.cpp new file mode 100644 index 00000000..e6ce5f94 --- /dev/null +++ b/chatwidgetheaderbutton.cpp @@ -0,0 +1,99 @@ +#include "chatwidgetheaderbutton.h" +#include "colorscheme.h" + +#include +#include + +ChatWidgetHeaderButton::ChatWidgetHeaderButton() + : QWidget() + , hbox() + , m_label() + , m_mouseOver(false) + , m_mouseDown(false) +{ + setLayout(&hbox); + + hbox.setMargin(0); + hbox.addSpacing(6); + hbox.addWidget(&m_label); + hbox.addSpacing(6); + + QObject::connect(&m_label, ChatWidgetHeaderButtonLabel::mouseUp, this, + labelMouseUp); + QObject::connect(&m_label, ChatWidgetHeaderButtonLabel::mouseDown, this, + labelMouseDown); +} + +void +ChatWidgetHeaderButton::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + + QBrush brush(ColorScheme::instance().IsLightTheme + ? QColor(0, 0, 0, 32) + : QColor(255, 255, 255, 32)); + + if (m_mouseDown) { + painter.fillRect(rect(), brush); + } + + if (m_mouseOver) { + painter.fillRect(rect(), brush); + } +} + +void +ChatWidgetHeaderButton::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + m_mouseDown = true; + + repaint(); + } +} + +void +ChatWidgetHeaderButton::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + m_mouseDown = false; + + repaint(); + + emit clicked(); + } +} + +void +ChatWidgetHeaderButton::enterEvent(QEvent *) +{ + m_mouseOver = true; + + repaint(); +} + +void +ChatWidgetHeaderButton::leaveEvent(QEvent *) +{ + m_mouseOver = false; + + repaint(); +} + +void +ChatWidgetHeaderButton::labelMouseUp() +{ + m_mouseDown = false; + + repaint(); + + emit clicked(); +} + +void +ChatWidgetHeaderButton::labelMouseDown() +{ + m_mouseDown = true; + + repaint(); +} diff --git a/chatwidgetheaderbutton.h b/chatwidgetheaderbutton.h new file mode 100644 index 00000000..759f709f --- /dev/null +++ b/chatwidgetheaderbutton.h @@ -0,0 +1,47 @@ +#ifndef CHATWIDGETHEADERBUTTON_H +#define CHATWIDGETHEADERBUTTON_H + +#include "chatwidgetheaderbuttonlabel.h" + +#include +#include +#include +#include + +class ChatWidgetHeaderButton : public QWidget +{ + Q_OBJECT + +public: + ChatWidgetHeaderButton(); + + ChatWidgetHeaderButtonLabel & + label() + { + return m_label; + } + +signals: + void clicked(); + +protected: + void paintEvent(QPaintEvent *) Q_DECL_OVERRIDE; + + void enterEvent(QEvent *) Q_DECL_OVERRIDE; + void leaveEvent(QEvent *) Q_DECL_OVERRIDE; + + void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE; + void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE; + +private: + QHBoxLayout hbox; + ChatWidgetHeaderButtonLabel m_label; + + bool m_mouseOver; + bool m_mouseDown; + + void labelMouseUp(); + void labelMouseDown(); +}; + +#endif // CHATWIDGETHEADERBUTTON_H diff --git a/chatwidgetheaderbuttonlabel.cpp b/chatwidgetheaderbuttonlabel.cpp new file mode 100644 index 00000000..d43bb40b --- /dev/null +++ b/chatwidgetheaderbuttonlabel.cpp @@ -0,0 +1,21 @@ +#include "chatwidgetheaderbuttonlabel.h" + +ChatWidgetHeaderButtonLabel::ChatWidgetHeaderButtonLabel() +{ +} + +void +ChatWidgetHeaderButtonLabel::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + emit mouseDown(); + } +} + +void +ChatWidgetHeaderButtonLabel::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + emit mouseUp(); + } +} diff --git a/chatwidgetheaderbuttonlabel.h b/chatwidgetheaderbuttonlabel.h new file mode 100644 index 00000000..c053ea44 --- /dev/null +++ b/chatwidgetheaderbuttonlabel.h @@ -0,0 +1,23 @@ +#ifndef CHATWIDGETHEADERBUTTONLABEL_H +#define CHATWIDGETHEADERBUTTONLABEL_H + +#include +#include + +class ChatWidgetHeaderButtonLabel : public QLabel +{ + Q_OBJECT + +public: + ChatWidgetHeaderButtonLabel(); + +signals: + void mouseDown(); + void mouseUp(); + +protected: + void mousePressEvent(QMouseEvent *event); + void mouseReleaseEvent(QMouseEvent *event); +}; + +#endif // CHATWIDGETHEADERBUTTONLABEL_H diff --git a/chatwidgetview.cpp b/chatwidgetview.cpp index 3405cf32..3a65a511 100644 --- a/chatwidgetview.cpp +++ b/chatwidgetview.cpp @@ -15,7 +15,7 @@ ChatWidgetView::ChatWidgetView() scroll->scrollTo(QPointF(0, 100)); - m_channel = Channel::getChannel("ian678"); + m_channel = Channel::getChannel("fourtf"); } void @@ -42,6 +42,8 @@ ChatWidgetView::paintEvent(QPaintEvent *) { QPainter painter(this); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + auto c = channel(); if (c == NULL) @@ -49,7 +51,7 @@ ChatWidgetView::paintEvent(QPaintEvent *) auto messages = c->getMessagesClone(); - int y = 32; + int y = 0; for (std::shared_ptr const &message : messages) { for (WordPart const &wordPart : message.get()->wordParts()) { @@ -77,8 +79,7 @@ ChatWidgetView::paintEvent(QPaintEvent *) painter.drawText( QRectF(wordPart.x(), wordPart.y() + y, 10000, 10000), - wordPart.getText(), - QTextOption(Qt::AlignLeft | Qt::AlignTop)); + wordPart.text(), QTextOption(Qt::AlignLeft | Qt::AlignTop)); } } diff --git a/emotes.cpp b/emotes.cpp index 17d75ceb..5509592c 100644 --- a/emotes.cpp +++ b/emotes.cpp @@ -13,6 +13,8 @@ ConcurrentMap Emotes::m_ffzChannelEmoteFromCaches; ConcurrentMap Emotes::m_twitchEmoteFromCache; ConcurrentMap Emotes::m_miscImageFromCache; +int Emotes::m_generation = 0; + Emotes::Emotes() { } diff --git a/emotes.h b/emotes.h index 87d911da..71564c8a 100644 --- a/emotes.h +++ b/emotes.h @@ -66,6 +66,18 @@ public: static LazyLoadedImage *getTwitchEmoteById(const QString &name, long int id); + static int + generation() + { + return m_generation; + } + + static void + incGeneration() + { + m_generation++; + } + private: Emotes(); @@ -83,6 +95,8 @@ private: static ConcurrentMap m_miscImageFromCache; static QString getTwitchEmoteLink(long id, qreal &scale); + + static int m_generation; }; #endif // EMOTES_H diff --git a/fonts.cpp b/fonts.cpp index 3f49b913..c85f8dc4 100644 --- a/fonts.cpp +++ b/fonts.cpp @@ -5,7 +5,7 @@ QFont *Fonts::medium = new QFont(DEFAULT_FONT, 14); QFont *Fonts::mediumBold = new QFont(DEFAULT_FONT, 14); QFont *Fonts::mediumItalic = new QFont(DEFAULT_FONT, 14); -QFont *Fonts::small = new QFont(DEFAULT_FONT, 12); +QFont *Fonts::small = new QFont(DEFAULT_FONT, 10); QFont *Fonts::large = new QFont(DEFAULT_FONT, 16); QFont *Fonts::veryLarge = new QFont(DEFAULT_FONT, 18); @@ -16,6 +16,8 @@ QFontMetrics *Fonts::metricsSmall = new QFontMetrics(*small); QFontMetrics *Fonts::metricsLarge = new QFontMetrics(*large); QFontMetrics *Fonts::metricsVeryLarge = new QFontMetrics(*veryLarge); +int Fonts::m_generation = 0; + Fonts::Fonts() { } diff --git a/fonts.h b/fonts.h index 37c4a5a1..737fb203 100644 --- a/fonts.h +++ b/fonts.h @@ -19,6 +19,18 @@ public: static QFont &getFont(Type type); static QFontMetrics &getFontMetrics(Type type); + static int + generation() + { + return m_generation; + } + + static void + incGeneration() + { + m_generation++; + } + private: Fonts(); @@ -35,6 +47,8 @@ private: static QFontMetrics *metricsSmall; static QFontMetrics *metricsLarge; static QFontMetrics *metricsVeryLarge; + + static int m_generation; }; #endif // FONTS_H diff --git a/images/button_ban.png b/images/button_ban.png new file mode 100644 index 0000000000000000000000000000000000000000..a983d1c8717f6bdb90feafbf206c7121437df87a GIT binary patch literal 8261 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 literal 0 HcmV?d00001 diff --git a/images/button_timeout.png b/images/button_timeout.png new file mode 100644 index 0000000000000000000000000000000000000000..a087ed1230cdc7e0633ce8750c3e52bd4e59bd57 GIT binary patch literal 8431 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 delta 213 zcmV;`04o2N0;B?vBnkm@Qb$4nuFf3kks%uj!vFvd!vV){sAQ2wAb&DL1Z(U7005Lp zL_t(IPo0sA4TK;NL@nuRpe;J32}@Xc!9m#P45gIMS8fzUg3Z0) z%RT2sQH5C<_KPBOg&iza?v+)K!y=h8+ivH8kaIRjz?Ve4A=gHD@Pe39nvS5kO#Btm z2setNickName("justinfan123"); c->setRealName("justinfan123"); c->sendRaw("JOIN #fourtf"); - c->sendRaw("JOIN #ian678"); - c->sendRaw("JOIN #nuuls"); c->sendCommand(IrcCommand::createCapability("REQ", "twitch.tv/commands")); c->sendCommand(IrcCommand::createCapability("REQ", "twitch.tv/tags")); diff --git a/lazyloadedimage.cpp b/lazyloadedimage.cpp index 83da3b5f..c6d8c396 100644 --- a/lazyloadedimage.cpp +++ b/lazyloadedimage.cpp @@ -1,6 +1,7 @@ #include "lazyloadedimage.h" #include "asyncexec.h" +#include "emotes.h" #include "ircmanager.h" #include @@ -56,6 +57,7 @@ LazyLoadedImage::loadImage() } m_pixmap = pixmap; + Emotes::incGeneration(); }); //})); } diff --git a/link.h b/link.h index 0109eabf..6539c824 100644 --- a/link.h +++ b/link.h @@ -11,6 +11,8 @@ public: Url, CloseCurrentSplit, UserInfo, + UserTimeout, + UserBan, InsertText, ShowMessage, }; diff --git a/main.cpp b/main.cpp index 082c6c8a..1d0baf45 100644 --- a/main.cpp +++ b/main.cpp @@ -3,6 +3,7 @@ #include "ircmanager.h" #include "mainwindow.h" #include "resources.h" +#include "windows.h" #include #include @@ -17,7 +18,7 @@ main(int argc, char *argv[]) ColorScheme::instance().setColors(0, -0.8); - MainWindow w; + MainWindow &w = Windows::mainWindow(); w.show(); Channel::addChannel("ian678"); diff --git a/message.cpp b/message.cpp index d868e0c5..b7643399 100644 --- a/message.cpp +++ b/message.cpp @@ -23,14 +23,14 @@ QRegularExpression *Message::cheerRegex = new QRegularExpression("cheer[1-9][0-9]*"); Message::Message(const QString &text) - : m_wordParts(new std::list()) + : m_wordParts(new std::vector()) { } Message::Message(const IrcPrivateMessage &ircMessage, const Channel &channel, bool enablePingSound, bool isReceivedWhisper, bool isSentWhisper, bool includeChannel) - : m_wordParts(new std::list()) + : m_wordParts(new std::vector()) { m_parseTime = std::chrono::system_clock::now(); @@ -70,6 +70,17 @@ Message::Message(const IrcPrivateMessage &ircMessage, const Channel &channel, ColorScheme::instance().SystemMessageColor, QString(), QString())); + // mod buttons + static QString buttonBanTooltip("Ban user"); + static QString buttonTimeoutTooltip("Timeout user"); + + words.push_back(Word(Resources::buttonBan(), Word::ButtonBan, QString(), + buttonBanTooltip, + Link(Link::UserBan, ircMessage.account()))); + words.push_back(Word(Resources::buttonTimeout(), Word::ButtonTimeout, + QString(), buttonTimeoutTooltip, + Link(Link::UserTimeout, ircMessage.account()))); + // badges iterator = tags.find("badges"); @@ -239,10 +250,8 @@ Message::Message(const IrcPrivateMessage &ircMessage, const Channel &channel, for (QString split : splits) { // twitch emote - if (currentTwitchEmote == twitchEmotes.end()) - break; - - if (currentTwitchEmote->first == i) { + if (currentTwitchEmote != twitchEmotes.end() && + currentTwitchEmote->first == i) { words.push_back(Word(currentTwitchEmote->second, Word::TwitchEmoteImage, currentTwitchEmote->second->name(), @@ -264,10 +273,10 @@ Message::Message(const IrcPrivateMessage &ircMessage, const Channel &channel, std::vector> parsed; Emojis::parseEmojis(parsed, split); - for (const std::tuple &tuple : parsed) { LazyLoadedImage *image = std::get<0>(tuple); - if (image == NULL) { + + if (image == NULL) { // is text QString string = std::get<1>(tuple); // cheers @@ -344,7 +353,7 @@ Message::Message(const IrcPrivateMessage &ircMessage, const Channel &channel, // bttv / ffz emotes LazyLoadedImage *bttvEmote; -#pragma message WARN("xD ignored emotes") +#pragma message WARN("ignored emotes") if (Emotes::bttvEmotes().tryGet(string, bttvEmote) || channel.bttvChannelEmotes().tryGet(string, bttvEmote) || Emotes::ffzEmotes().tryGet(string, bttvEmote) || @@ -364,6 +373,13 @@ Message::Message(const IrcPrivateMessage &ircMessage, const Channel &channel, words.push_back( Word(string, Word::Text, textColor, string, QString(), link.isEmpty() ? Link() : Link(Link::Url, link))); + } else { // is emoji + static QString emojiTooltip("Emoji"); + + words.push_back( + Word(image, Word::EmojiImage, image->name(), emojiTooltip)); + Word(image->name(), Word::EmojiText, textColor, image->name(), + emojiTooltip); } } @@ -390,12 +406,18 @@ Message::layout(int width, bool enableEmoteMargins) bool redraw = width != m_currentLayoutWidth || m_relayoutRequested; - if (m_recalculateImages || m_recalculateText) { + bool recalculateImages = m_emoteGeneration != Emotes::generation(); + bool recalculateText = m_fontGeneration != Fonts::generation(); + + if (recalculateImages || recalculateText) { + m_emoteGeneration = Emotes::generation(); + m_fontGeneration = Fonts::generation(); + redraw = true; for (auto &word : m_words) { if (word.isImage()) { - if (m_recalculateImages) { + if (recalculateImages) { auto &image = word.getImage(); qreal w = image.width(); @@ -413,16 +435,13 @@ Message::layout(int width, bool enableEmoteMargins) } } } else { - if (m_recalculateText) { + if (recalculateText) { QFontMetrics &metrics = word.getFontMetrics(); word.setSize(metrics.width(word.getText()), metrics.height()); } } } - - m_recalculateImages = false; - m_recalculateText = false; } if (!redraw) { @@ -434,20 +453,17 @@ Message::layout(int width, bool enableEmoteMargins) int right = width - MARGIN_RIGHT - MARGIN_LEFT; - std::list *parts = new std::list(); + std::vector *parts = new std::vector(); - auto lineStart = parts->begin(); + int lineStart = 0; int lineHeight = 0; + bool first = true; auto alignParts = [&lineStart, &lineHeight, &parts, this] { - for (auto it2 = lineStart; true; it2++) { - WordPart &wordPart2 = *it2; + for (int i = lineStart; i < parts->size(); i++) { + WordPart &wordPart2 = parts->at(i); wordPart2.setY(wordPart2.y() + lineHeight); - - if (it2 == parts->end()) { - break; - } } }; @@ -471,24 +487,70 @@ Message::layout(int width, bool enableEmoteMargins) } } + // word wrapping + if (word.isText() && word.width() + MARGIN_LEFT > right) { + alignParts(); + + y += lineHeight; + + const QString &text = word.getText(); + + int start = 0; + QFontMetrics &metrics = word.getFontMetrics(); + + int width = 0; + + std::vector &charWidths = word.characterWidthCache(); + + if (charWidths.size() == 0) { + charWidths.reserve(text.length()); + + 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); + + parts->push_back(WordPart(word, MARGIN_LEFT, y, width, + word.height(), mid, mid)); + + y += metrics.height(); + + start = i - 1; + + width = 0; + } + } + + QString mid(text.mid(start)); + width = metrics.width(mid); + + parts->push_back(WordPart(word, MARGIN_LEFT, y - word.height(), + width, word.height(), mid, mid)); + x = width + MARGIN_LEFT + spaceWidth; + + lineHeight = word.height(); + + lineStart = parts->size() - 1; + + first = false; + } + // fits in the line - if (x + word.width() + xOffset <= right) { + else if (first || x + word.width() + xOffset <= right) { parts->push_back( WordPart(word, x, y - word.height(), word.copyText())); x += word.width() + xOffset; - - if (word.hasTrailingSpace()) { - x += spaceWidth; - } + x += spaceWidth; lineHeight = std::max(word.height(), lineHeight); - } - // else if (word.isText() && word.getText().length() - // > 2) - // { - // } + first = false; + } // doesn't fit in the line else { @@ -499,15 +561,12 @@ Message::layout(int width, bool enableEmoteMargins) parts->push_back(WordPart(word, MARGIN_LEFT, y - word.height(), word.copyText())); - lineStart = parts->end(); + lineStart = parts->size() - 1; lineHeight = word.height(); - x = word.width(); - - if (word.hasTrailingSpace()) { - x += spaceWidth; - } + x = word.width() + MARGIN_LEFT; + x += spaceWidth; } } diff --git a/message.h b/message.h index 5d76195f..f9feb45f 100644 --- a/message.h +++ b/message.h @@ -60,7 +60,7 @@ public: return m_words; } - const std::list + const std::vector wordParts() const { return *m_wordParts; @@ -91,16 +91,6 @@ public: { m_relayoutRequested = true; } - void - requestTextRecalculation() - { - m_recalculateText = true; - } - void - requestImageRecalculation() - { - m_recalculateImages = true; - } private: static LazyLoadedImage *badgeStaff; @@ -126,12 +116,12 @@ private: int m_height = 0; std::vector m_words; - std::list *m_wordParts; + std::vector *m_wordParts; long m_currentLayoutWidth = -1; bool m_relayoutRequested = true; - bool m_recalculateText = true; - bool m_recalculateImages = true; + int m_fontGeneration = -1; + int m_emoteGeneration = -1; static QString matchLink(const QString &string); diff --git a/notebooktab.cpp b/notebooktab.cpp index e7504e07..d81150a7 100644 --- a/notebooktab.cpp +++ b/notebooktab.cpp @@ -1,49 +1,27 @@ #include "notebooktab.h" -#include #include "colorscheme.h" #include "notebook.h" +#include + NotebookTab::NotebookTab(Notebook *notebook) : QWidget(notebook) + , m_notebook(notebook) + , m_text("") + , m_selected(false) + , m_mouseOver(false) + , m_mouseDown(false) + , m_highlightStyle(HighlightNone) { - this->notebook = notebook; - text = ""; - calcSize(); setAcceptDrops(true); } -int -NotebookTab::getHighlightStyle() -{ - return highlightStyle; -} - -void -NotebookTab::setHighlightStyle(int style) -{ - highlightStyle = style; - repaint(); -} - -void -NotebookTab::setSelected(bool value) -{ - selected = value; - repaint(); -} - -bool -NotebookTab::getSelected() -{ - return selected; -} - void NotebookTab::calcSize() { - resize(fontMetrics().width(text) + 8, 24); + resize(fontMetrics().width(m_text) + 8, 24); } void @@ -55,16 +33,16 @@ NotebookTab::paintEvent(QPaintEvent *) auto colorScheme = ColorScheme::instance(); - if (selected) { + if (m_selected) { painter.fillRect(rect(), colorScheme.TabSelectedBackground); fg = colorScheme.TabSelectedText; - } else if (mouseOver) { + } else if (m_mouseOver) { painter.fillRect(rect(), colorScheme.TabHoverBackground); fg = colorScheme.TabHoverText; - } else if (highlightStyle == HighlightHighlighted) { + } else if (m_highlightStyle == HighlightHighlighted) { painter.fillRect(rect(), colorScheme.TabHighlightedBackground); fg = colorScheme.TabHighlightedText; - } else if (highlightStyle == HighlightNewMessage) { + } else if (m_highlightStyle == HighlightNewMessage) { painter.fillRect(rect(), colorScheme.TabNewMessageBackground); fg = colorScheme.TabHighlightedText; } else { @@ -73,23 +51,23 @@ NotebookTab::paintEvent(QPaintEvent *) } painter.setPen(fg); - painter.drawText(4, (height() + fontMetrics().height()) / 2, text); + painter.drawText(4, (height() + fontMetrics().height()) / 2, m_text); } void NotebookTab::mousePressEvent(QMouseEvent *) { - mouseDown = true; + m_mouseDown = true; repaint(); - notebook->select(page); + m_notebook->select(page); } void NotebookTab::mouseReleaseEvent(QMouseEvent *) { - mouseDown = false; + m_mouseDown = false; repaint(); } @@ -97,7 +75,7 @@ NotebookTab::mouseReleaseEvent(QMouseEvent *) void NotebookTab::enterEvent(QEvent *) { - mouseOver = true; + m_mouseOver = true; repaint(); } @@ -105,7 +83,7 @@ NotebookTab::enterEvent(QEvent *) void NotebookTab::leaveEvent(QEvent *) { - mouseOver = false; + m_mouseOver = false; repaint(); } @@ -113,5 +91,5 @@ NotebookTab::leaveEvent(QEvent *) void NotebookTab::dragEnterEvent(QDragEnterEvent *event) { - notebook->select(page); + m_notebook->select(page); } diff --git a/notebooktab.h b/notebooktab.h index 607bc986..98c8d790 100644 --- a/notebooktab.h +++ b/notebooktab.h @@ -1,7 +1,7 @@ #ifndef NOTEBOOKTAB_H #define NOTEBOOKTAB_H -#include "QWidget" +#include class Notebook; class NotebookPage; @@ -11,22 +11,55 @@ class NotebookTab : public QWidget Q_OBJECT public: - NotebookTab(Notebook *notebook); + enum HighlightStyle { + HighlightNone, + HighlightHighlighted, + HighlightNewMessage + }; + + NotebookTab(Notebook *m_notebook); void calcSize(); NotebookPage *page; - QString text; - bool getSelected(); - void setSelected(bool value); + const QString & + text() const + { + return m_text; + } - int getHighlightStyle(); - void setHighlightStyle(int style); + void + setText(const QString &text) + { + m_text = text; + } - static const int HighlightNone = 0; - static const int HighlightHighlighted = 1; - static const int HighlightNewMessage = 2; + bool + selected() + { + return m_selected; + } + + void + setSelected(bool value) + { + m_selected = value; + repaint(); + } + + HighlightStyle + highlightStyle() const + { + return m_highlightStyle; + } + + void + setHighlightStyle(HighlightStyle style) + { + m_highlightStyle = style; + repaint(); + } protected: void paintEvent(QPaintEvent *) Q_DECL_OVERRIDE; @@ -39,12 +72,14 @@ protected: void dragEnterEvent(QDragEnterEvent *event) Q_DECL_OVERRIDE; private: - Notebook *notebook; - bool selected = false; + Notebook *m_notebook; - bool mouseOver = false; - bool mouseDown = false; - int highlightStyle; + QString m_text; + + bool m_selected; + bool m_mouseOver; + bool m_mouseDown; + HighlightStyle m_highlightStyle; }; #endif // NOTEBOOKTAB_H diff --git a/resources.cpp b/resources.cpp index 30ac9b47..b18dd168 100644 --- a/resources.cpp +++ b/resources.cpp @@ -16,6 +16,9 @@ LazyLoadedImage *Resources::m_cheerBadge1000; LazyLoadedImage *Resources::m_cheerBadge100; LazyLoadedImage *Resources::m_cheerBadge1; +LazyLoadedImage *Resources::m_buttonBan; +LazyLoadedImage *Resources::m_buttonTimeout; + Resources::Resources() { } @@ -37,10 +40,17 @@ Resources::load() new LazyLoadedImage(new QPixmap(":/images/twitchprime_bg.png")); // cheer badges - m_cheerBadge100000 = new LazyLoadedImage(new QPixmap(":/cheer100000")); - m_cheerBadge10000 = new LazyLoadedImage(new QPixmap(":/cheer10000")); - m_cheerBadge5000 = new LazyLoadedImage(new QPixmap(":/cheer5000")); - m_cheerBadge1000 = new LazyLoadedImage(new QPixmap(":/cheer1000")); - m_cheerBadge100 = new LazyLoadedImage(new QPixmap(":/cheer100")); - m_cheerBadge1 = new LazyLoadedImage(new QPixmap(":/cheer1")); + m_cheerBadge100000 = + new LazyLoadedImage(new QPixmap(":/images/cheer100000")); + m_cheerBadge10000 = new LazyLoadedImage(new QPixmap(":/images/cheer10000")); + m_cheerBadge5000 = new LazyLoadedImage(new QPixmap(":/images/cheer5000")); + m_cheerBadge1000 = new LazyLoadedImage(new QPixmap(":/images/cheer1000")); + m_cheerBadge100 = new LazyLoadedImage(new QPixmap(":/images/cheer100")); + m_cheerBadge1 = new LazyLoadedImage(new QPixmap(":/images/cheer1")); + + // button + m_buttonBan = + new LazyLoadedImage(new QPixmap(":/images/button_ban.png"), 0.25); + m_buttonTimeout = + new LazyLoadedImage(new QPixmap(":/images/button_timeout.png"), 0.25); } diff --git a/resources.h b/resources.h index 94ffbdde..dbd32e8c 100644 --- a/resources.h +++ b/resources.h @@ -88,6 +88,18 @@ public: return m_cheerBadge1; } + static LazyLoadedImage * + buttonBan() + { + return m_buttonBan; + } + + static LazyLoadedImage * + buttonTimeout() + { + return m_buttonTimeout; + } + private: Resources(); @@ -105,6 +117,9 @@ private: static LazyLoadedImage *m_cheerBadge1000; static LazyLoadedImage *m_cheerBadge100; static LazyLoadedImage *m_cheerBadge1; + + static LazyLoadedImage *m_buttonBan; + static LazyLoadedImage *m_buttonTimeout; }; #endif // RESOURCES_H diff --git a/resources.qrc b/resources.qrc index 774b93bb..489d0d6d 100644 --- a/resources.qrc +++ b/resources.qrc @@ -25,5 +25,7 @@ images/staff_bg.png images/turbo_bg.png emojidata.txt + images/button_ban.png + images/button_timeout.png diff --git a/windows.cpp b/windows.cpp new file mode 100644 index 00000000..b2b4ad7a --- /dev/null +++ b/windows.cpp @@ -0,0 +1,10 @@ +#include "windows.h" + +QMutex Windows::m_windowMutex; + +MainWindow *Windows::m_mainWindow(NULL); + +void +Windows::invalidateEmotes() +{ +} diff --git a/windows.h b/windows.h new file mode 100644 index 00000000..0dd02226 --- /dev/null +++ b/windows.h @@ -0,0 +1,35 @@ +#ifndef WINDOWS_H +#define WINDOWS_H + +#include "mainwindow.h" + +#include + +class Windows +{ +public: + static void invalidateEmotes(); + + static MainWindow & + mainWindow() + { + m_windowMutex.lock(); + if (m_mainWindow == NULL) { + m_mainWindow = new MainWindow(); + } + m_windowMutex.unlock(); + + return *m_mainWindow; + } + +private: + Windows() + { + } + + static QMutex m_windowMutex; + + static MainWindow *m_mainWindow; +}; + +#endif // WINDOWS_H diff --git a/word.cpp b/word.cpp index b39d6dcd..f2c6c122 100644 --- a/word.cpp +++ b/word.cpp @@ -5,12 +5,13 @@ Word::Word(LazyLoadedImage *image, Type type, const QString ©text, const QString &tooltip, const Link &link) : m_image(image) , m_text() + , m_color() , m_isImage(true) , m_type(type) , m_copyText(copytext) , m_tooltip(tooltip) - , m_color() , m_link(link) + , m_characterWidthCache() { image->width(); // professional segfault test } @@ -20,11 +21,12 @@ Word::Word(const QString &text, Type type, const QColor &color, const QString ©text, const QString &tooltip, const Link &link) : m_image(NULL) , m_text(text) + , m_color(color) , m_isImage(false) , m_type(type) , m_copyText(copytext) , m_tooltip(tooltip) - , m_color(color) , m_link(link) + , m_characterWidthCache() { } diff --git a/word.h b/word.h index 7a54a399..bd2e94b1 100644 --- a/word.h +++ b/word.h @@ -49,9 +49,16 @@ public: Username = 0x800000, BitsAmount = 0x1000000, + ButtonBan = 0x2000000, + ButtonTimeout = 0x4000000, + + EmojiImage = 0x8000000, + EmojiText = 0x10000000, + Default = TimestampNoSeconds | Badges | Username | Bits | FfzEmoteImage | BttvEmoteImage | BttvGifEmoteImage | - TwitchEmoteImage | BitsAmount | Text + TwitchEmoteImage | BitsAmount | Text | ButtonBan | + ButtonTimeout }; explicit Word(LazyLoadedImage *m_image, Type type, const QString ©text, @@ -170,6 +177,12 @@ public: m_yOffset = std::max(0, yOffset); } + std::vector & + characterWidthCache() + { + return m_characterWidthCache; + } + private: LazyLoadedImage *m_image; QString m_text; @@ -188,6 +201,8 @@ private: bool m_hasTrailingSpace; Fonts::Type m_font = Fonts::Medium; Link m_link; + + std::vector m_characterWidthCache; }; #endif // WORD_H diff --git a/wordpart.cpp b/wordpart.cpp index e759a475..9317d792 100644 --- a/wordpart.cpp +++ b/wordpart.cpp @@ -10,11 +10,20 @@ WordPart::WordPart(Word &word, int x, int y, const QString ©Text, , m_width(word.width()) , m_height(word.height()) , m_trailingSpace(word.hasTrailingSpace() & allowTrailingSpace) + , m_text(word.isText() ? m_word.getText() : QString()) { } -const QString & -WordPart::getText() const +WordPart::WordPart(Word &word, int x, int y, int width, int height, + const QString ©Text, const QString &customText, + bool allowTrailingSpace) + : m_word(word) + , m_copyText(copyText) + , m_x(x) + , m_y(y) + , m_width(width) + , m_height(height) + , m_trailingSpace(word.hasTrailingSpace() & allowTrailingSpace) + , m_text(customText) { - return m_word.getText(); } diff --git a/wordpart.h b/wordpart.h index 5da733fd..bea7ef5f 100644 --- a/wordpart.h +++ b/wordpart.h @@ -12,6 +12,10 @@ public: WordPart(Word &word, int x, int y, const QString ©Text, bool allowTrailingSpace = true); + WordPart(Word &word, int x, int y, int width, int height, + const QString ©Text, const QString &customText, + bool allowTrailingSpace = true); + const Word & word() const { @@ -85,12 +89,17 @@ public: return m_trailingSpace; } - const QString &getText() const; + const QString & + text() const + { + return m_text; + } private: Word &m_word; QString m_copyText; + QString m_text; int m_x; int m_y;