1 /*
2  * Stellarium
3  * Copyright (C) 2008 Guillaume Chereau
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA  02110-1335, USA.
18 */
19 
20 #include "Dialog.hpp"
21 #include "SearchDialog.hpp"
22 #include "ui_searchDialogGui.h"
23 #include "StelApp.hpp"
24 #include "StelCore.hpp"
25 #include "StelModuleMgr.hpp"
26 #include "StelMovementMgr.hpp"
27 #include "StelLocaleMgr.hpp"
28 #include "StelTranslator.hpp"
29 #include "Planet.hpp"
30 #include "SpecialMarkersMgr.hpp"
31 #include "CustomObjectMgr.hpp"
32 #include "SolarSystem.hpp"
33 
34 #include "StelObjectMgr.hpp"
35 #include "StelGui.hpp"
36 #include "StelUtils.hpp"
37 
38 #include "StelFileMgr.hpp"
39 #include "StelJsonParser.hpp"
40 
41 #include <QDebug>
42 #include <QFrame>
43 #include <QLabel>
44 #include <QPushButton>
45 #include <QSettings>
46 #include <QString>
47 #include <QStringList>
48 #include <QtAlgorithms>
49 #include <QTextEdit>
50 #include <QLineEdit>
51 #include <QComboBox>
52 #include <QMenu>
53 #include <QMetaEnum>
54 #include <QClipboard>
55 #include <QSortFilterProxyModel>
56 #include <QStringListModel>
57 #include <QFileDialog>
58 #include <QDir>
59 #include <QSet>
60 #include <QDialog>
61 #include <QAbstractItemModel>
62 
63 #include "SimbadSearcher.hpp"
64 
65 // Start of members for class CompletionListModel
CompletionListModel(QObject * parent)66 CompletionListModel::CompletionListModel(QObject* parent):
67 	QStringListModel(parent),
68 	selectedIdx(0)
69 {
70 }
71 
CompletionListModel(const QStringList & string,QObject * parent)72 CompletionListModel::CompletionListModel(const QStringList &string, QObject* parent):
73 	QStringListModel(string, parent),
74 	selectedIdx(0)
75 {
76 }
77 
~CompletionListModel()78 CompletionListModel::~CompletionListModel()
79 {
80 }
81 
setValues(const QStringList & v,const QStringList & rv)82 void CompletionListModel::setValues(const QStringList& v, const QStringList& rv)
83 {
84 	values=v;
85 	recentValues=rv;
86 	updateText();
87 }
88 
appendRecentValues(const QStringList & v)89 void CompletionListModel::appendRecentValues(const QStringList& v)
90 {
91 	recentValues+=v;
92 }
93 
appendValues(const QStringList & v)94 void CompletionListModel::appendValues(const QStringList& v)
95 {
96 	values+=v;
97 	updateText();
98 }
99 
clearValues()100 void CompletionListModel::clearValues()
101 {
102 	// Default: Show recent values
103 	values.clear();
104 	values = recentValues;
105 	selectedIdx=0;
106 	updateText();
107 }
108 
getSelected() const109 QString CompletionListModel::getSelected() const
110 {
111 	if (values.isEmpty())
112 		return QString();
113 	return values.at(selectedIdx);
114 }
115 
selectNext()116 void CompletionListModel::selectNext()
117 {
118 	++selectedIdx;
119 	if (selectedIdx>=values.size())
120 		selectedIdx=0;
121 	updateText();
122 }
123 
selectPrevious()124 void CompletionListModel::selectPrevious()
125 {
126 	--selectedIdx;
127 	if (selectedIdx<0)
128 		selectedIdx = values.size()-1;
129 	updateText();
130 }
131 
selectFirst()132 void CompletionListModel::selectFirst()
133 {
134 	selectedIdx=0;
135 	updateText();
136 }
137 
updateText()138 void CompletionListModel::updateText()
139 {
140 	this->setStringList(values);
141 }
142 
data(const QModelIndex & index,int role) const143 QVariant CompletionListModel::data(const QModelIndex &index, int role) const
144 {
145 	if (!index.isValid())
146 	    return QVariant();
147 
148 	// Bold recent objects
149 	if(role == Qt::FontRole)
150 	{
151 	    QFont font;
152 	    bool toBold = recentValues.contains(index.data(Qt::DisplayRole).toString()) ?
153 				    true : false;
154 	    font.setBold(toBold);
155 	    return font;
156 	}
157 
158 	return QStringListModel::data(index, role);
159 }
160 
161 // Start of members for class SearchDialog
162 
163 const char* SearchDialog::DEF_SIMBAD_URL = "https://simbad.u-strasbg.fr/";
164 SearchDialog::SearchDialogStaticData SearchDialog::staticData;
165 QString SearchDialog::extSearchText = "";
166 
SearchDialog(QObject * parent)167 SearchDialog::SearchDialog(QObject* parent)
168 	: StelDialog("Search", parent)
169 	, simbadReply(Q_NULLPTR)
170 	, listModel(Q_NULLPTR)
171 	, proxyModel(Q_NULLPTR)
172 	, flagHasSelectedText(false)
173 	, shiftPressed(false)
174 {
175 	setObjectName("SearchDialog");
176 	ui = new Ui_searchDialogForm;
177 	simbadSearcher = new SimbadSearcher(this);
178 	objectMgr = GETSTELMODULE(StelObjectMgr);
179 	Q_ASSERT(objectMgr);
180 
181 	StelApp::getInstance().getStelPropertyManager()->registerObject(this);
182 	conf = StelApp::getInstance().getSettings();
183 	enableSimbadSearch(conf->value("search/flag_search_online", true).toBool());
184 	useStartOfWords = conf->value("search/flag_start_words", false).toBool();
185 	useLockPosition = conf->value("search/flag_lock_position", true).toBool();
186 	useFOVCenterMarker = conf->value("search/flag_fov_center_marker", true).toBool();
187 	fovCenterMarkerState = GETSTELMODULE(SpecialMarkersMgr)->getFlagFOVCenterMarker();
188 	simbadServerUrl = conf->value("search/simbad_server_url", DEF_SIMBAD_URL).toString();
189 	setCurrentCoordinateSystemKey(conf->value("search/coordinate_system", "equatorialJ2000").toString());
190 
191 	setSimbadQueryDist( conf->value("search/simbad_query_dist",  30).toInt());
192 	setSimbadQueryCount(conf->value("search/simbad_query_count",  3).toInt());
193 	setSimbadGetsIds(   conf->value("search/simbad_query_IDs",        true ).toBool());
194 	setSimbadGetsSpec(  conf->value("search/simbad_query_spec",       false).toBool());
195 	setSimbadGetsMorpho(conf->value("search/simbad_query_morpho",     false).toBool());
196 	setSimbadGetsTypes( conf->value("search/simbad_query_types",      false).toBool());
197 	setSimbadGetsDims(  conf->value("search/simbad_query_dimensions", false).toBool());
198 
199 	// Init CompletionListModel
200 	searchListModel = new CompletionListModel();
201 
202 	// Find recent object search data file
203 	recentObjectSearchesJsonPath = StelFileMgr::findFile("data", static_cast<StelFileMgr::Flags>(StelFileMgr::Directory | StelFileMgr::Writable)) + "/recentObjectSearches.json";
204 }
205 
~SearchDialog()206 SearchDialog::~SearchDialog()
207 {
208 	delete ui;
209 	if (simbadReply)
210 	{
211 		simbadReply->deleteLater();
212 		simbadReply = Q_NULLPTR;
213 	}
214 }
215 
retranslate()216 void SearchDialog::retranslate()
217 {
218 	if (dialog)
219 	{
220 		QString text(ui->lineEditSearchSkyObject->text());
221 		ui->retranslateUi(dialog);
222 		ui->lineEditSearchSkyObject->setText(text);
223 		populateSimbadServerList();
224 		populateCoordinateSystemsList();
225 		populateCoordinateAxis();
226 		populateRecentSearch();
227 		updateListTab();
228 	}
229 }
230 
setCurrentCoordinateSystemKey(QString key)231 void SearchDialog::setCurrentCoordinateSystemKey(QString key)
232 {
233 	const QMetaEnum& en = metaObject()->enumerator(metaObject()->indexOfEnumerator("CoordinateSystem"));
234 	CoordinateSystem coordSystem = static_cast<CoordinateSystem>(en.keyToValue(key.toLatin1().data()));
235 	if (coordSystem<0)
236 	{
237 		qWarning() << "[Search Tool] Unknown coordinate system: " << key << "setting \"equatorialJ2000\" instead";
238 		coordSystem = equatorialJ2000;
239 	}
240 	setCurrentCoordinateSystem(coordSystem);
241 }
242 
getCurrentCoordinateSystemKey() const243 QString SearchDialog::getCurrentCoordinateSystemKey() const
244 {
245 	return metaObject()->enumerator(metaObject()->indexOfEnumerator("CoordinateSystem")).key(currentCoordinateSystem);
246 }
247 
populateCoordinateSystemsList()248 void SearchDialog::populateCoordinateSystemsList()
249 {
250 	Q_ASSERT(ui->coordinateSystemComboBox);
251 
252 	QComboBox* csys = ui->coordinateSystemComboBox;
253 
254 	//Save the current selection to be restored later
255 	csys->blockSignals(true);
256 	int index = csys->currentIndex();
257 	QVariant selectedSystemId = csys->itemData(index);
258 	csys->clear();
259 	//For each coordinate system, display the localized name and store the key as user
260 	//data. Unfortunately, there's no other way to do this than with a cycle.
261 	csys->addItem(qc_("Equatorial (J2000.0)", "coordinate system"), "equatorialJ2000");
262 	csys->addItem(qc_("Equatorial", "coordinate system"), "equatorial");
263 	csys->addItem(qc_("Horizontal", "coordinate system"), "horizontal");
264 	csys->addItem(qc_("Galactic", "coordinate system"), "galactic");
265 	csys->addItem(qc_("Supergalactic", "coordinate system"), "supergalactic");
266 	csys->addItem(qc_("Ecliptic", "coordinate system"), "ecliptic");
267 	csys->addItem(qc_("Ecliptic (J2000.0)", "coordinate system"), "eclipticJ2000");
268 
269 	//Restore the selection
270 	index = csys->findData(selectedSystemId, Qt::UserRole, Qt::MatchCaseSensitive);
271 	csys->setCurrentIndex(index);
272 	csys->blockSignals(false);
273 }
274 
populateCoordinateAxis()275 void SearchDialog::populateCoordinateAxis()
276 {
277 	bool withDecimalDegree = StelApp::getInstance().getFlagShowDecimalDegrees();
278 	bool xnormal = true;
279 
280 	ui->AxisXSpinBox->setDecimals(2);
281 	ui->AxisYSpinBox->setDecimals(2);
282 
283 	// Allow rotating through longitudinal coordinates, but stop at poles.
284 	ui->AxisXSpinBox->setMinimum(  0., true);
285 	ui->AxisXSpinBox->setMaximum(360., true);
286 	ui->AxisXSpinBox->setWrapping(true);
287 	ui->AxisYSpinBox->setMinimum(-90., true);
288 	ui->AxisYSpinBox->setMaximum( 90., true);
289 	ui->AxisYSpinBox->setWrapping(false);
290 
291 	switch (getCurrentCoordinateSystem()) {
292 		case equatorialJ2000:
293 		case equatorial:
294 		{
295 			ui->AxisXLabel->setText(q_("Right ascension"));
296 			ui->AxisXSpinBox->setDisplayFormat(AngleSpinBox::HMSLetters);
297 			ui->AxisXSpinBox->setPrefixType(AngleSpinBox::Normal);
298 			ui->AxisYLabel->setText(q_("Declination"));
299 			ui->AxisYSpinBox->setDisplayFormat(AngleSpinBox::DMSSymbols);
300 			ui->AxisYSpinBox->setPrefixType(AngleSpinBox::NormalPlus);
301 			xnormal = true;
302 			break;
303 		}
304 		case horizontal:
305 		{
306 			ui->AxisXLabel->setText(q_("Azimuth"));
307 			ui->AxisXSpinBox->setDisplayFormat(AngleSpinBox::DMSSymbolsUnsigned);
308 			ui->AxisXSpinBox->setPrefixType(AngleSpinBox::NormalPlus);
309 			ui->AxisYLabel->setText(q_("Altitude"));
310 			ui->AxisYSpinBox->setDisplayFormat(AngleSpinBox::DMSSymbols);
311 			ui->AxisYSpinBox->setPrefixType(AngleSpinBox::NormalPlus);
312 			xnormal = false;
313 			break;
314 		}
315 		case ecliptic:
316 		case eclipticJ2000:
317 		case galactic:
318 		case supergalactic:
319 		{
320 			ui->AxisXLabel->setText(q_("Longitude"));
321 			ui->AxisXSpinBox->setDisplayFormat(AngleSpinBox::DMSSymbolsUnsigned);
322 			ui->AxisXSpinBox->setPrefixType(AngleSpinBox::NormalPlus);
323 			ui->AxisYLabel->setText(q_("Latitude"));
324 			ui->AxisYSpinBox->setDisplayFormat(AngleSpinBox::DMSSymbols);
325 			ui->AxisYSpinBox->setPrefixType(AngleSpinBox::NormalPlus);
326 			xnormal = false;
327 			break;
328 		}
329 	}
330 
331 	if (withDecimalDegree)
332 	{
333 		ui->AxisXSpinBox->setDecimals(5);
334 		ui->AxisYSpinBox->setDecimals(5);
335 		ui->AxisXSpinBox->setDisplayFormat(AngleSpinBox::DecimalDeg);
336 		ui->AxisYSpinBox->setDisplayFormat(AngleSpinBox::DecimalDeg);
337 		ui->AxisXSpinBox->setPrefixType(AngleSpinBox::NormalPlus);
338 	}
339 	else
340 	{
341 		if (xnormal)
342 			ui->AxisXSpinBox->setPrefixType(AngleSpinBox::Normal);
343 	}
344 }
345 
setCoordinateSystem(int csID)346 void SearchDialog::setCoordinateSystem(int csID)
347 {
348 	QString currentCoordinateSystemID = ui->coordinateSystemComboBox->itemData(csID).toString();
349 	setCurrentCoordinateSystemKey(currentCoordinateSystemID);
350 	populateCoordinateAxis();
351 	ui->AxisXSpinBox->setRadians(0.);
352 	ui->AxisYSpinBox->setRadians(0.);
353 	conf->setValue("search/coordinate_system", currentCoordinateSystemID);
354 }
355 
356 // Initialize the dialog widgets and connect the signals/slots
createDialogContent()357 void SearchDialog::createDialogContent()
358 {
359 	ui->setupUi(dialog);
360 	connect(&StelApp::getInstance(), SIGNAL(languageChanged()), this, SLOT(retranslate()));
361 	connect(&StelApp::getInstance(), SIGNAL(flagShowDecimalDegreesChanged(bool)), this, SLOT(populateCoordinateAxis()));
362 	connect(ui->closeStelWindow, SIGNAL(clicked()), this, SLOT(close()));
363 	connect(ui->TitleBar, SIGNAL(movedTo(QPoint)), this, SLOT(handleMovedTo(QPoint)));
364 	connect(ui->lineEditSearchSkyObject, SIGNAL(textChanged(const QString&)), this, SLOT(onSearchTextChanged(const QString&)));
365 	connect(ui->simbadCooQueryButton, SIGNAL(clicked()), this, SLOT(lookupCoordinates()));
366 	connect(GETSTELMODULE(StelObjectMgr), SIGNAL(selectedObjectChanged(StelModule::StelModuleSelectAction)), this, SLOT(clearSimbadText(StelModule::StelModuleSelectAction)));
367 	connect(ui->pushButtonGotoSearchSkyObject, SIGNAL(clicked()), this, SLOT(gotoObject()));
368 	onSearchTextChanged(ui->lineEditSearchSkyObject->text());
369 	connect(ui->lineEditSearchSkyObject, SIGNAL(returnPressed()), this, SLOT(gotoObject()));
370 	connect(ui->lineEditSearchSkyObject, SIGNAL(selectionChanged()), this, SLOT(setHasSelectedFlag()));
371 	connect(ui->lineEditSearchSkyObject, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showContextMenu(QPoint)));
372 
373 	ui->lineEditSearchSkyObject->installEventFilter(this);
374 
375 	// Kinetic scrolling
376 	kineticScrollingList << ui->objectsListView;
377 	StelGui* gui= dynamic_cast<StelGui*>(StelApp::getInstance().getGui());
378 	if (gui)
379 	{
380 		enableKineticScrolling(gui->getFlagUseKineticScrolling());
381 		connect(gui, SIGNAL(flagUseKineticScrollingChanged(bool)), this, SLOT(enableKineticScrolling(bool)));
382 	}
383 
384 	populateCoordinateSystemsList();
385 	populateCoordinateAxis();
386 	int idx = ui->coordinateSystemComboBox->findData(getCurrentCoordinateSystemKey(), Qt::UserRole, Qt::MatchCaseSensitive);
387 	if (idx==-1) // Use equatorialJ2000 as default
388 		idx = ui->coordinateSystemComboBox->findData(QVariant("equatorialJ2000"), Qt::UserRole, Qt::MatchCaseSensitive);
389 	ui->coordinateSystemComboBox->setCurrentIndex(idx);
390 	connect(ui->coordinateSystemComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(setCoordinateSystem(int)));
391 	connect(ui->AxisXSpinBox, SIGNAL(valueChanged()), this, SLOT(manualPositionChanged()));
392 	connect(ui->AxisYSpinBox, SIGNAL(valueChanged()), this, SLOT(manualPositionChanged()));
393 
394 	connect(ui->alphaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
395 	connect(ui->betaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
396 	connect(ui->gammaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
397 	connect(ui->deltaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
398 	connect(ui->epsilonPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
399 	connect(ui->zetaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
400 	connect(ui->etaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
401 	connect(ui->thetaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
402 	connect(ui->iotaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
403 	connect(ui->kappaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
404 	connect(ui->lambdaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
405 	connect(ui->muPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
406 	connect(ui->nuPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
407 	connect(ui->xiPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
408 	connect(ui->omicronPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
409 	connect(ui->piPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
410 	connect(ui->rhoPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
411 	connect(ui->sigmaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
412 	connect(ui->tauPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
413 	connect(ui->upsilonPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
414 	connect(ui->phiPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
415 	connect(ui->chiPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
416 	connect(ui->psiPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
417 	connect(ui->omegaPushButton, SIGNAL(clicked(bool)), this, SLOT(greekLetterClicked()));
418 
419 	connectBoolProperty(ui->simbadGroupBox, "SearchDialog.useSimbad");
420 	connectIntProperty(ui->searchRadiusSpinBox,    "SearchDialog.simbadDist");
421 	connectIntProperty(ui->resultsSpinBox,         "SearchDialog.simbadCount");
422 	connectBoolProperty(ui->allIDsCheckBox,        "SearchDialog.simbadGetIds");
423 	connectBoolProperty(ui->spectralClassCheckBox, "SearchDialog.simbadGetSpec");
424 	connectBoolProperty(ui->morphoCheckBox,        "SearchDialog.simbadGetMorpho");
425 	connectBoolProperty(ui->typesCheckBox,         "SearchDialog.simbadGetTypes");
426 	connectBoolProperty(ui->dimsCheckBox,          "SearchDialog.simbadGetDims");
427 
428 	populateSimbadServerList();
429 	idx = ui->serverListComboBox->findData(simbadServerUrl, Qt::UserRole, Qt::MatchCaseSensitive);
430 	if (idx==-1) // Use University of Strasbourg as default
431 		idx = ui->serverListComboBox->findData(QVariant(DEF_SIMBAD_URL), Qt::UserRole, Qt::MatchCaseSensitive);
432 	ui->serverListComboBox->setCurrentIndex(idx);
433 	connect(ui->serverListComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(selectSimbadServer(int)));
434 
435 	connect(ui->checkBoxUseStartOfWords, SIGNAL(clicked(bool)), this, SLOT(enableStartOfWordsAutofill(bool)));
436 	ui->checkBoxUseStartOfWords->setChecked(useStartOfWords);
437 
438 	connect(ui->checkBoxFOVCenterMarker, SIGNAL(clicked(bool)), this, SLOT(enableFOVCenterMarker(bool)));
439 	ui->checkBoxFOVCenterMarker->setChecked(useFOVCenterMarker);
440 
441 	connect(ui->checkBoxLockPosition, SIGNAL(clicked(bool)), this, SLOT(enableLockPosition(bool)));
442 	ui->checkBoxLockPosition->setChecked(useLockPosition);
443 
444 	// list views initialization
445 	listModel = new QStringListModel(this);
446 	proxyModel = new QSortFilterProxyModel(ui->objectsListView);
447 	proxyModel->setSourceModel(listModel);
448 	proxyModel->sort(0, Qt::AscendingOrder);
449 	proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
450 	ui->objectsListView->setModel(proxyModel);
451 
452 	connect(ui->objectTypeComboBox, SIGNAL(activated(int)), this, SLOT(updateListView(int)));
453 	connect(ui->searchInListLineEdit, SIGNAL(textChanged(const QString&)), proxyModel, SLOT(setFilterWildcard(const QString&)));
454 	connect(ui->searchInEnglishCheckBox, SIGNAL(toggled(bool)), this, SLOT(updateListTab()));
455 	QAction *clearAction = ui->searchInListLineEdit->addAction(QIcon(":/graphicGui/uieBackspaceInputButton.png"), QLineEdit::ActionPosition::TrailingPosition);
456 	connect(clearAction, SIGNAL(triggered()), this, SLOT(searchListClear()));
457 	updateListTab();
458 
459 	connect(ui->tabWidget, SIGNAL(currentChanged(int)), this, SLOT(changeTab(int)));
460 	// Set the focus directly on the line editDe	if (ui->tabWidget->currentIndex()==0)
461 	ui->lineEditSearchSkyObject->setFocus();
462 
463 	connect(StelApp::getInstance().getCore(), SIGNAL(updateSearchLists()), this, SLOT(updateListTab()));
464 
465 	QString style = "QLabel { color: rgb(238, 238, 238); }";
466 	ui->simbadStatusLabel->setStyleSheet(style);
467 	ui->labelGreekLetterTitle->setStyleSheet(style);
468 	ui->simbadCooStatusLabel->setStyleSheet(style);
469 
470 	// Get data from previous session
471 	loadRecentSearches();
472 
473 	// Create list model view
474 	ui->searchListView->setModel(searchListModel);
475 	searchListModel->setStringList(searchListModel->getValues());
476 
477 	// Auto display recent searches
478 	QStringList recentMatches = listMatchingRecentObjects("", recentObjectSearchesData.maxSize, useStartOfWords);
479 	resetSearchResultDisplay(recentMatches, recentMatches);
480 	setPushButtonGotoSearch();
481 
482 	// Update max size of "recent object searches"
483 	connect(ui->recentSearchSizeSpinBox, SIGNAL(editingFinished()), this, SLOT(recentSearchSizeEditingFinished()));
484 	// Clear data from recent search object
485 	connect(ui->recentSearchClearDataPushButton, SIGNAL(clicked()), this, SLOT(recentSearchClearDataClicked()));
486 	populateRecentSearch();
487 }
488 
populateRecentSearch()489 void SearchDialog::populateRecentSearch()
490 {
491 	// Tooltip for recentSearchSizeSpinBox
492 	QString toolTipComment = QString("%1: %2 | %3: %4 - %5 %6")
493 			.arg(qc_("Default", "search tool"))
494 			.arg(defaultMaxSize)
495 			.arg(qc_("Range", "search tool"))
496 			.arg(ui->recentSearchSizeSpinBox->minimum())
497 			.arg(ui->recentSearchSizeSpinBox->maximum())
498 			.arg(qc_("searches", "search tool"));
499 	ui->recentSearchSizeSpinBox->setToolTip(toolTipComment);
500 	setRecentSearchClearDataPushButton();
501 }
502 
changeTab(int index)503 void SearchDialog::changeTab(int index)
504 {
505 	if (index==0) // Search Tab
506 		ui->lineEditSearchSkyObject->setFocus();
507 
508 	if (index==2) // Position
509 	{
510 		if (useFOVCenterMarker)
511 			GETSTELMODULE(SpecialMarkersMgr)->setFlagFOVCenterMarker(true);
512 	}
513 	else
514 		GETSTELMODULE(SpecialMarkersMgr)->setFlagFOVCenterMarker(fovCenterMarkerState);
515 
516 	if (index==3) // Lists
517 	{
518 		updateListTab();
519 		ui->searchInListLineEdit->setFocus();
520 	}
521 }
522 
setHasSelectedFlag()523 void SearchDialog::setHasSelectedFlag()
524 {
525 	flagHasSelectedText = true;
526 }
527 
enableSimbadSearch(bool enable)528 void SearchDialog::enableSimbadSearch(bool enable)
529 {
530 	useSimbad = enable;
531 	conf->setValue("search/flag_search_online", useSimbad);
532 	if (dialog && ui->simbadStatusLabel) ui->simbadStatusLabel->clear();
533 	if (dialog && ui->simbadCooStatusLabel) ui->simbadCooStatusLabel->clear();
534 	if (dialog && ui->simbadTab) ui->simbadTab->setEnabled(enable);
535 	emit simbadUseChanged(enable);
536 }
537 
setSimbadQueryDist(int dist)538 void SearchDialog::setSimbadQueryDist(int dist)
539 {
540 	simbadDist=dist;
541 	conf->setValue("search/simbad_query_dist", simbadDist);
542 	emit simbadQueryDistChanged(dist);
543 }
544 
setSimbadQueryCount(int count)545 void SearchDialog::setSimbadQueryCount(int count)
546 {
547 	simbadCount=count;
548 	conf->setValue("search/simbad_query_count", simbadCount);
549 	emit simbadQueryCountChanged(count);
550 }
setSimbadGetsIds(bool b)551 void SearchDialog::setSimbadGetsIds(bool b)
552 {
553 	simbadGetIds=b;
554 	conf->setValue("search/simbad_query_IDs", b);
555 	emit simbadGetsIdsChanged(b);
556 }
557 
setSimbadGetsSpec(bool b)558 void SearchDialog::setSimbadGetsSpec(bool b)
559 {
560 	simbadGetSpec=b;
561 	conf->setValue("search/simbad_query_spec", b);
562 	emit simbadGetsSpecChanged(b);
563 }
564 
setSimbadGetsMorpho(bool b)565 void SearchDialog::setSimbadGetsMorpho(bool b)
566 {
567 	simbadGetMorpho=b;
568 	conf->setValue("search/simbad_query_morpho", b);
569 	emit simbadGetsMorphoChanged(b);
570 }
571 
setSimbadGetsTypes(bool b)572 void SearchDialog::setSimbadGetsTypes(bool b)
573 {
574 	simbadGetTypes=b;
575 	conf->setValue("search/simbad_query_types", b);
576 	emit simbadGetsTypesChanged(b);
577 }
578 
setSimbadGetsDims(bool b)579 void SearchDialog::setSimbadGetsDims(bool b)
580 {
581 	simbadGetDims=b;
582 	conf->setValue("search/simbad_query_dimensions", b);
583 	emit simbadGetsDimsChanged(b);
584 }
585 
recentSearchSizeEditingFinished()586 void SearchDialog::recentSearchSizeEditingFinished()
587 {
588 	// Update max size in dialog and user data
589 	int maxSize = ui->recentSearchSizeSpinBox->value();
590 	setRecentSearchSize(maxSize);
591 	maxSize = recentObjectSearchesData.maxSize; // Might not be the same
592 
593 	// Save maxSize to user's data
594 	saveRecentSearches();
595 
596 	// Update search result on "Object" tab
597 	onSearchTextChanged(ui->lineEditSearchSkyObject->text());
598 }
599 
recentSearchClearDataClicked()600 void SearchDialog::recentSearchClearDataClicked()
601 {
602 	// Clear recent list from current run
603 	recentObjectSearchesData.recentList.clear();
604 
605 	// Save empty list to user's data file
606 	saveRecentSearches();
607 
608 	// Update search result on "Object" tab
609 	onSearchTextChanged(ui->lineEditSearchSkyObject->text());
610 }
611 
setRecentSearchClearDataPushButton()612 void SearchDialog::setRecentSearchClearDataPushButton()
613 {
614 	// Enable clear button if recent list is greater than 0
615 	bool toEnable = recentObjectSearchesData.recentList.size() > 0;
616 	ui->recentSearchClearDataPushButton->setEnabled(toEnable);
617 	// Tool tip depends on recent list size
618 	QString toolTipText;
619 	toolTipText = toEnable ? q_("Clear search history: delete all search objects data") : q_("Clear search history: no data to delete");
620 	ui->recentSearchClearDataPushButton->setToolTip(toolTipText);
621 }
622 
enableStartOfWordsAutofill(bool enable)623 void SearchDialog::enableStartOfWordsAutofill(bool enable)
624 {
625 	useStartOfWords = enable;
626 	conf->setValue("search/flag_start_words", useStartOfWords);
627 
628 	// Update search result on "Object" tab
629 	onSearchTextChanged(ui->lineEditSearchSkyObject->text());
630 }
631 
enableLockPosition(bool enable)632 void SearchDialog::enableLockPosition(bool enable)
633 {
634 	useLockPosition = enable;
635 	conf->setValue("search/flag_lock_position", useLockPosition);
636 }
637 
enableFOVCenterMarker(bool enable)638 void SearchDialog::enableFOVCenterMarker(bool enable)
639 {
640 	useFOVCenterMarker = enable;
641 	fovCenterMarkerState = GETSTELMODULE(SpecialMarkersMgr)->getFlagFOVCenterMarker();
642 	conf->setValue("search/flag_fov_center_marker", useFOVCenterMarker);
643 }
644 
setSimpleStyle()645 void SearchDialog::setSimpleStyle()
646 {
647 	ui->AxisXSpinBox->setVisible(false);
648 	ui->AxisXSpinBox->setVisible(false);
649 	ui->simbadStatusLabel->setVisible(false);
650 	ui->simbadCooStatusLabel->setVisible(false);
651 	ui->AxisXLabel->setVisible(false);
652 	ui->AxisYLabel->setVisible(false);
653 	ui->coordinateSystemLabel->setVisible(false);
654 	ui->coordinateSystemComboBox->setVisible(false);
655 }
656 
657 
manualPositionChanged()658 void SearchDialog::manualPositionChanged()
659 {
660 	searchListModel->clearValues();
661 	StelCore* core = StelApp::getInstance().getCore();
662 	StelMovementMgr* mvmgr = GETSTELMODULE(StelMovementMgr);
663 	Vec3d pos;
664 	Vec3d aimUp;
665 	double spinLong=ui->AxisXSpinBox->valueRadians();
666 	double spinLat=ui->AxisYSpinBox->valueRadians();
667 
668 	// Since 0.15: proper handling of aimUp vector. This does not depend on the searched coordinate system, but on the MovementManager's C.S.
669 	// However, if those are identical, we have a problem when we want to look right into the pole. (e.g. zenith), which requires a special up vector.
670 	// aimUp depends on MovementMgr::MountMode mvmgr->mountMode!
671 	mvmgr->setViewUpVector(Vec3d(0., 0., 1.));
672 	aimUp=mvmgr->getViewUpVectorJ2000();
673 	StelMovementMgr::MountMode mountMode=mvmgr->getMountMode();
674 
675 	switch (getCurrentCoordinateSystem())
676 	{
677 		case equatorialJ2000:
678 		{
679 			StelUtils::spheToRect(spinLong, spinLat, pos);
680 			if ( (mountMode==StelMovementMgr::MountEquinoxEquatorial) && (fabs(spinLat)> (0.9*M_PI_2)) )
681 			{
682 				// make up vector more stable.
683 				// Strictly mount should be in a new J2000 mode, but this here also stabilizes searching J2000 coordinates.
684 				mvmgr->setViewUpVector(Vec3d(-cos(spinLong), -sin(spinLong), 0.) * (spinLat>0. ? 1. : -1. ));
685 				aimUp=mvmgr->getViewUpVectorJ2000();
686 			}
687 			break;
688 		}
689 		case equatorial:
690 		{
691 			StelUtils::spheToRect(spinLong, spinLat, pos);
692 			pos = core->equinoxEquToJ2000(pos, StelCore::RefractionOff);
693 
694 			if ( (mountMode==StelMovementMgr::MountEquinoxEquatorial) && (fabs(spinLat)> (0.9*M_PI_2)) )
695 			{
696 				// make up vector more stable.
697 				mvmgr->setViewUpVector(Vec3d(-cos(spinLong), -sin(spinLong), 0.) * (spinLat>0. ? 1. : -1. ));
698 				aimUp=mvmgr->getViewUpVectorJ2000();
699 			}
700 			break;
701 		}
702 		case horizontal:
703 		{
704 			double cx;
705 			cx = 3.*M_PI - spinLong; // N is zero, E is 90 degrees
706 			if (cx > 2.*M_PI)
707 				cx -= 2.*M_PI;
708 			StelUtils::spheToRect(cx, spinLat, pos);
709 			pos = core->altAzToJ2000(pos, StelCore::RefractionOff);
710 			core->setTimeRate(0.);
711 
712 			if ( (mountMode==StelMovementMgr::MountAltAzimuthal) && (fabs(spinLat)> (0.9*M_PI_2)) )
713 			{
714 				// make up vector more stable.
715 				mvmgr->setViewUpVector(Vec3d(-cos(cx), -sin(cx), 0.) * (spinLat>0. ? 1. : -1. ));
716 				aimUp=mvmgr->getViewUpVectorJ2000();
717 			}
718 			break;
719 		}
720 		case galactic:
721 		{
722 			StelUtils::spheToRect(spinLong, spinLat, pos);
723 			pos = core->galacticToJ2000(pos);
724 			if ( (mountMode==StelMovementMgr::MountGalactic) && (fabs(spinLat)> (0.9*M_PI_2)) )
725 			{
726 				// make up vector more stable.
727 				mvmgr->setViewUpVector(Vec3d(-cos(spinLong), -sin(spinLong), 0.) * (spinLat>0. ? 1. : -1. ));
728 				aimUp=mvmgr->getViewUpVectorJ2000();
729 			}
730 			break;
731 		}
732 		case supergalactic:
733 		{
734 			StelUtils::spheToRect(spinLong, spinLat, pos);
735 			pos = core->supergalacticToJ2000(pos);
736 			if ( (mountMode==StelMovementMgr::MountSupergalactic) && (fabs(spinLat)> (0.9*M_PI_2)) )
737 			{
738 				// make up vector more stable.
739 				mvmgr->setViewUpVector(Vec3d(-cos(spinLong), -sin(spinLong), 0.) * (spinLat>0. ? 1. : -1. ));
740 				aimUp=mvmgr->getViewUpVectorJ2000();
741 			}
742 			break;
743 		}
744 		case eclipticJ2000:
745 		{
746 			double ra, dec;
747 			StelUtils::eclToEqu(spinLong, spinLat, GETSTELMODULE(SolarSystem)->getEarth()->getRotObliquity(2451545.0), &ra, &dec);
748 			StelUtils::spheToRect(ra, dec, pos);
749 			break;
750 		}
751 		case ecliptic:
752 		{
753 			double ra, dec;
754 			StelUtils::eclToEqu(spinLong, spinLat, GETSTELMODULE(SolarSystem)->getEarth()->getRotObliquity(core->getJDE()), &ra, &dec);
755 			StelUtils::spheToRect(ra, dec, pos);
756 			pos = core->equinoxEquToJ2000(pos, StelCore::RefractionOff);
757 			break;
758 		}
759 	}
760 	mvmgr->setFlagTracking(false);
761 	mvmgr->moveToJ2000(pos, aimUp, mvmgr->getAutoMoveDuration());
762 	mvmgr->setFlagLockEquPos(useLockPosition);
763 }
764 
onSearchTextChanged(const QString & text)765 void SearchDialog::onSearchTextChanged(const QString& text)
766 {
767 	clearSimbadText(StelModule::ReplaceSelection);
768 	// This block needs to go before the trimmedText.isEmpty() or the SIMBAD result does not
769 	// get properly cleared.
770 	if (useSimbad)
771 	{
772 		if (simbadReply)
773 		{
774 			disconnect(simbadReply, SIGNAL(statusChanged()), this, SLOT(onSimbadStatusChanged()));
775 			delete simbadReply;
776 			simbadReply=Q_NULLPTR;
777 		}
778 		simbadResults.clear();
779 	}
780 
781 	// Use to adjust matches to be within range of maxNbItem
782 	int maxNbItem;
783 	QString trimmedText = text.trimmed();
784 	if (trimmedText.isEmpty())
785 	{
786 		searchListModel->clearValues();
787 
788 		maxNbItem = recentObjectSearchesData.maxSize;
789 		// Auto display recent searches
790 		QStringList recentMatches = listMatchingRecentObjects(trimmedText, maxNbItem, useStartOfWords);
791 		resetSearchResultDisplay(recentMatches, recentMatches);
792 
793 		ui->simbadStatusLabel->setText("");
794 		ui->simbadCooStatusLabel->setText("");
795 		setPushButtonGotoSearch();
796 	}
797 	else
798 	{
799 		if (useSimbad)
800 		{
801 			simbadReply = simbadSearcher->lookup(simbadServerUrl, trimmedText, 4);
802 			onSimbadStatusChanged();
803 			connect(simbadReply, SIGNAL(statusChanged()), this, SLOT(onSimbadStatusChanged()));
804 		}
805 
806 		// Get possible objects
807 		QStringList matches;
808 		QStringList recentMatches;
809 		QStringList allMatches;
810 
811 		QString greekText = substituteGreek(trimmedText);
812 
813 		int trimmedTextMaxNbItem = 13;
814 		int greekTextMaxMbItem = 0;
815 
816 		if(greekText != trimmedText)
817 		{
818 			trimmedTextMaxNbItem = 8;
819 			greekTextMaxMbItem = 18;
820 
821 			// Get recent matches
822 			// trimmedText
823 			recentMatches = listMatchingRecentObjects(trimmedText, trimmedTextMaxNbItem, useStartOfWords);
824 			// greekText
825 			recentMatches += listMatchingRecentObjects(greekText, (greekTextMaxMbItem - recentMatches.size()), useStartOfWords);
826 
827 			// Get rest of matches
828 			// trimmedText
829 			matches = objectMgr->listMatchingObjects(trimmedText, trimmedTextMaxNbItem, useStartOfWords);
830 			// greekText
831 			matches += objectMgr->listMatchingObjects(greekText, (greekTextMaxMbItem - matches.size()), useStartOfWords);
832 		}
833 		else
834 		{
835 			trimmedTextMaxNbItem = 13;
836 
837 			// Get recent matches
838 			recentMatches = listMatchingRecentObjects(trimmedText, trimmedTextMaxNbItem, useStartOfWords);
839 
840 			// Get rest of matches
841 			matches  = objectMgr->listMatchingObjects(trimmedText, trimmedTextMaxNbItem, useStartOfWords);
842 		}
843 		// Check in case either number changes since they were
844 		// hard coded
845 		maxNbItem  = qMax(greekTextMaxMbItem, trimmedTextMaxNbItem);
846 
847 		// Clean up matches
848 		adjustMatchesResult(allMatches, recentMatches, matches, maxNbItem);
849 
850 		// Updates values
851 		resetSearchResultDisplay(allMatches, recentMatches);
852 
853 		// Update push button enabled state
854 		setPushButtonGotoSearch();
855 	}
856 
857 	// Goto object when clicking in list
858 	connect(ui->searchListView, SIGNAL(clicked(const QModelIndex&)), this, SLOT(gotoObject(const QModelIndex&)), Qt::UniqueConnection);
859 	connect(ui->searchListView, SIGNAL(activated(const QModelIndex&)), this, SLOT(gotoObject(const QModelIndex&)), Qt::UniqueConnection);
860 }
861 
updateRecentSearchList(const QString & nameI18n)862 void SearchDialog::updateRecentSearchList(const QString &nameI18n)
863 {
864 	if(nameI18n.isEmpty())
865 		return;
866 
867 	// Prepend & remove duplicates
868 	recentObjectSearchesData.recentList.prepend(nameI18n);
869 	recentObjectSearchesData.recentList.removeDuplicates();
870 
871 	adjustRecentList(recentObjectSearchesData.maxSize);
872 
873 	// Auto display recent searches
874 	QStringList recentMatches = listMatchingRecentObjects("", recentObjectSearchesData.maxSize, useStartOfWords);
875 	resetSearchResultDisplay(recentMatches, recentMatches);
876 }
877 
adjustRecentList(int maxSize)878 void SearchDialog::adjustRecentList(int maxSize)
879 {
880 	// Check if max size was updated recently
881 	maxSize = (maxSize >= 0) ? maxSize : recentObjectSearchesData.maxSize;
882 	recentObjectSearchesData.maxSize = maxSize;
883 
884 	// Max amount of saved values "allowed"
885 	int spinBoxMaxSize = ui->recentSearchSizeSpinBox->maximum();
886 
887 	// Only removing old searches if the list grows larger than the largest
888 	// "allowed" size (to retain data in case the user switches from
889 	// high to low size)
890 	if( recentObjectSearchesData.recentList.size() > spinBoxMaxSize)
891 		recentObjectSearchesData.recentList = recentObjectSearchesData.recentList.mid(0, spinBoxMaxSize);
892 }
893 
adjustMatchesResult(QStringList & allMatches,QStringList & recentMatches,QStringList & matches,int maxNbItem)894 void SearchDialog::adjustMatchesResult(QStringList &allMatches, QStringList& recentMatches, QStringList& matches, int maxNbItem)
895 {
896 	int tempSize;
897 	QStringList tempMatches; // unsorted matches use for calculation
898 	// not displaying
899 
900 	// remove possible duplicates from completion list
901 	matches.removeDuplicates();
902 
903 	matches.sort(Qt::CaseInsensitive);
904 	// objects with short names should be searched first
905 	// examples: Moon, Hydra (moon); Jupiter, Ghost of Jupiter
906 	stringLengthCompare comparator;
907 	std::sort(matches.begin(), matches.end(), comparator);
908 
909 	// Adjust recent matches to prefered max size
910 	recentMatches = recentMatches.mid(0, recentObjectSearchesData.maxSize);
911 
912 	// Find total size of both matches
913 	tempMatches << recentMatches << matches; // unsorted
914 	tempMatches.removeDuplicates();
915 	tempSize = tempMatches.size();
916 
917 	// Adjust match size to be within range
918 	if(tempSize>maxNbItem)
919 	{
920 		int i = tempSize - maxNbItem;
921 		matches = matches.mid(0, matches.size() - i);
922 	}
923 
924 	// Combine list: ordered by recent searches then relevance
925 	allMatches << recentMatches << matches;
926 
927 	// Remove possible duplicates from both listQSt
928 	allMatches.removeDuplicates();
929 }
930 
931 
resetSearchResultDisplay(QStringList allMatches,QStringList recentMatches)932 void SearchDialog::resetSearchResultDisplay(QStringList allMatches,
933 					       QStringList recentMatches)
934 {
935 	// Updates values
936 	searchListModel->appendValues(allMatches);
937 	searchListModel->appendRecentValues(recentMatches);
938 
939 	// Update display
940 	searchListModel->setValues(allMatches, recentMatches);
941 	searchListModel->selectFirst();
942 
943 	// Update highlight to top
944 	ui->searchListView->scrollToTop();
945 	int row = searchListModel->getSelectedIdx();
946 	ui->searchListView->setCurrentIndex(searchListModel->index(row));
947 
948 	// Enable clear data button
949 	setRecentSearchClearDataPushButton();
950 }
951 
setPushButtonGotoSearch()952 void SearchDialog::setPushButtonGotoSearch()
953 {
954 	// Empty search and empty recently search object list
955 	if (searchListModel->isEmpty() && (recentObjectSearchesData.recentList.size() == 0))
956 		ui->pushButtonGotoSearchSkyObject->setEnabled(false); // Do not enable search button
957 	else
958 		ui->pushButtonGotoSearchSkyObject->setEnabled(true); // Do enable search  button
959 }
960 
loadRecentSearches()961 void SearchDialog::loadRecentSearches()
962 {
963 	QVariantMap map;
964 	QFile jsonFile(recentObjectSearchesJsonPath);
965 	if(!jsonFile.open(QIODevice::ReadOnly))
966 	{
967 		qWarning() << "[Search] Can not open data file for recent searches"
968 			   << QDir::toNativeSeparators(recentObjectSearchesJsonPath);
969 
970 		// Use default value for recent search size
971 		setRecentSearchSize(ui->recentSearchSizeSpinBox->value());
972 	}
973 	else
974 	{
975 		try
976 		{
977 			int readMaxSize;
978 
979 			map = StelJsonParser::parse(jsonFile.readAll()).toMap();
980 			jsonFile.close();
981 
982 			QVariantMap recentSearchData = map.value("recentObjectSearches").toMap();
983 
984 			// Get user's maxSize data (if possible)
985 			readMaxSize = recentSearchData.value("maxSize").toInt();
986 			 // Non-negative size only
987 			recentObjectSearchesData.maxSize = (readMaxSize >= 0) ? readMaxSize : recentObjectSearchesData.maxSize;
988 
989 			// Update dialog size to match user's preference
990 			ui->recentSearchSizeSpinBox->setValue(recentObjectSearchesData.maxSize);
991 
992 			// Get user's recentList data (if possible)
993 			recentObjectSearchesData.recentList = recentSearchData.value("recentList").toStringList();
994 		}
995 		catch (std::runtime_error &e)
996 		{
997 			qWarning() << "[Search] File format is Wrong! Error:"
998 				   << e.what();
999 			return;
1000 		}
1001 	}
1002 }
1003 
saveRecentSearches()1004 void SearchDialog::saveRecentSearches()
1005 {
1006 	if(recentObjectSearchesJsonPath.isEmpty())
1007 	{
1008 		qWarning() << "[Search] Error in saving recent object searches";
1009 		return;
1010 	}
1011 
1012 	QFile jsonFile(recentObjectSearchesJsonPath);
1013 	if(!jsonFile.open(QFile::WriteOnly | QFile::Text))
1014 	{
1015 		qWarning() << "[Search] Recent search could not be save. A file can not be open for writing:"
1016 			   << QDir::toNativeSeparators(recentObjectSearchesJsonPath);
1017 		return;
1018 	}
1019 
1020 	QVariantMap rslDataList;
1021 	rslDataList.insert("maxSize", recentObjectSearchesData.maxSize);
1022 	rslDataList.insert("recentList", recentObjectSearchesData.recentList);
1023 
1024 	QVariantMap rsList;
1025 	rsList.insert("recentObjectSearches", rslDataList);
1026 
1027 	// Convert the tree to JSON
1028 	StelJsonParser::write(rsList, &jsonFile);
1029 	jsonFile.flush();
1030 	jsonFile.close();
1031 }
1032 
listMatchingRecentObjects(const QString & objPrefix,int maxNbItem,bool useStartOfWords) const1033 QStringList SearchDialog::listMatchingRecentObjects(const QString& objPrefix, int maxNbItem, bool useStartOfWords) const
1034 {
1035 	QStringList result;
1036 
1037 	if(maxNbItem <= 0)
1038 		return result;
1039 
1040 	// For all recent objects:
1041 	for (int i = 0; i < recentObjectSearchesData.recentList.size(); i++)
1042 	{
1043 		bool toAppend = useStartOfWords ? recentObjectSearchesData.recentList[i].startsWith(objPrefix, Qt::CaseInsensitive)
1044 						: recentObjectSearchesData.recentList[i].contains(objPrefix, Qt::CaseInsensitive);
1045 
1046 		if(toAppend)
1047 			result.append(recentObjectSearchesData.recentList[i]);
1048 
1049 		if (result.size() >= maxNbItem)
1050 			break;
1051 	}
1052 	return result;
1053 }
1054 
1055 
lookupCoordinates()1056 void SearchDialog::lookupCoordinates()
1057 {
1058 	if (!useSimbad)
1059 		return;
1060 
1061 	StelCore * core=StelApp::getInstance().getCore();
1062 	const QList<StelObjectP>& sel=GETSTELMODULE(StelObjectMgr)->getSelectedObject();
1063 	if (sel.length()==0)
1064 		return;
1065 
1066 	Vec3d coords=sel.at(0)->getJ2000EquatorialPos(core);
1067 
1068 	simbadReply = simbadSearcher->lookupCoords(simbadServerUrl, coords, getSimbadQueryCount(), 500,
1069 						   getSimbadQueryDist(), getSimbadGetsIds(), getSimbadGetsTypes(),
1070 						   getSimbadGetsSpec(), getSimbadGetsMorpho(), getSimbadGetsDims());
1071 	onSimbadStatusChanged();
1072 	connect(simbadReply, SIGNAL(statusChanged()), this, SLOT(onSimbadStatusChanged()));
1073 }
1074 
clearSimbadText(StelModule::StelModuleSelectAction)1075 void SearchDialog::clearSimbadText(StelModule::StelModuleSelectAction)
1076 {
1077 	ui->simbadCooResultsTextBrowser->clear();
1078 }
1079 
1080 // Called when the current simbad query status changes
onSimbadStatusChanged()1081 void SearchDialog::onSimbadStatusChanged()
1082 {
1083 	Q_ASSERT(simbadReply);
1084 	int index = ui->tabWidget->currentIndex();
1085 	QString info;
1086 	if (simbadReply->getCurrentStatus()==SimbadLookupReply::SimbadLookupErrorOccured)
1087 	{
1088 		info = QString("%1: %2").arg(q_("Simbad Lookup Error")).arg(simbadReply->getErrorString());
1089 		index==1 ? ui->simbadCooStatusLabel->setText(info) : ui->simbadStatusLabel->setText(info);
1090 		setPushButtonGotoSearch();
1091 		ui->simbadCooResultsTextBrowser->clear();
1092 	}
1093 	else
1094 	{
1095 		info = QString("%1: %2").arg(q_("Simbad Lookup")).arg(simbadReply->getCurrentStatusString());
1096 		index==1 ? ui->simbadCooStatusLabel->setText(info) : ui->simbadStatusLabel->setText(info);
1097 		// Query not over, don't disable button
1098 		ui->pushButtonGotoSearchSkyObject->setEnabled(true);
1099 	}
1100 
1101 	if (simbadReply->getCurrentStatus()==SimbadLookupReply::SimbadLookupFinished)
1102 	{
1103 		simbadResults = simbadReply->getResults();
1104 		searchListModel->appendValues(simbadResults.keys());
1105 		// Update push button enabled state
1106 		setPushButtonGotoSearch();
1107 	}
1108 
1109 	if (simbadReply->getCurrentStatus()==SimbadLookupReply::SimbadCoordinateLookupFinished)
1110 	{
1111 		QString ret = simbadReply->getResult();
1112 		ui->simbadCooResultsTextBrowser->setText(ret);
1113 	}
1114 
1115 	if (simbadReply->getCurrentStatus()!=SimbadLookupReply::SimbadLookupQuerying)
1116 	{
1117 		disconnect(simbadReply, SIGNAL(statusChanged()), this, SLOT(onSimbadStatusChanged()));
1118 		delete simbadReply;
1119 		simbadReply=Q_NULLPTR;
1120 
1121 		// Update push button enabled state
1122 		setPushButtonGotoSearch();
1123 	}
1124 }
1125 
greekLetterClicked()1126 void SearchDialog::greekLetterClicked()
1127 {
1128 	QPushButton *sender = reinterpret_cast<QPushButton *>(this->sender());
1129 	QLineEdit* sso = ui->lineEditSearchSkyObject;
1130 	QString text;
1131 	if (sender)
1132 	{
1133 		shiftPressed ? text = sender->text().toUpper() : text = sender->text();
1134 		if (flagHasSelectedText)
1135 		{
1136 			sso->setText(text);
1137 			flagHasSelectedText = false;
1138 		}
1139 		else
1140 			sso->setText(sso->text() + text);
1141 	}
1142 	sso->setFocus();
1143 }
1144 
gotoObject()1145 void SearchDialog::gotoObject()
1146 {
1147 	gotoObject(searchListModel->getSelected());
1148 }
1149 
gotoObject(const QString & nameI18n)1150 void SearchDialog::gotoObject(const QString &nameI18n)
1151 {
1152 	if (nameI18n.isEmpty())
1153 		return;
1154 
1155 	// Save recent search list
1156 	updateRecentSearchList(nameI18n);
1157 	saveRecentSearches();
1158 
1159 	StelMovementMgr* mvmgr = GETSTELMODULE(StelMovementMgr);
1160 	if (simbadResults.contains(nameI18n))
1161 	{
1162 		if (objectMgr->findAndSelect(nameI18n))
1163 		{
1164 			const QList<StelObjectP> newSelected = objectMgr->getSelectedObject();
1165 			if (!newSelected.empty())
1166 			{
1167 				close();
1168 				ui->lineEditSearchSkyObject->setText(""); // https://wiki.qt.io/Technical_FAQ#Why_does_the_memory_keep_increasing_when_repeatedly_pasting_text_and_calling_clear.28.29_in_a_QLineEdit.3F
1169 
1170 				// Can't point to home planet
1171 				if (newSelected[0]->getEnglishName()!=StelApp::getInstance().getCore()->getCurrentLocation().planetName)
1172 				{
1173 					mvmgr->moveToObject(newSelected[0], mvmgr->getAutoMoveDuration());
1174 					mvmgr->setFlagTracking(true);
1175 				}
1176 				else
1177 					GETSTELMODULE(StelObjectMgr)->unSelect();
1178 			}
1179 		}
1180 		else
1181 		{
1182 			close();
1183 			GETSTELMODULE(CustomObjectMgr)->addCustomObject(nameI18n, simbadResults[nameI18n]);
1184 			ui->lineEditSearchSkyObject->clear();
1185 			searchListModel->clearValues();
1186 			if (objectMgr->findAndSelect(nameI18n))
1187 			{
1188 				const QList<StelObjectP> newSelected = objectMgr->getSelectedObject();
1189 				// Can't point to home planet
1190 				if (newSelected[0]->getEnglishName()!=StelApp::getInstance().getCore()->getCurrentLocation().planetName)
1191 				{
1192 					mvmgr->moveToObject(newSelected[0], mvmgr->getAutoMoveDuration());
1193 					mvmgr->setFlagTracking(true);
1194 				}
1195 				else
1196 					GETSTELMODULE(StelObjectMgr)->unSelect();
1197 			}
1198 		}
1199 	}
1200 	else if (objectMgr->findAndSelectI18n(nameI18n) || objectMgr->findAndSelect(nameI18n))
1201 	{
1202 		const QList<StelObjectP> newSelected = objectMgr->getSelectedObject();
1203 		if (!newSelected.empty())
1204 		{
1205 			close();
1206 			ui->lineEditSearchSkyObject->clear();
1207 
1208 			// Can't point to home planet
1209 			if (newSelected[0]->getEnglishName()!=StelApp::getInstance().getCore()->getCurrentLocation().planetName)
1210 			{
1211 				mvmgr->moveToObject(newSelected[0], mvmgr->getAutoMoveDuration());
1212 				mvmgr->setFlagTracking(true);
1213 			}
1214 			else
1215 				GETSTELMODULE(StelObjectMgr)->unSelect();
1216 		}
1217 	}
1218 	simbadResults.clear();
1219 }
1220 
gotoObject(const QModelIndex & modelIndex)1221 void SearchDialog::gotoObject(const QModelIndex &modelIndex)
1222 {
1223 	gotoObject(modelIndex.model()->data(modelIndex, Qt::DisplayRole).toString());
1224 }
1225 
searchListClear()1226 void SearchDialog::searchListClear()
1227 {
1228 	ui->searchInListLineEdit->setText(""); // https://wiki.qt.io/Technical_FAQ#Why_does_the_memory_keep_increasing_when_repeatedly_pasting_text_and_calling_clear.28.29_in_a_QLineEdit.3F
1229 }
1230 
eventFilter(QObject *,QEvent * event)1231 bool SearchDialog::eventFilter(QObject*, QEvent *event)
1232 {
1233 	if (event->type() == QEvent::KeyPress)
1234 	{
1235 		QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
1236 
1237 		if (keyEvent->key() == Qt::Key_Shift)
1238 		{
1239 			shiftPressed = true;
1240 			event->accept();
1241 			return true;
1242 		}
1243 	}
1244 	if (event->type() == QEvent::KeyRelease)
1245 	{
1246 		QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
1247 
1248 		if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Down)
1249 		{
1250 			searchListModel->selectNext();
1251 			int row = searchListModel->getSelectedIdx();
1252 			ui->searchListView->scrollTo(searchListModel->index(row));
1253 			ui->searchListView->setCurrentIndex(searchListModel->index(row));
1254 			event->accept();
1255 			return true;
1256 		}
1257 		if (keyEvent->key() == Qt::Key_Up)
1258 		{
1259 			searchListModel->selectPrevious();
1260 			int row = searchListModel->getSelectedIdx();
1261 			ui->searchListView->scrollTo(searchListModel->index(row));
1262 			ui->searchListView->setCurrentIndex(searchListModel->index(row));
1263 			event->accept();
1264 			return true;
1265 		}
1266 		if (keyEvent->key() == Qt::Key_Shift)
1267 		{
1268 			shiftPressed = false;
1269 			event->accept();
1270 			return true;
1271 		}
1272 	}
1273 	if (event->type() == QEvent::Show)
1274 	{
1275 		if (!extSearchText.isEmpty())
1276 		{
1277 			ui->lineEditSearchSkyObject->setText(extSearchText);
1278 			ui->lineEditSearchSkyObject->selectAll();
1279 			extSearchText.clear();
1280 		}
1281 	}
1282 	return false;
1283 }
1284 
substituteGreek(const QString & keyString)1285 QString SearchDialog::substituteGreek(const QString& keyString)
1286 {
1287 	if (!keyString.contains(' '))
1288 		return getGreekLetterByName(keyString);
1289 	else
1290 	{
1291 		#if (QT_VERSION>=QT_VERSION_CHECK(5, 14, 0))
1292 		QStringList nameComponents = keyString.split(" ", Qt::SkipEmptyParts);
1293 		#else
1294 		QStringList nameComponents = keyString.split(" ", QString::SkipEmptyParts);
1295 		#endif
1296 		if(!nameComponents.empty())
1297 			nameComponents[0] = getGreekLetterByName(nameComponents[0]);
1298 		return nameComponents.join(" ");
1299 	}
1300 }
1301 
getGreekLetterByName(const QString & potentialGreekLetterName)1302 QString SearchDialog::getGreekLetterByName(const QString& potentialGreekLetterName)
1303 {
1304 	if(staticData.greekLetters.contains(potentialGreekLetterName))
1305 		return staticData.greekLetters[potentialGreekLetterName];
1306 
1307 	// There can be indices (e.g. "α1 Cen" instead of "α Cen A"), so strip
1308 	// any trailing digit.
1309 	int lastCharacterIndex = potentialGreekLetterName.length()-1;
1310 	if(potentialGreekLetterName.at(lastCharacterIndex).isDigit())
1311 	{
1312 		QChar digit = potentialGreekLetterName.at(lastCharacterIndex);
1313 		QString name = potentialGreekLetterName.left(lastCharacterIndex);
1314 		if(staticData.greekLetters.contains(name))
1315 			return staticData.greekLetters[name] + digit;
1316 	}
1317 
1318 	return potentialGreekLetterName;
1319 }
1320 
populateSimbadServerList()1321 void SearchDialog::populateSimbadServerList()
1322 {
1323 	Q_ASSERT(ui->serverListComboBox);
1324 
1325 	QComboBox* servers = ui->serverListComboBox;
1326 	//Save the current selection to be restored later
1327 	servers->blockSignals(true);
1328 	int index = servers->currentIndex();
1329 	QVariant selectedUrl = servers->itemData(index);
1330 	servers->clear();
1331 	//For each server, display the localized description and store the URL as user data.
1332 	servers->addItem(q_("University of Strasbourg (France)"), DEF_SIMBAD_URL);
1333 	servers->addItem(q_("Harvard University (USA)"), "http://simbad.harvard.edu/");
1334 
1335 	//Restore the selection
1336 	index = servers->findData(selectedUrl, Qt::UserRole, Qt::MatchCaseSensitive);
1337 	servers->setCurrentIndex(index);
1338 	servers->model()->sort(0);
1339 	servers->blockSignals(false);
1340 }
1341 
setRecentSearchSize(int maxSize)1342 void SearchDialog::setRecentSearchSize(int maxSize)
1343 {
1344 	adjustRecentList(maxSize);
1345 	saveRecentSearches();
1346 	conf->setValue("search/recentSearchSize", recentObjectSearchesData.maxSize);
1347 }
1348 
selectSimbadServer(int index)1349 void SearchDialog::selectSimbadServer(int index)
1350 {
1351 	index < 0 ? simbadServerUrl = DEF_SIMBAD_URL : simbadServerUrl = ui->serverListComboBox->itemData(index).toString();
1352 	conf->setValue("search/simbad_server_url", simbadServerUrl);
1353 }
1354 
updateListView(int index)1355 void SearchDialog::updateListView(int index)
1356 {
1357 	QString moduleId = ui->objectTypeComboBox->itemData(index).toString();
1358 	bool englishNames = ui->searchInEnglishCheckBox->isChecked();
1359 	ui->searchInListLineEdit->setText(""); // https://wiki.qt.io/Technical_FAQ#Why_does_the_memory_keep_increasing_when_repeatedly_pasting_text_and_calling_clear.28.29_in_a_QLineEdit.3F
1360 	ui->objectsListView->blockSignals(true);
1361 	ui->objectsListView->reset();
1362 	listModel->setStringList(objectMgr->listAllModuleObjects(moduleId, englishNames));
1363 	proxyModel->setSourceModel(listModel);
1364 	proxyModel->sort(0, Qt::AscendingOrder);
1365 	ui->objectsListView->blockSignals(false);
1366 	//bugfix: prevent multiple connections, which seems to have happened before
1367 	connect(ui->objectsListView, SIGNAL(clicked(const QModelIndex&)), this, SLOT(gotoObject(const QModelIndex&)), Qt::UniqueConnection);
1368 }
1369 
updateListTab()1370 void SearchDialog::updateListTab()
1371 {
1372 	QVariant selectedObjectId;
1373 	QComboBox* objects = ui->objectTypeComboBox;
1374 	int index = objects->currentIndex();
1375 	if (index < 0)
1376 		selectedObjectId = QVariant("AsterismMgr"); // Let's enable "Asterisms" by default!
1377 	else
1378 		selectedObjectId = objects->itemData(index, Qt::UserRole);
1379 
1380 	if (StelApp::getInstance().getLocaleMgr().getAppLanguage().startsWith("en"))
1381 		ui->searchInEnglishCheckBox->hide(); // hide "names in English" checkbox
1382 	else
1383 		ui->searchInEnglishCheckBox->show();
1384 	objects->blockSignals(true);
1385 	objects->clear();
1386 	QMap<QString, QString> modulesMap = objectMgr->objectModulesMap();
1387 	for (auto it = modulesMap.begin(); it != modulesMap.end(); ++it)
1388 	{
1389 		// List of objects is not empty, let's add name of module or section!
1390 		if (!objectMgr->listAllModuleObjects(it.key(), ui->searchInEnglishCheckBox->isChecked()).isEmpty())
1391 			objects->addItem(q_(it.value()), QVariant(it.key()));
1392 	}
1393 	index = objects->findData(selectedObjectId, Qt::UserRole, Qt::MatchCaseSensitive);
1394 	objects->setCurrentIndex(index);
1395 	objects->model()->sort(0, Qt::AscendingOrder);
1396 	objects->blockSignals(false);
1397 	updateListView(objects->currentIndex());
1398 }
1399 
showContextMenu(const QPoint & pt)1400 void SearchDialog::showContextMenu(const QPoint &pt)
1401 {
1402 	QMenu *menu = ui->lineEditSearchSkyObject->createStandardContextMenu();
1403 	menu->addSeparator();
1404 	QString clipText;
1405 	QClipboard *clipboard = QApplication::clipboard();
1406 	if (clipboard)
1407 		clipText = clipboard->text();
1408 	if (!clipText.isEmpty())
1409 	{
1410 		if (clipText.length()>12)
1411 			clipText = clipText.right(9) + "...";
1412 		clipText = "\t(" + clipText + ")";
1413 	}
1414 
1415 	menu->addAction(q_("Paste and Search") + clipText, this, SLOT(pasteAndGo()));
1416 	menu->exec(ui->lineEditSearchSkyObject->mapToGlobal(pt));
1417 	delete menu;
1418 }
1419 
pasteAndGo()1420 void SearchDialog::pasteAndGo()
1421 {
1422 	// https://wiki.qt.io/Technical_FAQ#Why_does_the_memory_keep_increasing_when_repeatedly_pasting_text_and_calling_clear.28.29_in_a_QLineEdit.3F
1423 	ui->lineEditSearchSkyObject->setText(""); // clear current text
1424 	ui->lineEditSearchSkyObject->paste(); // paste text from clipboard
1425 	gotoObject(); // go to first finded object
1426 }
1427 
setVisible(bool v)1428 void SearchDialog::setVisible(bool v)
1429 {
1430 	StelDialog::setVisible(v);
1431 
1432 	// if from a previous search action, the first tab is shown but input line is not in focus,
1433 	// force focus on input line.
1434 	if (v && (ui->tabWidget->currentIndex()==0))
1435 	{
1436 		ui->lineEditSearchSkyObject->setFocus(Qt::PopupFocusReason);
1437 	}
1438 }
1439