1 /* SPDX-FileCopyrightText: 2020-2021 Tobias Leupold <tobias.leupold@gmx.de>
2 
3    SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
4 */
5 
6 // Local includes
7 #include "ImagesListView.h"
8 #include "SharedObjects.h"
9 #include "ImagesModel.h"
10 #include "Settings.h"
11 #include "ImagesListFilter.h"
12 
13 // KDE includes
14 #include <KLocalizedString>
15 
16 // Qt includes
17 #include <QMouseEvent>
18 #include <QApplication>
19 #include <QMimeData>
20 #include <QDrag>
21 #include <QDebug>
22 #include <QKeyEvent>
23 #include <QMenu>
24 
25 // C++ includes
26 #include <algorithm>
27 #include <functional>
28 
ImagesListView(KGeoTag::ImagesListType type,SharedObjects * sharedObjects,QWidget * parent)29 ImagesListView::ImagesListView(KGeoTag::ImagesListType type, SharedObjects *sharedObjects,
30                                QWidget *parent)
31     : QListView(parent),
32       m_listType(type),
33       m_bookmarks(sharedObjects->bookmarks())
34 {
35     viewport()->setAcceptDrops(true);
36     setDropIndicatorShown(true);
37     setDragDropMode(QAbstractItemView::DropOnly);
38 
39     m_listFilter = new ImagesListFilter(this, type);
40     m_listFilter->setSourceModel(sharedObjects->imagesModel());
41     setModel(m_listFilter);
42     connect(m_listFilter, &ImagesListFilter::requestAddingImages,
43             this, &ImagesListView::requestAddingImages);
44     connect(m_listFilter, &ImagesListFilter::requestRemoveCoordinates,
45             this, QOverload<const QVector<QString> &>::of(&ImagesListView::removeCoordinates));
46 
47     setSelectionMode(QAbstractItemView::ExtendedSelection);
48     setContextMenuPolicy(Qt::CustomContextMenu);
49     const int iconSize = sharedObjects->settings()->thumbnailSize();
50     setIconSize(QSize(iconSize, iconSize));
51 
52     connect(this, &QAbstractItemView::clicked, this, &ImagesListView::centerImage);
53     connect(this, &QAbstractItemView::clicked, this, &ImagesListView::imageSelected);
54 
55     // Context menu
56 
57     m_contextMenu = new QMenu(this);
58 
59     m_selectAll = m_contextMenu->addAction(i18n("Select all images"));
60     m_selectAll->setIcon(QIcon::fromTheme(QStringLiteral("select")));
61     connect(m_selectAll, &QAction::triggered, this, &QListView::selectAll);
62 
63     m_selectMenu = m_contextMenu->addMenu(i18n("Select"));
64     m_selectMenu->menuAction()->setIcon(QIcon::fromTheme(QStringLiteral("select")));
65     auto *all = m_selectMenu->addAction(i18n("All images"));
66     connect(all, &QAction::triggered, this, &QListView::selectAll);
67     auto *without = m_selectMenu->addAction(i18n("All images without coordinates"));
68     connect(without, &QAction::triggered,
69             this, std::bind(&ImagesListView::selectImages, this, false));
70     auto *with = m_selectMenu->addAction(i18n("All images with coordinates"));
71     connect(with, &QAction::triggered,
72             this, std::bind(&ImagesListView::selectImages, this, true));
73 
74     m_contextMenu->addSeparator();
75 
76     m_automaticMatchingMenu = m_contextMenu->addMenu(i18n("Automatic matching"));
77     m_automaticMatchingMenu->menuAction()->setIcon(QIcon::fromTheme(QStringLiteral("run-build")));
78 
79     auto *combinedMatchSearchAction = m_automaticMatchingMenu->addAction(
80         i18n("Combined match search"));
81     connect(combinedMatchSearchAction, &QAction::triggered,
82             this, std::bind(&ImagesListView::requestAutomaticMatching, this,
83                             this, KGeoTag::CombinedMatchSearch));
84 
85     m_automaticMatchingMenu->addSeparator();
86 
87     auto *searchExactMatchesAction = m_automaticMatchingMenu->addAction(
88         i18n("Search exact matches only"));
89     connect(searchExactMatchesAction, &QAction::triggered,
90             this, std::bind(&ImagesListView::requestAutomaticMatching, this,
91                             this, KGeoTag::ExactMatchSearch));
92 
93     auto *searchInterpolatedMatchesAction = m_automaticMatchingMenu->addAction(
94         i18n("Search interpolated matches only"));
95     connect(searchInterpolatedMatchesAction, &QAction::triggered,
96             this, std::bind(&ImagesListView::requestAutomaticMatching, this,
97                             this, KGeoTag::InterpolatedMatchSearch));
98 
99     m_bookmarksMenu = m_contextMenu->addMenu(i18n("Assign to bookmark"));
100     m_bookmarksMenu->menuAction()->setIcon(QIcon::fromTheme(QStringLiteral("bookmarks")));
101     updateBookmarks();
102 
103     m_contextMenu->addSeparator();
104 
105     m_assignToMapCenter = m_contextMenu->addAction(i18n("Assign to map center"));
106     m_assignToMapCenter->setIcon(QIcon::fromTheme(QStringLiteral("crosshairs")));
107     connect(m_assignToMapCenter, &QAction::triggered,
108             this, std::bind(&ImagesListView::assignToMapCenter, this, this));
109 
110     m_assignManually = m_contextMenu->addAction(i18n("Set coordinates manually"));
111     m_assignManually->setIcon(QIcon::fromTheme(QStringLiteral("add-placemark")));
112     connect(m_assignManually, &QAction::triggered,
113             this, std::bind(&ImagesListView::assignManually, this, this));
114 
115     m_editCoordinates = m_contextMenu->addAction(i18n("Edit coordinates"));
116     connect(m_editCoordinates, &QAction::triggered,
117             this, std::bind(&ImagesListView::editCoordinates, this, this));
118 
119     m_lookupElevation = m_contextMenu->addAction(i18n("Lookup elevation"));
120     m_lookupElevation->setIcon(QIcon::fromTheme(QStringLiteral("adjustcurves")));
121     connect(m_lookupElevation, &QAction::triggered,
122             this, std::bind(&ImagesListView::lookupElevation, this, this));
123 
124     m_contextMenu->addSeparator();
125 
126     m_save = m_contextMenu->addAction(i18n("Save changes"));
127     m_save->setIcon(QIcon::fromTheme(QStringLiteral("document-save")));
128     connect(m_save, &QAction::triggered,
129             this, std::bind(&ImagesListView::requestSaving, this, this));
130 
131     m_contextMenu->addSeparator();
132 
133     m_removeCoordinates = m_contextMenu->addAction(i18n("Remove coordinates"));
134     connect(m_removeCoordinates, &QAction::triggered,
135             this, std::bind(QOverload<ImagesListView *>::of(&ImagesListView::removeCoordinates),
136                             this, this));
137 
138     m_contextMenu->addSeparator();
139 
140     m_discardChanges = m_contextMenu->addAction(i18n("Discard changes"));
141     m_discardChanges->setIcon(QIcon::fromTheme(QStringLiteral("dialog-cancel")));
142     connect(m_discardChanges, &QAction::triggered,
143             this, std::bind(&ImagesListView::discardChanges, this, this));
144 
145     m_removeImages = m_contextMenu->addAction(i18np("Remove image", "Remove images", 1));
146     m_removeImages->setIcon(QIcon::fromTheme(QStringLiteral("document-close")));
147     connect(m_removeImages, &QAction::triggered,
148             this, std::bind(&ImagesListView::removeImages, this, this));
149 
150     connect(this, &QListView::customContextMenuRequested, this, &ImagesListView::showContextMenu);
151 }
152 
setListType(KGeoTag::ImagesListType type)153 void ImagesListView::setListType(KGeoTag::ImagesListType type)
154 {
155     m_listType = type;
156     m_listFilter->setListType(type);
157     m_selectAll->setVisible(type != KGeoTag::AllImages);
158     m_selectMenu->menuAction()->setVisible(type == KGeoTag::AllImages);
159 }
160 
currentChanged(const QModelIndex & current,const QModelIndex &)161 void ImagesListView::currentChanged(const QModelIndex &current, const QModelIndex &)
162 {
163     if (current.isValid()) {
164         emit imageSelected(current);
165         scrollTo(current);
166     }
167 }
168 
updateBookmarks()169 void ImagesListView::updateBookmarks()
170 {
171     m_bookmarksMenu->clear();
172     auto bookmarks = m_bookmarks->keys();
173 
174     if (bookmarks.count() == 0) {
175         m_bookmarksMenu->addAction(i18n("(No bookmarks defined)"));
176         return;
177     }
178 
179     std::sort(bookmarks.begin(), bookmarks.end());
180     for (const auto &label : std::as_const(bookmarks)) {
181         auto *entry = m_bookmarksMenu->addAction(label);
182         entry->setData(label);
183         connect(entry, &QAction::triggered,
184                 this, std::bind([this](QAction *action)
185                 {
186                     emit assignTo(selectedPaths(), m_bookmarks->value(action->data().toString()));
187                 },
188                 entry));
189     }
190 }
191 
mousePressEvent(QMouseEvent * event)192 void ImagesListView::mousePressEvent(QMouseEvent *event)
193 {
194     const auto pos = event->pos();
195     const auto selectedIndex = indexAt(pos);
196     if (event->button() == Qt::LeftButton && selectedIndex.isValid()) {
197         m_dragStarted = true;
198         m_dragStartPosition = event->pos();
199     } else {
200         m_dragStarted = false;
201     }
202 
203     QListView::mousePressEvent(event);
204 }
205 
mouseMoveEvent(QMouseEvent * event)206 void ImagesListView::mouseMoveEvent(QMouseEvent *event)
207 {
208     // Enable selecting more images by dragging when the shift key is pressed
209     if ((event->buttons() & Qt::LeftButton) && event->modifiers() == Qt::ShiftModifier) {
210         QListView::mouseMoveEvent(event);
211         return;
212     }
213 
214     if (! (event->buttons() & Qt::LeftButton)
215         || ! m_dragStarted
216         || (event->pos() - m_dragStartPosition).manhattanLength()
217            < QApplication::startDragDistance()) {
218 
219         return;
220     }
221 
222     auto *drag = new QDrag(this);
223     const auto paths = selectedPaths();
224 
225     if (paths.count() == 1) {
226         drag->setPixmap(currentIndex().data(KGeoTag::ThumbnailRole).value<QPixmap>());
227     }
228 
229     QMimeData *mimeData = new QMimeData;
230     QList<QUrl> urls;
231     for (const auto &path : paths) {
232         urls.append(QUrl::fromLocalFile(path));
233     }
234     mimeData->setUrls(urls);
235 
236     mimeData->setData(KGeoTag::SourceImagesListMimeType,
237                       KGeoTag::SourceImagesList.value(m_listType));
238 
239     drag->setMimeData(mimeData);
240     drag->exec(Qt::MoveAction);
241 }
242 
selectedPaths() const243 QVector<QString> ImagesListView::selectedPaths() const
244 {
245     QVector<QString> paths;
246     const auto selected = selectedIndexes();
247     for (const auto &index : selected) {
248         paths.append(index.data(KGeoTag::PathRole).toString());
249     }
250     return paths;
251 }
252 
keyPressEvent(QKeyEvent * event)253 void ImagesListView::keyPressEvent(QKeyEvent *event)
254 {
255     QListView::keyPressEvent(event);
256 
257     const auto key = event->key();
258     if (! (key == Qt::Key_Up || key == Qt::Key_Down
259            || key == Qt::Key_PageUp || key == Qt::Key_PageDown)) {
260 
261         return;
262     }
263 
264     emit centerImage(currentIndex());
265 }
266 
showContextMenu(const QPoint & point)267 void ImagesListView::showContextMenu(const QPoint &point)
268 {
269     const auto selected = selectedIndexes();
270     const int allSelected = selected.count();
271     const bool anySelected = allSelected > 0;
272 
273     m_selectMenu->setEnabled(model()->rowCount() > 0);
274     m_selectAll->setEnabled(model()->rowCount() > 0);
275 
276     m_automaticMatchingMenu->setEnabled(anySelected);
277     m_bookmarksMenu->setEnabled(anySelected);
278     m_assignToMapCenter->setEnabled(anySelected);
279     m_assignManually->setEnabled(anySelected);
280     m_editCoordinates->setEnabled(anySelected);
281     m_lookupElevation->setEnabled(anySelected);
282     m_removeCoordinates->setEnabled(anySelected);
283     m_discardChanges->setEnabled(anySelected);
284     m_removeImages->setEnabled(anySelected);
285     if (anySelected) {
286         m_removeImages->setText(i18np("Remove image", "Remove images", allSelected));
287     }
288 
289     int hasCoordinates = 0;
290     int changed = 0;
291 
292     for (const auto &index : selected) {
293         if (index.data(KGeoTag::CoordinatesRole).value<Coordinates>().isSet()) {
294             hasCoordinates++;
295         }
296 
297         if (index.data(KGeoTag::ChangedRole).toBool()) {
298             changed++;
299         }
300     }
301 
302     m_assignManually->setVisible(hasCoordinates == 0);
303     m_editCoordinates->setVisible(hasCoordinates > 0);
304     m_lookupElevation->setVisible(hasCoordinates == allSelected);
305     m_removeCoordinates->setVisible(hasCoordinates > 0);
306     m_discardChanges->setVisible(changed > 0);
307     m_save->setVisible(changed > 0);
308 
309     m_contextMenu->exec(mapToGlobal(point));
310 }
311 
selectImages(bool coordinatesSet)312 void ImagesListView::selectImages(bool coordinatesSet)
313 {
314     clearSelection();
315     for (int i = 0; i < model()->rowCount(); i++) {
316         const auto index = model()->index(i, 0);
317         if (index.data(KGeoTag::CoordinatesRole).value<Coordinates>().isSet() == coordinatesSet) {
318             selectionModel()->select(index, QItemSelectionModel::Select);
319         }
320     }
321 }
322