File indexing completed on 2024-04-28 04:21:23

0001 // SPDX-FileCopyrightText: 2006-2007 Dirk Mueller <mueller@kde.org>
0002 // SPDX-FileCopyrightText: 2006-2010 Tuomas Suutari <tuomas@nepnep.net>
0003 // SPDX-FileCopyrightText: 2006-2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0004 // SPDX-FileCopyrightText: 2007 Laurent Montel <montel@kde.org>
0005 // SPDX-FileCopyrightText: 2007-2009 Jan Kundrát <jkt@flaska.net>
0006 // SPDX-FileCopyrightText: 2008-2009 Henner Zeller <h.zeller@acm.org>
0007 // SPDX-FileCopyrightText: 2009-2010 Hassan Ibraheem <hasan.ibraheem@gmail.com>
0008 // SPDX-FileCopyrightText: 2010-2012 Miika Turkia <miika.turkia@gmail.com>
0009 // SPDX-FileCopyrightText: 2011 Andreas Neustifter <andreas.neustifter@gmail.com>
0010 // SPDX-FileCopyrightText: 2012-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0011 // SPDX-FileCopyrightText: 2018-2022 Tobias Leupold <tl@stonemx.de>
0012 // SPDX-FileCopyrightText: 2020 Robert Krawitz <rlk@alum.mit.edu>
0013 // SPDX-FileCopyrightText: 2020 Wolfgang Bauer <wbauer@tmo.at>
0014 //
0015 // SPDX-License-Identifier: GPL-2.0-or-later
0016 
0017 #include "ThumbnailWidget.h"
0018 
0019 #include "CellGeometry.h"
0020 #include "Delegate.h"
0021 #include "KeyboardEventHandler.h"
0022 #include "SelectionMaintainer.h"
0023 #include "ThumbnailDND.h"
0024 #include "ThumbnailFactory.h"
0025 #include "ThumbnailModel.h"
0026 
0027 #include <Browser/BrowserWidget.h>
0028 #include <DB/ImageDB.h>
0029 #include <DB/ImageInfoPtr.h>
0030 #include <kpabase/SettingsData.h>
0031 
0032 #include <KColorScheme>
0033 #include <KLocalizedString>
0034 #include <QCursor>
0035 #include <QDebug>
0036 #include <QFontMetrics>
0037 #include <QItemSelection>
0038 #include <QItemSelectionRange>
0039 #include <QPainter>
0040 #include <QScrollBar>
0041 #include <QTimer>
0042 #include <math.h>
0043 
0044 /**
0045  * \class ThumbnailView::ThumbnailWidget
0046  * This is the widget which shows the thumbnails.
0047  *
0048  * In previous versions this was implemented using a QIconView, but there
0049  * simply was too many problems, so after years of tears and pains I
0050  * rewrote it.
0051  */
0052 
0053 ThumbnailView::ThumbnailWidget::ThumbnailWidget(ThumbnailFactory *factory)
0054     : QListView()
0055     , ThumbnailComponent(factory)
0056     , m_isSettingDate(false)
0057     , m_gridResizeInteraction(factory)
0058     , m_wheelResizing(false)
0059     , m_externallyResizing(false)
0060     , m_selectionInteraction(factory)
0061     , m_mouseTrackingHandler(factory)
0062     , m_mouseHandler(&m_mouseTrackingHandler)
0063     , m_dndHandler(new ThumbnailDND(factory))
0064     , m_pressOnStackIndicator(false)
0065     , m_keyboardHandler(new KeyboardEventHandler(factory))
0066     , m_videoThumbnailCycler(new VideoThumbnailCycler(model()))
0067 {
0068     setModel(ThumbnailComponent::model());
0069     setResizeMode(QListView::Adjust);
0070     setViewMode(QListView::IconMode);
0071     setUniformItemSizes(true);
0072     setSelectionMode(QAbstractItemView::ExtendedSelection);
0073 
0074     // It beats me why I need to set mouse tracking on both, but without it doesn't work.
0075     viewport()->setMouseTracking(true);
0076     setMouseTracking(true);
0077 
0078     connect(selectionModel(), &QItemSelectionModel::currentChanged, this, &ThumbnailWidget::scheduleDateChangeSignal);
0079     viewport()->setAcceptDrops(true);
0080 
0081     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
0082     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0083 
0084     connect(&m_mouseTrackingHandler, &MouseTrackingInteraction::fileIdUnderCursorChanged, this, &ThumbnailWidget::fileIdUnderCursorChanged);
0085     connect(m_keyboardHandler, &KeyboardEventHandler::showSelection, this, &ThumbnailWidget::showSelection);
0086     connect(m_keyboardHandler, &KeyboardEventHandler::showSearch, this, &ThumbnailWidget::showSearch);
0087 
0088     updatePalette();
0089     connect(Settings::SettingsData::instance(), &Settings::SettingsData::colorSchemeChanged, this, &ThumbnailWidget::updatePalette);
0090     setItemDelegate(new Delegate(factory, this));
0091 
0092     connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &ThumbnailWidget::emitSelectionChangedSignal);
0093     // a reset of the item model invalidates the internal state, thus also the selection
0094     connect(model(), &QAbstractItemModel::modelReset, this, &ThumbnailWidget::emitSelectionChangedSignal);
0095 
0096     setDragEnabled(false); // We run our own dragging, so disable QListView's version.
0097 
0098     connect(verticalScrollBar(), &QScrollBar::valueChanged, model(), &ThumbnailModel::updateVisibleRowInfo);
0099     setupDateChangeTimer();
0100 }
0101 
0102 bool ThumbnailView::ThumbnailWidget::isGridResizing() const
0103 {
0104     return m_mouseHandler->isResizingGrid() || m_wheelResizing || m_externallyResizing;
0105 }
0106 
0107 void ThumbnailView::ThumbnailWidget::keyPressEvent(QKeyEvent *event)
0108 {
0109     if (!m_keyboardHandler->keyPressEvent(event))
0110         QListView::keyPressEvent(event);
0111 }
0112 
0113 void ThumbnailView::ThumbnailWidget::keyReleaseEvent(QKeyEvent *event)
0114 {
0115     const bool propagate = m_keyboardHandler->keyReleaseEvent(event);
0116     if (propagate)
0117         QListView::keyReleaseEvent(event);
0118 }
0119 
0120 bool ThumbnailView::ThumbnailWidget::isMouseOverStackIndicator(const QPoint &point)
0121 {
0122     // first check if image is stack, if not return.
0123     const DB::ImageInfoPtr imageInfo = DB::ImageDB::instance()->info(mediaIdUnderCursor());
0124     if (!imageInfo)
0125         return false;
0126     if (!imageInfo->isStacked())
0127         return false;
0128 
0129     const QModelIndex index = indexUnderCursor();
0130     const QRect itemRect = visualRect(index);
0131     const QPixmap pixmap = index.data(Qt::DecorationRole).value<QPixmap>();
0132     if (pixmap.isNull())
0133         return false;
0134 
0135     const QRect pixmapRect = cellGeometryInfo()->iconGeometry(pixmap).translated(itemRect.topLeft());
0136     const QRect blackOutRect = pixmapRect.adjusted(0, 0, -10, -10);
0137     return pixmapRect.contains(point) && !blackOutRect.contains(point);
0138 }
0139 
0140 static bool isMouseResizeGesture(QMouseEvent *event)
0141 {
0142     return (event->button() & Qt::MiddleButton) || ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::AltModifier));
0143 }
0144 
0145 void ThumbnailView::ThumbnailWidget::mousePressEvent(QMouseEvent *event)
0146 {
0147     if ((!(event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier))) && isMouseOverStackIndicator(event->pos())) {
0148         model()->toggleStackExpansion(mediaIdUnderCursor());
0149         m_pressOnStackIndicator = true;
0150         return;
0151     }
0152 
0153     if (isMouseResizeGesture(event))
0154         m_mouseHandler = &m_gridResizeInteraction;
0155     else
0156         m_mouseHandler = &m_selectionInteraction;
0157 
0158     if (!m_mouseHandler->mousePressEvent(event))
0159         QListView::mousePressEvent(event);
0160 
0161     if (event->button() & Qt::RightButton) // get out of selection mode if this is a right click
0162         m_mouseHandler = &m_mouseTrackingHandler;
0163 }
0164 
0165 void ThumbnailView::ThumbnailWidget::mouseMoveEvent(QMouseEvent *event)
0166 {
0167     if (m_pressOnStackIndicator)
0168         return;
0169 
0170     if (!m_mouseHandler->mouseMoveEvent(event))
0171         QListView::mouseMoveEvent(event);
0172 }
0173 
0174 void ThumbnailView::ThumbnailWidget::mouseReleaseEvent(QMouseEvent *event)
0175 {
0176     if (m_pressOnStackIndicator) {
0177         m_pressOnStackIndicator = false;
0178         return;
0179     }
0180 
0181     if (!m_mouseHandler->mouseReleaseEvent(event))
0182         QListView::mouseReleaseEvent(event);
0183 
0184     m_mouseHandler = &m_mouseTrackingHandler;
0185 }
0186 
0187 void ThumbnailView::ThumbnailWidget::mouseDoubleClickEvent(QMouseEvent *event)
0188 {
0189     if (isMouseOverStackIndicator(event->pos())) {
0190         model()->toggleStackExpansion(mediaIdUnderCursor());
0191         m_pressOnStackIndicator = true;
0192     } else if (!(event->modifiers() & Qt::ControlModifier)) {
0193         DB::FileName id = mediaIdUnderCursor();
0194         if (!id.isNull())
0195             Q_EMIT showImage(id);
0196     }
0197 }
0198 
0199 void ThumbnailView::ThumbnailWidget::wheelEvent(QWheelEvent *event)
0200 {
0201     if (event->modifiers() & Qt::ControlModifier) {
0202         event->setAccepted(true);
0203         if (!m_wheelResizing)
0204             m_gridResizeInteraction.enterGridResizingMode();
0205 
0206         m_wheelResizing = true;
0207 
0208         model()->beginResetModel();
0209         const int delta = -event->angleDelta().y() / 20;
0210         static int _minimum_ = Settings::SettingsData::instance()->minimumThumbnailSize();
0211         Settings::SettingsData::instance()->setActualThumbnailSize(qMax(_minimum_, Settings::SettingsData::instance()->actualThumbnailSize() + delta));
0212         cellGeometryInfo()->calculateCellSize();
0213         model()->endResetModel();
0214     } else {
0215         const auto angleDelta = event->angleDelta() / 5;
0216         QWheelEvent newevent = QWheelEvent(event->position(), event->globalPosition(), event->pixelDelta(), angleDelta, event->buttons(), event->modifiers(), event->phase(), event->inverted());
0217 
0218         QListView::wheelEvent(&newevent);
0219         event->setAccepted(newevent.isAccepted());
0220     }
0221 }
0222 
0223 void ThumbnailView::ThumbnailWidget::emitDateChange()
0224 {
0225     if (m_isSettingDate)
0226         return;
0227 
0228     int row = currentIndex().row();
0229     if (row == -1)
0230         return;
0231 
0232     DB::FileName fileName = model()->imageAt(row);
0233     if (fileName.isNull())
0234         return;
0235 
0236     static Utilities::FastDateTime lastDate;
0237     const Utilities::FastDateTime date = DB::ImageDB::instance()->info(fileName)->date().start();
0238     if (date != lastDate) {
0239         lastDate = date;
0240         if (date.date().year() != 1900)
0241             Q_EMIT currentDateChanged(date);
0242     }
0243 }
0244 
0245 /**
0246  * scroll to the date specified with the parameter date.
0247  * The boolean includeRanges tells whether we accept range matches or not.
0248  */
0249 void ThumbnailView::ThumbnailWidget::gotoDate(const DB::ImageDate &date, bool includeRanges)
0250 {
0251     m_isSettingDate = true;
0252     DB::FileName candidate = DB::ImageDB::instance()
0253                                  ->findFirstItemInRange(model()->imageList(ViewOrder), date, includeRanges);
0254     if (!candidate.isNull())
0255         setCurrentItem(candidate);
0256 
0257     m_isSettingDate = false;
0258 }
0259 
0260 void ThumbnailView::ThumbnailWidget::setExternallyResizing(bool state)
0261 {
0262     m_externallyResizing = state;
0263 }
0264 
0265 void ThumbnailView::ThumbnailWidget::reload(SelectionUpdateMethod method)
0266 {
0267     SelectionMaintainer maintainer(this, model());
0268     ThumbnailComponent::model()->beginResetModel();
0269     cellGeometryInfo()->flushCache();
0270     updatePalette();
0271     ThumbnailComponent::model()->endResetModel();
0272 
0273     if (method == ClearSelection)
0274         maintainer.disable();
0275 }
0276 
0277 DB::FileName ThumbnailView::ThumbnailWidget::mediaIdUnderCursor() const
0278 {
0279     const QModelIndex index = indexUnderCursor();
0280     if (index.isValid())
0281         return model()->imageAt(index.row());
0282     else
0283         return DB::FileName();
0284 }
0285 
0286 QModelIndex ThumbnailView::ThumbnailWidget::indexUnderCursor() const
0287 {
0288     return indexAt(mapFromGlobal(QCursor::pos()));
0289 }
0290 
0291 void ThumbnailView::ThumbnailWidget::dragMoveEvent(QDragMoveEvent *event)
0292 {
0293     m_dndHandler->contentsDragMoveEvent(event);
0294 }
0295 
0296 void ThumbnailView::ThumbnailWidget::dragLeaveEvent(QDragLeaveEvent *event)
0297 {
0298     m_dndHandler->contentsDragLeaveEvent(event);
0299 }
0300 
0301 void ThumbnailView::ThumbnailWidget::dropEvent(QDropEvent *event)
0302 {
0303     m_dndHandler->contentsDropEvent(event);
0304 }
0305 
0306 void ThumbnailView::ThumbnailWidget::dragEnterEvent(QDragEnterEvent *event)
0307 {
0308     m_dndHandler->contentsDragEnterEvent(event);
0309 }
0310 
0311 void ThumbnailView::ThumbnailWidget::setCurrentItem(const DB::FileName &fileName)
0312 {
0313     if (fileName.isNull())
0314         return;
0315 
0316     const int row = model()->indexOf(fileName);
0317     setCurrentIndex(QListView::model()->index(row, 0));
0318 }
0319 
0320 DB::FileName ThumbnailView::ThumbnailWidget::currentItem() const
0321 {
0322     if (!currentIndex().isValid())
0323         return DB::FileName();
0324 
0325     return model()->imageAt(currentIndex().row());
0326 }
0327 
0328 void ThumbnailView::ThumbnailWidget::updatePalette()
0329 {
0330     QPalette pal = palette();
0331     // if the scheme was set at startup from the scheme path (and not afterwards through KColorSchemeManager),
0332     // then KColorScheme would use the standard system scheme if we don't explicitly give a config:
0333     const auto schemeCfg = KSharedConfig::openConfig(Settings::SettingsData::instance()->colorScheme());
0334     KColorScheme::adjustBackground(pal, KColorScheme::NormalBackground, QPalette::Base, KColorScheme::Complementary, schemeCfg);
0335     KColorScheme::adjustForeground(pal, KColorScheme::NormalText, QPalette::Text, KColorScheme::Complementary, schemeCfg);
0336     setPalette(pal);
0337 }
0338 
0339 int ThumbnailView::ThumbnailWidget::cellWidth() const
0340 {
0341     return visualRect(QListView::model()->index(0, 0)).size().width();
0342 }
0343 
0344 void ThumbnailView::ThumbnailWidget::emitSelectionChangedSignal()
0345 {
0346     Q_EMIT selectionCountChanged(selection(ExpandCollapsedStacks).size());
0347 }
0348 
0349 void ThumbnailView::ThumbnailWidget::scheduleDateChangeSignal()
0350 {
0351     m_dateChangedTimer->start(200);
0352 }
0353 
0354 /**
0355  * During profiling, I found that emitting the dateChanged signal was
0356  * rather expensive, so now I delay that signal, so it is only emitted 200
0357  * msec after the scroll, which means it will not be emitted when the user
0358  * holds down, say the page down key for scrolling.
0359  */
0360 void ThumbnailView::ThumbnailWidget::setupDateChangeTimer()
0361 {
0362     m_dateChangedTimer = new QTimer(this);
0363     m_dateChangedTimer->setSingleShot(true);
0364     connect(m_dateChangedTimer, &QTimer::timeout, this, &ThumbnailWidget::emitDateChange);
0365 }
0366 
0367 void ThumbnailView::ThumbnailWidget::showEvent(QShowEvent *event)
0368 {
0369     model()->updateVisibleRowInfo();
0370     QListView::showEvent(event);
0371 }
0372 
0373 DB::FileNameList ThumbnailView::ThumbnailWidget::selection(ThumbnailView::SelectionMode mode) const
0374 {
0375     DB::FileNameList res;
0376     auto indexSelection = selectedIndexes();
0377     // selectedIndexes() is not sorted:
0378     std::sort(indexSelection.begin(), indexSelection.end());
0379     for (const QModelIndex &index : indexSelection) {
0380         const DB::FileName currFileName = model()->imageAt(index.row());
0381         bool includeAllStacks = false;
0382         switch (mode) {
0383         case IncludeAllStacks:
0384             includeAllStacks = true;
0385             /* FALLTHROUGH */
0386         case ExpandCollapsedStacks: {
0387             // if the selected image belongs to a collapsed thread,
0388             // imply that all images in the stack are selected:
0389             const DB::ImageInfoPtr imageInfo = DB::ImageDB::instance()->info(currFileName);
0390             if (imageInfo && imageInfo->isStacked()
0391                 && (includeAllStacks || !model()->isItemInExpandedStack(imageInfo->stackId()))) {
0392                 // add all images in the same stack
0393                 res.append(DB::ImageDB::instance()->getStackFor(currFileName));
0394             } else
0395                 res.append(currFileName);
0396         } break;
0397         case NoExpandCollapsedStacks:
0398             res.append(currFileName);
0399             break;
0400         }
0401     }
0402     return res;
0403 }
0404 
0405 bool ThumbnailView::ThumbnailWidget::isSelected(const DB::FileName &fileName) const
0406 {
0407     return selection(NoExpandCollapsedStacks).indexOf(fileName) != -1;
0408 }
0409 
0410 /**
0411    This very specific method will make the item specified by id selected,
0412    if there only are one item selected. This is used from the Viewer when
0413    you start it without a selection, and are going forward or backward.
0414 */
0415 void ThumbnailView::ThumbnailWidget::changeSingleSelection(const DB::FileName &fileName)
0416 {
0417     if (selection(NoExpandCollapsedStacks).size() == 1) {
0418         QItemSelectionModel *selection = selectionModel();
0419         selection->select(model()->fileNameToIndex(fileName), QItemSelectionModel::ClearAndSelect);
0420         setCurrentItem(fileName);
0421     }
0422 }
0423 
0424 void ThumbnailView::ThumbnailWidget::select(const DB::FileNameList &items)
0425 {
0426     QItemSelection selection;
0427     QModelIndex start;
0428     QModelIndex end;
0429     int count = 0;
0430     for (const DB::FileName &fileName : items) {
0431         QModelIndex index = model()->fileNameToIndex(fileName);
0432         if (count == 0) {
0433             start = index;
0434             end = index;
0435         } else if (index.row() == end.row() + 1) {
0436             end = index;
0437         } else {
0438             selection.merge(QItemSelection(start, end), QItemSelectionModel::Select);
0439             start = index;
0440             end = index;
0441         }
0442         count++;
0443     }
0444     if (count > 0) {
0445         selection.merge(QItemSelection(start, end), QItemSelectionModel::Select);
0446     }
0447     selectionModel()->select(selection, QItemSelectionModel::Select);
0448 }
0449 
0450 bool ThumbnailView::ThumbnailWidget::isItemUnderCursorSelected() const
0451 {
0452     return widget()->selection(ExpandCollapsedStacks).contains(mediaIdUnderCursor());
0453 }
0454 
0455 // vi:expandtab:tabstop=4 shiftwidth=4:
0456 
0457 #include "moc_ThumbnailWidget.cpp"