File indexing completed on 2024-03-24 15:15:33

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 }