You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp

544 lines
22 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <GemCatalog/GemItemDelegate.h>
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <AdjustableHeaderWidget.h>
#include <ProjectManagerDefs.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <QEvent>
#include <QAbstractItemView>
#include <QPainter>
#include <QMouseEvent>
#include <QHelpEvent>
#include <QToolTip>
#include <QHoverEvent>
#include <QTextDocument>
#include <QAbstractTextDocumentLayout>
#include <QDesktopServices>
#include <QMovie>
#include <QHeaderView>
#include <QDir>
namespace O3DE::ProjectManager
{
GemItemDelegate::GemItemDelegate(QAbstractItemModel* model, AdjustableHeaderWidget* header, QObject* parent)
: QStyledItemDelegate(parent)
, m_model(model)
, m_headerWidget(header)
{
AddPlatformIcon(GemInfo::Android, ":/Android.svg");
AddPlatformIcon(GemInfo::iOS, ":/iOS.svg");
AddPlatformIcon(GemInfo::Linux, ":/Linux.svg");
AddPlatformIcon(GemInfo::macOS, ":/macOS.svg");
AddPlatformIcon(GemInfo::Windows, ":/Windows.svg");
SetStatusIcon(m_notDownloadedPixmap, ":/Download.svg");
SetStatusIcon(m_unknownStatusPixmap, ":/X.svg");
SetStatusIcon(m_downloadSuccessfulPixmap, ":/checkmark.svg");
SetStatusIcon(m_downloadFailedPixmap, ":/Warning.svg");
m_downloadingMovie = new QMovie(":/in_progress.gif");
}
void GemItemDelegate::AddPlatformIcon(GemInfo::Platform platform, const QString& iconPath)
{
QPixmap pixmap(iconPath);
qreal aspectRatio = static_cast<qreal>(pixmap.width()) / pixmap.height();
m_platformIcons.insert(platform, QIcon(iconPath).pixmap(static_cast<int>(static_cast<qreal>(s_platformIconSize) * aspectRatio), s_platformIconSize));
}
void GemItemDelegate::SetStatusIcon(QPixmap& m_iconPixmap, const QString& iconPath)
{
QPixmap pixmap(iconPath);
float aspectRatio = static_cast<float>(pixmap.width()) / pixmap.height();
int xScaler = s_statusIconSize;
int yScaler = s_statusIconSize;
if (aspectRatio > 1.0f)
{
yScaler = static_cast<int>(1.0f / aspectRatio * s_statusIconSize);
}
else if (aspectRatio < 1.0f)
{
xScaler = static_cast<int>(aspectRatio * s_statusIconSize);
}
m_iconPixmap = QPixmap(QIcon(iconPath).pixmap(xScaler, yScaler));
}
void GemItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const
{
if (!modelIndex.isValid())
{
return;
}
QStyleOptionViewItem options(option);
initStyleOption(&options, modelIndex);
painter->setRenderHint(QPainter::Antialiasing);
QRect fullRect, itemRect, contentRect;
CalcRects(options, fullRect, itemRect, contentRect);
QRect buttonRect = CalcButtonRect(contentRect);
QFont standardFont(options.font);
standardFont.setPixelSize(static_cast<int>(s_fontSize));
QFontMetrics standardFontMetrics(standardFont);
painter->save();
painter->setClipping(true);
painter->setClipRect(fullRect);
painter->setFont(options.font);
// Draw background
painter->fillRect(fullRect, m_backgroundColor);
// Draw item background
const QColor itemBackgroundColor = options.state & QStyle::State_MouseOver ? m_itemBackgroundColor.lighter(120) : m_itemBackgroundColor;
painter->fillRect(itemRect, itemBackgroundColor);
// Draw border
if (options.state & QStyle::State_Selected)
{
painter->save();
QPen borderPen(m_borderColor);
borderPen.setWidth(s_borderWidth);
painter->setPen(borderPen);
painter->drawRect(itemRect);
painter->restore();
}
// Gem preview
QString previewPath = QDir(GemModel::GetPath(modelIndex)).filePath(ProjectPreviewImagePath);
QPixmap gemPreviewImage(previewPath);
QRect gemPreviewRect(
contentRect.left() + AdjustableHeaderWidget::s_headerTextIndent,
contentRect.center().y() - GemPreviewImageHeight / 2,
GemPreviewImageWidth, GemPreviewImageHeight);
painter->drawPixmap(gemPreviewRect, gemPreviewImage);
// Gem name
QString gemName = GemModel::GetDisplayName(modelIndex);
QFont gemNameFont(options.font);
QPair<int, int> nameXBounds = CalcColumnXBounds(HeaderOrder::Name);
const int nameStartX = nameXBounds.first;
const int nameColumnTextStartX = s_itemMargins.left() + nameStartX + AdjustableHeaderWidget::s_headerTextIndent;
const int nameColumnMaxTextWidth = nameXBounds.second - nameStartX - AdjustableHeaderWidget::s_headerTextIndent;
gemNameFont.setPixelSize(static_cast<int>(s_gemNameFontSize));
gemNameFont.setBold(true);
gemName = QFontMetrics(gemNameFont).elidedText(gemName, Qt::TextElideMode::ElideRight, nameColumnMaxTextWidth);
QRect gemNameRect = GetTextRect(gemNameFont, gemName, s_gemNameFontSize);
gemNameRect.moveTo(nameColumnTextStartX, contentRect.top());
painter->setFont(gemNameFont);
painter->setPen(m_textColor);
gemNameRect = painter->boundingRect(gemNameRect, Qt::TextSingleLine, gemName);
painter->drawText(gemNameRect, Qt::TextSingleLine, gemName);
// Gem creator
QString gemCreator = GemModel::GetCreator(modelIndex);
gemCreator = standardFontMetrics.elidedText(gemCreator, Qt::TextElideMode::ElideRight, nameColumnMaxTextWidth);
QRect gemCreatorRect = GetTextRect(standardFont, gemCreator, s_fontSize);
gemCreatorRect.moveTo(nameColumnTextStartX, contentRect.top() + gemNameRect.height());
painter->setFont(standardFont);
gemCreatorRect = painter->boundingRect(gemCreatorRect, Qt::TextSingleLine, gemCreator);
painter->drawText(gemCreatorRect, Qt::TextSingleLine, gemCreator);
// Gem summary
const QStringList featureTags = GemModel::GetFeatures(modelIndex);
const bool hasTags = !featureTags.isEmpty();
const QString summary = GemModel::GetSummary(modelIndex);
const QRect summaryRect = CalcSummaryRect(contentRect, hasTags);
DrawText(summary, painter, summaryRect, standardFont);
DrawDownloadStatusIcon(painter, contentRect, buttonRect, modelIndex);
DrawButton(painter, buttonRect, modelIndex);
DrawPlatformIcons(painter, contentRect, modelIndex);
DrawFeatureTags(painter, contentRect, featureTags, standardFont, summaryRect);
painter->restore();
}
QRect GemItemDelegate::CalcSummaryRect(const QRect& contentRect, bool hasTags) const
{
const int featureTagAreaHeight = 40;
const int summaryHeight = contentRect.height() - (hasTags * featureTagAreaHeight);
const auto [summaryStartX, summaryEndX] = CalcColumnXBounds(HeaderOrder::Summary);
const QSize summarySize =
QSize(summaryEndX - summaryStartX - AdjustableHeaderWidget::s_headerTextIndent - s_extraSummarySpacing,
summaryHeight);
return QRect(
QPoint(s_itemMargins.left() + summaryStartX + AdjustableHeaderWidget::s_headerTextIndent, contentRect.top()), summarySize);
}
QSize GemItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const
{
QStyleOptionViewItem options(option);
initStyleOption(&options, modelIndex);
int marginsHorizontal = s_itemMargins.left() + s_itemMargins.right() + s_contentMargins.left() + s_contentMargins.right();
return QSize(marginsHorizontal + s_buttonWidth + s_defaultSummaryStartX, s_height);
}
bool GemItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex)
{
if (!modelIndex.isValid())
{
return false;
}
if (event->type() == QEvent::KeyPress)
{
auto keyEvent = static_cast<const QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Space)
{
const bool isAdded = GemModel::IsAdded(modelIndex);
GemModel::SetIsAdded(*model, modelIndex, !isAdded);
return true;
}
}
else if (event->type() == QEvent::MouseButtonPress)
{
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
QRect fullRect, itemRect, contentRect;
CalcRects(option, fullRect, itemRect, contentRect);
const QRect buttonRect = CalcButtonRect(contentRect);
if (buttonRect.contains(mouseEvent->pos()))
{
const bool isAdded = GemModel::IsAdded(modelIndex);
GemModel::SetIsAdded(*model, modelIndex, !isAdded);
return true;
}
// we must manually handle html links because we aren't using QLabels
const QStringList featureTags = GemModel::GetFeatures(modelIndex);
const bool hasTags = !featureTags.isEmpty();
const QRect summaryRect = CalcSummaryRect(contentRect, hasTags);
if (summaryRect.contains(mouseEvent->pos()))
{
const QString html = GemModel::GetSummary(modelIndex);
QString anchor = anchorAt(html, mouseEvent->pos(), summaryRect);
if (!anchor.isEmpty())
{
QDesktopServices::openUrl(QUrl(anchor));
return true;
}
}
}
return QStyledItemDelegate::editorEvent(event, model, option, modelIndex);
}
QString GetGemNameList(const QVector<QModelIndex> modelIndices)
{
QString gemNameList;
for (int i = 0; i < modelIndices.size(); ++i)
{
if (!gemNameList.isEmpty())
{
if (i == modelIndices.size() - 1)
{
gemNameList.append(" and ");
}
else
{
gemNameList.append(", ");
}
}
gemNameList.append(GemModel::GetDisplayName(modelIndices[i]));
}
return gemNameList;
}
bool GemItemDelegate::helpEvent(QHelpEvent* event, QAbstractItemView* view, const QStyleOptionViewItem& option, const QModelIndex& index)
{
if (event->type() == QEvent::ToolTip)
{
QRect fullRect, itemRect, contentRect;
CalcRects(option, fullRect, itemRect, contentRect);
const QRect buttonRect = CalcButtonRect(contentRect);
if (buttonRect.contains(event->pos()))
{
if (!QToolTip::isVisible())
{
if(GemModel::IsAddedDependency(index) && !GemModel::IsAdded(index))
{
const GemModel* gemModel = GemModel::GetSourceModel(index.model());
AZ_Assert(gemModel, "Failed to obtain GemModel");
// we only want to display the gems that must be de-selected to automatically
// disable this dependency, so don't include any that haven't been selected (added)
constexpr bool addedOnly = true;
QVector<QModelIndex> dependents = gemModel->GatherDependentGems(index, addedOnly);
QString nameList = GetGemNameList(dependents);
if (!nameList.isEmpty())
{
QToolTip::showText(event->globalPos(), tr("This gem is a dependency of %1.\nTo disable this gem, first disable %1.").arg(nameList));
}
}
}
return true;
}
else if (QToolTip::isVisible())
{
QToolTip::hideText();
event->ignore();
return true;
}
}
return QStyledItemDelegate::helpEvent(event, view, option, index);
}
void GemItemDelegate::CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const
{
outFullRect = QRect(option.rect);
outItemRect = QRect(outFullRect.adjusted(s_itemMargins.left(), s_itemMargins.top(), -s_itemMargins.right(), -s_itemMargins.bottom()));
outContentRect = QRect(outItemRect.adjusted(s_contentMargins.left(), s_contentMargins.top(), -s_contentMargins.right(), -s_contentMargins.bottom()));
}
QRect GemItemDelegate::GetTextRect(QFont& font, const QString& text, qreal fontSize) const
{
font.setPixelSize(static_cast<int>(fontSize));
return QFontMetrics(font).boundingRect(text);
}
QPair<int, int> GemItemDelegate::CalcColumnXBounds(HeaderOrder header) const
{
return m_headerWidget->CalcColumnXBounds(static_cast<int>(header));
}
QRect GemItemDelegate::CalcButtonRect(const QRect& contentRect) const
{
const QPoint topLeft = QPoint(
s_itemMargins.left() + CalcColumnXBounds(HeaderOrder::Status).first + AdjustableHeaderWidget::s_headerTextIndent + s_statusIconSize +
s_statusButtonSpacing,
contentRect.center().y() - s_buttonHeight / 2);
const QSize size = QSize(s_buttonWidth, s_buttonHeight);
return QRect(topLeft, size);
}
void GemItemDelegate::DrawPlatformIcons(QPainter* painter, const QRect& contentRect, const QModelIndex& modelIndex) const
{
const GemInfo::Platforms platforms = GemModel::GetPlatforms(modelIndex);
int startX = s_itemMargins.left() + CalcColumnXBounds(HeaderOrder::Name).first + AdjustableHeaderWidget::s_headerTextIndent;
// Iterate and draw the platforms in the order they are defined in the enum.
for (int i = 0; i < GemInfo::NumPlatforms; ++i)
{
// Check if the platform is supported by the given gem.
const GemInfo::Platform platform = static_cast<GemInfo::Platform>(1 << i);
if (platforms & platform)
{
// Get the icon for the platform and draw it.
const auto iterator = m_platformIcons.find(platform);
if (iterator != m_platformIcons.end())
{
const QPixmap& pixmap = iterator.value();
painter->drawPixmap(contentRect.left() + startX, contentRect.bottom() - s_platformIconSize, pixmap);
qreal aspectRatio = static_cast<qreal>(pixmap.width()) / pixmap.height();
startX += static_cast<int>(s_platformIconSize * aspectRatio + s_platformIconSize / 2.5);
}
}
}
}
void GemItemDelegate::DrawFeatureTags(
QPainter* painter,
const QRect& contentRect,
const QStringList& featureTags,
const QFont& standardFont,
const QRect& summaryRect) const
{
QFont gemFeatureTagFont(standardFont);
gemFeatureTagFont.setPixelSize(s_featureTagFontSize);
gemFeatureTagFont.setBold(false);
painter->setFont(gemFeatureTagFont);
int x = CalcColumnXBounds(HeaderOrder::Summary).first + AdjustableHeaderWidget::s_headerTextIndent;
for (const QString& featureTag : featureTags)
{
QRect featureTagRect = GetTextRect(gemFeatureTagFont, featureTag, s_featureTagFontSize);
featureTagRect.moveTo(s_itemMargins.left() + x + s_featureTagBorderMarginX,
contentRect.top() + 47);
featureTagRect = painter->boundingRect(featureTagRect, Qt::TextSingleLine, featureTag);
QRect backgroundRect = featureTagRect;
backgroundRect = backgroundRect.adjusted(/*left=*/-s_featureTagBorderMarginX,
/*top=*/-s_featureTagBorderMarginY,
/*right=*/s_featureTagBorderMarginX,
/*bottom=*/s_featureTagBorderMarginY);
// Skip drawing all following feature tags as there is no more space available.
if (backgroundRect.right() > summaryRect.right())
{
break;
}
// Draw border.
painter->setPen(m_textColor);
painter->setBrush(Qt::NoBrush);
painter->drawRect(backgroundRect);
// Draw text within the border.
painter->setPen(m_textColor);
painter->drawText(featureTagRect, Qt::TextSingleLine, featureTag);
x += backgroundRect.width() + s_featureTagSpacing;
}
}
AZStd::unique_ptr<QTextDocument> GetTextDocument(const QString& text, int width)
{
// using unique_ptr as a workaround for QTextDocument having a private copy constructor
auto doc = AZStd::make_unique<QTextDocument>();
QTextOption textOption(doc->defaultTextOption());
textOption.setWrapMode(QTextOption::WordWrap);
doc->setDefaultTextOption(textOption);
doc->setHtml(text);
doc->setTextWidth(width);
return doc;
}
void GemItemDelegate::DrawText(const QString& text, QPainter* painter, const QRect& rect, const QFont& standardFont) const
{
painter->save();
if (text.contains('<'))
{
painter->translate(rect.topLeft());
// use QTextDocument because drawText does not support rich text or html
QAbstractTextDocumentLayout::PaintContext paintContext;
paintContext.clip = QRect(0, 0, rect.width(), rect.height());
paintContext.palette.setColor(QPalette::Text, painter->pen().color());
AZStd::unique_ptr<QTextDocument> textDocument = GetTextDocument(text, rect.width());
textDocument->documentLayout()->draw(painter, paintContext);
}
else
{
painter->setFont(standardFont);
painter->setPen(m_textColor);
painter->drawText(rect, Qt::AlignLeft | Qt::TextWordWrap, text);
}
painter->restore();
}
void GemItemDelegate::DrawButton(QPainter* painter, const QRect& buttonRect, const QModelIndex& modelIndex) const
{
painter->save();
QPoint circleCenter;
if (GemModel::IsAdded(modelIndex))
{
painter->setBrush(m_buttonEnabledColor);
painter->setPen(m_buttonEnabledColor);
circleCenter = buttonRect.center() + QPoint(buttonRect.width() / 2 - s_buttonBorderRadius + 1, 1);
}
else if (GemModel::IsAddedDependency(modelIndex))
{
painter->setBrush(m_buttonImplicitlyEnabledColor);
painter->setPen(m_buttonImplicitlyEnabledColor);
circleCenter = buttonRect.center() + QPoint(buttonRect.width() / 2 - s_buttonBorderRadius + 1, 1);
}
else
{
circleCenter = buttonRect.center() + QPoint(-buttonRect.width() / 2 + s_buttonBorderRadius + 1, 1);
}
// Rounded rect
painter->drawRoundedRect(buttonRect, s_buttonBorderRadius, s_buttonBorderRadius);
// Circle
painter->setBrush(m_textColor);
painter->drawEllipse(circleCenter, s_buttonCircleRadius, s_buttonCircleRadius);
painter->restore();
}
QString GemItemDelegate::anchorAt(const QString& html, const QPoint& position, const QRect& rect)
{
if (!html.isEmpty())
{
AZStd::unique_ptr<QTextDocument> doc = GetTextDocument(html, rect.width());
QAbstractTextDocumentLayout* layout = doc->documentLayout();
if (layout)
{
return layout->anchorAt(position - rect.topLeft());
}
}
return QString();
}
void GemItemDelegate::DrawDownloadStatusIcon(QPainter* painter, const QRect& contentRect, const QRect& buttonRect, const QModelIndex& modelIndex) const
{
const GemInfo::DownloadStatus downloadStatus = GemModel::GetDownloadStatus(modelIndex);
// Show no icon if gem is already downloaded
if (downloadStatus == GemInfo::DownloadStatus::Downloaded)
{
return;
}
QPixmap currentFrame;
const QPixmap* statusPixmap;
if (downloadStatus == GemInfo::DownloadStatus::Downloading)
{
if (m_downloadingMovie->state() != QMovie::Running)
{
m_downloadingMovie->start();
emit MovieStartedPlaying(m_downloadingMovie);
}
currentFrame = m_downloadingMovie->currentPixmap();
currentFrame = currentFrame.scaled(s_statusIconSize, s_statusIconSize);
statusPixmap = &currentFrame;
}
else if (downloadStatus == GemInfo::DownloadStatus::DownloadSuccessful)
{
statusPixmap = &m_downloadSuccessfulPixmap;
}
else if (downloadStatus == GemInfo::DownloadStatus::DownloadFailed)
{
statusPixmap = &m_downloadFailedPixmap;
}
else if (downloadStatus == GemInfo::DownloadStatus::NotDownloaded)
{
statusPixmap = &m_notDownloadedPixmap;
}
else
{
statusPixmap = &m_unknownStatusPixmap;
}
QSize statusSize = statusPixmap->size();
painter->drawPixmap(
buttonRect.left() - s_statusButtonSpacing - statusSize.width(),
contentRect.center().y() - statusSize.height() / 2,
*statusPixmap);
}
} // namespace O3DE::ProjectManager