2022-08-19 15:59:36 +02:00
|
|
|
// Copyright (C) 2019 The Qt Company Ltd.
|
2022-12-21 10:12:09 +01:00
|
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
2020-01-09 15:52:47 +01:00
|
|
|
|
|
|
|
|
#include "welcomepagehelper.h"
|
|
|
|
|
|
2023-01-16 17:20:07 +01:00
|
|
|
#include "coreplugintr.h"
|
|
|
|
|
|
2020-01-16 15:48:32 +01:00
|
|
|
#include <utils/algorithm.h>
|
2020-01-09 15:52:47 +01:00
|
|
|
#include <utils/fancylineedit.h>
|
|
|
|
|
#include <utils/qtcassert.h>
|
2021-11-29 20:48:43 +01:00
|
|
|
#include <utils/stylehelper.h>
|
2020-01-09 15:52:47 +01:00
|
|
|
#include <utils/theme/theme.h>
|
|
|
|
|
|
2021-09-19 19:32:52 +02:00
|
|
|
#include <QEasingCurve>
|
2021-11-29 20:48:43 +01:00
|
|
|
#include <QFontDatabase>
|
2022-11-18 09:02:15 +01:00
|
|
|
#include <QHBoxLayout>
|
2023-01-10 12:15:49 +01:00
|
|
|
#include <QHeaderView>
|
2020-01-09 15:52:47 +01:00
|
|
|
#include <QHoverEvent>
|
2023-01-10 12:15:49 +01:00
|
|
|
#include <QLabel>
|
2020-01-16 15:48:32 +01:00
|
|
|
#include <QMouseEvent>
|
|
|
|
|
#include <QPainter>
|
|
|
|
|
#include <QPixmapCache>
|
2023-01-10 12:15:49 +01:00
|
|
|
#include <QScrollArea>
|
2020-01-16 15:48:32 +01:00
|
|
|
#include <QTimer>
|
2020-01-09 15:52:47 +01:00
|
|
|
|
2021-09-19 19:32:52 +02:00
|
|
|
#include <qdrawutil.h>
|
|
|
|
|
|
2020-01-09 15:52:47 +01:00
|
|
|
namespace Core {
|
|
|
|
|
|
|
|
|
|
using namespace Utils;
|
|
|
|
|
|
2020-01-16 15:48:32 +01:00
|
|
|
static QColor themeColor(Theme::Color role)
|
|
|
|
|
{
|
|
|
|
|
return creatorTheme()->color(role);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QFont sizedFont(int size, const QWidget *widget)
|
|
|
|
|
{
|
|
|
|
|
QFont f = widget->font();
|
|
|
|
|
f.setPixelSize(size);
|
|
|
|
|
return f;
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
namespace WelcomePageHelpers {
|
|
|
|
|
|
|
|
|
|
QFont brandFont()
|
|
|
|
|
{
|
|
|
|
|
const static QFont f = []{
|
|
|
|
|
const int id = QFontDatabase::addApplicationFont(":/studiofonts/TitilliumWeb-Regular.ttf");
|
|
|
|
|
QFont result;
|
|
|
|
|
result.setPixelSize(16);
|
|
|
|
|
if (id >= 0) {
|
|
|
|
|
const QStringList fontFamilies = QFontDatabase::applicationFontFamilies(id);
|
|
|
|
|
result.setFamilies(fontFamilies);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}();
|
|
|
|
|
return f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QWidget *panelBar(QWidget *parent)
|
|
|
|
|
{
|
|
|
|
|
auto frame = new QWidget(parent);
|
|
|
|
|
frame->setAutoFillBackground(true);
|
|
|
|
|
frame->setMinimumWidth(WelcomePageHelpers::HSpacing);
|
2022-02-02 17:35:58 +01:00
|
|
|
QPalette pal;
|
|
|
|
|
pal.setBrush(QPalette::Window, {});
|
2021-11-29 20:48:43 +01:00
|
|
|
pal.setColor(QPalette::Window, themeColor(Theme::Welcome_BackgroundPrimaryColor));
|
|
|
|
|
frame->setPalette(pal);
|
|
|
|
|
return frame;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace WelcomePageHelpers
|
|
|
|
|
|
2020-01-09 15:52:47 +01:00
|
|
|
SearchBox::SearchBox(QWidget *parent)
|
|
|
|
|
: WelcomePageFrame(parent)
|
|
|
|
|
{
|
2021-11-29 20:48:43 +01:00
|
|
|
setAutoFillBackground(true);
|
2020-01-09 15:52:47 +01:00
|
|
|
|
|
|
|
|
m_lineEdit = new FancyLineEdit;
|
|
|
|
|
m_lineEdit->setFiltering(true);
|
|
|
|
|
m_lineEdit->setFrame(false);
|
2021-11-29 20:48:43 +01:00
|
|
|
m_lineEdit->setFont(WelcomePageHelpers::brandFont());
|
|
|
|
|
m_lineEdit->setMinimumHeight(33);
|
2020-01-09 15:52:47 +01:00
|
|
|
m_lineEdit->setAttribute(Qt::WA_MacShowFocusRect, false);
|
|
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
QPalette pal = buttonPalette(false, false, true);
|
|
|
|
|
// for the margins
|
|
|
|
|
pal.setColor(QPalette::Window, m_lineEdit->palette().color(QPalette::Base));
|
|
|
|
|
// for macOS dark mode
|
|
|
|
|
pal.setColor(QPalette::WindowText, themeColor(Theme::Welcome_ForegroundPrimaryColor));
|
|
|
|
|
pal.setColor(QPalette::Text, themeColor(Theme::Welcome_TextColor));
|
|
|
|
|
setPalette(pal);
|
|
|
|
|
|
2020-01-09 15:52:47 +01:00
|
|
|
auto box = new QHBoxLayout(this);
|
2021-11-29 20:48:43 +01:00
|
|
|
box->setContentsMargins(10, 0, 1, 0);
|
2020-01-09 15:52:47 +01:00
|
|
|
box->addWidget(m_lineEdit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GridView::GridView(QWidget *parent)
|
2021-11-24 22:19:46 +01:00
|
|
|
: QListView(parent)
|
2020-01-09 15:52:47 +01:00
|
|
|
{
|
2021-11-24 22:19:46 +01:00
|
|
|
setResizeMode(QListView::Adjust);
|
2020-01-09 15:52:47 +01:00
|
|
|
setMouseTracking(true); // To enable hover.
|
|
|
|
|
setSelectionMode(QAbstractItemView::NoSelection);
|
|
|
|
|
setFrameShape(QFrame::NoFrame);
|
2021-03-04 18:00:02 +01:00
|
|
|
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
2021-11-24 22:19:46 +01:00
|
|
|
setViewMode(IconMode);
|
|
|
|
|
setUniformItemSizes(true);
|
2020-01-09 15:52:47 +01:00
|
|
|
|
|
|
|
|
QPalette pal;
|
2021-11-29 20:48:43 +01:00
|
|
|
pal.setColor(QPalette::Base, themeColor(Theme::Welcome_BackgroundSecondaryColor));
|
2020-01-09 15:52:47 +01:00
|
|
|
setPalette(pal); // Makes a difference on Mac.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GridView::leaveEvent(QEvent *)
|
|
|
|
|
{
|
|
|
|
|
QHoverEvent hev(QEvent::HoverLeave, QPointF(), QPointF());
|
|
|
|
|
viewportEvent(&hev); // Seemingly needed to kill the hover paint.
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-10 12:15:49 +01:00
|
|
|
SectionGridView::SectionGridView(QWidget *parent)
|
|
|
|
|
: GridView(parent)
|
|
|
|
|
{}
|
|
|
|
|
|
|
|
|
|
bool SectionGridView::hasHeightForWidth() const
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int SectionGridView::heightForWidth(int width) const
|
|
|
|
|
{
|
|
|
|
|
const int columnCount = width / Core::ListItemDelegate::GridItemWidth;
|
|
|
|
|
const int rowCount = (model()->rowCount() + columnCount - 1) / columnCount;
|
|
|
|
|
return rowCount * Core::ListItemDelegate::GridItemHeight;
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
const QSize ListModel::defaultImageSize(214, 160);
|
2020-01-16 15:48:32 +01:00
|
|
|
|
|
|
|
|
ListModel::ListModel(QObject *parent)
|
|
|
|
|
: QAbstractListModel(parent)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ListModel::~ListModel()
|
|
|
|
|
{
|
2023-01-16 17:20:56 +01:00
|
|
|
clear();
|
2020-01-16 15:48:32 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-10 12:15:49 +01:00
|
|
|
void ListModel::appendItems(const QList<ListItem *> &items)
|
|
|
|
|
{
|
|
|
|
|
beginInsertRows(QModelIndex(), m_items.size(), m_items.size() + items.size());
|
|
|
|
|
m_items.append(items);
|
|
|
|
|
endInsertRows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QList<ListItem *> ListModel::items() const
|
|
|
|
|
{
|
|
|
|
|
return m_items;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-16 17:20:56 +01:00
|
|
|
void ListModel::clear()
|
|
|
|
|
{
|
|
|
|
|
beginResetModel();
|
|
|
|
|
if (m_ownsItems)
|
|
|
|
|
qDeleteAll(m_items);
|
|
|
|
|
m_items.clear();
|
|
|
|
|
endResetModel();
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 15:48:32 +01:00
|
|
|
int ListModel::rowCount(const QModelIndex &) const
|
|
|
|
|
{
|
|
|
|
|
return m_items.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariant ListModel::data(const QModelIndex &index, int role) const
|
|
|
|
|
{
|
|
|
|
|
if (!index.isValid() || index.row() >= m_items.count())
|
|
|
|
|
return QVariant();
|
|
|
|
|
|
|
|
|
|
ListItem *item = m_items.at(index.row());
|
|
|
|
|
switch (role) {
|
|
|
|
|
case Qt::DisplayRole: // for search only
|
|
|
|
|
return QString(item->name + ' ' + item->tags.join(' '));
|
|
|
|
|
case ItemRole:
|
|
|
|
|
return QVariant::fromValue(item);
|
|
|
|
|
case ItemImageRole: {
|
|
|
|
|
QPixmap pixmap;
|
|
|
|
|
if (QPixmapCache::find(item->imageUrl, &pixmap))
|
|
|
|
|
return pixmap;
|
2023-01-12 16:10:09 +01:00
|
|
|
if (pixmap.isNull() && m_fetchPixmapAndUpdatePixmapCache)
|
|
|
|
|
pixmap = m_fetchPixmapAndUpdatePixmapCache(item->imageUrl);
|
2020-01-16 15:48:32 +01:00
|
|
|
return pixmap;
|
|
|
|
|
}
|
|
|
|
|
case ItemTagsRole:
|
|
|
|
|
return item->tags;
|
|
|
|
|
default:
|
|
|
|
|
return QVariant();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 16:10:09 +01:00
|
|
|
void ListModel::setPixmapFunction(const PixmapFunction &fetchPixmapAndUpdatePixmapCache)
|
|
|
|
|
{
|
|
|
|
|
m_fetchPixmapAndUpdatePixmapCache = fetchPixmapAndUpdatePixmapCache;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-10 12:15:49 +01:00
|
|
|
void ListModel::setOwnsItems(bool owns)
|
|
|
|
|
{
|
|
|
|
|
m_ownsItems = owns;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 15:48:32 +01:00
|
|
|
ListModelFilter::ListModelFilter(ListModel *sourceModel, QObject *parent) :
|
|
|
|
|
QSortFilterProxyModel(parent)
|
|
|
|
|
{
|
|
|
|
|
setSourceModel(sourceModel);
|
|
|
|
|
setDynamicSortFilter(true);
|
|
|
|
|
setFilterCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
|
sort(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ListModelFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
|
|
|
|
{
|
|
|
|
|
const ListItem *item = sourceModel()->index(sourceRow, 0, sourceParent).data(
|
|
|
|
|
ListModel::ItemRole).value<Core::ListItem *>();
|
|
|
|
|
|
|
|
|
|
if (!item)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
bool earlyExitResult;
|
|
|
|
|
if (leaveFilterAcceptsRowBeforeFiltering(item, &earlyExitResult))
|
|
|
|
|
return earlyExitResult;
|
|
|
|
|
|
|
|
|
|
if (!m_filterTags.isEmpty()) {
|
|
|
|
|
return Utils::allOf(m_filterTags, [&item](const QString &filterTag) {
|
2022-04-06 08:50:53 +02:00
|
|
|
return item->tags.contains(filterTag, Qt::CaseInsensitive);
|
2020-01-16 15:48:32 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!m_filterStrings.isEmpty()) {
|
|
|
|
|
for (const QString &subString : m_filterStrings) {
|
|
|
|
|
bool wordMatch = false;
|
|
|
|
|
wordMatch |= bool(item->name.contains(subString, Qt::CaseInsensitive));
|
|
|
|
|
if (wordMatch)
|
|
|
|
|
continue;
|
2022-04-06 08:50:53 +02:00
|
|
|
const auto subMatch = [&subString](const QString &elem) {
|
|
|
|
|
return elem.contains(subString, Qt::CaseInsensitive);
|
|
|
|
|
};
|
2020-01-16 15:48:32 +01:00
|
|
|
wordMatch |= Utils::contains(item->tags, subMatch);
|
|
|
|
|
if (wordMatch)
|
|
|
|
|
continue;
|
|
|
|
|
wordMatch |= bool(item->description.contains(subString, Qt::CaseInsensitive));
|
|
|
|
|
if (!wordMatch)
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ListModelFilter::delayedUpdateFilter()
|
|
|
|
|
{
|
|
|
|
|
if (m_timerId != 0)
|
|
|
|
|
killTimer(m_timerId);
|
|
|
|
|
|
|
|
|
|
m_timerId = startTimer(320);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ListModelFilter::timerEvent(QTimerEvent *timerEvent)
|
|
|
|
|
{
|
|
|
|
|
if (m_timerId == timerEvent->timerId()) {
|
|
|
|
|
invalidateFilter();
|
|
|
|
|
emit layoutChanged();
|
|
|
|
|
killTimer(m_timerId);
|
|
|
|
|
m_timerId = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct SearchStringLexer
|
|
|
|
|
{
|
|
|
|
|
QString code;
|
|
|
|
|
const QChar *codePtr;
|
|
|
|
|
QChar yychar;
|
|
|
|
|
QString yytext;
|
|
|
|
|
|
|
|
|
|
enum TokenKind {
|
|
|
|
|
END_OF_STRING = 0,
|
|
|
|
|
TAG,
|
|
|
|
|
STRING_LITERAL,
|
|
|
|
|
UNKNOWN
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
inline void yyinp() { yychar = *codePtr++; }
|
|
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
explicit SearchStringLexer(const QString &code)
|
2020-01-16 15:48:32 +01:00
|
|
|
: code(code)
|
|
|
|
|
, codePtr(code.unicode())
|
|
|
|
|
, yychar(QLatin1Char(' ')) { }
|
|
|
|
|
|
|
|
|
|
int operator()() { return yylex(); }
|
|
|
|
|
|
|
|
|
|
int yylex() {
|
|
|
|
|
while (yychar.isSpace())
|
|
|
|
|
yyinp(); // skip all the spaces
|
|
|
|
|
|
|
|
|
|
yytext.clear();
|
|
|
|
|
|
|
|
|
|
if (yychar.isNull())
|
|
|
|
|
return END_OF_STRING;
|
|
|
|
|
|
|
|
|
|
QChar ch = yychar;
|
|
|
|
|
yyinp();
|
|
|
|
|
|
|
|
|
|
switch (ch.unicode()) {
|
|
|
|
|
case '"':
|
|
|
|
|
case '\'':
|
|
|
|
|
{
|
|
|
|
|
const QChar quote = ch;
|
|
|
|
|
yytext.clear();
|
|
|
|
|
while (!yychar.isNull()) {
|
|
|
|
|
if (yychar == quote) {
|
|
|
|
|
yyinp();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (yychar == QLatin1Char('\\')) {
|
|
|
|
|
yyinp();
|
|
|
|
|
switch (yychar.unicode()) {
|
|
|
|
|
case '"': yytext += QLatin1Char('"'); yyinp(); break;
|
|
|
|
|
case '\'': yytext += QLatin1Char('\''); yyinp(); break;
|
|
|
|
|
case '\\': yytext += QLatin1Char('\\'); yyinp(); break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
yytext += yychar;
|
|
|
|
|
yyinp();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return STRING_LITERAL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
if (ch.isLetterOrNumber() || ch == QLatin1Char('_')) {
|
|
|
|
|
yytext.clear();
|
|
|
|
|
yytext += ch;
|
|
|
|
|
while (yychar.isLetterOrNumber() || yychar == QLatin1Char('_')) {
|
|
|
|
|
yytext += yychar;
|
|
|
|
|
yyinp();
|
|
|
|
|
}
|
|
|
|
|
if (yychar == QLatin1Char(':') && yytext == QLatin1String("tag")) {
|
|
|
|
|
yyinp();
|
|
|
|
|
return TAG;
|
|
|
|
|
}
|
|
|
|
|
return STRING_LITERAL;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
yytext += ch;
|
|
|
|
|
return UNKNOWN;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void ListModelFilter::setSearchString(const QString &arg)
|
|
|
|
|
{
|
|
|
|
|
if (m_searchString == arg)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_searchString = arg;
|
|
|
|
|
m_filterTags.clear();
|
|
|
|
|
m_filterStrings.clear();
|
|
|
|
|
|
|
|
|
|
// parse and update
|
|
|
|
|
SearchStringLexer lex(arg);
|
|
|
|
|
bool isTag = false;
|
|
|
|
|
while (int tk = lex()) {
|
|
|
|
|
if (tk == SearchStringLexer::TAG) {
|
|
|
|
|
isTag = true;
|
|
|
|
|
m_filterStrings.append(lex.yytext);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (tk == SearchStringLexer::STRING_LITERAL) {
|
|
|
|
|
if (isTag) {
|
|
|
|
|
m_filterStrings.pop_back();
|
|
|
|
|
m_filterTags.append(lex.yytext);
|
|
|
|
|
isTag = false;
|
|
|
|
|
} else {
|
|
|
|
|
m_filterStrings.append(lex.yytext);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delayedUpdateFilter();
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-10 12:15:49 +01:00
|
|
|
ListModel *ListModelFilter::sourceListModel() const
|
|
|
|
|
{
|
|
|
|
|
return static_cast<ListModel *>(sourceModel());
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 15:48:32 +01:00
|
|
|
bool ListModelFilter::leaveFilterAcceptsRowBeforeFiltering(const ListItem *, bool *) const
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ListItemDelegate::ListItemDelegate()
|
2021-11-29 20:48:43 +01:00
|
|
|
: backgroundPrimaryColor(themeColor(Theme::Welcome_BackgroundPrimaryColor))
|
|
|
|
|
, backgroundSecondaryColor(themeColor(Theme::Welcome_BackgroundSecondaryColor))
|
|
|
|
|
, foregroundPrimaryColor(themeColor(Theme::Welcome_ForegroundPrimaryColor))
|
|
|
|
|
, hoverColor(themeColor(Theme::Welcome_HoverColor))
|
|
|
|
|
, textColor(themeColor(Theme::Welcome_TextColor))
|
2020-01-16 15:48:32 +01:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
|
|
|
|
|
const QModelIndex &index) const
|
|
|
|
|
{
|
|
|
|
|
const ListItem *item = index.data(ListModel::ItemRole).value<Core::ListItem *>();
|
|
|
|
|
|
|
|
|
|
const QRect rc = option.rect;
|
2021-11-29 20:48:43 +01:00
|
|
|
const QRect tileRect(0, 0, rc.width() - GridItemGap, rc.height() - GridItemGap);
|
|
|
|
|
const QSize thumbnailBgSize = ListModel::defaultImageSize.grownBy(QMargins(1, 1, 1, 1));
|
|
|
|
|
const QRect thumbnailBgRect((tileRect.width() - thumbnailBgSize.width()) / 2, GridItemGap,
|
|
|
|
|
thumbnailBgSize.width(), thumbnailBgSize.height());
|
|
|
|
|
const QRect textArea = tileRect.adjusted(GridItemGap, GridItemGap, -GridItemGap, -GridItemGap);
|
2020-01-16 15:48:32 +01:00
|
|
|
|
|
|
|
|
const bool hovered = option.state & QStyle::State_MouseOver;
|
|
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
constexpr int tagsBase = TagsSeparatorY + 17;
|
|
|
|
|
constexpr int shiftY = TagsSeparatorY - 16;
|
|
|
|
|
constexpr int nameY = TagsSeparatorY - 20;
|
|
|
|
|
|
|
|
|
|
const QRect textRect = textArea.translated(0, nameY);
|
2022-01-20 22:27:58 +01:00
|
|
|
const QFont descriptionFont = sizedFont(11, option.widget);
|
2020-01-16 15:48:32 +01:00
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
painter->save();
|
|
|
|
|
painter->translate(rc.topLeft());
|
|
|
|
|
|
|
|
|
|
painter->fillRect(tileRect, hovered ? hoverColor : backgroundPrimaryColor);
|
2020-01-16 15:48:32 +01:00
|
|
|
|
|
|
|
|
QTextOption wrapped;
|
|
|
|
|
wrapped.setWrapMode(QTextOption::WordWrap);
|
|
|
|
|
int offset = 0;
|
2021-09-19 19:32:52 +02:00
|
|
|
float animationProgress = 0; // Linear increase from 0.0 to 1.0 during hover animation
|
2020-01-16 15:48:32 +01:00
|
|
|
if (hovered) {
|
|
|
|
|
if (index != m_previousIndex) {
|
|
|
|
|
m_previousIndex = index;
|
2022-02-24 23:17:13 +01:00
|
|
|
m_currentTagRects.clear();
|
2021-11-29 20:48:43 +01:00
|
|
|
m_blurredThumbnail = QPixmap();
|
2020-01-16 15:48:32 +01:00
|
|
|
m_startTime.start();
|
|
|
|
|
m_currentWidget = qobject_cast<QAbstractItemView *>(
|
|
|
|
|
const_cast<QWidget *>(option.widget));
|
|
|
|
|
}
|
2021-11-29 20:48:43 +01:00
|
|
|
constexpr float hoverAnimationDuration = 260;
|
|
|
|
|
animationProgress = m_startTime.elapsed() / hoverAnimationDuration;
|
2022-01-20 15:55:25 +01:00
|
|
|
if (animationProgress < 1) {
|
|
|
|
|
static const QEasingCurve animationCurve(QEasingCurve::OutCubic);
|
|
|
|
|
offset = animationCurve.valueForProgress(animationProgress) * shiftY;
|
2021-09-19 19:32:52 +02:00
|
|
|
QTimer::singleShot(10, this, &ListItemDelegate::goon);
|
2022-01-20 15:55:25 +01:00
|
|
|
} else {
|
|
|
|
|
offset = shiftY;
|
|
|
|
|
}
|
2022-01-20 14:55:42 +01:00
|
|
|
} else if (index == m_previousIndex) {
|
2020-01-16 15:48:32 +01:00
|
|
|
m_previousIndex = QModelIndex();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QRect shiftedTextRect = textRect.adjusted(0, -offset, 0, -offset);
|
|
|
|
|
|
|
|
|
|
// The pixmap.
|
2021-11-29 20:48:43 +01:00
|
|
|
const QPixmap pm = index.data(ListModel::ItemImageRole).value<QPixmap>();
|
|
|
|
|
QPoint thumbnailPos = thumbnailBgRect.center();
|
|
|
|
|
if (!pm.isNull()) {
|
|
|
|
|
painter->fillRect(thumbnailBgRect, backgroundSecondaryColor);
|
|
|
|
|
|
|
|
|
|
thumbnailPos.rx() -= pm.width() / pm.devicePixelRatio() / 2 - 1;
|
|
|
|
|
thumbnailPos.ry() -= pm.height() / pm.devicePixelRatio() / 2 - 1;
|
|
|
|
|
painter->drawPixmap(thumbnailPos, pm);
|
|
|
|
|
|
|
|
|
|
painter->setPen(foregroundPrimaryColor);
|
|
|
|
|
drawPixmapOverlay(item, painter, option, thumbnailBgRect);
|
|
|
|
|
} else {
|
|
|
|
|
// The description text as fallback.
|
|
|
|
|
painter->setPen(textColor);
|
2022-01-20 22:27:58 +01:00
|
|
|
painter->setFont(descriptionFont);
|
|
|
|
|
painter->drawText(textArea, item->description, wrapped);
|
2021-09-19 19:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
// The description background
|
2022-01-20 22:27:58 +01:00
|
|
|
if (offset) {
|
|
|
|
|
QRect backgroundPortionRect = tileRect;
|
|
|
|
|
backgroundPortionRect.setTop(shiftY - offset);
|
|
|
|
|
if (!pm.isNull()) {
|
|
|
|
|
if (m_blurredThumbnail.isNull()) {
|
|
|
|
|
constexpr int blurRadius = 50;
|
|
|
|
|
QImage thumbnail(tileRect.size() + QSize(blurRadius, blurRadius) * 2,
|
|
|
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
|
|
|
thumbnail.fill(hoverColor);
|
|
|
|
|
QPainter thumbnailPainter(&thumbnail);
|
|
|
|
|
thumbnailPainter.translate(blurRadius, blurRadius);
|
|
|
|
|
thumbnailPainter.fillRect(thumbnailBgRect, backgroundSecondaryColor);
|
|
|
|
|
thumbnailPainter.drawPixmap(thumbnailPos, pm);
|
|
|
|
|
thumbnailPainter.setPen(foregroundPrimaryColor);
|
|
|
|
|
drawPixmapOverlay(item, &thumbnailPainter, option, thumbnailBgRect);
|
|
|
|
|
thumbnailPainter.end();
|
|
|
|
|
|
|
|
|
|
m_blurredThumbnail = QPixmap(tileRect.size());
|
|
|
|
|
QPainter blurredThumbnailPainter(&m_blurredThumbnail);
|
|
|
|
|
blurredThumbnailPainter.translate(-blurRadius, -blurRadius);
|
|
|
|
|
qt_blurImage(&blurredThumbnailPainter, thumbnail, blurRadius, false, false);
|
|
|
|
|
blurredThumbnailPainter.setOpacity(0.825);
|
|
|
|
|
blurredThumbnailPainter.fillRect(tileRect, hoverColor);
|
|
|
|
|
}
|
|
|
|
|
const QPixmap thumbnailPortionPM = m_blurredThumbnail.copy(backgroundPortionRect);
|
|
|
|
|
painter->drawPixmap(backgroundPortionRect.topLeft(), thumbnailPortionPM);
|
|
|
|
|
} else {
|
|
|
|
|
painter->fillRect(backgroundPortionRect, hoverColor);
|
2021-11-29 20:48:43 +01:00
|
|
|
}
|
2020-01-16 15:48:32 +01:00
|
|
|
}
|
|
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
// The description Text (unhovered or hovered)
|
|
|
|
|
painter->setPen(textColor);
|
|
|
|
|
painter->setFont(sizedFont(13, option.widget)); // Title font
|
2020-01-16 15:48:32 +01:00
|
|
|
if (offset) {
|
2021-11-29 20:48:43 +01:00
|
|
|
// The title of the example
|
|
|
|
|
const QRectF nameRect = painter->boundingRect(shiftedTextRect, item->name, wrapped);
|
2020-01-16 15:48:32 +01:00
|
|
|
painter->drawText(nameRect, item->name, wrapped);
|
|
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
// The separator line below the example title.
|
|
|
|
|
const int ll = nameRect.height() + 3;
|
|
|
|
|
const QLine line = QLine(0, ll, textArea.width(), ll).translated(shiftedTextRect.topLeft());
|
|
|
|
|
painter->setPen(foregroundPrimaryColor);
|
2021-09-19 19:32:52 +02:00
|
|
|
painter->setOpacity(animationProgress); // "fade in" separator line and description
|
2021-11-29 20:48:43 +01:00
|
|
|
painter->drawLine(line);
|
2020-01-16 15:48:32 +01:00
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
// The description text.
|
|
|
|
|
const int dd = ll + 5;
|
|
|
|
|
const QRect descRect = shiftedTextRect.adjusted(0, dd, 0, dd);
|
|
|
|
|
painter->setPen(textColor);
|
2022-01-20 22:27:58 +01:00
|
|
|
painter->setFont(descriptionFont);
|
2020-01-16 15:48:32 +01:00
|
|
|
painter->drawText(descRect, item->description, wrapped);
|
2021-09-19 19:32:52 +02:00
|
|
|
painter->setOpacity(1);
|
2021-11-29 20:48:43 +01:00
|
|
|
} else {
|
|
|
|
|
// The title of the example
|
|
|
|
|
const QString elidedName = painter->fontMetrics()
|
|
|
|
|
.elidedText(item->name, Qt::ElideRight, textRect.width());
|
|
|
|
|
painter->drawText(textRect, elidedName);
|
2020-01-16 15:48:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Separator line between text and 'Tags:' section
|
2021-11-29 20:48:43 +01:00
|
|
|
painter->setPen(foregroundPrimaryColor);
|
|
|
|
|
painter->drawLine(QLineF(textArea.topLeft(), textArea.topRight())
|
|
|
|
|
.translated(0, TagsSeparatorY));
|
2020-01-16 15:48:32 +01:00
|
|
|
|
|
|
|
|
// The 'Tags:' section
|
2021-11-29 20:48:43 +01:00
|
|
|
painter->setPen(foregroundPrimaryColor);
|
2020-01-16 15:48:32 +01:00
|
|
|
const QFont tagsFont = sizedFont(10, option.widget);
|
|
|
|
|
painter->setFont(tagsFont);
|
2021-11-29 20:48:43 +01:00
|
|
|
const QFontMetrics fm = painter->fontMetrics();
|
2023-01-16 17:20:07 +01:00
|
|
|
const QString tagsLabelText = Tr::tr("Tags:");
|
2021-11-29 20:48:43 +01:00
|
|
|
constexpr int tagsHorSpacing = 5;
|
|
|
|
|
const QRect tagsLabelRect =
|
|
|
|
|
QRect(0, 0, fm.horizontalAdvance(tagsLabelText) + tagsHorSpacing, fm.height())
|
|
|
|
|
.translated(textArea.x(), tagsBase);
|
|
|
|
|
painter->drawText(tagsLabelRect, tagsLabelText);
|
2020-01-16 15:48:32 +01:00
|
|
|
|
|
|
|
|
painter->setPen(themeColor(Theme::Welcome_LinkColor));
|
2022-02-10 09:22:17 +01:00
|
|
|
int emptyTagRowsLeft = 2;
|
2020-01-16 15:48:32 +01:00
|
|
|
int xx = 0;
|
2021-11-29 20:48:43 +01:00
|
|
|
int yy = 0;
|
2022-02-24 23:17:13 +01:00
|
|
|
const bool populateTagsRects = m_currentTagRects.empty();
|
2020-01-16 15:48:32 +01:00
|
|
|
for (const QString &tag : item->tags) {
|
2021-11-29 20:48:43 +01:00
|
|
|
const int ww = fm.horizontalAdvance(tag) + tagsHorSpacing;
|
|
|
|
|
if (xx + ww > textArea.width() - tagsLabelRect.width()) {
|
2022-02-10 09:22:17 +01:00
|
|
|
if (--emptyTagRowsLeft == 0)
|
|
|
|
|
break;
|
2021-11-29 20:48:43 +01:00
|
|
|
yy += fm.lineSpacing();
|
2020-01-16 15:48:32 +01:00
|
|
|
xx = 0;
|
|
|
|
|
}
|
2021-11-29 20:48:43 +01:00
|
|
|
const QRect tagRect = QRect(xx, yy, ww, tagsLabelRect.height())
|
|
|
|
|
.translated(tagsLabelRect.topRight());
|
2020-01-16 15:48:32 +01:00
|
|
|
painter->drawText(tagRect, tag);
|
2022-02-24 23:17:13 +01:00
|
|
|
if (populateTagsRects)
|
|
|
|
|
m_currentTagRects.append({ tag, tagRect });
|
2020-01-16 15:48:32 +01:00
|
|
|
xx += ww;
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-29 20:48:43 +01:00
|
|
|
painter->restore();
|
2020-01-16 15:48:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ListItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
|
|
|
|
const QStyleOptionViewItem &option, const QModelIndex &index)
|
|
|
|
|
{
|
|
|
|
|
if (event->type() == QEvent::MouseButtonRelease) {
|
|
|
|
|
const ListItem *item = index.data(ListModel::ItemRole).value<ListItem *>();
|
2020-01-22 06:58:21 +01:00
|
|
|
if (!item)
|
|
|
|
|
return false;
|
2020-01-16 15:48:32 +01:00
|
|
|
auto mev = static_cast<QMouseEvent *>(event);
|
2020-04-30 15:08:56 +02:00
|
|
|
|
|
|
|
|
if (mev->button() != Qt::LeftButton) // do not react on right click
|
|
|
|
|
return false;
|
|
|
|
|
|
2020-01-16 15:48:32 +01:00
|
|
|
if (index.isValid()) {
|
2022-02-24 23:17:13 +01:00
|
|
|
const QPoint mousePos = mev->pos() - option.rect.topLeft();
|
|
|
|
|
const auto tagUnderMouse =
|
|
|
|
|
Utils::findOrDefault(m_currentTagRects,
|
|
|
|
|
[&mousePos](const QPair<QString, QRect> &tag) {
|
|
|
|
|
return tag.second.contains(mousePos);
|
|
|
|
|
});
|
|
|
|
|
if (!tagUnderMouse.first.isEmpty())
|
|
|
|
|
emit tagClicked(tagUnderMouse.first);
|
|
|
|
|
else
|
2020-01-16 15:48:32 +01:00
|
|
|
clickAction(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return QStyledItemDelegate::editorEvent(event, model, option, index);
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-24 22:19:46 +01:00
|
|
|
QSize ListItemDelegate::sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const
|
|
|
|
|
{
|
|
|
|
|
return {GridItemWidth, GridItemHeight};
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 15:48:32 +01:00
|
|
|
void ListItemDelegate::drawPixmapOverlay(const ListItem *, QPainter *,
|
|
|
|
|
const QStyleOptionViewItem &, const QRect &) const
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ListItemDelegate::clickAction(const ListItem *) const
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ListItemDelegate::goon()
|
|
|
|
|
{
|
|
|
|
|
if (m_currentWidget)
|
2022-01-20 16:37:28 +01:00
|
|
|
m_currentWidget->update(m_previousIndex);
|
2020-01-16 15:48:32 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-10 12:15:49 +01:00
|
|
|
SectionedGridView::SectionedGridView(QWidget *parent)
|
|
|
|
|
: QStackedWidget(parent)
|
|
|
|
|
{
|
2023-03-17 15:18:59 +01:00
|
|
|
m_allItemsModel.reset(new ListModel);
|
|
|
|
|
m_allItemsModel->setPixmapFunction(m_pixmapFunction);
|
2023-01-10 12:15:49 +01:00
|
|
|
|
|
|
|
|
auto area = new QScrollArea(this);
|
|
|
|
|
area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
|
|
area->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
|
|
|
|
area->setFrameShape(QFrame::NoFrame);
|
|
|
|
|
area->setWidgetResizable(true);
|
|
|
|
|
|
|
|
|
|
auto sectionedView = new QWidget;
|
|
|
|
|
auto layout = new QVBoxLayout;
|
|
|
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
layout->addStretch();
|
|
|
|
|
sectionedView->setLayout(layout);
|
|
|
|
|
area->setWidget(sectionedView);
|
|
|
|
|
|
|
|
|
|
addWidget(area);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-17 15:18:59 +01:00
|
|
|
SectionedGridView::~SectionedGridView()
|
|
|
|
|
{
|
|
|
|
|
clear();
|
|
|
|
|
}
|
2023-01-10 12:15:49 +01:00
|
|
|
|
|
|
|
|
void SectionedGridView::setItemDelegate(QAbstractItemDelegate *delegate)
|
|
|
|
|
{
|
2023-03-17 15:18:59 +01:00
|
|
|
m_itemDelegate = delegate;
|
|
|
|
|
if (m_allItemsView)
|
|
|
|
|
m_allItemsView->setItemDelegate(delegate);
|
2023-01-10 12:15:49 +01:00
|
|
|
for (GridView *view : std::as_const(m_gridViews))
|
|
|
|
|
view->setItemDelegate(delegate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SectionedGridView::setPixmapFunction(const Core::ListModel::PixmapFunction &pixmapFunction)
|
|
|
|
|
{
|
|
|
|
|
m_pixmapFunction = pixmapFunction;
|
2023-03-17 15:18:59 +01:00
|
|
|
m_allItemsModel->setPixmapFunction(pixmapFunction);
|
2023-01-10 12:15:49 +01:00
|
|
|
for (ListModel *model : std::as_const(m_sectionModels))
|
|
|
|
|
model->setPixmapFunction(pixmapFunction);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SectionedGridView::setSearchString(const QString &searchString)
|
|
|
|
|
{
|
2023-03-17 15:18:59 +01:00
|
|
|
if (searchString.isEmpty()) {
|
|
|
|
|
// back to sectioned view
|
|
|
|
|
setCurrentIndex(0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!m_allItemsView) {
|
|
|
|
|
// We don't have a grid set for searching yet.
|
|
|
|
|
// Create all items view for filtering.
|
|
|
|
|
m_allItemsView.reset(new GridView);
|
|
|
|
|
m_allItemsView->setModel(new ListModelFilter(m_allItemsModel.get(), m_allItemsView.get()));
|
|
|
|
|
if (m_itemDelegate)
|
|
|
|
|
m_allItemsView->setItemDelegate(m_itemDelegate);
|
|
|
|
|
addWidget(m_allItemsView.get());
|
|
|
|
|
}
|
|
|
|
|
setCurrentWidget(m_allItemsView.get());
|
|
|
|
|
auto filterModel = static_cast<ListModelFilter *>(m_allItemsView.get()->model());
|
|
|
|
|
filterModel->setSearchString(searchString);
|
2023-01-10 12:15:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ListModel *SectionedGridView::addSection(const Section §ion, const QList<ListItem *> &items)
|
|
|
|
|
{
|
|
|
|
|
auto model = new ListModel(this);
|
|
|
|
|
model->setPixmapFunction(m_pixmapFunction);
|
2023-03-09 15:22:24 +01:00
|
|
|
// the sections only keep a weak reference to the items,
|
|
|
|
|
// they are owned by the allProducts model, since multiple sections can contain duplicates
|
|
|
|
|
// of the same item
|
|
|
|
|
model->setOwnsItems(false);
|
2023-01-10 12:15:49 +01:00
|
|
|
model->appendItems(items);
|
|
|
|
|
|
|
|
|
|
auto gridView = new SectionGridView(this);
|
2023-03-17 15:18:59 +01:00
|
|
|
gridView->setItemDelegate(m_itemDelegate);
|
2023-01-10 12:15:49 +01:00
|
|
|
gridView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
|
|
gridView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
|
|
gridView->setModel(model);
|
|
|
|
|
|
|
|
|
|
m_sectionModels.insert(section, model);
|
|
|
|
|
const auto it = m_gridViews.insert(section, gridView);
|
|
|
|
|
|
|
|
|
|
auto sectionLabel = new QLabel(section.name);
|
2023-01-16 17:20:56 +01:00
|
|
|
m_sectionLabels.append(sectionLabel);
|
2023-01-10 12:15:49 +01:00
|
|
|
sectionLabel->setContentsMargins(0, Core::WelcomePageHelpers::ItemGap, 0, 0);
|
|
|
|
|
sectionLabel->setFont(Core::WelcomePageHelpers::brandFont());
|
|
|
|
|
auto scrollArea = qobject_cast<QScrollArea *>(widget(0));
|
|
|
|
|
auto vbox = qobject_cast<QVBoxLayout *>(scrollArea->widget()->layout());
|
|
|
|
|
|
|
|
|
|
// insert new section depending on its priority, but before the last (stretch) item
|
|
|
|
|
int position = std::distance(m_gridViews.begin(), it) * 2; // a section has a label and a grid
|
|
|
|
|
QTC_ASSERT(position <= vbox->count() - 1, position = vbox->count() - 1);
|
|
|
|
|
vbox->insertWidget(position, sectionLabel);
|
|
|
|
|
vbox->insertWidget(position + 1, gridView);
|
|
|
|
|
|
|
|
|
|
// add the items also to the all products model to be able to search correctly
|
2023-03-17 15:18:59 +01:00
|
|
|
const QSet<ListItem *> allItems = toSet(m_allItemsModel->items());
|
2023-03-09 15:22:24 +01:00
|
|
|
const QList<ListItem *> newItems = filtered(items, [&allItems](ListItem *item) {
|
|
|
|
|
return !allItems.contains(item);
|
|
|
|
|
});
|
2023-03-17 15:18:59 +01:00
|
|
|
m_allItemsModel->appendItems(newItems);
|
2023-01-10 12:15:49 +01:00
|
|
|
|
2023-01-17 10:35:24 +01:00
|
|
|
// only show section label(s) if there is more than one section
|
|
|
|
|
m_sectionLabels.at(0)->setVisible(m_sectionLabels.size() > 1);
|
|
|
|
|
|
2023-01-10 12:15:49 +01:00
|
|
|
return model;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-16 17:20:56 +01:00
|
|
|
void SectionedGridView::clear()
|
|
|
|
|
{
|
2023-03-17 15:18:59 +01:00
|
|
|
m_allItemsModel->clear();
|
2023-01-16 17:20:56 +01:00
|
|
|
qDeleteAll(m_sectionModels);
|
|
|
|
|
qDeleteAll(m_sectionLabels);
|
|
|
|
|
qDeleteAll(m_gridViews);
|
|
|
|
|
m_sectionModels.clear();
|
|
|
|
|
m_sectionLabels.clear();
|
|
|
|
|
m_gridViews.clear();
|
2023-03-17 15:18:59 +01:00
|
|
|
m_allItemsView.reset();
|
2023-01-16 17:20:56 +01:00
|
|
|
}
|
|
|
|
|
|
2020-01-09 15:52:47 +01:00
|
|
|
} // namespace Core
|