From f2351b06e05f5a129df1e6a38557c692687674c6 Mon Sep 17 00:00:00 2001 From: Christian Stenger Date: Thu, 16 Jan 2020 15:48:32 +0100 Subject: [PATCH] QtSupport: Extract welcome page list model handling ...and move it to Core. Preparation for easier re-usage to avoid re-implementing the same more often. Change-Id: I4c902e74e63dd5416f2a52b4b08900e28e2a052a Reviewed-by: hjk --- src/plugins/coreplugin/welcomepagehelper.cpp | 444 +++++++++++++++++- src/plugins/coreplugin/welcomepagehelper.h | 96 ++++ src/plugins/qtsupport/exampleslistmodel.cpp | 383 +++++---------- src/plugins/qtsupport/exampleslistmodel.h | 40 +- .../qtsupport/gettingstartedwelcomepage.cpp | 258 ++-------- .../qtsupport/gettingstartedwelcomepage.h | 2 +- 6 files changed, 699 insertions(+), 524 deletions(-) diff --git a/src/plugins/coreplugin/welcomepagehelper.cpp b/src/plugins/coreplugin/welcomepagehelper.cpp index 86159f7cb7f..79d08dc172e 100644 --- a/src/plugins/coreplugin/welcomepagehelper.cpp +++ b/src/plugins/coreplugin/welcomepagehelper.cpp @@ -25,6 +25,7 @@ #include "welcomepagehelper.h" +#include #include #include #include @@ -32,23 +33,37 @@ #include #include #include +#include +#include +#include +#include namespace Core { using namespace Utils; +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; +} + SearchBox::SearchBox(QWidget *parent) : WelcomePageFrame(parent) { QPalette pal; - pal.setColor(QPalette::Base, Utils::creatorTheme()->color(Theme::Welcome_BackgroundColor)); + pal.setColor(QPalette::Base, themeColor(Theme::Welcome_BackgroundColor)); m_lineEdit = new FancyLineEdit; m_lineEdit->setFiltering(true); m_lineEdit->setFrame(false); - QFont f = font(); - f.setPixelSize(14); - m_lineEdit->setFont(f); + m_lineEdit->setFont(sizedFont(14, this)); m_lineEdit->setAttribute(Qt::WA_MacShowFocusRect, false); m_lineEdit->setPalette(pal); @@ -71,7 +86,7 @@ GridView::GridView(QWidget *parent) setGridStyle(Qt::NoPen); QPalette pal; - pal.setColor(QPalette::Base, Utils::creatorTheme()->color(Theme::Welcome_BackgroundColor)); + pal.setColor(QPalette::Base, themeColor(Theme::Welcome_BackgroundColor)); setPalette(pal); // Makes a difference on Mac. } @@ -195,4 +210,423 @@ QModelIndex GridProxyModel::mapFromSource(const QModelIndex &sourceIndex) const return index(proxyRow, proxyColumn, QModelIndex()); } +const QSize ListModel::defaultImageSize(188, 145); + +ListModel::ListModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ + qDeleteAll(m_items); + m_items.clear(); +} + +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; + if (pixmap.isNull()) + pixmap = fetchPixmapAndUpdatePixmapCache(item->imageUrl); + return pixmap; + } + case ItemTagsRole: + return item->tags; + default: + return QVariant(); + } +} + +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(); + + 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) { + return item->tags.contains(filterTag); + }); + } + + if (!m_filterStrings.isEmpty()) { + for (const QString &subString : m_filterStrings) { + bool wordMatch = false; + wordMatch |= bool(item->name.contains(subString, Qt::CaseInsensitive)); + if (wordMatch) + continue; + const auto subMatch = [&subString](const QString &elem) { return elem.contains(subString); }; + 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++; } + + SearchStringLexer(const QString &code) + : 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(); +} + +bool ListModelFilter::leaveFilterAcceptsRowBeforeFiltering(const ListItem *, bool *) const +{ + return false; +} + +ListItemDelegate::ListItemDelegate() +{ + lightColor = QColor(221, 220, 220); // color: "#dddcdc" + backgroundColor = themeColor(Theme::Welcome_BackgroundColor); + foregroundColor1 = themeColor(Theme::Welcome_ForegroundPrimaryColor); // light-ish. + foregroundColor2 = themeColor(Theme::Welcome_ForegroundSecondaryColor); // blacker. +} + +void ListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + const ListItem *item = index.data(ListModel::ItemRole).value(); + + // Quick hack for empty items in the last row. + if (!item) + return; + + const QRect rc = option.rect; + + const int d = 10; + const int x = rc.x() + d; + const int y = rc.y() + d; + const int w = rc.width() - 2 * d - GridProxyModel::GridItemGap; + const int h = rc.height() - 2 * d; + const bool hovered = option.state & QStyle::State_MouseOver; + + const int tagsBase = GridProxyModel::TagsSeparatorY + 10; + const int shiftY = GridProxyModel::TagsSeparatorY - 20; + const int nameY = GridProxyModel::TagsSeparatorY - 20; + + const QRect textRect = QRect(x, y + nameY, w, h); + + QTextOption wrapped; + wrapped.setWrapMode(QTextOption::WordWrap); + int offset = 0; + if (hovered) { + if (index != m_previousIndex) { + m_previousIndex = index; + m_startTime.start(); + m_currentArea = rc; + m_currentWidget = qobject_cast( + const_cast(option.widget)); + } + offset = m_startTime.elapsed() * GridProxyModel::GridItemHeight / 200; // Duration 200 ms. + if (offset < shiftY) + QTimer::singleShot(5, this, &ListItemDelegate::goon); + else if (offset > shiftY) + offset = shiftY; + } else { + m_previousIndex = QModelIndex(); + } + + const QFontMetrics fm(option.widget->font()); + const QRect shiftedTextRect = textRect.adjusted(0, -offset, 0, -offset); + + // The pixmap. + if (offset == 0) { + QPixmap pm = index.data(ListModel::ItemImageRole).value(); + QRect inner(x + 11, y - offset, ListModel::defaultImageSize.width(), + ListModel::defaultImageSize.height()); + QRect pixmapRect = inner; + if (!pm.isNull()) { + painter->setPen(foregroundColor2); + + adjustPixmapRect(&pixmapRect); + + QPoint pixmapPos = pixmapRect.center(); + pixmapPos.rx() -= pm.width() / pm.devicePixelRatio() / 2; + pixmapPos.ry() -= pm.height() / pm.devicePixelRatio() / 2; + painter->drawPixmap(pixmapPos, pm); + + drawPixmapOverlay(item, painter, option, pixmapRect); + + } else { + // The description text as fallback. + painter->setPen(foregroundColor2); + painter->setFont(sizedFont(11, option.widget)); + painter->drawText(pixmapRect.adjusted(6, 10, -6, -10), item->description, wrapped); + } + painter->setPen(foregroundColor1); + painter->drawRect(pixmapRect.adjusted(-1, -1, -1, -1)); + } + + // The title of the example. + painter->setPen(foregroundColor1); + painter->setFont(sizedFont(13, option.widget)); + QRectF nameRect; + if (offset) { + nameRect = painter->boundingRect(shiftedTextRect, item->name, wrapped); + painter->drawText(nameRect, item->name, wrapped); + } else { + nameRect = QRect(x, y + nameY, x + w, y + nameY + 20); + QString elidedName = fm.elidedText(item->name, Qt::ElideRight, w - 20); + painter->drawText(nameRect, elidedName); + } + + // The separator line below the example title. + if (offset) { + int ll = nameRect.bottom() + 5; + painter->setPen(lightColor); + painter->drawLine(x, ll, x + w, ll); + } + + // The description text. + if (offset) { + int dd = nameRect.height() + 10; + QRect descRect = shiftedTextRect.adjusted(0, dd, 0, dd); + painter->setPen(foregroundColor2); + painter->setFont(sizedFont(11, option.widget)); + painter->drawText(descRect, item->description, wrapped); + } + + // Separator line between text and 'Tags:' section + painter->setPen(lightColor); + painter->drawLine(x, y + GridProxyModel::TagsSeparatorY, + x + w, y + GridProxyModel::TagsSeparatorY); + + // The 'Tags:' section + const int tagsHeight = h - tagsBase; + const QFont tagsFont = sizedFont(10, option.widget); + const QFontMetrics tagsFontMetrics(tagsFont); + QRect tagsLabelRect = QRect(x, y + tagsBase, 30, tagsHeight - 2); + painter->setPen(foregroundColor2); + painter->setFont(tagsFont); + painter->drawText(tagsLabelRect, tr("Tags:")); + + painter->setPen(themeColor(Theme::Welcome_LinkColor)); + m_currentTagRects.clear(); + int xx = 0; + int yy = y + tagsBase; + for (const QString &tag : item->tags) { + const int ww = tagsFontMetrics.horizontalAdvance(tag) + 5; + if (xx + ww > w - 30) { + yy += 15; + xx = 0; + } + const QRect tagRect(xx + x + 30, yy, ww, 15); + painter->drawText(tagRect, tag); + m_currentTagRects.append({ tag, tagRect }); + xx += ww; + } + + // Box it when hovered. + if (hovered) { + painter->setPen(lightColor); + painter->drawRect(rc.adjusted(0, 0, -1, -1)); + } + +} + +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(); + QTC_ASSERT(item, return false); + auto mev = static_cast(event); + if (index.isValid()) { + const QPoint pos = mev->pos(); + if (pos.y() > option.rect.y() + GridProxyModel::TagsSeparatorY) { + //const QStringList tags = idx.data(Tags).toStringList(); + for (const auto &it : m_currentTagRects) { + if (it.second.contains(pos)) + emit tagClicked(it.first); + } + } else { + clickAction(item); + } + } + } + return QStyledItemDelegate::editorEvent(event, model, option, index); +} + +void ListItemDelegate::drawPixmapOverlay(const ListItem *, QPainter *, + const QStyleOptionViewItem &, const QRect &) const +{ +} + +void ListItemDelegate::clickAction(const ListItem *) const +{ +} + +void ListItemDelegate::adjustPixmapRect(QRect *) const +{ +} + +void ListItemDelegate::goon() +{ + if (m_currentWidget) + m_currentWidget->viewport()->update(m_currentArea); +} + } // namespace Core diff --git a/src/plugins/coreplugin/welcomepagehelper.h b/src/plugins/coreplugin/welcomepagehelper.h index be5d3a274e0..117535a4bba 100644 --- a/src/plugins/coreplugin/welcomepagehelper.h +++ b/src/plugins/coreplugin/welcomepagehelper.h @@ -30,6 +30,10 @@ #include +#include +#include +#include +#include #include namespace Utils { class FancyLineEdit; } @@ -81,4 +85,96 @@ private: int m_columnCount = 1; }; +class CORE_EXPORT ListItem +{ +public: + QString name; + QString description; + QString imageUrl; + QStringList tags; +}; + +class CORE_EXPORT ListModel : public QAbstractListModel +{ +public: + enum ListDataRole { + ItemRole = Qt::UserRole, + ItemImageRole, + ItemTagsRole + }; + + explicit ListModel(QObject *parent); + ~ListModel() override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const final; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual QPixmap fetchPixmapAndUpdatePixmapCache(const QString &url) const = 0; + + static const QSize defaultImageSize; + +protected: + QList m_items; +}; + +class CORE_EXPORT ListModelFilter : public QSortFilterProxyModel +{ +public: + ListModelFilter(ListModel *sourceModel, QObject *parent); + + void setSearchString(const QString &arg); + +protected: + virtual bool leaveFilterAcceptsRowBeforeFiltering(const ListItem *item, + bool *earlyExitResult) const; + +private: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const final; + void timerEvent(QTimerEvent *event) final; + + void delayedUpdateFilter(); + + QString m_searchString; + QStringList m_filterTags; + QStringList m_filterStrings; + int m_timerId = 0; +}; + +class CORE_EXPORT ListItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + ListItemDelegate(); + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +signals: + void tagClicked(const QString &tag); + +protected: + bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, + const QModelIndex &index) override; + + virtual void drawPixmapOverlay(const ListItem *item, QPainter *painter, + const QStyleOptionViewItem &option, + const QRect ¤tPixmapRect) const; + virtual void clickAction(const ListItem *item) const; + virtual void adjustPixmapRect(QRect *pixmapRect) const; + + void goon(); + + QColor lightColor; + QColor backgroundColor; + QColor foregroundColor1; + QColor foregroundColor2; + +private: + mutable QPersistentModelIndex m_previousIndex; + mutable QElapsedTimer m_startTime; + mutable QRect m_currentArea; + mutable QPointer m_currentWidget; + mutable QVector> m_currentTagRects; +}; + } // namespace Core + +Q_DECLARE_METATYPE(Core::ListItem *) diff --git a/src/plugins/qtsupport/exampleslistmodel.cpp b/src/plugins/qtsupport/exampleslistmodel.cpp index af27807cc03..64f9511b245 100644 --- a/src/plugins/qtsupport/exampleslistmodel.cpp +++ b/src/plugins/qtsupport/exampleslistmodel.cpp @@ -28,14 +28,11 @@ #include "screenshotcropper.h" #include -#include #include #include -#include #include #include #include -#include #include #include @@ -44,7 +41,6 @@ #include #include -#include #include #include #include @@ -54,8 +50,6 @@ namespace QtSupport { namespace Internal { -const QSize ExamplesListModel::exampleImageSize(188, 145); - static bool debugExamples() { static bool isDebugging = qEnvironmentVariableIsSet("QTC_DEBUG_EXAMPLESMODEL"); @@ -232,7 +226,7 @@ int ExampleSetModel::getExtraExampleSetIndex(int i) const } ExamplesListModel::ExamplesListModel(QObject *parent) - : QAbstractListModel(parent) + : Core::ListModel(parent) { connect(&m_exampleSetModel, &ExampleSetModel::selectedExampleSetChanged, this, &ExamplesListModel::updateExamples); @@ -271,53 +265,54 @@ static QString relativeOrInstallPath(const QString &path, const QString &manifes return relativeResolvedPath; } -static bool isValidExampleOrDemo(ExampleItem &item) +static bool isValidExampleOrDemo(ExampleItem *item) { + QTC_ASSERT(item, return false); static QString invalidPrefix = QLatin1String("qthelp:////"); /* means that the qthelp url doesn't have any namespace */ QString reason; bool ok = true; - if (!item.hasSourceCode || !QFileInfo::exists(item.projectPath)) { + if (!item->hasSourceCode || !QFileInfo::exists(item->projectPath)) { ok = false; - reason = QString::fromLatin1("projectPath \"%1\" empty or does not exist").arg(item.projectPath); - } else if (item.imageUrl.startsWith(invalidPrefix) || !QUrl(item.imageUrl).isValid()) { + reason = QString::fromLatin1("projectPath \"%1\" empty or does not exist").arg(item->projectPath); + } else if (item->imageUrl.startsWith(invalidPrefix) || !QUrl(item->imageUrl).isValid()) { ok = false; - reason = QString::fromLatin1("imageUrl \"%1\" not valid").arg(item.imageUrl); - } else if (!item.docUrl.isEmpty() - && (item.docUrl.startsWith(invalidPrefix) || !QUrl(item.docUrl).isValid())) { + reason = QString::fromLatin1("imageUrl \"%1\" not valid").arg(item->imageUrl); + } else if (!item->docUrl.isEmpty() + && (item->docUrl.startsWith(invalidPrefix) || !QUrl(item->docUrl).isValid())) { ok = false; - reason = QString::fromLatin1("docUrl \"%1\" non-empty but not valid").arg(item.docUrl); + reason = QString::fromLatin1("docUrl \"%1\" non-empty but not valid").arg(item->docUrl); } if (!ok) { - item.tags.append(QLatin1String("broken")); + item->tags.append(QLatin1String("broken")); if (debugExamples()) - qWarning() << QString::fromLatin1("ERROR: Item \"%1\" broken: %2").arg(item.name, reason); + qWarning() << QString::fromLatin1("ERROR: Item \"%1\" broken: %2").arg(item->name, reason); } - if (debugExamples() && item.description.isEmpty()) - qWarning() << QString::fromLatin1("WARNING: Item \"%1\" has no description").arg(item.name); + if (debugExamples() && item->description.isEmpty()) + qWarning() << QString::fromLatin1("WARNING: Item \"%1\" has no description").arg(item->name); return ok || debugExamples(); } void ExamplesListModel::parseExamples(QXmlStreamReader *reader, const QString &projectsOffset, const QString &examplesInstallPath) { - ExampleItem item; + ExampleItem *item = nullptr; const QChar slash = QLatin1Char('/'); while (!reader->atEnd()) { switch (reader->readNext()) { case QXmlStreamReader::StartElement: if (reader->name() == QLatin1String("example")) { - item = ExampleItem(); - item.type = Example; + item = new ExampleItem; + item->type = Example; QXmlStreamAttributes attributes = reader->attributes(); - item.name = attributes.value(QLatin1String("name")).toString(); - item.projectPath = attributes.value(QLatin1String("projectPath")).toString(); - item.hasSourceCode = !item.projectPath.isEmpty(); - item.projectPath = relativeOrInstallPath(item.projectPath, projectsOffset, examplesInstallPath); - item.imageUrl = attributes.value(QLatin1String("imageUrl")).toString(); - QPixmapCache::remove(item.imageUrl); - item.docUrl = attributes.value(QLatin1String("docUrl")).toString(); - item.isHighlighted = attributes.value(QLatin1String("isHighlighted")).toString() == QLatin1String("true"); + item->name = attributes.value(QLatin1String("name")).toString(); + item->projectPath = attributes.value(QLatin1String("projectPath")).toString(); + item->hasSourceCode = !item->projectPath.isEmpty(); + item->projectPath = relativeOrInstallPath(item->projectPath, projectsOffset, examplesInstallPath); + item->imageUrl = attributes.value(QLatin1String("imageUrl")).toString(); + QPixmapCache::remove(item->imageUrl); + item->docUrl = attributes.value(QLatin1String("docUrl")).toString(); + item->isHighlighted = attributes.value(QLatin1String("isHighlighted")).toString() == QLatin1String("true"); } else if (reader->name() == QLatin1String("fileToOpen")) { const QString mainFileAttribute = reader->attributes().value( @@ -325,23 +320,23 @@ void ExamplesListModel::parseExamples(QXmlStreamReader *reader, const QString filePath = relativeOrInstallPath( reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement), projectsOffset, examplesInstallPath); - item.filesToOpen.append(filePath); + item->filesToOpen.append(filePath); if (mainFileAttribute.compare(QLatin1String("true"), Qt::CaseInsensitive) == 0) - item.mainFile = filePath; + item->mainFile = filePath; } else if (reader->name() == QLatin1String("description")) { - item.description = fixStringForTags(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); + item->description = fixStringForTags(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); } else if (reader->name() == QLatin1String("dependency")) { - item.dependencies.append(projectsOffset + slash + reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); + item->dependencies.append(projectsOffset + slash + reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); } else if (reader->name() == QLatin1String("tags")) { - item.tags = trimStringList(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement).split(QLatin1Char(','), QString::SkipEmptyParts)); + item->tags = trimStringList(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement).split(QLatin1Char(','), QString::SkipEmptyParts)); } else if (reader->name() == QLatin1String("platforms")) { - item.platforms = trimStringList(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement).split(QLatin1Char(','), QString::SkipEmptyParts)); + item->platforms = trimStringList(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement).split(QLatin1Char(','), QString::SkipEmptyParts)); } break; case QXmlStreamReader::EndElement: if (reader->name() == QLatin1String("example")) { if (isValidExampleOrDemo(item)) - m_exampleItems.append(item); + m_items.append(item); } else if (reader->name() == QLatin1String("examples")) { return; } @@ -355,38 +350,38 @@ void ExamplesListModel::parseExamples(QXmlStreamReader *reader, void ExamplesListModel::parseDemos(QXmlStreamReader *reader, const QString &projectsOffset, const QString &demosInstallPath) { - ExampleItem item; + ExampleItem *item = nullptr; const QChar slash = QLatin1Char('/'); while (!reader->atEnd()) { switch (reader->readNext()) { case QXmlStreamReader::StartElement: if (reader->name() == QLatin1String("demo")) { - item = ExampleItem(); - item.type = Demo; + item = new ExampleItem; + item->type = Demo; QXmlStreamAttributes attributes = reader->attributes(); - item.name = attributes.value(QLatin1String("name")).toString(); - item.projectPath = attributes.value(QLatin1String("projectPath")).toString(); - item.hasSourceCode = !item.projectPath.isEmpty(); - item.projectPath = relativeOrInstallPath(item.projectPath, projectsOffset, demosInstallPath); - item.imageUrl = attributes.value(QLatin1String("imageUrl")).toString(); - QPixmapCache::remove(item.imageUrl); - item.docUrl = attributes.value(QLatin1String("docUrl")).toString(); - item.isHighlighted = attributes.value(QLatin1String("isHighlighted")).toString() == QLatin1String("true"); + item->name = attributes.value(QLatin1String("name")).toString(); + item->projectPath = attributes.value(QLatin1String("projectPath")).toString(); + item->hasSourceCode = !item->projectPath.isEmpty(); + item->projectPath = relativeOrInstallPath(item->projectPath, projectsOffset, demosInstallPath); + item->imageUrl = attributes.value(QLatin1String("imageUrl")).toString(); + QPixmapCache::remove(item->imageUrl); + item->docUrl = attributes.value(QLatin1String("docUrl")).toString(); + item->isHighlighted = attributes.value(QLatin1String("isHighlighted")).toString() == QLatin1String("true"); } else if (reader->name() == QLatin1String("fileToOpen")) { - item.filesToOpen.append(relativeOrInstallPath(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement), + item->filesToOpen.append(relativeOrInstallPath(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement), projectsOffset, demosInstallPath)); } else if (reader->name() == QLatin1String("description")) { - item.description = fixStringForTags(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); + item->description = fixStringForTags(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); } else if (reader->name() == QLatin1String("dependency")) { - item.dependencies.append(projectsOffset + slash + reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); + item->dependencies.append(projectsOffset + slash + reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); } else if (reader->name() == QLatin1String("tags")) { - item.tags = reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement).split(QLatin1Char(',')); + item->tags = reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement).split(QLatin1Char(',')); } break; case QXmlStreamReader::EndElement: if (reader->name() == QLatin1String("demo")) { if (isValidExampleOrDemo(item)) - m_exampleItems.append(item); + m_items.append(item); } else if (reader->name() == QLatin1String("demos")) { return; } @@ -399,40 +394,40 @@ void ExamplesListModel::parseDemos(QXmlStreamReader *reader, void ExamplesListModel::parseTutorials(QXmlStreamReader *reader, const QString &projectsOffset) { - ExampleItem item; + ExampleItem *item = nullptr; const QChar slash = QLatin1Char('/'); while (!reader->atEnd()) { switch (reader->readNext()) { case QXmlStreamReader::StartElement: if (reader->name() == QLatin1String("tutorial")) { - item = ExampleItem(); - item.type = Tutorial; + item = new ExampleItem; + item->type = Tutorial; QXmlStreamAttributes attributes = reader->attributes(); - item.name = attributes.value(QLatin1String("name")).toString(); - item.projectPath = attributes.value(QLatin1String("projectPath")).toString(); - item.hasSourceCode = !item.projectPath.isEmpty(); - item.projectPath.prepend(slash); - item.projectPath.prepend(projectsOffset); - item.imageUrl = Utils::StyleHelper::dpiSpecificImageFile( + item->name = attributes.value(QLatin1String("name")).toString(); + item->projectPath = attributes.value(QLatin1String("projectPath")).toString(); + item->hasSourceCode = !item->projectPath.isEmpty(); + item->projectPath.prepend(slash); + item->projectPath.prepend(projectsOffset); + item->imageUrl = Utils::StyleHelper::dpiSpecificImageFile( attributes.value(QLatin1String("imageUrl")).toString()); - QPixmapCache::remove(item.imageUrl); - item.docUrl = attributes.value(QLatin1String("docUrl")).toString(); - item.isVideo = attributes.value(QLatin1String("isVideo")).toString() == QLatin1String("true"); - item.videoUrl = attributes.value(QLatin1String("videoUrl")).toString(); - item.videoLength = attributes.value(QLatin1String("videoLength")).toString(); + QPixmapCache::remove(item->imageUrl); + item->docUrl = attributes.value(QLatin1String("docUrl")).toString(); + item->isVideo = attributes.value(QLatin1String("isVideo")).toString() == QLatin1String("true"); + item->videoUrl = attributes.value(QLatin1String("videoUrl")).toString(); + item->videoLength = attributes.value(QLatin1String("videoLength")).toString(); } else if (reader->name() == QLatin1String("fileToOpen")) { - item.filesToOpen.append(projectsOffset + slash + reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); + item->filesToOpen.append(projectsOffset + slash + reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); } else if (reader->name() == QLatin1String("description")) { - item.description = fixStringForTags(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); + item->description = fixStringForTags(reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); } else if (reader->name() == QLatin1String("dependency")) { - item.dependencies.append(projectsOffset + slash + reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); + item->dependencies.append(projectsOffset + slash + reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement)); } else if (reader->name() == QLatin1String("tags")) { - item.tags = reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement).split(QLatin1Char(',')); + item->tags = reader->readElementText(QXmlStreamReader::ErrorOnUnexpectedElement).split(QLatin1Char(',')); } break; case QXmlStreamReader::EndElement: if (reader->name() == QLatin1String("tutorial")) - m_exampleItems.append(item); + m_items.append(item); else if (reader->name() == QLatin1String("tutorials")) return; break; @@ -456,7 +451,8 @@ void ExamplesListModel::updateExamples() QStringList sources = m_exampleSetModel.exampleSources(&examplesInstallPath, &demosInstallPath); beginResetModel(); - m_exampleItems.clear(); + qDeleteAll(m_items); + m_items.clear(); foreach (const QString &exampleSource, sources) { QFile exampleFile(exampleSource); @@ -497,6 +493,27 @@ void ExamplesListModel::updateExamples() endResetModel(); } +QPixmap ExamplesListModel::fetchPixmapAndUpdatePixmapCache(const QString &url) const +{ + QPixmap pixmap; + pixmap.load(url); + if (pixmap.isNull()) + pixmap.load(resourcePath() + "/welcomescreen/widgets/" + url); + if (pixmap.isNull()) { + QByteArray fetchedData = Core::HelpManager::fileData(url); + if (!fetchedData.isEmpty()) { + QBuffer imgBuffer(&fetchedData); + imgBuffer.open(QIODevice::ReadOnly); + QImageReader reader(&imgBuffer); + QImage img = reader.read(); + img = ScreenshotCropper::croppedImage(img, url, ListModel::defaultImageSize); + pixmap = QPixmap::fromImage(img); + } + } + QPixmapCache::insert(url, pixmap); + return pixmap; +} + void ExampleSetModel::updateQtVersionList() { QList versions = QtVersionManager::sortVersions(QtVersionManager::versions( @@ -605,54 +622,26 @@ QStringList ExampleSetModel::exampleSources(QString *examplesInstallPath, QStrin return sources; } -int ExamplesListModel::rowCount(const QModelIndex &) const +QString prefixForItem(const ExampleItem *item) { - return m_exampleItems.size(); -} - -QString prefixForItem(const ExampleItem &item) -{ - if (item.isHighlighted) + QTC_ASSERT(item, return {}); + if (item->isHighlighted) return QLatin1String("0000 "); return QString(); } QVariant ExamplesListModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= m_exampleItems.count()) + if (!index.isValid() || index.row() >= m_items.count()) return QVariant(); - const ExampleItem &item = m_exampleItems.at(index.row()); + ExampleItem *item = static_cast(m_items.at(index.row())); switch (role) { case Qt::DisplayRole: // for search only - return QString(prefixForItem(item) + item.name + ' ' + item.tags.join(' ')); - case ExampleItemRole: - return QVariant::fromValue(item); - case ExampleImageRole: { - QPixmap pixmap; - if (QPixmapCache::find(item.imageUrl, &pixmap)) - return pixmap; - pixmap.load(item.imageUrl); - if (pixmap.isNull()) - pixmap.load(resourcePath() + "/welcomescreen/widgets/" + item.imageUrl); - if (pixmap.isNull()) { - QByteArray fetchedData = Core::HelpManager::fileData(item.imageUrl); - if (!fetchedData.isEmpty()) { - QBuffer imgBuffer(&fetchedData); - imgBuffer.open(QIODevice::ReadOnly); - QImageReader reader(&imgBuffer); - QImage img = reader.read(); - img = ScreenshotCropper::croppedImage(img, item.imageUrl, - ExamplesListModel::exampleImageSize); - pixmap = QPixmap::fromImage(img); - } - } - QPixmapCache::insert(item.imageUrl, pixmap); - return pixmap; - } + return QString(prefixForItem(item) + item->name + ' ' + item->tags.join(' ')); default: - return QVariant(); + return ListModel::data(index, role); } } @@ -699,181 +688,27 @@ void ExampleSetModel::tryToInitialize() ExamplesListModelFilter::ExamplesListModelFilter(ExamplesListModel *sourceModel, bool showTutorialsOnly, QObject *parent) : - QSortFilterProxyModel(parent), + Core::ListModelFilter(sourceModel, parent), m_showTutorialsOnly(showTutorialsOnly) { - setSourceModel(sourceModel); - setDynamicSortFilter(true); - setFilterCaseSensitivity(Qt::CaseInsensitive); - sort(0); } -bool ExamplesListModelFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +bool ExamplesListModelFilter::leaveFilterAcceptsRowBeforeFiltering(const Core::ListItem *item, + bool *earlyExitResult) const { - const ExampleItem item = sourceModel()->index(sourceRow, 0, sourceParent).data( - ExamplesListModel::ExampleItemRole).value(); + QTC_ASSERT(earlyExitResult, return false); - if (m_showTutorialsOnly && item.type != Tutorial) - return false; - - if (!m_showTutorialsOnly && item.type != Example && item.type != Demo) - return false; - - if (!m_filterTags.isEmpty()) { - return Utils::allOf(m_filterTags, [&item](const QString &filterTag) { - return item.tags.contains(filterTag); - }); + const ExampleItem *exampleItem = static_cast(item); + if (m_showTutorialsOnly && exampleItem->type != Tutorial) { + *earlyExitResult = false; + return true; } - if (!m_filterStrings.isEmpty()) { - for (const QString &subString : m_filterStrings) { - bool wordMatch = false; - wordMatch |= bool(item.name.contains(subString, Qt::CaseInsensitive)); - if (wordMatch) - continue; - const auto subMatch = [&subString](const QString &elem) { return elem.contains(subString); }; - wordMatch |= Utils::contains(item.tags, subMatch); - if (wordMatch) - continue; - wordMatch |= bool(item.description.contains(subString, Qt::CaseInsensitive)); - if (!wordMatch) - return false; - } + if (!m_showTutorialsOnly && exampleItem->type != Example && exampleItem->type != Demo) { + *earlyExitResult = false; + return true; } - - return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); -} - -void ExamplesListModelFilter::delayedUpdateFilter() -{ - if (m_timerId != 0) - killTimer(m_timerId); - - m_timerId = startTimer(320); -} - -void ExamplesListModelFilter::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++; } - - SearchStringLexer(const QString &code) - : 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 ExamplesListModelFilter::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(); + return false; } } // namespace Internal diff --git a/src/plugins/qtsupport/exampleslistmodel.h b/src/plugins/qtsupport/exampleslistmodel.h index 44d898f17dd..80b0a8fe333 100644 --- a/src/plugins/qtsupport/exampleslistmodel.h +++ b/src/plugins/qtsupport/exampleslistmodel.h @@ -25,6 +25,8 @@ #pragma once +#include + #include #include @@ -98,17 +100,13 @@ enum InstructionalType Example = 0, Demo, Tutorial }; -class ExampleItem +class ExampleItem : public Core::ListItem { public: - QString name; QString projectPath; - QString description; - QString imageUrl; QString docUrl; QStringList filesToOpen; QString mainFile; /* file to be visible after opening filesToOpen */ - QStringList tags; QStringList dependencies; InstructionalType type; int difficulty = 0; @@ -120,19 +118,12 @@ public: QStringList platforms; }; -class ExamplesListModel : public QAbstractListModel +class ExamplesListModel : public Core::ListModel { Q_OBJECT - public: - enum ExampleListDataRole { - ExampleItemRole = Qt::UserRole, - ExampleImageRole = Qt::UserRole + 1 - }; - explicit ExamplesListModel(QObject *parent); - int rowCount(const QModelIndex &parent = QModelIndex()) const final; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const final; void updateExamples(); @@ -140,7 +131,7 @@ public: QStringList exampleSets() const; ExampleSetModel *exampleSetModel() { return &m_exampleSetModel; } - static const QSize exampleImageSize; + QPixmap fetchPixmapAndUpdatePixmapCache(const QString &url) const override; signals: void selectedExampleSetChanged(int); @@ -155,32 +146,21 @@ private: void parseTutorials(QXmlStreamReader *reader, const QString &projectsOffset); ExampleSetModel m_exampleSetModel; - QList m_exampleItems; }; -class ExamplesListModelFilter : public QSortFilterProxyModel +class ExamplesListModelFilter : public Core::ListModelFilter { - Q_OBJECT - public: ExamplesListModelFilter(ExamplesListModel *sourceModel, bool showTutorialsOnly, QObject *parent); - void setSearchString(const QString &arg); - +protected: + bool leaveFilterAcceptsRowBeforeFiltering(const Core::ListItem *item, + bool *earlyExitResult) const override; private: - bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const final; - void timerEvent(QTimerEvent *event) final; - - void delayedUpdateFilter(); - const bool m_showTutorialsOnly; - QString m_searchString; - QStringList m_filterTags; - QStringList m_filterStrings; - int m_timerId = 0; }; } // namespace Internal } // namespace QtSupport -Q_DECLARE_METATYPE(QtSupport::Internal::ExampleItem) +Q_DECLARE_METATYPE(QtSupport::Internal::ExampleItem *) diff --git a/src/plugins/qtsupport/gettingstartedwelcomepage.cpp b/src/plugins/qtsupport/gettingstartedwelcomepage.cpp index a301ceb3ed9..db737fb5aca 100644 --- a/src/plugins/qtsupport/gettingstartedwelcomepage.cpp +++ b/src/plugins/qtsupport/gettingstartedwelcomepage.cpp @@ -168,18 +168,18 @@ QString ExamplesWelcomePage::copyToAlternativeLocation(const QFileInfo& proFileI return QString(); } -void ExamplesWelcomePage::openProject(const ExampleItem &item) +void ExamplesWelcomePage::openProject(const ExampleItem *item) { using namespace ProjectExplorer; - QString proFile = item.projectPath; + QString proFile = item->projectPath; if (proFile.isEmpty()) return; - QStringList filesToOpen = item.filesToOpen; - if (!item.mainFile.isEmpty()) { + QStringList filesToOpen = item->filesToOpen; + if (!item->mainFile.isEmpty()) { // ensure that the main file is opened on top (i.e. opened last) - filesToOpen.removeAll(item.mainFile); - filesToOpen.append(item.mainFile); + filesToOpen.removeAll(item->mainFile); + filesToOpen.append(item->mainFile); } QFileInfo proFileInfo(proFile); @@ -195,7 +195,7 @@ void ExamplesWelcomePage::openProject(const ExampleItem &item) || !QFileInfo(pathInfo.path()).isWritable() /* shadow build directory */; }); if (needsCopy) - proFile = copyToAlternativeLocation(proFileInfo, filesToOpen, item.dependencies); + proFile = copyToAlternativeLocation(proFileInfo, filesToOpen, item->dependencies); // don't try to load help and files if loading the help request is being cancelled if (proFile.isEmpty()) @@ -204,7 +204,7 @@ void ExamplesWelcomePage::openProject(const ExampleItem &item) if (result) { ICore::openFiles(filesToOpen); ModeManager::activateMode(Core::Constants::MODE_EDIT); - QUrl docUrl = QUrl::fromUserInput(item.docUrl); + QUrl docUrl = QUrl::fromUserInput(item->docUrl); if (docUrl.isValid()) HelpManager::showHelpUrl(docUrl, HelpManager::ExternalHelpAlways); ModeManager::activateMode(ProjectExplorer::Constants::MODE_SESSION); @@ -213,217 +213,49 @@ void ExamplesWelcomePage::openProject(const ExampleItem &item) } } -////////////////////////////// - -static QColor themeColor(Theme::Color role) +class ExampleDelegate : public ListItemDelegate { - return Utils::creatorTheme()->color(role); -} - -static QFont sizedFont(int size, const QWidget *widget, bool underline = false) -{ - QFont f = widget->font(); - f.setPixelSize(size); - f.setUnderline(underline); - return f; -} - -class ExampleDelegate : public QStyledItemDelegate -{ - Q_OBJECT - public: - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const final - { - const ExampleItem item = index.data(ExamplesListModel::ExampleItemRole).value(); - const QRect rc = option.rect; - - // Quick hack for empty items in the last row. - if (item.name.isEmpty()) - return; - - const int d = 10; - const int x = rc.x() + d; - const int y = rc.y() + d; - const int w = rc.width() - 2 * d - GridProxyModel::GridItemGap; - const int h = rc.height() - 2 * d; - const bool hovered = option.state & QStyle::State_MouseOver; - - const int tagsBase = GridProxyModel::TagsSeparatorY + 10; - const int shiftY = GridProxyModel::TagsSeparatorY - 20; - const int nameY = GridProxyModel::TagsSeparatorY - 20; - - const QRect textRect = QRect(x, y + nameY, w, h); - - QTextOption wrapped; - wrapped.setWrapMode(QTextOption::WordWrap); - int offset = 0; - if (hovered) { - if (index != m_previousIndex) { - m_previousIndex = index; - m_startTime.start(); - m_currentArea = rc; - m_currentWidget = qobject_cast( - const_cast(option.widget)); - } - offset = m_startTime.elapsed() * GridProxyModel::GridItemHeight / 200; // Duration 200 ms. - if (offset < shiftY) - QTimer::singleShot(5, this, &ExampleDelegate::goon); - else if (offset > shiftY) - offset = shiftY; - } else { - m_previousIndex = QModelIndex(); - } - - const QFontMetrics fm(option.widget->font()); - const QRect shiftedTextRect = textRect.adjusted(0, -offset, 0, -offset); - - // The pixmap. - if (offset == 0) { - QPixmap pm = index.data(ExamplesListModel::ExampleImageRole).value(); - QRect inner(x + 11, y - offset, ExamplesListModel::exampleImageSize.width(), - ExamplesListModel::exampleImageSize.height()); - QRect pixmapRect = inner; - if (!pm.isNull()) { - painter->setPen(foregroundColor2); - if (!m_showExamples) - pixmapRect = inner.adjusted(6, 20, -6, -15); - QPoint pixmapPos = pixmapRect.center(); - pixmapPos.rx() -= pm.width() / pm.devicePixelRatio() / 2; - pixmapPos.ry() -= pm.height() / pm.devicePixelRatio() / 2; - painter->drawPixmap(pixmapPos, pm); - if (item.isVideo) { - painter->setFont(sizedFont(13, option.widget)); - QString videoLen = item.videoLength; - painter->drawText(pixmapRect.adjusted(0, 0, 0, painter->font().pixelSize() + 3), - videoLen, Qt::AlignBottom | Qt::AlignHCenter); - } - } else { - // The description text as fallback. - painter->setPen(foregroundColor2); - painter->setFont(sizedFont(11, option.widget)); - painter->drawText(pixmapRect.adjusted(6, 10, -6, -10), item.description, wrapped); - } - painter->setPen(foregroundColor1); - painter->drawRect(pixmapRect.adjusted(-1, -1, -1, -1)); - } - - // The title of the example. - painter->setPen(foregroundColor1); - painter->setFont(sizedFont(13, option.widget)); - QRectF nameRect; - if (offset) { - nameRect = painter->boundingRect(shiftedTextRect, item.name, wrapped); - painter->drawText(nameRect, item.name, wrapped); - } else { - nameRect = QRect(x, y + nameY, x + w, y + nameY + 20); - QString elidedName = fm.elidedText(item.name, Qt::ElideRight, w - 20); - painter->drawText(nameRect, elidedName); - } - - // The separator line below the example title. - if (offset) { - int ll = nameRect.bottom() + 5; - painter->setPen(lightColor); - painter->drawLine(x, ll, x + w, ll); - } - - // The description text. - if (offset) { - int dd = nameRect.height() + 10; - QRect descRect = shiftedTextRect.adjusted(0, dd, 0, dd); - painter->setPen(foregroundColor2); - painter->setFont(sizedFont(11, option.widget)); - painter->drawText(descRect, item.description, wrapped); - } - - // Separator line between text and 'Tags:' section - painter->setPen(lightColor); - painter->drawLine(x, y + GridProxyModel::TagsSeparatorY, - x + w, y + GridProxyModel::TagsSeparatorY); - - // The 'Tags:' section - const int tagsHeight = h - tagsBase; - const QFont tagsFont = sizedFont(10, option.widget); - const QFontMetrics tagsFontMetrics(tagsFont); - QRect tagsLabelRect = QRect(x, y + tagsBase, 30, tagsHeight - 2); - painter->setPen(foregroundColor2); - painter->setFont(tagsFont); - painter->drawText(tagsLabelRect, ExamplesWelcomePage::tr("Tags:")); - - painter->setPen(themeColor(Theme::Welcome_LinkColor)); - m_currentTagRects.clear(); - int xx = 0; - int yy = y + tagsBase; - for (const QString &tag : item.tags) { - const int ww = tagsFontMetrics.horizontalAdvance(tag) + 5; - if (xx + ww > w - 30) { - yy += 15; - xx = 0; - } - const QRect tagRect(xx + x + 30, yy, ww, 15); - painter->drawText(tagRect, tag); - m_currentTagRects.append({ tag, tagRect }); - xx += ww; - } - - // Box it when hovered. - if (hovered) { - painter->setPen(lightColor); - painter->drawRect(rc.adjusted(0, 0, -1, -1)); - } - } - - void goon() - { - if (m_currentWidget) - m_currentWidget->viewport()->update(m_currentArea); - } - - bool editorEvent(QEvent *ev, QAbstractItemModel *model, - const QStyleOptionViewItem &option, const QModelIndex &idx) final - { - if (ev->type() == QEvent::MouseButtonRelease) { - const ExampleItem item = idx.data(Qt::UserRole).value(); - auto mev = static_cast(ev); - if (idx.isValid()) { - const QPoint pos = mev->pos(); - if (pos.y() > option.rect.y() + GridProxyModel::TagsSeparatorY) { - //const QStringList tags = idx.data(Tags).toStringList(); - for (const auto &it : m_currentTagRects) { - if (it.second.contains(pos)) - emit tagClicked(it.first); - } - } else { - if (item.isVideo) - QDesktopServices::openUrl(QUrl::fromUserInput(item.videoUrl)); - else if (item.hasSourceCode) - ExamplesWelcomePage::openProject(item); - else - HelpManager::showHelpUrl(QUrl::fromUserInput(item.docUrl), - HelpManager::ExternalHelpAlways); - } - } - } - return QStyledItemDelegate::editorEvent(ev, model, option, idx); - } void setShowExamples(bool showExamples) { m_showExamples = showExamples; goon(); } -signals: - void tagClicked(const QString &tag); +protected: + void clickAction(const ListItem *item) const override + { + QTC_ASSERT(item, return); + const auto exampleItem = static_cast(item); -private: - const QColor lightColor = QColor(221, 220, 220); // color: "#dddcdc" - const QColor backgroundColor = themeColor(Theme::Welcome_BackgroundColor); - const QColor foregroundColor1 = themeColor(Theme::Welcome_ForegroundPrimaryColor); // light-ish. - const QColor foregroundColor2 = themeColor(Theme::Welcome_ForegroundSecondaryColor); // blacker. + if (exampleItem->isVideo) + QDesktopServices::openUrl(QUrl::fromUserInput(exampleItem->videoUrl)); + else if (exampleItem->hasSourceCode) + ExamplesWelcomePage::openProject(exampleItem); + else + HelpManager::showHelpUrl(QUrl::fromUserInput(exampleItem->docUrl), + HelpManager::ExternalHelpAlways); + } + + void drawPixmapOverlay(const ListItem *item, QPainter *painter, + const QStyleOptionViewItem &option, + const QRect ¤tPixmapRect) const override + { + QTC_ASSERT(item, return); + const auto exampleItem = static_cast(item); + if (exampleItem->isVideo) { + QFont f = option.widget->font(); + f.setPixelSize(13); + painter->setFont(f); + QString videoLen = exampleItem->videoLength; + painter->drawText(currentPixmapRect.adjusted(0, 0, 0, painter->font().pixelSize() + 3), + videoLen, Qt::AlignBottom | Qt::AlignHCenter); + } + } + + void adjustPixmapRect(QRect *pixmapRect) const override + { + if (!m_showExamples) + *pixmapRect = pixmapRect->adjusted(6, 20, -6, -15); + } - mutable QPersistentModelIndex m_previousIndex; - mutable QElapsedTimer m_startTime; - mutable QRect m_currentArea; - mutable QPointer m_currentWidget; - mutable QVector> m_currentTagRects; bool m_showExamples = true; }; @@ -514,5 +346,3 @@ QWidget *ExamplesWelcomePage::createWidget() const } // namespace Internal } // namespace QtSupport - -#include "gettingstartedwelcomepage.moc" diff --git a/src/plugins/qtsupport/gettingstartedwelcomepage.h b/src/plugins/qtsupport/gettingstartedwelcomepage.h index 4065160e460..fe0b05bfdf0 100644 --- a/src/plugins/qtsupport/gettingstartedwelcomepage.h +++ b/src/plugins/qtsupport/gettingstartedwelcomepage.h @@ -49,7 +49,7 @@ public: Core::Id id() const final; QWidget *createWidget() const final; - static void openProject(const ExampleItem &item); + static void openProject(const ExampleItem *item); private: static QString copyToAlternativeLocation(const QFileInfo &fileInfo, QStringList &filesToOpen, const QStringList &dependencies);