"Professional CMake" book suggest the following: "Targets should build successfully with or without compiler support for precompiled headers. It should be considered an optimization, not a requirement. In particular, do not explicitly include a precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically generated precompile header on the compiler command line instead. This is more portable across the major compilers and is likely to be easier to maintain. It will also avoid warnings being generated from certain code checking tools like iwyu (include what you use)." Therefore, removed the "#include <PreCompiled.h>" from sources, also there is no need for the "#ifdef _PreComp_" anymore
1651 lines
54 KiB
C++
1651 lines
54 KiB
C++
/***************************************************************************
|
|
* Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net> *
|
|
* *
|
|
* This file is part of the FreeCAD CAx development system. *
|
|
* *
|
|
* This library is free software; you can redistribute it and/or *
|
|
* modify it under the terms of the GNU Library General Public *
|
|
* License as published by the Free Software Foundation; either *
|
|
* version 2 of the License, or (at your option) any later version. *
|
|
* *
|
|
* This library is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* GNU Library General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU Library General Public *
|
|
* License along with this library; see the file COPYING.LIB. If not, *
|
|
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
|
* Suite 330, Boston, MA 02111-1307, USA *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
|
|
# include <QApplication>
|
|
# include <QClipboard>
|
|
# include <QDockWidget>
|
|
# include <QKeyEvent>
|
|
# include <QMenu>
|
|
# include <QMessageBox>
|
|
# include <QMimeData>
|
|
# include <QTextCursor>
|
|
# include <QTextDocumentFragment>
|
|
# include <QTextStream>
|
|
# include <QTime>
|
|
# include <QUrl>
|
|
|
|
|
|
#include <Base/Interpreter.h>
|
|
#include <Base/Color.h>
|
|
|
|
#include "PythonConsole.h"
|
|
#include "PythonConsolePy.h"
|
|
#include "PythonTracing.h"
|
|
#include "Application.h"
|
|
#include "FileDialog.h"
|
|
#include "MainWindow.h"
|
|
#include "Tools.h"
|
|
|
|
|
|
using namespace Gui;
|
|
|
|
namespace Gui
|
|
{
|
|
|
|
static const QChar promptEnd( QLatin1Char(' ') ); //< char for detecting prompt end
|
|
|
|
inline int promptLength( const QString &lineStr )
|
|
{
|
|
return lineStr.indexOf( promptEnd ) + 1;
|
|
}
|
|
|
|
inline QString stripPromptFrom( const QString &lineStr )
|
|
{
|
|
return lineStr.mid( promptLength(lineStr) );
|
|
}
|
|
|
|
/**
|
|
* cursorBeyond checks if cursor is at a valid position to accept keyEvents.
|
|
* @param cursor - cursor to check
|
|
* @param limit - cursor that marks the begin of the input region
|
|
* @param shift - offset for shifting the limit for non-selection cursors [default: 0]
|
|
* @return true if a keyEvent is ok at cursor's position, false otherwise
|
|
*/
|
|
inline bool cursorBeyond( const QTextCursor &cursor, const QTextCursor &limit, int shift = 0 )
|
|
{
|
|
int pos = limit.position();
|
|
if (cursor.hasSelection()) {
|
|
return (cursor.selectionStart() >= pos && cursor.selectionEnd() >= pos);
|
|
}
|
|
|
|
return cursor.position() >= (pos + shift);
|
|
}
|
|
|
|
struct PythonConsoleP
|
|
{
|
|
enum Output {Error = 20, Message = 21};
|
|
enum CopyType {Normal, History, Command};
|
|
CopyType type;
|
|
PyObject *_stdoutPy=nullptr, *_stderrPy=nullptr, *_stdinPy=nullptr, *_stdin=nullptr;
|
|
InteractiveInterpreter* interpreter=nullptr;
|
|
ConsoleHistory history;
|
|
QString output, error, info, historyFile;
|
|
QStringList statements;
|
|
bool interactive;
|
|
ParameterGrp::handle hGrpSettings;
|
|
PythonConsoleP()
|
|
{
|
|
type = Normal;
|
|
interactive = false;
|
|
historyFile = QString::fromUtf8((App::Application::getUserAppDataDir() + "PythonHistory.log").c_str());
|
|
}
|
|
};
|
|
|
|
struct InteractiveInterpreterP
|
|
{
|
|
PyObject* interpreter{nullptr};
|
|
PyObject* sysmodule{nullptr};
|
|
QStringList buffer;
|
|
PythonTracing trace;
|
|
};
|
|
|
|
} // namespace Gui
|
|
|
|
InteractiveInterpreter::InteractiveInterpreter()
|
|
{
|
|
// import code.py and create an instance of InteractiveInterpreter
|
|
Base::PyGILStateLocker lock;
|
|
PyObject* module = PyImport_ImportModule("code");
|
|
if (!module) {
|
|
throw Base::PyException();
|
|
}
|
|
PyObject* func = PyObject_GetAttrString(module, "InteractiveInterpreter");
|
|
PyObject* args = Py_BuildValue("()");
|
|
d = new InteractiveInterpreterP;
|
|
d->interpreter = PyObject_CallObject(func,args);
|
|
Py_DECREF(args);
|
|
Py_DECREF(func);
|
|
Py_DECREF(module);
|
|
|
|
setPrompt();
|
|
}
|
|
|
|
InteractiveInterpreter::~InteractiveInterpreter()
|
|
{
|
|
Base::PyGILStateLocker lock;
|
|
Py_XDECREF(d->interpreter);
|
|
Py_XDECREF(d->sysmodule);
|
|
delete d;
|
|
}
|
|
|
|
/**
|
|
* Set the ps1 and ps2 members of the sys module if not yet defined.
|
|
*/
|
|
void InteractiveInterpreter::setPrompt()
|
|
{
|
|
// import code.py and create an instance of InteractiveInterpreter
|
|
Base::PyGILStateLocker lock;
|
|
d->sysmodule = PyImport_ImportModule("sys");
|
|
if (!PyObject_HasAttrString(d->sysmodule, "ps1")) {
|
|
PyObject_SetAttrString(d->sysmodule, "ps1", PyUnicode_FromString(">>> "));
|
|
}
|
|
if (!PyObject_HasAttrString(d->sysmodule, "ps2")) {
|
|
PyObject_SetAttrString(d->sysmodule, "ps2", PyUnicode_FromString("... "));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compile a command and determine whether it is incomplete.
|
|
*
|
|
* The source string may contain line feeds and/or carriage returns. \n
|
|
* Return value / exceptions raised:
|
|
* - Return a code object if the command is complete and valid
|
|
* - Return None if the command is incomplete
|
|
* - Raise SyntaxError, ValueError or OverflowError if the command is a
|
|
* syntax error (OverflowError and ValueError can be produced by
|
|
* malformed literals).
|
|
*/
|
|
PyObject* InteractiveInterpreter::compile(const char* source) const
|
|
{
|
|
Base::PyGILStateLocker lock;
|
|
PyObject* func = PyObject_GetAttrString(d->interpreter, "compile");
|
|
PyObject* args = Py_BuildValue("(s)", source);
|
|
PyObject* eval = PyObject_CallObject(func,args); // must decref later
|
|
|
|
Py_XDECREF(args);
|
|
Py_XDECREF(func);
|
|
|
|
if (eval){
|
|
return eval;
|
|
} else {
|
|
// do not throw Base::PyException as this clears the error indicator
|
|
throw Base::RuntimeError("Code evaluation failed");
|
|
}
|
|
|
|
// can never happen
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* Compile a command and determine whether it is incomplete.
|
|
*
|
|
* The source string may contain line feeds and/or carriage returns. \n
|
|
* Return value:
|
|
* - Return 1 if the command is incomplete
|
|
* - Return 0 if the command is complete and valid
|
|
* - Return -1 if the command is a syntax error
|
|
* .
|
|
* (OverflowError and ValueError can be produced by malformed literals).
|
|
*/
|
|
int InteractiveInterpreter::compileCommand(const char* source) const
|
|
{
|
|
Base::PyGILStateLocker lock;
|
|
PyObject* func = PyObject_GetAttrString(d->interpreter, "compile");
|
|
PyObject* args = Py_BuildValue("(s)", source);
|
|
PyObject* eval = PyObject_CallObject(func,args); // must decref later
|
|
|
|
Py_DECREF(args);
|
|
Py_DECREF(func);
|
|
|
|
int ret = 0;
|
|
if (eval){
|
|
if (PyObject_TypeCheck(Py_None, eval->ob_type)) {
|
|
ret = 1; // incomplete
|
|
}
|
|
else {
|
|
ret = 0; // complete
|
|
}
|
|
Py_DECREF(eval);
|
|
} else {
|
|
ret = -1; // invalid
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Compile and run some source in the interpreter.
|
|
*
|
|
* One several things can happen:
|
|
*
|
|
* - The input is incorrect; compile() raised an exception (SyntaxError or OverflowError).
|
|
* A syntax traceback will be printed by calling Python's PyErr_Print() method to the redirected stderr.
|
|
*
|
|
* - The input is incomplete, and more input is required; compile() returned 'None'.
|
|
* Nothing happens.
|
|
*
|
|
* - The input is complete; compile() returned a code object. The code is executed by calling
|
|
* runCode() (which also handles run-time exceptions, except for SystemExit).
|
|
* .
|
|
* The return value is True if the input is incomplete, False in the other cases (unless
|
|
* an exception is raised). The return value can be used to decide whether to use sys.ps1
|
|
* or sys.ps2 to prompt the next line.
|
|
*/
|
|
bool InteractiveInterpreter::runSource(const char* source) const
|
|
{
|
|
Base::PyGILStateLocker lock;
|
|
PyObject* code;
|
|
try {
|
|
code = compile(source);
|
|
} catch (const Base::Exception&) {
|
|
// A system, overflow or value error was raised.
|
|
// We clear the traceback info as this might be a longly
|
|
// message we don't need.
|
|
PyObject *errobj, *errdata, *errtraceback;
|
|
PyErr_Fetch(&errobj, &errdata, &errtraceback);
|
|
PyErr_Restore(errobj, errdata, nullptr);
|
|
// print error message
|
|
if (PyErr_Occurred()) PyErr_Print();
|
|
return false;
|
|
}
|
|
|
|
// the command is incomplete
|
|
if (PyObject_TypeCheck(Py_None, code->ob_type)) {
|
|
Py_DECREF(code);
|
|
return true;
|
|
}
|
|
|
|
// run the code and return false
|
|
runCode((PyCodeObject*)code);
|
|
return false;
|
|
}
|
|
|
|
bool InteractiveInterpreter::isOccupied() const
|
|
{
|
|
return d->trace.isActive();
|
|
}
|
|
|
|
bool InteractiveInterpreter::interrupt() const
|
|
{
|
|
return d->trace.interrupt();
|
|
}
|
|
|
|
/* Execute a code object.
|
|
*
|
|
* When an exception occurs, a traceback is displayed.
|
|
* All exceptions are caught except SystemExit, which is reraised.
|
|
*/
|
|
void InteractiveInterpreter::runCode(PyCodeObject* code) const
|
|
{
|
|
if (isOccupied()) {
|
|
return;
|
|
}
|
|
|
|
d->trace.fetchFromSettings();
|
|
PythonTracingLocker tracelock(d->trace);
|
|
|
|
Base::PyGILStateLocker lock;
|
|
PyObject *module, *dict, *presult; /* "exec code in d, d" */
|
|
module = PyImport_AddModule("__main__"); /* get module, init python */
|
|
if (!module) {
|
|
throw Base::PyException(); /* not incref'd */
|
|
}
|
|
dict = PyModule_GetDict(module); /* get dict namespace */
|
|
if (!dict) {
|
|
throw Base::PyException(); /* not incref'd */
|
|
}
|
|
|
|
// It seems that the return value is always 'None' or Null
|
|
presult = PyEval_EvalCode((PyObject*)code, dict, dict); /* run compiled bytecode */
|
|
Py_XDECREF(code); /* decref the code object */
|
|
if (!presult) {
|
|
if (PyErr_ExceptionMatches(PyExc_SystemExit)) {
|
|
// throw SystemExit exception
|
|
throw Base::SystemExitException();
|
|
}
|
|
if (PyErr_Occurred()) { /* get latest python exception information */
|
|
PyObject *errobj, *errdata, *errtraceback;
|
|
PyErr_Fetch(&errobj, &errdata, &errtraceback);
|
|
// the error message can be empty so errdata will be null
|
|
if (errdata && PyDict_Check(errdata)) {
|
|
PyObject* value = PyDict_GetItemString(errdata, "swhat");
|
|
if (value) {
|
|
Base::RuntimeError e;
|
|
e.setPyObject(errdata);
|
|
Py_DECREF(errdata);
|
|
|
|
std::stringstream str;
|
|
str << e.what();
|
|
if (!e.getFunction().empty()) {
|
|
str << " In " << e.getFunction();
|
|
}
|
|
if (!e.getFile().empty() && e.getLine() > 0) {
|
|
std::string file = e.getFile();
|
|
std::size_t pos = file.find("src");
|
|
if (pos!=std::string::npos) {
|
|
file = file.substr(pos);
|
|
}
|
|
str << " in " << file << ":" << e.getLine();
|
|
}
|
|
|
|
std::string err = str.str();
|
|
errdata = PyUnicode_FromString(err.c_str());
|
|
}
|
|
}
|
|
PyErr_Restore(errobj, errdata, errtraceback);
|
|
PyErr_Print(); /* and print the error to the error output */
|
|
}
|
|
} else {
|
|
Py_DECREF(presult);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store the line into the internal buffer and compile the total buffer.
|
|
* In case it is a complete Python command the buffer is emptied.
|
|
*/
|
|
bool InteractiveInterpreter::push(const char* line)
|
|
{
|
|
d->buffer.append(QString::fromUtf8(line));
|
|
QString source = d->buffer.join(QLatin1String("\n"));
|
|
try {
|
|
bool more = runSource(source.toUtf8());
|
|
if (!more) {
|
|
d->buffer.clear();
|
|
}
|
|
return more;
|
|
} catch (const Base::SystemExitException&) {
|
|
d->buffer.clear();
|
|
throw;
|
|
} catch (...) {
|
|
// indication of unhandled exception
|
|
d->buffer.clear();
|
|
if (PyErr_Occurred()) {
|
|
PyErr_Print();
|
|
}
|
|
throw;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool InteractiveInterpreter::hasPendingInput( ) const
|
|
{
|
|
return (!d->buffer.isEmpty());
|
|
}
|
|
|
|
QStringList InteractiveInterpreter::getBuffer() const
|
|
{
|
|
return d->buffer;
|
|
}
|
|
|
|
void InteractiveInterpreter::setBuffer(const QStringList& buf)
|
|
{
|
|
d->buffer = buf;
|
|
}
|
|
|
|
void InteractiveInterpreter::clearBuffer()
|
|
{
|
|
d->buffer.clear();
|
|
}
|
|
|
|
/* TRANSLATOR Gui::PythonConsole */
|
|
|
|
/**
|
|
* Constructs a PythonConsole which is a child of 'parent'.
|
|
*/
|
|
PythonConsole::PythonConsole(QWidget *parent)
|
|
: PythonTextEditor(parent), _sourceDrain(nullptr)
|
|
{
|
|
d = new PythonConsoleP();
|
|
d->interactive = false;
|
|
|
|
// create an instance of InteractiveInterpreter
|
|
try {
|
|
d->interpreter = new InteractiveInterpreter();
|
|
} catch (const Base::Exception& e) {
|
|
setPlainText(QString::fromLatin1(e.what()));
|
|
setEnabled(false);
|
|
}
|
|
|
|
// use the console highlighter
|
|
pythonSyntax = new PythonConsoleHighlighter(this);
|
|
setSyntaxHighlighter(pythonSyntax);
|
|
|
|
setVisibleLineNumbers(false);
|
|
setEnabledHighlightCurrentLine(false);
|
|
QFont serifFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
|
serifFont.setPointSize(10);
|
|
setFont(serifFont);
|
|
|
|
// set colors and font from settings
|
|
ParameterGrp::handle hPrefGrp = getWindowParameter();
|
|
hPrefGrp->NotifyAll();
|
|
|
|
d->hGrpSettings = WindowParameter::getDefaultParameter()->GetGroup("PythonConsole");
|
|
d->hGrpSettings->Attach(this);
|
|
d->hGrpSettings->NotifyAll();
|
|
|
|
// disable undo/redo stuff
|
|
setUndoRedoEnabled( false );
|
|
setAcceptDrops( true );
|
|
|
|
// try to override Python's stdout/err
|
|
Base::PyGILStateLocker lock;
|
|
d->_stdoutPy = new PythonStdout(this);
|
|
d->_stderrPy = new PythonStderr(this);
|
|
d->_stdinPy = new PythonStdin (this);
|
|
d->_stdin = PySys_GetObject("stdin");
|
|
|
|
// Don't override stdin when running FreeCAD as Python module
|
|
auto& cfg = App::Application::Config();
|
|
auto overrideStdIn = cfg.find("DontOverrideStdIn");
|
|
if (overrideStdIn == cfg.end()) {
|
|
PySys_SetObject("stdin", d->_stdinPy);
|
|
}
|
|
|
|
const char* version = PyUnicode_AsUTF8(PySys_GetObject("version"));
|
|
const char* platform = PyUnicode_AsUTF8(PySys_GetObject("platform"));
|
|
d->info = QStringLiteral("Python %1 on %2\n"
|
|
"Type 'help', 'copyright', 'credits' or 'license' for more information.")
|
|
.arg(QString::fromLatin1(version), QString::fromLatin1(platform));
|
|
d->output = d->info;
|
|
printPrompt(PythonConsole::Complete);
|
|
loadHistory();
|
|
|
|
flusher = new QTimer(this);
|
|
connect(flusher, &QTimer::timeout, this, &PythonConsole::flushOutput);
|
|
flusher->start(100);
|
|
}
|
|
|
|
/** Destroys the object and frees any allocated resources */
|
|
PythonConsole::~PythonConsole()
|
|
{
|
|
saveHistory();
|
|
Base::PyGILStateLocker lock;
|
|
d->hGrpSettings->Detach(this);
|
|
Py_XDECREF(d->_stdoutPy);
|
|
Py_XDECREF(d->_stderrPy);
|
|
Py_XDECREF(d->_stdinPy);
|
|
delete d->interpreter;
|
|
delete d;
|
|
}
|
|
|
|
/** Set new font and colors according to the parameters. */
|
|
void PythonConsole::OnChange(Base::Subject<const char*> &rCaller, const char* sReason)
|
|
{
|
|
const auto & rGrp = static_cast<ParameterGrp &>(rCaller);
|
|
|
|
if (strcmp(sReason, "PythonWordWrap") == 0) {
|
|
bool pythonWordWrap = rGrp.GetBool("PythonWordWrap", true);
|
|
if (pythonWordWrap) {
|
|
setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
|
}
|
|
else {
|
|
setWordWrapMode(QTextOption::NoWrap);
|
|
}
|
|
}
|
|
|
|
if (strcmp(sReason, "PythonBlockCursor") == 0) {
|
|
bool block = rGrp.GetBool("PythonBlockCursor", false);
|
|
if (block) {
|
|
setCursorWidth(QFontMetrics(font()).averageCharWidth());
|
|
}
|
|
else {
|
|
setCursorWidth(1);
|
|
}
|
|
}
|
|
|
|
if (strcmp(sReason, "EnableLineNumber") != 0) {
|
|
TextEditor::OnChange(rCaller, sReason);
|
|
}
|
|
}
|
|
|
|
int PythonConsole::getInputStringPosition()
|
|
{
|
|
QString rawLine = textCursor().block().text();
|
|
return textCursor().positionInBlock() - promptLength(rawLine);
|
|
}
|
|
|
|
QString PythonConsole::getInputString()
|
|
{
|
|
QString rawLine = textCursor().block().text();
|
|
return stripPromptFrom(rawLine);
|
|
}
|
|
|
|
/**
|
|
* Checks the input of the console to make the correct indentations.
|
|
* After a command is prompted completely the Python interpreter is started.
|
|
*/
|
|
void PythonConsole::keyPressEvent(QKeyEvent * e)
|
|
{
|
|
bool restartHistory = true;
|
|
QTextCursor cursor = this->textCursor();
|
|
QTextCursor inputLineBegin = this->inputBegin();
|
|
|
|
if (e->key() == Qt::Key_C && e->modifiers() == Qt::ControlModifier) {
|
|
if (d->interpreter->interrupt()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!cursorBeyond( cursor, inputLineBegin ))
|
|
{
|
|
/**
|
|
* The cursor is placed not on the input line (or within the prompt string)
|
|
* So we handle key input as follows:
|
|
* - don't allow changing previous lines.
|
|
* - allow full movement (no prompt restriction)
|
|
* - allow copying content (Ctrl+C)
|
|
* - "escape" to end of input line
|
|
*/
|
|
switch (e->key())
|
|
{
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
case Qt::Key_Escape:
|
|
case Qt::Key_Backspace:
|
|
this->moveCursor( QTextCursor::End );
|
|
break;
|
|
|
|
default:
|
|
if (e->text().isEmpty() ||
|
|
e->matches(QKeySequence::Copy) ||
|
|
e->matches(QKeySequence::SelectAll)) {
|
|
PythonTextEditor::keyPressEvent(e);
|
|
}
|
|
else if (!e->text().isEmpty() &&
|
|
(e->modifiers() == Qt::NoModifier ||
|
|
e->modifiers() == Qt::ShiftModifier)) {
|
|
this->moveCursor(QTextCursor::End);
|
|
PythonTextEditor::keyPressEvent(e);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/**
|
|
* The cursor sits somewhere on the input line (after the prompt)
|
|
* Here we handle key input a bit different:
|
|
* - restrict cursor movement to input line range (excluding the prompt characters)
|
|
* - roam the history by Up/Down keys
|
|
* - show call tips on period
|
|
*/
|
|
QTextBlock inputBlock = inputLineBegin.block(); //< get the last paragraph's text
|
|
QString inputLine = inputBlock.text();
|
|
QString inputStrg = stripPromptFrom( inputLine );
|
|
if (this->_sourceDrain && !this->_sourceDrain->isEmpty()) {
|
|
inputStrg = inputLine.mid(this->_sourceDrain->length());
|
|
}
|
|
|
|
switch (e->key())
|
|
{
|
|
case Qt::Key_Escape:
|
|
{
|
|
// disable current input string - i.e. put it to history but don't execute it.
|
|
if (!inputStrg.isEmpty())
|
|
{
|
|
d->history.append( QLatin1String("# ") + inputStrg ); //< put commented string to history ...
|
|
inputLineBegin.insertText( QStringLiteral("# ") ); //< and comment it on console
|
|
setTextCursor( inputLineBegin );
|
|
printPrompt(d->interpreter->hasPendingInput() //< print adequate prompt
|
|
? PythonConsole::Incomplete
|
|
: PythonConsole::Complete);
|
|
}
|
|
} break;
|
|
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
{
|
|
d->history.append( inputStrg ); //< put statement to history
|
|
runSource( inputStrg ); //< commit input string
|
|
} break;
|
|
|
|
case Qt::Key_Home:
|
|
{
|
|
QTextCursor::MoveMode mode = (e->modifiers() & Qt::ShiftModifier)? QTextCursor::KeepAnchor
|
|
/* else */ : QTextCursor::MoveAnchor;
|
|
cursor.setPosition( inputLineBegin.position(), mode );
|
|
setTextCursor( cursor );
|
|
ensureCursorVisible();
|
|
} break;
|
|
|
|
case Qt::Key_Up:
|
|
{
|
|
// if possible, move back in history
|
|
if (d->history.prev( inputStrg ))
|
|
{ overrideCursor( d->history.value() ); }
|
|
restartHistory = false;
|
|
} break;
|
|
|
|
case Qt::Key_Down:
|
|
{
|
|
// if possible, move forward in history
|
|
if (d->history.next())
|
|
{ overrideCursor( d->history.value() ); }
|
|
restartHistory = false;
|
|
} break;
|
|
|
|
case Qt::Key_Left:
|
|
{
|
|
if (cursor > inputLineBegin)
|
|
{ PythonTextEditor::keyPressEvent(e); }
|
|
restartHistory = false;
|
|
} break;
|
|
|
|
case Qt::Key_Right:
|
|
{
|
|
PythonTextEditor::keyPressEvent(e);
|
|
restartHistory = false;
|
|
} break;
|
|
|
|
case Qt::Key_Backspace:
|
|
{
|
|
if (cursorBeyond( cursor, inputLineBegin, +1 ))
|
|
{ PythonTextEditor::keyPressEvent(e); }
|
|
} break;
|
|
|
|
default:
|
|
{
|
|
PythonTextEditor::keyPressEvent(e);
|
|
} break;
|
|
}
|
|
|
|
// disable history restart if input line changed
|
|
restartHistory &= (inputLine != inputBlock.text());
|
|
}
|
|
// any cursor move resets the history to its latest item.
|
|
if (restartHistory) {
|
|
d->history.restart();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert an output message to the console. This message comes from
|
|
* the Python interpreter and is redirected from sys.stdout.
|
|
*/
|
|
void PythonConsole::insertPythonOutput( const QString& msg )
|
|
{
|
|
d->output += msg;
|
|
}
|
|
|
|
/**
|
|
* Insert an error message to the console. This message comes from
|
|
* the Python interpreter and is redirected from sys.stderr.
|
|
*/
|
|
void PythonConsole::insertPythonError ( const QString& err )
|
|
{
|
|
d->error += err;
|
|
}
|
|
|
|
void PythonConsole::onFlush()
|
|
{
|
|
printPrompt(PythonConsole::Flush);
|
|
}
|
|
|
|
void PythonConsole::flushOutput()
|
|
{
|
|
if (d->interpreter->isOccupied()) {
|
|
if (d->output.length() > 0 || d->error.length() > 0) {
|
|
printPrompt(PythonConsole::Complete);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Prints the ps1 prompt (>>> ) for complete and ps2 prompt (... ) for
|
|
* incomplete commands to the console window.
|
|
*/
|
|
void PythonConsole::printPrompt(PythonConsole::Prompt mode)
|
|
{
|
|
// write normal messages
|
|
if (!d->output.isEmpty()) {
|
|
appendOutput(d->output, (int)PythonConsoleP::Message);
|
|
d->output.clear();
|
|
}
|
|
|
|
// write error messages
|
|
if (!d->error.isEmpty()) {
|
|
appendOutput(d->error, (int)PythonConsoleP::Error);
|
|
d->error.clear();
|
|
}
|
|
|
|
// Append the prompt string
|
|
QTextCursor cursor = textCursor();
|
|
|
|
if (mode != PythonConsole::Special)
|
|
{
|
|
cursor.beginEditBlock();
|
|
cursor.movePosition(QTextCursor::End);
|
|
QTextBlock block = cursor.block();
|
|
|
|
// Python's print command appends a trailing '\n' to the system output.
|
|
// In this case, however, we should not add a new text block. We force
|
|
// the current block to be normal text (user state = 0) to be highlighted
|
|
// correctly and append the '>>> ' or '... ' to this block.
|
|
if (block.length() > 1)
|
|
cursor.insertBlock(cursor.blockFormat(), cursor.charFormat());
|
|
else
|
|
block.setUserState(0);
|
|
|
|
switch (mode)
|
|
{
|
|
case PythonConsole::Incomplete:
|
|
cursor.insertText(QStringLiteral("... "));
|
|
break;
|
|
case PythonConsole::Complete:
|
|
cursor.insertText(QStringLiteral(">>> "));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
cursor.endEditBlock();
|
|
}
|
|
// move cursor to the end
|
|
cursor.movePosition(QTextCursor::End);
|
|
setTextCursor(cursor);
|
|
}
|
|
|
|
/**
|
|
* Appends \a output to the console and set \a state as user state to
|
|
* the text block which is needed for the highlighting.
|
|
*/
|
|
void PythonConsole::appendOutput(const QString& output, int state)
|
|
{
|
|
QTextCursor cursor = textCursor();
|
|
cursor.movePosition(QTextCursor::End);
|
|
int pos = cursor.position() + 1;
|
|
|
|
// delay rehighlighting
|
|
cursor.beginEditBlock();
|
|
appendPlainText(output);
|
|
|
|
QTextBlock block = this->document()->findBlock(pos);
|
|
while (block.isValid()) {
|
|
block.setUserState(state);
|
|
block = block.next();
|
|
}
|
|
cursor.endEditBlock(); // start highlightiong
|
|
}
|
|
|
|
/**
|
|
* Builds up the Python command and pass it to the interpreter.
|
|
*/
|
|
void PythonConsole::runSource(const QString& line)
|
|
{
|
|
/**
|
|
* Check if there's a "source drain", which wants to consume the source in another way then just executing it.
|
|
* If so, put the source to the drain and emit a signal to notify the consumer, whomever this may be.
|
|
*/
|
|
if (this->_sourceDrain) {
|
|
*this->_sourceDrain = line;
|
|
Q_EMIT pendingSource();
|
|
return;
|
|
}
|
|
|
|
if (d->interpreter->isOccupied()) {
|
|
insertPythonError(QStringLiteral("Previous command still running!"));
|
|
return;
|
|
}
|
|
|
|
bool incomplete = false;
|
|
Base::PyGILStateLocker lock;
|
|
PyObject* default_stdout = PySys_GetObject("stdout");
|
|
PyObject* default_stderr = PySys_GetObject("stderr");
|
|
PySys_SetObject("stdout", d->_stdoutPy);
|
|
PySys_SetObject("stderr", d->_stderrPy);
|
|
d->interactive = true;
|
|
|
|
try {
|
|
d->history.markScratch(); //< mark current history position ...
|
|
// launch the command now
|
|
incomplete = d->interpreter->push(line.toUtf8());
|
|
if (!incomplete) {
|
|
d->history.doScratch();
|
|
} //< ... and scratch history entries that might have been added by executing the line.
|
|
setFocus(); // if focus was lost
|
|
}
|
|
catch (const Base::SystemExitException&) {
|
|
// In Python the exception must be cleared because when the message box below appears
|
|
// callable Python objects can be invoked and due to a failing assert the application
|
|
// will be aborted.
|
|
PyErr_Clear();
|
|
|
|
ParameterGrp::handle hPrefGrp = getWindowParameter();
|
|
bool check = hPrefGrp->GetBool("CheckSystemExit",true);
|
|
int ret = QMessageBox::Yes;
|
|
if (check) {
|
|
ret = QMessageBox::question(this, tr("System exit"),
|
|
tr("The application is still running.\nExit without saving all data?"),
|
|
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
|
}
|
|
if (ret == QMessageBox::Yes) {
|
|
PyErr_Clear();
|
|
throw;
|
|
}
|
|
else {
|
|
PyErr_Clear();
|
|
}
|
|
}
|
|
catch (const Py::Exception&) {
|
|
QMessageBox::critical(this, tr("Python Console"), tr("Unhandled PyCXX exception."));
|
|
}
|
|
catch (const Base::Exception&) {
|
|
QMessageBox::critical(this, tr("Python Console"), tr("Unhandled FreeCAD exception."));
|
|
}
|
|
catch (const std::exception&) {
|
|
QMessageBox::critical(this, tr("Python Console"), tr("Unhandled std C++ exception."));
|
|
}
|
|
catch (...) {
|
|
QMessageBox::critical(this, tr("Python Console"), tr("Unhandled unknown C++ exception."));
|
|
}
|
|
|
|
printPrompt(incomplete ? PythonConsole::Incomplete
|
|
: PythonConsole::Complete);
|
|
PySys_SetObject("stdout", default_stdout);
|
|
PySys_SetObject("stderr", default_stderr);
|
|
d->interactive = false;
|
|
for (const auto & it : d->statements) {
|
|
printStatement(it);
|
|
}
|
|
d->statements.clear();
|
|
}
|
|
|
|
bool PythonConsole::isComment(const QString& source) const
|
|
{
|
|
if (source.isEmpty()) {
|
|
return false;
|
|
}
|
|
int i=0;
|
|
while (i < source.length()) {
|
|
QChar ch = source.at(i++);
|
|
if (ch.isSpace()) {
|
|
continue;
|
|
}
|
|
else if (ch == QLatin1Char('#')) {
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Prints the Python statement cmd to the console.
|
|
* @note The statement gets only printed and added to the history but not invoked.
|
|
*/
|
|
void PythonConsole::printStatement( const QString& cmd )
|
|
{
|
|
// If we are in interactive mode we have to wait until the command is finished,
|
|
// afterwards we can print the statements.
|
|
if (d->interactive) {
|
|
d->statements << cmd;
|
|
return;
|
|
}
|
|
|
|
QTextCursor cursor = textCursor();
|
|
QStringList statements = cmd.split(QLatin1String("\n"));
|
|
for (const auto & statement : statements) {
|
|
// go to the end before inserting new text
|
|
cursor.movePosition(QTextCursor::End);
|
|
cursor.insertText( statement );
|
|
d->history.append( statement );
|
|
printPrompt(PythonConsole::Complete);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows the Python window and sets the focus to set text cursor.
|
|
*/
|
|
void PythonConsole::showEvent (QShowEvent * e)
|
|
{
|
|
TextEdit::showEvent(e);
|
|
// set also the text cursor to the edit field
|
|
setFocus();
|
|
}
|
|
|
|
void PythonConsole::visibilityChanged (bool visible)
|
|
{
|
|
if (visible) {
|
|
setFocus();
|
|
}
|
|
}
|
|
|
|
void PythonConsole::changeEvent(QEvent *e)
|
|
{
|
|
if (e->type() == QEvent::ParentChange) {
|
|
auto dw = qobject_cast<QDockWidget*>(this->parentWidget());
|
|
if (dw) {
|
|
connect(dw, &QDockWidget::visibilityChanged, this, &PythonConsole::visibilityChanged);
|
|
}
|
|
}
|
|
else if (e->type() == QEvent::StyleChange) {
|
|
QPalette pal = qApp->palette();
|
|
QColor color = pal.windowText().color();
|
|
unsigned int text = Base::Color::asPackedRGB<QColor>(color);
|
|
auto value = static_cast<unsigned long>(text);
|
|
// if this parameter is not already set use the style's window text color
|
|
value = getWindowParameter()->GetUnsigned("Text", value);
|
|
getWindowParameter()->SetUnsigned("Text", value);
|
|
}
|
|
TextEdit::changeEvent(e);
|
|
}
|
|
|
|
void PythonConsole::mouseReleaseEvent( QMouseEvent *e )
|
|
{
|
|
if (e->button() == Qt::MiddleButton && e->spontaneous()) {
|
|
// on Linux-like systems the middle mouse button is typically connected to a paste operation
|
|
// which will insert some text at the mouse position
|
|
QTextCursor cursor = this->textCursor();
|
|
if (cursor < this->inputBegin()) {
|
|
cursor.movePosition( QTextCursor::End );
|
|
this->setTextCursor( cursor );
|
|
}
|
|
// the text will be pasted at the cursor position (as for Ctrl-V operation)
|
|
QRect newPos = this->cursorRect();
|
|
|
|
// Now we must amend the received event and pass forward. As e->setLocalPos() is only
|
|
// available in Qt>=5.8, let's stop the original event propagation and generate a fake event
|
|
// with corrected pointer position (inside the prompt line of the widget)
|
|
#if QT_VERSION < QT_VERSION_CHECK(6,4,0)
|
|
QMouseEvent newEv(e->type(), QPoint(newPos.x(),newPos.y()),
|
|
e->button(), e->buttons(), e->modifiers());
|
|
#else
|
|
QMouseEvent newEv(e->type(), QPoint(newPos.x(),newPos.y()), e->globalPosition(),
|
|
e->button(), e->buttons(), e->modifiers());
|
|
#endif
|
|
e->accept();
|
|
QCoreApplication::sendEvent(this->viewport(), &newEv);
|
|
return;
|
|
}
|
|
TextEdit::mouseReleaseEvent( e );
|
|
}
|
|
|
|
/**
|
|
* Drops the event \a e and writes the right Python command.
|
|
*/
|
|
void PythonConsole::dropEvent (QDropEvent * e)
|
|
{
|
|
const QMimeData* mimeData = e->mimeData();
|
|
if (mimeData->hasFormat(QLatin1String("text/x-action-items"))) {
|
|
QByteArray itemData = mimeData->data(QLatin1String("text/x-action-items"));
|
|
QDataStream dataStream(&itemData, QIODevice::ReadOnly);
|
|
|
|
int ctActions; dataStream >> ctActions;
|
|
for (int i=0; i<ctActions; i++) {
|
|
QString action;
|
|
dataStream >> action;
|
|
printStatement(QStringLiteral("Gui.runCommand(\"%1\")").arg(action));
|
|
}
|
|
|
|
e->setDropAction(Qt::CopyAction);
|
|
e->accept();
|
|
}
|
|
else {
|
|
// always copy text when doing drag and drop
|
|
if (mimeData->hasText()) {
|
|
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
|
|
QTextCursor cursor = this->cursorForPosition(e->pos());
|
|
#else
|
|
QTextCursor cursor = this->cursorForPosition(e->position().toPoint());
|
|
#endif
|
|
QTextCursor inputLineBegin = this->inputBegin();
|
|
|
|
if (!cursorBeyond( cursor, inputLineBegin )) {
|
|
this->moveCursor(QTextCursor::End);
|
|
|
|
QRect newPos = this->cursorRect();
|
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
|
|
QDropEvent newEv(QPoint(newPos.x(), newPos.y()), Qt::CopyAction, mimeData, e->mouseButtons(), e->keyboardModifiers());
|
|
#else
|
|
QDropEvent newEv(QPoint(newPos.x(), newPos.y()), Qt::CopyAction, mimeData, e->buttons(), e->modifiers());
|
|
#endif
|
|
e->accept();
|
|
QPlainTextEdit::dropEvent(&newEv);
|
|
}
|
|
else {
|
|
e->setDropAction(Qt::CopyAction);
|
|
QPlainTextEdit::dropEvent(e);
|
|
}
|
|
}
|
|
else {
|
|
// this will call insertFromMimeData
|
|
QPlainTextEdit::dropEvent(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Dragging of action objects is allowed. */
|
|
void PythonConsole::dragMoveEvent( QDragMoveEvent *e )
|
|
{
|
|
const QMimeData* mimeData = e->mimeData();
|
|
if (mimeData->hasFormat(QLatin1String("text/x-action-items"))) {
|
|
e->accept();
|
|
}
|
|
else {
|
|
// this will call canInsertFromMimeData
|
|
QPlainTextEdit::dragMoveEvent(e);
|
|
}
|
|
}
|
|
|
|
/** Dragging of action objects is allowed. */
|
|
void PythonConsole::dragEnterEvent (QDragEnterEvent * e)
|
|
{
|
|
const QMimeData* mimeData = e->mimeData();
|
|
if (mimeData->hasFormat(QLatin1String("text/x-action-items"))) {
|
|
e->accept();
|
|
}
|
|
else {
|
|
// this will call canInsertFromMimeData
|
|
QPlainTextEdit::dragEnterEvent(e);
|
|
}
|
|
}
|
|
|
|
bool PythonConsole::canInsertFromMimeData (const QMimeData * source) const
|
|
{
|
|
if (source->hasText()) {
|
|
return true;
|
|
}
|
|
if (source->hasUrls()) {
|
|
QList<QUrl> uri = source->urls();
|
|
for (const auto & it : uri) {
|
|
QFileInfo info(it.toLocalFile());
|
|
if (info.exists() && info.isFile()) {
|
|
QString ext = info.suffix().toLower();
|
|
if (ext == QLatin1String("py") || ext == QLatin1String("fcmacro")) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Allow one to paste plain text or urls of text files.
|
|
*/
|
|
void PythonConsole::insertFromMimeData (const QMimeData * source)
|
|
{
|
|
if (!source) {
|
|
return;
|
|
}
|
|
// First check on urls instead of text otherwise it may happen that a url
|
|
// is handled as text
|
|
bool existingFile = false;
|
|
if (source->hasUrls()) {
|
|
QList<QUrl> uri = source->urls();
|
|
for (const auto & it : uri) {
|
|
// get the file name and check the extension
|
|
QFileInfo info(it.toLocalFile());
|
|
QString ext = info.suffix().toLower();
|
|
if (info.exists()) {
|
|
existingFile = true;
|
|
if (info.isFile() && (ext == QLatin1String("py") || ext == QLatin1String("fcmacro"))) {
|
|
// load the file and read-in the source code
|
|
QFile file(info.absoluteFilePath());
|
|
if (file.open(QIODevice::ReadOnly)) {
|
|
QTextStream str(&file);
|
|
runSourceFromMimeData(str.readAll());
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Some applications copy text into the clipboard with the formats
|
|
// 'text/plain' and 'text/uri-list'. In case the url is not an existing
|
|
// file we can handle it as normal text, then. See forum thread:
|
|
// https://forum.freecad.org/viewtopic.php?f=3&t=34618
|
|
if (source->hasText() && !existingFile) {
|
|
runSourceFromMimeData(source->text());
|
|
}
|
|
}
|
|
|
|
QTextCursor PythonConsole::inputBegin() const
|
|
{
|
|
// construct cursor at begin of input line ...
|
|
QTextCursor inputLineBegin(this->textCursor());
|
|
inputLineBegin.movePosition(QTextCursor::End);
|
|
inputLineBegin.movePosition(QTextCursor::StartOfBlock);
|
|
// ... and move cursor right beyond the prompt.
|
|
int prompt = promptLength(inputLineBegin.block().text());
|
|
if (this->_sourceDrain && !this->_sourceDrain->isEmpty()) {
|
|
prompt = this->_sourceDrain->length();
|
|
}
|
|
inputLineBegin.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, prompt);
|
|
return inputLineBegin;
|
|
}
|
|
|
|
QMimeData * PythonConsole::createMimeDataFromSelection () const
|
|
{
|
|
auto mime = new QMimeData();
|
|
|
|
switch (d->type) {
|
|
case PythonConsoleP::Normal:
|
|
{
|
|
const QTextDocumentFragment fragment(textCursor());
|
|
mime->setText(fragment.toPlainText());
|
|
} break;
|
|
case PythonConsoleP::Command:
|
|
{
|
|
QTextCursor cursor = textCursor();
|
|
int s = cursor.selectionStart();
|
|
int e = cursor.selectionEnd();
|
|
QTextBlock b;
|
|
QStringList lines;
|
|
for (b = document()->begin(); b.isValid(); b = b.next()) {
|
|
int pos = b.position();
|
|
if ( pos >= s && pos <= e ) {
|
|
if (b.userState() > -1 && b.userState() < pythonSyntax->maximumUserState()) {
|
|
lines << stripPromptFrom( b.text() );
|
|
}
|
|
}
|
|
}
|
|
|
|
QString text = lines.join(QLatin1String("\n"));
|
|
mime->setText(text);
|
|
} break;
|
|
case PythonConsoleP::History:
|
|
{
|
|
const QStringList& hist = d->history.values();
|
|
QString text = hist.join(QLatin1String("\n"));
|
|
mime->setText(text);
|
|
} break;
|
|
}
|
|
|
|
return mime;
|
|
}
|
|
|
|
void PythonConsole::runSourceFromMimeData(const QString& source)
|
|
{
|
|
// When inserting a big text block we must break it down into several command
|
|
// blocks instead of processing the text block as a whole or each single line.
|
|
// If we processed the complete block as a whole only the first valid Python
|
|
// command would be executed and the rest would be ignored. However, if we
|
|
// processed each line separately the interpreter might be confused that a block
|
|
// is complete but it might be not. This is for instance, if a class or method
|
|
// definition contains several empty lines which leads to error messages (almost
|
|
// indentation errors) later on.
|
|
QString text = source;
|
|
if (text.isNull()) {
|
|
return;
|
|
}
|
|
|
|
#if defined (Q_OS_LINUX)
|
|
// Need to convert CRLF to LF
|
|
text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
|
|
#elif defined(Q_OS_WIN32)
|
|
// Need to convert CRLF to LF
|
|
text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
|
|
#elif defined(Q_OS_MACOS)
|
|
//need to convert CR to LF
|
|
text.replace(QLatin1Char('\r'), QLatin1Char('\n'));
|
|
#endif
|
|
|
|
// separate the lines and get the last one
|
|
QStringList lines = text.split(QLatin1Char('\n'));
|
|
QString last = lines.back();
|
|
lines.pop_back();
|
|
|
|
QTextCursor cursor = textCursor();
|
|
QStringList buffer = d->interpreter->getBuffer();
|
|
d->interpreter->clearBuffer();
|
|
|
|
int countNewlines = lines.count(), i = 0;
|
|
for (QStringList::Iterator it = lines.begin(); it != lines.end(); ++it, ++i) {
|
|
QString line = *it;
|
|
|
|
// insert the text to the current cursor position
|
|
cursor.insertText(*it);
|
|
|
|
// for the very first line get the complete block
|
|
// because it may differ from the inserted text
|
|
if (i == 0) {
|
|
// get the text from the current cursor position to the end, remove it
|
|
// and add it to the last line
|
|
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
|
QString select = cursor.selectedText();
|
|
cursor.removeSelectedText();
|
|
last = last + select;
|
|
line = stripPromptFrom( cursor.block().text() );
|
|
}
|
|
|
|
// put statement to the history
|
|
d->history.append(line);
|
|
|
|
buffer.append(line);
|
|
int ret = d->interpreter->compileCommand(buffer.join(QLatin1String("\n")).toUtf8());
|
|
if (ret == 1) { // incomplete
|
|
printPrompt(PythonConsole::Incomplete);
|
|
}
|
|
else if (ret == 0) { // complete
|
|
// check if the following lines belong to the previous block
|
|
int k=i+1;
|
|
QString nextline;
|
|
while ((nextline.isEmpty() || isComment(nextline)) && k < countNewlines) {
|
|
nextline = lines[k];
|
|
k++;
|
|
}
|
|
|
|
int ret = d->interpreter->compileCommand(nextline.toUtf8());
|
|
|
|
// If the line is valid, i.e. complete or incomplete the previous block
|
|
// is finished
|
|
if (ret == -1) {
|
|
// the command is not finished yet
|
|
printPrompt(PythonConsole::Incomplete);
|
|
}
|
|
else {
|
|
runSource(buffer.join(QLatin1String("\n")));
|
|
buffer.clear();
|
|
}
|
|
}
|
|
else { // invalid
|
|
runSource(buffer.join(QLatin1String("\n")));
|
|
ensureCursorVisible();
|
|
return; // exit the method on error
|
|
}
|
|
}
|
|
|
|
// set the incomplete block to the interpreter and insert the last line
|
|
d->interpreter->setBuffer(buffer);
|
|
cursor.insertText(last);
|
|
ensureCursorVisible();
|
|
}
|
|
|
|
/**
|
|
* Overwrites the text of the cursor.
|
|
*/
|
|
void PythonConsole::overrideCursor(const QString& txt)
|
|
{
|
|
// Go to the last line and the fourth position, right after the prompt
|
|
QTextCursor cursor = this->inputBegin();
|
|
int blockLength = this->textCursor().block().text().length();
|
|
|
|
cursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, blockLength ); //<< select text to override
|
|
cursor.removeSelectedText();
|
|
cursor.insertText(txt);
|
|
// move cursor to the end
|
|
cursor.movePosition(QTextCursor::End);
|
|
setTextCursor(cursor);
|
|
}
|
|
|
|
void PythonConsole::contextMenuEvent ( QContextMenuEvent * e )
|
|
{
|
|
QMenu menu(this);
|
|
QAction *a;
|
|
bool mayPasteHere = cursorBeyond( this->textCursor(), this->inputBegin() );
|
|
|
|
a = menu.addAction(tr("&Copy"), this, &PythonConsole::copy);
|
|
a->setShortcut(QKeySequence(QStringLiteral("CTRL+C")));
|
|
a->setEnabled(textCursor().hasSelection());
|
|
|
|
a = menu.addAction(tr("&Copy Command"), this, &PythonConsole::onCopyCommand);
|
|
a->setEnabled(textCursor().hasSelection());
|
|
|
|
a = menu.addAction(tr("&Copy History"), this, &PythonConsole::onCopyHistory);
|
|
a->setEnabled(!d->history.isEmpty());
|
|
|
|
a = menu.addAction(tr("Save History As…"), this, &PythonConsole::onSaveHistoryAs);
|
|
a->setEnabled(!d->history.isEmpty());
|
|
|
|
QAction* saveh = menu.addAction(tr("Save History"));
|
|
saveh->setToolTip(tr("Saves Python history across %1 sessions").arg(qApp->applicationName()));
|
|
saveh->setCheckable(true);
|
|
saveh->setChecked(d->hGrpSettings->GetBool("SavePythonHistory", false));
|
|
|
|
menu.addSeparator();
|
|
|
|
a = menu.addAction(tr("&Paste"), this, &PythonConsole::paste);
|
|
a->setShortcut(QKeySequence(QStringLiteral("CTRL+V")));
|
|
const QMimeData *md = QApplication::clipboard()->mimeData();
|
|
a->setEnabled( mayPasteHere && md && canInsertFromMimeData(md));
|
|
|
|
a = menu.addAction(tr("Select All"), this, &PythonConsole::selectAll);
|
|
a->setShortcut(QKeySequence(QStringLiteral("CTRL+A")));
|
|
a->setEnabled(!document()->isEmpty());
|
|
|
|
a = menu.addAction(tr("Clear Console"), this, &PythonConsole::onClearConsole);
|
|
a->setEnabled(!document()->isEmpty());
|
|
|
|
menu.addSeparator();
|
|
menu.addAction(tr("Insert File Name…"), this, &PythonConsole::onInsertFileName);
|
|
menu.addSeparator();
|
|
|
|
QAction* wrap = menu.addAction(tr("Word Wrap"));
|
|
wrap->setCheckable(true);
|
|
|
|
wrap->setChecked(d->hGrpSettings->GetBool("PythonWordWrap", true));
|
|
QAction* exec = menu.exec(e->globalPos());
|
|
if (exec == wrap) {
|
|
d->hGrpSettings->SetBool("PythonWordWrap", wrap->isChecked());
|
|
}
|
|
else if (exec == saveh) {
|
|
d->hGrpSettings->SetBool("SavePythonHistory", saveh->isChecked());
|
|
}
|
|
}
|
|
|
|
void PythonConsole::onClearConsole()
|
|
{
|
|
clear();
|
|
d->output = d->info;
|
|
printPrompt(PythonConsole::Complete);
|
|
}
|
|
|
|
void PythonConsole::onSaveHistoryAs()
|
|
{
|
|
QString cMacroPath = QString::fromUtf8(getDefaultParameter()->GetGroup( "Macro" )->
|
|
GetASCII("MacroPath",App::Application::getUserMacroDir().c_str()).c_str());
|
|
QString fn = FileDialog::getSaveFileName(this, tr("Save History"), cMacroPath,
|
|
QStringLiteral("%1 (*.FCMacro *.py)").arg(tr("Macro Files")));
|
|
if (!fn.isEmpty()) {
|
|
int dot = fn.indexOf(QLatin1Char('.'));
|
|
if (dot != -1) {
|
|
QFile f(fn);
|
|
if (f.open(QIODevice::WriteOnly)) {
|
|
QTextStream t (&f);
|
|
const QStringList& hist = d->history.values();
|
|
for (const auto & it : hist) {
|
|
t << it << "\n";
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PythonConsole::onInsertFileName()
|
|
{
|
|
QString fn = Gui::FileDialog::getOpenFileName(Gui::getMainWindow(), tr("Insert file name"), QString(),
|
|
QStringLiteral("%1 (*.*)").arg(tr("All Files")));
|
|
if ( !fn.isEmpty() ) {
|
|
insertPlainText(fn);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy the history of the console into the clipboard.
|
|
*/
|
|
void PythonConsole::onCopyHistory()
|
|
{
|
|
if (d->history.isEmpty()) {
|
|
return;
|
|
}
|
|
d->type = PythonConsoleP::History;
|
|
QMimeData *data = createMimeDataFromSelection();
|
|
QApplication::clipboard()->setMimeData(data);
|
|
d->type = PythonConsoleP::Normal;
|
|
}
|
|
|
|
/**
|
|
* Copy the selected commands into the clipboard. This is a subset of the history.
|
|
*/
|
|
void PythonConsole::onCopyCommand()
|
|
{
|
|
d->type = PythonConsoleP::Command;
|
|
copy();
|
|
d->type = PythonConsoleP::Normal;
|
|
}
|
|
|
|
QString PythonConsole::readline( )
|
|
{
|
|
QEventLoop loop;
|
|
// output is set to the current prompt which we need to extract
|
|
// the actual user input
|
|
QString inputBuffer = d->output;
|
|
|
|
printPrompt(PythonConsole::Special);
|
|
this->_sourceDrain = &inputBuffer; //< enable source drain ...
|
|
// ... and wait until we get notified about pendingSource
|
|
QObject::connect( this, &PythonConsole::pendingSource, &loop, &QEventLoop::quit);
|
|
// application is about to quit
|
|
if (loop.exec() != 0) {
|
|
PyErr_SetInterrupt();
|
|
} //< send SIGINT to python
|
|
this->_sourceDrain = nullptr; //< disable source drain
|
|
return inputBuffer.append(QChar::fromLatin1('\n')); //< pass a newline here, since the readline-caller may need it!
|
|
}
|
|
|
|
/**
|
|
* loads history contents from the default history file
|
|
*/
|
|
void PythonConsole::loadHistory() const
|
|
{
|
|
// only load contents if history is empty, to not overwrite anything
|
|
if (!d->history.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
if (!d->hGrpSettings->GetBool("SavePythonHistory", false)) {
|
|
return;
|
|
}
|
|
QFile f(d->historyFile);
|
|
if (f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
QString l;
|
|
while (!f.atEnd()) {
|
|
l = QString::fromUtf8(f.readLine());
|
|
if (!l.isEmpty()) {
|
|
l.chop(1); // removes the last \n
|
|
d->history.append(l);
|
|
}
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* saves the current history to the default history file
|
|
*/
|
|
void PythonConsole::saveHistory() const
|
|
{
|
|
if (d->history.isEmpty()) {
|
|
return;
|
|
}
|
|
if (!d->hGrpSettings->GetBool("SavePythonHistory", false)) {
|
|
return;
|
|
}
|
|
QFile f(d->historyFile);
|
|
if (f.open(QIODevice::WriteOnly)) {
|
|
QTextStream t (&f);
|
|
QStringList hist = d->history.values();
|
|
// only save last 100 entries so we don't inflate forever...
|
|
if (hist.length() > 100) {
|
|
hist = hist.mid(hist.length()-100);
|
|
}
|
|
for (const auto & it : hist) {
|
|
t << it << "\n";
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
PythonConsoleHighlighter::PythonConsoleHighlighter(QObject* parent)
|
|
: PythonSyntaxHighlighter(parent)
|
|
{
|
|
}
|
|
|
|
PythonConsoleHighlighter::~PythonConsoleHighlighter() = default;
|
|
|
|
void PythonConsoleHighlighter::highlightBlock(const QString& text)
|
|
{
|
|
const int ErrorOutput = (int)PythonConsoleP::Error;
|
|
const int MessageOutput = (int)PythonConsoleP::Message;
|
|
|
|
// Get user state to re-highlight the blocks in the appropriate format
|
|
int stateOfPara = currentBlockState();
|
|
|
|
switch (stateOfPara)
|
|
{
|
|
case ErrorOutput:
|
|
{
|
|
// Error output
|
|
QTextCharFormat errorFormat;
|
|
errorFormat.setForeground(color(QLatin1String("Python error")));
|
|
errorFormat.setFontItalic(true);
|
|
setFormat( 0, text.length(), errorFormat);
|
|
} break;
|
|
case MessageOutput:
|
|
{
|
|
// Normal output
|
|
QTextCharFormat outputFormat;
|
|
outputFormat.setForeground(color(QLatin1String("Python output")));
|
|
setFormat( 0, text.length(), outputFormat);
|
|
} break;
|
|
default:
|
|
{
|
|
PythonSyntaxHighlighter::highlightBlock(text);
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void PythonConsoleHighlighter::colorChanged(const QString& type, const QColor& col)
|
|
{
|
|
Q_UNUSED(type);
|
|
Q_UNUSED(col);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
ConsoleHistory::ConsoleHistory()
|
|
: _scratchBegin(0)
|
|
{
|
|
_it = _history.cend();
|
|
}
|
|
|
|
ConsoleHistory::~ConsoleHistory() = default;
|
|
|
|
void ConsoleHistory::first()
|
|
{
|
|
_it = _history.cbegin();
|
|
}
|
|
|
|
bool ConsoleHistory::more()
|
|
{
|
|
return (_it != _history.cend());
|
|
}
|
|
|
|
/**
|
|
* next switches the history pointer to the next item.
|
|
* While searching the next item, the routine respects the search prefix set by prev().
|
|
* @return true if the pointer was switched to a later item, false otherwise.
|
|
*/
|
|
bool ConsoleHistory::next()
|
|
{
|
|
bool wentNext = false;
|
|
|
|
// if we didn't reach history's end ...
|
|
if (_it != _history.cend())
|
|
{
|
|
// we go forward until we find an item matching the prefix.
|
|
for (++_it; _it != _history.cend(); ++_it) {
|
|
if (!_it->isEmpty() && _it->startsWith( _prefix )) {
|
|
break;
|
|
}
|
|
}
|
|
// we did a step - no matter of a matching prefix.
|
|
wentNext = true;
|
|
}
|
|
return wentNext;
|
|
}
|
|
|
|
/**
|
|
* prev switches the history pointer to the previous item.
|
|
* The optional parameter prefix allows one to search the history selectively for commands that start
|
|
* with a certain character sequence.
|
|
* @param prefix - prefix string for searching backwards in history, empty string by default
|
|
* @return true if the pointer was switched to an earlier item, false otherwise.
|
|
*/
|
|
bool ConsoleHistory::prev( const QString &prefix )
|
|
{
|
|
bool wentPrev = false;
|
|
|
|
// store prefix if it's the first history access
|
|
if (_it == _history.cend()) {
|
|
_prefix = prefix;
|
|
}
|
|
|
|
// while we didn't go back or reach history's begin ...
|
|
while (!wentPrev && _it != _history.cbegin()) {
|
|
// go back in history and check if item matches prefix
|
|
// Skip empty items
|
|
--_it;
|
|
wentPrev = (!_it->isEmpty() && _it->startsWith( _prefix ));
|
|
}
|
|
return wentPrev;
|
|
}
|
|
|
|
bool ConsoleHistory::isEmpty() const
|
|
{
|
|
return _history.isEmpty();
|
|
}
|
|
|
|
const QString& ConsoleHistory::value() const
|
|
{
|
|
return ((_it != _history.end())? *_it
|
|
/* else */ : _prefix);
|
|
}
|
|
|
|
void ConsoleHistory::append( const QString& item )
|
|
{
|
|
_history.append( item );
|
|
// reset iterator to make the next history
|
|
// access begin with the latest item.
|
|
_it = _history.cend();
|
|
}
|
|
|
|
const QStringList& ConsoleHistory::values() const
|
|
{
|
|
return this->_history;
|
|
}
|
|
|
|
/**
|
|
* restart resets the history access to the latest item.
|
|
*/
|
|
void ConsoleHistory::restart( )
|
|
{
|
|
_it = _history.cend();
|
|
}
|
|
|
|
/**
|
|
* markScratch stores the current end index of the history list.
|
|
* Note: with simply remembering a start index, it does not work to nest scratch regions.
|
|
* However, just replace the index keeping by a stack - in case this is be a concern.
|
|
*/
|
|
void ConsoleHistory::markScratch( )
|
|
{
|
|
_scratchBegin = _history.length();
|
|
}
|
|
|
|
/**
|
|
* doScratch removes the tail of the history list, starting from the index marked lately.
|
|
*/
|
|
void ConsoleHistory::doScratch( )
|
|
{
|
|
if (_scratchBegin < _history.length()) {
|
|
_history.erase( _history.begin() + _scratchBegin, _history.end() );
|
|
this->restart();
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------
|
|
|
|
#include "moc_PythonConsole.cpp"
|