feat: create delegate for project item views

This allows us to define custom painting for list view items. In
particular, this is applied to the mod downloader, in order to allow
displaying both the mod name and mod description, and settings their
effects (like bold or underline) independent of each other.

Signed-off-by: flow <flowlnlnln@gmail.com>
This commit is contained in:
flow 2022-07-14 22:23:41 -03:00
parent 0f61f5ba03
commit 4a13dbe3bb
No known key found for this signature in database
GPG Key ID: 8D0F221F0A59F469
8 changed files with 139 additions and 24 deletions

View File

@ -884,6 +884,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/PageContainer.cpp ui/widgets/PageContainer.cpp
ui/widgets/PageContainer.h ui/widgets/PageContainer.h
ui/widgets/PageContainer_p.h ui/widgets/PageContainer_p.h
ui/widgets/ProjectItem.h
ui/widgets/ProjectItem.cpp
ui/widgets/VersionListView.cpp ui/widgets/VersionListView.cpp
ui/widgets/VersionListView.h ui/widgets/VersionListView.h
ui/widgets/VersionSelectWidget.cpp ui/widgets/VersionSelectWidget.cpp

View File

@ -6,6 +6,8 @@
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include <QMessageBox> #include <QMessageBox>
namespace ModPlatform { namespace ModPlatform {
@ -39,9 +41,6 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
ModPlatform::IndexedPack pack = modpacks.at(pos); ModPlatform::IndexedPack pack = modpacks.at(pos);
switch (role) { switch (role) {
case Qt::DisplayRole: {
return pack.name;
}
case Qt::ToolTipRole: { case Qt::ToolTipRole: {
if (pack.description.length() > 100) { if (pack.description.length() > 100) {
// some magic to prevent to long tooltips and replace html linebreaks // some magic to prevent to long tooltips and replace html linebreaks
@ -64,20 +63,20 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon; return icon;
} }
case Qt::SizeHintRole:
return QSize(0, 58);
case Qt::UserRole: { case Qt::UserRole: {
QVariant v; QVariant v;
v.setValue(pack); v.setValue(pack);
return v; return v;
} }
case Qt::FontRole: { // Custom data
QFont font; case UserDataTypes::TITLE:
if (m_parent->getDialog()->isModSelected(pack.name)) { return pack.name;
font.setBold(true); case UserDataTypes::DESCRIPTION:
font.setUnderline(true); return pack.description;
} case UserDataTypes::SELECTED:
return m_parent->getDialog()->isModSelected(pack.name);
return font;
}
default: default:
break; break;
} }

View File

@ -2,7 +2,6 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "net/NetJob.h" #include "net/NetJob.h"

View File

@ -43,6 +43,7 @@
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
: QWidget(dialog) : QWidget(dialog)
@ -71,6 +72,8 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{ connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{
ui->searchButton->setStyleSheet("text-decoration: none"); ui->searchButton->setStyleSheet("text-decoration: none");
}); });
ui->packView->setItemDelegate(new ProjectItemDelegate(this));
} }
ModPage::~ModPage() ModPage::~ModPage()

View File

@ -1,27 +1,33 @@
#include "Common.h" #include "Common.h"
// Origin: Qt // Origin: Qt
QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, // More specifically, this is a trimmed down version on the algorithm in:
qreal &widthUsed) // https://code.woboq.org/qt5/qtbase/src/widgets/styles/qcommonstyle.cpp.html#846
QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height)
{ {
QStringList lines; QList<std::pair<qreal, QString>> lines;
height = 0; height = 0;
widthUsed = 0;
textLayout.beginLayout(); textLayout.beginLayout();
QString str = textLayout.text(); QString str = textLayout.text();
while (true) while (true) {
{
QTextLine line = textLayout.createLine(); QTextLine line = textLayout.createLine();
if (!line.isValid()) if (!line.isValid())
break; break;
if (line.textLength() == 0) if (line.textLength() == 0)
break; break;
line.setLineWidth(lineWidth); line.setLineWidth(lineWidth);
line.setPosition(QPointF(0, height)); line.setPosition(QPointF(0, height));
height += line.height(); height += line.height();
lines.append(str.mid(line.textStart(), line.textLength()));
widthUsed = qMax(widthUsed, line.naturalTextWidth()); lines.append(std::make_pair(line.naturalTextWidth(), str.mid(line.textStart(), line.textLength())));
} }
textLayout.endLayout(); textLayout.endLayout();
return lines; return lines;
} }

View File

@ -1,6 +1,9 @@
#pragma once #pragma once
#include <QStringList>
#include <QTextLayout> #include <QTextLayout>
QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, /** Cuts out the text in textLayout into smaller pieces, according to the lineWidth.
qreal &widthUsed); * Returns a list of pairs, each containing the width of that line and that line's string, respectively.
* The total height of those lines is set in the last argument, 'height'.
*/
QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height);

View File

@ -0,0 +1,78 @@
#include "ProjectItem.h"
#include "Common.h"
#include <QIcon>
#include <QPainter>
ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {}
void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
painter->save();
QStyleOptionViewItem opt(option);
initStyleOption(&opt, index);
auto& rect = opt.rect;
auto icon_width = rect.height(), icon_height = rect.height();
auto remaining_width = rect.width() - icon_width;
if (opt.state & QStyle::State_Selected) {
painter->fillRect(rect, opt.palette.highlight());
painter->setPen(opt.palette.highlightedText().color());
} else if (opt.state & QStyle::State_MouseOver) {
painter->fillRect(rect, opt.palette.window());
}
{ // Icon painting
// Square-sized, occupying the left portion
opt.icon.paint(painter, rect.x(), rect.y(), icon_width, icon_height);
}
{ // Title painting
auto title = index.data(UserDataTypes::TITLE).toString();
painter->save();
auto font = opt.font;
if (index.data(UserDataTypes::SELECTED).toBool()) {
// Set nice font
font.setBold(true);
font.setUnderline(true);
}
font.setPointSize(font.pointSize() + 2);
painter->setFont(font);
// On the top, aligned to the left after the icon
painter->drawText(rect.x() + icon_width, rect.y() + QFontMetrics(font).height(), title);
painter->restore();
}
{ // Description painting
auto description = index.data(UserDataTypes::DESCRIPTION).toString();
QTextLayout text_layout(description, opt.font);
qreal height = 0;
auto cut_text = viewItemTextLayout(text_layout, remaining_width, height);
// Get first line unconditionally
description = cut_text.first().second;
// Get second line, elided if needed
if (cut_text.size() > 1) {
if (cut_text.size() > 2)
description += opt.fontMetrics.elidedText(cut_text.at(1).second, opt.textElideMode, cut_text.at(1).first);
else
description += cut_text.at(1).second;
}
// On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare)
painter->drawText(rect.x() + icon_width, rect.y() + rect.height() - 2.2 * opt.fontMetrics.height(), remaining_width,
2 * opt.fontMetrics.height(), Qt::TextWordWrap, description);
}
painter->restore();
}

View File

@ -0,0 +1,25 @@
#pragma once
#include <QStyledItemDelegate>
/* Custom data types for our custom list models :) */
enum UserDataTypes {
TITLE = 257, // QString
DESCRIPTION = 258, // QString
SELECTED = 259 // bool
};
/** This is an item delegate composed of:
* - An Icon on the left
* - A title
* - A description
* */
class ProjectItemDelegate final : public QStyledItemDelegate {
Q_OBJECT
public:
ProjectItemDelegate(QWidget* parent);
void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override;
};