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` diff --git a/chatterino.pro b/chatterino.pro index 99a0d7259..c70a48c42 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -163,7 +163,13 @@ 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 \ + src/widgets/settingspages/specialchannelspage.cpp \ + src/widgets/settingspages/keyboardsettingspage.cpp \ + src/widgets/helper/titlebarbutton.cpp \ + src/widgets/helper/label.cpp HEADERS += \ src/precompiled_header.hpp \ @@ -267,7 +273,14 @@ HEADERS += \ src/util/networkrequest.hpp \ src/util/networkworker.hpp \ src/util/networkrequester.hpp \ - src/util/flagsenum.h + src/util/flagsenum.h \ + src/widgets/settingspages/ignoreuserspage.hpp \ + src/widgets/settingspages/ignoremessagespage.hpp \ + src/widgets/settingspages/specialchannelspage.hpp \ + src/widgets/settingspages/keyboardsettings.hpp \ + src/widgets/settingspages/keyboardsettingspage.hpp \ + src/widgets/helper/titlebarbutton.hpp \ + src/widgets/helper/label.hpp RESOURCES += \ resources/resources.qrc diff --git a/resources/images/aboutlogo.png b/resources/images/aboutlogo.png index 8e8753521..3efa9ab7b 100644 Binary files a/resources/images/aboutlogo.png and b/resources/images/aboutlogo.png differ diff --git a/src/channel.hpp b/src/channel.hpp index 9581ba5dc..f6a80d9ef 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/messages/layouts/messagelayout.cpp b/src/messages/layouts/messagelayout.cpp index 39bc9c420..164d57854 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; @@ -98,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) { @@ -139,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(); @@ -164,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->flags & Message::Disabled) { @@ -174,6 +182,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 792876513..e4ed05117 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(); @@ -67,6 +68,7 @@ private: int currentLayoutWidth = -1; int fontGeneration = -1; int emoteGeneration = -1; + QString timestampFormat; float scale = -1; unsigned int bufferUpdatedCount = 0; diff --git a/src/messages/messageelement.cpp b/src/messages/messageelement.cpp index c28532971..7285d2721 100644 --- a/src/messages/messageelement.cpp +++ b/src/messages/messageelement.cpp @@ -61,45 +61,64 @@ 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()); + 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 & 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 +136,79 @@ 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) { + // 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)) { - container.addElementNoLineBreak( - getTextLayoutElement(word.text, word.width, this->hasTrailingSpace())); - continue; - } - - // see if the text fits in the next line - if (!container.atStartOfLine()) { - container.breakLine(); + // } + // 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 ab8e36d82..bdae0129b 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/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/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 4f5573be8..e6599b123 100644 --- a/src/singletons/ircmanager.cpp +++ b/src/singletons/ircmanager.cpp @@ -246,16 +246,23 @@ 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; twitch::TwitchMessageBuilder builder(c.get(), message, args); - c->addMessage(builder.parse()); + if (!builder.isIgnored()) { + messages::MessagePtr _message = builder.build(); + if (_message->flags & messages::Message::Highlighted) { + singletons::ChannelManager::getInstance().mentionsChannel->addMessage(_message); + } + + c->addMessage(_message); + } } void IrcManager::messageReceived(Communi::IrcMessage *message) @@ -384,7 +391,7 @@ void IrcManager::onConnected() MessagePtr connMsg = Message::createSystemMessage("connected to chat"); MessagePtr reconnMsg = Message::createSystemMessage("reconnected to chat"); - this->channelManager.doOnAll([connMsg, reconnMsg](SharedChannel channel) { + this->channelManager.doOnAll([connMsg, reconnMsg](ChannelPtr channel) { assert(channel); LimitedQueueSnapshot snapshot = channel->getMessageSnapshot(); @@ -407,7 +414,7 @@ void IrcManager::onDisconnected() MessagePtr msg = Message::createSystemMessage("disconnected from chat"); msg->flags &= Message::DisconnectedMessage; - this->channelManager.doOnAll([msg](SharedChannel channel) { + this->channelManager.doOnAll([msg](ChannelPtr channel) { assert(channel); channel->addMessage(msg); }); 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/singletons/settingsmanager.cpp b/src/singletons/settingsmanager.cpp index 0aa93c12a..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; @@ -17,6 +18,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 +31,10 @@ 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() @@ -135,6 +141,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 +213,22 @@ void SettingManager::updateModerationActions() } } } + +void SettingManager::updateIgnoredKeywords() +{ + static QRegularExpression newLineRegex("(\r\n?|\n)+"); + + auto items = new std::vector(); + + 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); +} } // 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/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/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/twitch/twitchchannel.cpp b/src/twitch/twitchchannel.cpp index 869912427..e308a1e22 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" @@ -156,8 +158,8 @@ void TwitchChannel::refreshLiveStatus() std::weak_ptr weak = this->shared_from_this(); - util::twitch::get2(url, QThread::currentThread(), [weak](const rapidjson::Document &d) { - SharedChannel shared = weak.lock(); + util::twitch::get2(url, QThread::currentThread(), false, [weak](const rapidjson::Document &d) { + ChannelPtr shared = weak.lock(); if (!shared) { return; @@ -218,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; @@ -230,7 +232,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 +240,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.build()); + } } channel->addMessagesAtStart(messages); } diff --git a/src/twitch/twitchmessagebuilder.cpp b/src/twitch/twitchmessagebuilder.cpp index 36cffbb8c..2c67b2963 100644 --- a/src/twitch/twitchmessagebuilder.cpp +++ b/src/twitch/twitchmessagebuilder.cpp @@ -27,15 +27,28 @@ TwitchMessageBuilder::TwitchMessageBuilder(Channel *_channel, , tags(this->ircMessage->tags()) , usernameColor(singletons::ThemeManager::getInstance().messages.textColors.system) { + this->originalMessage = this->ircMessage->content(); } -MessagePtr TwitchMessageBuilder::parse() +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::build() { singletons::SettingManager &settings = singletons::SettingManager::getInstance(); singletons::EmoteManager &emoteManager = singletons::EmoteManager::getInstance(); - this->originalMessage = this->ircMessage->content(); - // PARSING this->parseUsername(); @@ -43,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) @@ -58,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(); @@ -219,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); @@ -450,39 +457,36 @@ 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() +void TwitchMessageBuilder::appendTwitchBadges() { singletons::ResourceManager &resourceManager = singletons::ResourceManager::getInstance(); const auto &channelResources = resourceManager.channels[this->roomID]; @@ -639,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 80edc4f8f..7b6ecde8f 100644 --- a/src/twitch/twitchmessagebuilder.hpp +++ b/src/twitch/twitchmessagebuilder.hpp @@ -38,7 +38,8 @@ public: QString messageID; QString userName; - messages::MessagePtr parse(); + bool isIgnored() const; + messages::MessagePtr build(); private: QString roomID; @@ -47,7 +48,7 @@ private: void parseMessageID(); void parseRoomID(); - void parseChannelName(); + void appendChannelName(); void parseUsername(); void appendUsername(); void parseHighlights(); @@ -55,10 +56,9 @@ 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(); + void appendTwitchBadges(); + void appendChatterinoBadges(); bool tryParseCheermote(const QString &string); }; 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/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); // diff --git a/src/widgets/accountpopup.cpp b/src/widgets/accountpopup.cpp index 78020a8cf..7502a548a 100644 --- a/src/widgets/accountpopup.cpp +++ b/src/widgets/accountpopup.cpp @@ -18,13 +18,15 @@ namespace chatterino { namespace widgets { -AccountPopupWidget::AccountPopupWidget(SharedChannel _channel) +AccountPopupWidget::AccountPopupWidget(ChannelPtr _channel) : BaseWindow() , ui(new Ui::AccountPopup) , channel(_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(); @@ -154,7 +155,7 @@ AccountPopupWidget::AccountPopupWidget(SharedChannel _channel) this->hide(); // }); - this->dpiMultiplierChanged(this->getDpiMultiplier(), this->getDpiMultiplier()); + this->scaleChangedEvent(this->getScale()); } void AccountPopupWidget::setName(const QString &name) @@ -171,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; @@ -182,7 +183,7 @@ void AccountPopupWidget::User::refreshUserType(const SharedChannel &channel, boo } } -void AccountPopupWidget::setChannel(SharedChannel _channel) +void AccountPopupWidget::setChannel(ChannelPtr _channel) { this->channel = _channel; } @@ -246,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 e5bae7d1a..b2ec8b522 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(); @@ -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; @@ -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/basewidget.cpp b/src/widgets/basewidget.cpp index 808a59412..b915abbcb 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,56 @@ 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; } } +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]() { - this->refreshTheme(); + this->themeRefreshEvent(); this->update(); }); @@ -59,7 +93,69 @@ 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); + + this->setScaleIndependantSize(this->getScaleIndependantSize()); + + // 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 8030655ed..376d1f210 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,38 @@ 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; + + 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 dpiMultiplierChanged(float /*oldDpi*/, float /*newDpi*/) - { - } + virtual void childEvent(QChildEvent *) override; - float dpiMultiplier = 1.f; + virtual void scaleChangedEvent(float newScale); + virtual void themeRefreshEvent(); + + void setScale(float value); private: void init(); + float scale = 1.f; + QSize scaleIndependantSize; - virtual void refreshTheme(); + 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 54fbb780e..eb780184d 100644 --- a/src/widgets/basewindow.cpp +++ b/src/widgets/basewindow.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #ifdef USEWINSDK @@ -22,7 +23,7 @@ #define WM_DPICHANGED 0x02E0 #endif -#include "widgets/helper/rippleeffectlabel.hpp" +#include "widgets/helper/titlebarbutton.hpp" namespace chatterino { namespace widgets { @@ -43,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(); @@ -56,45 +57,57 @@ 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 *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; - 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->setScaleIndependantSize(46, 30); + _minButton->setButtonStyle(TitleBarButton::Minimize); + TitleBarButton *_maxButton = new TitleBarButton; + _maxButton->setScaleIndependantSize(46, 30); + _maxButton->setButtonStyle(TitleBarButton::Maximize); + TitleBarButton *_exitButton = new TitleBarButton; + _exitButton->setScaleIndependantSize(46, 30); + _exitButton->setButtonStyle(TitleBarButton::Close); - this->minButton = min; - this->maxButton = max; - this->exitButton = exit; + QObject::connect(_minButton, &TitleBarButton::clicked, this, [this] { + this->setWindowState(Qt::WindowMinimized | this->windowState()); + }); + QObject::connect(_maxButton, &TitleBarButton::clicked, this, [this] { + this->setWindowState(this->windowState() == Qt::WindowMaximized + ? Qt::WindowActive + : Qt::WindowMaximized); + }); + QObject::connect(_exitButton, &TitleBarButton::clicked, this, + [this] { this->close(); }); - this->widgets.push_back(min); - this->widgets.push_back(max); - this->widgets.push_back(exit); + this->minButton = _minButton; + this->maxButton = _maxButton; + this->exitButton = _exitButton; - buttons->addStretch(1); - buttons->addWidget(min); - buttons->addWidget(max); - buttons->addWidget(exit); + this->buttons.push_back(_minButton); + this->buttons.push_back(_maxButton); + this->buttons.push_back(_exitButton); + + buttonLayout->addStretch(1); + buttonLayout->addWidget(_minButton); + buttonLayout->addWidget(_maxButton); + buttonLayout->addWidget(_exitButton); + buttonLayout->setSpacing(0); } - this->layoutBase = new QWidget(this); - this->widgets.push_back(this->layoutBase); + this->layoutBase = new BaseWidget(this); layout->addWidget(this->layoutBase); } @@ -102,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()) { @@ -113,6 +126,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()) { @@ -125,37 +148,96 @@ QWidget *BaseWindow::getLayoutContainer() bool BaseWindow::hasCustomWindowFrame() { #ifdef Q_OS_WIN - // return this->enableCustomFrame; - return false; + return this->enableCustomFrame; +// return false; #else return false; #endif } -void BaseWindow::refreshTheme() +void BaseWindow::themeRefreshEvent() { QPalette palette; palette.setColor(QPalette::Background, this->themeManager.windowBg); palette.setColor(QPalette::Foreground, this->themeManager.windowText); this->setPalette(palette); + + for (RippleEffectButton *button : this->buttons) { + button->setMouseEffectColor(this->themeManager.windowText); + } } -void BaseWindow::addTitleBarButton(const QString &text) +void BaseWindow::addTitleBarButton(const TitleBarButton::Style &style, + std::function onClicked) { - RippleEffectLabel *label = new RippleEffectLabel; - label->getLabel().setText(text); - this->widgets.push_back(label); - this->titlebarBox->insertWidget(2, label); + TitleBarButton *button = new TitleBarButton; + button->setScaleIndependantSize(30, 30); + + 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(); + TooltipWidget::getInstance()->hide(); + +#ifdef USEWINSDK + if (this->hasCustomWindowFrame()) { + this->maxButton->setButtonStyle(this->windowState() & Qt::WindowMaximized + ? TitleBarButton::Unmaximize + : TitleBarButton::Maximize); + } +#endif } 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 @@ -165,17 +247,16 @@ 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 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; } @@ -250,12 +331,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 { @@ -263,8 +348,6 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *r } } - qDebug() << *result; - return true; } else { return QWidget::nativeEvent(eventType, message, result); @@ -272,9 +355,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: @@ -282,9 +366,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); @@ -294,22 +379,29 @@ 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) { - BaseWidget::paintEvent(event); - if (this->hasCustomWindowFrame()) { + BaseWidget::paintEvent(event); + QPainter painter(this); 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 5e1374f9b..fc6ccd756 100644 --- a/src/widgets/basewindow.hpp +++ b/src/widgets/basewindow.hpp @@ -1,11 +1,16 @@ #pragma once #include "basewidget.hpp" +#include "widgets/helper/titlebarbutton.hpp" + +#include class QHBoxLayout; namespace chatterino { namespace widgets { +class RippleEffectButton; +class TitleBarButton; class BaseWindow : public BaseWidget { @@ -17,7 +22,12 @@ public: QWidget *getLayoutContainer(); bool hasCustomWindowFrame(); - void addTitleBarButton(const QString &text); + void addTitleBarButton(const TitleBarButton::Style &style, std::function onClicked); + + void setStayInScreenRect(bool value); + bool getStayInScreenRect() const; + + void moveTo(QWidget *widget, QPoint point); protected: #ifdef USEWINSDK @@ -28,21 +38,25 @@ protected: virtual void changeEvent(QEvent *) override; virtual void leaveEvent(QEvent *) override; + virtual void resizeEvent(QResizeEvent *) override; - virtual void refreshTheme() override; + virtual void themeRefreshEvent() override; private: void init(); + void moveIntoDesktopRect(QWidget *parent); bool enableCustomFrame; + bool stayInScreenRect = false; + bool shown = false; QHBoxLayout *titlebarBox; QWidget *titleLabel; - QWidget *minButton; - QWidget *maxButton; - QWidget *exitButton; + TitleBarButton *minButton; + TitleBarButton *maxButton; + TitleBarButton *exitButton; QWidget *layoutBase; - std::vector widgets; + std::vector buttons; }; } // namespace widgets } // namespace chatterino diff --git a/src/widgets/emotepopup.cpp b/src/widgets/emotepopup.cpp index 0998120ea..e67f1b67c 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); @@ -30,9 +35,14 @@ 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(SharedChannel _channel) +void EmotePopup::loadChannel(ChannelPtr _channel) { TwitchChannel *channel = dynamic_cast(_channel.get()); @@ -40,7 +50,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 @@ -57,7 +67,7 @@ void EmotePopup::loadChannel(SharedChannel _channel) builder2.getMessage()->flags &= 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))); }); @@ -81,7 +91,7 @@ void EmotePopup::loadEmojis() { util::EmoteMap &emojis = singletons::EmoteManager::getInstance().getEmojis(); - SharedChannel emojiChannel(new Channel("")); + ChannelPtr emojiChannel(new Channel("")); // title messages::MessageBuilder builder1; @@ -96,7 +106,7 @@ void EmotePopup::loadEmojis() builder.getMessage()->flags &= 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/emotepopup.hpp b/src/widgets/emotepopup.hpp index d64f979d6..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 { @@ -12,9 +14,11 @@ class EmotePopup : public BaseWindow public: explicit EmotePopup(singletons::ThemeManager &); - void loadChannel(SharedChannel channel); + 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 c34bf4955..13b961732 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 @@ -24,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; @@ -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() @@ -111,6 +114,13 @@ ChannelView::~ChannelView() this->messageReplacedConnection.disconnect(); } +void ChannelView::themeRefreshEvent() +{ + BaseWidget::themeRefreshEvent(); + + this->layoutMessages(); +} + void ChannelView::queueUpdate() { if (this->updateTimer.isActive()) { @@ -164,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(); @@ -180,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(); @@ -281,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) { @@ -290,7 +310,7 @@ messages::LimitedQueueSnapshot ChannelView::getMessagesSnapsho return this->snapshot; } -void ChannelView::setChannel(SharedChannel newChannel) +void ChannelView::setChannel(ChannelPtr newChannel) { if (this->channel) { this->detachChannel(); @@ -328,7 +348,6 @@ void ChannelView::setChannel(SharedChannel 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))); } @@ -410,6 +429,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()); @@ -434,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()); @@ -442,6 +476,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; @@ -477,11 +514,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(); @@ -551,8 +594,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(); } @@ -574,8 +616,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(); @@ -782,12 +823,50 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) } auto &link = hoverLayoutElement->getLink(); + if (event->button() != Qt::LeftButton || + !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(); this->userPopupWidget.setName(user); - this->userPopupWidget.move(event->screenPos().toPoint()); + this->userPopupWidget.moveTo(this, event->screenPos().toPoint()); this->userPopupWidget.show(); this->userPopupWidget.setFocus(); @@ -795,7 +874,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: { diff --git a/src/widgets/helper/channelview.hpp b/src/widgets/helper/channelview.hpp index ed222f595..8c794263d 100644 --- a/src/widgets/helper/channelview.hpp +++ b/src/widgets/helper/channelview.hpp @@ -38,9 +38,12 @@ 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(); - void setChannel(SharedChannel channel); + void setChannel(ChannelPtr channel); messages::LimitedQueueSnapshot getMessagesSnapshot(); void layoutMessages(); @@ -49,8 +52,11 @@ public: boost::signals2::signal mouseDown; boost::signals2::signal selectionChanged; pajlada::Signals::NoArgSignal highlightedMessageReceived; + pajlada::Signals::Signal linkClicked; protected: + virtual void themeRefreshEvent() override; + virtual void resizeEvent(QResizeEvent *) override; virtual void paintEvent(QPaintEvent *) override; @@ -62,6 +68,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); @@ -72,6 +82,8 @@ private: bool messageWasAdded = false; bool paused = false; QTimer pauseTimeout; + boost::optional overrideFlags; + messages::MessageLayoutPtr lastReadMessage; messages::LimitedQueueSnapshot snapshot; @@ -82,7 +94,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/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/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(); diff --git a/src/widgets/helper/notebooktab.cpp b/src/widgets/helper/notebooktab.cpp index 38f215769..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()); @@ -291,14 +293,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/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 a2c46c8fe..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); @@ -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/helper/searchpopup.cpp b/src/widgets/helper/searchpopup.cpp index f5ae9b5e4..8de997357 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/settingsdialogtab.cpp b/src/widgets/helper/settingsdialogtab.cpp index 423e441a1..812e82de0 100644 --- a/src/widgets/helper/settingsdialogtab.cpp +++ b/src/widgets/helper/settingsdialogtab.cpp @@ -10,7 +10,8 @@ namespace widgets { SettingsDialogTab::SettingsDialogTab(SettingsDialog *_dialog, settingspages::SettingsPage *_page, QString imageFileName) - : dialog(_dialog) + : BaseWidget(_dialog) + , dialog(_dialog) , page(_page) { this->ui.labelText = page->getName(); @@ -47,8 +48,8 @@ void SettingsDialogTab::paintEvent(QPaintEvent *) this->style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this); - int a = (this->height() - 20) / 2; - QPixmap pixmap = this->ui.icon.pixmap(QSize(20, 20)); + int a = (this->height() - (20 * this->getScale())) / 2; + QPixmap pixmap = this->ui.icon.pixmap(QSize(this->height() - a * 2, this->height() - a * 2)); painter.drawPixmap(a, a, pixmap); diff --git a/src/widgets/helper/settingsdialogtab.hpp b/src/widgets/helper/settingsdialogtab.hpp index 003dd8ffc..8242e820f 100644 --- a/src/widgets/helper/settingsdialogtab.hpp +++ b/src/widgets/helper/settingsdialogtab.hpp @@ -4,6 +4,8 @@ #include #include +#include "widgets/basewidget.hpp" + namespace chatterino { namespace widgets { namespace settingspages { @@ -12,7 +14,7 @@ class SettingsPage; class SettingsDialog; -class SettingsDialogTab : public QWidget +class SettingsDialogTab : public BaseWidget { Q_OBJECT diff --git a/src/widgets/helper/splitheader.cpp b/src/widgets/helper/splitheader.cpp index 67c0ee355..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