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"