Files
renderdoc/qrenderdoc/Windows/PythonShell.cpp
T
baldurk 934800793a Improve workflow for capture import/export
* If the export doesn't need buffers, we export directly from the loaded
  capture file instead of re-loading it.
* Add progress bars for the load step so it shows what's happening
  instead of looking stalled.
* Reduce compression rate on XML+ZIP buffers as it took too long trying
  to compress when exporting large captures.
2018-02-19 00:52:13 +00:00

1113 lines
36 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2017-2018 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 <QMenu>
#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"
// a forwarder that invokes onto the UI thread wherever necessary.
// Note this does NOT make CaptureContext thread safe. We just invoke for any potentially UI
// operations. All invokes are blocking, so there can't be any times when the UI thread waits
// on the python thread.
struct CaptureContextInvoker : ICaptureContext
{
ICaptureContext &m_Ctx;
CaptureContextInvoker(ICaptureContext &ctx) : m_Ctx(ctx) {}
virtual ~CaptureContextInvoker() {}
//
///////////////////////////////////////////////////////////////////////
// pass-through functions that don't need the UI thread
///////////////////////////////////////////////////////////////////////
//
virtual rdcstr TempCaptureFilename(const rdcstr &appname) override
{
return m_Ctx.TempCaptureFilename(appname);
}
virtual IReplayManager &Replay() override { return m_Ctx.Replay(); }
virtual bool IsCaptureLoaded() override { return m_Ctx.IsCaptureLoaded(); }
virtual bool IsCaptureLocal() override { return m_Ctx.IsCaptureLocal(); }
virtual bool IsCaptureTemporary() override { return m_Ctx.IsCaptureTemporary(); }
virtual bool IsCaptureLoading() override { return m_Ctx.IsCaptureLoading(); }
virtual rdcstr GetCaptureFilename() override { return m_Ctx.GetCaptureFilename(); }
virtual CaptureModifications GetCaptureModifications() override
{
return m_Ctx.GetCaptureModifications();
}
virtual const FrameDescription &FrameInfo() override { return m_Ctx.FrameInfo(); }
virtual const APIProperties &APIProps() override { return m_Ctx.APIProps(); }
virtual uint32_t CurSelectedEvent() override { return m_Ctx.CurSelectedEvent(); }
virtual uint32_t CurEvent() override { return m_Ctx.CurEvent(); }
virtual const DrawcallDescription *CurSelectedDrawcall() override
{
return m_Ctx.CurSelectedDrawcall();
}
virtual const DrawcallDescription *CurDrawcall() override { return m_Ctx.CurDrawcall(); }
virtual const DrawcallDescription *GetFirstDrawcall() override
{
return m_Ctx.GetFirstDrawcall();
}
virtual const DrawcallDescription *GetLastDrawcall() override { return m_Ctx.GetLastDrawcall(); }
virtual const rdcarray<DrawcallDescription> &CurDrawcalls() override
{
return m_Ctx.CurDrawcalls();
}
virtual ResourceDescription *GetResource(ResourceId id) override { return m_Ctx.GetResource(id); }
virtual const rdcarray<ResourceDescription> &GetResources() override
{
return m_Ctx.GetResources();
}
virtual rdcstr GetResourceName(ResourceId id) override { return m_Ctx.GetResourceName(id); }
virtual bool IsAutogeneratedName(ResourceId id) override { return m_Ctx.IsAutogeneratedName(id); }
virtual bool HasResourceCustomName(ResourceId id) override
{
return m_Ctx.HasResourceCustomName(id);
}
virtual int ResourceNameCacheID() override { return m_Ctx.ResourceNameCacheID(); }
virtual TextureDescription *GetTexture(ResourceId id) override { return m_Ctx.GetTexture(id); }
virtual const rdcarray<TextureDescription> &GetTextures() override { return m_Ctx.GetTextures(); }
virtual BufferDescription *GetBuffer(ResourceId id) override { return m_Ctx.GetBuffer(id); }
virtual const rdcarray<BufferDescription> &GetBuffers() override { return m_Ctx.GetBuffers(); }
virtual const DrawcallDescription *GetDrawcall(uint32_t eventId) override
{
return m_Ctx.GetDrawcall(eventId);
}
virtual void CreateRGPMapping(uint32_t version) override
{
return m_Ctx.CreateRGPMapping(version);
}
virtual uint32_t GetRGPIdFromEventId(uint32_t eventId) override
{
return m_Ctx.GetRGPIdFromEventId(eventId);
}
virtual uint32_t GetEventIdFromRGPId(uint32_t RGPId) override
{
return m_Ctx.GetEventIdFromRGPId(RGPId);
}
virtual const SDFile &GetStructuredFile() override { return m_Ctx.GetStructuredFile(); }
virtual WindowingSystem CurWindowingSystem() override { return m_Ctx.CurWindowingSystem(); }
virtual WindowingData CreateWindowingData(uintptr_t winId) override
{
return m_Ctx.CreateWindowingData(winId);
}
virtual const rdcarray<DebugMessage> &DebugMessages() override { return m_Ctx.DebugMessages(); }
virtual int UnreadMessageCount() override { return m_Ctx.UnreadMessageCount(); }
virtual void MarkMessagesRead() override { return m_Ctx.MarkMessagesRead(); }
virtual rdcstr GetNotes(const rdcstr &key) override { return m_Ctx.GetNotes(key); }
virtual rdcarray<EventBookmark> GetBookmarks() override { return m_Ctx.GetBookmarks(); }
virtual const D3D11Pipe::State &CurD3D11PipelineState() override
{
return m_Ctx.CurD3D11PipelineState();
}
virtual const D3D12Pipe::State &CurD3D12PipelineState() override
{
return m_Ctx.CurD3D12PipelineState();
}
virtual const GLPipe::State &CurGLPipelineState() override { return m_Ctx.CurGLPipelineState(); }
virtual const VKPipe::State &CurVulkanPipelineState() override
{
return m_Ctx.CurVulkanPipelineState();
}
virtual CommonPipelineState &CurPipelineState() override { return m_Ctx.CurPipelineState(); }
virtual PersistantConfig &Config() override { return m_Ctx.Config(); }
//
///////////////////////////////////////////////////////////////////////
// functions that invoke onto the UI thread
///////////////////////////////////////////////////////////////////////
//
template <typename F, typename... paramTypes>
void InvokeVoidFunction(F ptr, paramTypes... params)
{
if(!GUIInvoke::onUIThread())
{
GUIInvoke::blockcall([this, ptr, params...]() { (m_Ctx.*ptr)(params...); });
return;
}
(m_Ctx.*ptr)(params...);
}
template <typename R, typename F, typename... paramTypes>
R InvokeRetFunction(F ptr, paramTypes... params)
{
if(!GUIInvoke::onUIThread())
{
R ret;
GUIInvoke::blockcall([this, &ret, ptr, params...]() { ret = (m_Ctx.*ptr)(params...); });
return ret;
}
return (m_Ctx.*ptr)(params...);
}
virtual void LoadCapture(const rdcstr &capture, const rdcstr &origFilename, bool temporary,
bool local) override
{
InvokeVoidFunction(&ICaptureContext::LoadCapture, capture, origFilename, temporary, local);
}
virtual bool SaveCaptureTo(const rdcstr &capture) override
{
return InvokeRetFunction<bool>(&ICaptureContext::SaveCaptureTo, capture);
}
virtual void RecompressCapture() override
{
InvokeVoidFunction(&ICaptureContext::RecompressCapture);
}
virtual void CloseCapture() override { InvokeVoidFunction(&ICaptureContext::CloseCapture); }
virtual bool ImportCapture(const CaptureFileFormat &fmt, const rdcstr &importfile,
const rdcstr &rdcfile) override
{
return InvokeRetFunction<bool>(&ICaptureContext::ImportCapture, fmt, importfile, rdcfile);
}
virtual void ExportCapture(const CaptureFileFormat &fmt, const rdcstr &exportfile) override
{
InvokeVoidFunction(&ICaptureContext::ExportCapture, fmt, exportfile);
}
virtual void SetEventID(const rdcarray<ICaptureViewer *> &exclude, uint32_t selectedEventID,
uint32_t eventId, bool force = false) override
{
InvokeVoidFunction(&ICaptureContext::SetEventID, exclude, selectedEventID, eventId, force);
}
virtual void RefreshStatus() override { InvokeVoidFunction(&ICaptureContext::RefreshStatus); }
virtual void AddCaptureViewer(ICaptureViewer *viewer) override
{
InvokeVoidFunction(&ICaptureContext::AddCaptureViewer, viewer);
}
virtual void RemoveCaptureViewer(ICaptureViewer *viewer) override
{
InvokeVoidFunction(&ICaptureContext::RemoveCaptureViewer, viewer);
}
virtual void AddMessages(const rdcarray<DebugMessage> &msgs) override
{
InvokeVoidFunction(&ICaptureContext::AddMessages, msgs);
}
virtual void SetResourceCustomName(ResourceId id, const rdcstr &name) override
{
InvokeVoidFunction(&ICaptureContext::SetResourceCustomName, id, name);
}
virtual void SetNotes(const rdcstr &key, const rdcstr &contents) override
{
InvokeVoidFunction(&ICaptureContext::SetNotes, key, contents);
}
virtual void SetBookmark(const EventBookmark &mark) override
{
InvokeVoidFunction(&ICaptureContext::SetBookmark, mark);
}
virtual void RemoveBookmark(uint32_t EID) override
{
InvokeVoidFunction(&ICaptureContext::RemoveBookmark, EID);
}
virtual IMainWindow *GetMainWindow() override
{
return InvokeRetFunction<IMainWindow *>(&ICaptureContext::GetMainWindow);
}
virtual IEventBrowser *GetEventBrowser() override
{
return InvokeRetFunction<IEventBrowser *>(&ICaptureContext::GetEventBrowser);
}
virtual IAPIInspector *GetAPIInspector() override
{
return InvokeRetFunction<IAPIInspector *>(&ICaptureContext::GetAPIInspector);
}
virtual ITextureViewer *GetTextureViewer() override
{
return InvokeRetFunction<ITextureViewer *>(&ICaptureContext::GetTextureViewer);
}
virtual IBufferViewer *GetMeshPreview() override
{
return InvokeRetFunction<IBufferViewer *>(&ICaptureContext::GetMeshPreview);
}
virtual IPipelineStateViewer *GetPipelineViewer() override
{
return InvokeRetFunction<IPipelineStateViewer *>(&ICaptureContext::GetPipelineViewer);
}
virtual ICaptureDialog *GetCaptureDialog() override
{
return InvokeRetFunction<ICaptureDialog *>(&ICaptureContext::GetCaptureDialog);
}
virtual IDebugMessageView *GetDebugMessageView() override
{
return InvokeRetFunction<IDebugMessageView *>(&ICaptureContext::GetDebugMessageView);
}
virtual ICommentView *GetCommentView() override
{
return InvokeRetFunction<ICommentView *>(&ICaptureContext::GetCommentView);
}
virtual IPerformanceCounterViewer *GetPerformanceCounterViewer() override
{
return InvokeRetFunction<IPerformanceCounterViewer *>(
&ICaptureContext::GetPerformanceCounterViewer);
}
virtual IStatisticsViewer *GetStatisticsViewer() override
{
return InvokeRetFunction<IStatisticsViewer *>(&ICaptureContext::GetStatisticsViewer);
}
virtual ITimelineBar *GetTimelineBar() override
{
return InvokeRetFunction<ITimelineBar *>(&ICaptureContext::GetTimelineBar);
}
virtual IPythonShell *GetPythonShell() override
{
return InvokeRetFunction<IPythonShell *>(&ICaptureContext::GetPythonShell);
}
virtual IResourceInspector *GetResourceInspector() override
{
return InvokeRetFunction<IResourceInspector *>(&ICaptureContext::GetResourceInspector);
}
virtual bool HasEventBrowser() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasEventBrowser);
}
virtual bool HasAPIInspector() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasAPIInspector);
}
virtual bool HasTextureViewer() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasTextureViewer);
}
virtual bool HasPipelineViewer() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasPipelineViewer);
}
virtual bool HasMeshPreview() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasMeshPreview);
}
virtual bool HasCaptureDialog() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasCaptureDialog);
}
virtual bool HasDebugMessageView() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasDebugMessageView);
}
virtual bool HasCommentView() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasCommentView);
}
virtual bool HasPerformanceCounterViewer() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasPerformanceCounterViewer);
}
virtual bool HasStatisticsViewer() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasStatisticsViewer);
}
virtual bool HasTimelineBar() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasTimelineBar);
}
virtual bool HasPythonShell() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasPythonShell);
}
virtual bool HasResourceInspector() override
{
return InvokeRetFunction<bool>(&ICaptureContext::HasResourceInspector);
}
virtual void ShowEventBrowser() override
{
InvokeVoidFunction(&ICaptureContext::ShowEventBrowser);
}
virtual void ShowAPIInspector() override
{
InvokeVoidFunction(&ICaptureContext::ShowAPIInspector);
}
virtual void ShowTextureViewer() override
{
InvokeVoidFunction(&ICaptureContext::ShowTextureViewer);
}
virtual void ShowMeshPreview() override { InvokeVoidFunction(&ICaptureContext::ShowMeshPreview); }
virtual void ShowPipelineViewer() override
{
InvokeVoidFunction(&ICaptureContext::ShowPipelineViewer);
}
virtual void ShowCaptureDialog() override
{
InvokeVoidFunction(&ICaptureContext::ShowCaptureDialog);
}
virtual void ShowDebugMessageView() override
{
InvokeVoidFunction(&ICaptureContext::ShowDebugMessageView);
}
virtual void ShowCommentView() override { InvokeVoidFunction(&ICaptureContext::ShowCommentView); }
virtual void ShowPerformanceCounterViewer() override
{
InvokeVoidFunction(&ICaptureContext::ShowPerformanceCounterViewer);
}
virtual void ShowStatisticsViewer() override
{
InvokeVoidFunction(&ICaptureContext::ShowStatisticsViewer);
}
virtual void ShowTimelineBar() override { InvokeVoidFunction(&ICaptureContext::ShowTimelineBar); }
virtual void ShowPythonShell() override { InvokeVoidFunction(&ICaptureContext::ShowPythonShell); }
virtual void ShowResourceInspector() override
{
InvokeVoidFunction(&ICaptureContext::ShowResourceInspector);
}
virtual IShaderViewer *EditShader(bool customShader, const rdcstr &entryPoint,
const rdcstrpairs &files,
IShaderViewer::SaveCallback saveCallback,
IShaderViewer::CloseCallback closeCallback) override
{
return InvokeRetFunction<IShaderViewer *>(&ICaptureContext::EditShader, customShader,
entryPoint, files, saveCallback, closeCallback);
}
virtual IShaderViewer *DebugShader(const ShaderBindpointMapping *bind,
const ShaderReflection *shader, ResourceId pipeline,
ShaderDebugTrace *trace, const rdcstr &debugContext) override
{
return InvokeRetFunction<IShaderViewer *>(&ICaptureContext::DebugShader, bind, shader, pipeline,
trace, debugContext);
}
virtual IShaderViewer *ViewShader(const ShaderReflection *shader, ResourceId pipeline) override
{
return InvokeRetFunction<IShaderViewer *>(&ICaptureContext::ViewShader, shader, pipeline);
}
virtual IBufferViewer *ViewBuffer(uint64_t byteOffset, uint64_t byteSize, ResourceId id,
const rdcstr &format = "") override
{
return InvokeRetFunction<IBufferViewer *>(&ICaptureContext::ViewBuffer, byteOffset, byteSize,
id, format);
}
virtual IBufferViewer *ViewTextureAsBuffer(uint32_t arrayIdx, uint32_t mip, ResourceId id,
const rdcstr &format = "") override
{
return InvokeRetFunction<IBufferViewer *>(&ICaptureContext::ViewTextureAsBuffer, arrayIdx, mip,
id, format);
}
virtual IConstantBufferPreviewer *ViewConstantBuffer(ShaderStage stage, uint32_t slot,
uint32_t idx) override
{
return InvokeRetFunction<IConstantBufferPreviewer *>(&ICaptureContext::ViewConstantBuffer,
stage, slot, idx);
}
virtual IPixelHistoryView *ViewPixelHistory(ResourceId texID, int x, int y,
const TextureDisplay &display) override
{
return InvokeRetFunction<IPixelHistoryView *>(&ICaptureContext::ViewPixelHistory, texID, x, y,
display);
}
virtual QWidget *CreateBuiltinWindow(const rdcstr &objectName) override
{
return InvokeRetFunction<QWidget *>(&ICaptureContext::CreateBuiltinWindow, objectName);
}
virtual void BuiltinWindowClosed(QWidget *window) override
{
InvokeVoidFunction(&ICaptureContext::BuiltinWindowClosed, window);
}
virtual void RaiseDockWindow(QWidget *dockWindow) override
{
InvokeVoidFunction(&ICaptureContext::RaiseDockWindow, dockWindow);
}
virtual void AddDockWindow(QWidget *newWindow, DockReference ref, QWidget *refWindow,
float percentage = 0.5f) override
{
InvokeVoidFunction(&ICaptureContext::AddDockWindow, newWindow, ref, refWindow, percentage);
}
};
PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
: QFrame(parent), ui(new Ui::PythonShell), m_Ctx(ctx)
{
ui->setupUi(this);
m_ThreadCtx = new CaptureContextInvoker(m_Ctx);
QObject::connect(ui->lineInput, &RDLineEdit::keyPress, this, &PythonShell::interactive_keypress);
QObject::connect(ui->helpSearch, &RDLineEdit::keyPress, this, &PythonShell::helpSearch_keypress);
ui->lineInput->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
ui->interactiveOutput->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
ui->scriptOutput->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
ui->helpText->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
ui->lineInput->setAcceptTabCharacters(true);
scriptEditor = new ScintillaEdit(this);
scriptEditor->styleSetFont(
STYLE_DEFAULT, QFontDatabase::systemFont(QFontDatabase::FixedFont).family().toUtf8().data());
scriptEditor->setMarginLeft(4.0 * devicePixelRatioF());
scriptEditor->setMarginWidthN(0, 32.0 * devicePixelRatioF());
scriptEditor->setMarginWidthN(1, 0);
scriptEditor->setMarginWidthN(2, 16.0 * devicePixelRatioF());
scriptEditor->setObjectName(lit("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);
scriptEditor->autoCSetMaxHeight(10);
scriptEditor->usePopUp(SC_POPUP_NEVER);
scriptEditor->setContextMenuPolicy(Qt::CustomContextMenu);
QObject::connect(scriptEditor, &ScintillaEdit::customContextMenuRequested, this,
&PythonShell::editor_contextMenu);
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);
}
});
QObject::connect(scriptEditor, &ScintillaEdit::charAdded, [this](int ch) {
if(ch == '.')
{
startAutocomplete();
}
});
QObject::connect(scriptEditor, &ScintillaEdit::keyPressed, [this](QKeyEvent *ev) {
if(ev->key() == Qt::Key_Space && (ev->modifiers() & Qt::ControlModifier))
{
startAutocomplete();
}
if(ev->key() == Qt::Key_F1)
{
QString curWord = getDottedWordAtPoint(scriptEditor->currentPos());
if(!curWord.isEmpty())
{
ui->helpSearch->setText(curWord);
refreshCurrentHelp();
}
}
});
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 m_ThreadCtx;
delete ui;
}
void PythonShell::on_execute_clicked()
{
QString command = ui->lineInput->text();
ANALYTIC_SET(UIFeatures.PythonInterop, true);
appendText(ui->interactiveOutput, command + lit("\n"));
history.push_front(command);
historyidx = -1;
ui->lineInput->clear();
// assume a trailing colon means there will be continuation. Store the command and add a continue
// prompt. If we're already continuing, then wait until we get a blank line before executing.
if(command.trimmed().right(1) == lit(":") || (!m_storedLines.isEmpty() && !command.isEmpty()))
{
appendText(ui->interactiveOutput, lit(".. "));
m_storedLines += command + lit("\n");
return;
}
// concatenate any previous lines if we are doing a multi-line command.
command = m_storedLines + command;
m_storedLines = QString();
if(command.trimmed().length() > 0)
interactiveContext->executeString(command);
appendText(ui->interactiveOutput, lit(">> "));
}
void PythonShell::on_clear_clicked()
{
QString minidocHeader = scriptHeader();
minidocHeader += lit("\n\n>> ");
ui->interactiveOutput->setText(minidocHeader);
if(interactiveContext)
interactiveContext->Finish();
interactiveContext = newContext();
}
void PythonShell::on_newScript_clicked()
{
QString minidocHeader = scriptHeader();
minidocHeader.replace(QLatin1Char('\n'), lit("\n# "));
minidocHeader = QFormatStr("# %1\n\n").arg(minidocHeader);
scriptEditor->setText(minidocHeader.toUtf8().data());
scriptEditor->emptyUndoBuffer();
}
void PythonShell::on_openScript_clicked()
{
QString filename = RDDialog::getOpenFileName(this, tr("Open Python Script"), QString(),
tr("Python scripts (*.py)"));
if(!filename.isEmpty())
{
QFile f(filename);
if(f.open(QIODevice::ReadOnly | QIODevice::Text))
{
scriptEditor->setText(f.readAll().data());
}
else
{
RDDialog::critical(this, tr("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.isEmpty())
{
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();
ANALYTIC_SET(UIFeatures.PythonInterop, true);
ui->outputHelpTabs->setCurrentIndex(0);
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(lit("script.py"), script);
scriptContext = NULL;
GUIInvoke::call([this, context]() {
context->Finish();
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, int finalLine,
QList<QString> frames)
{
QTextEdit *out = ui->scriptOutput;
if(QObject::sender() == (QObject *)interactiveContext)
out = ui->interactiveOutput;
QString exString;
if(finalLine >= 0)
traceLine(QString(), finalLine);
if(!out->toPlainText().endsWith(QLatin1Char('\n')))
exString = lit("\n");
if(!frames.isEmpty())
{
exString += tr("Traceback (most recent call last):\n");
for(const QString &f : frames)
exString += QFormatStr(" %1\n").arg(f);
}
exString += QFormatStr("%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::editor_contextMenu(const QPoint &pos)
{
int scintillaPos = scriptEditor->positionFromPoint(pos.x(), pos.y());
QMenu contextMenu(this);
QString curWord = getDottedWordAtPoint(scintillaPos);
bool valid = !curWord.isEmpty();
QAction help(valid ? tr("Help for '%1'").arg(curWord) : tr("Help"), this);
QObject::connect(&help, &QAction::triggered, [this, curWord] { selectedHelp(curWord); });
help.setEnabled(valid);
contextMenu.addAction(&help);
contextMenu.addSeparator();
QAction undo(tr("Undo"), this);
QAction redo(tr("Redo"), this);
QObject::connect(&undo, &QAction::triggered, [this] { scriptEditor->undo(); });
QObject::connect(&redo, &QAction::triggered, [this] { scriptEditor->redo(); });
undo.setEnabled(scriptEditor->canUndo());
redo.setEnabled(scriptEditor->canRedo());
contextMenu.addAction(&undo);
contextMenu.addAction(&redo);
contextMenu.addSeparator();
QAction cutText(tr("Cut"), this);
QAction copyText(tr("Copy"), this);
QAction pasteText(tr("Paste"), this);
QAction deleteText(tr("Delete"), this);
QObject::connect(&cutText, &QAction::triggered, [this] { scriptEditor->cut(); });
QObject::connect(&copyText, &QAction::triggered, [this] {
scriptEditor->copyRange(scriptEditor->selectionStart(), scriptEditor->selectionEnd());
});
QObject::connect(&pasteText, &QAction::triggered, [this] { scriptEditor->paste(); });
QObject::connect(&deleteText, &QAction::triggered, [this] {
scriptEditor->deleteRange(scriptEditor->selectionStart(), scriptEditor->selectionEnd());
});
contextMenu.addAction(&cutText);
contextMenu.addAction(&copyText);
contextMenu.addAction(&pasteText);
contextMenu.addAction(&deleteText);
contextMenu.addSeparator();
if(scriptEditor->selectionEmpty())
{
cutText.setEnabled(false);
copyText.setEnabled(false);
deleteText.setEnabled(false);
}
pasteText.setEnabled(scriptEditor->canPaste());
QAction selectAll(tr("Select All"), this);
QObject::connect(&selectAll, &QAction::triggered, [this] { scriptEditor->selectAll(); });
contextMenu.addAction(&selectAll);
RDDialog::show(&contextMenu, scriptEditor->viewport()->mapToGlobal(pos));
}
QString PythonShell::getDottedWordAtPoint(int scintillaPos)
{
QByteArray wordChars = scriptEditor->wordChars();
QByteArray wordCharsAndDot = wordChars;
if(wordCharsAndDot.indexOf('.') < 0)
wordCharsAndDot.append('.');
scriptEditor->setWordChars(wordCharsAndDot.data());
sptr_t start = scriptEditor->wordStartPosition(scintillaPos, true);
sptr_t end = scriptEditor->wordEndPosition(scintillaPos, true);
scriptEditor->setWordChars(wordChars.data());
QString curWord = QString::fromUtf8(scriptEditor->textRange(start, end));
bool valid = true;
if(curWord.isEmpty() || (!curWord[0].isLetterOrNumber() && curWord[0] != QLatin1Char('_')))
valid = false;
for(QChar c : curWord)
{
if(!c.isLetterOrNumber() && c != QLatin1Char('_') && c != QLatin1Char('.'))
valid = false;
}
return valid ? curWord : QString();
}
void PythonShell::selectedHelp(QString word)
{
ui->helpSearch->setText(word);
refreshCurrentHelp();
}
void PythonShell::refreshCurrentHelp()
{
PythonContext *context = newImportedDummyContext();
ui->helpText->clear();
QObject::connect(
context, &PythonContext::textOutput,
[this](bool isStdError, const QString &output) { appendText(ui->helpText, output); });
context->executeString(lit(R"(
try:
import keyword
if keyword.iskeyword("%1"):
help("%1")
else:
help(%1)
except ImportError:
help(%1)
)").arg(ui->helpSearch->text()));
context->Finish();
}
void PythonShell::interactive_keypress(QKeyEvent *event)
{
if(event->key() == Qt::Key_Tab)
{
QString base = ui->lineInput->text();
if(!base.isEmpty() && !base.rbegin()->isSpace())
{
// search backwards from the end for the first non dotted identifier first, and extract that
// substring. This is just ASCII, not unicode
for(int i = base.count() - 1; i >= 0; i--)
{
if(!base[i].isLetterOrNumber() && base[i] != QLatin1Char('.') && base[i] != QLatin1Char('_'))
{
base = base.right(base.count() - 1 - i);
break;
}
}
// skip any initial digits that got included in the coarse search above
while(!base.isEmpty() && base[0].isDigit())
base.remove(0, 1);
QStringList options = interactiveContext->completionOptions(base);
QString line = ui->lineInput->text();
if(!options.isEmpty())
{
QString commonSubstring = options[0];
for(int i = 1; i < options.count(); i++)
{
const QString &opt = options[i];
if(opt.count() < commonSubstring.count())
commonSubstring.truncate(opt.count());
for(int j = 0; j < commonSubstring.count(); j++)
{
if(commonSubstring[j] != opt[j])
{
commonSubstring.truncate(j);
break;
}
}
}
if(commonSubstring.length() > base.length())
{
line.chop(base.length());
line += commonSubstring;
ui->lineInput->setText(line);
}
if(options.count() > 1)
{
QString text;
text += line;
text += lit("\n");
for(const QString &opt : options)
{
text += opt;
text += lit("\n");
}
text += m_storedLines.isEmpty() ? lit(">> ") : lit(".. ");
appendText(ui->interactiveOutput, text);
}
}
return;
}
ui->lineInput->insert(lit("\t"));
return;
}
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();
}
}
void PythonShell::helpSearch_keypress(QKeyEvent *e)
{
if(e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter)
refreshCurrentHelp();
}
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);
}
void PythonShell::startAutocomplete()
{
sptr_t pos = scriptEditor->currentPos();
sptr_t line = scriptEditor->lineFromPosition(pos);
sptr_t lineStart = scriptEditor->positionFromLine(line);
QByteArray lineText = scriptEditor->getLine(line);
sptr_t end = pos - lineStart - 1;
sptr_t start;
for(start = end; start >= 0; start--)
{
char c = lineText[(int)start];
if(QChar::fromLatin1(c).isLetterOrNumber() || c == '.' || c == '_')
continue;
start++;
break;
}
QString comp = QString::fromUtf8(lineText.mid(start, end - start + 1));
PythonContext *context = newImportedDummyContext();
QStringList completions = context->completionOptions(comp);
context->Finish();
scriptEditor->autoCShow(comp.count(), completions.join(QLatin1Char(' ')).toUtf8().data());
}
PythonContext *PythonShell::newImportedDummyContext()
{
sptr_t pos = scriptEditor->currentPos();
PythonContext *context = new PythonContext();
setGlobals(context);
// super hack. Try to import any modules to get completion suggestions from them.
// we only process imports with no indentation since they should be unconditional. We ignore
// imports that fail.
QByteArray text = scriptEditor->getText(pos + 1);
for(int offs = 0; offs < text.length();)
{
// find the next newline (may be NULL if we're at the end)
int newline = text.indexOf('\n', offs);
// execute the import if there is one
const char *c = text.data() + offs;
if(!strncmp(c, "import ", 7))
{
context->executeString(newline >= 0 ? QString::fromUtf8(c, newline - offs + 1)
: QString::fromUtf8(c));
}
if(newline < 0)
break;
// move to the next line
offs = newline + 1;
}
return context;
}
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);
setGlobals(ret);
return ret;
}
void PythonShell::setGlobals(PythonContext *ret)
{
ret->setGlobal("pyrenderdoc", (ICaptureContext *)m_ThreadCtx);
}