Logo Search packages:      
Sourcecode: freecad version File versions  Download package

PythonConsole.cpp

/***************************************************************************
 *   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 "PreCompiled.h"
#ifndef _PreComp_
# include <QClipboard>
# include <QDockWidget>
# include <QGridLayout>
# include <QHBoxLayout>
# include <QKeyEvent>
# include <QMenu>
# include <QMessageBox>
# include <QPushButton>
# include <QSpacerItem>
# include <QTextCursor>
# include <QTextDocumentFragment>
# include <QTextStream>
# include <QUrl>
#endif

#include "PythonConsole.h"
#include "PythonConsolePy.h"
#include "CallTips.h"
#include "Application.h"
#include "Action.h"
#include "Command.h"
#include "DlgEditorImp.h"
#include "FileDialog.h"
#include "MainWindow.h"


#include <Base/Interpreter.h>
#include <Base/Exception.h>
#include <CXX/Exception.hxx>

using namespace Gui;

namespace Gui {
struct PythonConsoleP
{
    enum Output {Error = 20, Message = 21};
    enum CopyType {Normal, History, Command};
    CopyType type;
    PyObject *_stdoutPy, *_stderrPy, *_stdinPy;
    PyObject *_stdout, *_stderr, *_stdin;
    InteractiveInterpreter* interpreter;
    CallTipsList* callTipsList;
    ConsoleHistory history;
    QString output, error;
    QStringList statements;
    bool interactive;
    QMap<QString, QColor> colormap; // Color map
    PythonConsoleP()
    {
        type = Normal;
        interpreter = 0;
        colormap[QLatin1String("Text")] = Qt::black;
        colormap[QLatin1String("Bookmark")] = Qt::cyan;
        colormap[QLatin1String("Breakpoint")] = Qt::red;
        colormap[QLatin1String("Keyword")] = Qt::blue;
        colormap[QLatin1String("Comment")] = QColor(0, 170, 0);
        colormap[QLatin1String("Block comment")] = QColor(160, 160, 164);
        colormap[QLatin1String("Number")] = Qt::blue;
        colormap[QLatin1String("String")] = Qt::red;
        colormap[QLatin1String("Character")] = Qt::red;
        colormap[QLatin1String("Class name")] = QColor(255, 170, 0);
        colormap[QLatin1String("Define name")] = QColor(255, 170, 0);
        colormap[QLatin1String("Operator")] = QColor(160, 160, 164);
        colormap[QLatin1String("Python output")] = QColor(170, 170, 127);
        colormap[QLatin1String("Python error")] = Qt::red;
    }
};
struct InteractiveInterpreterP
{
    PyObject* interpreter;
    PyObject* sysmodule;
    QStringList buffer;
};
} // 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 = PyEval_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.
 */
00130 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", PyString_FromString(">>> "));
    if (!PyObject_HasAttrString(d->sysmodule, "ps2"))
        PyObject_SetAttrString(d->sysmodule, "ps2", PyString_FromString("... "));
}

/**
 * Compile a command and determine whether it is incomplete.
 * 
 * The source string may contain \n characters.
 * Return value / exceptions raised:
 * \item Return a code object if the command is complete and valid
 * \item Return None if the command is incomplete
 * \item Raise SyntaxError, ValueError or OverflowError if the command is a
 * syntax error (OverflowError and ValueError can be produced by
 * malformed literals).
 */
00152 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 = PyEval_CallObject(func,args);  // must decref later

    Py_DECREF(args);
    Py_DECREF(func);

    if (eval){
        return eval;
    } else {
        // do not throw Base::PyException as this clears the error indicator
        throw Base::Exception();
    }

    // can never happen
    return 0;
}

/**
 * Compile a command and determine whether it is incomplete.
 * 
 * The source string may contain \n characters.
 * Return value:
 * \item Return  1 if the command is incomplete
 * \item Return  0 if the command is complete and valid
 * \item Return -1 if the command is a syntax error 
 * (OverflowError and ValueError can be produced by
 * malformed literals).
 */
00184 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 = PyEval_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:
 *
 * \item 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.
 *
 * \item The input is incomplete, and more input is required; compile() returned 'None'. 
 * Nothing happens.
 *
 * \item 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.
 */
00226 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, 0);
        // 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;
}

