From 905aa4e923006dc989d8febbebf1da1cc512a5d5 Mon Sep 17 00:00:00 2001 From: nerix Date: Mon, 1 Apr 2024 00:04:11 +0200 Subject: [PATCH] refactor: About page (#5287) --- CHANGELOG.md | 1 + resources/avatars/anon.png | Bin 0 -> 1612 bytes resources/contributors.txt | 141 ++++++------- src/CMakeLists.txt | 3 + src/widgets/layout/FlowLayout.cpp | 252 ++++++++++++++++++++++++ src/widgets/layout/FlowLayout.hpp | 104 ++++++++++ src/widgets/settingspages/AboutPage.cpp | 62 +++--- 7 files changed, 475 insertions(+), 88 deletions(-) create mode 100644 resources/avatars/anon.png create mode 100644 src/widgets/layout/FlowLayout.cpp create mode 100644 src/widgets/layout/FlowLayout.hpp 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 0000000000000000000000000000000000000000..b7993edcbbbed49dc84078ac0ba2c44a3c7d2d91 GIT binary patch literal 1612 zcmV-S2DABzP)C00033P)t-s00030 z|NsC0|L5oD)YR0lu&~U`%-r1EudlDWySu2UsLRXCs;a8Mz`&ZCn#IM%kB^U_prC$! ze!#%Mn3$NNqN2sc#gmhhwY9Z(cXyeYnZLikj*gC-o1444yN{2LudlD0o14A8y>4!9 ziHV7voSch`i?+75iHV7CW*rKF^!p`oFho12xDm64H=jg5_pii(Ga zhk}BFfq{X5fPjC0e|~;`dwY9&dU|$tc5`!cZfH9JB{OjlQ2V{3ADfP;vUl9`{Us+kUJ^Yixh`Tzg_|NsC0|NsB~{wJ}e5&!@Mwn;=mRCwC$np;=eOcaLqB;kxGf=5&+ zBBCgMia`A|0XZfha{m8+r8CK7jT*oTV_aQpo~vH8ect`AD5%FPC>2_gr;{O%X9X)a*Z`9rWSGF`w*{tG{>%_eyPbN%E+)M_>I2fp(=3vez# zt2XK%(J=hKh!rKR@y=Toae*o4;u?t}T zz4|<`?Bj!=%$ka1K18_j!*>**)rZ)QnVk9&yaff%G*0_xp!CNJ7!W=2ZzK4Kw4?Ir zpW(ZISAf=VK3#eK85g`uCNce#p2^@eH zFvkAzcr+Xi2mg7%EzKQ+wEn*HGI|y)Mr-zc>FFV z11{bGIRUkA7l8eS(Y=(wEfgF8zkxCJcd;K+@Pb=Nc@m&GW%HW^!8OkD38;hd2p<6R z8%N+8DRP4&wg8Kq1a7g91JszGqA%Y8&v=_SK-~<0U!Y;5Ct(Tl8OIy(l~k=|7QpIR`+*x5s@fe4i2q%HtOAShU)LMgVc_x7r@ZWyaWE&`m`P}RZf zoB*9eX}?bXL%A-yKI41O()bn(Y&R+w>bo zG05i($w$y|pj#1I&0mK*J3EJ@ds}b%3734fkYpQhY@v;5wVEa^VyEL%3S2q}mZ{&l z1sZ|T!RR;u*90iZ0ywrnU~*f}xa5(*b7^3fX0YW{&*1YG;>JRB0#*|}v-9o%WsN21 zHpYC40a%+oV+}GgtWCJQlA7Pgz*AV+UkC#S{N?ybT0h@MP>ciG%Icr%vk?Rw3ETcHL=GbQ*+pa`GVsJp zg&Dh5DUvKMv=@)U2)LqT%~o?!2^hY;Y +#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(); } } }