diff --git a/CMakeLists.txt b/CMakeLists.txt index 1026ab0..4cf8380 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,9 @@ qt_add_executable(SagoImageBrowser src/exifreader.cpp src/exifreader.h src/pathcompletermodel.cpp - src/pathcompletermodel.h) + src/pathcompletermodel.h + src/thumbnaildelegate.cpp + src/thumbnaildelegate.h) target_include_directories(SagoImageBrowser PRIVATE ${EXIV2_INCLUDE_DIRS}) target_link_libraries(SagoImageBrowser PRIVATE Qt6::Widgets ${EXIV2_LIBRARIES}) \ No newline at end of file diff --git a/src/imagemodel.cpp b/src/imagemodel.cpp index 93cc8d4..27f2401 100644 --- a/src/imagemodel.cpp +++ b/src/imagemodel.cpp @@ -285,6 +285,9 @@ QVariant ImageModel::data(const QModelIndex &index, int role) const if (role == Qt::DisplayRole) return item.displayName; + if (role == Qt::UserRole) + return item.isFolder; + return {}; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 97b6eb5..0fd7ed4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -6,6 +6,7 @@ #include "exifreader.h" #include "fsdirmodel.h" #include "pathcompletermodel.h" +#include "thumbnaildelegate.h" #include #include @@ -104,6 +105,7 @@ void MainWindow::setupUi() m_listView->setSpacing(4); m_listView->setWordWrap(true); m_listView->setTextElideMode(Qt::ElideRight); + m_listView->setItemDelegate(new ThumbnailDelegate(this)); // Preview pane (right) m_previewLabel = new QLabel; diff --git a/src/thumbnaildelegate.cpp b/src/thumbnaildelegate.cpp new file mode 100644 index 0000000..6867107 --- /dev/null +++ b/src/thumbnaildelegate.cpp @@ -0,0 +1,90 @@ +#include "thumbnaildelegate.h" + +#include +#include +#include +#include +#include + +ThumbnailDelegate::ThumbnailDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ +} + +void ThumbnailDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + painter->save(); + + // Folder items use the default delegate rendering unchanged + if (index.data(Qt::UserRole).toBool()) { + painter->restore(); + QStyledItemDelegate::paint(painter, option, index); + return; + } + + // Draw selection / hover background + QStyle *style = option.widget ? option.widget->style() : QApplication::style(); + QStyleOptionViewItem bgOption = option; + bgOption.showDecorationSelected = true; + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &bgOption, painter, option.widget); + + const QRect cellRect = option.rect; + + // Fixed icon zone: top IconZoneHeight pixels of the cell + const QRect iconZone(cellRect.left(), cellRect.top(), + cellRect.width(), IconZoneHeight); + + // Fixed text zone: everything below the icon zone + const QRect textZone(cellRect.left(), cellRect.top() + IconZoneHeight, + cellRect.width(), cellRect.height() - IconZoneHeight); + + // --- Draw icon centered within the icon zone --- + const QVariant decorData = index.data(Qt::DecorationRole); + QPixmap pixmap; + if (decorData.userType() == QMetaType::QPixmap) { + pixmap = qvariant_cast(decorData); + } else if (decorData.userType() == QMetaType::QIcon) { + pixmap = qvariant_cast(decorData).pixmap(option.decorationSize); + } + if (!pixmap.isNull()) { + // Center the pixmap in the icon zone + const int x = iconZone.left() + (iconZone.width() - pixmap.width()) / 2; + const int y = iconZone.top() + (iconZone.height() - pixmap.height()) / 2; + painter->drawPixmap(x, y, pixmap); + } + + // --- Draw text top-aligned within the text zone --- + const QString text = index.data(Qt::DisplayRole).toString(); + if (!text.isEmpty()) { + QColor textColor = option.palette.color( + (option.state & QStyle::State_Selected) ? QPalette::HighlightedText + : QPalette::Text); + painter->setPen(textColor); + painter->setFont(option.font); + + QTextOption textOpt; + textOpt.setAlignment(Qt::AlignTop | Qt::AlignHCenter); + textOpt.setWrapMode(QTextOption::WordWrap); + + // Elide text that would overflow the text zone + const QFontMetrics fm(option.font); + const QString elidedText = fm.elidedText( + text, option.textElideMode, textZone.width() * 2); + + painter->drawText(QRectF(textZone), elidedText, textOpt); + } + + painter->restore(); +} + +QSize ThumbnailDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + // Return the grid size set on the view (148×168) + return option.rect.size().isValid() ? option.rect.size() + : QStyledItemDelegate::sizeHint(option, index); +} diff --git a/src/thumbnaildelegate.h b/src/thumbnaildelegate.h new file mode 100644 index 0000000..44aae83 --- /dev/null +++ b/src/thumbnaildelegate.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +class ThumbnailDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + static constexpr int IconZoneHeight = 128; + + explicit ThumbnailDelegate(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; +};