/* 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
{
    Base::PyGILStateLocker lock;
    PyObject *module, *dict, *presult;           /* "exec code in d, d" */
    module = PyImport_AddModule("__main__");     /* get module, init python */
    if (module == NULL) 
        throw Base::PyException();                 /* not incref'd */
    dict = PyModule_GetDict(module);             /* get dict namespace */
    if (dict == NULL) 
        throw Base::PyException();                 /* not incref'd */

    // It seems that the return value is always 'None' or Null
    presult = PyEval_EvalCode(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 */
            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.
 */
00290 bool InteractiveInterpreter::push(const char* line)
{
    d->buffer.append(QString::fromAscii(line));
    QString source = d->buffer.join(QLatin1String("\n"));
    try {
        // Source is already UTF-8, so we can use toAscii()
        bool more = runSource(source.toAscii());
        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;
}

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)
  : TextEdit(parent), WindowParameter( "Editor" )
{
    d = new PythonConsoleP();
    d->interactive = false;

    // create an instance of InteractiveInterpreter
    try { 
        d->interpreter = new InteractiveInterpreter();
    } catch (const Base::Exception& e) {
        setPlainText(QString::fromAscii(e.what()));
        setEnabled(false);
    }

    // use the console highlighter
    pythonSyntax = new PythonConsoleHighlighter(this);

    // create the window for call tips
    d->callTipsList = new CallTipsList(this);
    d->callTipsList->setFrameStyle(QFrame::Box|QFrame::Raised);
    d->callTipsList->setLineWidth(2);
    installEventFilter(d->callTipsList);
    viewport()->installEventFilter(d->callTipsList);
    d->callTipsList->setSelectionMode( QAbstractItemView::SingleSelection );
    d->callTipsList->hide();

    QFont serifFont(QLatin1String("Courier"), 10, QFont::Normal);
    setFont(serifFont);
    
    // set colors and font from settings
    ParameterGrp::handle hPrefGrp = getWindowParameter();
    hPrefGrp->Attach( this );
    hPrefGrp->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->_stdout = PySys_GetObject("stdout");
    d->_stderr = PySys_GetObject("stderr");
    d->_stdin  = PySys_GetObject("stdin");
    PySys_SetObject("stdin", d->_stdinPy);

    const char* version  = PyString_AsString(PySys_GetObject("version"));
    const char* platform = PyString_AsString(PySys_GetObject("platform"));
    d->output = QString::fromAscii("Python %1 on %2\n"
    "Type 'help', 'copyright', 'credits' or 'license' for more information.")
    .arg(QString::fromAscii(version)).arg(QString::fromAscii(platform));
    printPrompt(false);
}

/** Destroys the object and frees any allocated resources */
PythonConsole::~PythonConsole()
{
    Base::PyGILStateLocker lock;
    getWindowParameter()->Detach( this );
    delete pythonSyntax;
    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 paramerts. */  
void PythonConsole::OnChange( Base::Subject<const char*> &rCaller,const char* sReason )
{
    ParameterGrp::handle hPrefGrp = getWindowParameter();

    if (strcmp(sReason, "FontSize") == 0 || strcmp(sReason, "Font") == 0) {
        int fontSize = hPrefGrp->GetInt("FontSize", 10);
        QString fontFamily = QString::fromAscii(hPrefGrp->GetASCII("Font", "Courier").c_str());
        
        QFont font(fontFamily, fontSize);
        setFont(font);
        QFontMetrics metric(font);
        int width = metric.width(QLatin1String("0000"));
        setTabStopWidth(width);
    } else {
        QMap<QString, QColor>::ConstIterator it = d->colormap.find(QString::fromAscii(sReason));
        if (it != d->colormap.end()) {
            QColor color = it.value();
            unsigned long col = (color.red() << 24) | (color.green() << 16) | (color.blue() << 8);
            col = hPrefGrp->GetUnsigned( sReason, col);
            color.setRgb((col>>24)&0xff, (col>>16)&0xff, (col>>8)&0xff);
            pythonSyntax->setColor(QString::fromAscii(sReason), color);
        }
    }
}

/**
 * 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)
{
    if (e->modifiers() & Qt::ControlModifier) {
        switch( e->key() ) 
        {
        case Qt::Key_Up:
            {
                // no modification, just history facility
                if (!d->history.isEmpty()) {
                    if (d->history.prev()) {
                        QString cmd = d->history.value();
                        overrideCursor(cmd);
                    }   return;
                }
            }   break;
        case Qt::Key_Down:
            {
                // no modification, just history facility
                if (!d->history.isEmpty()) {
                    if (d->history.next()) {
                        QString cmd = d->history.value();
                        overrideCursor(cmd);
                    }   return;
                }
            }   break;
        default:
            break;
        }
    }

    switch (e->key())
    {
    // running Python interpreter?
    case Qt::Key_Return:
    case Qt::Key_Enter:
        {
            // make sure to be at the end
            QTextCursor cursor = textCursor();
            cursor.movePosition(QTextCursor::End);
 
            // get the last paragraph's text
            QTextBlock block = cursor.block();
            QString line = block.text();

            // and skip the first 4 characters consisting of either ">>> " or "... "
            line = line.mid(4);

            // put statement to the history
            d->history.append(line);

            // evaluate and run the command
            runSource(line);
        }   break;
    case Qt::Key_Period:
        {
            QTextCursor cursor = textCursor();
            QTextBlock block = cursor.block();
            QString text = block.text();
            int length = cursor.position() - block.position();
            TextEdit::keyPressEvent(e);
            d->callTipsList->showTips(text.left(length));
        }   break;
    case Qt::Key_Home:
        {
            if (e->modifiers() & Qt::ControlModifier) {
                TextEdit::keyPressEvent(e);
            } else {
                QTextCursor::MoveMode mode = e->modifiers() & Qt::ShiftModifier
                     ? QTextCursor::KeepAnchor
                     : QTextCursor::MoveAnchor;
                QTextCursor cursor = textCursor();
                QTextBlock block = cursor.block();
                QString text = block.text();
                int cursorPos = block.position();
                if (text.startsWith(QLatin1String(">>> ")) ||
                    text.startsWith(QLatin1String("... ")))
                    cursorPos += 4;
                cursor.setPosition(cursorPos, mode);
                setTextCursor(cursor);
                ensureCursorVisible();
            }
        }   break;
    default: 
        {
            TextEdit::keyPressEvent(e);

            // This can't be done in CallTipsList::eventFilter() because we must first perform
            // the event and afterwards update the list widget
            if (d->callTipsList->isVisible()) {
                d->callTipsList->validateCursor();
            }
        }   break;
    }  
}

/**
 * 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;
}

/** Prints the ps1 prompt (>>> ) for complete and ps2 prompt (... ) for
 * incomplete commands to the console window. 
 */ 
void PythonConsole::printPrompt(bool incomplete)
{
    // write normal messages
    if (!d->output.isEmpty()) {
        appendOutput(d->output, (int)PythonConsoleP::Message);
        d->output = QString::null;
    }

    // write error messages
    if (!d->error.isEmpty()) {
        appendOutput(d->error, (int)PythonConsoleP::Error);
        d->error = QString::null;
    }

    // Append the prompt string 
    QTextCursor cursor = textCursor();
    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);

    incomplete ? cursor.insertText(QString::fromAscii("... "))
               : cursor.insertText(QString::fromAscii(">>> "));
    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();
    append(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)
{
    bool incomplete = false;
    Base::PyGILStateLocker lock;
    PySys_SetObject("stdout", d->_stdoutPy);
    PySys_SetObject("stderr", d->_stderrPy);
    d->interactive = true;
    
    try {
        // launch the command now
        incomplete = d->interpreter->push(line.toUtf8());
        setFocus(); // if focus was lost
    } catch (const Base::SystemExitException&) {
        int ret = QMessageBox::question(this, tr("System exit"), tr("The application is still running.\nDo you want to exit without saving your data?"),
        QMessageBox::Yes, QMessageBox::No|QMessageBox::Escape|QMessageBox::Default);
        if (ret == QMessageBox::Yes) {
            Base::Interpreter().systemExit();
        } 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 (...) {
        QMessageBox::critical(this, tr("Python console"), tr("Unhandled unknown C++ exception."));
    }

    PySys_SetObject("stdout", d->_stdout);
    PySys_SetObject("stderr", d->_stderr);
    printPrompt(incomplete);
    d->interactive = false;
    for (QStringList::Iterator it = d->statements.begin(); it != d->statements.end(); ++it)
        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;
        if (ch == QLatin1Char('#'))
            return true;
    }

    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 (QStringList::Iterator it = statements.begin(); it != statements.end(); ++it) {
        // go to the end before inserting new text 
        cursor.movePosition(QTextCursor::End);
        cursor.insertText( *it );
        d->history.append( *it );
        printPrompt(false);
    }
}

/**
 * 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) {
        QDockWidget* dw = qobject_cast<QDockWidget*>(this->parentWidget());
        if (dw) {
            connect(dw, SIGNAL(visibilityChanged(bool)),
                    this, SLOT(visibilityChanged(bool)));
        }
    }
    TextEdit::changeEvent(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(QString::fromAscii("Gui.runCommand(\"%1\")").arg(action));
        }

        e->setDropAction(Qt::CopyAction);
        e->accept();
    }
    else // this will call insertFromMimeData
        QTextEdit::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
        QTextEdit::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
        QTextEdit::dragEnterEvent(e);
}

bool PythonConsole::canInsertFromMimeData (const QMimeData * source) const
{
    if (source->hasText())
        return true;
    if (source->hasUrls()) {
        QList<QUrl> uri = source->urls();
        for (QList<QUrl>::ConstIterator it = uri.begin(); it != uri.end(); ++it) {
            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 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
    if (source->hasUrls()) {
        QList<QUrl> uri = source->urls();
        for (QList<QUrl>::ConstIterator it = uri.begin(); it != uri.end(); ++it) {
            // get the file name and check the extension
            QFileInfo info((*it).toLocalFile());
            QString ext = info.suffix().toLower();
            if (info.exists() && 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();
            }
        }

        return;
    }
    if (source->hasText()) {
        runSourceFromMimeData(source->text());
        return;
    }
}

QMimeData * PythonConsole::createMimeDataFromSelection () const
{
    QMimeData* 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()) {
                            QString line = b.text();
                            // and skip the first 4 characters consisting of either ">>> " or "... "
                            line = line.mid(4);
                            lines << line;
                        }
                    }
                }

                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_MAC)
    //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 = cursor.block().text();
            line = line.mid(4);
        }

        // 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(true);
        }
        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(true);
            }
            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 = textCursor();
    QTextBlock block = cursor.block();
    cursor.movePosition(QTextCursor::End);
    cursor.movePosition(QTextCursor::StartOfLine);
    cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 4);
    cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, block.text().length());
    cursor.removeSelectedText();
    cursor.insertText(txt);
    // move cursor to the end
    cursor.movePosition(QTextCursor::End);
    setTextCursor(cursor);
}

void PythonConsole::contextMenuEvent ( QContextMenuEvent * e )
{
    QMenu* menu = new QMenu(this);
    QAction *a;

    a = menu->addAction(QTextEdit::tr("&Copy"), this, SLOT(copy()), Qt::CTRL+Qt::Key_C);
    a->setEnabled(textCursor().hasSelection());

    a = menu->addAction(tr("&Copy command"), this, SLOT(onCopyCommand()));
    a->setEnabled(textCursor().hasSelection());

    a = menu->addAction(tr("&Copy history"), this, SLOT(onCopyHistory()));
    a->setEnabled(!d->history.isEmpty());

    a = menu->addAction( tr("Save history as..."), this, SLOT(onSaveHistoryAs()));
    a->setEnabled(!d->history.isEmpty());

    menu->addSeparator();
    a = menu->addAction(QTextEdit::tr("&Paste"), this, SLOT(paste()), Qt::CTRL+Qt::Key_V);
    const QMimeData *md = QApplication::clipboard()->mimeData();
    a->setEnabled(md && canInsertFromMimeData(md));

    a = menu->addAction(QTextEdit::tr("Select All"), this, SLOT(selectAll()), Qt::CTRL+Qt::Key_A);
    a->setEnabled(!document()->isEmpty());

    menu->addSeparator();
    menu->addAction( tr("Insert file name..."), this, SLOT(onInsertFileName()));

    menu->exec(e->globalPos());
    delete menu;
}

void PythonConsole::onSaveHistoryAs()
{
    QString cMacroPath = QString::fromUtf8(getDefaultParameter()->GetGroup( "Macro" )->
        GetASCII("MacroPath",App::Application::getUserAppDataDir().c_str()).c_str());
    QString fn = FileDialog::getSaveFileName(this, tr("Save History"), cMacroPath,
        tr("Macro Files (*.FCMacro *.py)"));
    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 (QStringList::ConstIterator it = hist.begin(); it != hist.end(); ++it)
                    t << *it << "\n";
                f.close();
            }
        }
    }
}

void PythonConsole::onInsertFileName()
{
    QString fn = Gui::FileDialog::getOpenFileName(Gui::getMainWindow(), tr("Insert file name"), QString::null, tr("All Files (*.*)") );
    if ( fn.isEmpty() )
        return;
    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;
}

// ---------------------------------------------------------------------

PythonConsoleHighlighter::PythonConsoleHighlighter(QTextEdit* edit)
  : PythonSyntaxHighlighter(edit)
{
}

PythonConsoleHighlighter::~PythonConsoleHighlighter()
{
}

01069 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)
{
}

// ---------------------------------------------------------------------

ConsoleHistory::ConsoleHistory()
{
    it = _history.end();
}

ConsoleHistory::~ConsoleHistory()
{
}

void ConsoleHistory::first()
{
    it = _history.begin();
}

bool ConsoleHistory::more()
{
    return (it != _history.end());
}

bool ConsoleHistory::next() 
{
    if (it != _history.end()) {
        for (++it; it != _history.end(); ++it) {
            if (!it->isEmpty())
                break;
        }
        return true;
    }

    return false;
}

bool ConsoleHistory::prev() 
{
    if (it != _history.begin()) {
        for (--it; it != _history.begin(); --it) {
            if (!it->isEmpty())
                break;
        }
        return true;
    }

    return false;
}

bool ConsoleHistory::isEmpty() const
{
    return _history.isEmpty();
}

QString ConsoleHistory::value() const
{
    if ( it != _history.end() )
        return *it;
    else
        return QString::null;
}

void ConsoleHistory::append( const QString& item )
{
    _history.append( item );
    it = _history.end();
}

const QStringList& ConsoleHistory::values() const
{
    return this->_history;
}

// -----------------------------------------------------

PythonInputField::PythonInputField(QWidget* parent)
  : QWidget(parent)
{
    QGridLayout* gridLayout = new QGridLayout(this);
    gridLayout->setSpacing(6);
    gridLayout->setMargin(9);

    editField = new PythonEditor(this);
    gridLayout->addWidget(editField, 0, 0, 1, 1);

    QHBoxLayout* hboxLayout = new QHBoxLayout();
    hboxLayout->setSpacing(6);
    hboxLayout->setMargin(0);

    QSpacerItem* spacerItem = new QSpacerItem(131, 31, QSizePolicy::Expanding, QSizePolicy::Minimum);
    hboxLayout->addItem(spacerItem);

    okButton = new QPushButton(this);
    hboxLayout->addWidget(okButton);
    clearButton = new QPushButton(this);
    hboxLayout->addWidget(clearButton);
    gridLayout->addLayout(hboxLayout, 1, 0, 1, 1);


    this->setWindowTitle(Gui::PythonConsole::tr("Python Input Dialog"));
    okButton->setText(tr("OK"));
    clearButton->setText(tr("Clear"));

    QObject::connect(okButton, SIGNAL(clicked()), this, SIGNAL(textEntered()));
    QObject::connect(clearButton, SIGNAL(clicked()), editField, SLOT(clear()));
}

PythonInputField::~PythonInputField()
{
}

QString PythonInputField::getText() const
{
    return editField->toPlainText();
}

void PythonInputField::clear()
{
    return editField->clear();
}

void PythonInputField::changeEvent(QEvent *e)
{
    if (e->type() == QEvent::LanguageChange) {
        this->setWindowTitle(Gui::PythonConsole::tr("Python Input Dialog"));
        okButton->setText(tr("OK"));
        clearButton->setText(tr("Clear"));
    }
    else {
        QWidget::changeEvent(e);
    }
}

void PythonInputField::showEvent(QShowEvent* e)
{
    editField->setFocus();
}

#include "moc_PythonConsole.cpp"

Generated by  Doxygen 1.6.0   Back to index