refactor: About page (#5287)

This commit is contained in:
nerix
2024-04-01 00:04:11 +02:00
committed by GitHub
parent 694d53ad20
commit 905aa4e923
7 changed files with 475 additions and 88 deletions
+3
View File
@@ -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
+252
View File
@@ -0,0 +1,252 @@
#include "widgets/layout/FlowLayout.hpp"
#include <QSizePolicy>
#include <QStyle>
#include <QtGlobal>
#include <QWidget>
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<int>(this->itemList_.size());
}
QLayoutItem *FlowLayout::itemAt(int index) const
{
if (index >= 0 && index < static_cast<int>(this->itemList_.size()))
{
return this->itemList_[static_cast<size_t>(index)];
}
return nullptr;
}
QLayoutItem *FlowLayout::takeAt(int index)
{
if (index >= 0 && index < static_cast<int>(this->itemList_.size()))
{
auto *it = this->itemList_[static_cast<size_t>(index)];
this->itemList_.erase(this->itemList_.cbegin() +
static_cast<qsizetype>(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<Linebreak *>(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<QWidget *>(parent))
{
return widget->style()->pixelMetric(pm, nullptr, widget);
}
if (auto *layout = dynamic_cast<QLayout *>(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
+104
View File
@@ -0,0 +1,104 @@
#pragma once
#include <QLayout>
#include <QStyle>
#include <vector>
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<QLayoutItem *> itemList_;
int hSpace_ = -1;
int vSpace_ = -1;
int lineSpacing_ = -1;
};
} // namespace chatterino
+40 -22
View File
@@ -8,6 +8,7 @@
#include "util/RemoveScrollAreaBackground.hpp"
#include "widgets/BasePopup.hpp"
#include "widgets/helper/SignalLabel.hpp"
#include "widgets/layout/FlowLayout.hpp"
#include <QFile>
#include <QFormLayout>
@@ -54,6 +55,7 @@ AboutPage::AboutPage()
auto label = vbox.emplace<QLabel>(version.buildString() + "<br>" +
version.runningString());
label->setWordWrap(true);
label->setOpenExternalLinks(true);
label->setTextInteractionFlags(Qt::TextBrowserInteraction);
}
@@ -137,15 +139,15 @@ AboutPage::AboutPage()
l.emplace<QLabel>("Facebook emojis provided by <a href=\"https://facebook.com\">Facebook</a>")->setOpenExternalLinks(true);
l.emplace<QLabel>("Apple emojis provided by <a href=\"https://apple.com\">Apple</a>")->setOpenExternalLinks(true);
l.emplace<QLabel>("Google emojis provided by <a href=\"https://google.com\">Google</a>")->setOpenExternalLinks(true);
l.emplace<QLabel>("Emoji datasource provided by <a href=\"https://www.iamcal.com/\">Cal Henderson</a>"
l.emplace<QLabel>("Emoji datasource provided by <a href=\"https://www.iamcal.com/\">Cal Henderson</a> "
"(<a href=\"https://github.com/iamcal/emoji-data/blob/master/LICENSE\">show license</a>)")->setOpenExternalLinks(true);
// clang-format on
}
// Contributors
auto contributors = layout.emplace<QGroupBox>("Contributors");
auto contributors = layout.emplace<QGroupBox>("People");
{
auto l = contributors.emplace<QVBoxLayout>();
auto l = contributors.emplace<FlowLayout>();
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("<h1>%1</h1>")
.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("<a href=\"" + url + "\">" + username + "</a>");
usernameLabel->setOpenExternalLinks(true);
auto *roleLabel = new QLabel(role);
usernameLabel->setToolTip(url);
auto contributorBox2 = l.emplace<QHBoxLayout>();
auto contributorBox2 = l.emplace<QVBoxLayout>();
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<QLabel>();
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();
}
}
}