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 ¤t, 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