From 0fc928fd815f27eef77491224bb8922b4e070f2d Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 27 Jun 2019 07:03:40 -0400 Subject: rework of the images tab: - uses a custom widget with a scrollbar instead of a QScrollArea - scrolling by image instead of pixel - refactored all the geometry stuff in ImagesGeometry --- src/modinfodialog.ui | 51 +++--- src/modinfodialogimages.cpp | 393 ++++++++++++++++++++++++++------------------ src/modinfodialogimages.h | 189 ++++++++++++++++----- 3 files changed, 418 insertions(+), 215 deletions(-) diff --git a/src/modinfodialog.ui b/src/modinfodialog.ui index 3ea0a27b..a9af2593 100644 --- a/src/modinfodialog.ui +++ b/src/modinfodialog.ui @@ -172,7 +172,7 @@ false - + 3 @@ -189,20 +189,34 @@ 0 - - - Qt::ScrollBarAlwaysOn - - - - - 0 - 0 - 686 - 436 - + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 - + + + + + + + Qt::Vertical + + + + @@ -1221,16 +1235,15 @@ p, li { white-space: pre-wrap; }
texteditor.h
- ImagesScrollArea - QScrollArea + ImagesThumbnails + QWidget
modinfodialogimages.h
1
- ImagesThumbnails - QWidget + ImagesScrollbar + QScrollBar
modinfodialogimages.h
- 1
diff --git a/src/modinfodialogimages.cpp b/src/modinfodialogimages.cpp index e25fd838..5c93e3d4 100644 --- a/src/modinfodialogimages.cpp +++ b/src/modinfodialogimages.cpp @@ -3,11 +3,36 @@ #include "settings.h" #include "utility.h" +QSize resizeWithAspectRatio(const QSize& original, const QSize& available) +{ + const auto ratio = std::min({ + 1.0, + static_cast(available.width()) / original.width(), + static_cast(available.height()) / original.height()}); + + const QSize scaledSize( + static_cast(std::round(original.width() * ratio)), + static_cast(std::round(original.height() * ratio))); + + return scaledSize; +} + +QRect centeredRect(const QRect& rect, const QSize& size) +{ + return QRect( + (rect.left()+rect.width()/2) - size.width()/2, + (rect.top()+rect.height()/2) - size.height()/2, + size.width(), + size.height()); +} + + ImagesTab::ImagesTab( OrganizerCore& oc, PluginContainer& plugin, QWidget* parent, Ui::ModInfoDialog* ui, int id) : ModInfoDialogTab(oc, plugin, parent, ui, id), - m_image(new ScalableImage), m_margins(3), m_padding(5), m_border(1), + m_image(new ScalableImage), + m_margins(3), m_border(1), m_padding(0), m_spacing(5), m_selection(nullptr), m_ddsAvailable(false), m_ddsEnabled(false) { getSupportedFormats(); @@ -22,10 +47,12 @@ ImagesTab::ImagesTab( ui->tabImagesSplitter->setStretchFactor(0, 0); ui->tabImagesSplitter->setStretchFactor(1, 1); - ui->imagesScrollArea->setWidgetResizable(false); - ui->imagesScrollArea->setTab(this); ui->imagesThumbnails->setTab(this); + ui->imagesScrollerVBar->setTab(this); + ui->imagesScrollerVBar->setSingleStep(1); + connect(ui->imagesScrollerVBar, &QScrollBar::valueChanged, [&]{ onScrolled(); }); + ui->imagesShowDDS->setEnabled(m_ddsAvailable); m_filter.setEdit(ui->imagesFilter); @@ -35,12 +62,16 @@ ImagesTab::ImagesTab( connect(ui->imagesShowDDS, &QCheckBox::toggled, [&]{ onShowDDS(); }); ui->imagesShowDDS->setEnabled(m_ddsAvailable); + + ui->imagesThumbnails->setAutoFillBackground(false); + ui->imagesThumbnails->setAttribute(Qt::WA_OpaquePaintEvent, true); } void ImagesTab::clear() { m_files.clear(); m_filteredFiles.clear(); + ui->imagesScrollerVBar->setValue(0); select(nullptr); setHasData(false); @@ -202,157 +233,98 @@ void ImagesTab::select(const File* f) m_selection = f; } -int ImagesTab::calcThumbSize(int availableWidth) const -{ - return availableWidth - (m_margins * 2); -} - -int ImagesTab::calcWidgetHeight(int availableWidth) const +ImagesGeometry ImagesTab::makeGeometry() const { - if (fileCount() == 0) { - return 0; - } - - const auto thumbSize = calcThumbSize(availableWidth); - - int h = 0; - - // first thumb - h = thumbSize; - - // subsequent thumbs with padding before each - const auto thumbWithPadding = m_padding + thumbSize; - h += static_cast(thumbWithPadding * (fileCount() - 1)); - - // margin top and bottom - h += m_margins * 2; - - return h; -} - -QRect ImagesTab::calcTopThumbRect(int thumbSize) const -{ - return {m_margins, m_margins, thumbSize, thumbSize}; + return ImagesGeometry( + ui->imagesThumbnails->size(), + m_margins, m_border, m_padding, m_spacing); } -std::pair ImagesTab::calcVisibleRange( - int top, int bottom, int thumbSize) const +void ImagesTab::paintThumbnailsArea(QPaintEvent* e) { - const std::size_t begin = top / (thumbSize + m_padding); - const std::size_t end = bottom / (thumbSize + m_padding) + 1; + const auto geo = makeGeometry(); - return {begin, end}; -} + QPainter painter(ui->imagesThumbnails); -QRect ImagesTab::calcBorderRect( - const QRect& topRect, int thumbSize, std::size_t i) const -{ - return { - topRect.left(), - static_cast(topRect.top() + (i * (thumbSize + m_padding))), - thumbSize, - thumbSize - }; -} + painter.fillRect( + ui->imagesThumbnails->rect(), + ui->imagesThumbnails->palette().color(QPalette::Window)); -QRect ImagesTab::calcImageRect( - const QRect& topRect, int thumbSize, std::size_t i) const -{ - return calcBorderRect(topRect, thumbSize, i).adjusted( - m_border, m_border, -m_border, -m_border); -} + const auto visible = geo.fullyVisibleCount() + 1; + const auto first = ui->imagesScrollerVBar->value(); -QSize ImagesTab::calcScaledImageSize( - const QSize& originalSize, const QSize& imageSize) const -{ - const auto ratio = std::min({ - 1.0, - static_cast(imageSize.width()) / originalSize.width(), - static_cast(imageSize.height()) / originalSize.height()}); - - const QSize scaledSize( - static_cast(std::round(originalSize.width() * ratio)), - static_cast(std::round(originalSize.height() * ratio))); - - return scaledSize; -} - -void ImagesTab::paintThumbnails(QPaintEvent* e) -{ - PaintContext cx(ui->imagesThumbnails); - cx.thumbSize = calcThumbSize(ui->imagesThumbnails->width()); - cx.topRect = calcTopThumbRect(cx.thumbSize); - - const auto [begin, end] = calcVisibleRange( - e->rect().top(), e->rect().bottom(), cx.thumbSize); + for (std::size_t i=0; ifailed) { + if (file.failed) { return; } - const auto imageRect = calcImageRect(cx.topRect, cx.thumbSize, i); - - if (needsReload(*file, imageRect.size())) { - reload(*file, imageRect.size()); + if (needsReload(geo, file)) { + reload(geo, file); } - if (file->thumbnail.isNull()) { + if (file.thumbnail.isNull()) { return; } - // center scaled image in rect - const QRect scaledThumbRect( - (imageRect.left()+imageRect.width()/2) - file->thumbnail.width()/2, - (imageRect.top()+imageRect.height()/2) - file->thumbnail.height()/2, - file->thumbnail.width(), - file->thumbnail.height()); + const auto imageRect = geo.imageRect(i); + const auto scaledThumbRect = centeredRect(imageRect, file.thumbnail.size()); - cx.painter.drawImage(scaledThumbRect, file->thumbnail); + painter.drawImage(scaledThumbRect, file.thumbnail); } const ImagesTab::File* ImagesTab::fileAtPos(const QPoint& p) const { - const auto thumbSize = calcThumbSize(ui->imagesThumbnails->width()); + const auto geo = makeGeometry(); - // calculate index purely based on y position - const std::size_t i = p.y() / (thumbSize + m_padding); - if (i >= fileCount()) { + // this is the index relative to the top + const auto offset = geo.indexAt(p); + if (offset == ImagesGeometry::BadIndex) { return nullptr; } - // get actual rect - const auto topRect = calcTopThumbRect(thumbSize); - const auto rect = calcBorderRect(topRect, thumbSize, i); + const auto first = ui->imagesScrollerVBar->value(); + if (first < 0) { + return nullptr; + } - if (!rect.contains(p)) { + const auto i = static_cast(first) + offset; + if (i >= fileCount()) { return nullptr; } @@ -364,7 +336,7 @@ void ImagesTab::scrollAreaResized(const QSize&) resizeWidget(); } -void ImagesTab::thumbnailsMouseEvent(QMouseEvent* e) +void ImagesTab::thumbnailAreaMouseEvent(QMouseEvent* e) { if (e->button() != Qt::LeftButton) { return; @@ -373,6 +345,19 @@ void ImagesTab::thumbnailsMouseEvent(QMouseEvent* e) select(fileAtPos(e->pos())); } +void ImagesTab::thumbnailAreaWheelEvent(QWheelEvent* e) +{ + const auto d = (e->angleDelta() / 8).y(); + + ui->imagesScrollerVBar->setValue( + ui->imagesScrollerVBar->value() + (d > 0 ? -1 : 1)); +} + +void ImagesTab::onScrolled() +{ + ui->imagesThumbnails->update(); +} + void ImagesTab::showTooltip(QHelpEvent* e) { const auto* f = fileAtPos(e->pos()); @@ -410,7 +395,7 @@ void ImagesTab::onFilterChanged() update(); } -bool ImagesTab::needsReload(const File& file, const QSize& imageSize) const +bool ImagesTab::needsReload(const ImagesGeometry& geo, const File& file) const { if (file.failed) { return false; @@ -420,15 +405,11 @@ bool ImagesTab::needsReload(const File& file, const QSize& imageSize) const return true; } - const auto scaledSize = calcScaledImageSize(file.original.size(), imageSize); - if (file.thumbnail.size() != scaledSize) { - return true; - } - - return false; + const auto scaledSize = geo.scaledImageSize(file.original.size()); + return (file.thumbnail.size() != scaledSize); } -void ImagesTab::reload(File& file, const QSize& scaledSize) +void ImagesTab::reload(const ImagesGeometry& geo, File& file) { file.failed = false; @@ -441,56 +422,63 @@ void ImagesTab::reload(File& file, const QSize& scaledSize) } file.thumbnail = file.original.scaled( - calcScaledImageSize(file.original.size(), scaledSize), + geo.scaledImageSize(file.original.size()), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } void ImagesTab::resizeWidget() { if (fileCount() == 0) { - ui->imagesThumbnails->setGeometry(QRect()); + ui->imagesScrollerVBar->setRange(0, 0); + ui->imagesScrollerVBar->setEnabled(false); return; } - const auto availableWidth = ui->imagesScrollArea->viewport()->width(); + const auto geo = makeGeometry(); + const auto availableSize = ui->imagesThumbnails->size(); + const auto fullyVisible = geo.fullyVisibleCount(); - const int widgetHeight = std::max( - calcWidgetHeight(availableWidth), - ui->imagesScrollArea->viewport()->height()); - - ui->imagesThumbnails->setGeometry(QRect(0, 0, availableWidth, widgetHeight)); + if (fullyVisible >= fileCount()) { + ui->imagesScrollerVBar->setRange(0, 0); + ui->imagesScrollerVBar->setEnabled(false); + } else { + const auto d = fileCount() - fullyVisible; + ui->imagesScrollerVBar->setRange(0, static_cast(d)); + ui->imagesScrollerVBar->setEnabled(true); + } } -void ImagesScrollArea::setTab(ImagesTab* tab) +void ImagesThumbnails::setTab(ImagesTab* tab) { m_tab = tab; } -void ImagesScrollArea::resizeEvent(QResizeEvent* e) +void ImagesThumbnails::paintEvent(QPaintEvent* e) { if (m_tab) { - m_tab->scrollAreaResized(e->size()); + m_tab->paintThumbnailsArea(e); } } - -void ImagesThumbnails::setTab(ImagesTab* tab) +void ImagesThumbnails::mousePressEvent(QMouseEvent* e) { - m_tab = tab; + if (m_tab) { + m_tab->thumbnailAreaMouseEvent(e); + } } -void ImagesThumbnails::paintEvent(QPaintEvent* e) +void ImagesThumbnails::wheelEvent(QWheelEvent* e) { if (m_tab) { - m_tab->paintThumbnails(e); + m_tab->thumbnailAreaWheelEvent(e); } } -void ImagesThumbnails::mousePressEvent(QMouseEvent* e) +void ImagesThumbnails::resizeEvent(QResizeEvent* e) { if (m_tab) { - m_tab->thumbnailsMouseEvent(e); + m_tab->scrollAreaResized(e->size()); } } @@ -505,6 +493,19 @@ bool ImagesThumbnails::event(QEvent* e) } +void ImagesScrollbar::setTab(ImagesTab* tab) +{ + m_tab = tab; +} + +void ImagesScrollbar::wheelEvent(QWheelEvent* e) +{ + if (m_tab) { + m_tab->thumbnailAreaWheelEvent(e); + } +} + + ScalableImage::ScalableImage(QString path) : m_path(std::move(path)), m_border(1) { @@ -564,14 +565,8 @@ void ScalableImage::paintEvent(QPaintEvent* e) const QRect imageRect = widgetRect.adjusted( m_border, m_border, -m_border, -m_border); - const auto ratio = std::min({ - 1.0, - static_cast(imageRect.width()) / m_original.width(), - static_cast(imageRect.height()) / m_original.height()}); - - const QSize scaledSize( - static_cast(std::round(m_original.width() * ratio)), - static_cast(std::round(m_original.height() * ratio))); + const QSize scaledSize = resizeWithAspectRatio( + m_original.size(), imageRect.size()); if (m_scaled.isNull() || m_scaled.size() != scaledSize) { m_scaled = m_original.scaled( @@ -580,16 +575,98 @@ void ScalableImage::paintEvent(QPaintEvent* e) } const QRect drawBorderRect = widgetRect.adjusted(0, 0, -1, -1); - - const QRect drawImageRect( - (imageRect.left()+imageRect.width()/2) - m_scaled.width()/2, - (imageRect.top()+imageRect.height()/2) - m_scaled.height()/2, - m_scaled.width(), m_scaled.height()); - + const QRect drawImageRect = centeredRect(imageRect, m_scaled.size()); QPainter painter(this); - painter.setPen(QColor(Qt::black)); painter.drawRect(drawBorderRect); painter.drawImage(drawImageRect, m_scaled); } + + +ImagesGeometry::ImagesGeometry( + const QSize& widgetSize, int margins, int border, int padding, int spacing) : + m_widgetSize(widgetSize), + m_margins(margins), m_padding(padding), m_border(border), + m_spacing(spacing), m_topRect(calcTopRect()) +{ +} + +QRect ImagesGeometry::calcTopRect() const +{ + const auto thumbWidth = m_widgetSize.width() - (m_margins * 2); + const auto imageSize = thumbWidth - (m_border * 2) - (m_padding * 2); + const auto thumbHeight = m_padding + m_border + imageSize + m_border + m_padding; + + return {m_margins, m_margins, thumbWidth, thumbHeight}; +} + +std::size_t ImagesGeometry::fullyVisibleCount() const +{ + const auto r = thumbRect(0); + const auto visible = (m_widgetSize.height() / (r.height() + m_spacing)); + return static_cast(visible); +} + +QRect ImagesGeometry::thumbRect(std::size_t i) const +{ + // rect for the top thumbnail + QRect r = m_topRect; + + // move down + const auto thumbWithSpacing = m_spacing + r.height(); + r.translate(0, static_cast(i * thumbWithSpacing)); + + return r; +} + +QRect ImagesGeometry::borderRect(std::size_t i) const +{ + auto r = thumbRect(i); + + // border rect is currently the same as thumb rect, but may change if + // captions are added + + return r; +} + +QRect ImagesGeometry::imageRect(std::size_t i) const +{ + auto r = borderRect(i); + + // remove border and padding + const auto m = m_border + m_padding; + r.adjust(m, m, -m, -m); + + return r; +} + +std::size_t ImagesGeometry::indexAt(const QPoint& p) const +{ + // calculate index purely based on y position + const std::size_t offset = p.y() / (m_topRect.height() + m_spacing); + + if (!borderRect(offset).contains(p)) { + return BadIndex; + } + + return offset; +} + +QSize ImagesGeometry::scaledImageSize(const QSize& originalSize) const +{ + const auto availableSize = imageRect(0).size(); + return resizeWithAspectRatio(originalSize, availableSize); +} + +void ImagesGeometry::dump() const +{ + qDebug() + << "ImagesTab geometry:\n" + << " . widget size: " << m_widgetSize << "\n" + << " . margins: " << m_margins << "\n" + << " . border: " << m_border << "\n" + << " . padding: " << m_padding << "\n" + << " . spacing: " << m_spacing << "\n" + << " . top rect: " << m_topRect; +} diff --git a/src/modinfodialogimages.h b/src/modinfodialogimages.h index fa7115b0..60ce9def 100644 --- a/src/modinfodialogimages.h +++ b/src/modinfodialogimages.h @@ -3,26 +3,32 @@ #include "modinfodialogtab.h" #include "filterwidget.h" -#include +#include class ImagesTab; -class ImagesScrollArea : public QScrollArea +// vertical scrollbar, this is only to handle wheel events to scroll by one +// instead of the system's scroll setting +// +class ImagesScrollbar : public QScrollBar { - Q_OBJECT; - public: - using QScrollArea::QScrollArea; + using QScrollBar::QScrollBar; void setTab(ImagesTab* tab); protected: - void resizeEvent(QResizeEvent* e) override; + // forwards to ImagesTab::thumbnailAreaWheelEvent() + // + void wheelEvent(QWheelEvent* event) override; private: ImagesTab* m_tab = nullptr; }; +// widget inside the scroller, calls ImagesTab::paintThumbnailArea() when +// needed and also forwards mouse clicks and tooltip events +// class ImagesThumbnails : public QWidget { Q_OBJECT; @@ -32,8 +38,24 @@ public: void setTab(ImagesTab* tab); protected: + // forwards to ImagesTab::paintThumbnailArea() + // void paintEvent(QPaintEvent* e) override; + + // forwards to ImagesTab::thumbnailAreaMouseEvent() + // void mousePressEvent(QMouseEvent* e) override; + + // forwards to ImagesTab::thumbnailAreaWheelEvent() + // + void wheelEvent(QWheelEvent* e); + + // forwards to ImagesTab::scrollAreaResized() + // + void resizeEvent(QResizeEvent* e) override; + + // forwards to ImagesTab::showTooltip for tooltip events + // bool event(QEvent* e) override; private: @@ -41,6 +63,8 @@ private: }; +// a widget that draws an image scaled to fit while keeping the aspect ratio +// class ScalableImage : public QWidget { Q_OBJECT; @@ -48,12 +72,16 @@ class ScalableImage : public QWidget public: ScalableImage(QString path={}); + // sets the image to draw void setImage(const QString& path); void setImage(QImage image); + + // removes the image, won't draw the border nor the image void clear(); - bool hasHeightForWidth() const; - int heightForWidth(int w) const; + // tells the QWidget's layout manager this widget is always square + bool hasHeightForWidth() const override; + int heightForWidth(int w) const override; protected: void paintEvent(QPaintEvent* e) override; @@ -65,10 +93,105 @@ private: }; +// handles all the geometry calculations by ImagesTab for painting or handling +// mouse clicks +// +// a thumbnail looks like this: +// +// +-----------------------+ <-- thumb rect +// | margins | +// | | +// | +-border--------+ <------ border rect +// | | padding | | +// | | | | +// | | +-------+ <----------- image rect +// | | | | | | +// | | | image | | | +// | | | | | | +// | | +-------+ | | +// | | | | +// | +---------------+ | +// | | +// +-----------------------+ +// +// spacing +// +// +-----------------------+ <-- thumb rect +// | margins | +// | | +// .... +// +// +class ImagesGeometry +{ +public: + // returned by indexAt() if the point is outside any possible thumbnail + static constexpr std::size_t BadIndex = + std::numeric_limits::max(); + + ImagesGeometry( + const QSize& widgetSize, int margins, int border, int padding, int spacing); + + // returns the number of images fully visible in the widget + // + std::size_t fullyVisibleCount() const; + + // rectangle around the whole thumbnail + // + QRect thumbRect(std::size_t i) const; + + // rectangle of the border for the given thumbnail + // + QRect borderRect(std::size_t i) const; + + // rectangle of the image for the given thumbnail + // + QRect imageRect(std::size_t i) const; + + // returns the index of the image at the given point; this does not take into + // account any scrolling, the image at the top of widget is always 0 + // + std::size_t indexAt(const QPoint& p) const; + + // returns the size of the image that fits in imageRect() while keeping the + // same aspect ratio as the given one + // + QSize scaledImageSize(const QSize& originalSize) const; + + // dumps stuff to qDebug() + // + void dump() const; + +private: + // size of the widget containing all the thumbnails + const QSize m_widgetSize; + + // space outside the thumbnail border + const int m_margins; + + // size of the border + const int m_border; + + // space between the border and the image + const int m_padding; + + // spacing between thumbnails + const int m_spacing; + + // rectangle of the first thumbnail on top + const QRect m_topRect; + + + // calculates the top rectangle + // + QRect calcTopRect() const; +}; + + class ImagesTab : public ModInfoDialogTab { Q_OBJECT; - friend class ImagesScrollArea; + friend class ImagesScrollbar; friend class ImagesThumbnails; public: @@ -95,23 +218,11 @@ private: } }; - struct PaintContext - { - QPainter painter; - int thumbSize; - QRect topRect; - - PaintContext(QWidget* w) - : painter(w), thumbSize(0) - { - } - }; - ScalableImage* m_image; std::vector m_files; std::vector m_filteredFiles; std::vector m_supportedFormats; - int m_margins, m_padding, m_border; + int m_margins, m_border, m_padding, m_spacing; const File* m_selection; FilterWidget m_filter; bool m_ddsAvailable, m_ddsEnabled; @@ -122,27 +233,29 @@ private: bool needsFiltering() const; void scrollAreaResized(const QSize& s); - void paintThumbnails(QPaintEvent* e); - void thumbnailsMouseEvent(QMouseEvent* e); + void paintThumbnailsArea(QPaintEvent* e); + void thumbnailAreaMouseEvent(QMouseEvent* e); + void thumbnailAreaWheelEvent(QWheelEvent* e); + void onScrolled(); + void showTooltip(QHelpEvent* e); void onExplore(); void onShowDDS(); void onFilterChanged(); - int calcThumbSize(int availableWidth) const; - int calcWidgetHeight(int availableWidth) const; - QRect calcTopThumbRect(int thumbSize) const; - std::pair calcVisibleRange( - int top, int bottom, int thumbSize) const; + ImagesGeometry makeGeometry() const; + + void paintThumbnail( + QPainter& painter, const ImagesGeometry& geo, + File& file, std::size_t i); - QRect calcBorderRect(const QRect& topRect, int thumbSize, std::size_t i) const; - QRect calcImageRect(const QRect& topRect, int thumbSize, std::size_t i) const; - QSize calcScaledImageSize( - const QSize& originalSize, const QSize& imageSize) const; + void paintThumbnailBorder( + QPainter& painter, const ImagesGeometry& geo, + std::size_t i); - void paintThumbnail(PaintContext& cx, std::size_t i); - void paintThumbnailBorder(PaintContext& cx, std::size_t i); - void paintThumbnailImage(PaintContext& cx, std::size_t i); + void paintThumbnailImage( + QPainter& painter, const ImagesGeometry& geo, + File& file, std::size_t i); const File* fileAtPos(const QPoint& p) const; @@ -151,8 +264,8 @@ private: File* getFile(std::size_t i); void filterImages(); - bool needsReload(const File& file, const QSize& imageSize) const; - void reload(File& file, const QSize& imageSize); + bool needsReload(const ImagesGeometry& geo, const File& file) const; + void reload(const ImagesGeometry& geo, File& file); void resizeWidget(); }; -- cgit v1.3.1