Python Script Editor with HTML Output

File this one under Stupid Python Tricks.  I have written a bit about working on an app with an embedded Python run time.  It’s good fun.  I recently added a new feature to the script editor that was relatively easy, but for some reason isn’t very common.  There are a few small quirks to making a script editor do this, so in case anybody is curious how to do it in their own app, this is how I did it.

Behold the rich text glory!

Behold the rich text glory!  HTML output from Python directly in the script editor!  It really is like Christmas!

Capturing the output of a python script is surprisingly easy.

import sys
sys.stdout = OutputHandler

As long as your OutputHandler object has a write() method, everything that the script tries to print will go to your custom class.  You may want to add some support for things like flush() and the rest of the file API for completeness, but most scripts will never know the difference as long as write() is there.  You can do whatever you want in the implementation.  Throw it away.  Print it out.  Stuff it in a database.  Or print it to stdout and then also emit a signal in C++ to notify a Qt GUI widget that new output is available.  That’s what I did.

void PythonSupport::write(std::string input)
{
    std::cout << input << std::endl;
    emit newText(QString::fromStdString(input).trimmed());
    std::cout.flush();
}

I used Boost to expose a PythonSupport object with this method to Python as my OutputHandler.  Mine also has a similar writeErr() and writeHTML() method which emit a different signal.

For the GUI, I used a QTextEditor set to disallow user input but allow the user to select text to copy and paste it.  It automatically shows scrollbars when needed.

scriptOutput->setTextInteractionFlags(Qt::TextSelectableByMouse);

QTextEditor supports rich text using an HTML subset, so in the slot for output I can make script output green, but make errors show up in red.  QTextEditor::append() creates a new block of text, and blocks can have different colors.  Unfortunately append() will attempt to guess if something is HTML or plain text, but I want anything emitted as normal script output to be treated as plain text and shown exactly, so I use QTextCursor::insertText() to bypass the convenience guessing of QTextEdit::append().

void ScriptEditor::onScriptOutput(QString output)
{
    scriptOutput->append("");
    scriptOutput->moveCursor(QTextCursor::End);
    scriptOutput->setTextColor(Qt::green);
    scriptOutput->textCursor().insertText(output);
    scriptOutput->moveCursor(QTextCursor::End);
}

ScriptEditor::onScriptError(QString) looks pretty similar to onScriptOutput(), but makes the text red.  The next step is to actually capture errors from Python to send to the GUI.  That’s not quite as easy as capturing stdout.  Just setting sys.stderr won’t quite do it.  When I run the script, I check for errors and handle them as follows:

try {
        bpy::handle<> ignored((
             PyRun_String(script.c_str(), Py_file_input,
             main_namespace.ptr(), main_namespace.ptr())
        ));
}
catch (bpy::error_already_set) {
        while (PyErr_Occurred()) {
            dispatchPythonError();
        }
}
void PythonSupport::dispatchPythonError() {
    PyObject *ptype, *pvalue, *ptraceback;
    PyErr_Fetch(&ptype, &pvalue, &ptraceback);
    char *pStrErrorMessage = PyString_AsString(pvalue);
    if (pStrErrorMessage) {
        writeErr(std::string(pStrErrorMessage));
    }
}

Now we have working red and green (Christmas colors!) output to differentiate stdout and errors.  (You can also capture the traceback if you need it.)  Adding HTML support is almost trivial once we’ve made it this far.  Using the QTextCursor, we insertHtml() rather than insertText(), again avoiding the guessing of append() so the user can be explicit about what their script is outputting, and how it should be shown.  It’s important to capture the currentCharFormat() and restore it in case the user outputs broken HTML.  You can put the output in a weird state if you don’t close your tags, and have normal print statements outputting as the last HTML style if you don’t do this.

void ScriptEditor::onScriptHTML(QString htmlOutput)
{
    auto fmt = scriptOutput->currentCharFormat();
    scriptOutput->moveCursor(QTextCursor::End);
    scriptOutput->textCursor().insertHtml(htmlOutput);
    scriptOutput->moveCursor(QTextCursor::End);
    scriptOutput->setCurrentCharFormat(fmt);
}

Voila.  It may not be the most important feature anybody ever added to an app, but I am pleased with the result, so I figured it couldn’t hurt to share.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s