File indexing completed on 2024-12-01 09:39:16
0001 /* 0002 SPDX-FileCopyrightText: 2001 Jason Harris <jharris@30doradus.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "finddialog.h" 0008 0009 #include "kstars.h" 0010 #include "kstarsdata.h" 0011 #include "ksnotification.h" 0012 #include "Options.h" 0013 #include "detaildialog.h" 0014 #include "skymap.h" 0015 #include "skyobjects/skyobject.h" 0016 #include "skycomponents/starcomponent.h" 0017 #include "skycomponents/skymapcomposite.h" 0018 #include "tools/nameresolver.h" 0019 #include "skyobjectlistmodel.h" 0020 #include "catalogscomponent.h" 0021 #include <KMessageBox> 0022 0023 #include <QSortFilterProxyModel> 0024 #include <QStringListModel> 0025 #include <QTimer> 0026 #include <QComboBox> 0027 #include <QLineEdit> 0028 #include <QPointer> 0029 0030 FindDialog *FindDialog::m_Instance = nullptr; 0031 0032 FindDialogUI::FindDialogUI(QWidget *parent) : QFrame(parent) 0033 { 0034 setupUi(this); 0035 0036 FilterType->addItem(i18n("Any")); 0037 FilterType->addItem(i18n("Stars")); 0038 FilterType->addItem(i18n("Solar System")); 0039 FilterType->addItem(i18n("Open Clusters")); 0040 FilterType->addItem(i18n("Globular Clusters")); 0041 FilterType->addItem(i18n("Gaseous Nebulae")); 0042 FilterType->addItem(i18n("Planetary Nebulae")); 0043 FilterType->addItem(i18n("Galaxies")); 0044 FilterType->addItem(i18n("Comets")); 0045 FilterType->addItem(i18n("Asteroids")); 0046 FilterType->addItem(i18n("Constellations")); 0047 FilterType->addItem(i18n("Supernovae")); 0048 FilterType->addItem(i18n("Satellites")); 0049 0050 SearchList->setMinimumWidth(256); 0051 SearchList->setMinimumHeight(320); 0052 } 0053 0054 FindDialog *FindDialog::Instance() 0055 { 0056 if (m_Instance == nullptr) 0057 m_Instance = new FindDialog(KStars::Instance()); 0058 0059 return m_Instance; 0060 } 0061 0062 FindDialog::FindDialog(QWidget *parent) 0063 : QDialog(parent) 0064 , timer(nullptr) 0065 , m_targetObject(nullptr) 0066 , m_dbManager(CatalogsDB::dso_db_path()) 0067 { 0068 #ifdef Q_OS_OSX 0069 setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); 0070 #endif 0071 ui = new FindDialogUI(this); 0072 0073 setWindowTitle(i18nc("@title:window", "Find Object")); 0074 0075 QVBoxLayout *mainLayout = new QVBoxLayout; 0076 mainLayout->addWidget(ui); 0077 setLayout(mainLayout); 0078 0079 QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); 0080 mainLayout->addWidget(buttonBox); 0081 connect(buttonBox, SIGNAL(accepted()), this, SLOT(slotOk())); 0082 connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); 0083 0084 okB = buttonBox->button(QDialogButtonBox::Ok); 0085 okB->setEnabled(false); 0086 0087 QPushButton *detailB = new QPushButton(i18n("Details...")); 0088 buttonBox->addButton(detailB, QDialogButtonBox::ActionRole); 0089 connect(detailB, SIGNAL(clicked()), this, SLOT(slotDetails())); 0090 0091 ui->InternetSearchButton->setVisible(Options::resolveNamesOnline()); 0092 ui->InternetSearchButton->setEnabled(false); 0093 connect(ui->InternetSearchButton, SIGNAL(clicked()), this, SLOT(slotResolve())); 0094 0095 ui->FilterType->setCurrentIndex(0); // show all types of objects 0096 0097 fModel = new SkyObjectListModel(this); 0098 connect(KStars::Instance()->map(), &SkyMap::removeSkyObject, fModel, &SkyObjectListModel::removeSkyObject); 0099 sortModel = new QSortFilterProxyModel(ui->SearchList); 0100 sortModel->setFilterCaseSensitivity(Qt::CaseInsensitive); 0101 sortModel->setSourceModel(fModel); 0102 sortModel->setSortRole(Qt::DisplayRole); 0103 sortModel->setFilterRole(Qt::DisplayRole); 0104 sortModel->setDynamicSortFilter(true); 0105 sortModel->sort(0); 0106 0107 ui->SearchList->setModel(sortModel); 0108 0109 // Connect signals to slots 0110 connect(ui->clearHistoryB, &QPushButton::clicked, [&]() 0111 { 0112 ui->clearHistoryB->setEnabled(false); 0113 m_HistoryCombo->clear(); 0114 m_HistoryList.clear(); 0115 }); 0116 0117 m_HistoryCombo = new QComboBox(ui->showHistoryB); 0118 m_HistoryCombo->move(0, ui->showHistoryB->height()); 0119 connect(ui->showHistoryB, &QPushButton::clicked, [&]() 0120 { 0121 if (m_HistoryList.empty() == false) 0122 { 0123 m_HistoryCombo->showPopup(); 0124 } 0125 }); 0126 0127 connect(m_HistoryCombo, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), 0128 [&](int index) 0129 { 0130 m_targetObject = m_HistoryList[index]; 0131 m_targetObject->updateCoordsNow(KStarsData::Instance()->updateNum()); 0132 m_HistoryCombo->setCurrentIndex(-1); 0133 m_HistoryCombo->hidePopup(); 0134 accept(); 0135 }); 0136 connect(ui->SearchBox, &QLineEdit::textChanged, this, &FindDialog::enqueueSearch); 0137 connect(ui->SearchBox, &QLineEdit::returnPressed, this, &FindDialog::slotOk); 0138 connect(ui->FilterType, &QComboBox::currentTextChanged, this, &FindDialog::enqueueSearch); 0139 connect(ui->SearchList, SIGNAL(doubleClicked(QModelIndex)), SLOT(slotOk())); 0140 connect(ui->SearchList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FindDialog::slotUpdateButtons); 0141 0142 // Set focus to object name edit 0143 ui->SearchBox->setFocus(); 0144 0145 // First create and paint dialog and then load list 0146 QTimer::singleShot(0, this, SLOT(init())); 0147 0148 listFiltered = false; 0149 } 0150 0151 void FindDialog::init() 0152 { 0153 const auto &objs = m_dbManager.get_objects(Options::magLimitDrawDeepSky(), 100); 0154 for (const auto &obj : objs) 0155 { 0156 KStarsData::Instance()->skyComposite()->catalogsComponent()->insertStaticObject( 0157 obj); 0158 } 0159 ui->SearchBox->clear(); 0160 filterByType(); 0161 sortModel->sort(0); 0162 initSelection(); 0163 m_targetObject = nullptr; 0164 } 0165 0166 void FindDialog::showEvent(QShowEvent *e) 0167 { 0168 ui->SearchBox->setFocus(); 0169 e->accept(); 0170 } 0171 0172 void FindDialog::initSelection() 0173 { 0174 if (sortModel->rowCount() <= 0) 0175 { 0176 okB->setEnabled(false); 0177 return; 0178 } 0179 0180 // ui->SearchBox->setModel(sortModel); 0181 // ui->SearchBox->setModelColumn(0); 0182 0183 if (ui->SearchBox->text().isEmpty()) 0184 { 0185 //Pre-select the first item 0186 QModelIndex selectItem = sortModel->index(0, sortModel->filterKeyColumn(), QModelIndex()); 0187 switch (ui->FilterType->currentIndex()) 0188 { 0189 case 0: //All objects, choose Andromeda galaxy 0190 { 0191 QModelIndex qmi = fModel->index(fModel->indexOf(i18n("Andromeda Galaxy"))); 0192 selectItem = sortModel->mapFromSource(qmi); 0193 break; 0194 } 0195 case 1: //Stars, choose Aldebaran 0196 { 0197 QModelIndex qmi = fModel->index(fModel->indexOf(i18n("Aldebaran"))); 0198 selectItem = sortModel->mapFromSource(qmi); 0199 break; 0200 } 0201 case 2: //Solar system or Asteroids, choose Aaltje 0202 case 9: 0203 { 0204 QModelIndex qmi = fModel->index(fModel->indexOf(i18n("Aaltje"))); 0205 selectItem = sortModel->mapFromSource(qmi); 0206 break; 0207 } 0208 case 8: //Comets, choose 'Aarseth-Brewington (1989 W1)' 0209 { 0210 QModelIndex qmi = fModel->index(fModel->indexOf(i18n("Aarseth-Brewington (1989 W1)"))); 0211 selectItem = sortModel->mapFromSource(qmi); 0212 break; 0213 } 0214 } 0215 0216 if (selectItem.isValid()) 0217 { 0218 ui->SearchList->selectionModel()->select(selectItem, QItemSelectionModel::ClearAndSelect); 0219 ui->SearchList->scrollTo(selectItem); 0220 ui->SearchList->setCurrentIndex(selectItem); 0221 } 0222 } 0223 0224 listFiltered = true; 0225 } 0226 0227 void FindDialog::filterByType() 0228 { 0229 KStarsData *data = KStarsData::Instance(); 0230 0231 switch (ui->FilterType->currentIndex()) 0232 { 0233 case 0: // All object types 0234 { 0235 QVector<QPair<QString, const SkyObject *>> allObjects; 0236 foreach (int type, data->skyComposite()->objectLists().keys()) 0237 { 0238 allObjects.append(data->skyComposite()->objectLists(SkyObject::TYPE(type))); 0239 } 0240 fModel->setSkyObjectsList(allObjects); 0241 break; 0242 } 0243 case 1: //Stars 0244 { 0245 QVector<QPair<QString, const SkyObject *>> starObjects; 0246 starObjects.append(data->skyComposite()->objectLists(SkyObject::STAR)); 0247 starObjects.append(data->skyComposite()->objectLists(SkyObject::CATALOG_STAR)); 0248 fModel->setSkyObjectsList(starObjects); 0249 break; 0250 } 0251 case 2: //Solar system 0252 { 0253 QVector<QPair<QString, const SkyObject *>> ssObjects; 0254 ssObjects.append(data->skyComposite()->objectLists(SkyObject::PLANET)); 0255 ssObjects.append(data->skyComposite()->objectLists(SkyObject::COMET)); 0256 ssObjects.append(data->skyComposite()->objectLists(SkyObject::ASTEROID)); 0257 ssObjects.append(data->skyComposite()->objectLists(SkyObject::MOON)); 0258 0259 fModel->setSkyObjectsList(ssObjects); 0260 break; 0261 } 0262 case 3: //Open Clusters 0263 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::OPEN_CLUSTER)); 0264 break; 0265 case 4: //Globular Clusters 0266 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::GLOBULAR_CLUSTER)); 0267 break; 0268 case 5: //Gaseous nebulae 0269 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::GASEOUS_NEBULA)); 0270 break; 0271 case 6: //Planetary nebula 0272 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::PLANETARY_NEBULA)); 0273 break; 0274 case 7: //Galaxies 0275 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::GALAXY)); 0276 break; 0277 case 8: //Comets 0278 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::COMET)); 0279 break; 0280 case 9: //Asteroids 0281 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::ASTEROID)); 0282 break; 0283 case 10: //Constellations 0284 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::CONSTELLATION)); 0285 break; 0286 case 11: //Supernovae 0287 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::SUPERNOVA)); 0288 break; 0289 case 12: //Satellites 0290 fModel->setSkyObjectsList(data->skyComposite()->objectLists(SkyObject::SATELLITE)); 0291 break; 0292 } 0293 } 0294 0295 void FindDialog::filterList() 0296 { 0297 QString SearchText = processSearchText(); 0298 //const std::size_t searchId = m_currentSearchSequence; 0299 0300 // JM 2022.08.28: Disabling use of async DB manager until further notice since it appears to cause a crash 0301 // on MacOS and some embedded systems. 0302 // QEventLoop loop; 0303 // QMutexLocker {&dbCallMutex}; // To prevent re-entrant calls into this 0304 // connect(m_asyncDBManager.get(), &CatalogsDB::AsyncDBManager::resultReady, &loop, &QEventLoop::quit); 0305 // QMetaObject::invokeMethod(m_asyncDBManager.get(), [&](){ 0306 // m_asyncDBManager->find_objects_by_name(SearchText, 10); }); 0307 // loop.exec(); 0308 // std::unique_ptr<CatalogsDB::CatalogObjectList> objs = m_asyncDBManager->result(); 0309 // if (m_currentSearchSequence != searchId) { 0310 // return; // Ignore this search since the search text has changed 0311 // } 0312 0313 auto objs = m_dbManager.find_objects_by_name(SearchText, 10); 0314 0315 bool exactMatchExists = objs.size() > 0 ? QString::compare(objs.front().name(), SearchText, Qt::CaseInsensitive) : false; 0316 0317 for (const auto &obj : objs) 0318 { 0319 KStarsData::Instance()->skyComposite()->catalogsComponent()->insertStaticObject( 0320 obj); 0321 } 0322 0323 sortModel->setFilterFixedString(SearchText); 0324 ui->InternetSearchButton->setText(i18n("Search the Internet for %1", SearchText.isEmpty() ? i18nc("no text to search for", 0325 "(nothing)") : SearchText)); 0326 filterByType(); 0327 initSelection(); 0328 0329 bool enableInternetSearch = (!exactMatchExists) && (ui->FilterType->currentIndex() == 0); 0330 //Select the first item in the list that begins with the filter string 0331 if (!SearchText.isEmpty()) 0332 { 0333 QStringList mItems = 0334 fModel->filter(QRegExp('^' + SearchText, Qt::CaseInsensitive)); 0335 mItems.sort(); 0336 0337 if (mItems.size()) 0338 { 0339 QModelIndex qmi = fModel->index(fModel->indexOf(mItems[0])); 0340 QModelIndex selectItem = sortModel->mapFromSource(qmi); 0341 0342 if (selectItem.isValid()) 0343 { 0344 ui->SearchList->selectionModel()->select( 0345 selectItem, QItemSelectionModel::ClearAndSelect); 0346 ui->SearchList->scrollTo(selectItem); 0347 ui->SearchList->setCurrentIndex(selectItem); 0348 } 0349 } 0350 ui->InternetSearchButton->setEnabled(enableInternetSearch && !mItems.contains( 0351 SearchText, Qt::CaseInsensitive)); // Disable searching the internet when an exact match for SearchText exists in KStars 0352 } 0353 else 0354 ui->InternetSearchButton->setEnabled(false); 0355 0356 listFiltered = true; 0357 slotUpdateButtons(); 0358 } 0359 0360 void FindDialog::slotUpdateButtons() 0361 { 0362 okB->setEnabled(ui->SearchList->selectionModel()->hasSelection()); 0363 0364 if (okB->isEnabled()) 0365 { 0366 okB->setDefault(true); 0367 } 0368 else if (ui->InternetSearchButton->isEnabled()) 0369 { 0370 ui->InternetSearchButton->setDefault(true); 0371 } 0372 } 0373 0374 SkyObject *FindDialog::selectedObject() const 0375 { 0376 QModelIndex i = ui->SearchList->currentIndex(); 0377 QVariant sObj = sortModel->data(sortModel->index(i.row(), 0), SkyObjectListModel::SkyObjectRole); 0378 0379 return reinterpret_cast<SkyObject*>(sObj.value<void *>()); 0380 } 0381 0382 void FindDialog::enqueueSearch() 0383 { 0384 listFiltered = false; 0385 if (timer) 0386 { 0387 timer->stop(); 0388 } 0389 else 0390 { 0391 timer = new QTimer(this); 0392 timer->setSingleShot(true); 0393 connect(timer, &QTimer::timeout, [&]() 0394 { 0395 this->m_currentSearchSequence++; 0396 this->filterList(); 0397 }); 0398 } 0399 timer->start(500); 0400 } 0401 0402 // Process the search box text to replace equivalent names like "m93" with "m 93" 0403 QString FindDialog::processSearchText(QString searchText) 0404 { 0405 QRegExp re; 0406 re.setCaseSensitivity(Qt::CaseInsensitive); 0407 0408 // Remove multiple spaces and replace them by a single space 0409 re.setPattern(" +"); 0410 searchText.replace(re, " "); 0411 0412 // If it is an NGC/IC/M catalog number, as in "M 76" or "NGC 5139", check for absence of the space 0413 re.setPattern("^(m|ngc|ic)\\s*\\d*$"); 0414 if (searchText.contains(re)) 0415 { 0416 re.setPattern("\\s*(\\d+)"); 0417 searchText.replace(re, " \\1"); 0418 re.setPattern("\\s*$"); 0419 searchText.remove(re); 0420 re.setPattern("^\\s*"); 0421 searchText.remove(re); 0422 } 0423 0424 // If it is a comet, and starts with c20## or c 20## make it c/20## (or similar with p). 0425 re.setPattern("^(c|p)\\s*((19|20).*)"); 0426 if (searchText.contains(re)) 0427 { 0428 if (searchText.at(0) == 'c' || searchText.at(0) == 'C') 0429 searchText.replace(re, "c/\\2"); 0430 else searchText.replace(re, "p/\\2"); 0431 } 0432 0433 // TODO after KDE 4.1 release: 0434 // If it is a IAU standard three letter abbreviation for a constellation, then go to that constellation 0435 // Check for genetive names of stars. Example: alp CMa must go to alpha Canis Majoris 0436 0437 return searchText; 0438 } 0439 0440 void FindDialog::slotOk() 0441 { 0442 // JM 2022.04.20 Below does not work when a user is simply browsing 0443 // and selecting an item without entering any text in the search box. 0444 //If no valid object selected, show a sorry-box. Otherwise, emit accept() 0445 // if (ui->SearchBox->text().isEmpty()) 0446 // { 0447 // return; 0448 // } 0449 SkyObject *selObj; 0450 if (!listFiltered) 0451 { 0452 filterList(); 0453 } 0454 selObj = selectedObject(); 0455 finishProcessing(selObj, Options::resolveNamesOnline() && ui->InternetSearchButton->isEnabled()); 0456 } 0457 0458 void FindDialog::slotResolve() 0459 { 0460 finishProcessing(nullptr, true); 0461 } 0462 0463 CatalogObject *FindDialog::resolveAndAdd(CatalogsDB::DBManager &db_manager, const QString &query) 0464 { 0465 CatalogObject *dso = nullptr; 0466 const auto &cedata = NameResolver::resolveName(query); 0467 0468 if (cedata.first) 0469 { 0470 db_manager.add_object(CatalogsDB::user_catalog_id, cedata.second); 0471 const auto &added_object = 0472 db_manager.get_object(cedata.second.getId(), CatalogsDB::user_catalog_id); 0473 0474 if (added_object.first) 0475 { 0476 dso = &KStarsData::Instance() 0477 ->skyComposite() 0478 ->catalogsComponent() 0479 ->insertStaticObject(added_object.second); 0480 } 0481 } 0482 return dso; 0483 } 0484 0485 void FindDialog::finishProcessing(SkyObject *selObj, bool resolve) 0486 { 0487 if (!selObj && resolve) 0488 { 0489 selObj = resolveAndAdd(m_dbManager, processSearchText()); 0490 } 0491 m_targetObject = selObj; 0492 if (selObj == nullptr) 0493 { 0494 QString message = i18n("No object named %1 found.", ui->SearchBox->text()); 0495 KSNotification::sorry(message, i18n("Bad object name")); 0496 } 0497 else 0498 { 0499 selObj->updateCoordsNow(KStarsData::Instance()->updateNum()); 0500 if (m_HistoryList.contains(selObj) == false) 0501 { 0502 switch (selObj->type()) 0503 { 0504 case SkyObject::OPEN_CLUSTER: 0505 case SkyObject::GLOBULAR_CLUSTER: 0506 case SkyObject::GASEOUS_NEBULA: 0507 case SkyObject::PLANETARY_NEBULA: 0508 case SkyObject::SUPERNOVA_REMNANT: 0509 case SkyObject::GALAXY: 0510 if (selObj->name() != selObj->longname()) 0511 m_HistoryCombo->addItem(QString("%1 (%2)") 0512 .arg(selObj->name()) 0513 .arg(selObj->longname())); 0514 else 0515 m_HistoryCombo->addItem(QString("%1").arg(selObj->longname())); 0516 break; 0517 0518 case SkyObject::STAR: 0519 case SkyObject::CATALOG_STAR: 0520 case SkyObject::PLANET: 0521 case SkyObject::COMET: 0522 case SkyObject::ASTEROID: 0523 case SkyObject::CONSTELLATION: 0524 case SkyObject::MOON: 0525 case SkyObject::ASTERISM: 0526 case SkyObject::GALAXY_CLUSTER: 0527 case SkyObject::DARK_NEBULA: 0528 case SkyObject::QUASAR: 0529 case SkyObject::MULT_STAR: 0530 case SkyObject::RADIO_SOURCE: 0531 case SkyObject::SATELLITE: 0532 case SkyObject::SUPERNOVA: 0533 default: 0534 m_HistoryCombo->addItem(QString("%1").arg(selObj->longname())); 0535 break; 0536 } 0537 0538 m_HistoryList.append(selObj); 0539 } 0540 ui->clearHistoryB->setEnabled(true); 0541 accept(); 0542 } 0543 } 0544 void FindDialog::keyPressEvent(QKeyEvent *e) 0545 { 0546 switch (e->key()) 0547 { 0548 case Qt::Key_Escape: 0549 reject(); 0550 break; 0551 case Qt::Key_Up: 0552 { 0553 int currentRow = ui->SearchList->currentIndex().row(); 0554 if (currentRow > 0) 0555 { 0556 QModelIndex selectItem = sortModel->index(currentRow - 1, sortModel->filterKeyColumn(), QModelIndex()); 0557 ui->SearchList->selectionModel()->setCurrentIndex(selectItem, QItemSelectionModel::SelectCurrent); 0558 } 0559 break; 0560 } 0561 case Qt::Key_Down: 0562 { 0563 int currentRow = ui->SearchList->currentIndex().row(); 0564 if (currentRow < sortModel->rowCount() - 1) 0565 { 0566 QModelIndex selectItem = sortModel->index(currentRow + 1, sortModel->filterKeyColumn(), QModelIndex()); 0567 ui->SearchList->selectionModel()->setCurrentIndex(selectItem, QItemSelectionModel::SelectCurrent); 0568 } 0569 break; 0570 } 0571 } 0572 } 0573 0574 void FindDialog::slotDetails() 0575 { 0576 if (selectedObject()) 0577 { 0578 QPointer<DetailDialog> dd = new DetailDialog(selectedObject(), KStarsData::Instance()->ut(), 0579 KStarsData::Instance()->geo(), KStars::Instance()); 0580 dd->exec(); 0581 delete dd; 0582 } 0583 } 0584 0585 int FindDialog::execWithParent(QWidget* parent) 0586 { 0587 QWidget * const oldParent = parentWidget(); 0588 0589 if (nullptr != parent) 0590 { 0591 setParent(parent); 0592 setWindowFlag(Qt::Dialog, true); 0593 } 0594 0595 int const result = QDialog::exec(); 0596 0597 if (nullptr != parent) 0598 { 0599 setParent(oldParent); 0600 setWindowFlag(Qt::Dialog, true); 0601 } 0602 0603 return result; 0604 }