File indexing completed on 2025-01-19 03:51:20

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2010-06-01
0007  * Description : A widget to search for places.
0008  *
0009  * SPDX-FileCopyrightText: 2010-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2010-2011 by Michael G. Hansen <mike at mghansen dot de>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "searchresultwidget.h"
0017 
0018 // Qt includes
0019 
0020 #include <QContextMenuEvent>
0021 #include <QListView>
0022 #include <QPainter>
0023 #include <QPushButton>
0024 #include <QToolButton>
0025 #include <QTreeView>
0026 #include <QVBoxLayout>
0027 #include <QMenu>
0028 #include <QAction>
0029 #include <QComboBox>
0030 #include <QStandardPaths>
0031 #include <QLineEdit>
0032 #include <QMessageBox>
0033 #include <QItemSelectionModel>
0034 #include <QItemSelection>
0035 
0036 // KDE includes
0037 
0038 #include <kconfiggroup.h>
0039 #include <klocalizedstring.h>
0040 
0041 // Local includes
0042 
0043 #include "dlayoutbox.h"
0044 #include "searchresultmodel.h"
0045 #include "searchresultmodelhelper.h"
0046 #include "gpscommon.h"
0047 #include "gpsitemmodel.h"
0048 
0049 #ifdef GPSSYNC_MODELTEST
0050 #   include <modeltest.h>
0051 #endif
0052 
0053 using namespace Digikam;
0054 
0055 namespace DigikamGenericGeolocationEditPlugin
0056 {
0057 
0058 static int QItemSelectionModel_selectedRowsCount(const QItemSelectionModel* const selectionModel)
0059 {
0060     if (!selectionModel->hasSelection())
0061     {
0062         return 0;
0063     }
0064 
0065     return selectionModel->selectedRows().count();
0066 }
0067 
0068 class Q_DECL_HIDDEN SearchResultWidget::Private
0069 {
0070 public:
0071 
0072     explicit Private()
0073     {
0074         gpsBookmarkOwner                              = nullptr;
0075         actionBookmark                                = nullptr;
0076         mapWidget                                     = nullptr;
0077         gpsItemModel                                  = nullptr;
0078         gosImageSelectionModel                        = nullptr;
0079         searchTermLineEdit                            = nullptr;
0080         searchButton                                  = nullptr;
0081         searchBackend                                 = nullptr;
0082         searchResultsModel                            = nullptr;
0083         searchResultsSelectionModel                   = nullptr;
0084         searchResultModelHelper                       = nullptr;
0085         treeView                                      = nullptr;
0086         mainVBox                                      = nullptr;
0087         backendSelectionBox                           = nullptr;
0088         actionClearResultsList                        = nullptr;
0089         actionKeepOldResults                          = nullptr;
0090         actionToggleAllResultsVisibility              = nullptr;
0091         actionCopyCoordinates                         = nullptr;
0092         actionMoveImagesToThisResult                  = nullptr;
0093         actionRemovedSelectedSearchResultsFromList    = nullptr;
0094         searchInProgress                              = false;
0095         actionToggleAllResultsVisibilityIconUnchecked = QIcon::fromTheme(QLatin1String("layer-visible-off"));
0096         actionToggleAllResultsVisibilityIconChecked   = QIcon::fromTheme(QLatin1String("layer-visible-on"));
0097     }
0098 
0099     // Map
0100     MapWidget*               mapWidget;
0101     GPSItemModel*            gpsItemModel;
0102     QItemSelectionModel*     gosImageSelectionModel;
0103     QLineEdit*               searchTermLineEdit;
0104     QPushButton*             searchButton;
0105     GPSBookmarkOwner*        gpsBookmarkOwner;
0106     QAction*                 actionBookmark;
0107 
0108     // Search: backend
0109     SearchResultBackend*     searchBackend;
0110     SearchResultModel*       searchResultsModel;
0111     QItemSelectionModel*     searchResultsSelectionModel;
0112     SearchResultModelHelper* searchResultModelHelper;
0113 
0114     // Search: UI
0115     QTreeView*               treeView;
0116     QVBoxLayout*             mainVBox;
0117     QComboBox*               backendSelectionBox;
0118     QAction*                 actionClearResultsList;
0119     QAction*                 actionKeepOldResults;
0120     QAction*                 actionToggleAllResultsVisibility;
0121     QAction*                 actionCopyCoordinates;
0122     QAction*                 actionMoveImagesToThisResult;
0123     QAction*                 actionRemovedSelectedSearchResultsFromList;
0124     bool                     searchInProgress;
0125     QIcon                    actionToggleAllResultsVisibilityIconUnchecked;
0126     QIcon                    actionToggleAllResultsVisibilityIconChecked;
0127 };
0128 
0129 SearchResultWidget::SearchResultWidget(GPSBookmarkOwner* const gpsBookmarkOwner,
0130                            GPSItemModel* const gpsItemModel,
0131                            QItemSelectionModel* const gosImageSelectionModel,
0132                            QWidget* const parent)
0133     : QWidget(parent),
0134       d      (new Private())
0135 {
0136     d->gpsBookmarkOwner       = gpsBookmarkOwner;
0137     d->gpsItemModel           = gpsItemModel;
0138     d->gosImageSelectionModel = gosImageSelectionModel;
0139     d->searchBackend          = new SearchResultBackend(this);
0140     d->searchResultsModel     = new SearchResultModel(this);
0141 
0142 #ifdef GPSSYNC_MODELTEST
0143 
0144     new ModelTest(d->searchResultsModel, this);
0145 
0146 #endif
0147 
0148     d->searchResultsSelectionModel = new QItemSelectionModel(d->searchResultsModel);
0149     d->searchResultsModel->setSelectionModel(d->searchResultsSelectionModel);
0150     d->searchResultModelHelper     = new SearchResultModelHelper(d->searchResultsModel, d->searchResultsSelectionModel, d->gpsItemModel, this);
0151 
0152     d->mainVBox           = new QVBoxLayout(this);
0153     setLayout(d->mainVBox);
0154 
0155     DHBox* const topHBox  = new DHBox(this);
0156     d->mainVBox->addWidget(topHBox);
0157     d->searchTermLineEdit = new QLineEdit(topHBox);
0158     d->searchTermLineEdit->setClearButtonEnabled(true);
0159     d->searchButton       = new QPushButton(i18nc("Start the search", "Search"), topHBox);
0160 
0161     DHBox* const actionHBox = new DHBox(this);
0162     d->mainVBox->addWidget(actionHBox);
0163 
0164     d->actionClearResultsList = new QAction(this);
0165     d->actionClearResultsList->setIcon(QIcon::fromTheme(QLatin1String("edit-clear")));
0166     d->actionClearResultsList->setToolTip(i18n("Clear the search results."));
0167     QToolButton* const tbClearResultsList = new QToolButton(actionHBox);
0168     tbClearResultsList->setDefaultAction(d->actionClearResultsList);
0169 
0170     d->actionKeepOldResults = new QAction(this);
0171     d->actionKeepOldResults->setIcon(QIcon::fromTheme(QLatin1String("flag")));
0172     d->actionKeepOldResults->setCheckable(true);
0173     d->actionKeepOldResults->setChecked(false);
0174     d->actionKeepOldResults->setToolTip(i18n("Keep the results of old searches when doing a new search."));
0175     QToolButton* const tbKeepOldResults = new QToolButton(actionHBox);
0176     tbKeepOldResults->setDefaultAction(d->actionKeepOldResults);
0177 
0178     d->actionToggleAllResultsVisibility = new QAction(this);
0179     d->actionToggleAllResultsVisibility->setCheckable(true);
0180     d->actionToggleAllResultsVisibility->setChecked(true);
0181     d->actionToggleAllResultsVisibility->setToolTip(i18n("Toggle the visibility of the search results on the map."));
0182     QToolButton* const tbToggleAllVisibility = new QToolButton(actionHBox);
0183     tbToggleAllVisibility->setDefaultAction(d->actionToggleAllResultsVisibility);
0184 
0185     d->actionCopyCoordinates = new QAction(i18n("Copy coordinates"), this);
0186     d->actionCopyCoordinates->setIcon(QIcon::fromTheme(QLatin1String("edit-copy")));
0187 
0188     d->actionMoveImagesToThisResult = new QAction(i18n("Move selected images to this position"), this);
0189 
0190     d->actionRemovedSelectedSearchResultsFromList = new QAction(i18n("Remove from results list"), this);
0191     d->actionRemovedSelectedSearchResultsFromList->setIcon(QIcon::fromTheme(QLatin1String("list-remove")));
0192 
0193     d->backendSelectionBox                            = new QComboBox(actionHBox);
0194     d->backendSelectionBox->setToolTip(i18n("Select which service you would like to use."));
0195     const QList<QPair<QString, QString> > backendList = d->searchBackend->getBackends();
0196 
0197     for (int i = 0 ; i < backendList.count() ; ++i)
0198     {
0199         d->backendSelectionBox->addItem(backendList.at(i).first, backendList.at(i).second);
0200     }
0201 
0202     // add stretch after the controls:
0203 
0204     QHBoxLayout* const hBoxLayout = reinterpret_cast<QHBoxLayout*>(actionHBox->layout());
0205 
0206     if (hBoxLayout)
0207     {
0208         hBoxLayout->addStretch();
0209     }
0210 
0211     d->treeView = new QTreeView(this);
0212     d->mainVBox->addWidget(d->treeView);
0213     d->treeView->setRootIsDecorated(false);
0214     d->treeView->setModel(d->searchResultsModel);
0215     d->treeView->setSelectionModel(d->searchResultsSelectionModel);
0216     d->treeView->setSelectionMode(QAbstractItemView::ExtendedSelection);
0217 
0218     d->actionBookmark = new QAction(i18n("Bookmarks"), this);
0219     d->actionBookmark->setMenu(d->gpsBookmarkOwner->getMenu());
0220 
0221     connect(d->actionMoveImagesToThisResult, SIGNAL(triggered(bool)),
0222             this, SLOT(slotMoveSelectedImagesToThisResult()));
0223 
0224     connect(d->searchButton, SIGNAL(clicked()),
0225             this, SLOT(slotTriggerSearch()));
0226 
0227     connect(d->searchBackend, SIGNAL(signalSearchCompleted()),
0228             this, SLOT(slotSearchCompleted()));
0229 
0230     connect(d->searchTermLineEdit, SIGNAL(returnPressed()),
0231             this, SLOT(slotTriggerSearch()));
0232 
0233     connect(d->searchTermLineEdit, SIGNAL(textChanged(QString)),
0234             this, SLOT(slotUpdateActionAvailability()));
0235 
0236     connect(d->searchResultsSelectionModel, SIGNAL(currentChanged(QModelIndex,QModelIndex)),
0237             this, SLOT(slotCurrentlySelectedResultChanged(QModelIndex,QModelIndex)));
0238 
0239     connect(d->actionClearResultsList, SIGNAL(triggered(bool)),
0240             this, SLOT(slotClearSearchResults()));
0241 
0242     connect(d->actionToggleAllResultsVisibility, SIGNAL(triggered(bool)),
0243             this, SLOT(slotVisibilityChanged(bool)));
0244 
0245     connect(d->actionCopyCoordinates, SIGNAL(triggered(bool)),
0246             this, SLOT(slotCopyCoordinates()));
0247 
0248     connect(d->searchResultModelHelper, SIGNAL(signalUndoCommand(GPSUndoCommand*)),
0249             this, SIGNAL(signalUndoCommand(GPSUndoCommand*)));
0250 
0251     connect(d->actionRemovedSelectedSearchResultsFromList, SIGNAL(triggered(bool)),
0252             this, SLOT(slotRemoveSelectedFromResultsList()));
0253 
0254     d->treeView->installEventFilter(this);
0255 
0256     slotUpdateActionAvailability();
0257 }
0258 
0259 SearchResultWidget::~SearchResultWidget()
0260 {
0261     delete d;
0262 }
0263 
0264 void SearchResultWidget::slotSearchCompleted()
0265 {
0266     d->searchInProgress       = false;
0267     const QString errorString = d->searchBackend->getErrorMessage();
0268 
0269     if (!errorString.isEmpty())
0270     {
0271         QMessageBox::critical(this, i18nc("@title:window", "Search Failed"), i18n("Your search failed:\n%1", errorString));
0272         slotUpdateActionAvailability();
0273         return;
0274     }
0275 
0276     const SearchResultBackend::SearchResult::List searchResults = d->searchBackend->getResults();
0277     d->searchResultsModel->addResults(searchResults);
0278 
0279     slotUpdateActionAvailability();
0280 }
0281 
0282 void SearchResultWidget::slotTriggerSearch()
0283 {
0284     // this is necessary since this slot is also connected to QLineEdit::returnPressed
0285 
0286     if (d->searchTermLineEdit->text().isEmpty() || d->searchInProgress)
0287     {
0288         return;
0289     }
0290 
0291     if (!d->actionKeepOldResults->isChecked())
0292     {
0293         slotClearSearchResults();
0294     }
0295 
0296     d->searchInProgress = true;
0297 
0298     const QString searchBackendName = d->backendSelectionBox->itemData(d->backendSelectionBox->currentIndex()).toString();
0299     d->searchBackend->search(searchBackendName, d->searchTermLineEdit->text());
0300 
0301     slotUpdateActionAvailability();
0302 }
0303 
0304 GeoModelHelper* SearchResultWidget::getModelHelper() const
0305 {
0306     return d->searchResultModelHelper;
0307 }
0308 
0309 void SearchResultWidget::slotCurrentlySelectedResultChanged(const QModelIndex& current,
0310                                                       const QModelIndex& previous)
0311 {
0312     Q_UNUSED(previous)
0313 
0314     if (!current.isValid())
0315     {
0316         return;
0317     }
0318 
0319     const SearchResultModel::SearchResultItem currentItem = d->searchResultsModel->resultItem(current);
0320 
0321     if (d->mapWidget)
0322     {
0323         d->mapWidget->setCenter(currentItem.result.coordinates);
0324     }
0325 }
0326 
0327 void SearchResultWidget::slotClearSearchResults()
0328 {
0329     d->searchResultsModel->clearResults();
0330 
0331     slotUpdateActionAvailability();
0332 }
0333 
0334 void SearchResultWidget::slotVisibilityChanged(bool state)
0335 {
0336     d->searchResultModelHelper->setVisibility(state);
0337     slotUpdateActionAvailability();
0338 }
0339 
0340 void SearchResultWidget::slotUpdateActionAvailability()
0341 {
0342     const int nSelectedResults       = QItemSelectionModel_selectedRowsCount(d->searchResultsSelectionModel);
0343     const bool haveOneSelectedResult = (nSelectedResults == 1);
0344     const bool haveSelectedImages    = !d->gosImageSelectionModel->selectedRows().isEmpty();
0345 
0346     d->actionCopyCoordinates->setEnabled(haveOneSelectedResult);
0347     d->actionMoveImagesToThisResult->setEnabled(haveOneSelectedResult && haveSelectedImages);
0348     d->actionRemovedSelectedSearchResultsFromList->setEnabled(nSelectedResults>=1);
0349 
0350     const bool haveSearchText        = !d->searchTermLineEdit->text().isEmpty();
0351 
0352     d->searchButton->setEnabled(haveSearchText&&!d->searchInProgress);
0353     d->actionClearResultsList->setEnabled(d->searchResultsModel->rowCount()>0);
0354     d->actionToggleAllResultsVisibility->setIcon(d->actionToggleAllResultsVisibility->isChecked() ? d->actionToggleAllResultsVisibilityIconChecked
0355                                                                                                   : d->actionToggleAllResultsVisibilityIconUnchecked);
0356 }
0357 
0358 bool SearchResultWidget::eventFilter(QObject *watched, QEvent *event)
0359 {
0360     if (watched == d->treeView)
0361     {
0362         // we are only interested in context-menu events
0363 
0364         if (event->type() == QEvent::ContextMenu)
0365         {
0366             if (d->searchResultsSelectionModel->hasSelection())
0367             {
0368                 const QModelIndex currentIndex                         = d->searchResultsSelectionModel->currentIndex();
0369                 const SearchResultModel::SearchResultItem searchResult = d->searchResultsModel->resultItem(currentIndex);
0370                 d->gpsBookmarkOwner->setPositionAndTitle(searchResult.result.coordinates, searchResult.result.name);
0371             }
0372 
0373             slotUpdateActionAvailability();
0374 
0375             // construct the context-menu:
0376 
0377             QMenu* const menu          = new QMenu(d->treeView);
0378             menu->addAction(d->actionCopyCoordinates);
0379             menu->addAction(d->actionMoveImagesToThisResult);
0380             menu->addAction(d->actionRemovedSelectedSearchResultsFromList);
0381 //          menu->addAction(d->actionBookmark);
0382             d->gpsBookmarkOwner->changeAddBookmark(true);
0383 
0384             QContextMenuEvent* const e = static_cast<QContextMenuEvent*>(event);
0385             menu->exec(e->globalPos());
0386             delete menu;
0387         }
0388     }
0389 
0390     return QObject::eventFilter(watched, event);
0391 }
0392 
0393 void SearchResultWidget::slotCopyCoordinates()
0394 {
0395     const QModelIndex currentIndex                        = d->searchResultsSelectionModel->currentIndex();
0396     const SearchResultModel::SearchResultItem currentItem = d->searchResultsModel->resultItem(currentIndex);
0397 
0398     coordinatesToClipboard(currentItem.result.coordinates, QUrl(), currentItem.result.name);
0399 }
0400 
0401 void SearchResultWidget::saveSettingsToGroup(KConfigGroup* const group)
0402 {
0403     group->writeEntry("Keep old results", d->actionKeepOldResults->isChecked());
0404     group->writeEntry("Search backend",   d->backendSelectionBox->itemData(d->backendSelectionBox->currentIndex()).toString());
0405 
0406     slotUpdateActionAvailability();
0407 }
0408 
0409 void SearchResultWidget::readSettingsFromGroup(const KConfigGroup* const group)
0410 {
0411     d->actionKeepOldResults->setChecked(group->readEntry("Keep old results", false));
0412     const QString backendId = group->readEntry("Search backend", "osm");
0413 
0414     for (int i = 0 ; i < d->backendSelectionBox->count() ; ++i)
0415     {
0416         if (d->backendSelectionBox->itemData(i).toString()==backendId)
0417         {
0418             d->backendSelectionBox->setCurrentIndex(i);
0419             break;
0420         }
0421     }
0422 }
0423 
0424 void SearchResultWidget::slotMoveSelectedImagesToThisResult()
0425 {
0426     const QModelIndex currentIndex                        = d->searchResultsSelectionModel->currentIndex();
0427     const SearchResultModel::SearchResultItem currentItem = d->searchResultsModel->resultItem(currentIndex);
0428     const GeoCoordinates& targetCoordinates               = currentItem.result.coordinates;
0429     const QModelIndexList selectedImageIndices            = d->gosImageSelectionModel->selectedRows();
0430 
0431     if (selectedImageIndices.isEmpty())
0432     {
0433         return;
0434     }
0435 
0436     GPSUndoCommand* const undoCommand = new GPSUndoCommand();
0437 
0438     for (int i = 0 ; i < selectedImageIndices.count() ; ++i)
0439     {
0440         const QPersistentModelIndex itemIndex = selectedImageIndices.at(i);
0441         GPSItemContainer* const item          = d->gpsItemModel->itemFromIndex(itemIndex);
0442 
0443         GPSUndoCommand::UndoInfo undoInfo(itemIndex);
0444         undoInfo.readOldDataFromItem(item);
0445 
0446         GPSDataContainer newData;
0447         newData.setCoordinates(targetCoordinates);
0448         item->setGPSData(newData);
0449 
0450         undoInfo.readNewDataFromItem(item);
0451 
0452         undoCommand->addUndoInfo(undoInfo);
0453     }
0454 
0455     undoCommand->setText(i18np("1 image moved to '%2'",
0456                                "%1 images moved to '%2'", selectedImageIndices.count(), currentItem.result.name));
0457 
0458     Q_EMIT signalUndoCommand(undoCommand);
0459 }
0460 
0461 void SearchResultWidget::setPrimaryMapWidget(MapWidget* const mapWidget)
0462 {
0463     d->mapWidget = mapWidget;
0464 }
0465 
0466 void SearchResultWidget::slotRemoveSelectedFromResultsList()
0467 {
0468     const QItemSelection selectedRows = d->searchResultsSelectionModel->selection();
0469 
0470     if (selectedRows.isEmpty())
0471     {
0472         return;
0473     }
0474 
0475     d->searchResultsModel->removeRowsBySelection(selectedRows);
0476 
0477     slotUpdateActionAvailability();
0478 }
0479 
0480 } // namespace DigikamGenericGeolocationEditPlugin
0481 
0482 #include "moc_searchresultwidget.cpp"