From 305191d4b3f55738aa2db4a944a50b538101ed3b Mon Sep 17 00:00:00 2001 From: fourtf Date: Mon, 22 Jan 2018 20:52:32 +0100 Subject: [PATCH 01/30] fixed #177 user popup being off the screen --- src/widgets/accountpopup.cpp | 3 +- src/widgets/basewindow.cpp | 57 ++++++++++++++++++++++++++++-- src/widgets/basewindow.hpp | 8 +++++ src/widgets/helper/channelview.cpp | 2 +- src/widgets/split.cpp | 3 +- src/widgets/tooltipwidget.cpp | 40 +-------------------- src/widgets/tooltipwidget.hpp | 3 -- 7 files changed, 69 insertions(+), 47 deletions(-) diff --git a/src/widgets/accountpopup.cpp b/src/widgets/accountpopup.cpp index 78020a8cf..1c053653d 100644 --- a/src/widgets/accountpopup.cpp +++ b/src/widgets/accountpopup.cpp @@ -25,6 +25,8 @@ AccountPopupWidget::AccountPopupWidget(SharedChannel _channel) { this->ui->setupUi(this); + this->setStayInScreenRect(true); + this->layout()->setSizeConstraint(QLayout::SetFixedSize); this->setWindowFlags(Qt::FramelessWindowHint); @@ -49,7 +51,6 @@ AccountPopupWidget::AccountPopupWidget(SharedChannel _channel) this->loggedInUser.userID = currentTwitchUser->getUserId(); this->loggedInUser.refreshUserType(this->channel, true); - }); singletons::SettingManager &settings = singletons::SettingManager::getInstance(); diff --git a/src/widgets/basewindow.cpp b/src/widgets/basewindow.cpp index 54fbb780e..f27f5a8bf 100644 --- a/src/widgets/basewindow.cpp +++ b/src/widgets/basewindow.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #ifdef USEWINSDK @@ -113,6 +114,16 @@ void BaseWindow::init() } } +void BaseWindow::setStayInScreenRect(bool value) +{ + this->stayInScreenRect = value; +} + +bool BaseWindow::getStayInScreenRect() const +{ + return this->stayInScreenRect; +} + QWidget *BaseWindow::getLayoutContainer() { if (this->hasCustomWindowFrame()) { @@ -150,12 +161,54 @@ void BaseWindow::addTitleBarButton(const QString &text) void BaseWindow::changeEvent(QEvent *) { - // TooltipWidget::getInstance()->hide(); + TooltipWidget::getInstance()->hide(); } void BaseWindow::leaveEvent(QEvent *) { - // TooltipWidget::getInstance()->hide(); + TooltipWidget::getInstance()->hide(); +} + +void BaseWindow::moveTo(QWidget *parent, QPoint point) +{ + point.rx() += 16; + point.ry() += 16; + + this->move(point); + this->moveIntoDesktopRect(parent); +} + +void BaseWindow::resizeEvent(QResizeEvent *) +{ + this->moveIntoDesktopRect(this); +} + +void BaseWindow::moveIntoDesktopRect(QWidget *parent) +{ + if (!this->stayInScreenRect) + return; + + // move the widget into the screen geometry if it's not already in there + QDesktopWidget *desktop = QApplication::desktop(); + + QRect s = desktop->screenGeometry(parent); + QPoint p = this->pos(); + + if (p.x() < s.left()) { + p.setX(s.left()); + } + if (p.y() < s.top()) { + p.setY(s.top()); + } + if (p.x() + this->width() > s.right()) { + p.setX(s.right() - this->width()); + } + if (p.y() + this->height() > s.bottom()) { + p.setY(s.bottom() - this->height()); + } + + if (p != this->pos()) + this->move(p); } #ifdef USEWINSDK diff --git a/src/widgets/basewindow.hpp b/src/widgets/basewindow.hpp index 5e1374f9b..a0024b7f1 100644 --- a/src/widgets/basewindow.hpp +++ b/src/widgets/basewindow.hpp @@ -19,6 +19,11 @@ public: bool hasCustomWindowFrame(); void addTitleBarButton(const QString &text); + void setStayInScreenRect(bool value); + bool getStayInScreenRect() const; + + void moveTo(QWidget *widget, QPoint point); + protected: #ifdef USEWINSDK virtual void showEvent(QShowEvent *); @@ -28,13 +33,16 @@ protected: virtual void changeEvent(QEvent *) override; virtual void leaveEvent(QEvent *) override; + virtual void resizeEvent(QResizeEvent *) override; virtual void refreshTheme() override; private: void init(); + void moveIntoDesktopRect(QWidget *parent); bool enableCustomFrame; + bool stayInScreenRect = false; QHBoxLayout *titlebarBox; QWidget *titleLabel; diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index 586415d86..888254c88 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -787,7 +787,7 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) case messages::Link::UserInfo: { auto user = link.getValue(); this->userPopupWidget.setName(user); - this->userPopupWidget.move(event->screenPos().toPoint()); + this->userPopupWidget.moveTo(this, event->screenPos().toPoint()); this->userPopupWidget.show(); this->userPopupWidget.setFocus(); diff --git a/src/widgets/split.cpp b/src/widgets/split.cpp index 30085eae3..6f90a597a 100644 --- a/src/widgets/split.cpp +++ b/src/widgets/split.cpp @@ -460,6 +460,7 @@ void Split::doOpenViewerList() viewerDock->move(0, this->header.height()); auto accountPopup = new AccountPopupWidget(this->channel); + accountPopup->setAttribute(Qt::WA_DeleteOnClose); auto multiWidget = new QWidget(viewerDock); auto dockVbox = new QVBoxLayout(viewerDock); auto searchBar = new QLineEdit(viewerDock); @@ -538,9 +539,9 @@ void Split::doOpenViewerList() void Split::doOpenAccountPopupWidget(AccountPopupWidget *widget, QString user) { widget->setName(user); - widget->move(QCursor::pos()); widget->show(); widget->setFocus(); + widget->moveTo(this, QCursor::pos()); } void Split::doCopy() diff --git a/src/widgets/tooltipwidget.cpp b/src/widgets/tooltipwidget.cpp index dafe6511b..fa7e9c5dc 100644 --- a/src/widgets/tooltipwidget.cpp +++ b/src/widgets/tooltipwidget.cpp @@ -17,6 +17,7 @@ TooltipWidget::TooltipWidget(BaseWidget *parent) this->setStyleSheet("color: #fff; background: #000"); this->setWindowOpacity(0.8); this->updateFont(); + this->setStayInScreenRect(true); this->setAttribute(Qt::WA_ShowWithoutActivating); this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | @@ -55,45 +56,6 @@ void TooltipWidget::setText(QString text) this->displayText->setText(text); } -void TooltipWidget::moveTo(QWidget *parent, QPoint point) -{ - point.rx() += 16; - point.ry() += 16; - - this->move(point); - this->moveIntoDesktopRect(parent); -} - -void TooltipWidget::resizeEvent(QResizeEvent *) -{ - this->moveIntoDesktopRect(this); -} - -void TooltipWidget::moveIntoDesktopRect(QWidget *parent) -{ - QDesktopWidget *desktop = QApplication::desktop(); - - QRect s = desktop->screenGeometry(parent); - QPoint p = this->pos(); - - if (p.x() < s.left()) { - p.setX(s.left()); - } - if (p.y() < s.top()) { - p.setY(s.top()); - } - if (p.x() + this->width() > s.right()) { - p.setX(s.right() - this->width()); - } - if (p.y() + this->height() > s.bottom()) { - p.setY(s.bottom() - this->height()); - } - - if (p != this->pos()) { - this->move(p); - } -} - void TooltipWidget::changeEvent(QEvent *) { // clear parents event diff --git a/src/widgets/tooltipwidget.hpp b/src/widgets/tooltipwidget.hpp index 3b440dd3d..46fabbccb 100644 --- a/src/widgets/tooltipwidget.hpp +++ b/src/widgets/tooltipwidget.hpp @@ -16,7 +16,6 @@ public: ~TooltipWidget(); void setText(QString text); - void moveTo(QWidget *widget, QPoint point); static TooltipWidget *getInstance() { @@ -28,7 +27,6 @@ public: } protected: - virtual void resizeEvent(QResizeEvent *) override; virtual void changeEvent(QEvent *) override; virtual void leaveEvent(QEvent *) override; virtual void dpiMultiplierChanged(float, float) override; @@ -37,7 +35,6 @@ private: QLabel *displayText; pajlada::Signals::Connection fontChangedConnection; - void moveIntoDesktopRect(QWidget *parent); void updateFont(); }; From 06be94b9a6edb0f89317cb2dc78de3c6c4ee74bb Mon Sep 17 00:00:00 2001 From: fourtf Date: Mon, 22 Jan 2018 21:31:45 +0100 Subject: [PATCH 02/30] Fixes #179 tabs can't be repositioned --- src/widgets/helper/notebooktab.cpp | 8 ++++---- src/widgets/notebook.cpp | 8 ++++++-- src/widgets/notebook.hpp | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/widgets/helper/notebooktab.cpp b/src/widgets/helper/notebooktab.cpp index 38f215769..fc91dd4be 100644 --- a/src/widgets/helper/notebooktab.cpp +++ b/src/widgets/helper/notebooktab.cpp @@ -291,14 +291,14 @@ void NotebookTab::mouseMoveEvent(QMouseEvent *event) } } - if (this->mouseDown && !this->getDesiredRect().contains(event->pos())) { - QPoint relPoint = this->mapToParent(event->pos()); + QPoint relPoint = this->mapToParent(event->pos()); + if (this->mouseDown && !this->getDesiredRect().contains(relPoint)) { int index; - SplitContainer *clickedPage = notebook->tabAt(relPoint, index); + SplitContainer *clickedPage = notebook->tabAt(relPoint, index, this->width()); if (clickedPage != nullptr && clickedPage != this->page) { - this->notebook->rearrangePage(clickedPage, index); + this->notebook->rearrangePage(this->page, index); } } } diff --git a/src/widgets/notebook.cpp b/src/widgets/notebook.cpp index 09a815443..0f026a15a 100644 --- a/src/widgets/notebook.cpp +++ b/src/widgets/notebook.cpp @@ -151,12 +151,15 @@ int Notebook::tabCount() return this->pages.size(); } -SplitContainer *Notebook::tabAt(QPoint point, int &index) +SplitContainer *Notebook::tabAt(QPoint point, int &index, int maxWidth) { int i = 0; for (auto *page : this->pages) { - if (page->getTab()->getDesiredRect().contains(point)) { + QRect rect = page->getTab()->getDesiredRect(); + rect.setWidth(std::min(maxWidth, rect.width())); + + if (rect.contains(point)) { index = i; return page; } @@ -250,6 +253,7 @@ void Notebook::performLayout(bool animated) if (this->selectedPage != nullptr) { this->selectedPage->move(0, y + tabHeight); this->selectedPage->resize(width(), height() - y - tabHeight); + this->selectedPage->raise(); } } diff --git a/src/widgets/notebook.hpp b/src/widgets/notebook.hpp index b137beb79..4c24d19a8 100644 --- a/src/widgets/notebook.hpp +++ b/src/widgets/notebook.hpp @@ -41,7 +41,7 @@ public: void performLayout(bool animate = true); int tabCount(); - SplitContainer *tabAt(QPoint point, int &index); + SplitContainer *tabAt(QPoint point, int &index, int maxWidth = 2000000000); void rearrangePage(SplitContainer *page, int index); void nextTab(); From f39c0e9ee5b70fafe96ab69484237437efd7b603 Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 23 Jan 2018 14:51:54 +0100 Subject: [PATCH 03/30] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c369a43a4..c19ec2c10 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The code is normally formated using clang format in Qt Creator. [.clang-format]( To setup automatic code formating with QT Creator, see [this guide](https://gist.github.com/pajlada/0296454198eb8f8789fd6fe7ea660c5b). ## Building -Before building run `git submodule update --init --recursive` to get required submodules. In case you are new to using qt creator or c++ be sure to add -j to your make arguments as shown here [image](https://i.fourtf.com/GreenSweetImage.png) so it uses all your cpu cores to build. +Before building run `git submodule update --init --recursive` to get required submodules. ### Windows #### Using Qt Creator @@ -23,6 +23,7 @@ download the [boost library](https://sourceforge.net/projects/boost/files/boost/ #### Using MSYS2 Building using MSYS2 can be quite easier process. Check out MSYS2 at [msys2.org](http://www.msys2.org/). +Be sure to add "-j " as a make argument so it will use all your cpu cores to build. [example setup](https://i.imgur.com/qlESlS1.png) 1. open appropriate MSYS2 terminal and do `pacman -S mingw-w64--boost mingw-w64--qt5 mingw-w64--rapidjson` where `` is x86_64 or i686 2. go into the project directory 3. create build folder `mkdir build && cd build` From cb6af11b5aa56792772c5ea33627c7a07406dff9 Mon Sep 17 00:00:00 2001 From: Nikolai Zimmermann Date: Tue, 23 Jan 2018 13:34:26 +0100 Subject: [PATCH 04/30] Changed some multiplier from 1000 to 100 --- src/widgets/settingspages/appearancepage.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/settingspages/appearancepage.cpp b/src/widgets/settingspages/appearancepage.cpp index 1725c9ef8..3847479f9 100644 --- a/src/widgets/settingspages/appearancepage.cpp +++ b/src/widgets/settingspages/appearancepage.cpp @@ -77,7 +77,7 @@ QLayout *AppearancePage::createThemeColorChanger() // SLIDER QSlider *slider = new QSlider(Qt::Horizontal); layout->addWidget(slider); - slider->setValue(std::min(std::max(themeHue.getValue(), 0.0), 1.0) * 1000); + slider->setValue(std::min(std::max(themeHue.getValue(), 0.0), 1.0) * 100); // BUTTON QPushButton *button = new QPushButton; @@ -87,7 +87,7 @@ QLayout *AppearancePage::createThemeColorChanger() // SIGNALS QObject::connect(slider, &QSlider::valueChanged, this, [button, &themeHue](int value) mutable { - double newValue = value / 1000.0; + double newValue = value / 100.0; themeHue.setValue(newValue); From 91d45214d9a39050630cd3bf1f1545e2ea1d3955 Mon Sep 17 00:00:00 2001 From: fourtf Date: Mon, 22 Jan 2018 22:38:44 +0100 Subject: [PATCH 05/30] fixed emotes settings a little bit --- src/messages/messageelement.cpp | 193 ++++++++++++++++------------ src/messages/messageelement.hpp | 37 +++--- src/twitch/twitchmessagebuilder.cpp | 23 ++-- src/twitch/twitchmessagebuilder.hpp | 1 - 4 files changed, 134 insertions(+), 120 deletions(-) diff --git a/src/messages/messageelement.cpp b/src/messages/messageelement.cpp index c28532971..84c879a80 100644 --- a/src/messages/messageelement.cpp +++ b/src/messages/messageelement.cpp @@ -61,45 +61,65 @@ ImageElement::ImageElement(Image *_image, MessageElement::Flags flags) void ImageElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags _flags) { - QSize size(this->image->getWidth() * this->image->getScale() * container.scale, - this->image->getHeight() * this->image->getScale() * container.scale); + if (_flags & this->getFlags()) { + QSize size(this->image->getWidth() * this->image->getScale() * container.scale, + this->image->getHeight() * this->image->getScale() * container.scale); - container.addElement( - (new ImageLayoutElement(*this, this->image, size))->setLink(this->getLink())); + container.addElement( + (new ImageLayoutElement(*this, this->image, size))->setLink(this->getLink())); + } } // EMOTE EmoteElement::EmoteElement(const util::EmoteData &_data, MessageElement::Flags flags) : MessageElement(flags) , data(_data) + , textElement(nullptr) { if (_data.isValid()) { this->setTooltip(data.image1x->getTooltip()); + qDebug() << "valid xDDDDDDDDD" << _data.image1x->getName(); + this->textElement = new TextElement(_data.image1x->getName(), MessageElement::Misc); + } +} + +EmoteElement::~EmoteElement() +{ + if (this->textElement != nullptr) { + delete this->textElement; } } void EmoteElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags _flags) { - if (!this->data.isValid()) { - qDebug() << "EmoteElement::data is invalid xD"; - return; + if (_flags & this->getFlags()) { + if (_flags & this->getFlags() & MessageElement::EmoteImages) { + if (!this->data.isValid()) { + return; + } + + int quality = singletons::SettingManager::getInstance().preferredEmoteQuality; + + Image *_image; + if (quality == 3 && this->data.image3x != nullptr) { + _image = this->data.image3x; + } else if (quality >= 2 && this->data.image2x != nullptr) { + _image = this->data.image2x; + } else { + _image = this->data.image1x; + } + + QSize size((int)(container.scale * _image->getScaledWidth()), + (int)(container.scale * _image->getScaledHeight())); + + container.addElement( + (new ImageLayoutElement(*this, _image, size))->setLink(this->getLink())); + } else { + if (this->textElement != nullptr) { + this->textElement->addToContainer(container, MessageElement::Misc); + } + } } - - int quality = singletons::SettingManager::getInstance().preferredEmoteQuality; - - Image *_image; - if (quality == 3 && this->data.image3x != nullptr) { - _image = this->data.image3x; - } else if (quality >= 2 && this->data.image2x != nullptr) { - _image = this->data.image2x; - } else { - _image = this->data.image1x; - } - - QSize size((int)(container.scale * _image->getScaledWidth()), - (int)(container.scale * _image->getScaledHeight())); - - container.addElement((new ImageLayoutElement(*this, _image, size))->setLink(this->getLink())); } // TEXT @@ -117,75 +137,78 @@ TextElement::TextElement(const QString &text, MessageElement::Flags flags, void TextElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags _flags) { - QFontMetrics &metrics = - singletons::FontManager::getInstance().getFontMetrics(this->style, container.scale); - singletons::ThemeManager &themeManager = singletons::ThemeManager::ThemeManager::getInstance(); + if (_flags & this->getFlags()) { + QFontMetrics &metrics = + singletons::FontManager::getInstance().getFontMetrics(this->style, container.scale); + singletons::ThemeManager &themeManager = + singletons::ThemeManager::ThemeManager::getInstance(); - for (Word &word : this->words) { - auto getTextLayoutElement = [&](QString text, int width, bool trailingSpace) { - QColor color = this->color.getColor(themeManager); - themeManager.normalizeColor(color); + for (Word &word : this->words) { + auto getTextLayoutElement = [&](QString text, int width, bool trailingSpace) { + QColor color = this->color.getColor(themeManager); + themeManager.normalizeColor(color); - auto e = (new TextLayoutElement(*this, text, QSize(width, metrics.height()), color, - this->style, container.scale)) - ->setLink(this->getLink()); - e->setTrailingSpace(trailingSpace); - return e; - }; + auto e = (new TextLayoutElement(*this, text, QSize(width, metrics.height()), color, + this->style, container.scale)) + ->setLink(this->getLink()); + e->setTrailingSpace(trailingSpace); + return e; + }; - if (word.width == -1) { - word.width = metrics.width(word.text); - } - - // see if the text fits in the current line - if (container.fitsInLine(word.width)) { - container.addElementNoLineBreak( - getTextLayoutElement(word.text, word.width, this->hasTrailingSpace())); - continue; - } - - // see if the text fits in the next line - if (!container.atStartOfLine()) { - container.breakLine(); + if (word.width == -1) { + word.width = metrics.width(word.text); + } + // see if the text fits in the current line if (container.fitsInLine(word.width)) { container.addElementNoLineBreak( getTextLayoutElement(word.text, word.width, this->hasTrailingSpace())); continue; } - } - // we done goofed, we need to wrap the text - QString text = word.text; - int textLength = text.length(); - int wordStart = 0; - int width = metrics.width(text[0]); - int lastWidth = 0; - - for (int i = 1; i < textLength; i++) { - int charWidth = metrics.width(text[i]); - - if (!container.fitsInLine(width + charWidth)) { - container.addElementNoLineBreak(getTextLayoutElement( - text.mid(wordStart, i - wordStart), width - lastWidth, false)); + // see if the text fits in the next line + if (!container.atStartOfLine()) { container.breakLine(); - wordStart = i; - lastWidth = width; - width = 0; - if (textLength > i + 2) { - width += metrics.width(text[i]); - width += metrics.width(text[i + 1]); - i += 1; + if (container.fitsInLine(word.width)) { + container.addElementNoLineBreak( + getTextLayoutElement(word.text, word.width, this->hasTrailingSpace())); + continue; } - continue; } - width += charWidth; - } - container.addElement(getTextLayoutElement(text.mid(wordStart), word.width - lastWidth, - this->hasTrailingSpace())); - container.breakLine(); + // we done goofed, we need to wrap the text + QString text = word.text; + int textLength = text.length(); + int wordStart = 0; + int width = metrics.width(text[0]); + int lastWidth = 0; + + for (int i = 1; i < textLength; i++) { + int charWidth = metrics.width(text[i]); + + if (!container.fitsInLine(width + charWidth)) { + container.addElementNoLineBreak(getTextLayoutElement( + text.mid(wordStart, i - wordStart), width - lastWidth, false)); + container.breakLine(); + + wordStart = i; + lastWidth = width; + width = 0; + if (textLength > i + 2) { + width += metrics.width(text[i]); + width += metrics.width(text[i + 1]); + i += 1; + } + continue; + } + width += charWidth; + } + + container.addElement(getTextLayoutElement(text.mid(wordStart), word.width - lastWidth, + this->hasTrailingSpace())); + container.breakLine(); + } } } @@ -211,13 +234,15 @@ TimestampElement::~TimestampElement() void TimestampElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags _flags) { - if (singletons::SettingManager::getInstance().timestampFormat != this->format) { - this->format = singletons::SettingManager::getInstance().timestampFormat.getValue(); - delete this->element; - this->element = TimestampElement::formatTime(this->time); - } + if (_flags & this->getFlags()) { + if (singletons::SettingManager::getInstance().timestampFormat != this->format) { + this->format = singletons::SettingManager::getInstance().timestampFormat.getValue(); + delete this->element; + this->element = TimestampElement::formatTime(this->time); + } - this->element->addToContainer(container, _flags); + this->element->addToContainer(container, _flags); + } } TextElement *TimestampElement::formatTime(const QTime &time) @@ -239,8 +264,6 @@ TwitchModerationElement::TwitchModerationElement() void TwitchModerationElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags _flags) { - // qDebug() << _flags; - if (_flags & MessageElement::ModeratorTools) { QSize size((int)(container.scale * 16), (int)(container.scale * 16)); diff --git a/src/messages/messageelement.hpp b/src/messages/messageelement.hpp index dd3e7c482..b8e231412 100644 --- a/src/messages/messageelement.hpp +++ b/src/messages/messageelement.hpp @@ -145,20 +145,6 @@ public: MessageElement::Flags flags) override; }; -// contains emote data and will pick the emote based on : -// a) are images for the emote type enabled -// b) which size it wants -class EmoteElement : public MessageElement -{ - const util::EmoteData data; - -public: - EmoteElement(const util::EmoteData &data, MessageElement::Flags flags); - - virtual void addToContainer(MessageLayoutContainer &container, - MessageElement::Flags flags) override; -}; - // contains a text, it will split it into words class TextElement : public MessageElement { @@ -180,6 +166,22 @@ public: MessageElement::Flags flags) override; }; +// contains emote data and will pick the emote based on : +// a) are images for the emote type enabled +// b) which size it wants +class EmoteElement : public MessageElement +{ + const util::EmoteData data; + TextElement *textElement; + +public: + EmoteElement(const util::EmoteData &data, MessageElement::Flags flags); + ~EmoteElement(); + + virtual void addToContainer(MessageLayoutContainer &container, + MessageElement::Flags flags) override; +}; + // contains a text, formated depending on the preferences class TimestampElement : public MessageElement { @@ -208,12 +210,5 @@ public: virtual void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) override; }; - -// adds bits as text, static image or animated image -// class BitsElement : public MessageElement -//{ -// public: -// virtual void addToContainer(LayoutContainer &container) override; -//}; } // namespace messages } // namespace chatterino diff --git a/src/twitch/twitchmessagebuilder.cpp b/src/twitch/twitchmessagebuilder.cpp index e99a0db33..90a65832b 100644 --- a/src/twitch/twitchmessagebuilder.cpp +++ b/src/twitch/twitchmessagebuilder.cpp @@ -450,36 +450,33 @@ bool TwitchMessageBuilder::tryAppendEmote(QString &emoteString) singletons::EmoteManager &emoteManager = singletons::EmoteManager::getInstance(); util::EmoteData emoteData; + auto appendEmote = [=](MessageElement::Flags flags) { + this->emplace(emoteData, flags); + return true; + }; + if (emoteManager.bttvGlobalEmotes.tryGet(emoteString, emoteData)) { // BTTV Global Emote - return this->appendEmote(emoteData); + return appendEmote(MessageElement::BttvEmote); } else if (this->twitchChannel != nullptr && this->twitchChannel->bttvChannelEmotes->tryGet(emoteString, emoteData)) { // BTTV Channel Emote - return this->appendEmote(emoteData); + return appendEmote(MessageElement::BttvEmote); } else if (emoteManager.ffzGlobalEmotes.tryGet(emoteString, emoteData)) { // FFZ Global Emote - return this->appendEmote(emoteData); + return appendEmote(MessageElement::FfzEmote); } else if (this->twitchChannel != nullptr && this->twitchChannel->ffzChannelEmotes->tryGet(emoteString, emoteData)) { // FFZ Channel Emote - return this->appendEmote(emoteData); + return appendEmote(MessageElement::FfzEmote); } else if (emoteManager.getChatterinoEmotes().tryGet(emoteString, emoteData)) { // Chatterino Emote - return this->appendEmote(emoteData); + return appendEmote(MessageElement::Misc); } return false; } -bool TwitchMessageBuilder::appendEmote(const util::EmoteData &emoteData) -{ - this->emplace(emoteData, MessageElement::BttvEmote); - - // Perhaps check for ignored emotes here? - return true; -} - // fourtf: this is ugly // maybe put the individual badges into a map instead of this mess void TwitchMessageBuilder::parseTwitchBadges() diff --git a/src/twitch/twitchmessagebuilder.hpp b/src/twitch/twitchmessagebuilder.hpp index 80edc4f8f..30aa7e1f1 100644 --- a/src/twitch/twitchmessagebuilder.hpp +++ b/src/twitch/twitchmessagebuilder.hpp @@ -55,7 +55,6 @@ private: void appendTwitchEmote(const Communi::IrcPrivateMessage *ircMessage, const QString &emote, std::vector> &vec); bool tryAppendEmote(QString &emoteString); - bool appendEmote(const util::EmoteData &emoteData); void parseTwitchBadges(); void addChatterinoBadges(); From dd05ea28fe1530196edef605d8c3f2fdbfb65a67 Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 23 Jan 2018 21:33:49 +0100 Subject: [PATCH 06/30] added basic keyword ignore setting --- chatterino.pro | 8 ++- src/singletons/ircmanager.cpp | 4 +- src/singletons/settingsmanager.cpp | 20 ++++++++ src/singletons/settingsmanager.hpp | 7 +++ src/twitch/twitchchannel.cpp | 5 +- src/twitch/twitchmessagebuilder.cpp | 17 ++++++- src/twitch/twitchmessagebuilder.hpp | 1 + src/util/layoutcreator.hpp | 12 +++++ src/widgets/settingsdialog.cpp | 4 ++ .../settingspages/ignoremessagespage.cpp | 39 ++++++++++++++ .../settingspages/ignoremessagespage.hpp | 19 +++++++ src/widgets/settingspages/ignoreuserspage.cpp | 51 +++++++++++++++++++ src/widgets/settingspages/ignoreuserspage.hpp | 15 ++++++ 13 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 src/widgets/settingspages/ignoremessagespage.cpp create mode 100644 src/widgets/settingspages/ignoremessagespage.hpp create mode 100644 src/widgets/settingspages/ignoreuserspage.cpp create mode 100644 src/widgets/settingspages/ignoreuserspage.hpp diff --git a/chatterino.pro b/chatterino.pro index 18d67f250..81788de6e 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -163,7 +163,9 @@ SOURCES += \ src/widgets/basewindow.cpp \ src/singletons/helper/moderationaction.cpp \ src/widgets/streamview.cpp \ - src/util/networkrequest.cpp + src/util/networkrequest.cpp \ + src/widgets/settingspages/ignoreuserspage.cpp \ + src/widgets/settingspages/ignoremessagespage.cpp HEADERS += \ src/precompiled_header.hpp \ @@ -266,7 +268,9 @@ HEADERS += \ src/widgets/streamview.hpp \ src/util/networkrequest.hpp \ src/util/networkworker.hpp \ - src/util/networkrequester.hpp + src/util/networkrequester.hpp \ + src/widgets/settingspages/ignoreuserspage.hpp \ + src/widgets/settingspages/ignoremessagespage.hpp RESOURCES += \ resources/resources.qrc diff --git a/src/singletons/ircmanager.cpp b/src/singletons/ircmanager.cpp index d14530765..a82f5e586 100644 --- a/src/singletons/ircmanager.cpp +++ b/src/singletons/ircmanager.cpp @@ -255,7 +255,9 @@ void IrcManager::privateMessageReceived(Communi::IrcPrivateMessage *message) twitch::TwitchMessageBuilder builder(c.get(), message, args); - c->addMessage(builder.parse()); + if (!builder.isIgnored()) { + c->addMessage(builder.parse()); + } } void IrcManager::messageReceived(Communi::IrcMessage *message) diff --git a/src/singletons/settingsmanager.cpp b/src/singletons/settingsmanager.cpp index 0aa93c12a..87ab2a9d7 100644 --- a/src/singletons/settingsmanager.cpp +++ b/src/singletons/settingsmanager.cpp @@ -17,6 +17,7 @@ void _registerSetting(std::weak_ptr setting) SettingManager::SettingManager() : snapshot(nullptr) + , _ignoredKeywords(new std::vector) { this->wordFlagsListener.addSetting(this->showTimestamps); this->wordFlagsListener.addSetting(this->showBadges); @@ -29,6 +30,7 @@ SettingManager::SettingManager() }; this->moderationActions.connect([this](auto, auto) { this->updateModerationActions(); }); + this->ignoredKeywords.connect([this](auto, auto) { this->updateIgnoredKeywords(); }); } MessageElement::Flags SettingManager::getWordFlags() @@ -135,6 +137,11 @@ std::vector SettingManager::getModerationActions() const return this->_moderationActions; } +const std::shared_ptr> SettingManager::getIgnoredKeywords() const +{ + return this->_ignoredKeywords; +} + void SettingManager::updateModerationActions() { auto &resources = singletons::ResourceManager::getInstance(); @@ -202,5 +209,18 @@ void SettingManager::updateModerationActions() } } } + +void SettingManager::updateIgnoredKeywords() +{ + static QRegularExpression newLineRegex("(\r\n?|\n)+"); + + auto items = new std::vector(); + + for (QString line : this->ignoredKeywords.getValue().split(newLineRegex)) { + items->push_back(line); + } + + this->_ignoredKeywords = std::shared_ptr>(items); +} } // namespace singletons } // namespace chatterino diff --git a/src/singletons/settingsmanager.hpp b/src/singletons/settingsmanager.hpp index d87f32a70..986115a29 100644 --- a/src/singletons/settingsmanager.hpp +++ b/src/singletons/settingsmanager.hpp @@ -75,6 +75,10 @@ public: /// Links BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false}; + /// Ingored Users + BoolSetting enableTwitchIgnoredUsers = {"/ignore/enableTwitchIgnoredUsers", true}; + QStringSetting ignoredKeywords = {"/ignore/ignoredKeywords", ""}; + /// Moderation QStringSetting moderationActions = {"/moderation/actions", "/ban {user}\n/timeout {user} 300"}; @@ -107,6 +111,7 @@ public: void recallSnapshot(); std::vector getModerationActions() const; + const std::shared_ptr> getIgnoredKeywords() const; signals: void wordFlagsChanged(); @@ -114,10 +119,12 @@ signals: private: std::vector _moderationActions; std::unique_ptr snapshot; + std::shared_ptr> _ignoredKeywords; SettingManager(); void updateModerationActions(); + void updateIgnoredKeywords(); messages::MessageElement::Flags wordFlags = messages::MessageElement::Default; diff --git a/src/twitch/twitchchannel.cpp b/src/twitch/twitchchannel.cpp index 869912427..37f15c04b 100644 --- a/src/twitch/twitchchannel.cpp +++ b/src/twitch/twitchchannel.cpp @@ -230,7 +230,6 @@ void TwitchChannel::fetchRecentMessages() auto msgArray = obj.value("messages").toArray(); if (msgArray.size() > 0) { std::vector messages; - messages.resize(msgArray.size()); for (int i = 0; i < msgArray.size(); i++) { QByteArray content = msgArray[i].toString().toUtf8(); @@ -239,7 +238,9 @@ void TwitchChannel::fetchRecentMessages() messages::MessageParseArgs args; twitch::TwitchMessageBuilder builder(channel, privMsg, args); - messages.at(i) = builder.parse(); + if (!builder.isIgnored()) { + messages.push_back(builder.parse()); + } } channel->addMessagesAtStart(messages); } diff --git a/src/twitch/twitchmessagebuilder.cpp b/src/twitch/twitchmessagebuilder.cpp index 90a65832b..e7474ab50 100644 --- a/src/twitch/twitchmessagebuilder.cpp +++ b/src/twitch/twitchmessagebuilder.cpp @@ -27,6 +27,21 @@ TwitchMessageBuilder::TwitchMessageBuilder(Channel *_channel, , tags(this->ircMessage->tags()) , usernameColor(singletons::ThemeManager::getInstance().messages.textColors.system) { + this->originalMessage = this->ircMessage->content(); +} + +bool TwitchMessageBuilder::isIgnored() const +{ + singletons::SettingManager &settings = singletons::SettingManager::getInstance(); + std::shared_ptr> ignoredKeywords = settings.getIgnoredKeywords(); + + for (const QString &keyword : *ignoredKeywords) { + if (this->originalMessage.contains(keyword, Qt::CaseInsensitive)) { + return true; + } + } + + return false; } MessagePtr TwitchMessageBuilder::parse() @@ -34,8 +49,6 @@ MessagePtr TwitchMessageBuilder::parse() singletons::SettingManager &settings = singletons::SettingManager::getInstance(); singletons::EmoteManager &emoteManager = singletons::EmoteManager::getInstance(); - this->originalMessage = this->ircMessage->content(); - // PARSING this->parseUsername(); diff --git a/src/twitch/twitchmessagebuilder.hpp b/src/twitch/twitchmessagebuilder.hpp index 30aa7e1f1..180130297 100644 --- a/src/twitch/twitchmessagebuilder.hpp +++ b/src/twitch/twitchmessagebuilder.hpp @@ -38,6 +38,7 @@ public: QString messageID; QString userName; + bool isIgnored() const; messages::MessagePtr parse(); private: diff --git a/src/util/layoutcreator.hpp b/src/util/layoutcreator.hpp index 886886eeb..f28e569ed 100644 --- a/src/util/layoutcreator.hpp +++ b/src/util/layoutcreator.hpp @@ -47,6 +47,18 @@ public: return LayoutCreator(t); } + template ::value, int>::type = 0, + typename std::enable_if::value, int>::type = 0> + LayoutCreator setLayoutType() + { + T2 *layout = new T2; + + this->item->setLayout(layout); + + return LayoutCreator(layout); + } + LayoutCreator assign(T **ptr) { *ptr = this->item; diff --git a/src/widgets/settingsdialog.cpp b/src/widgets/settingsdialog.cpp index 9320e61a1..f6c62ee7f 100644 --- a/src/widgets/settingsdialog.cpp +++ b/src/widgets/settingsdialog.cpp @@ -8,6 +8,8 @@ #include "widgets/settingspages/commandpage.hpp" #include "widgets/settingspages/emotespage.hpp" #include "widgets/settingspages/highlightingpage.hpp" +#include "widgets/settingspages/ignoremessagespage.hpp" +#include "widgets/settingspages/ignoreuserspage.hpp" #include "widgets/settingspages/logspage.hpp" #include "widgets/settingspages/moderationpage.hpp" @@ -77,6 +79,8 @@ void SettingsDialog::addTabs() this->addTab(new settingspages::BehaviourPage); this->addTab(new settingspages::CommandPage); this->addTab(new settingspages::EmotesPage); + this->addTab(new settingspages::IgnoreUsersPage); + this->addTab(new settingspages::IgnoreMessagesPage); this->addTab(new settingspages::HighlightingPage); // this->addTab(new settingspages::LogsPage); this->addTab(new settingspages::ModerationPage); diff --git a/src/widgets/settingspages/ignoremessagespage.cpp b/src/widgets/settingspages/ignoremessagespage.cpp new file mode 100644 index 000000000..2b5ae4eb2 --- /dev/null +++ b/src/widgets/settingspages/ignoremessagespage.cpp @@ -0,0 +1,39 @@ +#include "ignoremessagespage.hpp" + +#include "util/layoutcreator.hpp" + +#include +#include + +namespace chatterino { +namespace widgets { +namespace settingspages { +IgnoreMessagesPage::IgnoreMessagesPage() + : SettingsPage("Ignore Messages", ":/images/theme.svg") +{ + singletons::SettingManager &settings = singletons::SettingManager::getInstance(); + util::LayoutCreator layoutCreator(this); + auto layout = layoutCreator.setLayoutType(); + + layout.emplace("Ignored keywords:"); + QTextEdit *textEdit = layout.emplace().getElement(); + + textEdit->setPlainText(settings.ignoredKeywords); + + QObject::connect(textEdit, &QTextEdit::textChanged, + [this] { this->keywordsUpdated.start(200); }); + + QObject::connect(&this->keywordsUpdated, &QTimer::timeout, [textEdit, &settings] { + QString text = textEdit->toPlainText(); + + settings.ignoredKeywords = text; + + qDebug() << "xD"; + }); + + // ---- misc + this->keywordsUpdated.setSingleShot(true); +} +} // namespace settingspages +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/settingspages/ignoremessagespage.hpp b/src/widgets/settingspages/ignoremessagespage.hpp new file mode 100644 index 000000000..f6cb3f772 --- /dev/null +++ b/src/widgets/settingspages/ignoremessagespage.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include "widgets/settingspages/settingspage.hpp" + +namespace chatterino { +namespace widgets { +namespace settingspages { + +class IgnoreMessagesPage : public SettingsPage +{ +public: + IgnoreMessagesPage(); + + QTimer keywordsUpdated; +}; +} // namespace settingspages +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/settingspages/ignoreuserspage.cpp b/src/widgets/settingspages/ignoreuserspage.cpp new file mode 100644 index 000000000..5999dd47d --- /dev/null +++ b/src/widgets/settingspages/ignoreuserspage.cpp @@ -0,0 +1,51 @@ +#include "ignoreuserspage.hpp" + +#include "singletons/settingsmanager.hpp" +#include "util/layoutcreator.hpp" + +#include +#include +#include +#include +#include + +// clang-format off +#define INFO "/ignore in chat ignores a user\n/unignore in chat unignores a user\n\nChatterino uses the twitch api for ignored users so they are shared with the webchat.\nIf you use your own oauth key make sure that it has the correct permissions." +// clang-format on + +namespace chatterino { +namespace widgets { +namespace settingspages { +IgnoreUsersPage::IgnoreUsersPage() + : SettingsPage("Ignore Users", ":/images/theme.svg") +{ + singletons::SettingManager &settings = singletons::SettingManager::getInstance(); + util::LayoutCreator layoutCreator(this); + auto layout = layoutCreator.setLayoutType().withoutMargin(); + + auto label = layout.emplace(INFO); + label->setWordWrap(true); + label->setStyleSheet("color: #BBB"); + + layout.append( + this->createCheckBox("Enable twitch ignored users", settings.enableTwitchIgnoredUsers)); + + auto anyways = layout.emplace().withoutMargin(); + { + anyways.emplace("Show anyways if:"); + anyways.emplace(); + anyways->addStretch(1); + } + + auto addremove = layout.emplace().withoutMargin(); + { + auto add = addremove.emplace("Ignore user"); + auto remove = addremove.emplace("Unignore User"); + addremove->addStretch(1); + } + + auto userList = layout.emplace(); +} +} // namespace settingspages +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/settingspages/ignoreuserspage.hpp b/src/widgets/settingspages/ignoreuserspage.hpp new file mode 100644 index 000000000..ab231eeaf --- /dev/null +++ b/src/widgets/settingspages/ignoreuserspage.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "widgets/settingspages/settingspage.hpp" + +namespace chatterino { +namespace widgets { +namespace settingspages { +class IgnoreUsersPage : public SettingsPage +{ +public: + IgnoreUsersPage(); +}; +} // namespace settingspages +} // namespace widgets +} // namespace chatterino From f292d2e097b55c2fdf72e4efeae9da771465e7d2 Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 23 Jan 2018 21:40:51 +0100 Subject: [PATCH 07/30] fixed live status being cached --- src/singletons/resourcemanager.cpp | 2 +- src/twitch/twitchchannel.cpp | 2 +- src/util/urlfetch.hpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/singletons/resourcemanager.cpp b/src/singletons/resourcemanager.cpp index 4383d8f14..6add6bc05 100644 --- a/src/singletons/resourcemanager.cpp +++ b/src/singletons/resourcemanager.cpp @@ -345,7 +345,7 @@ void ResourceManager::loadChannelData(const QString &roomID, bool bypassCache) QString cheermoteURL = "https://api.twitch.tv/kraken/bits/actions?channel_id=" + roomID; util::twitch::get2( - cheermoteURL, QThread::currentThread(), [this, roomID](const rapidjson::Document &d) { + cheermoteURL, QThread::currentThread(), true, [this, roomID](const rapidjson::Document &d) { ResourceManager::Channel &ch = this->channels[roomID]; ParseCheermoteSets(ch.jsonCheermoteSets, d); diff --git a/src/twitch/twitchchannel.cpp b/src/twitch/twitchchannel.cpp index 37f15c04b..ef998b88b 100644 --- a/src/twitch/twitchchannel.cpp +++ b/src/twitch/twitchchannel.cpp @@ -156,7 +156,7 @@ void TwitchChannel::refreshLiveStatus() std::weak_ptr weak = this->shared_from_this(); - util::twitch::get2(url, QThread::currentThread(), [weak](const rapidjson::Document &d) { + util::twitch::get2(url, QThread::currentThread(), false, [weak](const rapidjson::Document &d) { SharedChannel shared = weak.lock(); if (!shared) { diff --git a/src/util/urlfetch.hpp b/src/util/urlfetch.hpp index b590df3f6..cd83e7c2d 100644 --- a/src/util/urlfetch.hpp +++ b/src/util/urlfetch.hpp @@ -37,14 +37,14 @@ static void get(QString url, const QObject *caller, }); } -static void get2(QString url, const QObject *caller, +static void get2(QString url, const QObject *caller, bool useQuickLoadCache, std::function successCallback) { util::NetworkRequest req(url); req.setCaller(caller); req.setRawHeader("Client-ID", getDefaultClientID()); req.setRawHeader("Accept", "application/vnd.twitchtv.v5+json"); - req.setUseQuickLoadCache(true); + req.setUseQuickLoadCache(useQuickLoadCache); req.getJSON2([=](const rapidjson::Document &document) { successCallback(document); // From 8a77f918f61baea68cf3ff87eeedb460cb3ad5cc Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 23 Jan 2018 21:56:25 +0100 Subject: [PATCH 08/30] fixed timestamps not updating when changed in the settings --- src/messages/layouts/messagelayout.cpp | 6 ++++++ src/messages/layouts/messagelayout.hpp | 1 + src/singletons/settingsmanager.cpp | 12 ++++++++++-- src/widgets/settingspages/ignoremessagespage.cpp | 2 -- src/widgets/settingspages/ignoreuserspage.cpp | 2 +- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/messages/layouts/messagelayout.cpp b/src/messages/layouts/messagelayout.cpp index 489dc1724..475076c0d 100644 --- a/src/messages/layouts/messagelayout.cpp +++ b/src/messages/layouts/messagelayout.cpp @@ -89,6 +89,12 @@ bool MessageLayout::layout(int width, float scale, MessageElement::Flags flags) layoutRequired |= wordMaskChanged; this->currentWordFlags = flags; // singletons::SettingManager::getInstance().getWordTypeMask(); + // check if timestamp format changed + bool timestampFormatChanged = + this->timestampFormat != singletons::SettingManager::getInstance().timestampFormat; + + layoutRequired |= timestampFormatChanged; + // check if dpi changed bool scaleChanged = this->scale != scale; layoutRequired |= scaleChanged; diff --git a/src/messages/layouts/messagelayout.hpp b/src/messages/layouts/messagelayout.hpp index 8772e3764..52f1bf73e 100644 --- a/src/messages/layouts/messagelayout.hpp +++ b/src/messages/layouts/messagelayout.hpp @@ -67,6 +67,7 @@ private: int currentLayoutWidth = -1; int fontGeneration = -1; int emoteGeneration = -1; + QString timestampFormat; float scale = -1; unsigned int bufferUpdatedCount = 0; diff --git a/src/singletons/settingsmanager.cpp b/src/singletons/settingsmanager.cpp index 87ab2a9d7..9c697b6e0 100644 --- a/src/singletons/settingsmanager.cpp +++ b/src/singletons/settingsmanager.cpp @@ -2,6 +2,7 @@ #include "debug/log.hpp" #include "singletons/pathmanager.hpp" #include "singletons/resourcemanager.hpp" +#include "singletons/windowmanager.hpp" using namespace chatterino::messages; @@ -31,6 +32,9 @@ SettingManager::SettingManager() this->moderationActions.connect([this](auto, auto) { this->updateModerationActions(); }); this->ignoredKeywords.connect([this](auto, auto) { this->updateIgnoredKeywords(); }); + + this->timestampFormat.connect( + [](auto, auto) { singletons::WindowManager::getInstance().layoutVisibleChatWidgets(); }); } MessageElement::Flags SettingManager::getWordFlags() @@ -216,8 +220,12 @@ void SettingManager::updateIgnoredKeywords() auto items = new std::vector(); - for (QString line : this->ignoredKeywords.getValue().split(newLineRegex)) { - items->push_back(line); + for (const QString &line : this->ignoredKeywords.getValue().split(newLineRegex)) { + QString line2 = line.trimmed(); + + if (!line2.isEmpty()) { + items->push_back(line2); + } } this->_ignoredKeywords = std::shared_ptr>(items); diff --git a/src/widgets/settingspages/ignoremessagespage.cpp b/src/widgets/settingspages/ignoremessagespage.cpp index 2b5ae4eb2..d52c5a29e 100644 --- a/src/widgets/settingspages/ignoremessagespage.cpp +++ b/src/widgets/settingspages/ignoremessagespage.cpp @@ -27,8 +27,6 @@ IgnoreMessagesPage::IgnoreMessagesPage() QString text = textEdit->toPlainText(); settings.ignoredKeywords = text; - - qDebug() << "xD"; }); // ---- misc diff --git a/src/widgets/settingspages/ignoreuserspage.cpp b/src/widgets/settingspages/ignoreuserspage.cpp index 5999dd47d..986fc4f8a 100644 --- a/src/widgets/settingspages/ignoreuserspage.cpp +++ b/src/widgets/settingspages/ignoreuserspage.cpp @@ -21,7 +21,7 @@ IgnoreUsersPage::IgnoreUsersPage() { singletons::SettingManager &settings = singletons::SettingManager::getInstance(); util::LayoutCreator layoutCreator(this); - auto layout = layoutCreator.setLayoutType().withoutMargin(); + auto layout = layoutCreator.setLayoutType(); auto label = layout.emplace(INFO); label->setWordWrap(true); From 418189d39c447772ef84d9c295d7cd8c7805b763 Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 23 Jan 2018 22:00:58 +0100 Subject: [PATCH 09/30] Fixes #230 appearence settings not updating --- src/messages/layouts/messagelayout.cpp | 4 ++-- src/messages/messageelement.cpp | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/messages/layouts/messagelayout.cpp b/src/messages/layouts/messagelayout.cpp index 475076c0d..31c69e0ca 100644 --- a/src/messages/layouts/messagelayout.cpp +++ b/src/messages/layouts/messagelayout.cpp @@ -104,11 +104,11 @@ bool MessageLayout::layout(int width, float scale, MessageElement::Flags flags) // update word sizes if needed if (imagesChanged) { - // fourtf: update images + // this->container.updateImages(); this->addFlags(MessageLayout::RequiresBufferUpdate); } if (textChanged) { - // fourtf: update text + // this->container.updateText(); this->addFlags(MessageLayout::RequiresBufferUpdate); } if (widthChanged || wordMaskChanged) { diff --git a/src/messages/messageelement.cpp b/src/messages/messageelement.cpp index 84c879a80..42976eea1 100644 --- a/src/messages/messageelement.cpp +++ b/src/messages/messageelement.cpp @@ -155,9 +155,10 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme return e; }; - if (word.width == -1) { - word.width = metrics.width(word.text); - } + // fourtf: add again + // if (word.width == -1) { + word.width = metrics.width(word.text); + // } // see if the text fits in the current line if (container.fitsInLine(word.width)) { From 0f4ec70bf30c09333312ef37a6471d1790073486 Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 23 Jan 2018 22:48:33 +0100 Subject: [PATCH 10/30] Fixes #53 last read message indicator --- src/messages/layouts/messagelayout.cpp | 13 +++++++++++- src/messages/layouts/messagelayout.hpp | 3 ++- src/widgets/helper/channelview.cpp | 22 +++++++++++++++++++- src/widgets/helper/channelview.hpp | 2 ++ src/widgets/settingspages/behaviourpage.cpp | 2 +- src/widgets/split.cpp | 5 +++++ src/widgets/split.hpp | 1 + src/widgets/splitcontainer.cpp | 2 +- src/widgets/splitcontainer.hpp | 2 +- src/widgets/window.cpp | 23 ++++++++++++++++++++- src/widgets/window.hpp | 1 + 11 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/messages/layouts/messagelayout.cpp b/src/messages/layouts/messagelayout.cpp index 31c69e0ca..aec1df3b1 100644 --- a/src/messages/layouts/messagelayout.cpp +++ b/src/messages/layouts/messagelayout.cpp @@ -145,7 +145,8 @@ void MessageLayout::actuallyLayout(int width, MessageElement::Flags flags) } // Painting -void MessageLayout::paint(QPainter &painter, int y, int messageIndex, Selection &selection) +void MessageLayout::paint(QPainter &painter, int y, int messageIndex, Selection &selection, + bool isLastReadMessage, bool isWindowFocused) { QPixmap *pixmap = this->buffer.get(); singletons::ThemeManager &themeManager = singletons::ThemeManager::getInstance(); @@ -180,6 +181,16 @@ void MessageLayout::paint(QPainter &painter, int y, int messageIndex, Selection // draw gif emotes this->container.paintAnimatedElements(painter, y); + // draw last read message line + if (isLastReadMessage) { + QColor color = isWindowFocused ? themeManager.tabs.selected.backgrounds.regular.color() + : themeManager.tabs.selected.backgrounds.unfocused.color(); + + QBrush brush = QBrush(color, Qt::VerPattern); + + painter.fillRect(0, y + this->container.getHeight() - 1, this->container.width, 1, brush); + } + this->bufferValid = true; } diff --git a/src/messages/layouts/messagelayout.hpp b/src/messages/layouts/messagelayout.hpp index 52f1bf73e..44143a552 100644 --- a/src/messages/layouts/messagelayout.hpp +++ b/src/messages/layouts/messagelayout.hpp @@ -41,7 +41,8 @@ public: bool layout(int width, float scale, MessageElement::Flags flags); // Painting - void paint(QPainter &painter, int y, int messageIndex, Selection &selection); + void paint(QPainter &painter, int y, int messageIndex, Selection &selection, + bool isLastReadMessage, bool isWindowFocused); void invalidateBuffer(); void deleteBuffer(); diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index 888254c88..85fa42069 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -96,6 +96,9 @@ ChannelView::ChannelView(BaseWidget *parent) auto e = new QResizeEvent(this->size(), this->size()); this->resizeEvent(e); delete e; + + singletons::SettingManager::getInstance().showLastMessageIndicator.connect( + [this](auto, auto) { this->update(); }, this->managedConnections); } ChannelView::~ChannelView() @@ -410,6 +413,17 @@ void ChannelView::pause(int msecTimeout) this->pauseTimeout.start(msecTimeout); } +void ChannelView::updateLastReadMessage() +{ + auto _snapshot = this->getMessagesSnapshot(); + + if (_snapshot.getLength() > 0) { + this->lastReadMessage = _snapshot[_snapshot.getLength() - 1]; + } + + this->update(); +} + void ChannelView::resizeEvent(QResizeEvent *) { this->scrollBar.resize(this->scrollBar.width(), height()); @@ -477,11 +491,17 @@ void ChannelView::drawMessages(QPainter &painter) (fmod(this->scrollBar.getCurrentValue(), 1))); messages::MessageLayout *end = nullptr; + bool windowFocused = this->window() == QApplication::activeWindow(); for (size_t i = start; i < messagesSnapshot.getLength(); ++i) { messages::MessageLayout *layout = messagesSnapshot[i].get(); - layout->paint(painter, y, i, this->selection); + bool isLastMessage = false; + if (singletons::SettingManager::getInstance().showLastMessageIndicator) { + isLastMessage = this->lastReadMessage.get() == layout; + } + + layout->paint(painter, y, i, this->selection, isLastMessage, windowFocused); y += layout->getHeight(); diff --git a/src/widgets/helper/channelview.hpp b/src/widgets/helper/channelview.hpp index ed222f595..2397cabe6 100644 --- a/src/widgets/helper/channelview.hpp +++ b/src/widgets/helper/channelview.hpp @@ -39,6 +39,7 @@ public: void setEnableScrollingToBottom(bool); bool getEnableScrollingToBottom() const; void pause(int msecTimeout); + void updateLastReadMessage(); void setChannel(SharedChannel channel); messages::LimitedQueueSnapshot getMessagesSnapshot(); @@ -72,6 +73,7 @@ private: bool messageWasAdded = false; bool paused = false; QTimer pauseTimeout; + messages::MessageLayoutPtr lastReadMessage; messages::LimitedQueueSnapshot snapshot; diff --git a/src/widgets/settingspages/behaviourpage.cpp b/src/widgets/settingspages/behaviourpage.cpp index f9a00c144..f9caa5e24 100644 --- a/src/widgets/settingspages/behaviourpage.cpp +++ b/src/widgets/settingspages/behaviourpage.cpp @@ -8,7 +8,7 @@ #define WINDOW_TOPMOST "Window always on top (requires restart)" #define INPUT_EMPTY "Hide input box when empty" -#define LAST_MSG "Show last read message indicator" +#define LAST_MSG "Show last read message indicator (marks the spot where you left the window)" #define PAUSE_HOVERING "When hovering" #define STREAMLINK_QUALITY "Choose", "Source", "High", "Medium", "Low", "Audio only" diff --git a/src/widgets/split.cpp b/src/widgets/split.cpp index 6f90a597a..19ac3fa5e 100644 --- a/src/widgets/split.cpp +++ b/src/widgets/split.cpp @@ -252,6 +252,11 @@ void Split::updateGifEmotes() this->view.queueUpdate(); } +void Split::updateLastReadMessage() +{ + this->view.updateLastReadMessage(); +} + void Split::giveFocus(Qt::FocusReason reason) { this->input.textInput.setFocus(reason); diff --git a/src/widgets/split.hpp b/src/widgets/split.hpp index 64100ab8b..5e5e12e31 100644 --- a/src/widgets/split.hpp +++ b/src/widgets/split.hpp @@ -70,6 +70,7 @@ public: bool hasFocus() const; void layoutMessages(); void updateGifEmotes(); + void updateLastReadMessage(); void drag(); diff --git a/src/widgets/splitcontainer.cpp b/src/widgets/splitcontainer.cpp index d085aa6d6..ee6ad7d56 100644 --- a/src/widgets/splitcontainer.cpp +++ b/src/widgets/splitcontainer.cpp @@ -143,7 +143,7 @@ void SplitContainer::addToLayout(Split *widget, std::pair position) this->refreshCurrentFocusCoordinates(); } -const std::vector &SplitContainer::getChatWidgets() const +const std::vector &SplitContainer::getSplits() const { return this->splits; } diff --git a/src/widgets/splitcontainer.hpp b/src/widgets/splitcontainer.hpp index 601eb95a2..bf07022f3 100644 --- a/src/widgets/splitcontainer.hpp +++ b/src/widgets/splitcontainer.hpp @@ -33,7 +33,7 @@ public: std::pair removeFromLayout(Split *widget); void addToLayout(Split *widget, std::pair position = std::pair(-1, -1)); - const std::vector &getChatWidgets() const; + const std::vector &getSplits() const; NotebookTab *getTab() const; void addChat(bool openChannelNameDialog = false, std::string chatUUID = std::string()); diff --git a/src/widgets/window.cpp b/src/widgets/window.cpp index cf87c21fb..8a5030430 100644 --- a/src/widgets/window.cpp +++ b/src/widgets/window.cpp @@ -107,7 +107,7 @@ void Window::repaintVisibleChatWidgets(Channel *channel) return; } - const std::vector &widgets = page->getChatWidgets(); + const std::vector &widgets = page->getSplits(); for (auto it = widgets.begin(); it != widgets.end(); ++it) { Split *widget = *it; @@ -140,6 +140,27 @@ void Window::closeEvent(QCloseEvent *) this->closed(); } +bool Window::event(QEvent *e) +{ + switch (e->type()) { + case QEvent::WindowActivate: + break; + + case QEvent::WindowDeactivate: { + auto page = this->notebook.getSelectedPage(); + + if (page != nullptr) { + std::vector splits = page->getSplits(); + + for (Split *split : splits) { + split->updateLastReadMessage(); + } + } + } break; + }; + return BaseWindow::event(e); +} + void Window::loadGeometry() { bool doSetGeometry = false; diff --git a/src/widgets/window.hpp b/src/widgets/window.hpp index 475eff728..33827dbe3 100644 --- a/src/widgets/window.hpp +++ b/src/widgets/window.hpp @@ -57,6 +57,7 @@ public: protected: virtual void closeEvent(QCloseEvent *event) override; + virtual bool event(QEvent *event) override; private: singletons::ThemeManager &themeManager; From f42d48860cb39071e1e55b0483a59b42b7003d5e Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 23 Jan 2018 22:51:15 +0100 Subject: [PATCH 11/30] fixes #249 --- src/messages/layouts/messagelayout.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/messages/layouts/messagelayout.cpp b/src/messages/layouts/messagelayout.cpp index aec1df3b1..c0f7efc15 100644 --- a/src/messages/layouts/messagelayout.cpp +++ b/src/messages/layouts/messagelayout.cpp @@ -171,7 +171,8 @@ void MessageLayout::paint(QPainter &painter, int y, int messageIndex, Selection } // draw on buffer - painter.drawPixmap(0, y, this->container.width, this->container.getHeight(), *pixmap); + painter.drawPixmap(0, y, *pixmap); + // painter.drawPixmap(0, y, this->container.width, this->container.getHeight(), *pixmap); // draw disabled if (this->message->hasFlags(Message::Disabled)) { From d741bf6df326e33adcb7c01c053c602e19135037 Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 23 Jan 2018 23:10:27 +0100 Subject: [PATCH 12/30] added SpecialChannelPage with a description for /mentions --- chatterino.pro | 6 ++- src/widgets/settingsdialog.cpp | 9 +++-- .../settingspages/ignoremessagespage.cpp | 2 +- src/widgets/settingspages/ignoreuserspage.cpp | 38 ++++++++++--------- src/widgets/settingspages/logspage.cpp | 2 +- .../settingspages/specialchannelspage.cpp | 28 ++++++++++++++ .../settingspages/specialchannelspage.hpp | 15 ++++++++ 7 files changed, 76 insertions(+), 24 deletions(-) create mode 100644 src/widgets/settingspages/specialchannelspage.cpp create mode 100644 src/widgets/settingspages/specialchannelspage.hpp diff --git a/chatterino.pro b/chatterino.pro index 81788de6e..788a26abc 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -165,7 +165,8 @@ SOURCES += \ src/widgets/streamview.cpp \ src/util/networkrequest.cpp \ src/widgets/settingspages/ignoreuserspage.cpp \ - src/widgets/settingspages/ignoremessagespage.cpp + src/widgets/settingspages/ignoremessagespage.cpp \ + src/widgets/settingspages/specialchannelspage.cpp HEADERS += \ src/precompiled_header.hpp \ @@ -270,7 +271,8 @@ HEADERS += \ src/util/networkworker.hpp \ src/util/networkrequester.hpp \ src/widgets/settingspages/ignoreuserspage.hpp \ - src/widgets/settingspages/ignoremessagespage.hpp + src/widgets/settingspages/ignoremessagespage.hpp \ + src/widgets/settingspages/specialchannelspage.hpp RESOURCES += \ resources/resources.qrc diff --git a/src/widgets/settingsdialog.cpp b/src/widgets/settingsdialog.cpp index f6c62ee7f..edd23fd07 100644 --- a/src/widgets/settingsdialog.cpp +++ b/src/widgets/settingsdialog.cpp @@ -12,6 +12,7 @@ #include "widgets/settingspages/ignoreuserspage.hpp" #include "widgets/settingspages/logspage.hpp" #include "widgets/settingspages/moderationpage.hpp" +#include "widgets/settingspages/specialchannelspage.hpp" #include @@ -79,11 +80,13 @@ void SettingsDialog::addTabs() this->addTab(new settingspages::BehaviourPage); this->addTab(new settingspages::CommandPage); this->addTab(new settingspages::EmotesPage); - this->addTab(new settingspages::IgnoreUsersPage); - this->addTab(new settingspages::IgnoreMessagesPage); this->addTab(new settingspages::HighlightingPage); - // this->addTab(new settingspages::LogsPage); + this->addTab(new settingspages::IgnoreMessagesPage); + this->addTab(new settingspages::IgnoreUsersPage); + this->addTab(new settingspages::LogsPage); this->addTab(new settingspages::ModerationPage); + this->addTab(new settingspages::SpecialChannelsPage); + this->ui.tabContainer->addStretch(1); this->addTab(new settingspages::AboutPage, Qt::AlignBottom); } diff --git a/src/widgets/settingspages/ignoremessagespage.cpp b/src/widgets/settingspages/ignoremessagespage.cpp index d52c5a29e..19f8b5518 100644 --- a/src/widgets/settingspages/ignoremessagespage.cpp +++ b/src/widgets/settingspages/ignoremessagespage.cpp @@ -9,7 +9,7 @@ namespace chatterino { namespace widgets { namespace settingspages { IgnoreMessagesPage::IgnoreMessagesPage() - : SettingsPage("Ignore Messages", ":/images/theme.svg") + : SettingsPage("Ignore Messages", "") { singletons::SettingManager &settings = singletons::SettingManager::getInstance(); util::LayoutCreator layoutCreator(this); diff --git a/src/widgets/settingspages/ignoreuserspage.cpp b/src/widgets/settingspages/ignoreuserspage.cpp index 986fc4f8a..bccef00ce 100644 --- a/src/widgets/settingspages/ignoreuserspage.cpp +++ b/src/widgets/settingspages/ignoreuserspage.cpp @@ -4,20 +4,21 @@ #include "util/layoutcreator.hpp" #include +#include #include #include #include #include // clang-format off -#define INFO "/ignore in chat ignores a user\n/unignore in chat unignores a user\n\nChatterino uses the twitch api for ignored users so they are shared with the webchat.\nIf you use your own oauth key make sure that it has the correct permissions." +#define INFO "/ignore in chat ignores a user\n/unignore in chat unignores a user\n\nChatterino uses the twitch api for ignored users so they are shared with the webchat.\nIf you use your own oauth key make sure that it has the correct permissions.\n" // clang-format on namespace chatterino { namespace widgets { namespace settingspages { IgnoreUsersPage::IgnoreUsersPage() - : SettingsPage("Ignore Users", ":/images/theme.svg") + : SettingsPage("Ignore Users", "") { singletons::SettingManager &settings = singletons::SettingManager::getInstance(); util::LayoutCreator layoutCreator(this); @@ -27,24 +28,27 @@ IgnoreUsersPage::IgnoreUsersPage() label->setWordWrap(true); label->setStyleSheet("color: #BBB"); - layout.append( - this->createCheckBox("Enable twitch ignored users", settings.enableTwitchIgnoredUsers)); - - auto anyways = layout.emplace().withoutMargin(); + auto group = layout.emplace("Ignored users").setLayoutType(); { - anyways.emplace("Show anyways if:"); - anyways.emplace(); - anyways->addStretch(1); - } + group.append( + this->createCheckBox("Enable twitch ignored users", settings.enableTwitchIgnoredUsers)); - auto addremove = layout.emplace().withoutMargin(); - { - auto add = addremove.emplace("Ignore user"); - auto remove = addremove.emplace("Unignore User"); - addremove->addStretch(1); - } + auto anyways = group.emplace().withoutMargin(); + { + anyways.emplace("Show anyways if:"); + anyways.emplace(); + anyways->addStretch(1); + } - auto userList = layout.emplace(); + auto addremove = group.emplace().withoutMargin(); + { + auto add = addremove.emplace("Ignore user"); + auto remove = addremove.emplace("Unignore User"); + addremove->addStretch(1); + } + + auto userList = group.emplace(); + } } } // namespace settingspages } // namespace widgets diff --git a/src/widgets/settingspages/logspage.cpp b/src/widgets/settingspages/logspage.cpp index 606278c4c..62adefc5b 100644 --- a/src/widgets/settingspages/logspage.cpp +++ b/src/widgets/settingspages/logspage.cpp @@ -4,7 +4,7 @@ namespace chatterino { namespace widgets { namespace settingspages { LogsPage::LogsPage() - : SettingsPage("Logs", ":/images/VSO_Link_blue_16x.png") + : SettingsPage("Logs", "") { } } // namespace settingspages diff --git a/src/widgets/settingspages/specialchannelspage.cpp b/src/widgets/settingspages/specialchannelspage.cpp new file mode 100644 index 000000000..6c3685c74 --- /dev/null +++ b/src/widgets/settingspages/specialchannelspage.cpp @@ -0,0 +1,28 @@ +#include "specialchannelspage.hpp" + +#include "singletons/settingsmanager.hpp" +#include "util/layoutcreator.hpp" + +#include +#include +#include + +namespace chatterino { +namespace widgets { +namespace settingspages { +SpecialChannelsPage::SpecialChannelsPage() + : SettingsPage("Special channels", "") +{ + util::LayoutCreator layoutCreator(this); + auto layout = layoutCreator.setLayoutType(); + + auto mentions = layout.emplace("Mentions channel").setLayoutType(); + { + mentions.emplace("Join /mentions to view your mentions."); + } + + layout->addStretch(1); +} +} // namespace settingspages +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/settingspages/specialchannelspage.hpp b/src/widgets/settingspages/specialchannelspage.hpp new file mode 100644 index 000000000..5c9645592 --- /dev/null +++ b/src/widgets/settingspages/specialchannelspage.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "widgets/settingspages/settingspage.hpp" + +namespace chatterino { +namespace widgets { +namespace settingspages { +class SpecialChannelsPage : public SettingsPage +{ +public: + SpecialChannelsPage(); +}; +} // namespace settingspages +} // namespace widgets +} // namespace chatterino From fa344deaf0258693e104f62c85bff0257d65ede1 Mon Sep 17 00:00:00 2001 From: fourtf Date: Tue, 23 Jan 2018 23:28:06 +0100 Subject: [PATCH 13/30] fixed #237 /mentions --- src/messages/message.hpp | 6 ++---- src/messages/messageparseargs.hpp | 1 - src/singletons/ircmanager.cpp | 7 ++++++- src/twitch/twitchchannel.cpp | 4 +++- src/twitch/twitchmessagebuilder.cpp | 30 ++++++++++++----------------- src/twitch/twitchmessagebuilder.hpp | 8 ++++---- src/widgets/helper/channelview.cpp | 3 +++ src/widgets/notebook.cpp | 3 +++ 8 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/messages/message.hpp b/src/messages/message.hpp index 1fa4b3ea0..ce6a6b691 100644 --- a/src/messages/message.hpp +++ b/src/messages/message.hpp @@ -11,10 +11,9 @@ namespace chatterino { namespace messages { -class Message; - -typedef std::shared_ptr MessagePtr; typedef uint16_t MessageFlagsType; +class Message; +typedef std::shared_ptr MessagePtr; class Message { @@ -86,6 +85,5 @@ private: std::vector> elements; }; - } // namespace messages } // namespace chatterino diff --git a/src/messages/messageparseargs.hpp b/src/messages/messageparseargs.hpp index f02f64415..d0d355d74 100644 --- a/src/messages/messageparseargs.hpp +++ b/src/messages/messageparseargs.hpp @@ -8,7 +8,6 @@ public: bool disablePingSoungs = false; bool isReceivedWhisper = false; bool isSentWhisper = false; - bool includeChannelName = false; }; } // namespace messages diff --git a/src/singletons/ircmanager.cpp b/src/singletons/ircmanager.cpp index a82f5e586..43f615632 100644 --- a/src/singletons/ircmanager.cpp +++ b/src/singletons/ircmanager.cpp @@ -256,7 +256,12 @@ void IrcManager::privateMessageReceived(Communi::IrcPrivateMessage *message) twitch::TwitchMessageBuilder builder(c.get(), message, args); if (!builder.isIgnored()) { - c->addMessage(builder.parse()); + messages::MessagePtr message = builder.build(); + if (message->hasFlags(messages::Message::Highlighted)) { + singletons::ChannelManager::getInstance().mentionsChannel->addMessage(message); + } + + c->addMessage(message); } } diff --git a/src/twitch/twitchchannel.cpp b/src/twitch/twitchchannel.cpp index ef998b88b..4377ed5d4 100644 --- a/src/twitch/twitchchannel.cpp +++ b/src/twitch/twitchchannel.cpp @@ -1,5 +1,7 @@ #include "twitchchannel.hpp" #include "debug/log.hpp" +#include "messages/message.hpp" +#include "singletons/channelmanager.hpp" #include "singletons/emotemanager.hpp" #include "singletons/ircmanager.hpp" #include "singletons/settingsmanager.hpp" @@ -239,7 +241,7 @@ void TwitchChannel::fetchRecentMessages() messages::MessageParseArgs args; twitch::TwitchMessageBuilder builder(channel, privMsg, args); if (!builder.isIgnored()) { - messages.push_back(builder.parse()); + messages.push_back(builder.build()); } } channel->addMessagesAtStart(messages); diff --git a/src/twitch/twitchmessagebuilder.cpp b/src/twitch/twitchmessagebuilder.cpp index e7474ab50..1c841ac5d 100644 --- a/src/twitch/twitchmessagebuilder.cpp +++ b/src/twitch/twitchmessagebuilder.cpp @@ -44,7 +44,7 @@ bool TwitchMessageBuilder::isIgnored() const return false; } -MessagePtr TwitchMessageBuilder::parse() +MessagePtr TwitchMessageBuilder::build() { singletons::SettingManager &settings = singletons::SettingManager::getInstance(); singletons::EmoteManager &emoteManager = singletons::EmoteManager::getInstance(); @@ -56,11 +56,14 @@ MessagePtr TwitchMessageBuilder::parse() // this->appendWord(Word(Resources::getInstance().badgeCollapsed, Word::Collapsed, QString(), // QString())); - // The timestamp is always appended to the builder - // Whether or not will be rendered is decided/checked later + // PARSING + this->parseMessageID(); - // Appends the correct timestamp if the message is a past message + this->parseRoomID(); + this->appendChannelName(); + + // timestamp bool isPastMsg = this->tags.contains("historical"); if (isPastMsg) { // This may be architecture dependent(datatype) @@ -71,20 +74,11 @@ MessagePtr TwitchMessageBuilder::parse() this->emplace(); } - this->parseMessageID(); - - this->parseRoomID(); - - // TIMESTAMP this->emplace(); - this->parseTwitchBadges(); + this->appendTwitchBadges(); - this->addChatterinoBadges(); - - if (this->args.includeChannelName) { - this->parseChannelName(); - } + this->appendChatterinoBadges(); this->appendUsername(); @@ -232,7 +226,7 @@ void TwitchMessageBuilder::parseRoomID() } } -void TwitchMessageBuilder::parseChannelName() +void TwitchMessageBuilder::appendChannelName() { QString channelName("#" + this->channel->name); Link link(Link::Url, this->channel->name + "\n" + this->messageID); @@ -492,7 +486,7 @@ bool TwitchMessageBuilder::tryAppendEmote(QString &emoteString) // fourtf: this is ugly // maybe put the individual badges into a map instead of this mess -void TwitchMessageBuilder::parseTwitchBadges() +void TwitchMessageBuilder::appendTwitchBadges() { singletons::ResourceManager &resourceManager = singletons::ResourceManager::getInstance(); const auto &channelResources = resourceManager.channels[this->roomID]; @@ -649,7 +643,7 @@ void TwitchMessageBuilder::parseTwitchBadges() } } -void TwitchMessageBuilder::addChatterinoBadges() +void TwitchMessageBuilder::appendChatterinoBadges() { auto &badges = singletons::ResourceManager::getInstance().chatterinoBadges; auto it = badges.find(this->userName.toStdString()); diff --git a/src/twitch/twitchmessagebuilder.hpp b/src/twitch/twitchmessagebuilder.hpp index 180130297..7b6ecde8f 100644 --- a/src/twitch/twitchmessagebuilder.hpp +++ b/src/twitch/twitchmessagebuilder.hpp @@ -39,7 +39,7 @@ public: QString userName; bool isIgnored() const; - messages::MessagePtr parse(); + messages::MessagePtr build(); private: QString roomID; @@ -48,7 +48,7 @@ private: void parseMessageID(); void parseRoomID(); - void parseChannelName(); + void appendChannelName(); void parseUsername(); void appendUsername(); void parseHighlights(); @@ -57,8 +57,8 @@ private: std::vector> &vec); bool tryAppendEmote(QString &emoteString); - void parseTwitchBadges(); - void addChatterinoBadges(); + void appendTwitchBadges(); + void appendChatterinoBadges(); bool tryParseCheermote(const QString &string); }; diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index 85fa42069..866c2a51c 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -456,6 +456,9 @@ messages::MessageElement::Flags ChannelView::getFlags() const if (split->getModerationMode()) { flags = (MessageElement::Flags)(flags | MessageElement::ModeratorTools); } + if (this->channel == singletons::ChannelManager::getInstance().mentionsChannel) { + flags = (MessageElement::Flags)(flags | MessageElement::ChannelName); + } } return flags; diff --git a/src/widgets/notebook.cpp b/src/widgets/notebook.cpp index 0f026a15a..e9b2248f1 100644 --- a/src/widgets/notebook.cpp +++ b/src/widgets/notebook.cpp @@ -130,6 +130,9 @@ void Notebook::select(SplitContainer *page) if (this->selectedPage != nullptr) { this->selectedPage->setHidden(true); this->selectedPage->getTab()->setSelected(false); + for (auto split : this->selectedPage->getSplits()) { + split->updateLastReadMessage(); + } } this->selectedPage = page; From 2b94c4cd3375178a367dc43616e41263075ede74 Mon Sep 17 00:00:00 2001 From: fourtf Date: Wed, 24 Jan 2018 13:15:41 +0100 Subject: [PATCH 14/30] renamed SharedChannel to ChannelPtr for consistency --- src/channel.hpp | 2 +- src/singletons/channelmanager.cpp | 10 +++++----- src/singletons/channelmanager.hpp | 14 +++++++------- src/singletons/commandmanager.cpp | 2 +- src/singletons/ircmanager.cpp | 4 ++-- src/twitch/twitchchannel.cpp | 4 ++-- src/widgets/accountpopup.cpp | 6 +++--- src/widgets/accountpopup.hpp | 8 ++++---- src/widgets/emotepopup.cpp | 6 +++--- src/widgets/emotepopup.hpp | 2 +- src/widgets/helper/channelview.cpp | 2 +- src/widgets/helper/channelview.hpp | 4 ++-- src/widgets/helper/searchpopup.cpp | 4 ++-- src/widgets/helper/splitheader.cpp | 4 ++-- src/widgets/split.cpp | 10 +++++----- src/widgets/split.hpp | 8 ++++---- src/widgets/streamview.cpp | 2 +- 17 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/channel.hpp b/src/channel.hpp index ac06a2a04..be3564979 100644 --- a/src/channel.hpp +++ b/src/channel.hpp @@ -64,6 +64,6 @@ private: // std::shared_ptr loggingChannel; }; -typedef std::shared_ptr SharedChannel; +typedef std::shared_ptr ChannelPtr; } // namespace chatterino diff --git a/src/singletons/channelmanager.cpp b/src/singletons/channelmanager.cpp index 893d788cc..b18a56ff7 100644 --- a/src/singletons/channelmanager.cpp +++ b/src/singletons/channelmanager.cpp @@ -19,11 +19,11 @@ ChannelManager::ChannelManager() { } -const std::vector ChannelManager::getItems() +const std::vector ChannelManager::getItems() { QMutexLocker locker(&this->channelsMutex); - std::vector items; + std::vector items; for (auto &item : this->twitchChannels.values()) { items.push_back(std::get<0>(item)); @@ -32,7 +32,7 @@ const std::vector ChannelManager::getItems() return items; } -SharedChannel ChannelManager::addTwitchChannel(const QString &rawChannelName) +ChannelPtr ChannelManager::addTwitchChannel(const QString &rawChannelName) { QString channelName = rawChannelName.toLower(); @@ -63,7 +63,7 @@ SharedChannel ChannelManager::addTwitchChannel(const QString &rawChannelName) return std::get<0>(it.value()); } -SharedChannel ChannelManager::getTwitchChannel(const QString &channel) +ChannelPtr ChannelManager::getTwitchChannel(const QString &channel) { QMutexLocker locker(&this->channelsMutex); @@ -128,7 +128,7 @@ const std::string &ChannelManager::getUserID(const std::string &username) return temporary; } -void ChannelManager::doOnAll(std::function func) +void ChannelManager::doOnAll(std::function func) { for (const auto &channel : this->twitchChannels) { func(std::get<0>(channel)); diff --git a/src/singletons/channelmanager.hpp b/src/singletons/channelmanager.hpp index 3fcf6b41c..89e3d257e 100644 --- a/src/singletons/channelmanager.hpp +++ b/src/singletons/channelmanager.hpp @@ -17,20 +17,20 @@ class ChannelManager public: static ChannelManager &getInstance(); - const std::vector getItems(); + const std::vector getItems(); - SharedChannel addTwitchChannel(const QString &channel); - SharedChannel getTwitchChannel(const QString &channel); + ChannelPtr addTwitchChannel(const QString &channel); + ChannelPtr getTwitchChannel(const QString &channel); void removeTwitchChannel(const QString &channel); const std::string &getUserID(const std::string &username); - void doOnAll(std::function func); + void doOnAll(std::function func); // Special channels - const SharedChannel whispersChannel; - const SharedChannel mentionsChannel; - const SharedChannel emptyChannel; + const ChannelPtr whispersChannel; + const ChannelPtr mentionsChannel; + const ChannelPtr emptyChannel; private: std::map usernameToID; diff --git a/src/singletons/commandmanager.cpp b/src/singletons/commandmanager.cpp index 40b070fab..a19060f3b 100644 --- a/src/singletons/commandmanager.cpp +++ b/src/singletons/commandmanager.cpp @@ -88,7 +88,7 @@ QStringList CommandManager::getCommands() return this->commandsStringList; } -QString CommandManager::execCommand(const QString &text, SharedChannel channel, +QString CommandManager::execCommand(const QString &text, ChannelPtr channel, bool dryRun) { QStringList words = text.split(' ', QString::SkipEmptyParts); diff --git a/src/singletons/ircmanager.cpp b/src/singletons/ircmanager.cpp index 43f615632..59f829ec9 100644 --- a/src/singletons/ircmanager.cpp +++ b/src/singletons/ircmanager.cpp @@ -391,7 +391,7 @@ void IrcManager::onConnected() MessagePtr msg = Message::createSystemMessage("connected to chat"); MessagePtr remsg = Message::createSystemMessage("reconnected to chat"); - this->channelManager.doOnAll([msg, remsg](SharedChannel channel) { + this->channelManager.doOnAll([msg, remsg](ChannelPtr channel) { assert(channel); LimitedQueueSnapshot snapshot = channel->getMessageSnapshot(); @@ -412,7 +412,7 @@ void IrcManager::onDisconnected() MessagePtr msg = Message::createSystemMessage("disconnected from chat"); msg->addFlags(Message::DisconnectedMessage); - this->channelManager.doOnAll([msg](SharedChannel channel) { + this->channelManager.doOnAll([msg](ChannelPtr channel) { assert(channel); channel->addMessage(msg); }); diff --git a/src/twitch/twitchchannel.cpp b/src/twitch/twitchchannel.cpp index 4377ed5d4..e308a1e22 100644 --- a/src/twitch/twitchchannel.cpp +++ b/src/twitch/twitchchannel.cpp @@ -159,7 +159,7 @@ void TwitchChannel::refreshLiveStatus() std::weak_ptr weak = this->shared_from_this(); util::twitch::get2(url, QThread::currentThread(), false, [weak](const rapidjson::Document &d) { - SharedChannel shared = weak.lock(); + ChannelPtr shared = weak.lock(); if (!shared) { return; @@ -220,7 +220,7 @@ void TwitchChannel::fetchRecentMessages() std::weak_ptr weak = this->shared_from_this(); util::twitch::get(genericURL.arg(roomID), QThread::currentThread(), [weak](QJsonObject obj) { - SharedChannel shared = weak.lock(); + ChannelPtr shared = weak.lock(); if (!shared) { return; diff --git a/src/widgets/accountpopup.cpp b/src/widgets/accountpopup.cpp index 1c053653d..e1a84d732 100644 --- a/src/widgets/accountpopup.cpp +++ b/src/widgets/accountpopup.cpp @@ -18,7 +18,7 @@ namespace chatterino { namespace widgets { -AccountPopupWidget::AccountPopupWidget(SharedChannel _channel) +AccountPopupWidget::AccountPopupWidget(ChannelPtr _channel) : BaseWindow() , ui(new Ui::AccountPopup) , channel(_channel) @@ -172,7 +172,7 @@ void AccountPopupWidget::setName(const QString &name) this->popupWidgetUser.refreshUserType(this->channel, false); } -void AccountPopupWidget::User::refreshUserType(const SharedChannel &channel, bool loggedInUser) +void AccountPopupWidget::User::refreshUserType(const ChannelPtr &channel, bool loggedInUser) { if (channel->name == this->username) { this->userType = UserType::Owner; @@ -183,7 +183,7 @@ void AccountPopupWidget::User::refreshUserType(const SharedChannel &channel, boo } } -void AccountPopupWidget::setChannel(SharedChannel _channel) +void AccountPopupWidget::setChannel(ChannelPtr _channel) { this->channel = _channel; } diff --git a/src/widgets/accountpopup.hpp b/src/widgets/accountpopup.hpp index e5bae7d1a..aeac473b4 100644 --- a/src/widgets/accountpopup.hpp +++ b/src/widgets/accountpopup.hpp @@ -23,10 +23,10 @@ class AccountPopupWidget : public BaseWindow { Q_OBJECT public: - AccountPopupWidget(SharedChannel _channel); + AccountPopupWidget(ChannelPtr _channel); void setName(const QString &name); - void setChannel(SharedChannel _channel); + void setChannel(ChannelPtr _channel); public slots: void actuallyRefreshButtons(); @@ -52,7 +52,7 @@ private: enum class UserType { User, Mod, Owner }; - SharedChannel channel; + ChannelPtr channel; QPixmap avatar; @@ -63,7 +63,7 @@ private: QString userID; UserType userType = UserType::User; - void refreshUserType(const SharedChannel &channel, bool loggedInUser); + void refreshUserType(const ChannelPtr &channel, bool loggedInUser); }; User loggedInUser; diff --git a/src/widgets/emotepopup.cpp b/src/widgets/emotepopup.cpp index 8e8ee1118..1fce82c50 100644 --- a/src/widgets/emotepopup.cpp +++ b/src/widgets/emotepopup.cpp @@ -32,7 +32,7 @@ EmotePopup::EmotePopup(singletons::ThemeManager &themeManager) this->loadEmojis(); } -void EmotePopup::loadChannel(SharedChannel _channel) +void EmotePopup::loadChannel(ChannelPtr _channel) { TwitchChannel *channel = dynamic_cast(_channel.get()); @@ -40,7 +40,7 @@ void EmotePopup::loadChannel(SharedChannel _channel) return; } - SharedChannel emoteChannel(new Channel("")); + ChannelPtr emoteChannel(new Channel("")); auto addEmotes = [&](util::EmoteMap &map, const QString &title, const QString &emoteDesc) { // TITLE @@ -81,7 +81,7 @@ void EmotePopup::loadEmojis() { util::EmoteMap &emojis = singletons::EmoteManager::getInstance().getEmojis(); - SharedChannel emojiChannel(new Channel("")); + ChannelPtr emojiChannel(new Channel("")); // title messages::MessageBuilder builder1; diff --git a/src/widgets/emotepopup.hpp b/src/widgets/emotepopup.hpp index d64f979d6..e6727dd74 100644 --- a/src/widgets/emotepopup.hpp +++ b/src/widgets/emotepopup.hpp @@ -12,7 +12,7 @@ class EmotePopup : public BaseWindow public: explicit EmotePopup(singletons::ThemeManager &); - void loadChannel(SharedChannel channel); + void loadChannel(ChannelPtr channel); void loadEmojis(); private: diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index 866c2a51c..e5296c88d 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -293,7 +293,7 @@ messages::LimitedQueueSnapshot ChannelView::getMessagesSnapsho return this->snapshot; } -void ChannelView::setChannel(SharedChannel newChannel) +void ChannelView::setChannel(ChannelPtr newChannel) { if (this->channel) { this->detachChannel(); diff --git a/src/widgets/helper/channelview.hpp b/src/widgets/helper/channelview.hpp index 2397cabe6..edc43c7cf 100644 --- a/src/widgets/helper/channelview.hpp +++ b/src/widgets/helper/channelview.hpp @@ -41,7 +41,7 @@ public: void pause(int msecTimeout); void updateLastReadMessage(); - void setChannel(SharedChannel channel); + void setChannel(ChannelPtr channel); messages::LimitedQueueSnapshot getMessagesSnapshot(); void layoutMessages(); @@ -84,7 +84,7 @@ private: void setSelection(const messages::SelectionItem &start, const messages::SelectionItem &end); messages::MessageElement::Flags getFlags() const; - SharedChannel channel; + ChannelPtr channel; Scrollbar scrollBar; RippleEffectLabel *goToBottom; diff --git a/src/widgets/helper/searchpopup.cpp b/src/widgets/helper/searchpopup.cpp index 048f00a5a..db1f7a65a 100644 --- a/src/widgets/helper/searchpopup.cpp +++ b/src/widgets/helper/searchpopup.cpp @@ -58,7 +58,7 @@ void SearchPopup::initLayout() } } -void SearchPopup::setChannel(SharedChannel channel) +void SearchPopup::setChannel(ChannelPtr channel) { this->snapshot = channel->getMessageSnapshot(); this->performSearch(); @@ -70,7 +70,7 @@ void SearchPopup::performSearch() { QString text = searchInput->text(); - SharedChannel channel(new Channel("search")); + ChannelPtr channel(new Channel("search")); for (size_t i = 0; i < this->snapshot.getLength(); i++) { messages::MessagePtr message = this->snapshot[i]; diff --git a/src/widgets/helper/splitheader.cpp b/src/widgets/helper/splitheader.cpp index 67c0ee355..1390845c0 100644 --- a/src/widgets/helper/splitheader.cpp +++ b/src/widgets/helper/splitheader.cpp @@ -93,7 +93,7 @@ void SplitHeader::addDropdownItems(RippleEffectButton *label) this->dropdownMenu.addSeparator(); #ifdef USEWEBENGINE this->dropdownMenu.addAction("Start watching", this, [this]{ - SharedChannel _channel = this->split->getChannel(); + ChannelPtr _channel = this->split->getChannel(); twitch::TwitchChannel *tc = dynamic_cast(_channel.get()); if (tc != nullptr) { @@ -180,7 +180,7 @@ void SplitHeader::updateModerationModeIcon() : resourceManager.moderationmode_disabled->getPixmap()); bool modButtonVisible = false; - SharedChannel channel = this->split->getChannel(); + ChannelPtr channel = this->split->getChannel(); twitch::TwitchChannel *tc = dynamic_cast(channel.get()); diff --git a/src/widgets/split.cpp b/src/widgets/split.cpp index 19ac3fa5e..d61fcebc2 100644 --- a/src/widgets/split.cpp +++ b/src/widgets/split.cpp @@ -132,17 +132,17 @@ const std::string &Split::getUUID() const return this->uuid; } -SharedChannel Split::getChannel() const +ChannelPtr Split::getChannel() const { return this->channel; } -SharedChannel &Split::getChannelRef() +ChannelPtr &Split::getChannelRef() { return this->channel; } -void Split::setChannel(SharedChannel _newChannel) +void Split::setChannel(ChannelPtr _newChannel) { this->view.setChannel(_newChannel); @@ -355,7 +355,7 @@ void Split::doClearChat() void Split::doOpenChannel() { - SharedChannel _channel = this->channel; + ChannelPtr _channel = this->channel; twitch::TwitchChannel *tc = dynamic_cast(_channel.get()); if (tc != nullptr) { @@ -365,7 +365,7 @@ void Split::doOpenChannel() void Split::doOpenPopupPlayer() { - SharedChannel _channel = this->channel; + ChannelPtr _channel = this->channel; twitch::TwitchChannel *tc = dynamic_cast(_channel.get()); if (tc != nullptr) { diff --git a/src/widgets/split.hpp b/src/widgets/split.hpp index 5e5e12e31..42ec4c69b 100644 --- a/src/widgets/split.hpp +++ b/src/widgets/split.hpp @@ -55,8 +55,8 @@ public: } const std::string &getUUID() const; - SharedChannel getChannel() const; - SharedChannel &getChannelRef(); + ChannelPtr getChannel() const; + ChannelPtr &getChannelRef(); void setFlexSizeX(double x); double getFlexSizeX(); void setFlexSizeY(double y); @@ -83,7 +83,7 @@ protected: private: SplitContainer &parentPage; - SharedChannel channel; + ChannelPtr channel; QVBoxLayout vbox; SplitHeader header; @@ -97,7 +97,7 @@ private: boost::signals2::connection channelIDChangedConnection; boost::signals2::connection usermodeChangedConnection; - void setChannel(SharedChannel newChannel); + void setChannel(ChannelPtr newChannel); void doOpenAccountPopupWidget(AccountPopupWidget *widget, QString user); void channelNameUpdated(const std::string &newChannelName); void handleModifiers(QEvent *event, Qt::KeyboardModifiers modifiers); diff --git a/src/widgets/streamview.cpp b/src/widgets/streamview.cpp index d3ffd0416..7ad017d6d 100644 --- a/src/widgets/streamview.cpp +++ b/src/widgets/streamview.cpp @@ -11,7 +11,7 @@ namespace chatterino { namespace widgets { -StreamView::StreamView(SharedChannel channel, QUrl url) +StreamView::StreamView(ChannelPtr channel, QUrl url) { util::LayoutCreator layoutCreator(this); From 36b010e04616867495aa239956cdb6b7e11592a9 Mon Sep 17 00:00:00 2001 From: fourtf Date: Wed, 24 Jan 2018 15:08:22 +0100 Subject: [PATCH 15/30] added custom window frame for windows --- src/singletons/windowmanager.cpp | 31 ++++++++ src/singletons/windowmanager.hpp | 3 + src/widgets/basewindow.cpp | 81 +++++++++++++------- src/widgets/basewindow.hpp | 14 ++-- src/widgets/helper/rippleeffectbutton.cpp | 2 + src/widgets/notebook.cpp | 26 +------ src/widgets/settingspages/appearancepage.cpp | 3 + src/widgets/settingspages/moderationpage.cpp | 28 +++++-- src/widgets/window.cpp | 9 ++- 9 files changed, 134 insertions(+), 63 deletions(-) diff --git a/src/singletons/windowmanager.cpp b/src/singletons/windowmanager.cpp index 5f0f85007..578879f07 100644 --- a/src/singletons/windowmanager.cpp +++ b/src/singletons/windowmanager.cpp @@ -2,6 +2,8 @@ #include "debug/log.hpp" #include "singletons/fontmanager.hpp" #include "singletons/thememanager.hpp" +#include "widgets/accountswitchpopupwidget.hpp" +#include "widgets/settingsdialog.hpp" #include @@ -14,6 +16,35 @@ WindowManager &WindowManager::getInstance() return instance; } +void WindowManager::showSettingsDialog() +{ + QTimer::singleShot(80, [] { widgets::SettingsDialog::showDialog(); }); +} + +void WindowManager::showAccountSelectPopup(QPoint point) +{ + // static QWidget *lastFocusedWidget = nullptr; + static widgets::AccountSwitchPopupWidget *w = new widgets::AccountSwitchPopupWidget(); + + if (w->hasFocus()) { + w->hide(); + // if (lastFocusedWidget) { + // lastFocusedWidget->setFocus(); + // } + return; + } + + // lastFocusedWidget = this->focusWidget(); + + w->refresh(); + + QPoint buttonPos = point; + w->move(buttonPos.x(), buttonPos.y()); + + w->show(); + w->setFocus(); +} + WindowManager::WindowManager(ThemeManager &_themeManager) : themeManager(_themeManager) { diff --git a/src/singletons/windowmanager.hpp b/src/singletons/windowmanager.hpp index a59284fea..8ff0a4604 100644 --- a/src/singletons/windowmanager.hpp +++ b/src/singletons/windowmanager.hpp @@ -14,6 +14,9 @@ class WindowManager public: static WindowManager &getInstance(); + void showSettingsDialog(); + void showAccountSelectPopup(QPoint point); + void initMainWindow(); void layoutVisibleChatWidgets(Channel *channel = nullptr); void repaintVisibleChatWidgets(Channel *channel = nullptr); diff --git a/src/widgets/basewindow.cpp b/src/widgets/basewindow.cpp index f27f5a8bf..d6ec127c4 100644 --- a/src/widgets/basewindow.cpp +++ b/src/widgets/basewindow.cpp @@ -59,16 +59,17 @@ void BaseWindow::init() // CUSTOM WINDOW FRAME QVBoxLayout *layout = new QVBoxLayout; layout->setMargin(1); + layout->setSpacing(0); this->setLayout(layout); { - QHBoxLayout *buttons = this->titlebarBox = new QHBoxLayout; - buttons->setMargin(0); - layout->addLayout(buttons); + QHBoxLayout *buttonLayout = this->titlebarBox = new QHBoxLayout; + buttonLayout->setMargin(0); + layout->addLayout(buttonLayout); // title - QLabel *titleLabel = new QLabel("Chatterino"); - buttons->addWidget(titleLabel); - this->titleLabel = titleLabel; + QLabel *title = new QLabel(" Chatterino"); + buttonLayout->addWidget(title); + this->titleLabel = title; // buttons RippleEffectLabel *min = new RippleEffectLabel; @@ -81,21 +82,31 @@ void BaseWindow::init() exit->setFixedSize(46, 30); exit->getLabel().setText("exit"); + QObject::connect(min, &RippleEffectLabel::clicked, this, [this] { + this->setWindowState(Qt::WindowMinimized | this->windowState()); + }); + QObject::connect(max, &RippleEffectLabel::clicked, this, [this] { + this->setWindowState(this->windowState() == Qt::WindowMaximized + ? Qt::WindowActive + : Qt::WindowMaximized); + }); + QObject::connect(exit, &RippleEffectLabel::clicked, this, [this] { this->close(); }); + this->minButton = min; this->maxButton = max; this->exitButton = exit; - this->widgets.push_back(min); - this->widgets.push_back(max); - this->widgets.push_back(exit); + this->buttons.push_back(min); + this->buttons.push_back(max); + this->buttons.push_back(exit); - buttons->addStretch(1); - buttons->addWidget(min); - buttons->addWidget(max); - buttons->addWidget(exit); + buttonLayout->addStretch(1); + buttonLayout->addWidget(min); + buttonLayout->addWidget(max); + buttonLayout->addWidget(exit); + buttonLayout->setSpacing(0); } this->layoutBase = new QWidget(this); - this->widgets.push_back(this->layoutBase); layout->addWidget(this->layoutBase); } @@ -136,8 +147,8 @@ QWidget *BaseWindow::getLayoutContainer() bool BaseWindow::hasCustomWindowFrame() { #ifdef Q_OS_WIN - // return this->enableCustomFrame; - return false; + return this->enableCustomFrame; +// return false; #else return false; #endif @@ -149,14 +160,19 @@ void BaseWindow::refreshTheme() palette.setColor(QPalette::Background, this->themeManager.windowBg); palette.setColor(QPalette::Foreground, this->themeManager.windowText); this->setPalette(palette); + + for (RippleEffectLabel *label : this->buttons) { + label->setMouseEffectColor(this->themeManager.windowText); + } } -void BaseWindow::addTitleBarButton(const QString &text) +void BaseWindow::addTitleBarButton(const QString &text, std::function onClicked) { RippleEffectLabel *label = new RippleEffectLabel; label->getLabel().setText(text); - this->widgets.push_back(label); + this->buttons.push_back(label); this->titlebarBox->insertWidget(2, label); + QObject::connect(label, &RippleEffectLabel::clicked, this, [onClicked] { onClicked(); }); } void BaseWindow::changeEvent(QEvent *) @@ -303,12 +319,16 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *r bool client = false; QPoint point(x - winrect.left, y - winrect.top); - for (QWidget *widget : this->widgets) { + for (QWidget *widget : this->buttons) { if (widget->geometry().contains(point)) { client = true; } } + if (this->layoutBase->geometry().contains(point)) { + client = true; + } + if (client) { *result = HTCLIENT; } else { @@ -325,9 +345,10 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *r break; } // end case WM_NCHITTEST case WM_CLOSE: { - if (this->enableCustomFrame) { - return close(); - } + // if (this->enableCustomFrame) { + // this->close(); + // } + return QWidget::nativeEvent(eventType, message, result); break; } default: @@ -335,9 +356,10 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *r } } -void BaseWindow::showEvent(QShowEvent *) +void BaseWindow::showEvent(QShowEvent *event) { - if (this->isVisible() && this->hasCustomWindowFrame()) { + if (!this->shown && this->isVisible() && this->hasCustomWindowFrame()) { + this->shown = true; SetWindowLongPtr((HWND)this->winId(), GWL_STYLE, WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX); @@ -347,6 +369,8 @@ void BaseWindow::showEvent(QShowEvent *) SetWindowPos((HWND)this->winId(), 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE); } + + BaseWidget::showEvent(event); } void BaseWindow::paintEvent(QPaintEvent *event) @@ -358,11 +382,16 @@ void BaseWindow::paintEvent(QPaintEvent *event) bool windowFocused = this->window() == QApplication::activeWindow(); + QLinearGradient gradient(0, 0, 10, 250); + gradient.setColorAt(1, this->themeManager.tabs.selected.backgrounds.unfocused.color()); + if (windowFocused) { - painter.setPen(this->themeManager.tabs.selected.backgrounds.regular.color()); + gradient.setColorAt(.4, this->themeManager.tabs.selected.backgrounds.regular.color()); } else { - painter.setPen(this->themeManager.tabs.selected.backgrounds.unfocused.color()); + gradient.setColorAt(.4, this->themeManager.tabs.selected.backgrounds.unfocused.color()); } + painter.setPen(QPen(QBrush(gradient), 1)); + painter.drawRect(0, 0, this->width() - 1, this->height() - 1); } } diff --git a/src/widgets/basewindow.hpp b/src/widgets/basewindow.hpp index a0024b7f1..16e2f2a2a 100644 --- a/src/widgets/basewindow.hpp +++ b/src/widgets/basewindow.hpp @@ -2,10 +2,13 @@ #include "basewidget.hpp" +#include + class QHBoxLayout; namespace chatterino { namespace widgets { +class RippleEffectLabel; class BaseWindow : public BaseWidget { @@ -17,7 +20,7 @@ public: QWidget *getLayoutContainer(); bool hasCustomWindowFrame(); - void addTitleBarButton(const QString &text); + void addTitleBarButton(const QString &text, std::function onClicked); void setStayInScreenRect(bool value); bool getStayInScreenRect() const; @@ -43,14 +46,15 @@ private: bool enableCustomFrame; bool stayInScreenRect = false; + bool shown = false; QHBoxLayout *titlebarBox; QWidget *titleLabel; - QWidget *minButton; - QWidget *maxButton; - QWidget *exitButton; + RippleEffectLabel *minButton; + RippleEffectLabel *maxButton; + RippleEffectLabel *exitButton; QWidget *layoutBase; - std::vector widgets; + std::vector buttons; }; } // namespace widgets } // namespace chatterino diff --git a/src/widgets/helper/rippleeffectbutton.cpp b/src/widgets/helper/rippleeffectbutton.cpp index a2c46c8fe..6ce8d243f 100644 --- a/src/widgets/helper/rippleeffectbutton.cpp +++ b/src/widgets/helper/rippleeffectbutton.cpp @@ -57,6 +57,8 @@ void RippleEffectButton::paintEvent(QPaintEvent *) void RippleEffectButton::fancyPaint(QPainter &painter) { + painter.setRenderHint(QPainter::HighQualityAntialiasing); + painter.setRenderHint(QPainter::Antialiasing); QColor c; if (this->mouseEffectColor) { diff --git a/src/widgets/notebook.cpp b/src/widgets/notebook.cpp index e9b2248f1..1849f6341 100644 --- a/src/widgets/notebook.cpp +++ b/src/widgets/notebook.cpp @@ -1,7 +1,7 @@ #include "widgets/notebook.hpp" #include "debug/log.hpp" #include "singletons/thememanager.hpp" -#include "widgets/accountswitchpopupwidget.hpp" +#include "singletons/windowmanager.hpp" #include "widgets/helper/notebookbutton.hpp" #include "widgets/helper/notebooktab.hpp" #include "widgets/settingsdialog.hpp" @@ -277,31 +277,13 @@ void Notebook::resizeEvent(QResizeEvent *) void Notebook::settingsButtonClicked() { - QTimer::singleShot(80, [this] { SettingsDialog::showDialog(); }); + singletons::WindowManager::getInstance().showSettingsDialog(); } void Notebook::usersButtonClicked() { - static QWidget *lastFocusedWidget = nullptr; - static AccountSwitchPopupWidget *w = new AccountSwitchPopupWidget(this); - - if (w->hasFocus()) { - w->hide(); - if (lastFocusedWidget) { - lastFocusedWidget->setFocus(); - } - return; - } - - lastFocusedWidget = this->focusWidget(); - - w->refresh(); - - QPoint buttonPos = this->userButton.rect().bottomRight(); - w->move(buttonPos.x(), buttonPos.y()); - - w->show(); - w->setFocus(); + singletons::WindowManager::getInstance().showAccountSelectPopup( + this->mapToGlobal(this->userButton.rect().bottomRight())); } void Notebook::addPageButtonClicked() diff --git a/src/widgets/settingspages/appearancepage.cpp b/src/widgets/settingspages/appearancepage.cpp index 3847479f9..db080d632 100644 --- a/src/widgets/settingspages/appearancepage.cpp +++ b/src/widgets/settingspages/appearancepage.cpp @@ -44,8 +44,10 @@ AppearancePage::AppearancePage() form->addRow("Font:", this->createFontChanger()); form->addRow("Tab bar:", this->createCheckBox(TAB_X, settings.hideTabX)); +#ifndef USEWINSDK form->addRow("", this->createCheckBox(TAB_PREF, settings.hidePreferencesButton)); form->addRow("", this->createCheckBox(TAB_USER, settings.hideUserButton)); +#endif form->addRow("Scrolling:", this->createCheckBox(SCROLL_SMOOTH, settings.enableSmoothScrolling)); form->addRow("", this->createCheckBox(SCROLL_NEWMSG, settings.enableSmoothScrollingNewMessages)); @@ -59,6 +61,7 @@ AppearancePage::AppearancePage() { tbox.emplace("timestamp format (a = am/pm):"); tbox.append(this->createComboBox({TIMESTAMP_FORMATS}, settings.timestampFormat)); + tbox->addStretch(1); } messages.append(this->createCheckBox("Show badges", settings.showBadges)); messages.append(this->createCheckBox("Seperate messages", settings.seperateMessages)); diff --git a/src/widgets/settingspages/moderationpage.cpp b/src/widgets/settingspages/moderationpage.cpp index 0ab611288..625661643 100644 --- a/src/widgets/settingspages/moderationpage.cpp +++ b/src/widgets/settingspages/moderationpage.cpp @@ -1,5 +1,6 @@ #include "moderationpage.hpp" +#include #include #include #include @@ -18,22 +19,33 @@ ModerationPage::ModerationPage() singletons::SettingManager &settings = singletons::SettingManager::getInstance(); util::LayoutCreator layoutCreator(this); - auto layout = layoutCreator.emplace().withoutMargin(); + auto layout = layoutCreator.setLayoutType(); { // clang-format off - auto label = layout.emplace("In channels that you moderate there is a button to enable moderation mode.\n\nOne action per line. {user} will be replaced with the username.\nExample `/timeout {user} 120`"); + auto label = layout.emplace("Click the moderation mod button () in a channel that you moderate to enable moderator mode.
"); label->setWordWrap(true); + label->setStyleSheet("color: #bbb"); // clang-format on - auto text = layout.emplace().getElement(); + auto modButtons = + layout.emplace("Custom moderator buttons").setLayoutType(); + { + auto label2 = + modButtons.emplace("One action per line. {user} will be replaced with the " + "username.
Example `/timeout {user} 120`
"); + label2->setWordWrap(true); - text->setPlainText(settings.moderationActions); + auto text = modButtons.emplace().getElement(); - QObject::connect(text, &QTextEdit::textChanged, this, - [this] { this->itemsChangedTimer.start(200); }); + text->setPlainText(settings.moderationActions); - QObject::connect(&this->itemsChangedTimer, &QTimer::timeout, this, - [text, &settings]() { settings.moderationActions = text->toPlainText(); }); + QObject::connect(text, &QTextEdit::textChanged, this, + [this] { this->itemsChangedTimer.start(200); }); + + QObject::connect(&this->itemsChangedTimer, &QTimer::timeout, this, [text, &settings]() { + settings.moderationActions = text->toPlainText(); + }); + } } // ---- misc diff --git a/src/widgets/window.cpp b/src/widgets/window.cpp index 8a5030430..8c27e3d46 100644 --- a/src/widgets/window.cpp +++ b/src/widgets/window.cpp @@ -4,6 +4,8 @@ #include "singletons/ircmanager.hpp" #include "singletons/settingsmanager.hpp" #include "singletons/thememanager.hpp" +#include "singletons/windowmanager.hpp" +#include "widgets/accountswitchpopupwidget.hpp" #include "widgets/helper/shortcut.hpp" #include "widgets/notebook.hpp" #include "widgets/settingsdialog.hpp" @@ -35,8 +37,11 @@ Window::Window(const QString &windowName, singletons::ThemeManager &_themeManage }); if (this->hasCustomWindowFrame()) { - this->addTitleBarButton("Preferences"); - this->addTitleBarButton("User"); + this->addTitleBarButton( + "preferences", [] { singletons::WindowManager::getInstance().showSettingsDialog(); }); + this->addTitleBarButton("user", [this] { + singletons::WindowManager::getInstance().showAccountSelectPopup(QCursor::pos()); + }); } QVBoxLayout *layout = new QVBoxLayout(this); From 93cfcbd3f1e61654e1e6ced5a6eead96a2193493 Mon Sep 17 00:00:00 2001 From: fourtf Date: Wed, 24 Jan 2018 15:34:04 +0100 Subject: [PATCH 16/30] added empty ketboard settings page --- chatterino.pro | 7 +++++-- src/widgets/settingsdialog.cpp | 7 +++++++ .../settingspages/keyboardsettingspage.cpp | 12 ++++++++++++ .../settingspages/keyboardsettingspage.hpp | 15 +++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/widgets/settingspages/keyboardsettingspage.cpp create mode 100644 src/widgets/settingspages/keyboardsettingspage.hpp diff --git a/chatterino.pro b/chatterino.pro index 788a26abc..59c60c58a 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -166,7 +166,8 @@ SOURCES += \ src/util/networkrequest.cpp \ src/widgets/settingspages/ignoreuserspage.cpp \ src/widgets/settingspages/ignoremessagespage.cpp \ - src/widgets/settingspages/specialchannelspage.cpp + src/widgets/settingspages/specialchannelspage.cpp \ + src/widgets/settingspages/keyboardsettingspage.cpp HEADERS += \ src/precompiled_header.hpp \ @@ -272,7 +273,9 @@ HEADERS += \ src/util/networkrequester.hpp \ src/widgets/settingspages/ignoreuserspage.hpp \ src/widgets/settingspages/ignoremessagespage.hpp \ - src/widgets/settingspages/specialchannelspage.hpp + src/widgets/settingspages/specialchannelspage.hpp \ + src/widgets/settingspages/keyboardsettings.hpp \ + src/widgets/settingspages/keyboardsettingspage.hpp RESOURCES += \ resources/resources.qrc diff --git a/src/widgets/settingsdialog.cpp b/src/widgets/settingsdialog.cpp index edd23fd07..2b1f20c9c 100644 --- a/src/widgets/settingsdialog.cpp +++ b/src/widgets/settingsdialog.cpp @@ -10,6 +10,7 @@ #include "widgets/settingspages/highlightingpage.hpp" #include "widgets/settingspages/ignoremessagespage.hpp" #include "widgets/settingspages/ignoreuserspage.hpp" +#include "widgets/settingspages/keyboardsettingspage.hpp" #include "widgets/settingspages/logspage.hpp" #include "widgets/settingspages/moderationpage.hpp" #include "widgets/settingspages/specialchannelspage.hpp" @@ -75,14 +76,20 @@ SettingsDialog *SettingsDialog::getHandle() void SettingsDialog::addTabs() { + this->ui.tabContainer->setSpacing(0); + this->addTab(new settingspages::AccountsPage); this->addTab(new settingspages::AppearancePage); this->addTab(new settingspages::BehaviourPage); this->addTab(new settingspages::CommandPage); this->addTab(new settingspages::EmotesPage); this->addTab(new settingspages::HighlightingPage); + + this->ui.tabContainer->addStretch(1); + this->addTab(new settingspages::IgnoreMessagesPage); this->addTab(new settingspages::IgnoreUsersPage); + this->addTab(new settingspages::KeyboardSettingsPage); this->addTab(new settingspages::LogsPage); this->addTab(new settingspages::ModerationPage); this->addTab(new settingspages::SpecialChannelsPage); diff --git a/src/widgets/settingspages/keyboardsettingspage.cpp b/src/widgets/settingspages/keyboardsettingspage.cpp new file mode 100644 index 000000000..57565fe8a --- /dev/null +++ b/src/widgets/settingspages/keyboardsettingspage.cpp @@ -0,0 +1,12 @@ +#include "keyboardsettingspage.hpp" + +namespace chatterino { +namespace widgets { +namespace settingspages { +KeyboardSettingsPage::KeyboardSettingsPage() + : SettingsPage("Keybindings", "") +{ +} +} // namespace settingspages +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/settingspages/keyboardsettingspage.hpp b/src/widgets/settingspages/keyboardsettingspage.hpp new file mode 100644 index 000000000..3cf4a6149 --- /dev/null +++ b/src/widgets/settingspages/keyboardsettingspage.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "widgets/settingspages/settingspage.hpp" + +namespace chatterino { +namespace widgets { +namespace settingspages { +class KeyboardSettingsPage : public SettingsPage +{ +public: + KeyboardSettingsPage(); +}; +} // namespace settingspages +} // namespace widgets +} // namespace chatterino From 8c51df31e601340dd64881cdcd354b8f2b40c14b Mon Sep 17 00:00:00 2001 From: fourtf Date: Wed, 24 Jan 2018 15:35:23 +0100 Subject: [PATCH 17/30] removed "2" from the about page logo --- resources/images/aboutlogo.png | Bin 43142 -> 52153 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/images/aboutlogo.png b/resources/images/aboutlogo.png index 8e8753521637e45978b07def7050fd786dc3902b..3efa9ab7b81525c7c847e88b755ed01215c16909 100644 GIT binary patch literal 52153 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 QtGh;GB2vPIuitz97xzv&WdHyG literal 43142 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 Date: Wed, 24 Jan 2018 20:27:56 +0100 Subject: [PATCH 18/30] added buttons to the custom window frame --- chatterino.pro | 6 +- src/widgets/basewindow.cpp | 72 +++++++++------ src/widgets/basewindow.hpp | 14 +-- src/widgets/helper/titlebarbutton.cpp | 127 ++++++++++++++++++++++++++ src/widgets/helper/titlebarbutton.hpp | 25 +++++ src/widgets/window.cpp | 7 +- 6 files changed, 210 insertions(+), 41 deletions(-) create mode 100644 src/widgets/helper/titlebarbutton.cpp create mode 100644 src/widgets/helper/titlebarbutton.hpp diff --git a/chatterino.pro b/chatterino.pro index 59c60c58a..bd0109acc 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -167,7 +167,8 @@ SOURCES += \ src/widgets/settingspages/ignoreuserspage.cpp \ src/widgets/settingspages/ignoremessagespage.cpp \ src/widgets/settingspages/specialchannelspage.cpp \ - src/widgets/settingspages/keyboardsettingspage.cpp + src/widgets/settingspages/keyboardsettingspage.cpp \ + src/widgets/helper/titlebarbutton.cpp HEADERS += \ src/precompiled_header.hpp \ @@ -275,7 +276,8 @@ HEADERS += \ src/widgets/settingspages/ignoremessagespage.hpp \ src/widgets/settingspages/specialchannelspage.hpp \ src/widgets/settingspages/keyboardsettings.hpp \ - src/widgets/settingspages/keyboardsettingspage.hpp + src/widgets/settingspages/keyboardsettingspage.hpp \ + src/widgets/helper/titlebarbutton.hpp RESOURCES += \ resources/resources.qrc diff --git a/src/widgets/basewindow.cpp b/src/widgets/basewindow.cpp index d6ec127c4..8417c5cb8 100644 --- a/src/widgets/basewindow.cpp +++ b/src/widgets/basewindow.cpp @@ -23,7 +23,7 @@ #define WM_DPICHANGED 0x02E0 #endif -#include "widgets/helper/rippleeffectlabel.hpp" +#include "widgets/helper/titlebarbutton.hpp" namespace chatterino { namespace widgets { @@ -72,38 +72,39 @@ void BaseWindow::init() this->titleLabel = title; // buttons - RippleEffectLabel *min = new RippleEffectLabel; - min->getLabel().setText("min"); - min->setFixedSize(46, 30); - RippleEffectLabel *max = new RippleEffectLabel; - max->setFixedSize(46, 30); - max->getLabel().setText("max"); - RippleEffectLabel *exit = new RippleEffectLabel; - exit->setFixedSize(46, 30); - exit->getLabel().setText("exit"); + TitleBarButton *_minButton = new TitleBarButton; + _minButton->setFixedSize(46, 30); + _minButton->setButtonStyle(TitleBarButton::Minimize); + TitleBarButton *_maxButton = new TitleBarButton; + _maxButton->setFixedSize(46, 30); + _maxButton->setButtonStyle(TitleBarButton::Maximize); + TitleBarButton *_exitButton = new TitleBarButton; + _exitButton->setFixedSize(46, 30); + _exitButton->setButtonStyle(TitleBarButton::Close); - QObject::connect(min, &RippleEffectLabel::clicked, this, [this] { + QObject::connect(_minButton, &TitleBarButton::clicked, this, [this] { this->setWindowState(Qt::WindowMinimized | this->windowState()); }); - QObject::connect(max, &RippleEffectLabel::clicked, this, [this] { + QObject::connect(_maxButton, &TitleBarButton::clicked, this, [this] { this->setWindowState(this->windowState() == Qt::WindowMaximized ? Qt::WindowActive : Qt::WindowMaximized); }); - QObject::connect(exit, &RippleEffectLabel::clicked, this, [this] { this->close(); }); + QObject::connect(_exitButton, &TitleBarButton::clicked, this, + [this] { this->close(); }); - this->minButton = min; - this->maxButton = max; - this->exitButton = exit; + this->minButton = _minButton; + this->maxButton = _maxButton; + this->exitButton = _exitButton; - this->buttons.push_back(min); - this->buttons.push_back(max); - this->buttons.push_back(exit); + this->buttons.push_back(_minButton); + this->buttons.push_back(_maxButton); + this->buttons.push_back(_exitButton); buttonLayout->addStretch(1); - buttonLayout->addWidget(min); - buttonLayout->addWidget(max); - buttonLayout->addWidget(exit); + buttonLayout->addWidget(_minButton); + buttonLayout->addWidget(_maxButton); + buttonLayout->addWidget(_exitButton); buttonLayout->setSpacing(0); } this->layoutBase = new QWidget(this); @@ -161,23 +162,34 @@ void BaseWindow::refreshTheme() palette.setColor(QPalette::Foreground, this->themeManager.windowText); this->setPalette(palette); - for (RippleEffectLabel *label : this->buttons) { - label->setMouseEffectColor(this->themeManager.windowText); + for (RippleEffectButton *button : this->buttons) { + button->setMouseEffectColor(this->themeManager.windowText); } } -void BaseWindow::addTitleBarButton(const QString &text, std::function onClicked) +void BaseWindow::addTitleBarButton(const TitleBarButton::Style &style, + std::function onClicked) { - RippleEffectLabel *label = new RippleEffectLabel; - label->getLabel().setText(text); - this->buttons.push_back(label); - this->titlebarBox->insertWidget(2, label); - QObject::connect(label, &RippleEffectLabel::clicked, this, [onClicked] { onClicked(); }); + TitleBarButton *button = new TitleBarButton; + + this->buttons.push_back(button); + this->titlebarBox->insertWidget(2, button); + button->setButtonStyle(style); + + QObject::connect(button, &TitleBarButton::clicked, this, [onClicked] { onClicked(); }); } void BaseWindow::changeEvent(QEvent *) { TooltipWidget::getInstance()->hide(); + +#ifdef USEWINSDK + if (this->hasCustomWindowFrame()) { + this->maxButton->setButtonStyle(this->windowState() & Qt::WindowMaximized + ? TitleBarButton::Unmaximize + : TitleBarButton::Maximize); + } +#endif } void BaseWindow::leaveEvent(QEvent *) diff --git a/src/widgets/basewindow.hpp b/src/widgets/basewindow.hpp index 16e2f2a2a..2ec8e196f 100644 --- a/src/widgets/basewindow.hpp +++ b/src/widgets/basewindow.hpp @@ -1,6 +1,7 @@ #pragma once #include "basewidget.hpp" +#include "widgets/helper/titlebarbutton.hpp" #include @@ -8,7 +9,8 @@ class QHBoxLayout; namespace chatterino { namespace widgets { -class RippleEffectLabel; +class RippleEffectButton; +class TitleBarButton; class BaseWindow : public BaseWidget { @@ -20,7 +22,7 @@ public: QWidget *getLayoutContainer(); bool hasCustomWindowFrame(); - void addTitleBarButton(const QString &text, std::function onClicked); + void addTitleBarButton(const TitleBarButton::Style &style, std::function onClicked); void setStayInScreenRect(bool value); bool getStayInScreenRect() const; @@ -50,11 +52,11 @@ private: QHBoxLayout *titlebarBox; QWidget *titleLabel; - RippleEffectLabel *minButton; - RippleEffectLabel *maxButton; - RippleEffectLabel *exitButton; + TitleBarButton *minButton; + TitleBarButton *maxButton; + TitleBarButton *exitButton; QWidget *layoutBase; - std::vector buttons; + std::vector buttons; }; } // namespace widgets } // namespace chatterino diff --git a/src/widgets/helper/titlebarbutton.cpp b/src/widgets/helper/titlebarbutton.cpp new file mode 100644 index 000000000..df5326847 --- /dev/null +++ b/src/widgets/helper/titlebarbutton.cpp @@ -0,0 +1,127 @@ +#include "titlebarbutton.hpp" + +namespace chatterino { +namespace widgets { +TitleBarButton::TitleBarButton() + : RippleEffectButton(nullptr) +{ +} + +TitleBarButton::Style TitleBarButton::getButtonStyle() const +{ + return this->style; +} + +void TitleBarButton::setButtonStyle(Style _style) +{ + this->style = _style; + this->update(); +} + +void TitleBarButton::resizeEvent(QResizeEvent *) +{ + if (this->style & (Maximize | Minimize | Unmaximize | Close)) { + this->setFixedWidth(this->height() * 46 / 30); + } else { + this->setFixedWidth(this->height()); + } +} + +void TitleBarButton::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + + QColor color = "#000"; + QColor background = "#fff"; + + int xD = this->height() / 3; + int centerX = this->width() / 2; + + painter.setRenderHint(QPainter::Antialiasing, false); + + switch (this->style) { + case Minimize: { + painter.fillRect(centerX - xD / 2, xD * 3 / 2, xD, 1, color); + break; + } + case Maximize: { + painter.setPen(color); + painter.drawRect(centerX - xD / 2, xD, xD - 1, xD - 1); + break; + } + case Unmaximize: { + int xD2 = xD * 1 / 5; + int xD3 = xD * 4 / 5; + + painter.drawRect(centerX - xD / 2 + xD2, xD, xD3, xD3); + painter.fillRect(centerX - xD / 2, xD + xD2, xD3, xD3, QColor("#fff")); + painter.drawRect(centerX - xD / 2, xD + xD2, xD3, xD3); + break; + } + case Close: { + QRect rect(centerX - xD / 2, xD, xD - 1, xD - 1); + painter.setPen(QPen(color, 1)); + + painter.drawLine(rect.topLeft(), rect.bottomRight()); + painter.drawLine(rect.topRight(), rect.bottomLeft()); + break; + } + case User: { + color = QColor("#333"); + + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::HighQualityAntialiasing); + + auto a = xD / 3; + QPainterPath path; + + painter.save(); + painter.translate(3, 3); + + path.arcMoveTo(a, 4 * a, 6 * a, 6 * a, 0); + path.arcTo(a, 4 * a, 6 * a, 6 * a, 0, 180); + + painter.fillPath(path, color); + + painter.setBrush(background); + painter.drawEllipse(2 * a, 1 * a, 4 * a, 4 * a); + + painter.setBrush(color); + painter.drawEllipse(2.5 * a, 1.5 * a, 3 * a + 1, 3 * a); + painter.restore(); + + break; + } + case Settings: { + color = QColor("#333"); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::HighQualityAntialiasing); + + painter.save(); + painter.translate(3, 3); + + auto a = xD / 3; + QPainterPath path; + + path.arcMoveTo(a, a, 6 * a, 6 * a, 0 - (360 / 32.0)); + + for (int i = 0; i < 8; i++) { + path.arcTo(a, a, 6 * a, 6 * a, i * (360 / 8.0) - (360 / 32.0), (360 / 32.0)); + path.arcTo(2 * a, 2 * a, 4 * a, 4 * a, i * (360 / 8.0) + (360 / 32.0), + (360 / 32.0)); + } + + painter.strokePath(path, color); + painter.fillPath(path, color); + + painter.setBrush(background); + painter.drawEllipse(3 * a, 3 * a, 2 * a, 2 * a); + painter.restore(); + break; + } + } + + this->fancyPaint(painter); +} +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/helper/titlebarbutton.hpp b/src/widgets/helper/titlebarbutton.hpp new file mode 100644 index 000000000..83ce40713 --- /dev/null +++ b/src/widgets/helper/titlebarbutton.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "widgets/helper/rippleeffectbutton.hpp" + +namespace chatterino { +namespace widgets { +class TitleBarButton : public RippleEffectButton +{ +public: + enum Style { Minimize = 1, Maximize = 2, Unmaximize = 4, Close = 8, User = 16, Settings = 32 }; + + TitleBarButton(); + + Style getButtonStyle() const; + void setButtonStyle(Style style); + +protected: + virtual void paintEvent(QPaintEvent *) override; + virtual void resizeEvent(QResizeEvent *) override; + +private: + Style style; +}; +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/window.cpp b/src/widgets/window.cpp index 8c27e3d46..c7ecdb039 100644 --- a/src/widgets/window.cpp +++ b/src/widgets/window.cpp @@ -37,9 +37,10 @@ Window::Window(const QString &windowName, singletons::ThemeManager &_themeManage }); if (this->hasCustomWindowFrame()) { - this->addTitleBarButton( - "preferences", [] { singletons::WindowManager::getInstance().showSettingsDialog(); }); - this->addTitleBarButton("user", [this] { + this->addTitleBarButton(TitleBarButton::Settings, [] { + singletons::WindowManager::getInstance().showSettingsDialog(); + }); + this->addTitleBarButton(TitleBarButton::User, [this] { singletons::WindowManager::getInstance().showAccountSelectPopup(QCursor::pos()); }); } From 74fd6c9663b97ba8aba5054ccd98a599e9a3405f Mon Sep 17 00:00:00 2001 From: fourtf Date: Wed, 24 Jan 2018 20:35:26 +0100 Subject: [PATCH 19/30] Fixes #258 theme change doesn't apply --- src/widgets/basewidget.hpp | 4 ++-- src/widgets/helper/channelview.cpp | 7 +++++++ src/widgets/helper/channelview.hpp | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/widgets/basewidget.hpp b/src/widgets/basewidget.hpp index 8030655ed..0228418b1 100644 --- a/src/widgets/basewidget.hpp +++ b/src/widgets/basewidget.hpp @@ -30,10 +30,10 @@ protected: float dpiMultiplier = 1.f; + virtual void refreshTheme(); + private: void init(); - - virtual void refreshTheme(); }; } // namespace widgets diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index e5296c88d..6f25aba52 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -114,6 +114,13 @@ ChannelView::~ChannelView() this->messageReplacedConnection.disconnect(); } +void ChannelView::refreshTheme() +{ + BaseWidget::refreshTheme(); + + this->layoutMessages(); +} + void ChannelView::queueUpdate() { if (this->updateTimer.isActive()) { diff --git a/src/widgets/helper/channelview.hpp b/src/widgets/helper/channelview.hpp index edc43c7cf..2661f2031 100644 --- a/src/widgets/helper/channelview.hpp +++ b/src/widgets/helper/channelview.hpp @@ -52,6 +52,8 @@ public: pajlada::Signals::NoArgSignal highlightedMessageReceived; protected: + virtual void refreshTheme() override; + virtual void resizeEvent(QResizeEvent *) override; virtual void paintEvent(QPaintEvent *) override; From 05339aad2dc2bb2e4ab92a0a56f5eb4f52f044cf Mon Sep 17 00:00:00 2001 From: fourtf Date: Wed, 24 Jan 2018 20:58:53 +0100 Subject: [PATCH 20/30] started fixing clicking emtoes --- src/widgets/emotepopup.cpp | 5 +++++ src/widgets/emotepopup.hpp | 4 ++++ src/widgets/helper/channelview.cpp | 2 ++ src/widgets/helper/channelview.hpp | 1 + src/widgets/helper/splitinput.cpp | 14 ++++++++++++-- src/widgets/helper/splitinput.hpp | 3 ++- 6 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/widgets/emotepopup.cpp b/src/widgets/emotepopup.cpp index 1fce82c50..e2aa58256 100644 --- a/src/widgets/emotepopup.cpp +++ b/src/widgets/emotepopup.cpp @@ -30,6 +30,11 @@ EmotePopup::EmotePopup(singletons::ThemeManager &themeManager) tabs->addTab(this->viewEmojis, "Emojis"); this->loadEmojis(); + + this->viewEmotes->linkClicked.connect( + [this](const Link &link) { this->linkClicked.invoke(link); }); + this->viewEmojis->linkClicked.connect( + [this](const Link &link) { this->linkClicked.invoke(link); }); } void EmotePopup::loadChannel(ChannelPtr _channel) diff --git a/src/widgets/emotepopup.hpp b/src/widgets/emotepopup.hpp index e6727dd74..53fd9885d 100644 --- a/src/widgets/emotepopup.hpp +++ b/src/widgets/emotepopup.hpp @@ -4,6 +4,8 @@ #include "widgets/basewindow.hpp" #include "widgets/helper/channelview.hpp" +#include + namespace chatterino { namespace widgets { @@ -15,6 +17,8 @@ public: void loadChannel(ChannelPtr channel); void loadEmojis(); + pajlada::Signals::Signal linkClicked; + private: ChannelView *viewEmotes; ChannelView *viewEmojis; diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index 6f25aba52..b6c18e828 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -834,6 +834,8 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) this->channel->sendMessage(value); } } + + this->linkClicked.invoke(link); } bool ChannelView::tryGetMessageAt(QPoint p, std::shared_ptr &_message, diff --git a/src/widgets/helper/channelview.hpp b/src/widgets/helper/channelview.hpp index 2661f2031..a57691154 100644 --- a/src/widgets/helper/channelview.hpp +++ b/src/widgets/helper/channelview.hpp @@ -50,6 +50,7 @@ public: boost::signals2::signal mouseDown; boost::signals2::signal selectionChanged; pajlada::Signals::NoArgSignal highlightedMessageReceived; + pajlada::Signals::Signal linkClicked; protected: virtual void refreshTheme() override; diff --git a/src/widgets/helper/splitinput.cpp b/src/widgets/helper/splitinput.cpp index 125920c3d..8e17a0500 100644 --- a/src/widgets/helper/splitinput.cpp +++ b/src/widgets/helper/splitinput.cpp @@ -52,8 +52,13 @@ SplitInput::SplitInput(Split *_chatWidget) "/>"); connect(&this->emotesLabel, &RippleEffectLabel::clicked, [this] { - if (this->emotePopup == nullptr) { - this->emotePopup = new EmotePopup(this->themeManager); + if (!this->emotePopup) { + this->emotePopup = std::make_unique(this->themeManager); + this->emotePopup->linkClicked.connect([this](const messages::Link &link) { + if (link.getType() == messages::Link::InsertText) { + this->insertText(link.getValue()); + } + }); } this->emotePopup->resize((int)(300 * this->emotePopup->getDpiMultiplier()), @@ -210,6 +215,11 @@ QString SplitInput::getInputText() const return this->textInput.toPlainText(); } +void SplitInput::insertText(const QString &text) +{ + this->textInput.insertPlainText(text); +} + void SplitInput::refreshTheme() { QPalette palette; diff --git a/src/widgets/helper/splitinput.hpp b/src/widgets/helper/splitinput.hpp index 63321af74..951efd9c2 100644 --- a/src/widgets/helper/splitinput.hpp +++ b/src/widgets/helper/splitinput.hpp @@ -27,6 +27,7 @@ public: void clearSelection(); QString getInputText() const; + void insertText(const QString &text); pajlada::Signals::Signal textChanged; @@ -38,7 +39,7 @@ protected: private: Split *const chatWidget; - EmotePopup *emotePopup = nullptr; + std::unique_ptr emotePopup; std::vector managedConnections; QHBoxLayout hbox; From f35ca0d2c82b4a41d53f2651831db9472a3866a9 Mon Sep 17 00:00:00 2001 From: fourtf Date: Wed, 24 Jan 2018 21:16:00 +0100 Subject: [PATCH 21/30] fixed right clicking links --- src/widgets/helper/channelview.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index b6c18e828..ed9c5374b 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -13,6 +13,7 @@ #include "widgets/split.hpp" #include "widgets/tooltipwidget.hpp" +#include #include #include #include @@ -825,7 +826,24 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) break; } case messages::Link::Url: { - QDesktopServices::openUrl(QUrl(link.getValue())); + if (event->button() == Qt::RightButton) { + static QMenu *menu = nullptr; + static QString url; + + if (menu == nullptr) { + menu = new QMenu; + menu->addAction("Open in browser", + [] { QDesktopServices::openUrl(QUrl(url)); }); + menu->addAction("Copy to clipboard", + [] { QApplication::clipboard()->setText(url); }); + } + + url = link.getValue(); + menu->move(QCursor::pos()); + menu->show(); + } else { + QDesktopServices::openUrl(QUrl(link.getValue())); + } break; } case messages::Link::UserAction: { From de9e1b641da68a132025221591f1d68daac15618 Mon Sep 17 00:00:00 2001 From: fourtf Date: Wed, 24 Jan 2018 21:44:31 +0100 Subject: [PATCH 22/30] Fixes #234 links --- src/widgets/basewindow.cpp | 4 +-- src/widgets/helper/channelview.cpp | 39 +++++++++++++++++++-- src/widgets/helper/channelview.hpp | 4 +++ src/widgets/settingspages/behaviourpage.cpp | 22 +++++++++--- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/widgets/basewindow.cpp b/src/widgets/basewindow.cpp index 8417c5cb8..9ef8cb871 100644 --- a/src/widgets/basewindow.cpp +++ b/src/widgets/basewindow.cpp @@ -387,9 +387,9 @@ void BaseWindow::showEvent(QShowEvent *event) void BaseWindow::paintEvent(QPaintEvent *event) { - BaseWidget::paintEvent(event); - if (this->hasCustomWindowFrame()) { + BaseWidget::paintEvent(event); + QPainter painter(this); bool windowFocused = this->window() == QApplication::activeWindow(); diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index ed9c5374b..c6eb3a512 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -813,7 +813,44 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) } auto &link = hoverLayoutElement->getLink(); + if (!singletons::SettingManager::getInstance().linksDoubleClickOnly) { + this->handleLinkClick(event, link, layout.get()); + } + this->linkClicked.invoke(link); +} + +void ChannelView::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (singletons::SettingManager::getInstance().linksDoubleClickOnly) { + std::shared_ptr layout; + QPoint relativePos; + int messageIndex; + + if (!tryGetMessageAt(event->pos(), layout, relativePos, messageIndex)) { + return; + } + + // message under cursor is collapsed + if (layout->getFlags() & MessageLayout::Collapsed) { + return; + } + + const messages::MessageLayoutElement *hoverLayoutElement = + layout->getElementAt(relativePos); + + if (hoverLayoutElement == nullptr) { + return; + } + + auto &link = hoverLayoutElement->getLink(); + this->handleLinkClick(event, link, layout.get()); + } +} + +void ChannelView::handleLinkClick(QMouseEvent *event, const messages::Link &link, + messages::MessageLayout *layout) +{ switch (link.getType()) { case messages::Link::UserInfo: { auto user = link.getValue(); @@ -852,8 +889,6 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) this->channel->sendMessage(value); } } - - this->linkClicked.invoke(link); } bool ChannelView::tryGetMessageAt(QPoint p, std::shared_ptr &_message, diff --git a/src/widgets/helper/channelview.hpp b/src/widgets/helper/channelview.hpp index a57691154..113e3b981 100644 --- a/src/widgets/helper/channelview.hpp +++ b/src/widgets/helper/channelview.hpp @@ -66,6 +66,10 @@ protected: virtual void mouseMoveEvent(QMouseEvent *event) override; virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseReleaseEvent(QMouseEvent *event) override; + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; + + void handleLinkClick(QMouseEvent *event, const messages::Link &link, + messages::MessageLayout *layout); bool tryGetMessageAt(QPoint p, std::shared_ptr &message, QPoint &relativePos, int &index); diff --git a/src/widgets/settingspages/behaviourpage.cpp b/src/widgets/settingspages/behaviourpage.cpp index f9caa5e24..e45c5b0eb 100644 --- a/src/widgets/settingspages/behaviourpage.cpp +++ b/src/widgets/settingspages/behaviourpage.cpp @@ -1,6 +1,7 @@ #include "behaviourpage.hpp" #include +#include #include #include @@ -22,7 +23,9 @@ BehaviourPage::BehaviourPage() singletons::SettingManager &settings = singletons::SettingManager::getInstance(); util::LayoutCreator layoutCreator(this); - auto form = layoutCreator.emplace().withoutMargin(); + auto layout = layoutCreator.setLayoutType(); + + auto form = layout.emplace().withoutMargin(); { form->addRow("Window:", this->createCheckBox(WINDOW_TOPMOST, settings.windowTopMost)); form->addRow("Messages:", this->createCheckBox(INPUT_EMPTY, settings.hideEmptyInput)); @@ -30,10 +33,21 @@ BehaviourPage::BehaviourPage() form->addRow("Pause chat:", this->createCheckBox(PAUSE_HOVERING, settings.pauseChatHover)); form->addRow("Mouse scroll speed:", this->createMouseScrollSlider()); - form->addRow("Streamlink path:", this->createLineEdit(settings.streamlinkPath)); - form->addRow("Prefered quality:", - this->createComboBox({STREAMLINK_QUALITY}, settings.preferredQuality)); + form->addRow("Links:", this->createCheckBox("Open links only on double click", + settings.linksDoubleClickOnly)); } + + layout->addSpacing(16); + + auto group = layout.emplace("Streamlink"); + { + auto groupLayout = group.setLayoutType(); + groupLayout->addRow("Streamlink path:", this->createLineEdit(settings.streamlinkPath)); + groupLayout->addRow("Prefered quality:", + this->createComboBox({STREAMLINK_QUALITY}, settings.preferredQuality)); + } + + layout->addStretch(1); } QSlider *BehaviourPage::createMouseScrollSlider() From 8ab0fa437824f9ad6ab254b2ef32b2112d4a778c Mon Sep 17 00:00:00 2001 From: fourtf Date: Wed, 24 Jan 2018 22:09:26 +0100 Subject: [PATCH 23/30] Fixes #259 dropping split on + button --- src/singletons/ircmanager.cpp | 8 ++--- src/widgets/helper/channelview.cpp | 3 +- src/widgets/helper/notebookbutton.cpp | 47 +++++++++++++++++++++++++++ src/widgets/helper/notebookbutton.hpp | 7 ++-- 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/singletons/ircmanager.cpp b/src/singletons/ircmanager.cpp index 59f829ec9..aa5753cf0 100644 --- a/src/singletons/ircmanager.cpp +++ b/src/singletons/ircmanager.cpp @@ -256,12 +256,12 @@ void IrcManager::privateMessageReceived(Communi::IrcPrivateMessage *message) twitch::TwitchMessageBuilder builder(c.get(), message, args); if (!builder.isIgnored()) { - messages::MessagePtr message = builder.build(); - if (message->hasFlags(messages::Message::Highlighted)) { - singletons::ChannelManager::getInstance().mentionsChannel->addMessage(message); + messages::MessagePtr _message = builder.build(); + if (_message->hasFlags(messages::Message::Highlighted)) { + singletons::ChannelManager::getInstance().mentionsChannel->addMessage(_message); } - c->addMessage(message); + c->addMessage(_message); } } diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index c6eb3a512..12944c25a 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -813,7 +813,8 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) } auto &link = hoverLayoutElement->getLink(); - if (!singletons::SettingManager::getInstance().linksDoubleClickOnly) { + if (event->button() != Qt::LeftButton || + !singletons::SettingManager::getInstance().linksDoubleClickOnly) { this->handleLinkClick(event, link, layout.get()); } diff --git a/src/widgets/helper/notebookbutton.cpp b/src/widgets/helper/notebookbutton.cpp index 3f567f3ff..6aa06dc1a 100644 --- a/src/widgets/helper/notebookbutton.cpp +++ b/src/widgets/helper/notebookbutton.cpp @@ -1,12 +1,16 @@ #include "widgets/helper/notebookbutton.hpp" #include "singletons/thememanager.hpp" #include "widgets/helper/rippleeffectbutton.hpp" +#include "widgets/notebook.hpp" +#include "widgets/splitcontainer.hpp" #include #include #include #include +#define nuuls nullptr + namespace chatterino { namespace widgets { @@ -14,6 +18,8 @@ NotebookButton::NotebookButton(BaseWidget *parent) : RippleEffectButton(parent) { setMouseEffectColor(QColor(0, 0, 0)); + + this->setAcceptDrops(true); } void NotebookButton::paintEvent(QPaintEvent *) @@ -97,5 +103,46 @@ void NotebookButton::mouseReleaseEvent(QMouseEvent *event) RippleEffectButton::mouseReleaseEvent(event); } +void NotebookButton::dragEnterEvent(QDragEnterEvent *event) +{ + if (!event->mimeData()->hasFormat("chatterino/split")) + return; + + event->acceptProposedAction(); + + auto e = new QMouseEvent(QMouseEvent::MouseButtonPress, + QPointF(this->width() / 2, this->height() / 2), Qt::LeftButton, + Qt::LeftButton, 0); + RippleEffectButton::mousePressEvent(e); + delete e; +} + +void NotebookButton::dragLeaveEvent(QDragLeaveEvent *) +{ + this->mouseDown = true; + this->update(); + + auto e = new QMouseEvent(QMouseEvent::MouseButtonRelease, + QPointF(this->width() / 2, this->height() / 2), Qt::LeftButton, + Qt::LeftButton, 0); + RippleEffectButton::mouseReleaseEvent(e); + delete e; +} + +void NotebookButton::dropEvent(QDropEvent *event) +{ + if (SplitContainer::isDraggingSplit) { + event->acceptProposedAction(); + + Notebook *notebook = dynamic_cast(this->parentWidget()); + + if (notebook != nuuls) { + SplitContainer *tab = notebook->addNewPage(); + + SplitContainer::draggingSplit->setParent(tab); + tab->addToLayout(SplitContainer::draggingSplit); + } + } +} } // namespace widgets } // namespace chatterino diff --git a/src/widgets/helper/notebookbutton.hpp b/src/widgets/helper/notebookbutton.hpp index 811c21667..a62b80026 100644 --- a/src/widgets/helper/notebookbutton.hpp +++ b/src/widgets/helper/notebookbutton.hpp @@ -21,8 +21,11 @@ public: NotebookButton(BaseWidget *parent); protected: - void paintEvent(QPaintEvent *) override; - void mouseReleaseEvent(QMouseEvent *event) override; + virtual void paintEvent(QPaintEvent *) override; + virtual void mouseReleaseEvent(QMouseEvent *) override; + virtual void dragEnterEvent(QDragEnterEvent *) override; + virtual void dragLeaveEvent(QDragLeaveEvent *) override; + virtual void dropEvent(QDropEvent *) override; signals: void clicked(); From 0a8073d0e5fc4cb63c077683f23d10c47e27354d Mon Sep 17 00:00:00 2001 From: fourtf Date: Thu, 25 Jan 2018 20:49:49 +0100 Subject: [PATCH 24/30] refactored SplitInput --- .vs/ProjectSettings.json | 3 + .vs/VSWorkspaceState.json | 7 + .vs/slnx.sqlite | Bin 0 -> 73728 bytes src/widgets/accountpopup.cpp | 4 +- src/widgets/accountpopup.hpp | 2 +- src/widgets/basewidget.cpp | 77 +++++++-- src/widgets/basewidget.hpp | 23 ++- src/widgets/basewindow.cpp | 26 ++-- src/widgets/basewindow.hpp | 2 +- src/widgets/helper/channelview.cpp | 17 +- src/widgets/helper/channelview.hpp | 2 +- src/widgets/helper/notebooktab.cpp | 24 +-- src/widgets/helper/notebooktab.hpp | 20 +-- src/widgets/helper/rippleeffectbutton.cpp | 2 +- src/widgets/helper/splitheader.cpp | 6 +- src/widgets/helper/splitheader.hpp | 2 +- src/widgets/helper/splitinput.cpp | 180 +++++++++++++--------- src/widgets/helper/splitinput.hpp | 27 +++- src/widgets/notebook.cpp | 21 ++- src/widgets/notebook.hpp | 1 + src/widgets/scrollbar.cpp | 8 +- src/widgets/settingsdialog.cpp | 4 +- src/widgets/settingsdialog.hpp | 2 +- src/widgets/split.cpp | 6 +- src/widgets/tooltipwidget.cpp | 4 +- src/widgets/tooltipwidget.hpp | 2 +- src/widgets/window.cpp | 4 +- 27 files changed, 296 insertions(+), 180 deletions(-) create mode 100644 .vs/ProjectSettings.json create mode 100644 .vs/VSWorkspaceState.json create mode 100644 .vs/slnx.sqlite diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 000000000..866f1e137 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 000000000..b23974898 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,7 @@ +{ + "ExpandedNodes": [ + "" + ], + "SelectedNode": "\\ISSUE_TEMPLATE.md", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..e43235e20bc1d323a9dcfe3d8b2b214a77c57261 GIT binary patch literal 73728 zcmeI4O>Y~=8OOP%B+`^As&QK$bjZL!V6l3MZN-2QAVBHm%HEnHX}-u7R6>|l z$t)Mn{LE#3mHGL~$$UTcUGk^ocH*1a$y;S9iz*WryS&^|D%GU2G|(Iw_UeU!z!DB~E*le(ywY(|)kJD@3Zw zrczZ(vQi_vuAtppbJ@GWYUb?5xPs*pDJVsSnIq?Ga=xIfIvo}0F*VwnVRHLk*_#i+ z>rcGJ`*o~vI&M@7oTJn68FUJbN=PS$C}h3ZIkR89^|tk?Xy#Fq)Ro8fh!MG5EOPTo zWoDp6qI+RTlv&L^@mQ&NsrvZQrsv}#vgp_*Uwr3!JerZ5n>7mKMxc5O}is%9k#(QNU0 zKAaxJ;y`W?i-I0~hLqq0svGFR+82@$Y&e4BI{8Up<|dKNVW)HYaHw_k1D&>`8i#UzYkuch&DtM)#dthXy9_d3I_ zF)qiW`Of7;R#}st&0B#YD!*6J*yLsIao}QE%5l#|NqyK*7!)g(MX6v5Uc`t(JcOen zW|hr8mI7i0o^nT5lff7lMJ%1|7#E8!hhtnU4%9ri#<}N>gcajoH2sA@OEV1Gm$lZB z4KZ+?49B>oN}xCp<6==TO3x7E?zAr&8*JuH{5dq~o_juiXQ@Y+(u90a*SmCJYTe@| z;R}koy2WN)XWCntOJqOHxq;#;%-h@JZ|)6y)sl#e4Ohmijg*`A_81O&0Am}jX@H@AyJ}$h@01m_fmz-k(EbT!^P}(V?obqz&3vO+C#_-MngC2UGMvUY%T-0a zSF$!5%vmSt71$pHw-h45w>w9&vG2tb*_%t!=zV+nM@>^V4g-0;X5X1{#@t&le z>QtCCQROO^=CUg*(vIb7gC}e~LH7pq$&l~-gv)ry(S*WfgUz(7tY&U>UrJ<`mZWEg zR-29j&c~%t8@sM{{%+B^^X^=8Tk8y&ulccQ??%|>crle`@A1q>vCO|Re`g^rdCfKUz^QKzj~!Wl{1Pmc?L(fh5UXvlhc4;VpRZqWjm4BD=A10X_IlI(ehl>Rq}O()bn?X3fYaQ)J&(A(<#DUQKgDu>gK81ZW6Ur zSMDiQQmLw2`RWdNpzM%*qh3}^tcxwBR41h}`)d@7tHf!q((j$7+0`dA_b+WFmvR5P0kmTRi~o@J*GxmGfZyZD|_=Hc>RgD zc)yMnPRET(fpc^^K7&r7Q3>h95QVH4J7@Nbx8Als70oSo=aUf(=J-Tqi#X%-kfhIqY;!9}cySexTEKR0Gjb@_>zh zz=ho;=vbU@wdi1=@9Q0wM<AL2XlaH)`?A(LvLObp zli?V*R0$LZVq7c=M(G)1+@1DCV}s4Si9d%X-E+^!?=1BQQ<{(u>Ux(BOs#v|Bz!?p zSGU-#>r8tqbBXMSIX6&Tg?W2>{LQ^#uUZn3vEj;il|1)yOPn!da8TN8lkMuyY)X1S`U_e$1A zgE{LYy#o7#;Fdxp_;%+=Huk-EB71X58oh5X|EOu|#$h0@*X%np&X~LF&@nkLK%EMc zCaPTJ(p+|BMcT1kZSaJxC+OaQJ{j_zpKuuuIhs(oY_OSjmDS9R?n{a6(vtM-&}!3B z!1=foYGc>c&fhIMcix?AZfl((^EE#<-Q5Tq4Zi=MoBdlX^XbAr=6|?+l>C-m-~|F8 z00JQJOCj*Z;$q@AYir5BK049qXMD%aPok{Vl1>NWwKD^4tI8uHmJw}`PCZiV_t{aJ z+^E*nZDm|0UvrK&`5|779UaPbA}$QnHp$n>U2Wi;rrJw4wY^FvEjhTnY%f}D5x)vw zaD|G^BeGd7Z;kcp?`7EDwp{WrZ7uz4u6boWLcOraW^Mia>W(#GssH)s2)_=Du=soZpBb|DgvS(3}sX9rw@iAf!dY_$fM>`8qYu3hZw310=cXHB56OKm; znyKxLbFj98{%tPxUU-ceE}Ybax9$v!^W~?1$3@&+-L-aAxxXAQSi8QjmV_M-*Tb*J zuQK5UWVkSLufuD-=5o?+-Ba1$u(&&pceg)q_ebu=GH8{z0Sl_*3fkJA{|U(X_q?yM zH*^-@-eUvT$_so>z+xNEz3|+N&w}j5K%D#BFZ3J^&%MGA8tlVsZz>QU^8Eii_rm}G h@0YT~haMmR0w4eaAOHd&00JNY0w4eaAaHR6{s&#D)+7J` literal 0 HcmV?d00001 diff --git a/src/widgets/accountpopup.cpp b/src/widgets/accountpopup.cpp index e1a84d732..7502a548a 100644 --- a/src/widgets/accountpopup.cpp +++ b/src/widgets/accountpopup.cpp @@ -155,7 +155,7 @@ AccountPopupWidget::AccountPopupWidget(ChannelPtr _channel) this->hide(); // }); - this->dpiMultiplierChanged(this->getDpiMultiplier(), this->getDpiMultiplier()); + this->scaleChangedEvent(this->getScale()); } void AccountPopupWidget::setName(const QString &name) @@ -247,7 +247,7 @@ void AccountPopupWidget::loadAvatar(const QUrl &avatarUrl) } } -void AccountPopupWidget::dpiMultiplierChanged(float /*oldDpi*/, float newDpi) +void AccountPopupWidget::scaleChangedEvent(float newDpi) { this->setStyleSheet(QString("* { font-size: px; }") .replace("", QString::number((int)(12 * newDpi)))); diff --git a/src/widgets/accountpopup.hpp b/src/widgets/accountpopup.hpp index aeac473b4..b2ec8b522 100644 --- a/src/widgets/accountpopup.hpp +++ b/src/widgets/accountpopup.hpp @@ -35,7 +35,7 @@ signals: void refreshButtons(); protected: - virtual void dpiMultiplierChanged(float oldDpi, float newDpi) override; + virtual void scaleChangedEvent(float newDpi) override; private: Ui::AccountPopup *ui; diff --git a/src/widgets/basewidget.cpp b/src/widgets/basewidget.cpp index 808a59412..c15ef9cbf 100644 --- a/src/widgets/basewidget.cpp +++ b/src/widgets/basewidget.cpp @@ -2,6 +2,7 @@ #include "singletons/settingsmanager.hpp" #include "singletons/thememanager.hpp" +#include #include #include #include @@ -25,13 +26,7 @@ BaseWidget::BaseWidget(BaseWidget *parent, Qt::WindowFlags f) this->init(); } -BaseWidget::BaseWidget(QWidget *parent, Qt::WindowFlags f) - : QWidget(parent, f) - , themeManager(singletons::ThemeManager::getInstance()) -{ -} - -float BaseWidget::getDpiMultiplier() +float BaseWidget::getScale() const { // return 1.f; BaseWidget *baseWidget = dynamic_cast(this->window()); @@ -39,17 +34,14 @@ float BaseWidget::getDpiMultiplier() if (baseWidget == nullptr) { return 1.f; } else { - return baseWidget->dpiMultiplier; - // int screenNr = QApplication::desktop()->screenNumber(this); - // QScreen *screen = QApplication::screens().at(screenNr); - // return screen->logicalDotsPerInch() / 96.f; + return baseWidget->scale; } } void BaseWidget::init() { auto connection = this->themeManager.updated.connect([this]() { - this->refreshTheme(); + this->themeRefreshEvent(); this->update(); }); @@ -59,7 +51,66 @@ void BaseWidget::init() }); } -void BaseWidget::refreshTheme() +void BaseWidget::childEvent(QChildEvent *event) +{ + if (event->added()) { + BaseWidget *widget = dynamic_cast(event->child()); + + if (widget) { + this->widgets.push_back(widget); + } + } else if (event->removed()) { + for (auto it = this->widgets.begin(); it != this->widgets.end(); it++) { + if (*it == event->child()) { + this->widgets.erase(it); + break; + } + } + } +} +void BaseWidget::setScale(float value) +{ + // update scale value + this->scale = value; + + this->scaleChangedEvent(value); + this->scaleChanged.invoke(value); + + // set scale for all children + BaseWidget::setScaleRecursive(value, this); +} + +void BaseWidget::setScaleRecursive(float scale, QObject *object) +{ + for (QObject *child : object->children()) { + BaseWidget *widget = dynamic_cast(child); + if (widget != nullptr) { + widget->setScale(scale); + continue; + } + + // QLayout *layout = nullptr; + // QWidget *widget = dynamic_cast(child); + + // if (widget != nullptr) { + // layout = widget->layout(); + // } + + // else { + QLayout *layout = dynamic_cast(object); + + if (layout != nullptr) { + setScaleRecursive(scale, layout); + } + // } + } +} + +void BaseWidget::scaleChangedEvent(float newDpi) +{ +} + +void BaseWidget::themeRefreshEvent() { // Do any color scheme updates here } diff --git a/src/widgets/basewidget.hpp b/src/widgets/basewidget.hpp index 0228418b1..86479ab15 100644 --- a/src/widgets/basewidget.hpp +++ b/src/widgets/basewidget.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace chatterino { namespace singletons { @@ -8,6 +9,7 @@ class ThemeManager; } namespace widgets { +class BaseWindow; class BaseWidget : public QWidget { @@ -17,23 +19,30 @@ public: explicit BaseWidget(singletons::ThemeManager &_themeManager, QWidget *parent, Qt::WindowFlags f = Qt::WindowFlags()); explicit BaseWidget(BaseWidget *parent, Qt::WindowFlags f = Qt::WindowFlags()); - explicit BaseWidget(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); singletons::ThemeManager &themeManager; - float getDpiMultiplier(); + float getScale() const; + + pajlada::Signals::Signal scaleChanged; protected: - virtual void dpiMultiplierChanged(float /*oldDpi*/, float /*newDpi*/) - { - } + virtual void childEvent(QChildEvent *) override; - float dpiMultiplier = 1.f; + virtual void scaleChangedEvent(float newScale); + virtual void themeRefreshEvent(); - virtual void refreshTheme(); + void setScale(float value); private: void init(); + float scale = 1.f; + + std::vector widgets; + + static void setScaleRecursive(float scale, QObject *object); + + friend class BaseWindow; }; } // namespace widgets diff --git a/src/widgets/basewindow.cpp b/src/widgets/basewindow.cpp index 9ef8cb871..3bf96fd6c 100644 --- a/src/widgets/basewindow.cpp +++ b/src/widgets/basewindow.cpp @@ -44,7 +44,7 @@ BaseWindow::BaseWindow(BaseWidget *parent, bool _enableCustomFrame) } BaseWindow::BaseWindow(QWidget *parent, bool _enableCustomFrame) - : BaseWidget(parent, Qt::Window) + : BaseWidget(singletons::ThemeManager::getInstance(), parent, Qt::Window) , enableCustomFrame(_enableCustomFrame) { this->init(); @@ -57,12 +57,12 @@ void BaseWindow::init() #ifdef USEWINSDK if (this->hasCustomWindowFrame()) { // CUSTOM WINDOW FRAME - QVBoxLayout *layout = new QVBoxLayout; + QVBoxLayout *layout = new QVBoxLayout(); layout->setMargin(1); layout->setSpacing(0); this->setLayout(layout); { - QHBoxLayout *buttonLayout = this->titlebarBox = new QHBoxLayout; + QHBoxLayout *buttonLayout = this->titlebarBox = new QHBoxLayout(); buttonLayout->setMargin(0); layout->addLayout(buttonLayout); @@ -107,7 +107,7 @@ void BaseWindow::init() buttonLayout->addWidget(_exitButton); buttonLayout->setSpacing(0); } - this->layoutBase = new QWidget(this); + this->layoutBase = new BaseWidget(this); layout->addWidget(this->layoutBase); } @@ -115,10 +115,10 @@ void BaseWindow::init() auto dpi = util::getWindowDpi(this->winId()); if (dpi) { - this->dpiMultiplier = dpi.value() / 96.f; + this->scale = dpi.value() / 96.f; } - this->dpiMultiplierChanged(1, this->dpiMultiplier); + this->scaleChangedEvent(this->scale); #endif if (singletons::SettingManager::getInstance().windowTopMost.getValue()) { @@ -155,7 +155,7 @@ bool BaseWindow::hasCustomWindowFrame() #endif } -void BaseWindow::refreshTheme() +void BaseWindow::themeRefreshEvent() { QPalette palette; palette.setColor(QPalette::Background, this->themeManager.windowBg); @@ -249,14 +249,14 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *r qDebug() << "dpi changed"; int dpi = HIWORD(msg->wParam); - float oldDpiMultiplier = this->dpiMultiplier; - this->dpiMultiplier = dpi / 96.f; - float scale = this->dpiMultiplier / oldDpiMultiplier; + float oldScale = this->scale; + float _scale = dpi / 96.f; + float resizeScale = _scale / oldScale; - this->dpiMultiplierChanged(oldDpiMultiplier, this->dpiMultiplier); + this->resize(static_cast(this->width() * resizeScale), + static_cast(this->height() * resizeScale)); - this->resize(static_cast(this->width() * scale), - static_cast(this->height() * scale)); + this->setScale(_scale); return true; } diff --git a/src/widgets/basewindow.hpp b/src/widgets/basewindow.hpp index 2ec8e196f..fc6ccd756 100644 --- a/src/widgets/basewindow.hpp +++ b/src/widgets/basewindow.hpp @@ -40,7 +40,7 @@ protected: virtual void leaveEvent(QEvent *) override; virtual void resizeEvent(QResizeEvent *) override; - virtual void refreshTheme() override; + virtual void themeRefreshEvent() override; private: void init(); diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index 12944c25a..8d7525b2e 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -25,8 +25,7 @@ #include #include -#define LAYOUT_WIDTH \ - (this->width() - (this->scrollBar.isVisible() ? 16 : 4) * this->getDpiMultiplier()) +#define LAYOUT_WIDTH (this->width() - (this->scrollBar.isVisible() ? 16 : 4) * this->getScale()) using namespace chatterino::messages; @@ -115,9 +114,9 @@ ChannelView::~ChannelView() this->messageReplacedConnection.disconnect(); } -void ChannelView::refreshTheme() +void ChannelView::themeRefreshEvent() { - BaseWidget::refreshTheme(); + BaseWidget::themeRefreshEvent(); this->layoutMessages(); } @@ -175,7 +174,7 @@ void ChannelView::actuallyLayoutMessages() for (size_t i = start; i < messagesSnapshot.getLength(); ++i) { auto message = messagesSnapshot[i]; - redrawRequired |= message->layout(layoutWidth, this->getDpiMultiplier(), flags); + redrawRequired |= message->layout(layoutWidth, this->getScale(), flags); y += message->getHeight(); @@ -191,7 +190,7 @@ void ChannelView::actuallyLayoutMessages() for (int i = (int)messagesSnapshot.getLength() - 1; i >= 0; i--) { auto *message = messagesSnapshot[i].get(); - message->layout(layoutWidth, this->getDpiMultiplier(), flags); + message->layout(layoutWidth, this->getScale(), flags); h -= message->getHeight(); @@ -582,8 +581,7 @@ void ChannelView::wheelEvent(QWheelEvent *event) if (i == 0) { desired = 0; } else { - snapshot[i - 1]->layout(LAYOUT_WIDTH, this->getDpiMultiplier(), - this->getFlags()); + snapshot[i - 1]->layout(LAYOUT_WIDTH, this->getScale(), this->getFlags()); scrollFactor = 1; currentScrollLeft = snapshot[i - 1]->getHeight(); } @@ -605,8 +603,7 @@ void ChannelView::wheelEvent(QWheelEvent *event) if (i == snapshotLength - 1) { desired = snapshot.getLength(); } else { - snapshot[i + 1]->layout(LAYOUT_WIDTH, this->getDpiMultiplier(), - this->getFlags()); + snapshot[i + 1]->layout(LAYOUT_WIDTH, this->getScale(), this->getFlags()); scrollFactor = 1; currentScrollLeft = snapshot[i + 1]->getHeight(); diff --git a/src/widgets/helper/channelview.hpp b/src/widgets/helper/channelview.hpp index 113e3b981..9212e3368 100644 --- a/src/widgets/helper/channelview.hpp +++ b/src/widgets/helper/channelview.hpp @@ -53,7 +53,7 @@ public: pajlada::Signals::Signal linkClicked; protected: - virtual void refreshTheme() override; + virtual void themeRefreshEvent() override; virtual void resizeEvent(QResizeEvent *) override; diff --git a/src/widgets/helper/notebooktab.cpp b/src/widgets/helper/notebooktab.cpp index fc91dd4be..0034dc76e 100644 --- a/src/widgets/helper/notebooktab.cpp +++ b/src/widgets/helper/notebooktab.cpp @@ -25,7 +25,6 @@ NotebookTab::NotebookTab(Notebook *_notebook, const std::string &_uuid) , useDefaultBehaviour(fS("{}/useDefaultBehaviour", this->settingRoot), true) , menu(this) { - this->calcSize(); this->setAcceptDrops(true); this->positionChangedAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InCubic)); @@ -65,22 +64,25 @@ NotebookTab::NotebookTab(Notebook *_notebook, const std::string &_uuid) this->menu.addAction(enableHighlightsOnNewMessageAction); - connect(enableHighlightsOnNewMessageAction, &QAction::toggled, [this](bool newValue) { + QObject::connect(enableHighlightsOnNewMessageAction, &QAction::toggled, [this](bool newValue) { debug::Log("New value is {}", newValue); // }); } -void NotebookTab::calcSize() +void NotebookTab::themeRefreshEvent() { - float scale = getDpiMultiplier(); + this->update(); +} + +void NotebookTab::updateSize() +{ + float scale = getScale(); QString qTitle(qS(this->title)); if (singletons::SettingManager::getInstance().hideTabX) { - this->resize(static_cast((fontMetrics().width(qTitle) + 16) * scale), - static_cast(24 * scale)); + this->resize((int)((fontMetrics().width(qTitle) + 16) * scale), (int)(24 * scale)); } else { - this->resize(static_cast((fontMetrics().width(qTitle) + 8 + 24) * scale), - static_cast(24 * scale)); + this->resize((int)((fontMetrics().width(qTitle) + 8 + 24) * scale), (int)(24 * scale)); } if (this->parent() != nullptr) { @@ -97,7 +99,7 @@ void NotebookTab::setTitle(const QString &newTitle) { this->title = newTitle.toStdString(); - this->calcSize(); + this->updateSize(); } bool NotebookTab::isSelected() const @@ -134,7 +136,7 @@ QRect NotebookTab::getDesiredRect() const void NotebookTab::hideTabXChanged(bool) { - this->calcSize(); + this->updateSize(); this->update(); } @@ -197,7 +199,7 @@ void NotebookTab::paintEvent(QPaintEvent *) painter.setPen(colors.text); // set area for text - float scale = this->getDpiMultiplier(); + float scale = this->getScale(); int rectW = (settingManager.hideTabX ? 0 : static_cast(16) * scale); QRect rect(0, 0, this->width() - rectW, this->height()); diff --git a/src/widgets/helper/notebooktab.hpp b/src/widgets/helper/notebooktab.hpp index f51cc9977..f97171932 100644 --- a/src/widgets/helper/notebooktab.hpp +++ b/src/widgets/helper/notebooktab.hpp @@ -25,7 +25,7 @@ class NotebookTab : public BaseWidget public: explicit NotebookTab(Notebook *_notebook, const std::string &_uuid); - void calcSize(); + void updateSize(); SplitContainer *page; @@ -42,16 +42,18 @@ public: void hideTabXChanged(bool); protected: - void paintEvent(QPaintEvent *) override; + virtual void themeRefreshEvent() override; - void mousePressEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void enterEvent(QEvent *) override; - void leaveEvent(QEvent *) override; + virtual void paintEvent(QPaintEvent *) override; - void dragEnterEvent(QDragEnterEvent *event) override; + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void mouseReleaseEvent(QMouseEvent *event) override; + virtual void enterEvent(QEvent *) override; + virtual void leaveEvent(QEvent *) override; - void mouseMoveEvent(QMouseEvent *event) override; + virtual void dragEnterEvent(QDragEnterEvent *event) override; + + virtual void mouseMoveEvent(QMouseEvent *event) override; private: std::vector managedConnections; @@ -80,7 +82,7 @@ private: QRect getXRect() { - float scale = this->getDpiMultiplier(); + float scale = this->getScale(); return QRect(this->width() - static_cast(20 * scale), static_cast(4 * scale), static_cast(16 * scale), static_cast(16 * scale)); } diff --git a/src/widgets/helper/rippleeffectbutton.cpp b/src/widgets/helper/rippleeffectbutton.cpp index 6ce8d243f..9462cf118 100644 --- a/src/widgets/helper/rippleeffectbutton.cpp +++ b/src/widgets/helper/rippleeffectbutton.cpp @@ -44,7 +44,7 @@ void RippleEffectButton::paintEvent(QPaintEvent *) if (this->pixmap != nullptr) { QRect rect = this->rect(); - int xD = 6 * this->getDpiMultiplier(); + int xD = 6 * this->getScale(); rect.moveLeft(xD); rect.setRight(rect.right() - xD - xD); diff --git a/src/widgets/helper/splitheader.cpp b/src/widgets/helper/splitheader.cpp index 1390845c0..b9f09925d 100644 --- a/src/widgets/helper/splitheader.cpp +++ b/src/widgets/helper/splitheader.cpp @@ -66,7 +66,7 @@ SplitHeader::SplitHeader(Split *_split) // ---- misc this->layout()->setMargin(0); - this->refreshTheme(); + this->themeRefreshEvent(); this->updateChannelText(); @@ -135,7 +135,7 @@ void SplitHeader::initializeChannelSignals() void SplitHeader::resizeEvent(QResizeEvent *event) { - int w = 28 * getDpiMultiplier(); + int w = 28 * getScale(); this->setFixedHeight(w); this->dropdownButton->setFixedWidth(w); @@ -242,7 +242,7 @@ void SplitHeader::rightButtonClicked() { } -void SplitHeader::refreshTheme() +void SplitHeader::themeRefreshEvent() { QPalette palette; palette.setColor(QPalette::Foreground, this->themeManager.splits.header.text); diff --git a/src/widgets/helper/splitheader.hpp b/src/widgets/helper/splitheader.hpp index 900f151dc..568b75158 100644 --- a/src/widgets/helper/splitheader.hpp +++ b/src/widgets/helper/splitheader.hpp @@ -57,7 +57,7 @@ private: void rightButtonClicked(); - virtual void refreshTheme() override; + virtual void themeRefreshEvent() override; void initializeChannelSignals(); diff --git a/src/widgets/helper/splitinput.cpp b/src/widgets/helper/splitinput.cpp index 8e17a0500..06c4638ed 100644 --- a/src/widgets/helper/splitinput.cpp +++ b/src/widgets/helper/splitinput.cpp @@ -4,6 +4,7 @@ #include "singletons/ircmanager.hpp" #include "singletons/settingsmanager.hpp" #include "singletons/thememanager.hpp" +#include "util/layoutcreator.hpp" #include "widgets/notebook.hpp" #include "widgets/split.hpp" #include "widgets/splitcontainer.hpp" @@ -17,41 +18,59 @@ namespace widgets { SplitInput::SplitInput(Split *_chatWidget) : BaseWidget(_chatWidget) , chatWidget(_chatWidget) - , emotesLabel(this) { - this->setLayout(&this->hbox); + this->initLayout(); - this->hbox.setMargin(4); + // auto completion + auto completer = new QCompleter( + singletons::CompletionManager::getInstance().createModel(this->chatWidget->channelName)); - this->hbox.addLayout(&this->editContainer); - this->hbox.addLayout(&this->vbox); + this->ui.textEdit->setCompleter(completer); + // misc + this->installKeyPressedEvent(); + this->themeRefreshEvent(); + this->scaleChangedEvent(this->getScale()); +} + +void SplitInput::initLayout() +{ auto &fontManager = singletons::FontManager::getInstance(); + util::LayoutCreator layoutCreator(this); + + auto layout = layoutCreator.setLayoutType().withoutMargin().assign(&this->ui.hbox); + + // input + auto textEdit = layout.emplace().assign(&this->ui.textEdit); + connect(textEdit.getElement(), &ResizingTextEdit::textChanged, this, + &SplitInput::editTextChanged); + + // right box + auto box = layout.emplace().withoutMargin(); + box->setSpacing(0); + { + auto textEditLength = box.emplace().assign(&this->ui.textEditLength); + textEditLength->setAlignment(Qt::AlignRight); + + box->addStretch(1); + box.emplace().assign(&this->ui.emoteButton); + } + + this->ui.emoteButton->getLabel().setTextFormat(Qt::RichText); + + // ---- misc + + // set edit font + this->ui.textEdit->setFont( + fontManager.getFont(singletons::FontManager::Type::Medium, this->getScale())); - this->textInput.setFont( - fontManager.getFont(singletons::FontManager::Type::Medium, this->getDpiMultiplier())); this->managedConnections.emplace_back(fontManager.fontChanged.connect([this, &fontManager]() { - this->textInput.setFont( - fontManager.getFont(singletons::FontManager::Type::Medium, this->getDpiMultiplier())); + this->ui.textEdit->setFont( + fontManager.getFont(singletons::FontManager::Type::Medium, this->getScale())); })); - this->editContainer.addWidget(&this->textInput); - this->editContainer.setMargin(2); - - this->emotesLabel.setMinimumHeight(24); - - this->vbox.addWidget(&this->textLengthLabel); - this->vbox.addStretch(1); - this->vbox.addWidget(&this->emotesLabel); - - this->textLengthLabel.setText(""); - this->textLengthLabel.setAlignment(Qt::AlignRight); - - this->emotesLabel.getLabel().setTextFormat(Qt::RichText); - this->emotesLabel.getLabel().setText(""); - - connect(&this->emotesLabel, &RippleEffectLabel::clicked, [this] { + // open emote popup + QObject::connect(this->ui.emoteButton, &RippleEffectLabel::clicked, [this] { if (!this->emotePopup) { this->emotePopup = std::make_unique(this->themeManager); this->emotePopup->linkClicked.connect([this](const messages::Link &link) { @@ -61,29 +80,62 @@ SplitInput::SplitInput(Split *_chatWidget) }); } - this->emotePopup->resize((int)(300 * this->emotePopup->getDpiMultiplier()), - (int)(500 * this->emotePopup->getDpiMultiplier())); + this->emotePopup->resize((int)(300 * this->emotePopup->getScale()), + (int)(500 * this->emotePopup->getScale())); this->emotePopup->loadChannel(this->chatWidget->getChannel()); this->emotePopup->show(); }); - connect(&textInput, &ResizingTextEdit::textChanged, this, &SplitInput::editTextChanged); + // clear channelview selection when selecting in the input + QObject::connect(this->ui.textEdit, &QTextEdit::copyAvailable, [this](bool available) { + if (available) { + this->chatWidget->view.clearSelection(); + } + }); - this->refreshTheme(); - textLengthLabel.setHidden(!singletons::SettingManager::getInstance().showMessageLength); + // textEditLength visibility + singletons::SettingManager::getInstance().showMessageLength.connect( + [this](const bool &value, auto) { this->ui.textEditLength->setHidden(!value); }, + this->managedConnections); +} - auto completer = new QCompleter( - singletons::CompletionManager::getInstance().createModel(this->chatWidget->channelName)); +void SplitInput::scaleChangedEvent(float scale) +{ + // update the icon size of the emote button + QString text = ""; + text.replace("xD", QString::number((int)12 * scale)); - this->textInput.setCompleter(completer); + this->ui.emoteButton->getLabel().setText(text); + this->ui.emoteButton->setFixedHeight((int)18 * scale); - this->textInput.keyPressed.connect([this](QKeyEvent *event) { + // set maximum height + this->setMaximumHeight((int)(150 * this->getScale())); + + this->themeRefreshEvent(); +} + +void SplitInput::themeRefreshEvent() +{ + QPalette palette; + + palette.setColor(QPalette::Foreground, this->themeManager.splits.input.text); + + this->ui.textEditLength->setPalette(palette); + + this->ui.textEdit->setStyleSheet(this->themeManager.splits.input.styleSheet); + + this->ui.hbox->setMargin((this->themeManager.isLightTheme() ? 4 : 2) * this->getScale()); +} + +void SplitInput::installKeyPressedEvent() +{ + this->ui.textEdit->keyPressed.connect([this](QKeyEvent *event) { if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) { auto c = this->chatWidget->getChannel(); if (c == nullptr) { return; } - QString message = textInput.toPlainText(); + QString message = ui.textEdit->toPlainText(); QString sendMessage = singletons::CommandManager::getInstance().execCommand(message, c, false); @@ -94,9 +146,9 @@ SplitInput::SplitInput(Split *_chatWidget) event->accept(); if (!(event->modifiers() == Qt::ControlModifier)) { - this->textInput.setText(QString()); + this->ui.textEdit->setText(QString()); this->prevIndex = 0; - } else if (this->textInput.toPlainText() == + } else if (this->ui.textEdit->toPlainText() == this->prevMsg.at(this->prevMsg.size() - 1)) { this->prevMsg.removeLast(); } @@ -115,7 +167,7 @@ SplitInput::SplitInput(Split *_chatWidget) } else { if (this->prevMsg.size() && this->prevIndex) { this->prevIndex--; - this->textInput.setText(this->prevMsg.at(this->prevIndex)); + this->ui.textEdit->setText(this->prevMsg.at(this->prevIndex)); } } } else if (event->key() == Qt::Key_Down) { @@ -133,10 +185,10 @@ SplitInput::SplitInput(Split *_chatWidget) if (this->prevIndex != (this->prevMsg.size() - 1) && this->prevIndex != this->prevMsg.size()) { this->prevIndex++; - this->textInput.setText(this->prevMsg.at(this->prevIndex)); + this->ui.textEdit->setText(this->prevMsg.at(this->prevIndex)); } else { this->prevIndex = this->prevMsg.size(); - this->textInput.setText(QString()); + this->ui.textEdit->setText(QString()); } } } else if (event->key() == Qt::Key_Left) { @@ -188,54 +240,32 @@ SplitInput::SplitInput(Split *_chatWidget) } } }); - - singletons::SettingManager::getInstance().showMessageLength.connect( - [this](const bool &value, auto) { this->textLengthLabel.setHidden(!value); }, - this->managedConnections); - - QObject::connect(&this->textInput, &QTextEdit::copyAvailable, [this](bool available) { - if (available) { - this->chatWidget->view.clearSelection(); - } - }); } void SplitInput::clearSelection() { - QTextCursor c = this->textInput.textCursor(); + QTextCursor c = this->ui.textEdit->textCursor(); c.setPosition(c.position()); c.setPosition(c.position(), QTextCursor::KeepAnchor); - this->textInput.setTextCursor(c); + this->ui.textEdit->setTextCursor(c); } QString SplitInput::getInputText() const { - return this->textInput.toPlainText(); + return this->ui.textEdit->toPlainText(); } void SplitInput::insertText(const QString &text) { - this->textInput.insertPlainText(text); -} - -void SplitInput::refreshTheme() -{ - QPalette palette; - - palette.setColor(QPalette::Foreground, this->themeManager.splits.input.text); - - this->textLengthLabel.setPalette(palette); - - this->textInput.setStyleSheet(this->themeManager.splits.input.styleSheet); - - this->hbox.setMargin((this->themeManager.isLightTheme() ? 4 : 2) * this->getDpiMultiplier()); + this->ui.textEdit->insertPlainText(text); } void SplitInput::editTextChanged() { - QString text = this->textInput.toPlainText(); + // set textLengthLabel value + QString text = this->ui.textEdit->toPlainText(); this->textChanged.invoke(text); @@ -254,7 +284,7 @@ void SplitInput::editTextChanged() labelText = QString::number(text.length()); } - this->textLengthLabel.setText(labelText); + this->ui.textEditLength->setText(labelText); } void SplitInput::paintEvent(QPaintEvent *) @@ -265,7 +295,7 @@ void SplitInput::paintEvent(QPaintEvent *) QPen pen(this->themeManager.splits.input.border); if (this->themeManager.isLightTheme()) { - pen.setWidth((int)(6 * this->getDpiMultiplier())); + pen.setWidth((int)(6 * this->getScale())); } painter.setPen(pen); painter.drawRect(0, 0, this->width() - 1, this->height() - 1); @@ -274,14 +304,10 @@ void SplitInput::paintEvent(QPaintEvent *) void SplitInput::resizeEvent(QResizeEvent *) { if (this->height() == this->maximumHeight()) { - this->textInput.setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + this->ui.textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { - this->textInput.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + this->ui.textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } - - this->setMaximumHeight((int)(150 * this->getDpiMultiplier())); - - this->refreshTheme(); } void SplitInput::mousePressEvent(QMouseEvent *) diff --git a/src/widgets/helper/splitinput.hpp b/src/widgets/helper/splitinput.hpp index 951efd9c2..9718b984e 100644 --- a/src/widgets/helper/splitinput.hpp +++ b/src/widgets/helper/splitinput.hpp @@ -32,6 +32,8 @@ public: pajlada::Signals::Signal textChanged; protected: + virtual void scaleChangedEvent(float scale) override; + virtual void paintEvent(QPaintEvent *) override; virtual void resizeEvent(QResizeEvent *) override; @@ -41,16 +43,27 @@ private: Split *const chatWidget; std::unique_ptr emotePopup; + struct { + ResizingTextEdit *textEdit; + QLabel *textEditLength; + RippleEffectLabel *emoteButton; + + QHBoxLayout *hbox; + } ui; + std::vector managedConnections; - QHBoxLayout hbox; - QVBoxLayout vbox; - QHBoxLayout editContainer; - ResizingTextEdit textInput; - QLabel textLengthLabel; - RippleEffectLabel emotesLabel; + // QHBoxLayout hbox; + // QVBoxLayout vbox; + // QHBoxLayout editContainer; + // ResizingTextEdit textInput; + // QLabel textLengthLabel; + // RippleEffectLabel emotesLabel; QStringList prevMsg; int prevIndex = 0; - virtual void refreshTheme() override; + + void initLayout(); + void installKeyPressedEvent(); + virtual void themeRefreshEvent() override; private slots: void editTextChanged(); diff --git a/src/widgets/notebook.cpp b/src/widgets/notebook.cpp index 1849f6341..3158c24b9 100644 --- a/src/widgets/notebook.cpp +++ b/src/widgets/notebook.cpp @@ -53,6 +53,8 @@ Notebook::Notebook(Window *parent, bool _showButtons, const std::string &setting closeConfirmDialog.setIcon(QMessageBox::Icon::Question); closeConfirmDialog.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); closeConfirmDialog.setDefaultButton(QMessageBox::Yes); + + // this->scaleChangedEvent(this->getScale()); } SplitContainer *Notebook::addNewPage() @@ -212,7 +214,7 @@ void Notebook::performLayout(bool animated) singletons::SettingManager &settings = singletons::SettingManager::getInstance(); int x = 0, y = 0; - float scale = this->getDpiMultiplier(); + float scale = this->getScale(); bool customFrame = this->parentWindow->hasCustomWindowFrame(); if (!this->showButtons || settings.hidePreferencesButton || customFrame) { @@ -262,17 +264,20 @@ void Notebook::performLayout(bool animated) void Notebook::resizeEvent(QResizeEvent *) { - float scale = this->getDpiMultiplier(); + this->performLayout(false); +} - this->settingsButton.resize(static_cast(24 * scale), static_cast(24 * scale)); - this->userButton.resize(static_cast(24 * scale), static_cast(24 * scale)); - this->addButton.resize(static_cast(24 * scale), static_cast(24 * scale)); +void Notebook::scaleChangedEvent(float) +{ + float h = 24 * this->getScale(); + + this->settingsButton.setFixedSize(h, h); + this->userButton.setFixedSize(h, h); + this->addButton.setFixedSize(h, h); for (auto &i : this->pages) { - i->getTab()->calcSize(); + i->getTab()->updateSize(); } - - this->performLayout(false); } void Notebook::settingsButtonClicked() diff --git a/src/widgets/notebook.hpp b/src/widgets/notebook.hpp index 4c24d19a8..082768afd 100644 --- a/src/widgets/notebook.hpp +++ b/src/widgets/notebook.hpp @@ -48,6 +48,7 @@ public: void previousTab(); protected: + void scaleChangedEvent(float scale); void resizeEvent(QResizeEvent *); void settingsButtonMouseReleased(QMouseEvent *event); diff --git a/src/widgets/scrollbar.cpp b/src/widgets/scrollbar.cpp index 08755bf6a..720c2e88e 100644 --- a/src/widgets/scrollbar.cpp +++ b/src/widgets/scrollbar.cpp @@ -18,7 +18,7 @@ Scrollbar::Scrollbar(ChannelView *parent) , currentValueAnimation(this, "currentValue") , smoothScrollingSetting(singletons::SettingManager::getInstance().enableSmoothScrolling) { - resize((int)(16 * this->getDpiMultiplier()), 100); + resize((int)(16 * this->getScale()), 100); this->currentValueAnimation.setDuration(150); this->currentValueAnimation.setEasingCurve(QEasingCurve(QEasingCurve::OutCubic)); @@ -29,7 +29,7 @@ Scrollbar::Scrollbar(ChannelView *parent) timer->setSingleShot(true); connect(timer, &QTimer::timeout, [=]() { - resize((int)(16 * this->getDpiMultiplier()), 100); + resize((int)(16 * this->getScale()), 100); timer->deleteLater(); }); @@ -194,7 +194,7 @@ void Scrollbar::printCurrentState(const QString &prefix) const void Scrollbar::paintEvent(QPaintEvent *) { bool mouseOver = this->mouseOverIndex != -1; - int xOffset = mouseOver ? 0 : width() - (int)(4 * this->getDpiMultiplier()); + int xOffset = mouseOver ? 0 : width() - (int)(4 * this->getScale()); QPainter painter(this); // painter.fillRect(rect(), this->themeManager.ScrollbarBG); @@ -248,7 +248,7 @@ void Scrollbar::paintEvent(QPaintEvent *) void Scrollbar::resizeEvent(QResizeEvent *) { - this->resize((int)(16 * this->getDpiMultiplier()), this->height()); + this->resize((int)(16 * this->getScale()), this->height()); } void Scrollbar::mouseMoveEvent(QMouseEvent *event) diff --git a/src/widgets/settingsdialog.cpp b/src/widgets/settingsdialog.cpp index 2b1f20c9c..039c6ee8b 100644 --- a/src/widgets/settingsdialog.cpp +++ b/src/widgets/settingsdialog.cpp @@ -29,7 +29,7 @@ SettingsDialog::SettingsDialog() this->addTabs(); - this->dpiMultiplierChanged(this->getDpiMultiplier(), this->getDpiMultiplier()); + this->scaleChangedEvent(this->getScale()); } void SettingsDialog::initUi() @@ -149,7 +149,7 @@ void SettingsDialog::refresh() singletons::SettingManager::getInstance().saveSnapshot(); } -void SettingsDialog::dpiMultiplierChanged(float oldDpi, float newDpi) +void SettingsDialog::scaleChangedEvent(float newDpi) { QFile file(":/qss/settings.qss"); file.open(QFile::ReadOnly); diff --git a/src/widgets/settingsdialog.hpp b/src/widgets/settingsdialog.hpp index d870e1931..2feecfc99 100644 --- a/src/widgets/settingsdialog.hpp +++ b/src/widgets/settingsdialog.hpp @@ -32,7 +32,7 @@ public: static void showDialog(PreferredTab preferredTab = PreferredTab::NoPreference); protected: - virtual void dpiMultiplierChanged(float oldDpi, float newDpi) override; + virtual void scaleChangedEvent(float newDpi) override; private: void refresh(); diff --git a/src/widgets/split.cpp b/src/widgets/split.cpp index d61fcebc2..6f5952b06 100644 --- a/src/widgets/split.cpp +++ b/src/widgets/split.cpp @@ -87,7 +87,7 @@ Split::Split(SplitContainer *parent, const std::string &_uuid) this->channelNameUpdated(this->channelName.getValue()); - this->input.textInput.installEventFilter(parent); + this->input.ui.textEdit->installEventFilter(parent); this->view.mouseDown.connect([this](QMouseEvent *) { this->giveFocus(Qt::MouseFocusReason); }); this->view.selectionChanged.connect([this]() { @@ -259,12 +259,12 @@ void Split::updateLastReadMessage() void Split::giveFocus(Qt::FocusReason reason) { - this->input.textInput.setFocus(reason); + this->input.ui.textEdit->setFocus(reason); } bool Split::hasFocus() const { - return this->input.textInput.hasFocus(); + return this->input.ui.textEdit->hasFocus(); } void Split::paintEvent(QPaintEvent *) diff --git a/src/widgets/tooltipwidget.cpp b/src/widgets/tooltipwidget.cpp index fa7e9c5dc..3d4ed9e66 100644 --- a/src/widgets/tooltipwidget.cpp +++ b/src/widgets/tooltipwidget.cpp @@ -40,7 +40,7 @@ TooltipWidget::~TooltipWidget() this->fontChangedConnection.disconnect(); } -void TooltipWidget::dpiMultiplierChanged(float, float) +void TooltipWidget::scaleChangedEvent(float) { this->updateFont(); } @@ -48,7 +48,7 @@ void TooltipWidget::dpiMultiplierChanged(float, float) void TooltipWidget::updateFont() { this->setFont(singletons::FontManager::getInstance().getFont( - singletons::FontManager::Type::MediumSmall, this->getDpiMultiplier())); + singletons::FontManager::Type::MediumSmall, this->getScale())); } void TooltipWidget::setText(QString text) diff --git a/src/widgets/tooltipwidget.hpp b/src/widgets/tooltipwidget.hpp index 46fabbccb..96f3af2ca 100644 --- a/src/widgets/tooltipwidget.hpp +++ b/src/widgets/tooltipwidget.hpp @@ -29,7 +29,7 @@ public: protected: virtual void changeEvent(QEvent *) override; virtual void leaveEvent(QEvent *) override; - virtual void dpiMultiplierChanged(float, float) override; + virtual void scaleChangedEvent(float) override; private: QLabel *displayText; diff --git a/src/widgets/window.cpp b/src/widgets/window.cpp index c7ecdb039..0b5f58bcc 100644 --- a/src/widgets/window.cpp +++ b/src/widgets/window.cpp @@ -23,7 +23,7 @@ Window::Window(const QString &windowName, singletons::ThemeManager &_themeManage : BaseWindow(_themeManager, nullptr, true) , settingRoot(fS("/windows/{}", windowName)) , windowGeometry(this->settingRoot) - , dpi(this->getDpiMultiplier()) + , dpi(this->getScale()) , themeManager(_themeManager) , notebook(this, _isMainWindow, this->settingRoot) { @@ -58,7 +58,7 @@ Window::Window(const QString &windowName, singletons::ThemeManager &_themeManage // set margin layout->setMargin(0); - this->refreshTheme(); + this->themeRefreshEvent(); this->loadGeometry(); From 98339830081d732e6b206f929d793e34eafbb68c Mon Sep 17 00:00:00 2001 From: fourtf Date: Thu, 25 Jan 2018 20:50:27 +0100 Subject: [PATCH 25/30] removed vs files --- .vs/ProjectSettings.json | 3 --- .vs/VSWorkspaceState.json | 7 ------- .vs/slnx.sqlite | Bin 73728 -> 0 bytes 3 files changed, 10 deletions(-) delete mode 100644 .vs/ProjectSettings.json delete mode 100644 .vs/VSWorkspaceState.json delete mode 100644 .vs/slnx.sqlite diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json deleted file mode 100644 index 866f1e137..000000000 --- a/.vs/ProjectSettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "CurrentProjectSetting": null -} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json deleted file mode 100644 index b23974898..000000000 --- a/.vs/VSWorkspaceState.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ExpandedNodes": [ - "" - ], - "SelectedNode": "\\ISSUE_TEMPLATE.md", - "PreviewInSolutionExplorer": false -} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite deleted file mode 100644 index e43235e20bc1d323a9dcfe3d8b2b214a77c57261..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73728 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 From ac6cbe9dafd6a7f2952ea417624a788f747d3eef Mon Sep 17 00:00:00 2001 From: fourtf Date: Thu, 25 Jan 2018 20:51:17 +0100 Subject: [PATCH 26/30] fixed + button size --- src/widgets/notebook.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/notebook.cpp b/src/widgets/notebook.cpp index 3158c24b9..dcca1a8bc 100644 --- a/src/widgets/notebook.cpp +++ b/src/widgets/notebook.cpp @@ -54,7 +54,7 @@ Notebook::Notebook(Window *parent, bool _showButtons, const std::string &setting closeConfirmDialog.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); closeConfirmDialog.setDefaultButton(QMessageBox::Yes); - // this->scaleChangedEvent(this->getScale()); + this->scaleChangedEvent(this->getScale()); } SplitContainer *Notebook::addNewPage() From 56a7b051033ce4d5152beb94fe1534cf7644428a Mon Sep 17 00:00:00 2001 From: fourtf Date: Thu, 25 Jan 2018 21:11:14 +0100 Subject: [PATCH 27/30] fixed titlebar scaling --- src/widgets/basewidget.cpp | 45 +++++++++++++++++++++++++++ src/widgets/basewidget.hpp | 10 +++++- src/widgets/basewindow.cpp | 7 +++-- src/widgets/helper/splitheader.cpp | 5 +-- src/widgets/helper/splitheader.hpp | 6 ++-- src/widgets/helper/titlebarbutton.cpp | 9 ------ src/widgets/helper/titlebarbutton.hpp | 1 - 7 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/widgets/basewidget.cpp b/src/widgets/basewidget.cpp index c15ef9cbf..b915abbcb 100644 --- a/src/widgets/basewidget.cpp +++ b/src/widgets/basewidget.cpp @@ -38,6 +38,48 @@ float BaseWidget::getScale() const } } +QSize BaseWidget::getScaleIndependantSize() const +{ + return this->scaleIndependantSize; +} + +int BaseWidget::getScaleIndependantWidth() const +{ + return this->scaleIndependantSize.width(); +} + +int BaseWidget::getScaleIndependantHeight() const +{ + return this->scaleIndependantSize.height(); +} + +void BaseWidget::setScaleIndependantSize(int width, int height) +{ + this->setScaleIndependantSize(QSize(width, height)); +} + +void BaseWidget::setScaleIndependantSize(QSize size) +{ + this->scaleIndependantSize = size; + + if (size.width() > 0) { + this->setFixedWidth((int)(size.width() * this->getScale())); + } + if (size.height() > 0) { + this->setFixedHeight((int)(size.height() * this->getScale())); + } +} + +void BaseWidget::setScaleIndependantWidth(int value) +{ + this->setScaleIndependantSize(QSize(value, this->scaleIndependantSize.height())); +} + +void BaseWidget::setScaleIndependantHeight(int value) +{ + this->setScaleIndependantSize(QSize(this->scaleIndependantSize.height(), value)); +} + void BaseWidget::init() { auto connection = this->themeManager.updated.connect([this]() { @@ -68,6 +110,7 @@ void BaseWidget::childEvent(QChildEvent *event) } } } + void BaseWidget::setScale(float value) { // update scale value @@ -76,6 +119,8 @@ void BaseWidget::setScale(float value) this->scaleChangedEvent(value); this->scaleChanged.invoke(value); + this->setScaleIndependantSize(this->getScaleIndependantSize()); + // set scale for all children BaseWidget::setScaleRecursive(value, this); } diff --git a/src/widgets/basewidget.hpp b/src/widgets/basewidget.hpp index 86479ab15..376d1f210 100644 --- a/src/widgets/basewidget.hpp +++ b/src/widgets/basewidget.hpp @@ -23,9 +23,16 @@ public: singletons::ThemeManager &themeManager; float getScale() const; - pajlada::Signals::Signal scaleChanged; + QSize getScaleIndependantSize() const; + int getScaleIndependantWidth() const; + int getScaleIndependantHeight() const; + void setScaleIndependantSize(int width, int height); + void setScaleIndependantSize(QSize); + void setScaleIndependantWidth(int value); + void setScaleIndependantHeight(int value); + protected: virtual void childEvent(QChildEvent *) override; @@ -37,6 +44,7 @@ protected: private: void init(); float scale = 1.f; + QSize scaleIndependantSize; std::vector widgets; diff --git a/src/widgets/basewindow.cpp b/src/widgets/basewindow.cpp index 3bf96fd6c..c3853b41c 100644 --- a/src/widgets/basewindow.cpp +++ b/src/widgets/basewindow.cpp @@ -73,13 +73,13 @@ void BaseWindow::init() // buttons TitleBarButton *_minButton = new TitleBarButton; - _minButton->setFixedSize(46, 30); + _minButton->setScaleIndependantSize(46, 30); _minButton->setButtonStyle(TitleBarButton::Minimize); TitleBarButton *_maxButton = new TitleBarButton; - _maxButton->setFixedSize(46, 30); + _maxButton->setScaleIndependantSize(46, 30); _maxButton->setButtonStyle(TitleBarButton::Maximize); TitleBarButton *_exitButton = new TitleBarButton; - _exitButton->setFixedSize(46, 30); + _exitButton->setScaleIndependantSize(46, 30); _exitButton->setButtonStyle(TitleBarButton::Close); QObject::connect(_minButton, &TitleBarButton::clicked, this, [this] { @@ -171,6 +171,7 @@ void BaseWindow::addTitleBarButton(const TitleBarButton::Style &style, std::function onClicked) { TitleBarButton *button = new TitleBarButton; + button->setScaleIndependantSize(30, 30); this->buttons.push_back(button); this->titlebarBox->insertWidget(2, button); diff --git a/src/widgets/helper/splitheader.cpp b/src/widgets/helper/splitheader.cpp index b9f09925d..1750331f0 100644 --- a/src/widgets/helper/splitheader.cpp +++ b/src/widgets/helper/splitheader.cpp @@ -67,6 +67,7 @@ SplitHeader::SplitHeader(Split *_split) // ---- misc this->layout()->setMargin(0); this->themeRefreshEvent(); + this->scaleChangedEvent(this->getScale()); this->updateChannelText(); @@ -133,9 +134,9 @@ void SplitHeader::initializeChannelSignals() } } -void SplitHeader::resizeEvent(QResizeEvent *event) +void SplitHeader::scaleChangedEvent(float scale) { - int w = 28 * getScale(); + int w = 28 * scale; this->setFixedHeight(w); this->dropdownButton->setFixedWidth(w); diff --git a/src/widgets/helper/splitheader.hpp b/src/widgets/helper/splitheader.hpp index 568b75158..69c051d4e 100644 --- a/src/widgets/helper/splitheader.hpp +++ b/src/widgets/helper/splitheader.hpp @@ -34,12 +34,14 @@ public: void updateModerationModeIcon(); protected: + virtual void scaleChangedEvent(float) override; + virtual void themeRefreshEvent() override; + virtual void paintEvent(QPaintEvent *) override; virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseMoveEvent(QMouseEvent *event) override; virtual void leaveEvent(QEvent *event) override; virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - virtual void resizeEvent(QResizeEvent *event) override; private: Split *const split; @@ -57,8 +59,6 @@ private: void rightButtonClicked(); - virtual void themeRefreshEvent() override; - void initializeChannelSignals(); QString tooltip; diff --git a/src/widgets/helper/titlebarbutton.cpp b/src/widgets/helper/titlebarbutton.cpp index df5326847..1d3f45aa4 100644 --- a/src/widgets/helper/titlebarbutton.cpp +++ b/src/widgets/helper/titlebarbutton.cpp @@ -18,15 +18,6 @@ void TitleBarButton::setButtonStyle(Style _style) this->update(); } -void TitleBarButton::resizeEvent(QResizeEvent *) -{ - if (this->style & (Maximize | Minimize | Unmaximize | Close)) { - this->setFixedWidth(this->height() * 46 / 30); - } else { - this->setFixedWidth(this->height()); - } -} - void TitleBarButton::paintEvent(QPaintEvent *) { QPainter painter(this); diff --git a/src/widgets/helper/titlebarbutton.hpp b/src/widgets/helper/titlebarbutton.hpp index 83ce40713..cdc13026f 100644 --- a/src/widgets/helper/titlebarbutton.hpp +++ b/src/widgets/helper/titlebarbutton.hpp @@ -16,7 +16,6 @@ public: protected: virtual void paintEvent(QPaintEvent *) override; - virtual void resizeEvent(QResizeEvent *) override; private: Style style; From d33adff5c938605ae50ff6fce635bfa279a3fba4 Mon Sep 17 00:00:00 2001 From: fourtf Date: Sat, 27 Jan 2018 21:13:22 +0100 Subject: [PATCH 28/30] fixed emotes popup emotes --- chatterino.pro | 6 ++- src/messages/messageelement.cpp | 3 +- src/singletons/ircmanager.cpp | 6 +-- src/singletons/thememanager.cpp | 25 +++++---- src/twitch/twitchmessagebuilder.cpp | 2 +- src/widgets/basewindow.cpp | 3 -- src/widgets/emotepopup.cpp | 9 +++- src/widgets/helper/channelview.cpp | 15 +++++- src/widgets/helper/channelview.hpp | 3 ++ src/widgets/helper/label.cpp | 78 +++++++++++++++++++++++++++++ src/widgets/helper/label.hpp | 33 ++++++++++++ src/widgets/helper/splitheader.cpp | 5 ++ src/widgets/helper/splitheader.hpp | 2 + src/widgets/split.cpp | 1 - 14 files changed, 163 insertions(+), 28 deletions(-) create mode 100644 src/widgets/helper/label.cpp create mode 100644 src/widgets/helper/label.hpp diff --git a/chatterino.pro b/chatterino.pro index bd0109acc..304086ea3 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -168,7 +168,8 @@ SOURCES += \ src/widgets/settingspages/ignoremessagespage.cpp \ src/widgets/settingspages/specialchannelspage.cpp \ src/widgets/settingspages/keyboardsettingspage.cpp \ - src/widgets/helper/titlebarbutton.cpp + src/widgets/helper/titlebarbutton.cpp \ + src/widgets/helper/label.cpp HEADERS += \ src/precompiled_header.hpp \ @@ -277,7 +278,8 @@ HEADERS += \ src/widgets/settingspages/specialchannelspage.hpp \ src/widgets/settingspages/keyboardsettings.hpp \ src/widgets/settingspages/keyboardsettingspage.hpp \ - src/widgets/helper/titlebarbutton.hpp + src/widgets/helper/titlebarbutton.hpp \ + src/widgets/helper/label.hpp RESOURCES += \ resources/resources.qrc diff --git a/src/messages/messageelement.cpp b/src/messages/messageelement.cpp index 42976eea1..7285d2721 100644 --- a/src/messages/messageelement.cpp +++ b/src/messages/messageelement.cpp @@ -78,7 +78,6 @@ EmoteElement::EmoteElement(const util::EmoteData &_data, MessageElement::Flags f { if (_data.isValid()) { this->setTooltip(data.image1x->getTooltip()); - qDebug() << "valid xDDDDDDDDD" << _data.image1x->getName(); this->textElement = new TextElement(_data.image1x->getName(), MessageElement::Misc); } } @@ -93,7 +92,7 @@ EmoteElement::~EmoteElement() void EmoteElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags _flags) { if (_flags & this->getFlags()) { - if (_flags & this->getFlags() & MessageElement::EmoteImages) { + if (_flags & MessageElement::EmoteImages) { if (!this->data.isValid()) { return; } diff --git a/src/singletons/ircmanager.cpp b/src/singletons/ircmanager.cpp index aa5753cf0..0103d7840 100644 --- a/src/singletons/ircmanager.cpp +++ b/src/singletons/ircmanager.cpp @@ -246,10 +246,10 @@ void IrcManager::privateMessageReceived(Communi::IrcPrivateMessage *message) return; } - auto xd = message->content(); - auto xd2 = message->toData(); + // auto xd = message->content(); + // auto xd2 = message->toData(); - debug::Log("HEHE: {}", xd2.toStdString()); + // debug::Log("HEHE: {}", xd2.toStdString()); messages::MessageParseArgs args; diff --git a/src/singletons/thememanager.cpp b/src/singletons/thememanager.cpp index c729d04b3..fb9ff0043 100644 --- a/src/singletons/thememanager.cpp +++ b/src/singletons/thememanager.cpp @@ -59,24 +59,23 @@ void ThemeManager::actuallyUpdate(double hue, double multiplier) QColor themeColor = QColor::fromHslF(hue, 0.5, 0.5); QColor themeColorNoSat = QColor::fromHslF(hue, 0, 0.5); - //#ifdef USEWINSDK - // isLightTabs = isLight; - // QColor tabFg = isLight ? "#000" : "#fff"; - // this->windowBg = isLight ? "#fff" : "#444"; - - //#else - isLightTabs = true; - QColor tabFg = isLightTabs ? "#000" : "#fff"; - this->windowBg = "#fff"; - - //#endif - - qreal sat = 0.05; + qreal sat = 0.1; + // 0.05; auto getColor = [multiplier](double h, double s, double l, double a = 1.0) { return QColor::fromHslF(h, s, ((l - 0.5) * multiplier) + 0.5, a); }; + //#ifdef USEWINSDK + // isLightTabs = isLight; + // QColor tabFg = isLight ? "#000" : "#fff"; + // this->windowBg = isLight ? "#fff" : getColor(0, sat, 0.9); + //#else + isLightTabs = true; + QColor tabFg = isLightTabs ? "#000" : "#fff"; + this->windowBg = "#fff"; + //#endif + // Ubuntu style // TODO: add setting for this // TabText = QColor(210, 210, 210); diff --git a/src/twitch/twitchmessagebuilder.cpp b/src/twitch/twitchmessagebuilder.cpp index 1c841ac5d..7aea3714b 100644 --- a/src/twitch/twitchmessagebuilder.cpp +++ b/src/twitch/twitchmessagebuilder.cpp @@ -457,7 +457,7 @@ bool TwitchMessageBuilder::tryAppendEmote(QString &emoteString) singletons::EmoteManager &emoteManager = singletons::EmoteManager::getInstance(); util::EmoteData emoteData; - auto appendEmote = [=](MessageElement::Flags flags) { + auto appendEmote = [&](MessageElement::Flags flags) { this->emplace(emoteData, flags); return true; }; diff --git a/src/widgets/basewindow.cpp b/src/widgets/basewindow.cpp index c3853b41c..eb780184d 100644 --- a/src/widgets/basewindow.cpp +++ b/src/widgets/basewindow.cpp @@ -247,7 +247,6 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *r switch (msg->message) { case WM_DPICHANGED: { - qDebug() << "dpi changed"; int dpi = HIWORD(msg->wParam); float oldScale = this->scale; @@ -349,8 +348,6 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *r } } - qDebug() << *result; - return true; } else { return QWidget::nativeEvent(eventType, message, result); diff --git a/src/widgets/emotepopup.cpp b/src/widgets/emotepopup.cpp index e2aa58256..21c0e47e8 100644 --- a/src/widgets/emotepopup.cpp +++ b/src/widgets/emotepopup.cpp @@ -18,6 +18,11 @@ EmotePopup::EmotePopup(singletons::ThemeManager &themeManager) this->viewEmotes = new ChannelView(); this->viewEmojis = new ChannelView(); + this->viewEmotes->setOverrideFlags((MessageElement::Flags)( + MessageElement::Default | MessageElement::AlwaysShow | MessageElement::EmoteImages)); + this->viewEmojis->setOverrideFlags((MessageElement::Flags)( + MessageElement::Default | MessageElement::AlwaysShow | MessageElement::EmoteImages)); + this->viewEmotes->setEnableScrollingToBottom(false); this->viewEmojis->setEnableScrollingToBottom(false); @@ -62,7 +67,7 @@ void EmotePopup::loadChannel(ChannelPtr _channel) builder2.getMessage()->addFlags(Message::DisableCompactEmotes); map.each([&](const QString &key, const util::EmoteData &value) { - builder2.appendElement((new EmoteElement(value, MessageElement::Flags::AlwaysShow)) // + builder2.appendElement((new EmoteElement(value, MessageElement::Flags::AlwaysShow)) ->setLink(Link(Link::InsertText, key))); }); @@ -101,7 +106,7 @@ void EmotePopup::loadEmojis() builder.getMessage()->addFlags(Message::DisableCompactEmotes); emojis.each([this, &builder](const QString &key, const util::EmoteData &value) { - builder.appendElement((new EmoteElement(value, MessageElement::Flags::AlwaysShow)) // + builder.appendElement((new EmoteElement(value, MessageElement::Flags::AlwaysShow)) ->setLink(Link(Link::Type::InsertText, key))); }); emojiChannel->addMessage(builder.getMessage()); diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index 8d7525b2e..da94ac2df 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -291,6 +291,16 @@ bool ChannelView::getEnableScrollingToBottom() const return this->enableScrollingToBottom; } +void ChannelView::setOverrideFlags(boost::optional value) +{ + this->overrideFlags = value; +} + +const boost::optional &ChannelView::getOverrideFlags() const +{ + return this->overrideFlags; +} + messages::LimitedQueueSnapshot ChannelView::getMessagesSnapshot() { if (!this->paused) { @@ -338,7 +348,6 @@ void ChannelView::setChannel(ChannelPtr newChannel) newChannel->messagesAddedAtStart.connect([this](std::vector &messages) { std::vector messageRefs; messageRefs.resize(messages.size()); - qDebug() << messages.size(); for (size_t i = 0; i < messages.size(); i++) { messageRefs.at(i) = MessageLayoutPtr(new MessageLayout(messages.at(i))); } @@ -455,6 +464,10 @@ void ChannelView::setSelection(const SelectionItem &start, const SelectionItem & messages::MessageElement::Flags ChannelView::getFlags() const { + if (this->overrideFlags) { + return this->overrideFlags.get(); + } + MessageElement::Flags flags = singletons::SettingManager::getInstance().getWordFlags(); Split *split = dynamic_cast(this->parentWidget()); diff --git a/src/widgets/helper/channelview.hpp b/src/widgets/helper/channelview.hpp index 9212e3368..8c794263d 100644 --- a/src/widgets/helper/channelview.hpp +++ b/src/widgets/helper/channelview.hpp @@ -38,6 +38,8 @@ public: void clearSelection(); void setEnableScrollingToBottom(bool); bool getEnableScrollingToBottom() const; + void setOverrideFlags(boost::optional value); + const boost::optional &getOverrideFlags() const; void pause(int msecTimeout); void updateLastReadMessage(); @@ -80,6 +82,7 @@ private: bool messageWasAdded = false; bool paused = false; QTimer pauseTimeout; + boost::optional overrideFlags; messages::MessageLayoutPtr lastReadMessage; messages::LimitedQueueSnapshot snapshot; diff --git a/src/widgets/helper/label.cpp b/src/widgets/helper/label.cpp new file mode 100644 index 000000000..a8332d9e4 --- /dev/null +++ b/src/widgets/helper/label.cpp @@ -0,0 +1,78 @@ +#include "label.hpp" +#include "singletons/fontmanager.hpp" + +#include + +namespace chatterino { +namespace widgets { +Label::Label(BaseWidget *parent) + : BaseWidget(parent) +{ + singletons::FontManager::getInstance().fontChanged.connect( + [this]() { this->scaleChangedEvent(this->getScale()); }); +} + +const QString &Label::getText() const +{ + return this->text; +} + +void Label::setText(const QString &value) +{ + this->text = value; + this->scaleChangedEvent(this->getScale()); +} + +FontStyle Label::getFontStyle() const +{ + return this->fontStyle; +} + +void Label::setFontStyle(FontStyle style) +{ + this->fontStyle = style; + this->scaleChangedEvent(this->getScale()); +} + +void Label::scaleChangedEvent(float scale) +{ + QFontMetrics metrics = + singletons::FontManager::getInstance().getFontMetrics(this->fontStyle, scale); + + this->preferedSize = QSize(metrics.width(this->text), metrics.height()); + + this->updateGeometry(); +} + +QSize Label::sizeHint() const +{ + return this->preferedSize; +} + +QSize Label::minimumSizeHint() const +{ + return this->preferedSize; +} + +void Label::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.setFont(singletons::FontManager::getInstance().getFont( + this->fontStyle, this->getScale() / painter.device()->devicePixelRatioF())); + + int width = singletons::FontManager::getInstance() + .getFontMetrics(this->fontStyle, this->getScale()) + .width(this->text); + + int flags = Qt::TextSingleLine; + + if (this->width() < width) { + flags |= Qt::AlignLeft | Qt::AlignVCenter; + } else { + flags |= Qt::AlignCenter; + } + + painter.drawText(this->rect(), flags, this->text); +} +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/helper/label.hpp b/src/widgets/helper/label.hpp new file mode 100644 index 000000000..ea3b3b604 --- /dev/null +++ b/src/widgets/helper/label.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "singletons/fontmanager.hpp" +#include "widgets/basewidget.hpp" + +namespace chatterino { +namespace widgets { + +class Label : public BaseWidget +{ +public: + Label(BaseWidget *parent); + + const QString &getText() const; + void setText(const QString &text); + + FontStyle getFontStyle() const; + void setFontStyle(FontStyle style); + +protected: + virtual void scaleChangedEvent(float scale) override; + virtual void paintEvent(QPaintEvent *event) override; + + virtual QSize sizeHint() const override; + virtual QSize minimumSizeHint() const override; + +private: + QSize preferedSize; + QString text; + FontStyle fontStyle = FontStyle::Medium; +}; +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/helper/splitheader.cpp b/src/widgets/helper/splitheader.cpp index 1750331f0..b6f9b885f 100644 --- a/src/widgets/helper/splitheader.cpp +++ b/src/widgets/helper/splitheader.cpp @@ -4,6 +4,7 @@ #include "twitch/twitchchannel.hpp" #include "util/layoutcreator.hpp" #include "util/urlfetch.hpp" +#include "widgets/helper/label.hpp" #include "widgets/split.hpp" #include "widgets/splitcontainer.hpp" #include "widgets/tooltipwidget.hpp" @@ -47,7 +48,9 @@ SplitHeader::SplitHeader(Split *_split) layout->addStretch(1); // channel name label + // auto title = layout.emplace