1 /*
2 For general Scribus (>=1.3.2) copyright and licensing information please refer
3 to the COPYING file provided with the program. Following this notice may exist
4 a copyright and/or license notice that predates the release of Scribus 1.3.2
5 for which a new license (GPL+exception) is in place.
6 */
7 /*
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or
11 (at your option) any later version.
12 */
13 
14 #include "pconsole.h"
15 
16 #include <QFileDialog>
17 
18 #include "commonstrings.h"
19 #include "iconmanager.h"
20 #include "prefscontext.h"
21 #include "prefsfile.h"
22 #include "prefsmanager.h"
23 #include "scribuscore.h"
24 
25 
PythonConsole(QWidget * parent)26 PythonConsole::PythonConsole( QWidget* parent)
27 	: QMainWindow( parent )
28 {
29 	setupUi(this);
30 	setWindowIcon(IconManager::instance().loadIcon("AppIcon.png"));
31 
32 	changedLabel = new QLabel(this);
33 	cursorTemplate = tr("Col: %1 Row: %2/%3");
34 	cursorLabel = new QLabel(this);
35 	statusBar()->addPermanentWidget(changedLabel);
36 	statusBar()->addPermanentWidget(cursorLabel);
37 
38 	action_Open->setIcon(IconManager::instance().loadIcon("16/document-open.png"));
39 	action_Save->setIcon(IconManager::instance().loadIcon("16/document-save.png"));
40 	actionSave_As->setIcon(IconManager::instance().loadIcon("16/document-save-as.png"));
41 	action_Exit->setIcon(IconManager::instance().loadIcon("exit.png"));
42 	action_Run->setIcon(IconManager::instance().loadIcon("ok.png"));
43 
44 	action_Open->setShortcut(tr("Ctrl+O"));
45 	action_Save->setShortcut(tr("Ctrl+S"));
46 	action_Run->setShortcut(Qt::Key_F9);
47 	actionRun_As_Console->setShortcut(Qt::CTRL + Qt::Key_F9);
48 
49 	commandEdit->setTabStopDistance(qRound(commandEdit->fontPointSize() * 4));
50 
51 	// install syntax highlighter.
52 	new SyntaxHighlighter(commandEdit);
53 
54 	languageChange();
55 	commandEdit_cursorPositionChanged();
56 
57 	// welcome note
58 	QString welcomeText(R"(""")");
59 	welcomeText += tr("Scribus Python Console");
60 	welcomeText += "\n\n";
61 	welcomeText += tr(
62 			"This is a standard Python console with some \n"
63 			"known limitations. Please consult the Scribus \n"
64 			"Scripter documentation for further information. ");
65 	welcomeText += "\"\"\"\n";
66 	commandEdit->setText(welcomeText);
67 	commandEdit->selectAll();
68 
69 	connect(commandEdit, SIGNAL(cursorPositionChanged()), this, SLOT(commandEdit_cursorPositionChanged()));
70 	connect(commandEdit->document(), SIGNAL(modificationChanged(bool)), this, SLOT(documentChanged(bool)));
71 
72 	connect(action_Open, SIGNAL(triggered()), this, SLOT(slot_open()));
73 	connect(action_Save, SIGNAL(triggered()), this, SLOT(slot_save()));
74 	connect(actionSave_As, SIGNAL(triggered()), this, SLOT(slot_saveAs()));
75 	connect(action_Exit, SIGNAL(triggered()), this, SLOT(slot_quit()));
76 	connect(action_Run, SIGNAL(triggered()), this, SLOT(slot_runScript()));
77 	connect(actionRun_As_Console, SIGNAL(triggered()), this, SLOT(slot_runScriptAsConsole()));
78 	connect(action_Save_Output, SIGNAL(triggered()), this, SLOT(slot_saveOutput()));
79 }
80 
81 PythonConsole::~PythonConsole() = default;
82 
updateSyntaxHighlighter()83 void PythonConsole::updateSyntaxHighlighter()
84 {
85 	new SyntaxHighlighter(commandEdit);
86 }
87 
setFonts()88 void PythonConsole::setFonts()
89 {
90 	QFont font = QFont("Fixed");
91 	font.setStyleHint(QFont::TypeWriter);
92 	font.setPointSize(PrefsManager::instance().appPrefs.uiPrefs.applicationFontSize);
93 	commandEdit->setFont(font);
94 	outputEdit->setFont(font);
95 }
96 
changeEvent(QEvent * e)97 void PythonConsole::changeEvent(QEvent *e)
98 {
99 	if (e->type() == QEvent::LanguageChange)
100 	{
101 		languageChange();
102 		return;
103 	}
104 	QMainWindow::changeEvent(e);
105 }
106 
closeEvent(QCloseEvent *)107 void PythonConsole::closeEvent(QCloseEvent *)
108 {
109 	emit paletteShown(false);
110 }
111 
commandEdit_cursorPositionChanged()112 void PythonConsole::commandEdit_cursorPositionChanged()
113 {
114 	QTextCursor cur(commandEdit->textCursor());
115 	cursorLabel->setText(cursorTemplate.arg(cur.columnNumber()+1)
116 										.arg(cur.blockNumber()+1)
117 										.arg(commandEdit->document()->blockCount()));
118 }
119 
documentChanged(bool state)120 void PythonConsole::documentChanged(bool state)
121 {
122 	changedLabel->setText(state ? "*" : " ");
123 }
124 
languageChange()125 void PythonConsole::languageChange()
126 {
127 	Ui::PythonConsole::retranslateUi(this);
128 
129 	cursorTemplate = tr("Col: %1 Row: %2/%3");
130 	commandEdit_cursorPositionChanged();
131 
132 	commandEdit->setToolTip( "<qt>" + tr("Write your commands here. A selection is processed as script.") + "</qt>");
133 	outputEdit->setToolTip( "<qt>" + tr("Output of your script") + "</qt>");
134 }
135 
slot_runScript()136 void PythonConsole::slot_runScript()
137 {
138 	outputEdit->clear();
139 
140 	//Prevent two scripts to be run concurrently or face crash!
141 	if (ScCore->primaryMainWindow()->scriptIsRunning())
142 	{
143 		outputEdit->append( tr("Another script is already running...") );
144 		outputEdit->append( tr("Please let it finish its task...") );
145 		return;
146 	}
147 
148 	parsePythonString();
149 	emit runCommand();
150 	commandEdit->textCursor().movePosition(QTextCursor::Start);
151 }
152 
slot_runScriptAsConsole()153 void PythonConsole::slot_runScriptAsConsole()
154 {
155 	//Prevent two scripts to be run concurrently or face crash!
156 	if (ScCore->primaryMainWindow()->scriptIsRunning())
157 	{
158 		outputEdit->append( tr("\n>>> Another script is already running...") );
159 		outputEdit->append( tr("Please let it finish its task...") );
160 		return;
161 	}
162 
163 	parsePythonString();
164 	commandEdit->clear();
165 	// content is destroyed. This is to prevent overwriting
166 	filename.clear();
167 	outputEdit->append("\n>>> " + m_command);
168 	emit runCommand();
169 }
170 
parsePythonString()171 void PythonConsole::parsePythonString()
172 {
173 	if (commandEdit->textCursor().hasSelection())
174 		m_command = commandEdit->textCursor().selectedText();
175 	else
176 	{
177 		commandEdit->selectAll();
178 		m_command = commandEdit->textCursor().selectedText();
179 	}
180 	// Per Qt doc, "If the selection obtained from an editor spans a line break, the text
181 	// will contain a Unicode U+2029 paragraph separator character instead of a newline"
182 	m_command.replace(QChar(0x2029), QChar('\n'));
183 	// prevent user's wrong selection
184 	m_command += '\n';
185 }
186 
187 /*
188  * supplementary slots. Saving etc.
189  */
slot_open()190 void PythonConsole::slot_open()
191 {
192 	filename = QFileDialog::getOpenFileName(this,
193 			tr("Open Python Script File"),
194 			".",
195 			tr("Python Scripts (*.py *.PY)"));
196 	if (filename.isNull())
197 		return;
198 	QFile file(filename);
199 	if (file.open(QIODevice::ReadOnly))
200 	{
201 		QTextStream stream(&file);
202 		commandEdit->setPlainText(stream.readAll());
203 		file.close();
204 	}
205 }
206 
slot_save()207 void PythonConsole::slot_save()
208 {
209 	if (filename.isNull())
210 	{
211 		slot_saveAs();
212 		return;
213 	}
214 	QFile f(filename);
215 	if (f.open(QIODevice::WriteOnly))
216 	{
217 		QTextStream stream(&f);
218 		stream << commandEdit->toPlainText();
219 		f.close();
220 	}
221 }
222 
slot_saveAs()223 void PythonConsole::slot_saveAs()
224 {
225 	QString oldFname = filename;
226 	QString dirName  = QDir::homePath();
227 	if (!filename.isEmpty())
228 	{
229 		QFileInfo fInfo(filename);
230 		QDir fileDir = fInfo.absoluteDir();
231 		if (fileDir.exists())
232 			dirName = fileDir.absolutePath();
233 	}
234 	filename = QFileDialog::getSaveFileName(this,
235 			tr("Save the Python Commands in File"),
236 			dirName,
237 			tr("Python Scripts (*.py *.PY)"));
238 	if (filename.isEmpty())
239 	{
240 		filename = oldFname;
241 		return;
242 	}
243 	slot_save();
244 }
245 
slot_saveOutput()246 void PythonConsole::slot_saveOutput()
247 {
248 	QString dname = QDir::homePath();
249 	QString fname = QFileDialog::getSaveFileName(this,
250 			tr("Save Current Output"),
251 			dname,
252 			tr("Text Files (*.txt)"));
253 	if (fname.isEmpty())
254 		return;
255 	QFile f(fname);
256 	// save
257 	if (f.open(QIODevice::WriteOnly))
258 	{
259 		QTextStream stream(&f);
260 		stream << outputEdit->toPlainText();
261 		f.close();
262 	}
263 }
264 
slot_quit()265 void PythonConsole::slot_quit()
266 {
267 	emit paletteShown(false);
268 }
269 
270 /*
271  * Syntax highlighting
272  */
SyntaxHighlighter(QTextEdit * textEdit)273 SyntaxHighlighter::SyntaxHighlighter(QTextEdit *textEdit) : QSyntaxHighlighter(textEdit)
274 {
275 	// Reserved keywords in Python 2.4
276 	QStringList keywords;
277 	HighlightingRule rule;
278 
279 	keywords << "and" << "assert" << "break" << "class" << "continue" << "def"
280 			 << "del" << "elif" << "else" << "except" << "exec" << "finally"
281 			 << "for" << "from" << "global" << "if" << "import" << "in"
282 			 << "is" << "lambda" << "not" << "or" << "pass" << "print" << "raise"
283 			 << "return" << "try" << "while" << "yield";
284 
285 	keywordFormat.setForeground(colors.keywordColor);
286 	keywordFormat.setFontWeight(QFont::Bold);
287 	singleLineCommentFormat.setForeground(colors.commentColor);
288 	singleLineCommentFormat.setFontItalic(true);
289 	quotationFormat.setForeground(colors.stringColor);
290 	numberFormat.setForeground(colors.numberColor);
291 	operatorFormat.setForeground(colors.signColor);
292 
293 	foreach (const QString& kw, keywords)
294 	{
295 		rule.pattern = QRegExp("\\b" + kw + "\\b", Qt::CaseInsensitive);
296 		rule.format = keywordFormat;
297 		highlightingRules.append(rule);
298 	}
299 
300 	rule.pattern = QRegExp("#[^\n]*");
301 	rule.format = singleLineCommentFormat;
302 	highlightingRules.append(rule);
303 
304 	rule.pattern = QRegExp("\'.*\'");
305 	rule.pattern.setMinimal(true);
306 	rule.format = quotationFormat;
307 	highlightingRules.append(rule);
308 
309 	rule.pattern = QRegExp("\".*\"");
310 	rule.pattern.setMinimal(true);
311 	rule.format = quotationFormat;
312 	highlightingRules.append(rule);
313 
314 	rule.pattern = QRegExp("\\b\\d+\\b");
315 	rule.pattern.setMinimal(true);
316 	rule.format = numberFormat;
317 	highlightingRules.append(rule);
318 
319 	rule.pattern = QRegExp("[\\\\|\\<|\\>|\\=|\\!|\\+|\\-|\\*|\\/|\\%]+");
320 	rule.pattern.setMinimal(true);
321 	rule.format = operatorFormat;
322 	highlightingRules.append(rule);
323 }
324 
highlightBlock(const QString & text)325 void SyntaxHighlighter::highlightBlock(const QString &text)
326 {
327 	// Apply default text color
328 	setFormat(0, text.length(), colors.textColor);
329 
330 	foreach (HighlightingRule rule, highlightingRules)
331 	{
332 		QRegExp expression(rule.pattern);
333 		int index = expression.indexIn(text);
334 		while (index >= 0)
335 		{
336 			int length = expression.matchedLength();
337 			setFormat(index, length, rule.format);
338 			index = expression.indexIn(text, index + length);
339 		}
340 	}
341 	setCurrentBlockState(0);
342 
343 	// multiline strings handling
344 	int startIndex = 0;
345 	if (previousBlockState() != 1)
346 		startIndex = text.indexOf("\"\"\"");
347 
348 	while (startIndex >= 0)
349 	{
350 		int endIndex = text.indexOf("\"\"\"", startIndex);
351 		int commentLength;
352 
353 		if (endIndex == -1)
354 		{
355 			setCurrentBlockState(1);
356 			commentLength = text.length() - startIndex;
357 		}
358 		else
359 		{
360 			commentLength = endIndex - startIndex + 3;//commentEndExpression.matchedLength();
361 		}
362 		setFormat(startIndex, commentLength, quotationFormat);
363 		startIndex = text.indexOf("\"\"\"", startIndex + commentLength);
364 	}
365 }
366 
SyntaxColors()367 SyntaxColors::SyntaxColors()
368 {
369 	PrefsContext* prefs = PrefsManager::instance().prefsFile->getPluginContext("scriptplugin");
370 	if (prefs)
371 	{
372 		errorColor.setNamedColor(prefs->get("syntaxerror", "#aa0000"));
373 		commentColor.setNamedColor(prefs->get("syntaxcomment", "#A0A0A0"));
374 		keywordColor.setNamedColor(prefs->get("syntaxkeyword", "#00007f"));
375 		signColor.setNamedColor(prefs->get("syntaxsign", "#aa00ff"));
376 		numberColor.setNamedColor(prefs->get("syntaxnumber", "#ffaa00"));
377 		stringColor.setNamedColor(prefs->get("syntaxstring", "#005500"));
378 		textColor.setNamedColor(prefs->get("syntaxtext", "#000000"));
379 	}
380 	else
381 	{
382 		errorColor.setNamedColor("#aa0000");
383 		commentColor.setNamedColor("#A0A0A0");
384 		keywordColor.setNamedColor("#00007f");
385 		signColor.setNamedColor("#aa00ff");
386 		numberColor.setNamedColor("#ffaa00");
387 		stringColor.setNamedColor("#005500");
388 		textColor.setNamedColor("#000000");
389 	}
390 }
391 
saveToPrefs()392 void SyntaxColors::saveToPrefs()
393 {
394 	PrefsContext* prefs = PrefsManager::instance().prefsFile->getPluginContext("scriptplugin");
395 	if (!prefs)
396 		return;
397 	prefs->set("syntaxerror", qcolor2named(errorColor));
398 	prefs->set("syntaxcomment", qcolor2named(commentColor));
399 	prefs->set("syntaxkeyword", qcolor2named(keywordColor));
400 	prefs->set("syntaxsign", qcolor2named(signColor));
401 	prefs->set("syntaxnumber", qcolor2named(numberColor));
402 	prefs->set("syntaxstring", qcolor2named(stringColor));
403 	prefs->set("syntaxtext", qcolor2named(textColor));
404 }
405 
qcolor2named(const QColor & color)406 QString SyntaxColors::qcolor2named(const QColor& color)
407 {
408 	int r;
409 	int g;
410 	int b;
411 	QString retval("#");
412 	QString oct;
413 	color.getRgb(&r, &g, &b);
414 	retval += oct.setNum(r, 16).rightJustified(2, '0');
415 	retval += oct.setNum(g, 16).rightJustified(2, '0');
416 	retval += oct.setNum(b, 16).rightJustified(2, '0');
417 	return retval;
418 }
419