1
/***************************************************************************
2
* Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net> *
4
* This file is part of the FreeCAD CAx development system. *
6
* This library is free software; you can redistribute it and/or *
7
* modify it under the terms of the GNU Library General Public *
8
* License as published by the Free Software Foundation; either *
9
* version 2 of the License, or (at your option) any later version. *
11
* This library 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 Library General Public License for more details. *
16
* You should have received a copy of the GNU Library General Public *
17
* License along with this library; see the file COPYING.LIB. If not, *
18
* write to the Free Software Foundation, Inc., 59 Temple Place, *
19
* Suite 330, Boston, MA 02111-1307, USA *
21
***************************************************************************/
23
#include "PreCompiled.h"
25
# include <QApplication>
27
# include <QDockWidget>
30
# include <QMessageBox>
32
# include <QTextCursor>
33
# include <QTextDocumentFragment>
34
# include <QTextStream>
39
#include <Base/Interpreter.h>
42
#include "PythonConsole.h"
43
#include "PythonConsolePy.h"
44
#include "PythonTracing.h"
45
#include "Application.h"
47
#include "FileDialog.h"
48
#include "MainWindow.h"
57
static const QChar promptEnd( QLatin1Char(' ') ); //< char for detecting prompt end
59
inline int promptLength( const QString &lineStr )
61
return lineStr.indexOf( promptEnd ) + 1;
64
inline QString stripPromptFrom( const QString &lineStr )
66
return lineStr.mid( promptLength(lineStr) );
70
* cursorBeyond checks if cursor is at a valid position to accept keyEvents.
71
* @param cursor - cursor to check
72
* @param limit - cursor that marks the begin of the input region
73
* @param shift - offset for shifting the limit for non-selection cursors [default: 0]
74
* @return true if a keyEvent is ok at cursor's position, false otherwise
76
inline bool cursorBeyond( const QTextCursor &cursor, const QTextCursor &limit, int shift = 0 )
78
int pos = limit.position();
79
if (cursor.hasSelection()) {
80
return (cursor.selectionStart() >= pos && cursor.selectionEnd() >= pos);
83
return cursor.position() >= (pos + shift);
88
enum Output {Error = 20, Message = 21};
89
enum CopyType {Normal, History, Command};
91
PyObject *_stdoutPy=nullptr, *_stderrPy=nullptr, *_stdinPy=nullptr, *_stdin=nullptr;
92
InteractiveInterpreter* interpreter=nullptr;
93
CallTipsList* callTipsList=nullptr;
94
ConsoleHistory history;
95
QString output, error, info, historyFile;
96
QStringList statements;
98
QMap<QString, QColor> colormap; // Color map
99
ParameterGrp::handle hGrpSettings;
104
historyFile = QString::fromUtf8((App::Application::getUserAppDataDir() + "PythonHistory.log").c_str());
105
colormap[QLatin1String("Text")] = qApp->palette().windowText().color();
106
colormap[QLatin1String("Bookmark")] = Qt::cyan;
107
colormap[QLatin1String("Breakpoint")] = Qt::red;
108
colormap[QLatin1String("Keyword")] = Qt::blue;
109
colormap[QLatin1String("Comment")] = QColor(0, 170, 0);
110
colormap[QLatin1String("Block comment")] = QColor(160, 160, 164);
111
colormap[QLatin1String("Number")] = Qt::blue;
112
colormap[QLatin1String("String")] = Qt::red;
113
colormap[QLatin1String("Character")] = Qt::red;
114
colormap[QLatin1String("Class name")] = QColor(255, 170, 0);
115
colormap[QLatin1String("Define name")] = QColor(255, 170, 0);
116
colormap[QLatin1String("Operator")] = QColor(160, 160, 164);
117
colormap[QLatin1String("Python output")] = QColor(170, 170, 127);
118
colormap[QLatin1String("Python error")] = Qt::red;
122
struct InteractiveInterpreterP
124
PyObject* interpreter{nullptr};
125
PyObject* sysmodule{nullptr};
132
InteractiveInterpreter::InteractiveInterpreter()
134
// import code.py and create an instance of InteractiveInterpreter
135
Base::PyGILStateLocker lock;
136
PyObject* module = PyImport_ImportModule("code");
138
throw Base::PyException();
140
PyObject* func = PyObject_GetAttrString(module, "InteractiveInterpreter");
141
PyObject* args = Py_BuildValue("()");
142
d = new InteractiveInterpreterP;
143
#if PY_VERSION_HEX < 0x03090000
144
d->interpreter = PyEval_CallObject(func,args);
146
d->interpreter = PyObject_CallObject(func,args);
155
InteractiveInterpreter::~InteractiveInterpreter()
157
Base::PyGILStateLocker lock;
158
Py_XDECREF(d->interpreter);
159
Py_XDECREF(d->sysmodule);
164
* Set the ps1 and ps2 members of the sys module if not yet defined.
166
void InteractiveInterpreter::setPrompt()
168
// import code.py and create an instance of InteractiveInterpreter
169
Base::PyGILStateLocker lock;
170
d->sysmodule = PyImport_ImportModule("sys");
171
if (!PyObject_HasAttrString(d->sysmodule, "ps1")) {
172
PyObject_SetAttrString(d->sysmodule, "ps1", PyUnicode_FromString(">>> "));
174
if (!PyObject_HasAttrString(d->sysmodule, "ps2")) {
175
PyObject_SetAttrString(d->sysmodule, "ps2", PyUnicode_FromString("... "));
180
* Compile a command and determine whether it is incomplete.
182
* The source string may contain line feeds and/or carriage returns. \n
183
* Return value / exceptions raised:
184
* - Return a code object if the command is complete and valid
185
* - Return None if the command is incomplete
186
* - Raise SyntaxError, ValueError or OverflowError if the command is a
187
* syntax error (OverflowError and ValueError can be produced by
188
* malformed literals).
190
PyObject* InteractiveInterpreter::compile(const char* source) const
192
Base::PyGILStateLocker lock;
193
PyObject* func = PyObject_GetAttrString(d->interpreter, "compile");
194
PyObject* args = Py_BuildValue("(s)", source);
195
#if PY_VERSION_HEX < 0x03090000
196
PyObject* eval = PyEval_CallObject(func,args); // must decref later
198
PyObject* eval = PyObject_CallObject(func,args); // must decref later
207
// do not throw Base::PyException as this clears the error indicator
208
throw Base::RuntimeError("Code evaluation failed");
216
* Compile a command and determine whether it is incomplete.
218
* The source string may contain line feeds and/or carriage returns. \n
220
* - Return 1 if the command is incomplete
221
* - Return 0 if the command is complete and valid
222
* - Return -1 if the command is a syntax error
224
* (OverflowError and ValueError can be produced by malformed literals).
226
int InteractiveInterpreter::compileCommand(const char* source) const
228
Base::PyGILStateLocker lock;
229
PyObject* func = PyObject_GetAttrString(d->interpreter, "compile");
230
PyObject* args = Py_BuildValue("(s)", source);
231
#if PY_VERSION_HEX < 0x03090000
232
PyObject* eval = PyEval_CallObject(func,args); // must decref later
234
PyObject* eval = PyObject_CallObject(func,args); // must decref later
242
if (PyObject_TypeCheck(Py_None, eval->ob_type)) {
243
ret = 1; // incomplete
257
* Compile and run some source in the interpreter.
259
* One several things can happen:
261
* - The input is incorrect; compile() raised an exception (SyntaxError or OverflowError).
262
* A syntax traceback will be printed by calling Python's PyErr_Print() method to the redirected stderr.
264
* - The input is incomplete, and more input is required; compile() returned 'None'.
267
* - The input is complete; compile() returned a code object. The code is executed by calling
268
* runCode() (which also handles run-time exceptions, except for SystemExit).
270
* The return value is True if the input is incomplete, False in the other cases (unless
271
* an exception is raised). The return value can be used to decide whether to use sys.ps1
272
* or sys.ps2 to prompt the next line.
274
bool InteractiveInterpreter::runSource(const char* source) const
276
Base::PyGILStateLocker lock;
279
code = compile(source);
280
} catch (const Base::Exception&) {
281
// A system, overflow or value error was raised.
282
// We clear the traceback info as this might be a longly
283
// message we don't need.
284
PyObject *errobj, *errdata, *errtraceback;
285
PyErr_Fetch(&errobj, &errdata, &errtraceback);
286
PyErr_Restore(errobj, errdata, nullptr);
287
// print error message
288
if (PyErr_Occurred()) PyErr_Print();
292
// the command is incomplete
293
if (PyObject_TypeCheck(Py_None, code->ob_type)) {
298
// run the code and return false
299
runCode((PyCodeObject*)code);
303
bool InteractiveInterpreter::isOccupied() const
305
return d->trace.isActive();
308
bool InteractiveInterpreter::interrupt() const
310
return d->trace.interrupt();
313
/* Execute a code object.
315
* When an exception occurs, a traceback is displayed.
316
* All exceptions are caught except SystemExit, which is reraised.
318
void InteractiveInterpreter::runCode(PyCodeObject* code) const
324
d->trace.fetchFromSettings();
325
PythonTracingLocker tracelock(d->trace);
327
Base::PyGILStateLocker lock;
328
PyObject *module, *dict, *presult; /* "exec code in d, d" */
329
module = PyImport_AddModule("__main__"); /* get module, init python */
331
throw Base::PyException(); /* not incref'd */
333
dict = PyModule_GetDict(module); /* get dict namespace */
335
throw Base::PyException(); /* not incref'd */
338
// It seems that the return value is always 'None' or Null
339
presult = PyEval_EvalCode((PyObject*)code, dict, dict); /* run compiled bytecode */
340
Py_XDECREF(code); /* decref the code object */
342
if (PyErr_ExceptionMatches(PyExc_SystemExit)) {
343
// throw SystemExit exception
344
throw Base::SystemExitException();
346
if (PyErr_Occurred()) { /* get latest python exception information */
347
PyObject *errobj, *errdata, *errtraceback;
348
PyErr_Fetch(&errobj, &errdata, &errtraceback);
349
// the error message can be empty so errdata will be null
350
if (errdata && PyDict_Check(errdata)) {
351
PyObject* value = PyDict_GetItemString(errdata, "swhat");
353
Base::RuntimeError e;
354
e.setPyObject(errdata);
357
std::stringstream str;
359
if (!e.getFunction().empty()) {
360
str << " In " << e.getFunction();
362
if (!e.getFile().empty() && e.getLine() > 0) {
363
std::string file = e.getFile();
364
std::size_t pos = file.find("src");
365
if (pos!=std::string::npos) {
366
file = file.substr(pos);
368
str << " in " << file << ":" << e.getLine();
371
std::string err = str.str();
372
errdata = PyUnicode_FromString(err.c_str());
375
PyErr_Restore(errobj, errdata, errtraceback);
376
PyErr_Print(); /* and print the error to the error output */
384
* Store the line into the internal buffer and compile the total buffer.
385
* In case it is a complete Python command the buffer is emptied.
387
bool InteractiveInterpreter::push(const char* line)
389
d->buffer.append(QString::fromUtf8(line));
390
QString source = d->buffer.join(QLatin1String("\n"));
392
bool more = runSource(source.toUtf8());
397
} catch (const Base::SystemExitException&) {
401
// indication of unhandled exception
403
if (PyErr_Occurred()) {
412
bool InteractiveInterpreter::hasPendingInput( ) const
414
return (!d->buffer.isEmpty());
417
QStringList InteractiveInterpreter::getBuffer() const
422
void InteractiveInterpreter::setBuffer(const QStringList& buf)
427
void InteractiveInterpreter::clearBuffer()
432
/* TRANSLATOR Gui::PythonConsole */
435
* Constructs a PythonConsole which is a child of 'parent'.
437
PythonConsole::PythonConsole(QWidget *parent)
438
: TextEdit(parent), WindowParameter( "Editor" ), _sourceDrain(nullptr)
440
d = new PythonConsoleP();
441
d->interactive = false;
443
// create an instance of InteractiveInterpreter
445
d->interpreter = new InteractiveInterpreter();
446
} catch (const Base::Exception& e) {
447
setPlainText(QString::fromLatin1(e.what()));
451
// use the console highlighter
452
pythonSyntax = new PythonConsoleHighlighter(this);
453
pythonSyntax->setDocument(this->document());
455
// create the window for call tips
456
d->callTipsList = new CallTipsList(this);
457
d->callTipsList->setFrameStyle(QFrame::Box);
458
d->callTipsList->setFrameShadow(QFrame::Raised);
459
d->callTipsList->setLineWidth(2);
460
installEventFilter(d->callTipsList);
461
viewport()->installEventFilter(d->callTipsList);
462
d->callTipsList->setSelectionMode( QAbstractItemView::SingleSelection );
463
d->callTipsList->hide();
465
QFont serifFont(QLatin1String("Courier"), 10, QFont::Normal);
468
// set colors and font from settings
469
ParameterGrp::handle hPrefGrp = getWindowParameter();
470
hPrefGrp->Attach(this);
471
hPrefGrp->NotifyAll();
473
d->hGrpSettings = WindowParameter::getDefaultParameter()->GetGroup("PythonConsole");
474
d->hGrpSettings->Attach(this);
475
d->hGrpSettings->NotifyAll();
477
// disable undo/redo stuff
478
setUndoRedoEnabled( false );
479
setAcceptDrops( true );
481
// try to override Python's stdout/err
482
Base::PyGILStateLocker lock;
483
d->_stdoutPy = new PythonStdout(this);
484
d->_stderrPy = new PythonStderr(this);
485
d->_stdinPy = new PythonStdin (this);
486
d->_stdin = PySys_GetObject("stdin");
488
// Don't override stdin when running FreeCAD as Python module
489
auto& cfg = App::Application::Config();
490
auto overrideStdIn = cfg.find("DontOverrideStdIn");
491
if (overrideStdIn == cfg.end()) {
492
PySys_SetObject("stdin", d->_stdinPy);
495
const char* version = PyUnicode_AsUTF8(PySys_GetObject("version"));
496
const char* platform = PyUnicode_AsUTF8(PySys_GetObject("platform"));
497
d->info = QString::fromLatin1("Python %1 on %2\n"
498
"Type 'help', 'copyright', 'credits' or 'license' for more information.")
499
.arg(QString::fromLatin1(version), QString::fromLatin1(platform));
501
printPrompt(PythonConsole::Complete);
504
flusher = new QTimer(this);
505
connect(flusher, &QTimer::timeout, this, &PythonConsole::flushOutput);
509
/** Destroys the object and frees any allocated resources */
510
PythonConsole::~PythonConsole()
513
Base::PyGILStateLocker lock;
514
d->hGrpSettings->Detach(this);
515
getWindowParameter()->Detach(this);
517
Py_XDECREF(d->_stdoutPy);
518
Py_XDECREF(d->_stderrPy);
519
Py_XDECREF(d->_stdinPy);
520
delete d->interpreter;
524
/** Set new font and colors according to the parameters. */
525
void PythonConsole::OnChange(Base::Subject<const char*> &rCaller, const char* sReason )
527
const auto & rGrp = static_cast<ParameterGrp &>(rCaller);
529
if (strcmp(sReason, "PythonWordWrap") == 0) {
530
bool pythonWordWrap = rGrp.GetBool("PythonWordWrap", true);
531
if (pythonWordWrap) {
532
setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
535
setWordWrapMode(QTextOption::NoWrap);
539
if (strcmp(sReason, "FontSize") == 0 || strcmp(sReason, "Font") == 0) {
540
int fontSize = rGrp.GetInt("FontSize", 10);
541
QString fontFamily = QString::fromLatin1(rGrp.GetASCII("Font", "Courier").c_str());
543
QFont font(fontFamily, fontSize);
545
QFontMetrics metric(font);
546
int width = QtTools::horizontalAdvance(metric, QLatin1String("0000"));
547
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
548
setTabStopWidth(width);
550
setTabStopDistance(width);
554
QMap<QString, QColor>::Iterator it = d->colormap.find(QString::fromLatin1(sReason));
555
if (it != d->colormap.end()) {
556
QColor color = it.value();
557
unsigned int col = App::Color::asPackedRGB<QColor>(color);
558
auto value = static_cast<unsigned long>(col);
559
value = rGrp.GetUnsigned(sReason, value);
560
col = static_cast<unsigned int>(value);
561
color.setRgb((col>>24)&0xff, (col>>16)&0xff, (col>>8)&0xff);
562
pythonSyntax->setColor(QString::fromLatin1(sReason), color);
566
if (strcmp(sReason, "PythonBlockCursor") == 0) {
567
bool block = rGrp.GetBool("PythonBlockCursor", false);
569
setCursorWidth(QFontMetrics(font()).averageCharWidth());
578
* Checks the input of the console to make the correct indentations.
579
* After a command is prompted completely the Python interpreter is started.
581
void PythonConsole::keyPressEvent(QKeyEvent * e)
583
bool restartHistory = true;
584
QTextCursor cursor = this->textCursor();
585
QTextCursor inputLineBegin = this->inputBegin();
587
if (e->key() == Qt::Key_C && e->modifiers() == Qt::ControlModifier) {
588
if (d->interpreter->interrupt()) {
593
if (!cursorBeyond( cursor, inputLineBegin ))
596
* The cursor is placed not on the input line (or within the prompt string)
597
* So we handle key input as follows:
598
* - don't allow changing previous lines.
599
* - allow full movement (no prompt restriction)
600
* - allow copying content (Ctrl+C)
601
* - "escape" to end of input line
608
case Qt::Key_Backspace:
609
this->moveCursor( QTextCursor::End );
613
if (e->text().isEmpty() ||
614
e->matches(QKeySequence::Copy) ||
615
e->matches(QKeySequence::SelectAll)) {
616
TextEdit::keyPressEvent(e);
618
else if (!e->text().isEmpty() &&
619
(e->modifiers() == Qt::NoModifier ||
620
e->modifiers() == Qt::ShiftModifier)) {
621
this->moveCursor(QTextCursor::End);
622
TextEdit::keyPressEvent(e);
630
* The cursor sits somewhere on the input line (after the prompt)
631
* Here we handle key input a bit different:
632
* - restrict cursor movement to input line range (excluding the prompt characters)
633
* - roam the history by Up/Down keys
634
* - show call tips on period
636
QTextBlock inputBlock = inputLineBegin.block(); //< get the last paragraph's text
637
QString inputLine = inputBlock.text();
638
QString inputStrg = stripPromptFrom( inputLine );
639
if (this->_sourceDrain && !this->_sourceDrain->isEmpty()) {
640
inputStrg = inputLine.mid(this->_sourceDrain->length());
647
// disable current input string - i.e. put it to history but don't execute it.
648
if (!inputStrg.isEmpty())
650
d->history.append( QLatin1String("# ") + inputStrg ); //< put commented string to history ...
651
inputLineBegin.insertText( QString::fromLatin1("# ") ); //< and comment it on console
652
setTextCursor( inputLineBegin );
653
printPrompt(d->interpreter->hasPendingInput() //< print adequate prompt
654
? PythonConsole::Incomplete
655
: PythonConsole::Complete);
662
d->history.append( inputStrg ); //< put statement to history
663
runSource( inputStrg ); //< commit input string
668
// In Qt 4.8 there is a strange behaviour because when pressing ":"
669
// then key is also set to 'Period' instead of 'Colon'. So we have
670
// to make sure we only handle the period.
671
if (e->text() == QLatin1String(".")) {
672
// analyse context and show available call tips
673
int contextLength = cursor.position() - inputLineBegin.position();
674
TextEdit::keyPressEvent(e);
675
d->callTipsList->showTips( inputStrg.left( contextLength ) );
678
TextEdit::keyPressEvent(e);
684
QTextCursor::MoveMode mode = (e->modifiers() & Qt::ShiftModifier)? QTextCursor::KeepAnchor
685
/* else */ : QTextCursor::MoveAnchor;
686
cursor.setPosition( inputLineBegin.position(), mode );
687
setTextCursor( cursor );
688
ensureCursorVisible();
693
// if possible, move back in history
694
if (d->history.prev( inputStrg ))
695
{ overrideCursor( d->history.value() ); }
696
restartHistory = false;
701
// if possible, move forward in history
702
if (d->history.next())
703
{ overrideCursor( d->history.value() ); }
704
restartHistory = false;
709
if (cursor > inputLineBegin)
710
{ TextEdit::keyPressEvent(e); }
711
restartHistory = false;
716
TextEdit::keyPressEvent(e);
717
restartHistory = false;
720
case Qt::Key_Backspace:
722
if (cursorBeyond( cursor, inputLineBegin, +1 ))
723
{ TextEdit::keyPressEvent(e); }
728
TextEdit::keyPressEvent(e);
731
// This can't be done in CallTipsList::eventFilter() because we must first perform
732
// the event and afterwards update the list widget
733
if (d->callTipsList->isVisible())
734
{ d->callTipsList->validateCursor(); }
736
// disable history restart if input line changed
737
restartHistory &= (inputLine != inputBlock.text());
739
// any cursor move resets the history to its latest item.
740
if (restartHistory) {
741
d->history.restart();
746
* Insert an output message to the console. This message comes from
747
* the Python interpreter and is redirected from sys.stdout.
749
void PythonConsole::insertPythonOutput( const QString& msg )
755
* Insert an error message to the console. This message comes from
756
* the Python interpreter and is redirected from sys.stderr.
758
void PythonConsole::insertPythonError ( const QString& err )
763
void PythonConsole::onFlush()
765
printPrompt(PythonConsole::Flush);
768
void PythonConsole::flushOutput()
770
if (d->interpreter->isOccupied()) {
771
if (d->output.length() > 0 || d->error.length() > 0) {
772
printPrompt(PythonConsole::Complete);
777
/** Prints the ps1 prompt (>>> ) for complete and ps2 prompt (... ) for
778
* incomplete commands to the console window.
780
void PythonConsole::printPrompt(PythonConsole::Prompt mode)
782
// write normal messages
783
if (!d->output.isEmpty()) {
784
appendOutput(d->output, (int)PythonConsoleP::Message);
788
// write error messages
789
if (!d->error.isEmpty()) {
790
appendOutput(d->error, (int)PythonConsoleP::Error);
794
// Append the prompt string
795
QTextCursor cursor = textCursor();
797
if (mode != PythonConsole::Special)
799
cursor.beginEditBlock();
800
cursor.movePosition(QTextCursor::End);
801
QTextBlock block = cursor.block();
803
// Python's print command appends a trailing '\n' to the system output.
804
// In this case, however, we should not add a new text block. We force
805
// the current block to be normal text (user state = 0) to be highlighted
806
// correctly and append the '>>> ' or '... ' to this block.
807
if (block.length() > 1)
808
cursor.insertBlock(cursor.blockFormat(), cursor.charFormat());
810
block.setUserState(0);
814
case PythonConsole::Incomplete:
815
cursor.insertText(QString::fromLatin1("... "));
817
case PythonConsole::Complete:
818
cursor.insertText(QString::fromLatin1(">>> "));
823
cursor.endEditBlock();
825
// move cursor to the end
826
cursor.movePosition(QTextCursor::End);
827
setTextCursor(cursor);
831
* Appends \a output to the console and set \a state as user state to
832
* the text block which is needed for the highlighting.
834
void PythonConsole::appendOutput(const QString& output, int state)
836
QTextCursor cursor = textCursor();
837
cursor.movePosition(QTextCursor::End);
838
int pos = cursor.position() + 1;
840
// delay rehighlighting
841
cursor.beginEditBlock();
842
appendPlainText(output);
844
QTextBlock block = this->document()->findBlock(pos);
845
while (block.isValid()) {
846
block.setUserState(state);
847
block = block.next();
849
cursor.endEditBlock(); // start highlightiong
853
* Builds up the Python command and pass it to the interpreter.
855
void PythonConsole::runSource(const QString& line)
858
* Check if there's a "source drain", which wants to consume the source in another way then just executing it.
859
* If so, put the source to the drain and emit a signal to notify the consumer, whomever this may be.
861
if (this->_sourceDrain) {
862
*this->_sourceDrain = line;
863
Q_EMIT pendingSource();
867
if (d->interpreter->isOccupied()) {
868
insertPythonError(QString::fromLatin1("Previous command still running!"));
872
bool incomplete = false;
873
Base::PyGILStateLocker lock;
874
PyObject* default_stdout = PySys_GetObject("stdout");
875
PyObject* default_stderr = PySys_GetObject("stderr");
876
PySys_SetObject("stdout", d->_stdoutPy);
877
PySys_SetObject("stderr", d->_stderrPy);
878
d->interactive = true;
881
d->history.markScratch(); //< mark current history position ...
882
// launch the command now
883
incomplete = d->interpreter->push(line.toUtf8());
885
d->history.doScratch();
886
} //< ... and scratch history entries that might have been added by executing the line.
887
setFocus(); // if focus was lost
889
catch (const Base::SystemExitException&) {
890
// In Python the exception must be cleared because when the message box below appears
891
// callable Python objects can be invoked and due to a failing assert the application
895
ParameterGrp::handle hPrefGrp = getWindowParameter();
896
bool check = hPrefGrp->GetBool("CheckSystemExit",true);
897
int ret = QMessageBox::Yes;
899
ret = QMessageBox::question(this, tr("System exit"),
900
tr("The application is still running.\nDo you want to exit without saving your data?"),
901
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
903
if (ret == QMessageBox::Yes) {
911
catch (const Py::Exception&) {
912
QMessageBox::critical(this, tr("Python console"), tr("Unhandled PyCXX exception."));
914
catch (const Base::Exception&) {
915
QMessageBox::critical(this, tr("Python console"), tr("Unhandled FreeCAD exception."));
917
catch (const std::exception&) {
918
QMessageBox::critical(this, tr("Python console"), tr("Unhandled std C++ exception."));
921
QMessageBox::critical(this, tr("Python console"), tr("Unhandled unknown C++ exception."));
924
printPrompt(incomplete ? PythonConsole::Incomplete
925
: PythonConsole::Complete);
926
PySys_SetObject("stdout", default_stdout);
927
PySys_SetObject("stderr", default_stderr);
928
d->interactive = false;
929
for (const auto & it : d->statements) {
932
d->statements.clear();
935
bool PythonConsole::isComment(const QString& source) const
937
if (source.isEmpty()) {
941
while (i < source.length()) {
942
QChar ch = source.at(i++);
946
else if (ch == QLatin1Char('#')) {
958
* Prints the Python statement cmd to the console.
959
* @note The statement gets only printed and added to the history but not invoked.
961
void PythonConsole::printStatement( const QString& cmd )
963
// If we are in interactive mode we have to wait until the command is finished,
964
// afterwards we can print the statements.
965
if (d->interactive) {
966
d->statements << cmd;
970
QTextCursor cursor = textCursor();
971
QStringList statements = cmd.split(QLatin1String("\n"));
972
for (const auto & statement : statements) {
973
// go to the end before inserting new text
974
cursor.movePosition(QTextCursor::End);
975
cursor.insertText( statement );
976
d->history.append( statement );
977
printPrompt(PythonConsole::Complete);
982
* Shows the Python window and sets the focus to set text cursor.
984
void PythonConsole::showEvent (QShowEvent * e)
986
TextEdit::showEvent(e);
987
// set also the text cursor to the edit field
991
void PythonConsole::visibilityChanged (bool visible)
998
void PythonConsole::changeEvent(QEvent *e)
1000
if (e->type() == QEvent::ParentChange) {
1001
auto dw = qobject_cast<QDockWidget*>(this->parentWidget());
1003
connect(dw, &QDockWidget::visibilityChanged, this, &PythonConsole::visibilityChanged);
1006
else if (e->type() == QEvent::StyleChange) {
1007
QPalette pal = qApp->palette();
1008
QColor color = pal.windowText().color();
1009
unsigned int text = App::Color::asPackedRGB<QColor>(color);
1010
auto value = static_cast<unsigned long>(text);
1011
// if this parameter is not already set use the style's window text color
1012
value = getWindowParameter()->GetUnsigned("Text", value);
1013
getWindowParameter()->SetUnsigned("Text", value);
1015
TextEdit::changeEvent(e);
1018
void PythonConsole::mouseReleaseEvent( QMouseEvent *e )
1020
if (e->button() == Qt::MiddleButton && e->spontaneous()) {
1021
// on Linux-like systems the middle mouse button is typically connected to a paste operation
1022
// which will insert some text at the mouse position
1023
QTextCursor cursor = this->textCursor();
1024
if (cursor < this->inputBegin()) {
1025
cursor.movePosition( QTextCursor::End );
1026
this->setTextCursor( cursor );
1028
// the text will be pasted at the cursor position (as for Ctrl-V operation)
1029
QRect newPos = this->cursorRect();
1031
// Now we must amend the received event and pass forward. As e->setLocalPos() is only
1032
// available in Qt>=5.8, let's stop the original event propagation and generate a fake event
1033
// with corrected pointer position (inside the prompt line of the widget)
1034
#if QT_VERSION < QT_VERSION_CHECK(6,4,0)
1035
QMouseEvent newEv(e->type(), QPoint(newPos.x(),newPos.y()),
1036
e->button(), e->buttons(), e->modifiers());
1038
QMouseEvent newEv(e->type(), QPoint(newPos.x(),newPos.y()), e->globalPosition(),
1039
e->button(), e->buttons(), e->modifiers());
1042
QCoreApplication::sendEvent(this->viewport(), &newEv);
1045
TextEdit::mouseReleaseEvent( e );
1049
* Drops the event \a e and writes the right Python command.
1051
void PythonConsole::dropEvent (QDropEvent * e)
1053
const QMimeData* mimeData = e->mimeData();
1054
if (mimeData->hasFormat(QLatin1String("text/x-action-items"))) {
1055
QByteArray itemData = mimeData->data(QLatin1String("text/x-action-items"));
1056
QDataStream dataStream(&itemData, QIODevice::ReadOnly);
1058
int ctActions; dataStream >> ctActions;
1059
for (int i=0; i<ctActions; i++) {
1061
dataStream >> action;
1062
printStatement(QString::fromLatin1("Gui.runCommand(\"%1\")").arg(action));
1065
e->setDropAction(Qt::CopyAction);
1069
// always copy text when doing drag and drop
1070
if (mimeData->hasText()) {
1071
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1072
QTextCursor cursor = this->cursorForPosition(e->pos());
1074
QTextCursor cursor = this->cursorForPosition(e->position().toPoint());
1076
QTextCursor inputLineBegin = this->inputBegin();
1078
if (!cursorBeyond( cursor, inputLineBegin )) {
1079
this->moveCursor(QTextCursor::End);
1081
QRect newPos = this->cursorRect();
1083
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1084
QDropEvent newEv(QPoint(newPos.x(), newPos.y()), Qt::CopyAction, mimeData, e->mouseButtons(), e->keyboardModifiers());
1086
QDropEvent newEv(QPoint(newPos.x(), newPos.y()), Qt::CopyAction, mimeData, e->buttons(), e->modifiers());
1089
QPlainTextEdit::dropEvent(&newEv);
1092
e->setDropAction(Qt::CopyAction);
1093
QPlainTextEdit::dropEvent(e);
1097
// this will call insertFromMimeData
1098
QPlainTextEdit::dropEvent(e);
1103
/** Dragging of action objects is allowed. */
1104
void PythonConsole::dragMoveEvent( QDragMoveEvent *e )
1106
const QMimeData* mimeData = e->mimeData();
1107
if (mimeData->hasFormat(QLatin1String("text/x-action-items"))) {
1111
// this will call canInsertFromMimeData
1112
QPlainTextEdit::dragMoveEvent(e);
1116
/** Dragging of action objects is allowed. */
1117
void PythonConsole::dragEnterEvent (QDragEnterEvent * e)
1119
const QMimeData* mimeData = e->mimeData();
1120
if (mimeData->hasFormat(QLatin1String("text/x-action-items"))) {
1124
// this will call canInsertFromMimeData
1125
QPlainTextEdit::dragEnterEvent(e);
1129
bool PythonConsole::canInsertFromMimeData (const QMimeData * source) const
1131
if (source->hasText()) {
1134
if (source->hasUrls()) {
1135
QList<QUrl> uri = source->urls();
1136
for (const auto & it : uri) {
1137
QFileInfo info(it.toLocalFile());
1138
if (info.exists() && info.isFile()) {
1139
QString ext = info.suffix().toLower();
1140
if (ext == QLatin1String("py") || ext == QLatin1String("fcmacro")) {
1151
* Allow to paste plain text or urls of text files.
1153
void PythonConsole::insertFromMimeData (const QMimeData * source)
1158
// First check on urls instead of text otherwise it may happen that a url
1159
// is handled as text
1160
bool existingFile = false;
1161
if (source->hasUrls()) {
1162
QList<QUrl> uri = source->urls();
1163
for (const auto & it : uri) {
1164
// get the file name and check the extension
1165
QFileInfo info(it.toLocalFile());
1166
QString ext = info.suffix().toLower();
1167
if (info.exists()) {
1168
existingFile = true;
1169
if (info.isFile() && (ext == QLatin1String("py") || ext == QLatin1String("fcmacro"))) {
1170
// load the file and read-in the source code
1171
QFile file(info.absoluteFilePath());
1172
if (file.open(QIODevice::ReadOnly)) {
1173
QTextStream str(&file);
1174
runSourceFromMimeData(str.readAll());
1182
// Some applications copy text into the clipboard with the formats
1183
// 'text/plain' and 'text/uri-list'. In case the url is not an existing
1184
// file we can handle it as normal text, then. See forum thread:
1185
// https://forum.freecad.org/viewtopic.php?f=3&t=34618
1186
if (source->hasText() && !existingFile) {
1187
runSourceFromMimeData(source->text());
1191
QTextCursor PythonConsole::inputBegin() const
1193
// construct cursor at begin of input line ...
1194
QTextCursor inputLineBegin(this->textCursor());
1195
inputLineBegin.movePosition(QTextCursor::End);
1196
inputLineBegin.movePosition(QTextCursor::StartOfBlock);
1197
// ... and move cursor right beyond the prompt.
1198
int prompt = promptLength(inputLineBegin.block().text());
1199
if (this->_sourceDrain && !this->_sourceDrain->isEmpty()) {
1200
prompt = this->_sourceDrain->length();
1202
inputLineBegin.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, prompt);
1203
return inputLineBegin;
1206
QMimeData * PythonConsole::createMimeDataFromSelection () const
1208
auto mime = new QMimeData();
1211
case PythonConsoleP::Normal:
1213
const QTextDocumentFragment fragment(textCursor());
1214
mime->setText(fragment.toPlainText());
1216
case PythonConsoleP::Command:
1218
QTextCursor cursor = textCursor();
1219
int s = cursor.selectionStart();
1220
int e = cursor.selectionEnd();
1223
for (b = document()->begin(); b.isValid(); b = b.next()) {
1224
int pos = b.position();
1225
if ( pos >= s && pos <= e ) {
1226
if (b.userState() > -1 && b.userState() < pythonSyntax->maximumUserState()) {
1227
lines << stripPromptFrom( b.text() );
1232
QString text = lines.join(QLatin1String("\n"));
1233
mime->setText(text);
1235
case PythonConsoleP::History:
1237
const QStringList& hist = d->history.values();
1238
QString text = hist.join(QLatin1String("\n"));
1239
mime->setText(text);
1246
void PythonConsole::runSourceFromMimeData(const QString& source)
1248
// When inserting a big text block we must break it down into several command
1249
// blocks instead of processing the text block as a whole or each single line.
1250
// If we processed the complete block as a whole only the first valid Python
1251
// command would be executed and the rest would be ignored. However, if we
1252
// processed each line separately the interpreter might be confused that a block
1253
// is complete but it might be not. This is for instance, if a class or method
1254
// definition contains several empty lines which leads to error messages (almost
1255
// indentation errors) later on.
1256
QString text = source;
1257
if (text.isNull()) {
1261
#if defined (Q_OS_LINUX)
1262
// Need to convert CRLF to LF
1263
text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
1264
#elif defined(Q_OS_WIN32)
1265
// Need to convert CRLF to LF
1266
text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
1267
#elif defined(Q_OS_MAC)
1268
//need to convert CR to LF
1269
text.replace(QLatin1Char('\r'), QLatin1Char('\n'));
1272
// separate the lines and get the last one
1273
QStringList lines = text.split(QLatin1Char('\n'));
1274
QString last = lines.back();
1277
QTextCursor cursor = textCursor();
1278
QStringList buffer = d->interpreter->getBuffer();
1279
d->interpreter->clearBuffer();
1281
int countNewlines = lines.count(), i = 0;
1282
for (QStringList::Iterator it = lines.begin(); it != lines.end(); ++it, ++i) {
1285
// insert the text to the current cursor position
1286
cursor.insertText(*it);
1288
// for the very first line get the complete block
1289
// because it may differ from the inserted text
1291
// get the text from the current cursor position to the end, remove it
1292
// and add it to the last line
1293
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
1294
QString select = cursor.selectedText();
1295
cursor.removeSelectedText();
1296
last = last + select;
1297
line = stripPromptFrom( cursor.block().text() );
1300
// put statement to the history
1301
d->history.append(line);
1303
buffer.append(line);
1304
int ret = d->interpreter->compileCommand(buffer.join(QLatin1String("\n")).toUtf8());
1305
if (ret == 1) { // incomplete
1306
printPrompt(PythonConsole::Incomplete);
1308
else if (ret == 0) { // complete
1309
// check if the following lines belong to the previous block
1312
while ((nextline.isEmpty() || isComment(nextline)) && k < countNewlines) {
1313
nextline = lines[k];
1317
int ret = d->interpreter->compileCommand(nextline.toUtf8());
1319
// If the line is valid, i.e. complete or incomplete the previous block
1322
// the command is not finished yet
1323
printPrompt(PythonConsole::Incomplete);
1326
runSource(buffer.join(QLatin1String("\n")));
1331
runSource(buffer.join(QLatin1String("\n")));
1332
ensureCursorVisible();
1333
return; // exit the method on error
1337
// set the incomplete block to the interpreter and insert the last line
1338
d->interpreter->setBuffer(buffer);
1339
cursor.insertText(last);
1340
ensureCursorVisible();
1344
* Overwrites the text of the cursor.
1346
void PythonConsole::overrideCursor(const QString& txt)
1348
// Go to the last line and the fourth position, right after the prompt
1349
QTextCursor cursor = this->inputBegin();
1350
int blockLength = this->textCursor().block().text().length();
1352
cursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, blockLength ); //<< select text to override
1353
cursor.removeSelectedText();
1354
cursor.insertText(txt);
1355
// move cursor to the end
1356
cursor.movePosition(QTextCursor::End);
1357
setTextCursor(cursor);
1360
void PythonConsole::contextMenuEvent ( QContextMenuEvent * e )
1364
bool mayPasteHere = cursorBeyond( this->textCursor(), this->inputBegin() );
1366
a = menu.addAction(tr("&Copy"), this, &PythonConsole::copy);
1367
a->setShortcut(QKeySequence(QString::fromLatin1("CTRL+C")));
1368
a->setEnabled(textCursor().hasSelection());
1370
a = menu.addAction(tr("&Copy command"), this, &PythonConsole::onCopyCommand);
1371
a->setEnabled(textCursor().hasSelection());
1373
a = menu.addAction(tr("&Copy history"), this, &PythonConsole::onCopyHistory);
1374
a->setEnabled(!d->history.isEmpty());
1376
a = menu.addAction( tr("Save history as..."), this, &PythonConsole::onSaveHistoryAs);
1377
a->setEnabled(!d->history.isEmpty());
1379
QAction* saveh = menu.addAction(tr("Save history"));
1380
saveh->setToolTip(tr("Saves Python history across %1 sessions").arg(qApp->applicationName()));
1381
saveh->setCheckable(true);
1382
saveh->setChecked(d->hGrpSettings->GetBool("SavePythonHistory", false));
1384
menu.addSeparator();
1386
a = menu.addAction(tr("&Paste"), this, &PythonConsole::paste);
1387
a->setShortcut(QKeySequence(QString::fromLatin1("CTRL+V")));
1388
const QMimeData *md = QApplication::clipboard()->mimeData();
1389
a->setEnabled( mayPasteHere && md && canInsertFromMimeData(md));
1391
a = menu.addAction(tr("Select All"), this, &PythonConsole::selectAll);
1392
a->setShortcut(QKeySequence(QString::fromLatin1("CTRL+A")));
1393
a->setEnabled(!document()->isEmpty());
1395
a = menu.addAction(tr("Clear console"), this, &PythonConsole::onClearConsole);
1396
a->setEnabled(!document()->isEmpty());
1398
menu.addSeparator();
1399
menu.addAction( tr("Insert file name..."), this, &PythonConsole::onInsertFileName);
1400
menu.addSeparator();
1402
QAction* wrap = menu.addAction(tr("Word wrap"));
1403
wrap->setCheckable(true);
1405
wrap->setChecked(d->hGrpSettings->GetBool("PythonWordWrap", true));
1406
QAction* exec = menu.exec(e->globalPos());
1408
d->hGrpSettings->SetBool("PythonWordWrap", wrap->isChecked());
1410
else if (exec == saveh) {
1411
d->hGrpSettings->SetBool("SavePythonHistory", saveh->isChecked());
1415
void PythonConsole::onClearConsole()
1418
d->output = d->info;
1419
printPrompt(PythonConsole::Complete);
1422
void PythonConsole::onSaveHistoryAs()
1424
QString cMacroPath = QString::fromUtf8(getDefaultParameter()->GetGroup( "Macro" )->
1425
GetASCII("MacroPath",App::Application::getUserMacroDir().c_str()).c_str());
1426
QString fn = FileDialog::getSaveFileName(this, tr("Save History"), cMacroPath,
1427
QString::fromLatin1("%1 (*.FCMacro *.py)").arg(tr("Macro Files")));
1428
if (!fn.isEmpty()) {
1429
int dot = fn.indexOf(QLatin1Char('.'));
1432
if (f.open(QIODevice::WriteOnly)) {
1434
const QStringList& hist = d->history.values();
1435
for (const auto & it : hist) {
1444
void PythonConsole::onInsertFileName()
1446
QString fn = Gui::FileDialog::getOpenFileName(Gui::getMainWindow(), tr("Insert file name"), QString(),
1447
QString::fromLatin1("%1 (*.*)").arg(tr("All Files")));
1448
if ( !fn.isEmpty() ) {
1449
insertPlainText(fn);
1454
* Copy the history of the console into the clipboard.
1456
void PythonConsole::onCopyHistory()
1458
if (d->history.isEmpty()) {
1461
d->type = PythonConsoleP::History;
1462
QMimeData *data = createMimeDataFromSelection();
1463
QApplication::clipboard()->setMimeData(data);
1464
d->type = PythonConsoleP::Normal;
1468
* Copy the selected commands into the clipboard. This is a subset of the history.
1470
void PythonConsole::onCopyCommand()
1472
d->type = PythonConsoleP::Command;
1474
d->type = PythonConsoleP::Normal;
1477
QString PythonConsole::readline( )
1480
// output is set to the current prompt which we need to extract
1481
// the actual user input
1482
QString inputBuffer = d->output;
1484
printPrompt(PythonConsole::Special);
1485
this->_sourceDrain = &inputBuffer; //< enable source drain ...
1486
// ... and wait until we get notified about pendingSource
1487
QObject::connect( this, &PythonConsole::pendingSource, &loop, &QEventLoop::quit);
1488
// application is about to quit
1489
if (loop.exec() != 0) {
1490
PyErr_SetInterrupt();
1491
} //< send SIGINT to python
1492
this->_sourceDrain = nullptr; //< disable source drain
1493
return inputBuffer.append(QChar::fromLatin1('\n')); //< pass a newline here, since the readline-caller may need it!
1497
* loads history contents from the default history file
1499
void PythonConsole::loadHistory() const
1501
// only load contents if history is empty, to not overwrite anything
1502
if (!d->history.isEmpty()) {
1506
if (!d->hGrpSettings->GetBool("SavePythonHistory", false)) {
1509
QFile f(d->historyFile);
1510
if (f.open(QIODevice::ReadOnly | QIODevice::Text)) {
1512
while (!f.atEnd()) {
1513
l = QString::fromUtf8(f.readLine());
1515
l.chop(1); // removes the last \n
1516
d->history.append(l);
1524
* saves the current history to the default history file
1526
void PythonConsole::saveHistory() const
1528
if (d->history.isEmpty()) {
1531
if (!d->hGrpSettings->GetBool("SavePythonHistory", false)) {
1534
QFile f(d->historyFile);
1535
if (f.open(QIODevice::WriteOnly)) {
1537
QStringList hist = d->history.values();
1538
// only save last 100 entries so we don't inflate forever...
1539
if (hist.length() > 100) {
1540
hist = hist.mid(hist.length()-100);
1542
for (const auto & it : hist) {
1549
// ---------------------------------------------------------------------
1551
PythonConsoleHighlighter::PythonConsoleHighlighter(QObject* parent)
1552
: PythonSyntaxHighlighter(parent)
1556
PythonConsoleHighlighter::~PythonConsoleHighlighter() = default;
1558
void PythonConsoleHighlighter::highlightBlock(const QString& text)
1560
const int ErrorOutput = (int)PythonConsoleP::Error;
1561
const int MessageOutput = (int)PythonConsoleP::Message;
1563
// Get user state to re-highlight the blocks in the appropriate format
1564
int stateOfPara = currentBlockState();
1566
switch (stateOfPara)
1571
QTextCharFormat errorFormat;
1572
errorFormat.setForeground(color(QLatin1String("Python error")));
1573
errorFormat.setFontItalic(true);
1574
setFormat( 0, text.length(), errorFormat);
1579
QTextCharFormat outputFormat;
1580
outputFormat.setForeground(color(QLatin1String("Python output")));
1581
setFormat( 0, text.length(), outputFormat);
1585
PythonSyntaxHighlighter::highlightBlock(text);
1590
void PythonConsoleHighlighter::colorChanged(const QString& type, const QColor& col)
1596
// ---------------------------------------------------------------------
1598
ConsoleHistory::ConsoleHistory()
1601
_it = _history.cend();
1604
ConsoleHistory::~ConsoleHistory() = default;
1606
void ConsoleHistory::first()
1608
_it = _history.cbegin();
1611
bool ConsoleHistory::more()
1613
return (_it != _history.cend());
1617
* next switches the history pointer to the next item.
1618
* While searching the next item, the routine respects the search prefix set by prev().
1619
* @return true if the pointer was switched to a later item, false otherwise.
1621
bool ConsoleHistory::next()
1623
bool wentNext = false;
1625
// if we didn't reach history's end ...
1626
if (_it != _history.cend())
1628
// we go forward until we find an item matching the prefix.
1629
for (++_it; _it != _history.cend(); ++_it) {
1630
if (!_it->isEmpty() && _it->startsWith( _prefix )) {
1634
// we did a step - no matter of a matching prefix.
1641
* prev switches the history pointer to the previous item.
1642
* The optional parameter prefix allows to search the history selectively for commands that start
1643
* with a certain character sequence.
1644
* @param prefix - prefix string for searching backwards in history, empty string by default
1645
* @return true if the pointer was switched to an earlier item, false otherwise.
1647
bool ConsoleHistory::prev( const QString &prefix )
1649
bool wentPrev = false;
1651
// store prefix if it's the first history access
1652
if (_it == _history.cend()) {
1656
// while we didn't go back or reach history's begin ...
1657
while (!wentPrev && _it != _history.cbegin()) {
1658
// go back in history and check if item matches prefix
1661
wentPrev = (!_it->isEmpty() && _it->startsWith( _prefix ));
1666
bool ConsoleHistory::isEmpty() const
1668
return _history.isEmpty();
1671
const QString& ConsoleHistory::value() const
1673
return ((_it != _history.end())? *_it
1674
/* else */ : _prefix);
1677
void ConsoleHistory::append( const QString& item )
1679
_history.append( item );
1680
// reset iterator to make the next history
1681
// access begin with the latest item.
1682
_it = _history.cend();
1685
const QStringList& ConsoleHistory::values() const
1687
return this->_history;
1691
* restart resets the history access to the latest item.
1693
void ConsoleHistory::restart( )
1695
_it = _history.cend();
1699
* markScratch stores the current end index of the history list.
1700
* Note: with simply remembering a start index, it does not work to nest scratch regions.
1701
* However, just replace the index keeping by a stack - in case this is be a concern.
1703
void ConsoleHistory::markScratch( )
1705
_scratchBegin = _history.length();
1709
* doScratch removes the tail of the history list, starting from the index marked lately.
1711
void ConsoleHistory::doScratch( )
1713
if (_scratchBegin < _history.length()) {
1714
_history.erase( _history.begin() + _scratchBegin, _history.end() );
1719
// -----------------------------------------------------
1721
#include "moc_PythonConsole.cpp"