1 /*
2  *
3  * Stellarium
4  * Copyright (C) 2009 Matthew Gates
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA  02110-1335, USA.
19 */
20 
21 #include "ScriptConsole.hpp"
22 #include "ui_scriptConsole.h"
23 #include "StelMainView.hpp"
24 #include "StelScriptMgr.hpp"
25 #include "StelFileMgr.hpp"
26 #include "StelApp.hpp"
27 #include "StelTranslator.hpp"
28 #include "StelScriptSyntaxHighlighter.hpp"
29 
30 #include <QDialog>
31 #include <QMessageBox>
32 #include <QDebug>
33 #include <QTextStream>
34 #include <QTemporaryFile>
35 #include <QDir>
36 #include <QFile>
37 #include <QFileDialog>
38 #include <QDateTime>
39 #include <QSyntaxHighlighter>
40 #include <QTextDocumentFragment>
41 #include <QRegularExpression>
42 
ScriptConsole(QObject * parent)43 ScriptConsole::ScriptConsole(QObject *parent)
44 	: StelDialog("ScriptConsole", parent)
45 	, highlighter(Q_NULLPTR)
46 	, useUserDir(false)
47 	, hideWindowAtScriptRun(false)
48 	, clearOutput(false)
49 	, scriptFileName("")
50 	, isNew(true)
51 	, dirty(false)
52 {
53 	ui = new Ui_scriptConsoleForm;
54 }
55 
~ScriptConsole()56 ScriptConsole::~ScriptConsole()
57 {
58 	delete ui;
59 	delete highlighter; highlighter = Q_NULLPTR;
60 }
61 
retranslate()62 void ScriptConsole::retranslate()
63 {
64 	if (dialog)
65 	{
66 		ui->retranslateUi(dialog);
67 		populateQuickRunList();
68 	}
69 }
70 
styleChanged()71 void ScriptConsole::styleChanged()
72 {
73 	if (highlighter)
74 	{
75 		highlighter->setFormats();
76 		highlighter->rehighlight();
77 	}
78 }
79 
populateQuickRunList()80 void ScriptConsole::populateQuickRunList()
81 {
82 	ui->quickrunCombo->clear();
83 	ui->quickrunCombo->addItem(""); // First line is empty!
84 	ui->quickrunCombo->addItem(qc_("selected text as script","command"));
85 	ui->quickrunCombo->addItem(qc_("remove screen text","command"));
86 	ui->quickrunCombo->addItem(qc_("remove screen images","command"));
87 	ui->quickrunCombo->addItem(qc_("remove screen markers","command"));
88 	ui->quickrunCombo->addItem(qc_("clear map: natural","command"));
89 	ui->quickrunCombo->addItem(qc_("clear map: starchart","command"));
90 	ui->quickrunCombo->addItem(qc_("clear map: deepspace","command"));
91 	ui->quickrunCombo->addItem(qc_("clear map: galactic","command"));
92 	ui->quickrunCombo->addItem(qc_("clear map: supergalactic","command"));
93 }
94 
createDialogContent()95 void ScriptConsole::createDialogContent()
96 {
97 	ui->setupUi(dialog);
98 	connect(&StelApp::getInstance(), SIGNAL(languageChanged()), this, SLOT(retranslate()));
99 
100 	highlighter = new StelScriptSyntaxHighlighter(ui->scriptEdit->document());
101 	ui->includeEdit->setText(StelFileMgr::getInstallationDir() + "/scripts");
102 
103 	populateQuickRunList();
104 
105 	connect(ui->scriptEdit, SIGNAL(cursorPositionChanged()), this, SLOT(rowColumnChanged()));
106 	connect(ui->scriptEdit, SIGNAL(textChanged()), this, SLOT(setDirty()));
107 	connect(ui->closeStelWindow, SIGNAL(clicked()), this, SLOT(close()));
108 	connect(ui->TitleBar, SIGNAL(movedTo(QPoint)), this, SLOT(handleMovedTo(QPoint)));
109 	connect(ui->loadButton, SIGNAL(clicked()), this, SLOT(loadScript()));
110 	connect(ui->saveButton, SIGNAL(clicked()), this, SLOT(saveScript()));
111 	connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clearButtonPressed()));
112 	connect(ui->preprocessSSCButton, SIGNAL(clicked()), this, SLOT(preprocessScript()));
113 	connect(ui->runButton, SIGNAL(clicked()), this, SLOT(runScript()));
114 	connect(ui->stopButton, SIGNAL(clicked()), &StelApp::getInstance().getScriptMgr(), SLOT(stopScript()));
115 	connect(ui->includeBrowseButton, SIGNAL(clicked()), this, SLOT(includeBrowse()));
116 	connect(ui->quickrunCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(quickRun(int)));
117 	connect(&StelApp::getInstance().getScriptMgr(), SIGNAL(scriptRunning()), this, SLOT(scriptStarted()));
118 	connect(&StelApp::getInstance().getScriptMgr(), SIGNAL(scriptStopped()), this, SLOT(scriptEnded()));
119 	connect(&StelApp::getInstance().getScriptMgr(), SIGNAL(scriptDebug(const QString&)), this, SLOT(appendLogLine(const QString&)));
120 	connect(&StelApp::getInstance().getScriptMgr(), SIGNAL(scriptOutput(const QString&)), this, SLOT(appendOutputLine(const QString&)));
121 	ui->tabs->setCurrentIndex(0);
122 
123 	// get decent indentation
124 	QFont font = ui->scriptEdit->font();
125 	QFontMetrics fontMetrics = QFontMetrics(font);
126 	int width = fontMetrics.boundingRect("0").width();
127 #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
128 	ui->scriptEdit->setTabStopDistance(4*width); // 4 characters
129 #else
130 	ui->scriptEdit->setTabStopWidth(4*width); // 4 characters
131 #endif
132 	ui->scriptEdit->setFocus();
133 
134 	QSettings* conf = StelApp::getInstance().getSettings();
135 	useUserDir = conf->value("gui/flag_scripts_user_dir", false).toBool();
136 	ui->useUserDirCheckBox->setChecked(useUserDir);
137 	hideWindowAtScriptRun = conf->value("gui/flag_scripts_hide_window", false).toBool();
138 	ui->closeWindowAtScriptRunCheckbox->setChecked(hideWindowAtScriptRun);
139 	clearOutput = conf->value("gui/flag_scripts_clear_output", false).toBool();
140 	ui->clearOutputCheckbox->setChecked(clearOutput);
141 	connect(ui->useUserDirCheckBox, SIGNAL(toggled(bool)), this, SLOT(setFlagUserDir(bool)));
142 	connect(ui->closeWindowAtScriptRunCheckbox, SIGNAL(toggled(bool)), this, SLOT(setFlagHideWindow(bool)));
143 	connect(ui->clearOutputCheckbox, SIGNAL(toggled(bool)), this, SLOT(setFlagClearOutput(bool)));
144 
145 	// Let's improve visibility of the text
146 	QString style = "QLabel { color: rgb(238, 238, 238); }";
147 	ui->quickrunLabel->setStyleSheet(style);
148 	dirty = false;
149 }
150 
setFlagUserDir(bool b)151 void ScriptConsole::setFlagUserDir(bool b)
152 {
153 	if (b!=useUserDir)
154 	{
155 		useUserDir = b;
156 		StelApp::getInstance().getSettings()->setValue("gui/flag_scripts_user_dir", b);
157 	}
158 }
159 
setFlagHideWindow(bool b)160 void ScriptConsole::setFlagHideWindow(bool b)
161 {
162 	if (b!=hideWindowAtScriptRun)
163 	{
164 		hideWindowAtScriptRun = b;
165 		StelApp::getInstance().getSettings()->setValue("gui/flag_scripts_hide_window", b);
166 	}
167 }
168 
setFlagClearOutput(bool b)169 void ScriptConsole::setFlagClearOutput(bool b)
170 {
171 	if (b!=clearOutput)
172 	{
173 		clearOutput = b;
174 		StelApp::getInstance().getSettings()->setValue("gui/flag_scripts_clear_output", b);
175 	}
176 }
177 
loadScript()178 void ScriptConsole::loadScript()
179 {
180 	if (dirty)
181 	{
182 		// We are loaded and dirty: don't just overwrite!
183         if (QMessageBox::question(&StelMainView::getInstance(), q_("Caution!"), q_("Are you sure you want to load script without saving changes?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No)
184 			return;
185 	}
186 
187 	QString openDir;
188 	if (getFlagUserDir())
189 	{
190 		openDir = StelFileMgr::findFile("scripts", StelFileMgr::Flags(StelFileMgr::Writable|StelFileMgr::Directory));
191 		if (openDir.isEmpty() || openDir.contains(StelFileMgr::getInstallationDir()))
192 			openDir = StelFileMgr::getUserDir();
193 	}
194 	else
195 		openDir = StelFileMgr::getInstallationDir() + "/scripts";
196 
197 	QString filter = q_("Stellarium Script Files");
198 	filter.append(" (*.ssc *.inc);;");
199 	filter.append(getFileMask());
200 	QString aFile = QFileDialog::getOpenFileName(Q_NULLPTR, q_("Load Script"), openDir, filter);
201 	if (aFile.isNull())
202 		return;
203 	scriptFileName = aFile;
204 	QFile file(scriptFileName);
205 	if (file.open(QIODevice::ReadOnly))
206 	{
207 		ui->scriptEdit->setPlainText(file.readAll());
208 		dirty = false;
209 		ui->includeEdit->setText(StelFileMgr::dirName(scriptFileName));
210 		file.close();
211 	}
212 	ui->tabs->setCurrentIndex(0);
213 }
214 
saveScript()215 void ScriptConsole::saveScript()
216 {
217 	QString saveDir = StelFileMgr::findFile("scripts", StelFileMgr::Flags(StelFileMgr::Writable|StelFileMgr::Directory));
218 	if (saveDir.isEmpty())
219 		saveDir = StelFileMgr::getUserDir();
220 
221 	QString defaultFilter("(*.ssc)");
222 	// Let's ask file name, when file is new and overwrite him in other case
223 	if (scriptFileName.isEmpty())
224 	{
225 		QString aFile = QFileDialog::getSaveFileName(Q_NULLPTR, q_("Save Script"), saveDir + "/myscript.ssc", getFileMask(), &defaultFilter);
226 		if (aFile.isNull())
227 			return;
228 		scriptFileName = aFile;
229 	}
230 	else
231 	{
232 		// skip save
233 		if (!dirty)
234 			return;
235 	}
236 	QFile file(scriptFileName);
237 	if (file.open(QIODevice::WriteOnly))
238 	{
239 		QTextStream out(&file);
240 		out.setCodec("UTF-8");
241 		out << ui->scriptEdit->toPlainText();
242 		file.close();
243 		dirty = false;
244 	}
245 	else
246 		qWarning() << "[ScriptConsole] ERROR - cannot write script file";
247 }
248 
clearButtonPressed()249 void ScriptConsole::clearButtonPressed()
250 {
251 	if (ui->tabs->currentIndex() == 0)
252 	{
253 		if (QMessageBox::question(&StelMainView::getInstance(), q_("Caution!"), q_("Are you sure you want to clear script?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)
254 		{
255 			ui->scriptEdit->clear();
256 			scriptFileName = ""; // OK, it's a new file!
257 			dirty = false;
258 		}
259 	}
260 	else if (ui->tabs->currentIndex() == 1)
261 		ui->logBrowser->clear();
262 	else if (ui->tabs->currentIndex() == 2)
263 		ui->outputBrowser->clear();
264 }
265 
preprocessScript()266 void ScriptConsole::preprocessScript()
267 {
268 	//perform pre-processing without an intermediate temp file
269 	QString dest;
270 	QString src = ui->scriptEdit->toPlainText();
271 
272 	int errLoc = 0;
273 	if (sender() == ui->preprocessSSCButton)
274 	{
275 		qDebug() << "[ScriptConsole] Preprocessing with SSC proprocessor";
276 		StelApp::getInstance().getScriptMgr().preprocessScript( scriptFileName, src, dest, ui->includeEdit->text(), errLoc );
277 	}
278 	else
279 		qDebug() << "[ScriptConsole] WARNING - unknown preprocessor type";
280 
281 	ui->scriptEdit->setPlainText(dest);
282 	scriptFileName = ""; // OK, it's a new file!
283 	dirty = true;
284 	ui->tabs->setCurrentIndex( 0 );
285 	if( errLoc != -1 ){
286 		QTextCursor tc = ui->scriptEdit->textCursor();
287 		tc.setPosition( errLoc );
288 		ui->scriptEdit->setTextCursor( tc );
289 	}
290 }
291 
runScript()292 void ScriptConsole::runScript()
293 {
294 	ui->tabs->setCurrentIndex(1);
295 	ui->logBrowser->clear();
296 	if( clearOutput )
297 		ui->outputBrowser->clear();
298 
299 	appendLogLine(QString("Starting script at %1").arg(QDateTime::currentDateTime().toString()));
300 	int errLoc = 0;
301 	if (!StelApp::getInstance().getScriptMgr().runScriptDirect(scriptFileName, ui->scriptEdit->toPlainText(), errLoc, ui->includeEdit->text()))
302 	{
303 		QString msg = QString("ERROR - cannot run script");
304 		qWarning() << "[ScriptConsole] " + msg;
305 		appendLogLine(msg);
306 		if( errLoc != -1 ){
307 			QTextCursor tc = ui->scriptEdit->textCursor();
308 			tc.setPosition( errLoc );
309 			ui->scriptEdit->setTextCursor( tc );
310 		}
311 		return;
312 	}
313 }
314 
scriptStarted()315 void ScriptConsole::scriptStarted()
316 {
317 	//prevent strating of scripts while any script is running
318 	ui->quickrunCombo->setEnabled(false);
319 	ui->runButton->setEnabled(false);
320 	ui->stopButton->setEnabled(true);
321 	if (hideWindowAtScriptRun)
322 		dialog->setVisible(false);
323 }
324 
scriptEnded()325 void ScriptConsole::scriptEnded()
326 {
327 	qDebug() << "ScriptConsole::scriptEnded";
328 	appendLogLine(QString("Script finished at %1").arg(QDateTime::currentDateTime().toString()));
329 	ui->quickrunCombo->setEnabled(true);
330 	ui->runButton->setEnabled(true);
331 	ui->stopButton->setEnabled(false);
332 	if (hideWindowAtScriptRun)
333 		dialog->setVisible(true);
334 }
335 
appendLogLine(const QString & s)336 void ScriptConsole::appendLogLine(const QString& s)
337 {
338 	QString html = ui->logBrowser->toHtml();
339 	html.replace(QRegularExpression("^\\s+"), "");
340 	html += s;
341 	ui->logBrowser->setHtml(html);
342 }
343 
appendOutputLine(const QString & s)344 void ScriptConsole::appendOutputLine(const QString& s)
345 {
346 	if (s.isEmpty())
347 	{
348 		ui->outputBrowser->clear();
349 	}
350 	else
351 	{
352 		QString html = ui->outputBrowser->toHtml();
353 		html.replace(QRegularExpression("^\\s+"), "");
354 		html += s;
355 		ui->outputBrowser->setHtml(html);
356 	}
357 }
358 
includeBrowse()359 void ScriptConsole::includeBrowse()
360 {
361 	QString aDir = QFileDialog::getExistingDirectory(Q_NULLPTR, q_("Select Script Include Directory"), StelFileMgr::getInstallationDir() + "/scripts");
362 	if (!aDir.isNull())
363 		ui->includeEdit->setText(aDir);
364 }
365 
quickRun(int idx)366 void ScriptConsole::quickRun(int idx)
367 {
368 	if (idx==0)
369 		return;
370 	static const QMap<int, QString>map = {
371 		{2, "LabelMgr.deleteAllLabels();\n"},
372 		{3, "ScreenImageMgr.deleteAllImages();\n"},
373 		{4, "MarkerMgr.deleteAllMarkers();\n"},
374 		{5, "core.clear(\"natural\");\n"},
375 		{6, "core.clear(\"starchart\");\n"},
376 		{7, "core.clear(\"deepspace\");\n"},
377 		{8, "core.clear(\"galactic\");\n"},
378 		{9, "core.clear(\"supergalactic\");\n"}};
379 	QString scriptText = map.value(idx, QTextDocumentFragment::fromHtml(ui->scriptEdit->textCursor().selectedText(), ui->scriptEdit->document()).toPlainText());
380 
381 	if (!scriptText.isEmpty())
382 	{
383 		appendLogLine(QString("Running: %1").arg(scriptText));
384 		int errLoc;
385 		StelApp::getInstance().getScriptMgr().runScriptDirect( "<>", scriptText, errLoc );
386 		ui->quickrunCombo->setCurrentIndex(0);
387 	}
388 }
389 
rowColumnChanged()390 void ScriptConsole::rowColumnChanged()
391 {
392 	// TRANSLATORS: The first letter of word "Row"
393 	QString row = qc_("R", "text cursor");
394 	// TRANSLATORS: The first letter of word "Column"
395 	QString column = qc_("C", "text cursor");
396 	ui->rowColumnLabel->setText(QString("%1:%2 %3:%4")
397 				    .arg(row).arg(ui->scriptEdit->textCursor().blockNumber())
398 				    .arg(column).arg(ui->scriptEdit->textCursor().columnNumber()));
399 }
400 
setDirty()401 void ScriptConsole::setDirty()
402 {
403 	if (isNew)
404 		isNew = false;
405 	else
406 		dirty = true;
407 }
408 
getFileMask()409 const QString ScriptConsole::getFileMask()
410 {
411 	QString filter = q_("Stellarium Script");
412 	filter.append(" (*.ssc);;");
413 	filter.append(q_("Include File"));
414 	filter.append(" (*.inc)");
415 	return  filter;
416 }
417 
418