File indexing completed on 2024-04-28 15:39:06
0001 // SPDX-FileCopyrightText: 2020-2024 Tobias Leupold <tl at stonemx dot de> 0002 // 0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 0005 // Local includes 0006 #include "ImagesListView.h" 0007 #include "SharedObjects.h" 0008 #include "ImagesModel.h" 0009 #include "Settings.h" 0010 #include "ImagesListFilter.h" 0011 0012 // KDE includes 0013 #include <KLocalizedString> 0014 0015 // Qt includes 0016 #include <QMouseEvent> 0017 #include <QApplication> 0018 #include <QMimeData> 0019 #include <QDrag> 0020 #include <QDebug> 0021 #include <QKeyEvent> 0022 #include <QMenu> 0023 #include <QDesktopServices> 0024 #include <QClipboard> 0025 #include <QRegularExpression> 0026 0027 // C++ includes 0028 #include <algorithm> 0029 #include <functional> 0030 0031 ImagesListView::ImagesListView(KGeoTag::ImagesListType type, SharedObjects *sharedObjects, 0032 QWidget *parent) 0033 : QListView(parent), 0034 m_listType(type), 0035 m_bookmarks(sharedObjects->bookmarks()) 0036 { 0037 viewport()->setAcceptDrops(true); 0038 setDropIndicatorShown(true); 0039 setDragDropMode(QAbstractItemView::DropOnly); 0040 0041 m_listFilter = new ImagesListFilter(this, type); 0042 m_listFilter->setSourceModel(sharedObjects->imagesModel()); 0043 setModel(m_listFilter); 0044 connect(m_listFilter, &ImagesListFilter::requestAddingImages, 0045 this, &ImagesListView::requestAddingImages); 0046 connect(m_listFilter, &ImagesListFilter::requestRemoveCoordinates, 0047 this, QOverload<const QVector<QString> &>::of(&ImagesListView::removeCoordinates)); 0048 0049 setSelectionMode(QAbstractItemView::ExtendedSelection); 0050 setContextMenuPolicy(Qt::CustomContextMenu); 0051 const int iconSize = sharedObjects->settings()->thumbnailSize(); 0052 setIconSize(QSize(iconSize, iconSize)); 0053 0054 connect(this, &QAbstractItemView::clicked, this, &ImagesListView::centerImage); 0055 connect(this, &QAbstractItemView::clicked, this, &ImagesListView::imageSelected); 0056 0057 // Context menu 0058 0059 m_contextMenu = new QMenu(this); 0060 0061 m_selectAll = m_contextMenu->addAction(i18n("Select all images")); 0062 m_selectAll->setIcon(QIcon::fromTheme(QStringLiteral("select"))); 0063 connect(m_selectAll, &QAction::triggered, this, &QListView::selectAll); 0064 0065 m_selectMenu = m_contextMenu->addMenu(i18n("Select")); 0066 m_selectMenu->menuAction()->setIcon(QIcon::fromTheme(QStringLiteral("select"))); 0067 auto *all = m_selectMenu->addAction(i18n("All images")); 0068 connect(all, &QAction::triggered, this, &QListView::selectAll); 0069 auto *without = m_selectMenu->addAction(i18n("All images without coordinates")); 0070 connect(without, &QAction::triggered, 0071 this, std::bind(&ImagesListView::selectImages, this, false)); 0072 auto *with = m_selectMenu->addAction(i18n("All images with coordinates")); 0073 connect(with, &QAction::triggered, 0074 this, std::bind(&ImagesListView::selectImages, this, true)); 0075 0076 m_contextMenu->addSeparator(); 0077 0078 m_automaticMatchingMenu = m_contextMenu->addMenu(i18n("Automatic matching")); 0079 m_automaticMatchingMenu->menuAction()->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); 0080 0081 auto *combinedMatchSearchAction = m_automaticMatchingMenu->addAction( 0082 i18n("Combined match search")); 0083 connect(combinedMatchSearchAction, &QAction::triggered, 0084 this, std::bind(&ImagesListView::requestAutomaticMatching, this, 0085 this, KGeoTag::CombinedMatchSearch)); 0086 0087 m_automaticMatchingMenu->addSeparator(); 0088 0089 auto *searchExactMatchesAction = m_automaticMatchingMenu->addAction( 0090 i18n("Search exact matches only")); 0091 connect(searchExactMatchesAction, &QAction::triggered, 0092 this, std::bind(&ImagesListView::requestAutomaticMatching, this, 0093 this, KGeoTag::ExactMatchSearch)); 0094 0095 auto *searchInterpolatedMatchesAction = m_automaticMatchingMenu->addAction( 0096 i18n("Search interpolated matches only")); 0097 connect(searchInterpolatedMatchesAction, &QAction::triggered, 0098 this, std::bind(&ImagesListView::requestAutomaticMatching, this, 0099 this, KGeoTag::InterpolatedMatchSearch)); 0100 0101 m_bookmarksMenu = m_contextMenu->addMenu(i18n("Assign to bookmark")); 0102 m_bookmarksMenu->menuAction()->setIcon(QIcon::fromTheme(QStringLiteral("bookmarks"))); 0103 updateBookmarks(); 0104 0105 m_contextMenu->addSeparator(); 0106 0107 m_assignToMapCenter = m_contextMenu->addAction(i18n("Assign to map center")); 0108 m_assignToMapCenter->setIcon(QIcon::fromTheme(QStringLiteral("crosshairs"))); 0109 connect(m_assignToMapCenter, &QAction::triggered, 0110 this, std::bind(&ImagesListView::assignToMapCenter, this, this)); 0111 0112 m_assignToClipboard = m_contextMenu->addAction(i18n("Set coordinates from clipboard")); 0113 connect(m_assignToClipboard, &QAction::triggered, this, &ImagesListView::assignToClipboard); 0114 0115 m_assignManually = m_contextMenu->addAction(i18n("Set coordinates manually")); 0116 m_assignManually->setIcon(QIcon::fromTheme(QStringLiteral("add-placemark"))); 0117 connect(m_assignManually, &QAction::triggered, 0118 this, std::bind(&ImagesListView::assignManually, this, this)); 0119 0120 m_contextMenu->addSeparator(); 0121 0122 m_findClosestTrackPoint = m_contextMenu->addAction(i18n("Find closest trackpoint")); 0123 m_findClosestTrackPoint->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); 0124 connect(m_findClosestTrackPoint, &QAction::triggered, 0125 this, [this] 0126 { 0127 Q_EMIT findClosestTrackPoint(currentIndex().data(KGeoTag::PathRole).toString()); 0128 }); 0129 0130 m_editCoordinates = m_contextMenu->addAction(i18n("Edit coordinates")); 0131 connect(m_editCoordinates, &QAction::triggered, 0132 this, std::bind(&ImagesListView::editCoordinates, this, this)); 0133 0134 m_lookupElevation = m_contextMenu->addAction(i18n("Lookup elevation")); 0135 m_lookupElevation->setIcon(QIcon::fromTheme(QStringLiteral("adjustcurves"))); 0136 connect(m_lookupElevation, &QAction::triggered, 0137 this, std::bind(&ImagesListView::lookupElevation, this, this)); 0138 0139 m_contextMenu->addSeparator(); 0140 0141 m_save = m_contextMenu->addAction(i18n("Save changes")); 0142 m_save->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); 0143 connect(m_save, &QAction::triggered, 0144 this, std::bind(&ImagesListView::requestSaving, this, this)); 0145 0146 m_contextMenu->addSeparator(); 0147 0148 m_removeCoordinates = m_contextMenu->addAction(i18n("Remove coordinates")); 0149 connect(m_removeCoordinates, &QAction::triggered, 0150 this, std::bind(QOverload<ImagesListView *>::of(&ImagesListView::removeCoordinates), 0151 this, this)); 0152 0153 m_contextMenu->addSeparator(); 0154 0155 m_discardChanges = m_contextMenu->addAction(i18n("Discard changes")); 0156 m_discardChanges->setIcon(QIcon::fromTheme(QStringLiteral("dialog-cancel"))); 0157 connect(m_discardChanges, &QAction::triggered, 0158 this, std::bind(&ImagesListView::discardChanges, this, this)); 0159 0160 m_removeImages = m_contextMenu->addAction(i18np("Remove image", "Remove images", 1)); 0161 m_removeImages->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); 0162 connect(m_removeImages, &QAction::triggered, 0163 this, std::bind(&ImagesListView::removeImages, this, this)); 0164 0165 m_contextMenu->addSeparator(); 0166 0167 m_openExternally = m_contextMenu->addAction(i18n("Open with default viewer")); 0168 m_openExternally->setIcon(QIcon::fromTheme(QStringLiteral("file-zoom-in"))); 0169 connect(m_openExternally, &QAction::triggered, this, &ImagesListView::openExternally); 0170 0171 connect(this, &QListView::customContextMenuRequested, this, &ImagesListView::showContextMenu); 0172 } 0173 0174 void ImagesListView::setListType(KGeoTag::ImagesListType type) 0175 { 0176 m_listType = type; 0177 m_listFilter->setListType(type); 0178 m_selectAll->setVisible(type != KGeoTag::AllImages); 0179 m_selectMenu->menuAction()->setVisible(type == KGeoTag::AllImages); 0180 } 0181 0182 void ImagesListView::currentChanged(const QModelIndex ¤t, const QModelIndex &) 0183 { 0184 if (current.isValid()) { 0185 Q_EMIT imageSelected(current); 0186 scrollTo(current); 0187 } 0188 } 0189 0190 void ImagesListView::updateBookmarks() 0191 { 0192 m_bookmarksMenu->clear(); 0193 auto bookmarks = m_bookmarks->keys(); 0194 0195 if (bookmarks.count() == 0) { 0196 m_bookmarksMenu->addAction(i18n("(No bookmarks defined)")); 0197 return; 0198 } 0199 0200 std::sort(bookmarks.begin(), bookmarks.end()); 0201 for (const auto &label : std::as_const(bookmarks)) { 0202 auto *entry = m_bookmarksMenu->addAction(label); 0203 entry->setData(label); 0204 connect(entry, &QAction::triggered, 0205 this, std::bind([this](QAction *action) 0206 { 0207 Q_EMIT assignTo(selectedPaths(), m_bookmarks->value(action->data().toString())); 0208 }, 0209 entry)); 0210 } 0211 } 0212 0213 void ImagesListView::mousePressEvent(QMouseEvent *event) 0214 { 0215 const auto pos = event->pos(); 0216 const auto selectedIndex = indexAt(pos); 0217 if (event->button() == Qt::LeftButton && selectedIndex.isValid()) { 0218 m_dragStarted = true; 0219 m_dragStartPosition = event->pos(); 0220 } else { 0221 m_dragStarted = false; 0222 } 0223 0224 QListView::mousePressEvent(event); 0225 } 0226 0227 void ImagesListView::mouseMoveEvent(QMouseEvent *event) 0228 { 0229 // Enable selecting more images by dragging when the shift key is pressed 0230 if ((event->buttons() & Qt::LeftButton) && event->modifiers() == Qt::ShiftModifier) { 0231 QListView::mouseMoveEvent(event); 0232 return; 0233 } 0234 0235 if (! (event->buttons() & Qt::LeftButton) 0236 || ! m_dragStarted 0237 || (event->pos() - m_dragStartPosition).manhattanLength() 0238 < QApplication::startDragDistance()) { 0239 0240 return; 0241 } 0242 0243 auto *drag = new QDrag(this); 0244 const auto paths = selectedPaths(); 0245 0246 if (paths.count() == 1) { 0247 drag->setPixmap(currentIndex().data(KGeoTag::ThumbnailRole).value<QPixmap>()); 0248 } 0249 0250 QMimeData *mimeData = new QMimeData; 0251 QList<QUrl> urls; 0252 for (const auto &path : paths) { 0253 urls.append(QUrl::fromLocalFile(path)); 0254 } 0255 mimeData->setUrls(urls); 0256 0257 mimeData->setData(KGeoTag::SourceImagesListMimeType, 0258 KGeoTag::SourceImagesList.value(m_listType)); 0259 0260 drag->setMimeData(mimeData); 0261 drag->exec(Qt::MoveAction); 0262 } 0263 0264 QVector<QString> ImagesListView::selectedPaths() const 0265 { 0266 QVector<QString> paths; 0267 const auto selected = selectedIndexes(); 0268 for (const auto &index : selected) { 0269 paths.append(index.data(KGeoTag::PathRole).toString()); 0270 } 0271 return paths; 0272 } 0273 0274 void ImagesListView::keyPressEvent(QKeyEvent *event) 0275 { 0276 QListView::keyPressEvent(event); 0277 0278 const auto key = event->key(); 0279 if (! (key == Qt::Key_Up || key == Qt::Key_Down 0280 || key == Qt::Key_PageUp || key == Qt::Key_PageDown)) { 0281 0282 return; 0283 } 0284 0285 Q_EMIT centerImage(currentIndex()); 0286 } 0287 0288 void ImagesListView::showContextMenu(const QPoint &point) 0289 { 0290 const auto selected = selectedIndexes(); 0291 const int allSelected = selected.count(); 0292 const bool anySelected = allSelected > 0; 0293 0294 m_selectMenu->setEnabled(model()->rowCount() > 0); 0295 m_selectAll->setEnabled(model()->rowCount() > 0); 0296 0297 m_automaticMatchingMenu->setEnabled(anySelected); 0298 m_bookmarksMenu->setEnabled(anySelected); 0299 m_assignToMapCenter->setEnabled(anySelected); 0300 m_assignToClipboard->setEnabled(anySelected); 0301 m_assignManually->setEnabled(anySelected); 0302 m_editCoordinates->setEnabled(anySelected); 0303 m_findClosestTrackPoint->setEnabled(allSelected == 1); 0304 m_lookupElevation->setEnabled(anySelected); 0305 m_removeCoordinates->setEnabled(anySelected); 0306 m_discardChanges->setEnabled(anySelected); 0307 m_removeImages->setEnabled(anySelected); 0308 if (anySelected) { 0309 m_removeImages->setText(i18np("Remove image", "Remove images", allSelected)); 0310 } 0311 m_openExternally->setEnabled(allSelected == 1); 0312 0313 int hasCoordinates = 0; 0314 int changed = 0; 0315 0316 for (const auto &index : selected) { 0317 if (index.data(KGeoTag::CoordinatesRole).value<Coordinates>().isSet()) { 0318 hasCoordinates++; 0319 } 0320 0321 if (index.data(KGeoTag::ChangedRole).toBool()) { 0322 changed++; 0323 } 0324 } 0325 0326 m_findClosestTrackPoint->setVisible(m_listType == KGeoTag::UnAssignedImages 0327 || m_listType == KGeoTag::AllImages); 0328 m_assignManually->setVisible(hasCoordinates == 0); 0329 m_editCoordinates->setVisible(hasCoordinates > 0); 0330 m_lookupElevation->setVisible(m_listType == KGeoTag::AssignedImages 0331 || m_listType == KGeoTag::AllImages); 0332 m_removeCoordinates->setVisible(hasCoordinates > 0); 0333 m_discardChanges->setVisible(changed > 0); 0334 m_save->setVisible(changed > 0); 0335 0336 m_contextMenu->exec(mapToGlobal(point)); 0337 } 0338 0339 void ImagesListView::selectImages(bool coordinatesSet) 0340 { 0341 clearSelection(); 0342 for (int i = 0; i < model()->rowCount(); i++) { 0343 const auto index = model()->index(i, 0); 0344 if (index.data(KGeoTag::CoordinatesRole).value<Coordinates>().isSet() == coordinatesSet) { 0345 selectionModel()->select(index, QItemSelectionModel::Select); 0346 } 0347 } 0348 } 0349 0350 void ImagesListView::openExternally() 0351 { 0352 // selectedPaths() always contains exactly one entry when this is called, 0353 // because the connected action is only enabled in this very case 0354 QDesktopServices::openUrl(QUrl::fromLocalFile(selectedPaths().first())); 0355 } 0356 0357 void ImagesListView::assignToClipboard() 0358 { 0359 const auto data = QGuiApplication::clipboard()->text().simplified(); 0360 bool dataParsed = false; 0361 double lon = 0.0; 0362 double lat = 0.0; 0363 0364 QRegularExpression re; 0365 QRegularExpressionMatch match; 0366 0367 // Google Maps 0368 // Schema: xx.xxxxxxxxxxxxxx, xx.xxxxxxxxxxxxx 0369 if (! dataParsed) { 0370 re = QRegularExpression(QStringLiteral("^(\\d+\\.\\d+), (\\d+\\.\\d+)$")); 0371 match = re.match(data); 0372 if (match.hasMatch()) { 0373 bool lonOkay = false; 0374 bool latOkay = false; 0375 lat = match.captured(1).toDouble(&latOkay); 0376 lon = match.captured(2).toDouble(&lonOkay); 0377 dataParsed = latOkay && lonOkay; 0378 } 0379 } 0380 0381 // OpenStreetMap Geo-URI 0382 // Schema: geo:xx.xxxxx,xx.xxxxx?z=xx 0383 if (! dataParsed) { 0384 re = QRegularExpression(QStringLiteral("^geo:(\\d+\\.\\d+),(\\d+\\.\\d+)\\?z=\\d+$")); 0385 match = re.match(data); 0386 if (match.hasMatch()) { 0387 bool latOkay = false; 0388 bool lonOkay = false; 0389 lat = match.captured(1).toDouble(&latOkay); 0390 lon = match.captured(2).toDouble(&lonOkay); 0391 dataParsed = latOkay && lonOkay; 0392 } 0393 } 0394 0395 if (dataParsed) { 0396 Q_EMIT assignTo(selectedPaths(), Coordinates(lon, lat, 0.0, true)); 0397 } else { 0398 Q_EMIT failedToParseClipboard(); 0399 } 0400 } 0401 0402 void ImagesListView::highlightImage(const QModelIndex &index) 0403 { 0404 const auto mappedIndex = m_listFilter->mapFromSource(index); 0405 scrollTo(mappedIndex); 0406 clearSelection(); 0407 selectionModel()->select(mappedIndex, QItemSelectionModel::Select); 0408 }