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"