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