Files
o3de/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/AssetFolderThumbnailView.cpp
T
2021-06-23 10:55:22 -07:00

800 lines
28 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzQtComponents/Components/Widgets/AssetFolderThumbnailView.h>
#include <AzQtComponents/Components/Style.h>
AZ_PUSH_DISABLE_WARNING(4244 4251 4800, "-Wunknown-warning-option") // 4244: 'initializing': conversion from 'int' to 'float', possible loss of data
// 4251: 'QInputEvent::modState': class 'QFlags<Qt::KeyboardModifier>' needs to have dll-interface to be used by clients of class 'QInputEvent'
// 4800: 'QFlags<QPainter::RenderHint>::Int': forcing value to bool 'true' or 'false' (performance warning)
#include <QAbstractItemDelegate>
#include <QMouseEvent>
#include <QPainter>
#include <QScrollBar>
#include <QSettings>
AZ_POP_DISABLE_WARNING
namespace
{
QString elidedTextWithExtension(const QFontMetrics& fm, const QString& text, int width)
{
if (fm.horizontalAdvance(text) <= width)
return text;
const int dot = text.lastIndexOf(QLatin1Char{'.'});
if (dot != -1)
{
const auto baseName = text.left(dot);
const auto extension = text.mid(dot + 1);
return fm.elidedText(baseName, Qt::ElideRight, width - fm.horizontalAdvance(extension)) + extension;
}
else
{
return fm.elidedText(text, Qt::ElideRight, width);
}
}
}
namespace AzQtComponents
{
static void paintExpandButton(QPainter* painter, const QRect& rect, bool closed, const AssetFolderThumbnailView::Config::ExpandButton& config)
{
// rectangle
const auto buttonRect = rect.adjusted(2, 2, -2, -2);
painter->setPen(Qt::NoPen);
painter->setBrush(config.backgroundColor);
painter->drawRoundedRect(buttonRect, config.borderRadius, config.borderRadius);
// caret
const auto caretWidth = config.caretWidth;
const auto center = QRectF{buttonRect}.center();
const auto caretDirection = closed ? 1.0 : -1.0;
QPolygonF caret;
caret.append(center + QPointF{-.5f * caretWidth * caretDirection, -caretWidth});
caret.append(center + QPointF{-.5f * caretWidth * caretDirection, caretWidth});
caret.append(center + QPointF{.5f * caretWidth * caretDirection, 0});
painter->setBrush(config.caretColor);
painter->drawConvexPolygon(caret);
}
class AssetFolderThumbnailViewDelegate : public QAbstractItemDelegate
{
public:
explicit AssetFolderThumbnailViewDelegate(QObject* parent = nullptr);
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void polish(const AssetFolderThumbnailView::Config& config);
private:
AssetFolderThumbnailView::Config m_config;
};
AssetFolderThumbnailViewDelegate::AssetFolderThumbnailViewDelegate(QObject* parent)
: QAbstractItemDelegate(parent)
{
}
void AssetFolderThumbnailViewDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
const auto isTopLevel = index.parent().isValid() == false;
painter->save();
painter->setFont(option.font);
const auto& config = isTopLevel ? m_config.rootThumbnail : m_config.childThumbnail;
const auto borderRadius = config.borderRadius;
const auto padding = config.padding;
const auto& rect = option.rect;
const auto thumbnailRect = QRect{rect.left(), rect.top(), rect.width(), rect.width()};
const auto imageRect = thumbnailRect.adjusted(padding, padding, -padding, -padding);
// border
const auto halfBorderThickness = qMax(config.borderThickness, config.selectedBorderThickness) / 2;
const auto borderRect = QRectF{thumbnailRect}.adjusted(halfBorderThickness, halfBorderThickness, -halfBorderThickness, -halfBorderThickness);
QPen borderPen;
if (option.state & QStyle::State_Selected)
{
borderPen = {config.selectedBorderColor, config.selectedBorderThickness};
}
else
{
borderPen = {config.borderColor, config.borderThickness};
}
painter->setBrush(config.backgroundColor);
painter->setPen(borderPen);
painter->drawRoundedRect(borderRect, borderRadius, borderRadius);
// pixmap
const auto& iconVariant = index.data(Qt::DecorationRole);
if (!iconVariant.isNull())
{
const auto& icon = iconVariant.value<QIcon>();
icon.paint(painter, imageRect);
}
if (isTopLevel)
{
// expand button
if ((option.state & QStyle::State_UpArrow) || (option.state & QStyle::State_DownArrow))
{
const auto& buttonConfig = m_config.expandButton;
const auto width = buttonConfig.width;
const auto buttonRect = QRect{rect.left() + rect.width() - width, rect.top(), width, rect.width()};
paintExpandButton(painter, buttonRect, (option.state & QStyle::State_DownArrow) != 0, buttonConfig);
}
// text
const auto textHeight = option.fontMetrics.height();
const auto textRect = QRect{rect.left(), rect.bottom() - textHeight, rect.width(), textHeight};
painter->setPen(option.palette.color(QPalette::Text));
painter->drawText(textRect, elidedTextWithExtension(option.fontMetrics, index.data().toString(), textRect.width()), QTextOption{option.decorationAlignment});
}
painter->restore();
}
QSize AssetFolderThumbnailViewDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
Q_UNUSED(index);
return option.rect.size();
}
void AssetFolderThumbnailViewDelegate::polish(const AssetFolderThumbnailView::Config& config)
{
m_config = config;
}
static void readColor(QSettings& settings, const QString& name, QColor& color)
{
// only overwrite the value if it's set; otherwise, it'll stay the default
if (settings.contains(name))
{
color = QColor(settings.value(name).toString());
}
}
static void readThumbnail(QSettings& settings, AssetFolderThumbnailView::Config::Thumbnail& thumbnail)
{
thumbnail.smallSize = settings.value(QStringLiteral("SmallSize"), thumbnail.smallSize).toInt();
thumbnail.mediumSize = settings.value(QStringLiteral("MediumSize"), thumbnail.mediumSize).toInt();
thumbnail.largeSize = settings.value(QStringLiteral("LargeSize"), thumbnail.largeSize).toInt();
thumbnail.borderRadius = settings.value(QStringLiteral("BorderRadius"), thumbnail.borderRadius).toReal();
thumbnail.padding = settings.value(QStringLiteral("Padding"), thumbnail.padding).toInt();
readColor(settings, "BackgroundColor", thumbnail.backgroundColor);
thumbnail.borderThickness = settings.value(QStringLiteral("BorderThickness"), thumbnail.borderThickness).toReal();
thumbnail.selectedBorderThickness = settings.value(QStringLiteral("SelectedBorderThickness"), thumbnail.selectedBorderThickness).toReal();
readColor(settings, "BorderColor", thumbnail.borderColor);
readColor(settings, "SelectedBorderColor", thumbnail.selectedBorderColor);
}
static void readExpandButton(QSettings& settings, AssetFolderThumbnailView::Config::ExpandButton& expandButton)
{
expandButton.width = settings.value(QStringLiteral("Width"), expandButton.width).toInt();
expandButton.caretWidth = settings.value(QStringLiteral("CaretWidth"), expandButton.caretWidth).toReal();
expandButton.borderRadius = settings.value(QStringLiteral("BorderRadius"), expandButton.borderRadius).toReal();
readColor(settings, "BackgroundColor", expandButton.backgroundColor);
readColor(settings, "CaretColor", expandButton.caretColor);
}
static void readChildFrame(QSettings& settings, AssetFolderThumbnailView::Config::ChildFrame& childFrame)
{
childFrame.padding = settings.value(QStringLiteral("Padding"), childFrame.padding).toInt();
childFrame.borderRadius = settings.value(QStringLiteral("BorderRadius"), childFrame.borderRadius).toReal();
readColor(settings, "BackgroundColor", childFrame.backgroundColor);
}
AssetFolderThumbnailView::Config AssetFolderThumbnailView::loadConfig(QSettings& settings)
{
auto config = defaultConfig();
config.margin = settings.value(QStringLiteral("Margin"), config.margin).toInt();
settings.beginGroup(QStringLiteral("RootThumbnail"));
readThumbnail(settings, config.rootThumbnail);
settings.endGroup();
settings.beginGroup(QStringLiteral("ChildThumbnail"));
readThumbnail(settings, config.childThumbnail);
settings.endGroup();
settings.beginGroup(QStringLiteral("ExpandButton"));
readExpandButton(settings, config.expandButton);
settings.endGroup();
settings.beginGroup(QStringLiteral("ChildFrame"));
readChildFrame(settings, config.childFrame);
settings.endGroup();
return config;
}
AssetFolderThumbnailView::Config AssetFolderThumbnailView::defaultConfig()
{
Config config;
config.margin = 8;
config.rootThumbnail.smallSize = 72;
config.rootThumbnail.mediumSize = 84;
config.rootThumbnail.largeSize = 96;
config.rootThumbnail.borderRadius = 8.0;
config.rootThumbnail.padding = 12;
config.rootThumbnail.backgroundColor = QColor(0x44, 0x44, 0x44);
config.rootThumbnail.borderThickness = 1.0;
config.rootThumbnail.selectedBorderThickness = 2.0;
config.rootThumbnail.borderColor = QColor(0x00, 0x00, 0x00);
config.rootThumbnail.selectedBorderColor = QColor(0xff, 0xff, 0xff);
config.childThumbnail.smallSize = 39;
config.childThumbnail.mediumSize = 48;
config.childThumbnail.largeSize = 54;
config.childThumbnail.borderRadius = 2.4;
config.childThumbnail.padding = 7;
config.childThumbnail.backgroundColor = QColor(0x44, 0x44, 0x44);
config.childThumbnail.borderThickness = 1.0;
config.childThumbnail.selectedBorderThickness = 2.0;
config.childThumbnail.borderColor = QColor(0x38, 0x38, 0x38);
config.childThumbnail.selectedBorderColor = QColor(0xff, 0xff, 0xff);
config.expandButton.width = 18;
config.expandButton.caretWidth = 8.0;
config.expandButton.borderRadius = 4.0;
config.expandButton.backgroundColor = QColor(0x55, 0x55, 0x55);
config.expandButton.caretColor = QColor(0xff, 0xff, 0xff);
config.childFrame.padding = 18;
config.childFrame.borderRadius = 4.0;
config.childFrame.backgroundColor = QColor(0x00, 0x00, 0x00);
return config;
}
bool AssetFolderThumbnailView::polish(Style* style, QWidget* widget, const ScrollBar::Config& scrollBarConfig, const Config& config)
{
auto thumbnailView = qobject_cast<AssetFolderThumbnailView*>(widget);
if (!thumbnailView)
{
return false;
}
ScrollBar::polish(style, thumbnailView, scrollBarConfig);
thumbnailView->polish(config);
style->repolishOnSettingsChange(thumbnailView);
return true;
}
void AssetFolderThumbnailView::polish(const Config& config)
{
m_config = config;
m_delegate->polish(config);
}
AssetFolderThumbnailView::AssetFolderThumbnailView(QWidget* parent)
: QAbstractItemView(parent)
, m_delegate(new AssetFolderThumbnailViewDelegate(this))
, m_thumbnailSize(ThumbnailSize::Small)
, m_config(defaultConfig())
{
setItemDelegate(m_delegate);
}
AssetFolderThumbnailView::~AssetFolderThumbnailView() = default;
void AssetFolderThumbnailView::setThumbnailSize(ThumbnailSize size)
{
if (size == m_thumbnailSize)
{
return;
}
m_thumbnailSize = size;
scheduleDelayedItemsLayout();
}
AssetFolderThumbnailView::ThumbnailSize AssetFolderThumbnailView::thumbnailSize() const
{
return m_thumbnailSize;
}
QModelIndex AssetFolderThumbnailView::indexAt(const QPoint& point) const
{
if (!model())
{
return {};
}
const auto p = point + QPoint{horizontalOffset(), verticalOffset()};
const auto it = std::find_if(m_itemGeometry.constBegin(), m_itemGeometry.constEnd(), [&p](const QRect& rect) {
return rect.contains(p);
});
if (it != m_itemGeometry.end())
{
return it.key();
}
return {};
}
void AssetFolderThumbnailView::scrollTo(const QModelIndex& index, QAbstractItemView::ScrollHint hint)
{
if (!index.isValid())
{
return;
}
auto it = m_itemGeometry.find(index);
if (it == m_itemGeometry.end())
{
return;
}
const auto& rect = it.value();
switch (hint)
{
case EnsureVisible:
if (horizontalOffset() > rect.right())
{
horizontalScrollBar()->setValue(rect.left());
}
else if ((horizontalOffset() + viewport()->width()) < rect.left())
{
horizontalScrollBar()->setValue(rect.right() - viewport()->width());
}
if (verticalOffset() > rect.bottom())
{
verticalScrollBar()->setValue(rect.top());
}
else if ((verticalOffset() + viewport()->height()) < rect.top())
{
verticalScrollBar()->setValue(rect.bottom() - viewport()->height());
}
break;
case PositionAtTop:
horizontalScrollBar()->setValue(rect.left());
verticalScrollBar()->setValue(rect.top());
break;
case PositionAtBottom:
horizontalScrollBar()->setValue(rect.right() - viewport()->width());
verticalScrollBar()->setValue(rect.bottom() - viewport()->height());
break;
case PositionAtCenter:
horizontalScrollBar()->setValue(rect.center().x() - (viewport()->width() / 2));
verticalScrollBar()->setValue(rect.center().y() - (viewport()->height() / 2));
break;
}
}
QRect AssetFolderThumbnailView::visualRect(const QModelIndex& index) const
{
if (!index.isValid())
{
return {};
}
const auto it = m_itemGeometry.find(index);
if (it == m_itemGeometry.end())
{
return {};
}
const auto& rect = it.value();
return rect.translated(-horizontalOffset(), -verticalOffset());
}
QModelIndex AssetFolderThumbnailView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
{
Q_UNUSED(modifiers);
if (!model())
{
return {};
}
const auto rowCount = model()->rowCount();
if (rowCount == 0)
{
return {};
}
switch (cursorAction)
{
case MoveHome:
return model()->index(0, 0, rootIndex());
case MoveEnd:
return model()->index(rowCount - 1, 0, rootIndex());
case MovePrevious:
{
const auto current = currentIndex();
if (current.isValid())
{
return model()->index((current.row() - 1) % rowCount, 0, rootIndex());
}
}
break;
case MoveNext:
{
const auto current = currentIndex();
if (current.isValid())
{
return model()->index((current.row() + 1) % rowCount, 0, rootIndex());
}
}
break;
case MoveUp:
case MoveDown:
case MoveLeft:
case MoveRight:
case MovePageUp:
case MovePageDown:
// TODO
break;
}
return {};
}
int AssetFolderThumbnailView::horizontalOffset() const
{
return horizontalScrollBar()->value();
}
int AssetFolderThumbnailView::verticalOffset() const
{
return verticalScrollBar()->value();
}
bool AssetFolderThumbnailView::isIndexHidden(const QModelIndex&) const
{
return false;
}
void AssetFolderThumbnailView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags flags)
{
if (!model())
{
return;
}
const auto translatedRect = rect.translated(horizontalOffset(), verticalOffset());
for (auto it = m_itemGeometry.constBegin(); it != m_itemGeometry.constEnd(); ++it)
{
if (it.value().intersects(translatedRect))
{
selectionModel()->select(it.key(), flags);
}
}
}
QRegion AssetFolderThumbnailView::visualRegionForSelection(const QItemSelection& selection) const
{
QRegion region;
for (const auto& index : selection.indexes())
{
region += visualRect(index);
}
return region;
}
bool AssetFolderThumbnailView::isExpandable(const QModelIndex& index) const
{
return !index.parent().isValid() && model()->rowCount(index) > 0;
}
void AssetFolderThumbnailView::paintEvent(QPaintEvent* event)
{
QAbstractItemView::paintEvent(event);
const auto rowCount = model()->rowCount();
if (m_itemGeometry.isEmpty() && rowCount)
{
updateGeometries();
}
QPainter painter{viewport()};
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
painter.setBackground(palette().window());
painter.setFont(font());
painter.translate(-horizontalOffset(), -verticalOffset());
paintChildFrames(&painter);
paintItems(&painter);
}
void AssetFolderThumbnailView::paintChildFrames(QPainter* painter) const
{
for (const auto& childFrame : m_childFrames)
{
const auto& index = childFrame.index;
const auto childCount = model()->rowCount(index);
const auto title = tr("%1 Assets (%2)").arg(index.data().toString()).arg(childCount);
bool firstFrame = true;
for (const auto& rect : childFrame.rects)
{
// draw frame
const auto borderRadius = m_config.childFrame.borderRadius;
painter->setPen(Qt::NoPen);
painter->setBrush(m_config.childFrame.backgroundColor);
painter->drawRoundedRect(rect, borderRadius, borderRadius);
if (firstFrame)
{
// draw title
const auto textHeight = fontMetrics().height();
const auto textWidth = rect.width() - 2 * m_config.childFrame.padding;
const auto textRect = QRect{rect.left() + m_config.childFrame.padding, rect.top(), textWidth, textHeight};
painter->setPen(palette().color(QPalette::Text));
painter->drawText(textRect, fontMetrics().elidedText(title, Qt::ElideRight, textRect.width()));
firstFrame = false;
}
}
// draw expand button
const auto& lastRect = childFrame.rects.last();
const auto& buttonConfig = m_config.expandButton;
const auto width = buttonConfig.width;
const auto buttonRect = QRect{lastRect.left() + lastRect.width() - width, lastRect.top(), width, lastRect.height()};
paintExpandButton(painter, buttonRect, false, buttonConfig);
}
}
void AssetFolderThumbnailView::paintItems(QPainter* painter) const
{
QStyleOptionViewItem option;
option.palette = palette();
option.font = font();
option.fontMetrics = fontMetrics();
option.decorationAlignment = Qt::AlignCenter;
const QRect visibleRect{QPoint{horizontalOffset(), verticalOffset()}, viewport()->contentsRect().size()};
for (auto it = m_itemGeometry.constBegin(); it != m_itemGeometry.constEnd(); ++it)
{
const auto& index = it.key();
option.rect = it.value();
if (!option.rect.intersects(visibleRect))
{
continue;
}
option.state = QStyle::State_None;
if (selectionModel()->isSelected(index))
{
option.state |= QStyle::State_Selected;
}
if (isExpandable(index))
{
if (m_expandedRows.contains(index.row()))
{
option.state |= QStyle::State_UpArrow;
}
else
{
option.state |= QStyle::State_DownArrow;
}
}
itemDelegate(index)->paint(painter, option, index);
}
}
void AssetFolderThumbnailView::mousePressEvent(QMouseEvent* event)
{
const auto p = event->pos() + QPoint{horizontalOffset(), verticalOffset()};
// check the expand/collapse buttons on one of the top level items was clicked
{
auto it = std::find_if(m_itemGeometry.keyBegin(), m_itemGeometry.keyEnd(), [this, &p](const QModelIndex& index) {
if (isExpandable(index))
{
const auto& rect = m_itemGeometry.value(index);
const auto width = m_config.expandButton.width;
const auto buttonRect = QRect{rect.left() + rect.width() - width, rect.top(), width, rect.width()};
return buttonRect.contains(p);
}
return false;
});
if (it != m_itemGeometry.keyEnd())
{
const auto row = it->row();
if (m_expandedRows.contains(row))
{
m_expandedRows.remove(row);
}
else
{
m_expandedRows.insert(row);
}
scheduleDelayedItemsLayout();
return;
}
}
// check the collapse button on one of the child frames was clicked
{
auto it = std::find_if(m_childFrames.begin(), m_childFrames.end(), [this, &p](const ChildFrame& childFrame) {
const auto& rect = childFrame.rects.last();
const auto width = m_config.expandButton.width;
const auto buttonRect = QRect{rect.left() + rect.width() - width, rect.top(), width, rect.width()};
return buttonRect.contains(p);
});
if (it != m_childFrames.end())
{
const auto row = it->index.row();
if (m_expandedRows.contains(row))
{
m_expandedRows.remove(row);
scheduleDelayedItemsLayout();
return;
}
}
}
QAbstractItemView::mousePressEvent(event);
}
int AssetFolderThumbnailView::rootThumbnailSizeInPixels() const
{
switch (m_thumbnailSize)
{
case ThumbnailSize::Small:
return m_config.rootThumbnail.smallSize;
case ThumbnailSize::Medium:
return m_config.rootThumbnail.mediumSize;
case ThumbnailSize::Large:
return m_config.rootThumbnail.largeSize;
}
Q_UNREACHABLE();
}
int AssetFolderThumbnailView::childThumbnailSizeInPixels() const
{
switch (m_thumbnailSize)
{
case ThumbnailSize::Small:
return m_config.childThumbnail.smallSize;
case ThumbnailSize::Medium:
return m_config.childThumbnail.mediumSize;
case ThumbnailSize::Large:
return m_config.childThumbnail.largeSize;
}
Q_UNREACHABLE();
}
void AssetFolderThumbnailView::updateGeometries()
{
m_itemGeometry.clear();
m_childFrames.clear();
if (!model())
{
return;
}
const auto rowCount = model()->rowCount();
if (rowCount == 0)
{
return;
}
const auto rootThumbnailSize = rootThumbnailSizeInPixels();
const QSize itemSize{rootThumbnailSize, rootThumbnailSize + 4 + fontMetrics().height()};
const int viewportWidth = viewport()->width() - m_config.margin;
const int rowHeight = itemSize.height() + m_config.margin;
const auto childThumbnailSize = childThumbnailSizeInPixels();
const QSize childItemSize{childThumbnailSize, childThumbnailSize};
const int childItemYOffset = (rootThumbnailSize - childThumbnailSize) / 2;
int x = m_config.margin;
int y = m_config.margin;
for (int row = 0; row < rowCount; ++row)
{
if (row > 0 && x + itemSize.width() > viewportWidth)
{
x = m_config.margin;
y += rowHeight;
}
// add item geometry
const auto index = model()->index(row, 0, rootIndex());
m_itemGeometry[index] = {QPoint{x, y}, itemSize};
x += itemSize.width() + m_config.margin;
// add child items and frames if item is expanded
const auto childRowCount = model()->rowCount(index);
if (childRowCount && m_expandedRows.contains(row))
{
ChildFrame childFrame{index};
auto& childFrameRects = childFrame.rects;
const auto addChildFrame = [this, &childFrameRects, childItemYOffset, childThumbnailSize](int frameLeft, int frameRight, int yRow) {
const auto top = yRow + childItemYOffset - m_config.childFrame.padding;
const auto width = frameRight - frameLeft + m_config.childFrame.padding;
const auto height = childThumbnailSize + 2 * m_config.childFrame.padding;
childFrameRects.append({frameLeft, top, width, height});
};
auto frameLeft = x;
x += m_config.childFrame.padding;
for (int childRow = 0; childRow < childRowCount; ++childRow)
{
if (x + childItemSize.width() + m_config.childFrame.padding > viewportWidth)
{
if (childRow > 0)
{
addChildFrame(frameLeft, x - m_config.margin, y);
}
x = m_config.margin + m_config.childFrame.padding;
y += rowHeight;
frameLeft = m_config.margin;
}
const auto childIndex = model()->index(childRow, 0, index);
m_itemGeometry[childIndex] = {QPoint{x, y + childItemYOffset}, childItemSize};
x += childItemSize.width() + m_config.margin;
}
addChildFrame(frameLeft, x - m_config.margin, y);
m_childFrames.append(childFrame);
x += m_config.childFrame.padding;
}
}
verticalScrollBar()->setPageStep(viewport()->height());
verticalScrollBar()->setRange(0, y + rowHeight - viewport()->height());
}
}
#include "Components/Widgets/moc_AssetFolderThumbnailView.cpp"