diff --git a/CHANGELOG.md b/CHANGELOG.md index af86fe273..c41fd9570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) - Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) - Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) +- Minor: Changed the layout of the about page. (#5287) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/resources/avatars/anon.png b/resources/avatars/anon.png new file mode 100644 index 000000000..b7993edcb Binary files /dev/null and b/resources/avatars/anon.png differ diff --git a/resources/contributors.txt b/resources/contributors.txt index b5907812e..d60742040 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -3,80 +3,89 @@ # TODO: Parse this into a CONTRIBUTORS.md too # Adding yourself? Copy and paste this template at the bottom of this file and fill in the fields in a PR! -# Name | Link | Avatar (Loaded as a resource, avatars are not required) | Title (description of work done). +# Name | Link | Avatar (Loaded as a resource, avatars are not required). # Avatar should be located in avatars/ directory. Its size should be 128x128 (get it from https://github.com/username.png?size=128). # Make sure to reduce avatar's size as much as possible with tool like pngcrush or optipng (if the file is in png format). # Contributor is what we use for someone who has contributed in general (like sent a programming-related PR). -fourtf | https://fourtf.com | :/avatars/fourtf.png | Author, main developer -pajlada | https://pajlada.se | :/avatars/pajlada.png | Collaborator, co-developer -zneix | https://github.com/zneix | :/avatars/zneix.png | Collaborator -Mm2PL | https://github.com/mm2pl | :/avatars/mm2pl.png | Collaborator -YungLPR | https://github.com/leon-richardt | | Collaborator -dnsge | https://github.com/dnsge | | Collaborator -Felanbird | https://github.com/Felanbird | | Collaborator -kornes | https://github.com/kornes | | Collaborator +@header Maintainers -Cranken | https://github.com/Cranken | | Contributor -hemirt | https://github.com/hemirt | | Contributor -LajamerrMittesdine | https://github.com/LajamerrMittesdine | | Contributor -coral | https://github.com/coral | | Contributor, design -apa420 | https://github.com/apa420 | | Contributor -DatGuy1 | https://github.com/DatGuy1 | | Contributor -Confuseh | https://github.com/Confuseh | | Contributor -ch-ems | https://github.com/ch-ems | | Contributor -Bur0k | https://github.com/Bur0k | | Contributor -nuuls | https://github.com/nuuls | | Contributor -Chronophylos | https://github.com/Chronophylos | | Contributor -Ckath | https://github.com/Ckath | | Contributor -matijakevic | https://github.com/matijakevic | | Contributor -nforro | https://github.com/nforro | | Contributor -vanolpfan | https://github.com/vanolpfan | | Contributor -23rd | https://github.com/23rd | | Contributor -machgo | https://github.com/machgo | | Contributor -TranRed | https://github.com/TranRed | | Contributor -RAnders00 | https://github.com/RAnders00 | | Contributor -gempir | https://github.com/gempir | | Contributor -mfmarlow | https://github.com/mfmarlow | | Contributor -y0dax | https://github.com/y0dax | | Contributor -Iulian Onofrei | https://github.com/revolter | :/avatars/revolter.jpg | Contributor -matthewde | https://github.com/m4tthewde | :/avatars/matthewde.jpg | Contributor -Karar Al-Remahy | https://github.com/KararTY | :/avatars/kararty.png | Contributor -Talen | https://github.com/talneoran | | Contributor -SLCH | https://github.com/SLCH | :/avatars/slch.png | Contributor -ALazyMeme | https://github.com/alazymeme | :/avatars/alazymeme.png | Contributor -xHeaveny_ | https://github.com/xHeaveny | :/avatars/xheaveny.png | Contributor -1xelerate | https://github.com/xel86 | :/avatars/_1xelerate.png | Contributor -acdvs | https://github.com/acdvs | | Contributor -karl-police | https://github.com/karl-police | :/avatars/karlpolice.png | Contributor -brian6932 | https://github.com/brian6932 | :/avatars/brian6932.png | Contributor -hicupalot | https://github.com/hicupalot | :/avatars/hicupalot.png | Contributor -iProdigy | https://github.com/iProdigy | :/avatars/iprodigy.png | Contributor -Jaxkey | https://github.com/Jaxkey | :/avatars/jaxkey.png | Contributor -Explooosion | https://github.com/Explooosion-code | :/avatars/explooosion_code.png | Contributor -mohad12211 | https://github.com/mohad12211 | :/avatars/mohad12211.png | Contributor -Wissididom | https://github.com/Wissididom | :/avatars/wissididom.png | Contributor -03y | https://github.com/03y | | Contributor -ScrubN | https://github.com/ScrubN | | Contributor -Cyclone | https://github.com/PsycloneTM | :/avatars/cyclone.png | Contributor -2547techno | https://github.com/2547techno | :/avatars/techno.png | Contributor -ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png | Contributor -olafyang | https://github.com/olafyang | | Contributor -chrrs | https://github.com/chrrs | | Contributor -4rneee | https://github.com/4rneee | | Contributor -crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png | Contributor -SputNikPlop | https://github.com/SputNikPlop | | Contributor -fraxx | https://github.com/fraxxio | :/avatars/fraxx.png | Contributor -KleberPF | https://github.com/KleberPF | | Contributor +fourtf | https://fourtf.com | :/avatars/fourtf.png +pajlada | https://pajlada.se | :/avatars/pajlada.png + +@header Collaborators + +zneix | https://github.com/zneix | :/avatars/zneix.png +Mm2PL | https://github.com/mm2pl | :/avatars/mm2pl.png +YungLPR | https://github.com/leon-richardt | +dnsge | https://github.com/dnsge | +Felanbird | https://github.com/Felanbird | +kornes | https://github.com/kornes | + +@header Contributors + +Cranken | https://github.com/Cranken | +hemirt | https://github.com/hemirt | +LajamerrMittesdine | https://github.com/LajamerrMittesdine | +coral | https://github.com/coral | +apa420 | https://github.com/apa420 | +DatGuy1 | https://github.com/DatGuy1 | +Confuseh | https://github.com/Confuseh | +ch-ems | https://github.com/ch-ems | +Bur0k | https://github.com/Bur0k | +nuuls | https://github.com/nuuls | +Chronophylos | https://github.com/Chronophylos | +Ckath | https://github.com/Ckath | +matijakevic | https://github.com/matijakevic | +nforro | https://github.com/nforro | +vanolpfan | https://github.com/vanolpfan | +23rd | https://github.com/23rd | +machgo | https://github.com/machgo | +TranRed | https://github.com/TranRed | +RAnders00 | https://github.com/RAnders00 | +gempir | https://github.com/gempir | +mfmarlow | https://github.com/mfmarlow | +y0dax | https://github.com/y0dax | +Iulian Onofrei | https://github.com/revolter | :/avatars/revolter.jpg +matthewde | https://github.com/m4tthewde | :/avatars/matthewde.jpg +Karar Al-Remahy | https://github.com/KararTY | :/avatars/kararty.png +Talen | https://github.com/talneoran | +SLCH | https://github.com/SLCH | :/avatars/slch.png +ALazyMeme | https://github.com/alazymeme | :/avatars/alazymeme.png +xHeaveny_ | https://github.com/xHeaveny | :/avatars/xheaveny.png +1xelerate | https://github.com/xel86 | :/avatars/_1xelerate.png +acdvs | https://github.com/acdvs | +karl-police | https://github.com/karl-police | :/avatars/karlpolice.png +brian6932 | https://github.com/brian6932 | :/avatars/brian6932.png +hicupalot | https://github.com/hicupalot | :/avatars/hicupalot.png +iProdigy | https://github.com/iProdigy | :/avatars/iprodigy.png +Jaxkey | https://github.com/Jaxkey | :/avatars/jaxkey.png +Explooosion | https://github.com/Explooosion-code | :/avatars/explooosion_code.png +mohad12211 | https://github.com/mohad12211 | :/avatars/mohad12211.png +Wissididom | https://github.com/Wissididom | :/avatars/wissididom.png +03y | https://github.com/03y | +ScrubN | https://github.com/ScrubN | +Cyclone | https://github.com/PsycloneTM | :/avatars/cyclone.png +2547techno | https://github.com/2547techno | :/avatars/techno.png +ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png +olafyang | https://github.com/olafyang | +chrrs | https://github.com/chrrs | +4rneee | https://github.com/4rneee | +crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png +SputNikPlop | https://github.com/SputNikPlop | +fraxx | https://github.com/fraxxio | :/avatars/fraxx.png +KleberPF | https://github.com/KleberPF | # If you are a contributor add yourself above this line -Defman21 | https://github.com/Defman21 | | Documentation -vilgotf | https://github.com/vilgotf | | Documentation -Ian321 | https://github.com/Ian321 | | Documentation -Yardanico | https://github.com/Yardanico | | Documentation -huti26 | https://github.com/huti26 | | Documentation -chrisduerr | https://github.com/chrisduerr | | Documentation +@header Documentation + +Defman21 | https://github.com/Defman21 | +vilgotf | https://github.com/vilgotf | +Ian321 | https://github.com/Ian321 | +Yardanico | https://github.com/Yardanico | +huti26 | https://github.com/huti26 | +chrisduerr | https://github.com/chrisduerr | # Otherwise add yourself right above this one diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ba5e85b4d..fa545a644 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -656,6 +656,9 @@ set(SOURCE_FILES widgets/helper/TitlebarButtons.cpp widgets/helper/TitlebarButtons.hpp + widgets/layout/FlowLayout.cpp + widgets/layout/FlowLayout.hpp + widgets/listview/GenericItemDelegate.cpp widgets/listview/GenericItemDelegate.hpp widgets/listview/GenericListItem.cpp diff --git a/src/widgets/layout/FlowLayout.cpp b/src/widgets/layout/FlowLayout.cpp new file mode 100644 index 000000000..d815f6237 --- /dev/null +++ b/src/widgets/layout/FlowLayout.cpp @@ -0,0 +1,252 @@ +#include "widgets/layout/FlowLayout.hpp" + +#include +#include +#include +#include + +namespace { + +using namespace chatterino; + +class Linebreak : public QWidget +{ +}; + +} // namespace + +namespace chatterino { + +FlowLayout::FlowLayout(QWidget *parent, Options options) + : QLayout(parent) + , hSpace_(options.hSpacing) + , vSpace_(options.vSpacing) +{ + if (options.margin >= 0) + { + this->setContentsMargins(options.margin, options.margin, options.margin, + options.margin); + } +} + +FlowLayout::FlowLayout(Options options) + : FlowLayout(nullptr, options) +{ +} + +FlowLayout::~FlowLayout() +{ + for (auto *item : this->itemList_) + { + delete item; + } + this->itemList_ = {}; +} + +void FlowLayout::addItem(QLayoutItem *item) +{ + this->itemList_.push_back(item); +} + +void FlowLayout::addLinebreak(int height) +{ + auto *linebreak = new Linebreak; + linebreak->setFixedHeight(height); + this->addWidget(linebreak); +} + +int FlowLayout::horizontalSpacing() const +{ + if (this->hSpace_ >= 0) + { + return this->hSpace_; + } + + return this->defaultSpacing(QStyle::PM_LayoutHorizontalSpacing); +} + +void FlowLayout::setHorizontalSpacing(int value) +{ + if (this->hSpace_ == value) + { + return; + } + this->hSpace_ = value; + this->invalidate(); +} + +int FlowLayout::verticalSpacing() const +{ + if (this->vSpace_ >= 0) + { + return this->vSpace_; + } + + return this->defaultSpacing(QStyle::PM_LayoutVerticalSpacing); +} + +void FlowLayout::setVerticalSpacing(int value) +{ + if (this->vSpace_ == value) + { + return; + } + this->vSpace_ = value; + this->invalidate(); +} + +int FlowLayout::count() const +{ + return static_cast(this->itemList_.size()); +} + +QLayoutItem *FlowLayout::itemAt(int index) const +{ + if (index >= 0 && index < static_cast(this->itemList_.size())) + { + return this->itemList_[static_cast(index)]; + } + return nullptr; +} + +QLayoutItem *FlowLayout::takeAt(int index) +{ + if (index >= 0 && index < static_cast(this->itemList_.size())) + { + auto *it = this->itemList_[static_cast(index)]; + this->itemList_.erase(this->itemList_.cbegin() + + static_cast(index)); + return it; + } + return nullptr; +} + +Qt::Orientations FlowLayout::expandingDirections() const +{ + return {}; +} + +bool FlowLayout::hasHeightForWidth() const +{ + return true; +} + +int FlowLayout::heightForWidth(int width) const +{ + return this->doLayout({0, 0, width, 0}, true); +} + +void FlowLayout::setGeometry(const QRect &rect) +{ + QLayout::setGeometry(rect); + this->doLayout(rect, false); +} + +QSize FlowLayout::sizeHint() const +{ + return this->minimumSize(); +} + +QSize FlowLayout::minimumSize() const +{ + QSize size; + for (const auto *item : this->itemList_) + { + size = size.expandedTo(item->minimumSize()); + } + + const QMargins margins = contentsMargins(); + size += QSize(margins.left() + margins.right(), + margins.top() + margins.bottom()); + return size; +} + +int FlowLayout::doLayout(const QRect &rect, bool testOnly) const +{ + auto margins = this->contentsMargins(); + QRect effectiveRect = rect.adjusted(margins.left(), margins.top(), + -margins.right(), -margins.bottom()); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + for (QLayoutItem *item : this->itemList_) + { + auto *linebreak = dynamic_cast(item->widget()); + if (linebreak) + { + item->setGeometry({x, y, 0, linebreak->height()}); + x = effectiveRect.x(); + y = y + lineHeight + linebreak->height(); + lineHeight = 0; + continue; + } + + auto space = this->getSpacing(item); + int nextX = x + item->sizeHint().width() + space.width(); + if (nextX - space.width() > effectiveRect.right() && lineHeight > 0) + { + x = effectiveRect.x(); + y = y + lineHeight + space.height(); + nextX = x + item->sizeHint().width() + space.width(); + lineHeight = 0; + } + + if (!testOnly) + { + item->setGeometry({QPoint{x, y}, item->sizeHint()}); + } + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + + return y + lineHeight - rect.y() + margins.bottom(); +} + +int FlowLayout::defaultSpacing(QStyle::PixelMetric pm) const +{ + QObject *parent = this->parent(); + if (!parent) + { + return -1; + } + if (auto *widget = dynamic_cast(parent)) + { + return widget->style()->pixelMetric(pm, nullptr, widget); + } + if (auto *layout = dynamic_cast(parent)) + { + return layout->spacing(); + } + return -1; +} + +QSize FlowLayout::getSpacing(QLayoutItem *item) const +{ + // called if there isn't any parent or the parent can't provide any spacing + auto fallbackSpacing = [&](auto dir) { + if (auto *widget = item->widget()) + { + return widget->style()->layoutSpacing(QSizePolicy::PushButton, + QSizePolicy::PushButton, dir); + } + if (auto *layout = item->layout()) + { + return layout->spacing(); + } + return 0; + }; + + QSize spacing(this->horizontalSpacing(), this->verticalSpacing()); + if (spacing.width() == -1) + { + spacing.rwidth() = fallbackSpacing(Qt::Horizontal); + } + if (spacing.height() == -1) + { + spacing.rheight() = fallbackSpacing(Qt::Vertical); + } + return spacing; +} + +} // namespace chatterino diff --git a/src/widgets/layout/FlowLayout.hpp b/src/widgets/layout/FlowLayout.hpp new file mode 100644 index 000000000..39a359ff1 --- /dev/null +++ b/src/widgets/layout/FlowLayout.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include + +#include + +namespace chatterino { + +/// @brief A QLayout wrapping items +/// +/// Similar to a box layout that wraps its items. It's not super optimized. +/// Some computations in #doLayout() could be cached. +/// +/// This is based on the Qt flow layout example: +/// https://doc.qt.io/qt-6/qtwidgets-layouts-flowlayout-example.html +class FlowLayout : public QLayout +{ +public: + struct Options { + int margin = -1; + int hSpacing = -1; + int vSpacing = -1; + }; + + explicit FlowLayout(QWidget *parent, Options options = {-1, -1, -1}); + explicit FlowLayout(Options options = {-1, -1, -1}); + + ~FlowLayout() override; + FlowLayout(const FlowLayout &) = delete; + FlowLayout(FlowLayout &&) = delete; + FlowLayout &operator=(const FlowLayout &) = delete; + FlowLayout &operator=(FlowLayout &&) = delete; + + /// @brief Adds @a item to this layout + /// + /// Ownership of @a item is transferred. This method isn't usually called + /// in application code (use addWidget/addLayout). + /// See QLayout::addItem for more information. + void addItem(QLayoutItem *item) override; + + /// @brief Adds a linebreak to this layout + /// + /// @param height Specifies the height of the linebreak + void addLinebreak(int height = 0); + + /// @brief Spacing on the horizontal axis + /// + /// -1 if the default spacing for an item will be used. + [[nodiscard]] int horizontalSpacing() const; + + /// Setter for #horizontalSpacing(). -1 to use defaults. + void setHorizontalSpacing(int value); + + /// @brief Spacing on the vertical axis + /// + /// -1 if the default spacing for an item will be used. + [[nodiscard]] int verticalSpacing() const; + + /// Setter for #verticalSpacing(). -1 to use defaults. + void setVerticalSpacing(int value); + + /// From QLayout. This layout doesn't expand in any direction. + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int width) const override; + + QSize minimumSize() const override; + QSize sizeHint() const override; + + void setGeometry(const QRect &rect) override; + + int count() const override; + QLayoutItem *itemAt(int index) const override; + + /// From QLayout. Ownership is transferred to the caller + QLayoutItem *takeAt(int index) override; + +private: + /// @brief Computes the layout + /// + /// @param rect The area in which items can be layed out + /// @param testOnly If set, items won't be moved, only the total height + /// will be computed. + /// @returns The total height including margins. + int doLayout(const QRect &rect, bool testOnly) const; + + /// @brief Computes the default spacing based for items on the parent + /// + /// @param pm Either PM_LayoutHorizontalSpacing or PM_LayoutVerticalSpacing + /// for the respective direction. + /// @returns The spacing in dp, -1 if there isn't any parent + int defaultSpacing(QStyle::PixelMetric pm) const; + + /// Computes the spacing for @a item + QSize getSpacing(QLayoutItem *item) const; + + std::vector itemList_; + int hSpace_ = -1; + int vSpace_ = -1; + int lineSpacing_ = -1; +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 78597c5fc..89c985c5e 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -8,6 +8,7 @@ #include "util/RemoveScrollAreaBackground.hpp" #include "widgets/BasePopup.hpp" #include "widgets/helper/SignalLabel.hpp" +#include "widgets/layout/FlowLayout.hpp" #include #include @@ -54,6 +55,7 @@ AboutPage::AboutPage() auto label = vbox.emplace(version.buildString() + "
" + version.runningString()); + label->setWordWrap(true); label->setOpenExternalLinks(true); label->setTextInteractionFlags(Qt::TextBrowserInteraction); } @@ -137,15 +139,15 @@ AboutPage::AboutPage() l.emplace("Facebook emojis provided by Facebook")->setOpenExternalLinks(true); l.emplace("Apple emojis provided by Apple")->setOpenExternalLinks(true); l.emplace("Google emojis provided by Google")->setOpenExternalLinks(true); - l.emplace("Emoji datasource provided by Cal Henderson" + l.emplace("Emoji datasource provided by Cal Henderson " "(show license)")->setOpenExternalLinks(true); // clang-format on } // Contributors - auto contributors = layout.emplace("Contributors"); + auto contributors = layout.emplace("People"); { - auto l = contributors.emplace(); + auto l = contributors.emplace(); QFile contributorsFile(":/contributors.txt"); contributorsFile.open(QFile::ReadOnly); @@ -166,11 +168,24 @@ AboutPage::AboutPage() continue; } + if (line.startsWith(u"@header")) + { + if (l->count() != 0) + { + l->addLinebreak(20); + } + auto *label = new QLabel(QStringLiteral("

%1

") + .arg(line.mid(8).trimmed())); + l->addWidget(label); + l->addLinebreak(8); + continue; + } + QStringList contributorParts = line.split("|"); - if (contributorParts.size() != 4) + if (contributorParts.size() != 3) { - qCDebug(chatterinoWidget) + qCWarning(chatterinoWidget) << "Missing parts in line" << line; continue; } @@ -178,39 +193,42 @@ AboutPage::AboutPage() QString username = contributorParts[0].trimmed(); QString url = contributorParts[1].trimmed(); QString avatarUrl = contributorParts[2].trimmed(); - QString role = contributorParts[3].trimmed(); auto *usernameLabel = new QLabel("" + username + ""); usernameLabel->setOpenExternalLinks(true); - auto *roleLabel = new QLabel(role); + usernameLabel->setToolTip(url); - auto contributorBox2 = l.emplace(); + auto contributorBox2 = l.emplace(); - const auto addAvatar = [&avatarUrl, &contributorBox2] { - if (!avatarUrl.isEmpty()) + const auto addAvatar = [&] { + auto *avatar = new QLabel(); + QPixmap avatarPixmap; + if (avatarUrl.isEmpty()) { - QPixmap avatarPixmap; - avatarPixmap.load(avatarUrl); - - auto avatar = contributorBox2.emplace(); - avatar->setPixmap(avatarPixmap); - avatar->setFixedSize(64, 64); - avatar->setScaledContents(true); + // TODO: or anon.png + avatarPixmap.load(":/avatars/anon.png"); } + else + { + avatarPixmap.load(avatarUrl); + } + + avatar->setPixmap(avatarPixmap); + avatar->setFixedSize(64, 64); + avatar->setScaledContents(true); + contributorBox2->addWidget(avatar, 0, Qt::AlignCenter); }; - const auto addLabels = [&contributorBox2, &usernameLabel, - &roleLabel] { + const auto addLabels = [&] { auto *labelBox = new QVBoxLayout(); contributorBox2->addLayout(labelBox); - labelBox->addWidget(usernameLabel); - labelBox->addWidget(roleLabel); + labelBox->addWidget(usernameLabel, 0, Qt::AlignCenter); }; - addLabels(); addAvatar(); + addLabels(); } } }