Add python shell qt window

This commit is contained in:
baldurk
2017-04-19 18:03:19 +01:00
parent 31050d651e
commit 2a4596e06a
13 changed files with 858 additions and 4 deletions
+10
View File
@@ -1399,6 +1399,16 @@ void MainWindow::on_action_Statistics_Viewer_triggered()
ui->toolWindowManager->addToolWindow(stats, mainToolArea());
}
void MainWindow::on_action_Python_Shell_triggered()
{
QWidget *py = m_Ctx.GetPythonShell()->Widget();
if(ui->toolWindowManager->toolWindows().contains(py))
ToolWindowManager::raiseToolWindow(py);
else
ui->toolWindowManager->addToolWindow(py, mainToolArea());
}
void MainWindow::on_action_Resolve_Symbols_triggered()
{
m_Ctx.Replay().AsyncInvoke([this](IReplayController *r) { r->InitResolver(); });
+2
View File
@@ -89,6 +89,7 @@ public:
void showCaptureDialog() { on_action_Capture_Log_triggered(); }
void showDebugMessageView() { on_action_Errors_and_Warnings_triggered(); }
void showStatisticsViewer() { on_action_Statistics_Viewer_triggered(); }
void showPythonShell() { on_action_Python_Shell_triggered(); }
private slots:
// automatic slots
void on_action_Exit_triggered();
@@ -104,6 +105,7 @@ private slots:
void on_action_Capture_Log_triggered();
void on_action_Errors_and_Warnings_triggered();
void on_action_Statistics_Viewer_triggered();
void on_action_Python_Shell_triggered();
void on_action_Inject_into_Process_triggered();
void on_action_Resolve_Symbols_triggered();
void on_action_Attach_to_Running_Instance_triggered();
+343
View File
@@ -0,0 +1,343 @@
/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2017 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include "PythonShell.h"
#include <QFontDatabase>
#include <QKeyEvent>
#include <QScrollBar>
#include "3rdparty/scintilla/include/SciLexer.h"
#include "3rdparty/scintilla/include/qt/ScintillaEdit.h"
#include "Code/ScintillaSyntax.h"
#include "Code/pyrenderdoc/PythonContext.h"
#include "ui_PythonShell.h"
PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
: QFrame(parent), ui(new Ui::PythonShell), m_Ctx(ctx)
{
ui->setupUi(this);
QObject::connect(ui->lineInput, &RDLineEdit::keyPress, this, &PythonShell::interactive_keypress);
ui->lineInput->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
ui->interactiveOutput->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
ui->scriptOutput->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
scriptEditor = new ScintillaEdit(this);
scriptEditor->setMarginLeft(4);
scriptEditor->setMarginWidthN(0, 32);
scriptEditor->setMarginWidthN(1, 0);
scriptEditor->setMarginWidthN(2, 16);
scriptEditor->setObjectName("scriptEditor");
scriptEditor->markerSetBack(CURRENT_MARKER, SCINTILLA_COLOUR(240, 128, 128));
scriptEditor->markerSetBack(CURRENT_MARKER + 1, SCINTILLA_COLOUR(240, 128, 128));
scriptEditor->markerDefine(CURRENT_MARKER, SC_MARK_SHORTARROW);
scriptEditor->markerDefine(CURRENT_MARKER + 1, SC_MARK_BACKGROUND);
ConfigureSyntax(scriptEditor, SCLEX_PYTHON);
scriptEditor->setTabWidth(4);
scriptEditor->setScrollWidth(1);
scriptEditor->setScrollWidthTracking(true);
scriptEditor->colourise(0, -1);
QObject::connect(scriptEditor, &ScintillaEdit::modified, [this](int type, int, int, int,
const QByteArray &, int, int, int) {
if(type & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT | SC_MOD_BEFOREINSERT | SC_MOD_BEFOREDELETE))
{
scriptEditor->markerDeleteAll(CURRENT_MARKER);
scriptEditor->markerDeleteAll(CURRENT_MARKER + 1);
}
});
ui->scriptSplitter->insertWidget(0, scriptEditor);
int w = ui->scriptSplitter->rect().width();
ui->scriptSplitter->setSizes({w * 2 / 3, w / 3});
ui->tabWidget->setCurrentIndex(0);
interactiveContext = NULL;
enableButtons(true);
// reset output to default
on_clear_clicked();
on_newScript_clicked();
}
PythonShell::~PythonShell()
{
m_Ctx.BuiltinWindowClosed(this);
interactiveContext->Finish();
delete ui;
}
void PythonShell::on_execute_clicked()
{
QString command = ui->lineInput->text();
appendText(ui->interactiveOutput, command + "\n");
if(command.trimmed().length() > 0)
interactiveContext->executeString(command, true);
history.push_front(command);
historyidx = -1;
ui->lineInput->clear();
appendText(ui->interactiveOutput, ">> ");
}
void PythonShell::on_clear_clicked()
{
QString minidocHeader = scriptHeader();
minidocHeader += "\n\n>> ";
ui->interactiveOutput->setText(minidocHeader);
if(interactiveContext)
interactiveContext->Finish();
interactiveContext = newContext();
}
void PythonShell::on_newScript_clicked()
{
QString minidocHeader = scriptHeader();
minidocHeader.replace('\n', "\n# ");
minidocHeader = "# " + minidocHeader + "\n\n";
scriptEditor->setText(minidocHeader.toUtf8().data());
scriptEditor->emptyUndoBuffer();
}
void PythonShell::on_openScript_clicked()
{
QString filename =
RDDialog::getOpenFileName(this, "Open Python Script", "", tr("Python scripts (*.py)"));
if(filename != "")
{
QFile f(filename);
if(f.open(QIODevice::ReadOnly | QIODevice::Text))
{
scriptEditor->setText(f.readAll().data());
}
else
{
RDDialog::critical(this, "Error loading script", tr("Couldn't open path %1.").arg(filename));
}
}
}
void PythonShell::on_saveScript_clicked()
{
QString filename = RDDialog::getSaveFileName(this, tr("Save Python Script"), QString(),
tr("Python scripts (*.py)"));
if(filename != "")
{
QDir dirinfo = QFileInfo(filename).dir();
if(dirinfo.exists())
{
QFile f(filename);
if(f.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
f.write(scriptEditor->getText(scriptEditor->textLength() + 1));
}
else
{
RDDialog::critical(
this, tr("Error saving script"),
tr("Couldn't open path %1 for write.\n%2").arg(filename).arg(f.errorString()));
}
}
else
{
RDDialog::critical(this, tr("Invalid directory"),
tr("Cannot find target directory to save to"));
}
}
}
void PythonShell::on_runScript_clicked()
{
PythonContext *context = newContext();
ui->scriptOutput->clear();
QString script = QString::fromUtf8(scriptEditor->getText(scriptEditor->textLength() + 1));
enableButtons(false);
LambdaThread *thread = new LambdaThread([this, script, context]() {
scriptContext = context;
context->executeString("script.py", script);
scriptContext = NULL;
context->Finish();
GUIInvoke::call([this]() { enableButtons(true); });
});
thread->selfDelete(true);
thread->start();
}
void PythonShell::on_abortRun_clicked()
{
if(scriptContext)
scriptContext->abort();
}
void PythonShell::traceLine(const QString &file, int line)
{
if(QObject::sender() == (QObject *)interactiveContext)
return;
scriptEditor->markerDeleteAll(CURRENT_MARKER);
scriptEditor->markerDeleteAll(CURRENT_MARKER + 1);
scriptEditor->markerAdd(line > 0 ? line - 1 : 0, CURRENT_MARKER);
scriptEditor->markerAdd(line > 0 ? line - 1 : 0, CURRENT_MARKER + 1);
}
void PythonShell::exception(const QString &type, const QString &value, QList<QString> frames)
{
QTextEdit *out = ui->scriptOutput;
if(QObject::sender() == (QObject *)interactiveContext)
out = ui->interactiveOutput;
QString exString;
if(!out->toPlainText().endsWith('\n'))
exString = "\n";
if(!frames.isEmpty())
{
exString += "Traceback (most recent call last):\n";
for(const QString &f : frames)
exString += QString(" %1\n").arg(f);
}
exString += QString("%1: %2\n").arg(type).arg(value);
appendText(out, exString);
}
void PythonShell::textOutput(bool isStdError, const QString &output)
{
QTextEdit *out = ui->scriptOutput;
if(QObject::sender() == (QObject *)interactiveContext)
out = ui->interactiveOutput;
appendText(out, output);
}
void PythonShell::interactive_keypress(QKeyEvent *event)
{
if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
on_execute_clicked();
bool moved = false;
if(event->key() == Qt::Key_Down && historyidx > -1)
{
historyidx--;
moved = true;
}
QString workingtext = "";
if(event->key() == Qt::Key_Up && historyidx + 1 < history.count())
{
if(historyidx == -1)
workingtext = ui->lineInput->text();
historyidx++;
moved = true;
}
if(moved)
{
if(historyidx == -1)
ui->lineInput->setText(workingtext);
else
ui->lineInput->setText(history[historyidx]);
ui->lineInput->deselect();
}
}
QString PythonShell::scriptHeader()
{
return tr(R"(RenderDoc Python console, powered by python %1.
The 'pyrenderdoc' object is the current CaptureContext instance.
The 'renderdoc' and 'qrenderdoc' modules are available.
Documentation is available: https://renderdoc.org/docs/python_api/index.html)")
.arg(interactiveContext->versionString());
}
void PythonShell::appendText(QTextEdit *output, const QString &text)
{
output->moveCursor(QTextCursor::End);
output->insertPlainText(text);
// scroll to the bottom
QScrollBar *vscroll = output->verticalScrollBar();
vscroll->setValue(vscroll->maximum());
}
void PythonShell::enableButtons(bool enable)
{
ui->newScript->setEnabled(enable);
ui->openScript->setEnabled(enable);
ui->saveScript->setEnabled(enable);
ui->runScript->setEnabled(enable);
ui->abortRun->setEnabled(!enable);
}
PythonContext *PythonShell::newContext()
{
PythonContext *ret = new PythonContext();
QObject::connect(ret, &PythonContext::traceLine, this, &PythonShell::traceLine);
QObject::connect(ret, &PythonContext::exception, this, &PythonShell::exception);
QObject::connect(ret, &PythonContext::textOutput, this, &PythonShell::textOutput);
ret->setGlobal("pyrenderdoc", &m_Ctx);
return ret;
}
+84
View File
@@ -0,0 +1,84 @@
/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2017 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#pragma once
#include <QFrame>
#include "Code/CaptureContext.h"
class PythonContext;
class ScintillaEdit;
class QTextEdit;
namespace Ui
{
class PythonShell;
}
class PythonShell : public QFrame, public IPythonShell
{
Q_OBJECT
public:
explicit PythonShell(ICaptureContext &ctx, QWidget *parent = 0);
~PythonShell();
// IPythonShell
QWidget *Widget() override { return this; }
private slots:
// automatic slots
void on_execute_clicked();
void on_clear_clicked();
void on_newScript_clicked();
void on_openScript_clicked();
void on_saveScript_clicked();
void on_runScript_clicked();
void on_abortRun_clicked();
// manual slots
void interactive_keypress(QKeyEvent *e);
void traceLine(const QString &file, int line);
void exception(const QString &type, const QString &value, QList<QString> frames);
void textOutput(bool isStdError, const QString &output);
private:
Ui::PythonShell *ui;
ICaptureContext &m_Ctx;
ScintillaEdit *scriptEditor;
static const int CURRENT_MARKER = 0;
PythonContext *interactiveContext, *scriptContext;
QList<QString> history;
int historyidx = -1;
PythonContext *newContext();
QString scriptHeader();
void appendText(QTextEdit *output, const QString &text);
void enableButtons(bool enable);
};
+315
View File
@@ -0,0 +1,315 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PythonShell</class>
<widget class="QFrame" name="PythonShell">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>544</width>
<height>346</height>
</rect>
</property>
<property name="windowTitle">
<string>Interactive Python Shell</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="interactiveTab">
<attribute name="title">
<string>Interactive Shell</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="RDTextEdit" name="interactiveOutput">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="RDLineEdit" name="lineInput"/>
</item>
<item>
<widget class="QPushButton" name="execute">
<property name="text">
<string>Execute</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clear">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="scriptTab">
<attribute name="title">
<string>Run Scripts</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QFrame" name="toolbar">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QToolButton" name="newScript">
<property name="toolTip">
<string>Create a new blank script</string>
</property>
<property name="text">
<string>New</string>
</property>
<property name="icon">
<iconset resource="../Resources/resources.qrc">
<normaloff>:/page_white_edit.png</normaloff>:/page_white_edit.png</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="openScript">
<property name="toolTip">
<string>Open an existing python script</string>
</property>
<property name="text">
<string>Open</string>
</property>
<property name="icon">
<iconset resource="../Resources/resources.qrc">
<normaloff>:/folder_page.png</normaloff>:/folder_page.png</iconset>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="saveScript">
<property name="toolTip">
<string>Save the current script to disk</string>
</property>
<property name="text">
<string>Save As</string>
</property>
<property name="icon">
<iconset resource="../Resources/resources.qrc">
<normaloff>:/save.png</normaloff>:/save.png</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Line" name="sep">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="runScript">
<property name="toolTip">
<string>Begin running the script in python</string>
</property>
<property name="text">
<string>Run</string>
</property>
<property name="icon">
<iconset resource="../Resources/resources.qrc">
<normaloff>:/runfwd.png</normaloff>:/runfwd.png</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="abortRun">
<property name="toolTip">
<string>Stop execution of the current script</string>
</property>
<property name="text">
<string>Abort</string>
</property>
<property name="icon">
<iconset resource="../Resources/resources.qrc">
<normaloff>:/del.png</normaloff>:/del.png</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QSplitter" name="scriptSplitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="RDTextEdit" name="scriptOutput">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>RDLineEdit</class>
<extends>QLineEdit</extends>
<header>Widgets/Extended/RDLineEdit.h</header>
</customwidget>
<customwidget>
<class>RDTextEdit</class>
<extends>QTextEdit</extends>
<header>Widgets/Extended/RDTextEdit.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../Resources/resources.qrc"/>
</resources>
<connections/>
</ui>