1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2006-05-16
7  * Description : A tool to edit geolocation
8  *
9  * Copyright (C) 2006-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10  * Copyright (C) 2010-2014 by Michael G. Hansen <mike at mghansen dot de>
11  * Copyright (C) 2010      by Gabriel Voicu <ping dot gabi at gmail dot com>
12  * Copyright (C) 2014      by Justus Schwartz <justus at gmx dot li>
13  *
14  * This program is free software; you can redistribute it
15  * and/or modify it under the terms of the GNU General
16  * Public License as published by the Free Software Foundation;
17  * either version 2, or (at your option)
18  * any later version.
19  *
20  * This program is distributed in the hope that it will be useful,
21  * but WITHOUT ANY WARRANTY; without even the implied warranty of
22  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23  * GNU General Public License for more details.
24  *
25  * ============================================================ */
26 
27 #include "geolocationedit.h"
28 
29 // Qt includes
30 
31 #include <QtConcurrentMap>
32 #include <QButtonGroup>
33 #include <QCheckBox>
34 #include <QCloseEvent>
35 #include <QFuture>
36 #include <QFutureWatcher>
37 #include <QGroupBox>
38 #include <QHBoxLayout>
39 #include <QHeaderView>
40 #include <QLabel>
41 #include <QPushButton>
42 #include <QGridLayout>
43 #include <QPointer>
44 #include <QRadioButton>
45 #include <QSplitter>
46 #include <QStackedLayout>
47 #include <QStackedWidget>
48 #include <QTimer>
49 #include <QToolButton>
50 #include <QTreeView>
51 #include <QMenu>
52 #include <QUndoView>
53 #include <QAction>
54 #include <QApplication>
55 #include <QComboBox>
56 #include <QDialogButtonBox>
57 
58 // KDE includes
59 
60 #include <kconfiggroup.h>
61 #include <ksharedconfig.h>
62 #include <klocalizedstring.h>
63 
64 // Local includes
65 
66 #include "dlayoutbox.h"
67 #include "digikam_config.h"
68 #include "itemmarkertiler.h"
69 #include "trackmanager.h"
70 #include "gpscommon.h"
71 #include "gpsitemmodel.h"
72 #include "mapdragdrophandler.h"
73 #include "gpsitemlist.h"
74 #include "gpsitemlistdragdrophandler.h"
75 #include "gpsitemlistcontextmenu.h"
76 #include "gpscorrelatorwidget.h"
77 #include "digikam_debug.h"
78 #include "dmessagebox.h"
79 #include "rgwidget.h"
80 #include "kmlwidget.h"
81 #include "statusprogressbar.h"
82 #include "searchwidget.h"
83 #include "backend-rg.h"
84 #include "gpsitemdetails.h"
85 #include "gpsgeoifacemodelhelper.h"
86 #include "dxmlguiwindow.h"
87 #include "gpsbookmarkowner.h"
88 #include "gpsbookmarkmodelhelper.h"
89 
90 #ifdef GPSSYNC_MODELTEST
91 #   include <modeltest.h>
92 #endif
93 
94 using namespace Digikam;
95 
96 namespace DigikamGenericGeolocationEditPlugin
97 {
98 
99 struct SaveChangedImagesHelper
100 {
101 public:
102 
SaveChangedImagesHelperDigikamGenericGeolocationEditPlugin::SaveChangedImagesHelper103     explicit SaveChangedImagesHelper(GPSItemModel* const model)
104         : imageModel(model)
105     {
106     }
107 
operator ()DigikamGenericGeolocationEditPlugin::SaveChangedImagesHelper108     QPair<QUrl, QString> operator()(const QPersistentModelIndex& itemIndex)
109     {
110         GPSItemContainer* const item = imageModel->itemFromIndex(itemIndex);
111 
112         if (!item)
113         {
114             return QPair<QUrl, QString>(QUrl(), QString());
115         }
116 
117         return QPair<QUrl, QString>(item->url(), item->saveChanges());
118     }
119 
120 public:
121 
122     typedef QPair<QUrl, QString> result_type;
123     GPSItemModel* const          imageModel;
124 };
125 
126 // ---------------------------------------------------------------------------------
127 
128 struct LoadFileMetadataHelper
129 {
130 public:
131 
LoadFileMetadataHelperDigikamGenericGeolocationEditPlugin::LoadFileMetadataHelper132     explicit LoadFileMetadataHelper(GPSItemModel* const model)
133         : imageModel(model)
134     {
135     }
136 
operator ()DigikamGenericGeolocationEditPlugin::LoadFileMetadataHelper137     QPair<QUrl, QString> operator()(const QPersistentModelIndex& itemIndex)
138     {
139         GPSItemContainer* const item = imageModel->itemFromIndex(itemIndex);
140 
141         if (!item)
142         {
143             return QPair<QUrl, QString>(QUrl(), QString());
144         }
145 
146         item->loadImageData();
147 
148         return QPair<QUrl, QString>(item->url(), QString());
149     }
150 
151 public:
152 
153     typedef QPair<QUrl, QString> result_type;
154     GPSItemModel* const          imageModel;
155 };
156 
157 // ---------------------------------------------------------------------------------
158 
159 class Q_DECL_HIDDEN GeolocationEdit::Private
160 {
161 public:
162 
Private()163     explicit Private()
164     {
165         imageModel               = nullptr;
166         selectionModel           = nullptr;
167         uiEnabled                = true;
168         listViewContextMenu      = nullptr;
169         trackManager             = nullptr;
170         fileIOFutureWatcher      = nullptr;
171         fileIOCountDone          = 0;
172         fileIOCountTotal         = 0;
173         fileIOCloseAfterSaving   = false;
174         buttonBox                = nullptr;
175         VSplitter                = nullptr;
176         HSplitter                = nullptr;
177         treeView                 = nullptr;
178         stackedWidget            = nullptr;
179         tabBar                   = nullptr;
180         splitterSize             = 0;
181         undoStack                = nullptr;
182         undoView                 = nullptr;
183         progressBar              = nullptr;
184         progressCancelButton     = nullptr;
185         progressCancelObject     = nullptr;
186         detailsWidget            = nullptr;
187         correlatorWidget         = nullptr;
188         rgWidget                 = nullptr;
189         searchWidget             = nullptr;
190         kmlWidget                = nullptr;
191         mapSplitter              = nullptr;
192         mapWidget                = nullptr;
193         mapWidget2               = nullptr;
194         mapDragDropHandler       = nullptr;
195         mapModelHelper           = nullptr;
196         geoifaceMarkerModel      = nullptr;
197         sortActionOldestFirst    = nullptr;
198         sortActionYoungestFirst  = nullptr;
199         sortMenu                 = nullptr;
200         mapLayout                = MapLayoutOne;
201         cbMapLayout              = nullptr;
202         bookmarkOwner            = nullptr;
203         actionBookmarkVisibility = nullptr;
204         iface                    = nullptr;
205     }
206 
207     // General things
208     GPSItemModel*                            imageModel;
209     QItemSelectionModel*                     selectionModel;
210     bool                                     uiEnabled;
211     GPSItemListContextMenu*                  listViewContextMenu;
212     TrackManager*                            trackManager;
213 
214     // Loading and saving
215     QFuture<QPair<QUrl,QString> >            fileIOFuture;
216     QFutureWatcher<QPair<QUrl,QString> >*    fileIOFutureWatcher;
217     int                                      fileIOCountDone;
218     int                                      fileIOCountTotal;
219     bool                                     fileIOCloseAfterSaving;
220 
221     // UI
222     QDialogButtonBox*                        buttonBox;
223     QSplitter*                               VSplitter;
224     QSplitter*                               HSplitter;
225     GPSItemList*                             treeView;
226     QStackedWidget*                          stackedWidget;
227     QTabBar*                                 tabBar;
228     int                                      splitterSize;
229     QUndoStack*                              undoStack;
230     QUndoView*                               undoView;
231 
232     // UI: progress
233     StatusProgressBar*                       progressBar;
234     QPushButton*                             progressCancelButton;
235     QObject*                                 progressCancelObject;
236     QString                                  progressCancelSlot;
237 
238     // UI: tab widgets
239     GPSItemDetails*                          detailsWidget;
240     GPSCorrelatorWidget*                     correlatorWidget;
241     RGWidget*                                rgWidget;
242     SearchWidget*                            searchWidget;
243     KmlWidget*                               kmlWidget;
244 
245     // map: UI
246     MapLayout                                mapLayout;
247     QSplitter*                               mapSplitter;
248     MapWidget*                               mapWidget;
249     MapWidget*                               mapWidget2;
250 
251     // map: helpers
252     MapDragDropHandler*                      mapDragDropHandler;
253     GPSGeoIfaceModelHelper*                  mapModelHelper;
254     ItemMarkerTiler*                         geoifaceMarkerModel;
255 
256     // map: actions
257     QAction*                                 sortActionOldestFirst;
258     QAction*                                 sortActionYoungestFirst;
259     QMenu*                                   sortMenu;
260     QComboBox*                               cbMapLayout;
261 
262     GPSBookmarkOwner*                        bookmarkOwner;
263     QAction*                                 actionBookmarkVisibility;
264 
265     DInfoInterface*                          iface;
266 };
267 
GeolocationEdit(QWidget * const parent,DInfoInterface * const iface)268 GeolocationEdit::GeolocationEdit(QWidget* const parent, DInfoInterface* const iface)
269     : DPluginDialog(parent, QLatin1String("Geolocation Edit Settings")),
270       d            (new Private)
271 {
272     setAttribute(Qt::WA_DeleteOnClose, true);
273     setWindowTitle(i18nc("@title", "Geolocation Editor"));
274     setMinimumSize(300, 400);
275     setModal(true);
276 
277     d->iface          = iface;
278     d->imageModel     = new GPSItemModel(this);
279     d->selectionModel = new QItemSelectionModel(d->imageModel);
280     d->trackManager   = new TrackManager(this);
281 
282 #ifdef GPSSYNC_MODELTEST
283 
284     new ModelTest(d->imageModel, this);
285 
286 #endif
287 
288     d->bookmarkOwner = new GPSBookmarkOwner(d->imageModel, this);
289     d->undoStack     = new QUndoStack(this);
290     d->stackedWidget = new QStackedWidget();
291     d->searchWidget  = new SearchWidget(d->bookmarkOwner,
292                                         d->imageModel,
293                                         d->selectionModel,
294                                         d->stackedWidget);
295 
296     GPSItemContainer::setHeaderData(d->imageModel);
297     d->mapModelHelper      = new GPSGeoIfaceModelHelper(d->imageModel, d->selectionModel, this);
298     d->mapModelHelper->addUngroupedModelHelper(d->bookmarkOwner->bookmarkModelHelper());
299 
300     d->mapModelHelper->addUngroupedModelHelper(d->searchWidget->getModelHelper());
301     d->mapDragDropHandler  = new MapDragDropHandler(d->imageModel, d->mapModelHelper);
302     d->geoifaceMarkerModel = new ItemMarkerTiler(d->mapModelHelper, this);
303 
304     d->actionBookmarkVisibility = new QAction(this);
305     d->actionBookmarkVisibility->setIcon(QIcon::fromTheme(QLatin1String("bookmark-new")));
306     d->actionBookmarkVisibility->setToolTip(i18nc("@info", "Display bookmarked positions on the map."));
307     d->actionBookmarkVisibility->setCheckable(true);
308 
309     connect(d->actionBookmarkVisibility, SIGNAL(changed()),
310             this, SLOT(slotBookmarkVisibilityToggled()));
311 
312     QVBoxLayout* const mainLayout = new QVBoxLayout(this);
313     setLayout(mainLayout);
314 
315     DHBox* const hboxMain = new DHBox(this);
316     mainLayout->addWidget(hboxMain, 10);
317 
318     d->HSplitter          = new QSplitter(Qt::Horizontal, hboxMain);
319     d->HSplitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
320 
321     // ------------------------------------------------------------------------------------------------
322 
323     DHBox* const hbox            = new DHBox(this);
324     QLabel* const labelMapLayout = new QLabel(i18nc("@label", "Layout:"), hbox);
325     d->cbMapLayout               = new QComboBox(hbox);
326     d->cbMapLayout->addItem(i18nc("@item", "One map"),               QVariant::fromValue(MapLayoutOne));
327     d->cbMapLayout->addItem(i18nc("@item", "Two maps - horizontal"), QVariant::fromValue(MapLayoutHorizontal));
328     d->cbMapLayout->addItem(i18nc("@item", "Two maps - vertical"),   QVariant::fromValue(MapLayoutVertical));
329     labelMapLayout->setBuddy(d->cbMapLayout);
330 
331     d->progressBar          = new StatusProgressBar(hbox);
332     d->progressBar->setVisible(false);
333     d->progressBar->setProgressBarMode(StatusProgressBar::ProgressBarMode);
334     d->progressBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
335 
336     d->progressCancelButton = new QPushButton(hbox);
337     d->progressCancelButton->setVisible(false);
338     d->progressCancelButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
339     d->progressCancelButton->setIcon(QIcon::fromTheme(QLatin1String("dialog-cancel")));
340 
341     connect(d->progressCancelButton, SIGNAL(clicked()),
342             this, SLOT(slotProgressCancelButtonClicked()));
343 
344     m_buttons->addButton(QDialogButtonBox::Apply);
345     m_buttons->addButton(QDialogButtonBox::Close);
346     m_buttons->setParent(hbox);
347 
348     connect(m_buttons->button(QDialogButtonBox::Apply), &QPushButton::clicked,
349             this, &GeolocationEdit::slotApplyClicked);
350 
351     connect(m_buttons->button(QDialogButtonBox::Close), &QPushButton::clicked,
352             this, &GeolocationEdit::close);
353 
354     mainLayout->addWidget(hbox, 0);
355 
356     // ------------------------------------------------------------------------------------------------
357 
358     d->VSplitter = new QSplitter(Qt::Vertical, d->HSplitter);
359     d->HSplitter->addWidget(d->VSplitter);
360     d->HSplitter->setStretchFactor(0, 10);
361 
362     d->sortMenu = new QMenu(this);
363     d->sortMenu->setTitle(i18nc("@title", "Sorting"));
364     QActionGroup* const sortOrderExclusive = new QActionGroup(d->sortMenu);
365     sortOrderExclusive->setExclusive(true);
366 
367     connect(sortOrderExclusive, SIGNAL(triggered(QAction*)),
368             this, SLOT(slotSortOptionTriggered(QAction*)));
369 
370     d->sortActionOldestFirst = new QAction(i18nc("@action", "Show oldest first"), sortOrderExclusive);
371     d->sortActionOldestFirst->setCheckable(true);
372     d->sortMenu->addAction(d->sortActionOldestFirst);
373 
374     d->sortActionYoungestFirst = new QAction(i18nc("@action", "Show youngest first"), sortOrderExclusive);
375     d->sortMenu->addAction(d->sortActionYoungestFirst);
376     d->sortActionYoungestFirst->setCheckable(true);
377 
378     QWidget* mapVBox = nullptr;
379     d->mapWidget     = makeMapWidget(&mapVBox);
380     d->searchWidget->setPrimaryMapWidget(d->mapWidget);
381     d->mapSplitter   = new QSplitter(this);
382     d->mapSplitter->addWidget(mapVBox);
383     d->VSplitter->addWidget(d->mapSplitter);
384 
385     d->treeView      = new GPSItemList(this);
386     d->treeView->setModelAndSelectionModel(d->imageModel, d->selectionModel);
387     d->treeView->setDragDropHandler(new GPSItemListDragDropHandler(this));
388     d->treeView->setDragEnabled(true);
389 
390     /**
391      * TODO: save and restore the state of the header
392      * TODO: add a context menu to the header to select which columns should be visible
393      * TODO: add sorting by column
394      */
395 
396     d->treeView->setSelectionMode(QAbstractItemView::ExtendedSelection);
397     d->treeView->setSortingEnabled(true);
398     d->VSplitter->addWidget(d->treeView);
399 
400     d->listViewContextMenu  = new GPSItemListContextMenu(d->treeView, d->bookmarkOwner);
401     d->HSplitter->addWidget(d->stackedWidget);
402     d->HSplitter->setCollapsible(1, true);
403     d->splitterSize         = 0;
404 
405     DVBox* const vboxTabBar = new DVBox(hboxMain);
406     vboxTabBar->layout()->setContentsMargins(QMargins());
407     vboxTabBar->layout()->setSpacing(0);
408 
409     d->tabBar = new QTabBar(vboxTabBar);
410     d->tabBar->setShape(QTabBar::RoundedEast);
411 
412     dynamic_cast<QVBoxLayout*>(vboxTabBar->layout())->addStretch(200);
413 
414     d->tabBar->addTab(i18nc("@item: map tool", "Details"));
415     d->tabBar->addTab(i18nc("@item: map tool", "GPS Correlator"));
416     d->tabBar->addTab(i18nc("@item: map tool", "Undo/Redo"));
417     d->tabBar->addTab(i18nc("@item: map tool", "Reverse Geocoding"));
418     d->tabBar->addTab(i18nc("@item: map tool", "Search"));
419     d->tabBar->addTab(i18nc("@item: map tool", "KML Export"));
420 
421     d->tabBar->installEventFilter(this);
422 
423     d->detailsWidget    = new GPSItemDetails(d->stackedWidget, d->imageModel);
424     d->stackedWidget->addWidget(d->detailsWidget);
425 
426     d->correlatorWidget = new GPSCorrelatorWidget(d->stackedWidget, d->imageModel, d->trackManager);
427     d->stackedWidget->addWidget(d->correlatorWidget);
428 
429     d->undoView         = new QUndoView(d->undoStack, d->stackedWidget);
430     d->stackedWidget->addWidget(d->undoView);
431 
432     d->rgWidget         = new RGWidget(d->imageModel, d->selectionModel, d->iface->tagFilterModel(), d->stackedWidget);
433     d->stackedWidget->addWidget(d->rgWidget);
434 
435     d->stackedWidget->addWidget(d->searchWidget);
436 
437     d->kmlWidget        = new KmlWidget(this, d->imageModel, d->iface);
438     d->stackedWidget->addWidget(d->kmlWidget);
439 
440     // ---------------------------------------------------------------
441 
442     connect(d->treeView, SIGNAL(signalImageActivated(QModelIndex)),
443             this, SLOT(slotImageActivated(QModelIndex)));
444 
445     connect(d->correlatorWidget, SIGNAL(signalSetUIEnabled(bool)),
446             this, SLOT(slotSetUIEnabled(bool)));
447 
448     connect(d->correlatorWidget, SIGNAL(signalSetUIEnabled(bool,QObject*const,QString)),
449             this, SLOT(slotSetUIEnabled(bool,QObject*const,QString)));
450 
451     connect(d->correlatorWidget, SIGNAL(signalProgressSetup(int,QString)),
452             this, SLOT(slotProgressSetup(int,QString)));
453 
454     connect(d->correlatorWidget, SIGNAL(signalProgressChanged(int)),
455             this, SLOT(slotProgressChanged(int)));
456 
457     connect(d->correlatorWidget, SIGNAL(signalUndoCommand(GPSUndoCommand*)),
458             this, SLOT(slotGPSUndoCommand(GPSUndoCommand*)));
459 
460     connect(d->correlatorWidget, SIGNAL(signalTrackListChanged(Digikam::GeoCoordinates)),
461             this, SLOT(slotTrackListChanged(Digikam::GeoCoordinates)));
462 
463     connect(d->mapModelHelper, SIGNAL(signalUndoCommand(GPSUndoCommand*)),
464             this, SLOT(slotGPSUndoCommand(GPSUndoCommand*)));
465 
466     connect(d->rgWidget, SIGNAL(signalSetUIEnabled(bool)),
467             this, SLOT(slotSetUIEnabled(bool)));
468 
469     connect(d->rgWidget, SIGNAL(signalSetUIEnabled(bool,QObject*const,QString)),
470             this, SLOT(slotSetUIEnabled(bool,QObject*const,QString)));
471 
472     connect(d->rgWidget, SIGNAL(signalProgressSetup(int,QString)),
473             this, SLOT(slotProgressSetup(int,QString)));
474 
475     connect(d->rgWidget, SIGNAL(signalProgressChanged(int)),
476             this, SLOT(slotProgressChanged(int)));
477 
478     connect(d->rgWidget, SIGNAL(signalUndoCommand(GPSUndoCommand*)),
479             this, SLOT(slotGPSUndoCommand(GPSUndoCommand*)));
480 
481     connect(d->searchWidget, SIGNAL(signalUndoCommand(GPSUndoCommand*)),
482             this, SLOT(slotGPSUndoCommand(GPSUndoCommand*)));
483 
484     connect(d->listViewContextMenu, SIGNAL(signalSetUIEnabled(bool)),
485             this, SLOT(slotSetUIEnabled(bool)));
486 
487     connect(d->listViewContextMenu, SIGNAL(signalSetUIEnabled(bool,QObject*const,QString)),
488             this, SLOT(slotSetUIEnabled(bool,QObject*const,QString)));
489 
490     connect(d->listViewContextMenu, SIGNAL(signalProgressSetup(int,QString)),
491             this, SLOT(slotProgressSetup(int,QString)));
492 
493     connect(d->listViewContextMenu, SIGNAL(signalProgressChanged(int)),
494             this, SLOT(slotProgressChanged(int)));
495 
496     connect(d->listViewContextMenu, SIGNAL(signalUndoCommand(GPSUndoCommand*)),
497             this, SLOT(slotGPSUndoCommand(GPSUndoCommand*)));
498 
499     connect(d->tabBar, SIGNAL(currentChanged(int)),
500             this, SLOT(slotCurrentTabChanged(int)));
501 
502     connect(d->bookmarkOwner->bookmarkModelHelper(),
503             SIGNAL(signalUndoCommand(GPSUndoCommand*)),
504             this, SLOT(slotGPSUndoCommand(GPSUndoCommand*)));
505 
506     connect(d->detailsWidget, SIGNAL(signalUndoCommand(GPSUndoCommand*)),
507             this, SLOT(slotGPSUndoCommand(GPSUndoCommand*)));
508 
509     connect(d->cbMapLayout, SIGNAL(activated(int)),
510             this, SLOT(slotLayoutChanged(int)));
511 
512     connect(this, SIGNAL(signalMetadataChangedForUrl(QUrl)),
513             d->iface, SLOT(slotMetadataChangedForUrl(QUrl)));
514 
515     readSettings();
516 
517     d->mapWidget->setActive(true);
518 
519     setItems(d->iface->currentGPSItems());
520 }
521 
~GeolocationEdit()522 GeolocationEdit::~GeolocationEdit()
523 {
524     delete d;
525 }
526 
eventFilter(QObject * const o,QEvent * const e)527 bool GeolocationEdit::eventFilter(QObject* const o, QEvent* const e)
528 {
529     if ((o == d->tabBar) && (e->type() == QEvent::MouseButtonPress))
530     {
531         QMouseEvent const* m = static_cast<QMouseEvent*>(e);
532 
533         QPoint p (m->x(), m->y());
534         const int var = d->tabBar->tabAt(p);
535 
536         if (var < 0)
537         {
538             return false;
539         }
540 
541         QList<int> sizes = d->HSplitter->sizes();
542 
543         if (d->splitterSize == 0)
544         {
545             if      (sizes.at(1) == 0)
546             {
547                 sizes[1] = d->stackedWidget->widget(var)->minimumSizeHint().width();
548             }
549             else if (d->tabBar->currentIndex() == var)
550             {
551                 d->splitterSize = sizes.at(1);
552                 sizes[1]        = 0;
553             }
554         }
555         else
556         {
557             sizes[1]        = d->splitterSize;
558             d->splitterSize = 0;
559         }
560 
561         d->tabBar->setCurrentIndex(var);
562         d->stackedWidget->setCurrentIndex(var);
563         d->HSplitter->setSizes(sizes);
564         d->detailsWidget->slotSetActive((d->stackedWidget->currentWidget() == d->detailsWidget) &&
565                                         (d->splitterSize == 0));
566 
567         return true;
568     }
569 
570     return QWidget::eventFilter(o, e);
571 }
572 
slotCurrentTabChanged(int index)573 void GeolocationEdit::slotCurrentTabChanged(int index)
574 {
575     d->tabBar->setCurrentIndex(index);
576     d->stackedWidget->setCurrentIndex(index);
577     d->detailsWidget->slotSetActive(d->stackedWidget->currentWidget() == d->detailsWidget);
578 }
579 
setCurrentTab(int index)580 void GeolocationEdit::setCurrentTab(int index)
581 {
582     d->tabBar->setCurrentIndex(index);
583     d->stackedWidget->setCurrentIndex(index);
584     QList<int> sizes = d->HSplitter->sizes();
585 
586     if (d->splitterSize >= 0)
587     {
588         sizes[1] = d->splitterSize;
589         d->splitterSize = 0;
590     }
591 
592     d->HSplitter->setSizes(sizes);
593     d->detailsWidget->slotSetActive((d->stackedWidget->currentWidget() == d->detailsWidget) &&
594                                     (d->splitterSize == 0));
595 }
596 
setImages(const QList<QUrl> & images)597 void GeolocationEdit::setImages(const QList<QUrl>& images)
598 {
599     QList<GPSItemContainer*> items;
600 
601     foreach (const QUrl& u, images)
602     {
603         items << new GPSItemContainer(u);
604     }
605 
606     setItems(items);
607 }
608 
setItems(const QList<GPSItemContainer * > & items)609 void GeolocationEdit::setItems(const QList<GPSItemContainer*>& items)
610 {
611     foreach (GPSItemContainer* const newItem, items)
612     {
613         newItem->loadImageData();
614         d->imageModel->addItem(newItem);
615     }
616 
617     QList<QPersistentModelIndex> imagesToLoad;
618 
619     for (int i = 0 ; i < d->imageModel->rowCount() ; ++i)
620     {
621         imagesToLoad << d->imageModel->index(i, 0);
622     }
623 
624     slotSetUIEnabled(false);
625     slotProgressSetup(imagesToLoad.count(), i18nc("@info", "Loading metadata -"));
626 
627     // initiate the saving
628 
629     d->fileIOCountDone     = 0;
630     d->fileIOCountTotal    = imagesToLoad.count();
631     d->fileIOFutureWatcher = new QFutureWatcher<QPair<QUrl, QString> >(this);
632 
633     connect(d->fileIOFutureWatcher, SIGNAL(resultsReadyAt(int,int)),
634             this, SLOT(slotFileMetadataLoaded(int,int)));
635 
636     d->fileIOFuture = QtConcurrent::mapped(imagesToLoad, LoadFileMetadataHelper(d->imageModel));
637     d->fileIOFutureWatcher->setFuture(d->fileIOFuture);
638 }
639 
slotFileMetadataLoaded(int beginIndex,int endIndex)640 void GeolocationEdit::slotFileMetadataLoaded(int beginIndex, int endIndex)
641 {
642     qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << beginIndex << endIndex;
643     d->fileIOCountDone += (endIndex-beginIndex);
644     slotProgressChanged(d->fileIOCountDone);
645 
646     if (d->fileIOCountDone == d->fileIOCountTotal)
647     {
648         slotSetUIEnabled(true);
649     }
650 }
651 
readSettings()652 void GeolocationEdit::readSettings()
653 {
654     KSharedConfig::Ptr config = KSharedConfig::openConfig();
655     KConfigGroup group        = config->group("Geolocation Edit Settings");
656 
657     // --------------------------
658 
659     // TODO: sanely determine a default backend
660 
661     const KConfigGroup groupMapWidget = KConfigGroup(&group, "Map Widget");
662     d->mapWidget->readSettingsFromGroup(&groupMapWidget);
663 
664     const KConfigGroup groupCorrelatorWidget = KConfigGroup(&group, "Correlator Widget");
665     d->correlatorWidget->readSettingsFromGroup(&groupCorrelatorWidget);
666 
667     const KConfigGroup groupTreeView = KConfigGroup(&group, "Tree View");
668     d->treeView->readSettingsFromGroup(&groupTreeView);
669 
670     const KConfigGroup groupSearchWidget = KConfigGroup(&group, "Search Widget");
671     d->searchWidget->readSettingsFromGroup(&groupSearchWidget);
672 
673     const KConfigGroup groupRGWidget = KConfigGroup(&group, "Reverse Geocoding Widget");
674     d->rgWidget->readSettingsFromGroup(&groupRGWidget);
675 
676     const KConfigGroup groupDialog = KConfigGroup(&group, "Dialog");
677 
678     // --------------------------
679 
680     setCurrentTab(group.readEntry("Current Tab", 0));
681     const bool showOldestFirst = group.readEntry("Show oldest images first", false);
682 
683     if (showOldestFirst)
684     {
685         d->sortActionOldestFirst->setChecked(true);
686         d->mapWidget->setSortKey(1);
687     }
688     else
689     {
690         d->sortActionYoungestFirst->setChecked(true);
691         d->mapWidget->setSortKey(0);
692     }
693 
694     d->actionBookmarkVisibility->setChecked(group.readEntry("Bookmarks visible", false));
695     slotBookmarkVisibilityToggled();
696 
697     if (group.hasKey("SplitterState V1"))
698     {
699         const QByteArray splitterState = QByteArray::fromBase64(group.readEntry("SplitterState V1", QByteArray()));
700 
701         if (!splitterState.isEmpty())
702         {
703             d->VSplitter->restoreState(splitterState);
704         }
705     }
706 
707     if (group.hasKey("SplitterState H1"))
708     {
709         const QByteArray splitterState = QByteArray::fromBase64(group.readEntry("SplitterState H1", QByteArray()));
710 
711         if (!splitterState.isEmpty())
712         {
713             d->HSplitter->restoreState(splitterState);
714         }
715     }
716 
717     d->splitterSize = group.readEntry("Splitter H1 CollapsedSize", 0);
718 
719     // ----------------------------------
720 
721     d->mapLayout = MapLayout(group.readEntry("Map Layout", QVariant::fromValue(int(MapLayoutOne))).value<int>());
722     d->cbMapLayout->setCurrentIndex(d->mapLayout);
723     adjustMapLayout(false);
724 
725     if (d->mapWidget2)
726     {
727         const KConfigGroup groupMapWidget2 = KConfigGroup(&group, "Map Widget 2");
728         d->mapWidget2->readSettingsFromGroup(&groupMapWidget2);
729 
730         d->mapWidget2->setActive(true);
731     }
732 }
733 
saveSettings()734 void GeolocationEdit::saveSettings()
735 {
736     KSharedConfig::Ptr config = KSharedConfig::openConfig();
737     KConfigGroup group = config->group("Geolocation Edit Settings");
738 
739     // --------------------------
740 
741     KConfigGroup groupMapWidget = KConfigGroup(&group, "Map Widget");
742     d->mapWidget->saveSettingsToGroup(&groupMapWidget);
743 
744     if (d->mapWidget2)
745     {
746         KConfigGroup groupMapWidget2 = KConfigGroup(&group, "Map Widget 2");
747         d->mapWidget2->saveSettingsToGroup(&groupMapWidget2);
748     }
749 
750     KConfigGroup groupCorrelatorWidget = KConfigGroup(&group, "Correlator Widget");
751     d->correlatorWidget->saveSettingsToGroup(&groupCorrelatorWidget);
752 
753     KConfigGroup groupTreeView = KConfigGroup(&group, "Tree View");
754     d->treeView->saveSettingsToGroup(&groupTreeView);
755 
756     KConfigGroup groupSearchWidget = KConfigGroup(&group, "Search Widget");
757     d->searchWidget->saveSettingsToGroup(&groupSearchWidget);
758 
759     KConfigGroup groupRGWidget = KConfigGroup(&group, "Reverse Geocoding Widget");
760     d->rgWidget->saveSettingsToGroup(&groupRGWidget);
761 
762     // --------------------------
763 
764     group.writeEntry("Current Tab",               d->tabBar->currentIndex());
765     group.writeEntry("Show oldest images first",  d->sortActionOldestFirst->isChecked());
766     group.writeEntry("SplitterState V1",          d->VSplitter->saveState().toBase64());
767     group.writeEntry("SplitterState H1",          d->HSplitter->saveState().toBase64());
768     group.writeEntry("Splitter H1 CollapsedSize", d->splitterSize);
769     group.writeEntry("Map Layout",                QVariant::fromValue(int(d->mapLayout)));
770     group.writeEntry("Bookmarks visible",         d->actionBookmarkVisibility->isChecked());
771 
772     config->sync();
773 }
774 
reject()775 void GeolocationEdit::reject()
776 {
777     close();
778 }
779 
closeEvent(QCloseEvent * e)780 void GeolocationEdit::closeEvent(QCloseEvent *e)
781 {
782     if (!e)
783     {
784         return;
785     }
786 
787     // is the UI locked?
788 
789     if (!d->uiEnabled)
790     {
791         // please wait until we are done ...
792 
793         e->ignore();
794 
795         return;
796     }
797 
798     // are there any modified images?
799 
800     int dirtyImagesCount = 0;
801 
802     for (int i = 0 ; i < d->imageModel->rowCount() ; ++i)
803     {
804         const QModelIndex itemIndex  = d->imageModel->index(i, 0);
805         GPSItemContainer* const item = d->imageModel->itemFromIndex(itemIndex);
806 
807         if (item->isDirty() || item->isTagListDirty())
808         {
809             ++dirtyImagesCount;
810         }
811     }
812 
813     if (dirtyImagesCount > 0)
814     {
815         const QString message = i18ncp("@info",
816                     "You have 1 modified image.",
817                     "You have %1 modified images.",
818                     dirtyImagesCount
819                 );
820 
821         const int chosenAction = DMessageBox::showYesNo(QMessageBox::Warning,
822                                                         this,
823                                                         i18nc("@title", "Unsaved changes"),
824                                                         i18nc("@info", "%1 Would you like to save the changes you made to them?", message)
825                                                        );
826 
827         if (chosenAction == QMessageBox::No)
828         {
829             saveSettings();
830             e->accept();
831             return;
832         }
833 
834         if (chosenAction == QMessageBox::Yes)
835         {
836             // the user wants to save his changes.
837             // this will initiate the saving process and then close the dialog.
838 
839             saveChanges(true);
840         }
841 
842         // do not close the dialog for now
843 
844         e->ignore();
845         return;
846     }
847 
848     saveSettings();
849     e->accept();
850 }
851 
slotImageActivated(const QModelIndex & index)852 void GeolocationEdit::slotImageActivated(const QModelIndex& index)
853 {
854     d->detailsWidget->slotSetCurrentImage(index);
855 
856     if (!index.isValid())
857     {
858         return;
859     }
860 
861     GPSItemContainer* const item = d->imageModel->itemFromIndex(index);
862 
863     if (!item)
864     {
865         return;
866     }
867 
868     const GeoCoordinates imageCoordinates = item->coordinates();
869 
870     if (imageCoordinates.hasCoordinates())
871     {
872         d->mapWidget->setCenter(imageCoordinates);
873     }
874 }
875 
slotSetUIEnabled(const bool enabledState,QObject * const cancelObject,const QString & cancelSlot)876 void GeolocationEdit::slotSetUIEnabled(const bool enabledState,
877                                        QObject* const cancelObject,
878                                        const QString& cancelSlot)
879 {
880     if (enabledState)
881     {
882         // hide the progress bar
883 
884         d->progressBar->setVisible(false);
885         d->progressCancelButton->setVisible(false);
886         d->progressBar->setProgressValue(d->progressBar->progressTotalSteps());
887     }
888 
889     /// TODO: disable the worldmapwidget and the images list (at least disable editing operations)
890 
891     d->progressCancelObject = cancelObject;
892     d->progressCancelSlot   = cancelSlot;
893     d->uiEnabled            = enabledState;
894     m_buttons->setEnabled(enabledState);
895     d->correlatorWidget->setUIEnabledExternal(enabledState);
896     d->detailsWidget->setUIEnabledExternal(enabledState);
897     d->rgWidget->setUIEnabled(enabledState);
898     d->treeView->setEditEnabled(enabledState);
899     d->listViewContextMenu->setEnabled(enabledState);
900     d->mapWidget->setAllowModifications(enabledState);
901 }
902 
slotSetUIEnabled(const bool enabledState)903 void GeolocationEdit::slotSetUIEnabled(const bool enabledState)
904 {
905     slotSetUIEnabled(enabledState, nullptr, QString());
906 }
907 
saveChanges(const bool closeAfterwards)908 void GeolocationEdit::saveChanges(const bool closeAfterwards)
909 {
910     /**
911      * TODO: actually save the changes are there any modified images?
912      */
913 
914     QList<QPersistentModelIndex> dirtyImages;
915 
916     for (int i = 0 ; i < d->imageModel->rowCount() ; ++i)
917     {
918         const QModelIndex itemIndex  = d->imageModel->index(i, 0);
919         GPSItemContainer* const item = d->imageModel->itemFromIndex(itemIndex);
920 
921         if (item->isDirty() || item->isTagListDirty())
922         {
923             dirtyImages << itemIndex;
924         }
925     }
926 
927     if (dirtyImages.isEmpty())
928     {
929         if (closeAfterwards)
930         {
931             close();
932         }
933 
934         return;
935     }
936 
937     /// TODO: disable the UI and provide progress and cancel information
938 
939     slotSetUIEnabled(false);
940     slotProgressSetup(dirtyImages.count(), i18nc("@info", "Saving changes -"));
941 
942     // initiate the saving
943 
944     d->fileIOCountDone        = 0;
945     d->fileIOCountTotal       = dirtyImages.count();
946     d->fileIOCloseAfterSaving = closeAfterwards;
947     d->fileIOFutureWatcher    = new QFutureWatcher<QPair<QUrl, QString> >(this);
948 
949     connect(d->fileIOFutureWatcher, SIGNAL(resultsReadyAt(int,int)),
950             this, SLOT(slotFileChangesSaved(int,int)));
951 
952     d->fileIOFuture = QtConcurrent::mapped(dirtyImages, SaveChangedImagesHelper(d->imageModel));
953     d->fileIOFutureWatcher->setFuture(d->fileIOFuture);
954 }
955 
slotFileChangesSaved(int beginIndex,int endIndex)956 void GeolocationEdit::slotFileChangesSaved(int beginIndex, int endIndex)
957 {
958     qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << beginIndex << endIndex;
959 
960     d->fileIOCountDone += (endIndex-beginIndex);
961     slotProgressChanged(d->fileIOCountDone);
962 
963     if (d->fileIOCountDone == d->fileIOCountTotal)
964     {
965         slotSetUIEnabled(true);
966 
967         // any errors?
968 
969         QList<QPair<QUrl, QString> > errorList;
970 
971         for (int i = 0 ; i < d->fileIOFuture.resultCount() ; ++i)
972         {
973             if (!d->fileIOFuture.resultAt(i).second.isEmpty())
974             {
975                 errorList << d->fileIOFuture.resultAt(i);
976             }
977 
978             if (!d->iface->supportAlbums())
979             {
980                 // To rescan item metadata from host.
981 
982                 emit signalMetadataChangedForUrl(d->fileIOFuture.resultAt(i).first);
983             }
984         }
985 
986         if (!errorList.isEmpty())
987         {
988             QStringList errorStrings;
989 
990             for (int i = 0 ; i < errorList.count() ; ++i)
991             {
992                 errorStrings << QString::fromLatin1("%1: %2")
993                     .arg(errorList.at(i).first.toLocalFile())
994                     .arg(errorList.at(i).second);
995             }
996 
997             DMessageBox::showInformationList(QMessageBox::Critical,
998                                              this,
999                                              i18nc("@title", "Error"),
1000                                              i18nc("@info", "Failed to save some information:"),
1001                                              errorStrings);
1002         }
1003 
1004         // done saving files
1005 
1006         if (d->fileIOCloseAfterSaving)
1007         {
1008             close();
1009         }
1010     }
1011 }
1012 
slotApplyClicked()1013 void GeolocationEdit::slotApplyClicked()
1014 {
1015     // save the changes, but do not close afterwards
1016 
1017     saveChanges(false);
1018 }
1019 
slotProgressChanged(const int currentProgress)1020 void GeolocationEdit::slotProgressChanged(const int currentProgress)
1021 {
1022     d->progressBar->setProgressValue(currentProgress);
1023 }
1024 
slotProgressSetup(const int maxProgress,const QString & progressText)1025 void GeolocationEdit::slotProgressSetup(const int maxProgress, const QString& progressText)
1026 {
1027     d->progressBar->setProgressText(progressText);
1028     d->progressBar->setProgressTotalSteps(maxProgress);
1029     d->progressBar->setProgressValue(0);
1030     d->progressBar->setNotify(true);
1031     d->progressBar->setNotificationTitle(i18nc("@title", "Edit Geolocation"), QIcon::fromTheme(QLatin1String("globe")));
1032     d->progressBar->setVisible(true);
1033     d->progressCancelButton->setVisible(d->progressCancelObject != nullptr);
1034 }
1035 
slotGPSUndoCommand(GPSUndoCommand * undoCommand)1036 void GeolocationEdit::slotGPSUndoCommand(GPSUndoCommand* undoCommand)
1037 {
1038     d->undoStack->push(undoCommand);
1039 }
1040 
slotSortOptionTriggered(QAction *)1041 void GeolocationEdit::slotSortOptionTriggered(QAction* /*sortAction*/)
1042 {
1043     int newSortKey = 0;
1044 
1045     if (d->sortActionOldestFirst->isChecked())
1046     {
1047         newSortKey |= 1;
1048     }
1049 
1050     d->mapWidget->setSortKey(newSortKey);
1051 }
1052 
slotProgressCancelButtonClicked()1053 void GeolocationEdit::slotProgressCancelButtonClicked()
1054 {
1055     if (d->progressCancelObject)
1056     {
1057         QTimer::singleShot(0, d->progressCancelObject, d->progressCancelSlot.toUtf8().constData());
1058 
1059         d->progressBar->setProgressValue(d->progressBar->progressTotalSteps());
1060     }
1061 }
1062 
slotBookmarkVisibilityToggled()1063 void GeolocationEdit::slotBookmarkVisibilityToggled()
1064 {
1065     d->bookmarkOwner->bookmarkModelHelper()->setVisible(d->actionBookmarkVisibility->isChecked());
1066 }
1067 
slotLayoutChanged(int lay)1068 void GeolocationEdit::slotLayoutChanged(int lay)
1069 {
1070     d->mapLayout = (MapLayout)lay;
1071     adjustMapLayout(true);
1072 }
1073 
slotTrackListChanged(const GeoCoordinates & coordinate)1074 void GeolocationEdit::slotTrackListChanged(const GeoCoordinates& coordinate)
1075 {
1076     d->mapWidget->setCenter(coordinate);
1077 }
1078 
makeMapWidget(QWidget ** const pvbox)1079 MapWidget* GeolocationEdit::makeMapWidget(QWidget** const pvbox)
1080 {
1081     QWidget* const dummyWidget = new QWidget(this);
1082     QVBoxLayout* const vbox    = new QVBoxLayout(dummyWidget);
1083     MapWidget* const mapWidget = new MapWidget(dummyWidget);
1084     mapWidget->setAvailableMouseModes(MouseModePan | MouseModeZoomIntoGroup | MouseModeSelectThumbnail);
1085     mapWidget->setVisibleMouseModes(MouseModePan | MouseModeZoomIntoGroup | MouseModeSelectThumbnail);
1086     mapWidget->setMouseMode(MouseModeSelectThumbnail);
1087     mapWidget->setGroupedModel(d->geoifaceMarkerModel);
1088     mapWidget->setDragDropHandler(d->mapDragDropHandler);
1089     mapWidget->addUngroupedModel(d->bookmarkOwner->bookmarkModelHelper());
1090     mapWidget->addUngroupedModel(d->searchWidget->getModelHelper());
1091     mapWidget->setTrackManager(d->trackManager);
1092     mapWidget->setSortOptionsMenu(d->sortMenu);
1093 
1094     vbox->addWidget(mapWidget);
1095     vbox->addWidget(mapWidget->getControlWidget());
1096 
1097     QToolButton* const bookmarkVisibilityButton = new QToolButton(mapWidget);
1098     bookmarkVisibilityButton->setDefaultAction(d->actionBookmarkVisibility);
1099     mapWidget->addWidgetToControlWidget(bookmarkVisibilityButton);
1100 
1101     *pvbox = dummyWidget;
1102 
1103     return mapWidget;
1104 }
1105 
adjustMapLayout(const bool syncSettings)1106 void GeolocationEdit::adjustMapLayout(const bool syncSettings)
1107 {
1108     if (d->mapLayout == MapLayoutOne)
1109     {
1110         if (d->mapSplitter->count() > 1)
1111         {
1112             delete d->mapSplitter->widget(1);
1113             d->mapWidget2 = nullptr;
1114         }
1115     }
1116     else
1117     {
1118         if (d->mapSplitter->count() == 1)
1119         {
1120             QWidget* mapHolder = nullptr;
1121             d->mapWidget2      = makeMapWidget(&mapHolder);
1122             d->mapSplitter->addWidget(mapHolder);
1123 
1124             if (syncSettings)
1125             {
1126                 KSharedConfig::Ptr config         = KSharedConfig::openConfig();
1127                 KConfigGroup group                = config->group("Geolocation Edit Settings");
1128                 const KConfigGroup groupMapWidget = KConfigGroup(&group, "Map Widget");
1129                 d->mapWidget2->readSettingsFromGroup(&groupMapWidget);
1130                 d->mapWidget2->setActive(true);
1131             }
1132         }
1133 
1134         if (d->mapLayout == MapLayoutHorizontal)
1135         {
1136             d->mapSplitter->setOrientation(Qt::Horizontal);
1137         }
1138         else
1139         {
1140             d->mapSplitter->setOrientation(Qt::Vertical);
1141         }
1142     }
1143 }
1144 
1145 } // namespace DigikamGenericGeolocationEditPlugin
1146