Files
renderdoc/qrenderdoc/Windows/PythonShell.cpp
T
baldurk 85075565cd Don't hang onto new extensions item, recreate each time
* Clearing an RDTreeWidget deletes all items in it, so we would either need to
  remove it before clearing or just recreate it. We only need the handle in
  order to check against it when we see clicks
2026-08-24 17:04:14 +01:00

2557 lines
72 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2017-2026 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 <QAbstractItemView>
#include <QCompleter>
#include <QDesktopServices>
#include <QDialogButtonBox>
#include <QFileSystemWatcher>
#include <QFontDatabase>
#include <QKeyEvent>
#include <QListWidget>
#include <QMenu>
#include <QScrollBar>
#include <QStringListModel>
#include <QTimer>
#include "Code/QRDUtils.h"
#include "Code/Resources.h"
#include "Code/ScintillaSyntax.h"
#include "Code/pyrenderdoc/PythonContext.h"
#include "Widgets/Extended/RDLabel.h"
#include "Widgets/Extended/RDToolTip.h"
#include "Widgets/FindReplace.h"
#include "scintilla/include/SciLexer.h"
#include "toolwindowmanager/ToolWindowManagerArea.h"
#include "ui_PythonShell.h"
enum
{
AllOutputFilter,
ScriptOutputFilter,
FirstExtensionOutputFilter,
};
EditorWrapper::EditorWrapper(PythonShell *parent) : QFrame(parent), m_PyShell(parent)
{
m_Scintilla = new ScintillaEdit(this);
m_Warning = new RDLabel(this);
m_Warning->setForegroundRole(QPalette::ToolTipText);
m_Warning->setBackgroundRole(QPalette::ToolTipBase);
m_Warning->setAutoFillBackground(true);
m_Warning->setMargin(6);
m_Warning->setFrameStyle(QFrame::Box);
m_Warning->setAlignment(Qt::AlignLeft);
m_Warning->setIndent(1);
m_Warning->hide();
setLayout(new QVBoxLayout(this));
layout()->addWidget(m_Warning);
layout()->addWidget(m_Scintilla);
layout()->setSpacing(0);
layout()->setMargin(0);
layout()->setContentsMargins(0, 0, 0, 0);
m_Title = tr("Untitled Script");
}
EditorWrapper::~EditorWrapper()
{
m_PyShell->removeEditor(this);
}
void EditorWrapper::setFilename(QString filename)
{
m_Filename = filename;
if(!m_Filename.isEmpty())
m_Title.clear();
updateTitle();
}
void EditorWrapper::setTitle(QString title)
{
if(!m_Filename.isEmpty())
return;
m_Title = title;
updateTitle();
}
void EditorWrapper::setWarning(QString text)
{
m_Warning->setText(text);
m_Warning->setVisible(!text.isEmpty());
}
void EditorWrapper::markModified(bool modified)
{
m_Modified = modified;
updateTitle();
}
void EditorWrapper::updateTitle()
{
QString title;
if(m_Filename.isEmpty())
{
if(isModified())
setWindowTitle(m_Title + lit(" *"));
else
setWindowTitle(m_Title);
}
else
{
if(isModified())
setWindowTitle(QFileInfo(m_Filename).fileName() + lit(" *"));
else
setWindowTitle(QFileInfo(m_Filename).fileName());
ToolWindowManager *manager = ToolWindowManager::managerOf(this);
if(manager)
{
ToolWindowManagerArea *editorTabs = manager->areaOf(this);
if(editorTabs)
{
int idx = editorTabs->indexOf(this);
if(idx >= 0)
editorTabs->setTabToolTip(idx, m_Filename);
}
}
}
}
bool EditorWrapper::checkAllowClose()
{
if(isModified())
{
QString filename = m_Filename;
bool untitled = false;
if(filename.isEmpty())
{
untitled = true;
filename = lit("Untitled Script");
}
QMessageBox::StandardButton res = RDDialog::question(this, tr("Python script is modified"),
tr("You have unsaved changes to '%1'.\n"
"Do you want to save them?")
.arg(QFileInfo(filename).fileName()),
RDDialog::YesNoCancel);
if(res == QMessageBox::Cancel)
return false;
if(res == QMessageBox::No)
return true;
if(untitled)
return m_PyShell->saveEditorAs(this);
else
return m_PyShell->saveEditor(this, filename);
}
return true;
}
// See PythonInvokers.cpp
ICaptureContext *MakeCaptureContextInvoker(PythonShell *shell, ICaptureContext &ctx);
void FreeCaptureContextInvoker(ICaptureContext *ctx);
PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
: QFrame(parent), ui(new Ui::PythonShell), m_Ctx(ctx)
{
ui->setupUi(this);
m_ThreadCtx = MakeCaptureContextInvoker(this, m_Ctx);
QObject::connect(ui->lineInput, &RDLineEdit::keyPress, this, &PythonShell::interactive_keypress);
QObject::connect(ui->helpSearch, &RDLineEdit::keyPress, this, &PythonShell::helpSearch_keypress);
QObject::connect(ui->lineInput, &RDLineEdit::leave, [this]() { hideFunccompleteTooltip(); });
// we create this up front so its state stays persistent as much as possible.
m_FindReplace = new FindReplace(m_Scintillas, this);
m_FindReplace->allowFindAll(false);
m_FindReplace->setDockManager(ui->docking);
{
m_FindResults = new ScintillaEdit(this);
m_FindResults->styleSetFont(STYLE_DEFAULT, Formatter::FixedFont().family().toUtf8().data());
m_FindResults->styleSetSize(STYLE_DEFAULT, Formatter::FixedFont().pointSize());
m_FindResults->styleSetFont(STYLE_ERROR, Formatter::FixedFont().family().toUtf8().data());
m_FindResults->styleSetSize(STYLE_ERROR, Formatter::FixedFont().pointSize());
m_FindResults->styleSetBack(STYLE_ERROR, IsDarkTheme() ? SCINTILLA_COLOUR(175, 70, 70)
: SCINTILLA_COLOUR(255, 150, 150));
ConfigureSyntax(m_FindResults, SCLEX_NULL);
m_FindResults->usePopUp(SC_POPUP_NEVER);
m_FindResults->setWrapMode(SC_WRAP_WORD);
m_FindResults->setReadOnly(true);
m_FindResults->setWindowTitle(lit("Find Results"));
}
m_FindReplace->setFindIndicator(2);
m_FindReplace->setFindAllResultsDisplay(m_FindResults);
ui->lineInput->setFont(Formatter::FixedFont());
ui->interactiveOutput->setFont(Formatter::FixedFont());
ui->scriptOutput->setFont(Formatter::FixedFont());
ui->helpText->setFont(Formatter::FixedFont());
ui->outputGroup->setWindowTitle(tr("Output"));
ui->helpGroup->setWindowTitle(tr("Help"));
ui->replGroup->setWindowTitle(tr("Interactive REPL"));
ui->lineInput->setAcceptTabCharacters(true);
// don't repeatedly re-parse for errors. Have a reasonable timeout
m_SyntaxCheckTimer = new QTimer(this);
m_SyntaxCheckTimer->setSingleShot(true);
m_SyntaxCheckTimer->setInterval(1200);
m_CompletionTipTimer = new QTimer(this);
m_CompletionTipTimer->setSingleShot(false);
// this timer will only be active while auto completing so we can be aggressive with its timer
m_CompletionTipTimer->setInterval(10);
QObject::connect(m_CompletionTipTimer, &QTimer::timeout, [this]() {
EditorWrapper *editor = curEditor();
if(!editor || !editor->scintilla()->autoCActive())
{
m_CompletionTipTimer->stop();
m_CompletionTipList.clear();
m_CurrentCompletionTip = -1;
updateCompletionTip();
}
else
{
if(editor->scintilla()->autoCCurrent() != m_CurrentCompletionTip)
{
m_CurrentCompletionTip = editor->scintilla()->autoCCurrent();
updateCompletionTip();
}
}
});
// only update the current line intermittently. We don't need to update every single time and if
// there is a large number of traces this will rate limit it.
m_CurLineTimer = new QTimer(this);
m_CurLineTimer->setSingleShot(false);
m_CurLineTimer->setInterval(10);
QObject::connect(m_CurLineTimer, &QTimer::timeout, [this]() {
if(m_CurLineDirty)
{
if(runningScriptEditor)
{
runningScriptEditor->markerDeleteAll(CURRENT_MARKER);
runningScriptEditor->markerDeleteAll(CURRENT_MARKER + 1);
runningScriptEditor->markerAdd(m_CurLine > 0 ? m_CurLine - 1 : 0, CURRENT_MARKER);
runningScriptEditor->markerAdd(m_CurLine > 0 ? m_CurLine - 1 : 0, CURRENT_MARKER + 1);
}
m_CurLineDirty = false;
}
});
completionContext = new PythonContext();
setGlobals(completionContext);
// if we're help printing in the completion context, append it to the help text
QObject::connect(completionContext, &PythonContext::textOutput,
[this](const QString &, bool isStdError, const QString &output) {
if(m_HelpPrinting)
appendText(ui->helpText, output);
});
QObject::connect(m_SyntaxCheckTimer, &QTimer::timeout, this, &PythonShell::doSyntaxCheck);
QObject::connect(ui->interactiveOutput, &RDTextEdit::keyPress, [this](QKeyEvent *e) {
// ignore keypresses that aren't typing, but for up/down redirect that to the line input to get history
if((e->text().isEmpty() || !e->text()[0].isPrint()) && e->key() != Qt::Key_Up &&
e->key() != Qt::Key_Down)
return;
ui->lineInput->setFocus(Qt::OtherFocusReason);
QApplication::postEvent(ui->lineInput, new QKeyEvent(*e));
});
m_ToolTip = new RDToolTip(this);
m_ToolTip->installEventFilter(this);
m_ToolTip->setFont(Formatter::FixedFont());
m_CompletionTip = new RDToolTip(this);
m_CompletionTip->setFont(Formatter::FixedFont());
m_InteractiveCompleter = new QCompleter(this);
m_InteractiveCompleter->popup()->setFont(Formatter::FixedFont());
m_InteractiveCompleter->popup()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
m_InteractiveCompleter->popup()->setTextElideMode(Qt::ElideNone);
m_InteractiveCompleter->setWidget(ui->lineInput);
m_InteractiveCompleter->setCompletionMode(QCompleter::UnfilteredPopupCompletion);
m_InteractiveCompleter->setWrapAround(false);
m_InteractiveCompletionModel = new QStringListModel(this);
m_InteractiveCompleter->setModel(m_InteractiveCompletionModel);
m_InteractiveCompleter->setCompletionRole(Qt::DisplayRole);
m_InteractiveCompleter->popup()->installEventFilter(this);
QObject::connect(m_InteractiveCompleter,
OverloadedSlot<const QModelIndex &>::of(&QCompleter::highlighted),
[this](const QModelIndex &idx) {
if(idx.isValid() && idx.row() < m_CompletionTipList.count())
{
if(m_CurrentCompletionTip != idx.row())
{
m_CurrentCompletionTip = idx.row();
updateCompletionTip();
}
}
});
QObject::connect(m_InteractiveCompleter,
OverloadedSlot<const QModelIndex &>::of(&QCompleter::activated),
[this](const QModelIndex &idx) {
int i = idx.row();
if(i >= 0 && i < m_InteractiveCompletionModel->rowCount())
{
QString curText = ui->lineInput->text();
curText.resize(curText.size() - m_InteractiveCompletionPrefix);
curText += m_InteractiveCompletionModel->stringList()[i];
ui->lineInput->setText(curText);
ui->lineInput->setCursorPosition(curText.size());
}
});
// reset output to default
on_clear_clicked();
on_newScript_clicked();
ui->saveScript->setEnabled(false);
setupTabs();
ui->docking->addToolWindow(m_FindReplace, ToolWindowManager::NoArea);
ui->docking->setToolWindowProperties(m_FindReplace, ToolWindowManager::HideOnClose);
ui->docking->addToolWindow(m_FindResults, ToolWindowManager::NoArea);
ui->docking->setToolWindowProperties(
m_FindResults, ToolWindowManager::HideOnClose | ToolWindowManager::DisallowFloatWindow);
ui->docking->addToolWindow(
ui->replGroup, ToolWindowManager::AreaReference(ToolWindowManager::BottomOf,
ui->docking->areaOf(m_Editors[0]), 0.3f));
ui->docking->setToolWindowProperties(
ui->replGroup, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow);
ui->projectExplorer->setWindowTitle(tr("Project Explorer"));
ui->projectExplorer->setColumns({tr("Name")});
ui->projectExplorer->hideGridLines();
m_Watcher = new QFileSystemWatcher({}, this);
QObject::connect(m_Watcher, &QFileSystemWatcher::fileChanged, this, &PythonShell::openFileModified);
QObject::connect(m_Watcher, &QFileSystemWatcher::directoryChanged, this,
&PythonShell::updateExtensionProjects);
QTimer *pyStatusTimer = new QTimer(this);
QObject::connect(pyStatusTimer, &QTimer::timeout, [this]() {
QList<QString> curModExts;
for(const ExtensionMetadata &m : m_Ctx.Extensions().GetInstalledExtensions())
{
if(m.hasChanges)
{
curModExts.push_back(m.package);
}
}
curModExts.sort();
if(curModExts != m_ModifiedExtensions)
{
updateExtensionProjects();
m_ModifiedExtensions = curModExts;
}
bool hasDebugger = PythonContext::IsDebuggerConnected();
if(m_DebuggerAttached != hasDebugger)
{
updateButtonStates();
updateNonDebugWarning();
}
m_DebuggerAttached = hasDebugger;
});
pyStatusTimer->setSingleShot(false);
pyStatusTimer->setInterval(500);
pyStatusTimer->start();
ui->projectExplorer->setContextMenuPolicy(Qt::CustomContextMenu);
QObject::connect(ui->projectExplorer, &RDTreeWidget::customContextMenuRequested, this,
&PythonShell::projectExplorer_contextMenu);
ui->projectExplorer->beginUpdate();
m_Examples = new RDTreeWidgetItem({lit("Examples")});
m_Examples->setData(0, Qt::UserRole + 1, m_Examples->text(0));
m_Examples->setSelectable(false);
m_Examples->setBold(true);
m_Examples->setIcon(0, Icons::help());
const QPair<QString, QString> examples[] = {
{tr("Tutorial: First Steps with Python"), lit(":/py/tutorial/first_steps.py")},
{tr("Tutorial: UI extensions"), lit(":/py/tutorial/ui_extensions.py")},
{tr("Show buffer with format"), lit(":/py/examples/show_buffer.py")},
{tr("Show and save a texture"), lit(":/py/examples/show_texture.py")},
{tr("Iterating over Actions"), lit(":/py/examples/iter_actions.py")},
{tr("Pipeline State"), lit(":/py/examples/pipe_state.py")},
{tr("Shader Reflection"), lit(":/py/examples/shader_refl.py")},
{tr("Resource Usage"), lit(":/py/examples/resource_usage.py")},
{tr("Memory bindings"), lit(":/py/examples/mem_binds.py")},
{tr("Pixel History & Shader Debug"), lit(":/py/examples/history_debug.py")},
{tr("Mesh Output"), lit(":/py/examples/mesh_output.py")},
{tr("Advanced Buffers"), lit(":/py/examples/advanced_buffers.py")},
{tr("Launching an application"), lit(":/py/examples/exe_launching.py")},
{tr("Custom event filter"), lit(":/py/examples/event_filter.py")},
{tr("Mini-Qt UI"), lit(":/py/examples/miniqt_ui.py")},
};
for(const QPair<QString, QString> &example : examples)
{
RDTreeWidgetItem *ex = new RDTreeWidgetItem({example.first});
QFile file(example.second);
file.open(QFile::ReadOnly);
ex->setData(0, Qt::UserRole, QString::fromUtf8(file.readAll()));
file.close();
m_Examples->addChild(ex);
}
m_UIExtensions = new RDTreeWidgetItem({lit("UI Extensions")});
m_UIExtensions->setData(0, Qt::UserRole + 1, m_UIExtensions->text(0));
m_UIExtensions->setSelectable(false);
m_UIExtensions->setBold(true);
m_UIExtensions->setIcon(0, Icons::plugin());
m_RecentFiles = new RDTreeWidgetItem({lit("Recent files")});
m_RecentFiles->setData(0, Qt::UserRole + 1, m_UIExtensions->text(0));
m_RecentFiles->setSelectable(false);
m_RecentFiles->setBold(true);
m_RecentFiles->setIcon(0, Icons::page_white_edit());
ui->projectExplorer->addTopLevelItem(m_Examples);
ui->projectExplorer->addTopLevelItem(m_UIExtensions);
ui->projectExplorer->addTopLevelItem(m_RecentFiles);
updateRecentFiles(false);
updateExtensionProjects();
ui->projectExplorer->endUpdate();
ui->projectExplorer->expandItem(m_RecentFiles);
ui->docking->addToolWindow(
ui->projectExplorer, ToolWindowManager::AreaReference(
ToolWindowManager::LeftOf, ui->docking->areaOf(m_Editors[0]), 0.2f));
ui->docking->setToolWindowProperties(
ui->projectExplorer,
ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow);
ui->docking->addToolWindow(ui->outputGroup,
ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
ui->docking->areaOf(ui->replGroup)));
ui->docking->setToolWindowProperties(
ui->outputGroup, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow);
ui->docking->addToolWindow(ui->helpGroup,
ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
ui->docking->areaOf(ui->replGroup)));
ui->docking->setToolWindowProperties(
ui->helpGroup, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow);
ToolWindowManager::raiseToolWindow(ui->replGroup);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setSpacing(3);
layout->setContentsMargins(3, 3, 3, 3);
layout->addWidget(ui->toolbar);
layout->addWidget(ui->docking);
enableButtons(true);
Q_ASSERT(ui->outputContext->count() == AllOutputFilter);
ui->outputContext->addItem(tr("All"));
Q_ASSERT(ui->outputContext->count() == ScriptOutputFilter);
ui->outputContext->addItem(tr("Script"));
Q_ASSERT(ui->outputContext->count() == FirstExtensionOutputFilter);
for(const ExtensionMetadata &e : m_Ctx.Extensions().GetInstalledExtensions())
{
if(m_Ctx.Extensions().IsExtensionLoaded(e.package))
{
ui->outputContext->addItem(tr("Extension %1").arg(e.package));
loadedExtensions.push_back(e.package);
}
}
QObject::connect(PythonContext::GetExtensionContext(), &PythonContext::textOutput, this,
&PythonShell::textOutput);
QObject::connect(PythonContext::GetExtensionContext(), &PythonContext::exception, this,
&PythonShell::exception);
QObject::connect(PythonContext::GetExtensionContext(), &PythonContext::extensionLoaded, this,
&PythonShell::extensionLoaded);
m_Ctx.GetMainWindow()->RegisterShortcut("CTRL+S", this,
[this](QWidget *) { this->on_saveScript_clicked(); });
updateButtonStates();
// we defer debugging loading onto a thread so check after a delay
QTimer::singleShot(1200, [this]() {
if(!PythonContext::IsDebuggingEnabled())
{
updateButtonStates();
}
});
}
PythonShell::~PythonShell()
{
m_Ctx.BuiltinWindowClosed(this);
m_Ctx.GetMainWindow()->UnregisterShortcut("CTRL+S", this);
// on a clean shutdown, remove the unsaved temp file
QString unsavedFile = interactiveContext->GetTempFilename(lit("script.py"));
if(!unsavedFile.isEmpty() && QFile::exists(unsavedFile))
QFile::remove(unsavedFile);
for(EditorWrapper *edit : m_Editors)
delete edit;
delete m_ToolTip;
completionContext->Finish();
interactiveContext->Finish();
FreeCaptureContextInvoker(m_ThreadCtx);
delete ui;
}
void PythonShell::doSyntaxCheck()
{
EditorWrapper *editor = curEditor();
if(!editor)
return;
ScintillaEdit *sc = editor->scintilla();
if(sc->lexer() != SCLEX_PYTHON)
return;
// don't syntax check while the user still seems to be editing, e.g. with autocomplete or a
// function tooltip active. The syntax check timer will be restarted when these go away
if(sc->autoCActive() || m_FuncTip)
return;
// also don't do it while running, as it makes no sense and also could stall the UI thread if we're debugging
if(runningScriptEditor)
return;
QByteArray script = sc->getText(sc->textLength() + 1);
PyParseError parseError = completionContext->CheckPyParse(script, "script.py");
if(parseError.lineno >= 0)
{
sptr_t end = sc->lineLength(parseError.lineno - 1);
sptr_t linePos = sc->positionFromLine(parseError.lineno - 1);
while(QChar(QLatin1Char(script[int(linePos + end - 1)])).isSpace())
end--;
sc->setIndicatorCurrent(0);
sc->indicatorFillRange(linePos + parseError.offset - 1, end + 1 - parseError.offset);
sc->annotationSetText(parseError.lineno - 1, parseError.errStr.c_str());
sc->annotationSetVisible(ANNOTATION_BOXED);
sc->annotationSetStyle(parseError.lineno - 1, STYLE_ERROR);
}
}
void PythonShell::editorTab_Changed(int index)
{
EditorWrapper *editor = curEditor();
ui->saveScript->setEnabled(editor && editor->filename() != QString());
updateButtonStates();
updateNonDebugWarning();
}
void PythonShell::openFileModified(const QString &path)
{
for(EditorWrapper *edit : m_Editors)
{
if(edit->filename() == path)
{
// delay slightly to avoid reading while the file is being written or if it was deleted before
// being written as some editors do
QTimer::singleShot(150, [this, edit, path]() {
bool mod = edit->isModified();
// re-add the path, it may have been removed if the file was deleted
m_Watcher->addPath(path);
if(!mod && !m_Ctx.Config().Python_PromptReloadUnchanged)
{
// unconditionally reload
}
else
{
QString prompt = tr("Reload from disk and overwrite your changes?");
if(!mod)
prompt = tr("Reload from disk?");
QMessageBox::StandardButton response = RDDialog::question(
this, tr("%1 has been modified on disk").arg(QFileInfo(path).fileName()),
tr("%1 has been modified on disk. %2").arg(QFileInfo(path).fileName()).arg(prompt));
if(response == QMessageBox::No)
return;
}
QFile f(path);
if(f.open(QIODevice::ReadOnly | QIODevice::Text))
{
edit->scintilla()->setText(f.readAll().data());
edit->markModified(false);
return;
}
});
}
}
}
void PythonShell::editorTab_Menu(const QPoint &pos)
{
ToolWindowManagerArea *editorTabs = ui->docking->areaOf(m_Editors[0]);
int tabIndex = editorTabs->tabBar()->tabAt(pos);
if(tabIndex == -1)
return;
if(editorTabs->count() == 1)
return;
QAction closeTab(tr("Close tab"), this);
QAction closeOtherTabs(tr("Close other tabs"), this);
QAction closeRightTabs(tr("Close tabs to the right"), this);
QMenu contextMenu(this);
contextMenu.addAction(&closeTab);
contextMenu.addAction(&closeOtherTabs);
contextMenu.addAction(&closeRightTabs);
QObject::connect(&closeTab, &QAction::triggered, [editorTabs, tabIndex]() {
// remove the tab at this index
delete editorTabs->widget(tabIndex);
});
QObject::connect(&closeRightTabs, &QAction::triggered, [editorTabs, tabIndex]() {
for(int i = editorTabs->count() - 1; i > tabIndex; i--)
delete editorTabs->widget(i);
});
QObject::connect(&closeOtherTabs, &QAction::triggered, [editorTabs, tabIndex]() {
for(int i = editorTabs->count() - 1; i >= 0; i--)
if(i != tabIndex)
delete editorTabs->widget(i);
});
RDDialog::show(&contextMenu, QCursor::pos());
}
void PythonShell::updateExtensionProjects()
{
RDTreeViewExpansionState expansion;
ui->projectExplorer->saveExpansion(expansion, 0, Qt::UserRole + 1);
ui->projectExplorer->beginUpdate();
m_UIExtensions->clear();
for(const ExtensionMetadata &ext : m_Ctx.Extensions().GetInstalledExtensions())
{
QString name = ext.name;
if(ext.hasChanges)
name += tr(" (Reload required)");
RDTreeWidgetItem *root = new RDTreeWidgetItem({name});
root->setData(0, Qt::UserRole + 1, ext.package);
if(ext.hasChanges)
root->setItalic(true);
addExtensionDirItems(root, QDir(ext.filePath));
m_UIExtensions->addChild(root);
}
m_NewExtension = new RDTreeWidgetItem({tr("Create new...")});
m_NewExtension->setData(0, Qt::UserRole + 1, m_NewExtension->text(0));
m_NewExtension->setItalic(true);
m_NewExtension->setIcon(0, Icons::plugin_add());
m_UIExtensions->addChild(m_NewExtension);
ui->projectExplorer->endUpdate();
ui->projectExplorer->applyExpansion(expansion, 0, Qt::UserRole + 1);
}
void PythonShell::addExtensionDirItems(RDTreeWidgetItem *root, QDir dir)
{
m_Watcher->addPath(dir.absolutePath());
for(QString child : dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot))
{
QString path = dir.absoluteFilePath(child);
QFileInfo fileInfo(path);
// only show .py files, .md files (for README.md) and the extension.json
if(fileInfo.isFile() && fileInfo.suffix().toLower() != lit("py") &&
fileInfo.suffix().toLower() != lit("md") && child.toLower() != lit("extension.json"))
continue;
RDTreeWidgetItem *item = new RDTreeWidgetItem({child});
item->setData(0, Qt::UserRole + 1, child);
if(fileInfo.isDir())
{
// ignore some directories
if(child.toLower() == lit("__pycache__") || child.toLower() == lit(".git") ||
child.toLower() == lit(".vscode"))
continue;
item->setIcon(0, Icons::folder());
addExtensionDirItems(item, QDir(path));
}
else
{
item->setData(0, Qt::UserRole, path);
}
root->addChild(item);
}
}
EditorWrapper *PythonShell::curEditor()
{
for(EditorWrapper *edit : m_Editors)
{
if(edit->isVisible())
return edit;
}
return NULL;
}
void PythonShell::makeEditor(rdcstr filename, rdcstr text)
{
EditorWrapper *editor = new EditorWrapper(this);
editor->setObjectName(lit("scriptEditor"));
ScintillaEdit *sc = editor->scintilla();
editor->setFilename(filename);
sc->indicSetFore(0, 0x0000ff);
m_FindReplace->configureFindIndicator(sc);
sc->styleSetFont(STYLE_DEFAULT, Formatter::FixedFont().family().toUtf8().data());
sc->styleSetSize(STYLE_DEFAULT, Formatter::FixedFont().pointSize());
sc->styleSetFont(STYLE_ERROR, Formatter::FixedFont().family().toUtf8().data());
sc->styleSetSize(STYLE_ERROR, Formatter::FixedFont().pointSize());
sc->styleSetBack(STYLE_ERROR,
IsDarkTheme() ? SCINTILLA_COLOUR(175, 70, 70) : SCINTILLA_COLOUR(255, 150, 150));
sc->setMarginLeft(4.0);
sc->setMarginWidthN(0, 32.0);
sc->setMarginWidthN(1, 0.0);
sc->setMarginWidthN(2, 16.0);
sc->markerSetBack(CURRENT_MARKER, SCINTILLA_COLOUR(240, 128, 128));
sc->markerSetBack(CURRENT_MARKER + 1, SCINTILLA_COLOUR(240, 128, 128));
sc->markerDefine(CURRENT_MARKER, SC_MARK_SHORTARROW);
sc->markerDefine(CURRENT_MARKER + 1, SC_MARK_BACKGROUND);
sc->usePopUp(SC_POPUP_NEVER);
sc->setContextMenuPolicy(Qt::CustomContextMenu);
QObject::connect(sc, &ScintillaEdit::customContextMenuRequested, this,
&PythonShell::editor_contextMenu);
QString suffix;
if(!filename.isEmpty())
suffix = QFileInfo(filename).suffix().toLower();
if(suffix == lit("md"))
{
ConfigureSyntax(sc, SCLEX_NULL);
sc->setWrapMode(SC_WRAP_WORD);
}
else if(suffix.toLower() == lit("json"))
{
ConfigureSyntax(sc, SCLEX_JSON);
sc->setWrapMode(SC_WRAP_WORD);
}
else
{
ConfigureSyntax(sc, SCLEX_PYTHON);
}
sc->setTabWidth(4);
sc->setUseTabs(false);
sc->setScrollWidth(1);
sc->setScrollWidthTracking(true);
sc->colourise(0, -1);
sc->autoCSetMaxHeight(10);
sc->autoCSetCancelAtStart(false);
sc->setMouseDwellTime(400);
sc->installEventFilter(this);
// start syntax checking if we exit autocomplete
QObject::connect(sc, &ScintillaEdit::autoCompleteCancelled,
[this]() { m_SyntaxCheckTimer->start(); });
QObject::connect(sc, &ScintillaEdit::autoCompleteSelection,
[this]() { m_SyntaxCheckTimer->start(); });
QObject::connect(
sc, &ScintillaEdit::modified,
[this, editor, sc](int type, int, int, int, const QByteArray &text, int, int, int) {
if(type & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT | SC_MOD_BEFOREINSERT | SC_MOD_BEFOREDELETE))
{
m_FindReplace->clearFindState();
editor->markModified(true);
sc->markerDeleteAll(CURRENT_MARKER);
sc->markerDeleteAll(CURRENT_MARKER + 1);
// always remove errors immediately
sc->setIndicatorCurrent(0);
sc->indicatorClearRange(0, sc->textLength());
sc->annotationClearAll();
m_SyntaxCheckTimer->start();
}
if(type & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT))
{
if(!sc->autoCActive() || text.contains('\r') || text.contains('\n'))
{
completionContext->reflectSource(QString::fromUtf8(sc->getText(sc->textLength() + 1)));
}
else if(sc->autoCActive())
{
// delay updating the autocomplete so the current cursor position is updated
GUIInvoke::defer(this, [this, sc]() { doAutocomplete(sc); });
}
}
});
QObject::connect(sc, &ScintillaEdit::dwellStart, [this, sc](int x, int y) {
if(sc->autoCActive())
return;
if(m_ToolTip->isVisible() && m_FuncTip)
return;
if(!sc->geometry().contains(sc->mapFromGlobal(QCursor::pos())))
return;
QWidget *widgetInCursor = QApplication::widgetAt(QCursor::pos());
if(widgetInCursor && sc != widgetInCursor && sc != widgetInCursor->parentWidget())
return;
sptr_t pos = sc->positionFromPointClose(x, y);
if(pos == -1)
return;
sptr_t line = sc->lineFromPosition(pos);
sptr_t col = pos - sc->positionFromLine(line);
QString tooltip = completionContext->tooltipForLoc(line + 1, col);
if(!tooltip.isEmpty())
{
hideFunccompleteTooltip();
m_ToolTip->configureTip(this, tooltip);
m_ToolTip->showTipAtPos(QCursor::pos() + QPoint(5, 5));
}
});
QObject::connect(sc, &ScintillaEdit::dwellEnd, [this, sc](int, int) {
if(sc->autoCActive())
return;
if(!m_FuncTip)
m_ToolTip->hideTip();
});
QObject::connect(sc, &ScintillaEdit::charAdded, [this, sc](int ch) { doAutocomplete(sc); });
QObject::connect(sc, &ScintillaEdit::buttonPressed,
[this](QMouseEvent *ev) { hideFunccompleteTooltip(); });
QObject::connect(sc, &ScintillaEdit::keyPressed, [this, sc](QKeyEvent *ev) {
if(ev->key() == Qt::Key_Space && (ev->modifiers() & Qt::ControlModifier))
doAutocomplete(sc);
if(m_ToolTip->isVisible() && m_FuncTip)
{
if(sc->lineFromPosition(sc->currentPos()) == m_FuncTipLine)
{
doFunccomplete(sc);
return;
}
hideFunccompleteTooltip();
}
if(ev->key() == Qt::Key_F1)
{
sptr_t pos = sc->currentPos();
if(pos >= 0)
{
sptr_t line = sc->lineFromPosition(pos);
sptr_t col = pos - sc->positionFromLine(line);
QString typeName = completionContext->typenameForLoc(line + 1, col);
if(!typeName.isEmpty())
selectedHelp(typeName);
}
}
m_FindReplace->handleEditorKeypress(sc, ev);
});
if(m_Editors.empty())
{
ui->docking->addToolWindow(editor, ToolWindowManager::EmptySpace);
}
else
{
ui->docking->addToolWindow(editor,
ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
ui->docking->areaOf(m_Editors[0])));
}
m_Editors.push_back(editor);
m_Scintillas.push_back(sc);
updateEditorCloseButton();
sc->setText(text.c_str());
sc->emptyUndoBuffer();
editor->markModified(false);
}
void PythonShell::updateEditorCloseButton()
{
for(EditorWrapper *edit : m_Editors)
{
ToolWindowManager::ToolWindowProperty props =
ToolWindowManager::DisallowUserDocking | ToolWindowManager::AlwaysDisplayFullTabs;
// disallow closing last scintilla
if(m_Editors.size() == 1)
props = props | ToolWindowManager::HideCloseButton;
ui->docking->setToolWindowProperties(edit, props);
}
}
void PythonShell::updateNonDebugWarning()
{
m_DebuggerAttached = PythonContext::IsDebuggerConnected();
for(EditorWrapper *edit : m_Editors)
{
if(edit->filename().isEmpty())
{
if(m_DebuggerAttached)
edit->setWarning(
tr("External debugger will not work for unsaved files. "
"Save script to disk to allow debugging."));
}
else
{
edit->setWarning(QString());
}
}
updateButtonStates();
}
void PythonShell::updateButtonStates()
{
enableButtons(ui->newScript->isEnabled());
}
void PythonShell::addRecentFile(rdcstr filename)
{
// don't add recent files from UI extensions
for(const ExtensionMetadata &ext : m_Ctx.Extensions().GetInstalledExtensions())
{
if(QString(QFileInfo(filename).absolutePath())
.toLower()
.startsWith(QDir(ext.filePath).absolutePath().toLower()))
{
return;
}
}
m_Ctx.Config().Python_RecentFiles.removeIf([filename](const rdcstr &o) { return o == filename; });
if(m_Ctx.Config().Python_RecentFiles.size() == 10)
m_Ctx.Config().Python_RecentFiles.pop_back();
m_Ctx.Config().Python_RecentFiles.insert(0, filename);
m_Ctx.Config().Save();
updateRecentFiles(true);
}
void PythonShell::updateRecentFiles(bool added)
{
ui->projectExplorer->beginUpdate();
bool expanded = ui->projectExplorer->isItemExpanded(m_RecentFiles);
// expand if we're adding the first recent file
if(m_Ctx.Config().Python_RecentFiles.size() == 1 && added)
expanded = true;
m_RecentFiles->clear();
QString unsavedFile = interactiveContext->GetTempFilename(lit("script.py"));
if(!unsavedFile.isEmpty() && QFile::exists(unsavedFile) && !m_IgnoreRecovered)
{
RDTreeWidgetItem *recent = new RDTreeWidgetItem({tr("Recovered Script")});
recent->setItalic(true);
recent->setData(0, Qt::UserRole, QString(unsavedFile));
m_RecentFiles->addChild(recent);
}
else
{
// we didn't have a recovered script, remember that and don't display the file in future
// refreshes appears in future due to temp script running. When the window is closed the script
// will be removed, and if we crash and restart then the script will be found
m_IgnoreRecovered = true;
}
for(rdcstr file : m_Ctx.Config().Python_RecentFiles)
{
RDTreeWidgetItem *recent = new RDTreeWidgetItem({QFileInfo(file).fileName()});
recent->setData(0, Qt::UserRole, QString(file));
m_RecentFiles->addChild(recent);
}
ui->projectExplorer->endUpdate();
if(expanded)
ui->projectExplorer->expandItem(m_RecentFiles);
else
ui->projectExplorer->collapseItem(m_RecentFiles);
}
bool PythonShell::eventFilter(QObject *watched, QEvent *event)
{
if(qobject_cast<ScintillaEdit *>(watched))
{
if(event->type() == QEvent::Leave)
{
if(!m_FuncTip)
{
m_ToolTip->hideTip();
}
else if(m_FuncTip)
{
if(!m_ToolTip->geometry().contains(QCursor::pos()))
{
hideFunccompleteTooltip();
}
}
}
else if(event->type() == QEvent::KeyPress && ((QKeyEvent *)event)->key() == Qt::Key_Escape)
{
hideFunccompleteTooltip();
}
}
if(m_InteractiveCompleter && watched == m_InteractiveCompleter->popup() &&
(event->type() == QEvent::Hide || event->type() == QEvent::FocusOut))
{
m_CurrentCompletionTip = -1;
updateCompletionTip();
}
if(m_FuncTip && watched == m_FuncTipWidget && event->type() == QEvent::FocusOut)
{
hideFunccompleteTooltip();
}
return QObject::eventFilter(watched, event);
}
QVariant PythonShell::persistData()
{
QVariantMap state = ui->docking->saveState();
RDTreeViewExpansionState expansion;
ui->projectExplorer->saveExpansion(expansion, 0);
QVariantList expansionList;
for(uint x : expansion)
expansionList.append(x);
state[lit("projectExplorerExpansion")] = expansionList;
return state;
}
void PythonShell::setPersistData(const QVariant &persistData)
{
QVariantMap state = persistData.toMap();
ui->docking->restoreState(state);
ui->projectExplorer->collapseAll();
RDTreeViewExpansionState expansion;
for(QVariant x : state[lit("projectExplorerExpansion")].toList())
expansion.insert(x.toUInt());
ui->projectExplorer->applyExpansion(expansion, 0);
if(ui->docking->areaOf(m_Editors[0]) == NULL)
ui->docking->addToolWindow(m_Editors[0], ToolWindowManager::AreaReference(
ToolWindowManager::RightOf,
ui->docking->areaOf(ui->projectExplorer), 0.8f));
setupTabs();
}
void PythonShell::setupTabs()
{
ToolWindowManagerArea *editorTabs = ui->docking->areaOf(m_Editors[0]);
QObject::connect(editorTabs, &QTabWidget::currentChanged, this, &PythonShell::editorTab_Changed);
editorTabs->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu);
QObject::connect(editorTabs->tabBar(), &QTabBar::customContextMenuRequested, this,
&PythonShell::editorTab_Menu);
}
PythonContext *PythonShell::GetScriptContext()
{
return scriptContext;
}
bool PythonShell::CheckUnsavedChanges()
{
return checkAllowClose();
}
bool PythonShell::LoadScriptFromFilename(rdcstr filename)
{
if(!filename.isEmpty())
{
QFile f(filename);
if(f.open(QIODevice::ReadOnly | QIODevice::Text))
{
makeEditor(filename, f.readAll().data());
ui->saveScript->setEnabled(true);
QString unsavedFile = interactiveContext->GetTempFilename(lit("script.py"));
if(QString(filename) != unsavedFile)
addRecentFile(filename);
m_Watcher->addPath(filename);
return true;
}
}
return false;
}
void PythonShell::CreateNewScriptEditor(rdcstr name, rdcstr text)
{
makeEditor(name, text);
ui->saveScript->setEnabled(false);
updateNonDebugWarning();
}
rdcstr PythonShell::GetScriptText()
{
EditorWrapper *editor = curEditor();
if(!editor)
return rdcstr();
ScintillaEdit *sc = editor->scintilla();
return sc->getText(sc->textLength() + 1).data();
}
void PythonShell::SetExtensionOutputFilter(const rdcstr &extensionName)
{
if(extensionName.empty())
return;
int idx = loadedExtensions.indexOf(extensionName);
if(idx < 0)
return;
ui->outputContext->setCurrentIndex(FirstExtensionOutputFilter + idx);
}
void PythonShell::SetScriptOutputFilter()
{
ui->outputContext->setCurrentIndex(ScriptOutputFilter);
}
void PythonShell::RemoveOutputFilter()
{
ui->outputContext->setCurrentIndex(AllOutputFilter);
}
void PythonShell::ShowOutput()
{
ToolWindowManager::raiseToolWindow(ui->outputGroup);
}
void PythonShell::ShowREPL()
{
ToolWindowManager::raiseToolWindow(ui->replGroup);
}
void PythonShell::ShowHelp()
{
ToolWindowManager::raiseToolWindow(ui->helpGroup);
}
void PythonShell::RunScript()
{
EditorWrapper *editor = curEditor();
if(!editor)
return;
if(editor->isModified() && !editor->filename().isEmpty())
saveEditor(editor, editor->filename());
PythonContext *context = newContext();
ANALYTIC_SET(UIFeatures.PythonInterop, true);
ShowOutput();
scriptOutputLines.removeIf([](const ScriptOutputLine &l) { return l.extension.isEmpty(); });
updateScriptOutput(true);
QString script = GetScriptText();
enableButtons(false);
// save any changes
if(editor->isModified() && !editor->filename().isEmpty())
{
saveEditor(editor, editor->filename());
}
else if(editor->filename().isEmpty())
{
// write the script as a temporary file, so if we crash it will be recoverable.
// this only applies if editor is an unsaved file
QString unsavedFile = context->GetTempFilename(lit("script.py"));
QFile f(unsavedFile);
f.open(QFile::Truncate | QFile::WriteOnly);
f.write(script.toUtf8().data());
f.close();
}
m_CurLineTimer->start();
LambdaThread *thread = new LambdaThread([this, script, context, editor]() {
PythonContext::AddDebuggableThread();
scriptContext = context;
runningScriptEditor = editor->scintilla();
context->executeString(editor->filename(), script);
scriptContext = NULL;
GUIInvoke::call(this, [this, context]() {
m_CurLineTimer->stop();
context->Finish();
runningScriptEditor = NULL;
enableButtons(true);
QString unsavedFile = context->GetTempFilename(lit("script.py"));
QFile::remove(unsavedFile);
});
PythonContext::RemoveDebuggableThread();
});
thread->setName(lit("Python script"));
thread->selfDelete(true);
thread->start();
}
void PythonShell::AttachDebugger(const rdcstr &contextLocation)
{
QString path = contextLocation;
for(const ExtensionMetadata &e : m_Ctx.Extensions().GetInstalledExtensions())
{
if(e.package == contextLocation)
path = e.filePath;
}
if(!QDir(path).exists())
return;
PythonContext::LaunchDebugger(this, m_Ctx.Config(), path);
updateButtonStates();
updateNonDebugWarning();
}
void PythonShell::on_findReplace_clicked()
{
m_FindReplace->raiseOrShow();
}
void PythonShell::on_execute_clicked()
{
QString command = ui->lineInput->text();
ANALYTIC_SET(UIFeatures.PythonInterop, true);
appendText(ui->interactiveOutput, command + lit("\n"));
if(!command.trimmed().isEmpty())
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);
interactiveContext->reflectSource(QString());
}
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();
interactiveContext->reflectSource(QString());
}
void PythonShell::on_newScript_clicked()
{
QString minidocHeader = scriptHeader();
minidocHeader.replace(QLatin1Char('\n'), lit("\n# "));
minidocHeader = QFormatStr("# %1\n\n").arg(minidocHeader);
CreateNewScriptEditor("", minidocHeader);
}
void PythonShell::on_openScript_clicked()
{
QString filename = RDDialog::getOpenFileName(this, tr("Open Python Script"), QString(),
tr("Python scripts (*.py)"));
if(!LoadScriptFromFilename(filename))
{
RDDialog::critical(this, tr("Error loading script"), tr("Couldn't open path %1.").arg(filename));
}
}
void PythonShell::on_saveScript_clicked()
{
EditorWrapper *editor = curEditor();
if(!editor)
return;
QString filename = editor->filename();
if(!QFileInfo(filename).isAbsolute())
return on_saveAsScript_clicked();
if(saveEditor(editor, filename))
editor->markModified(false);
}
void PythonShell::on_saveAsScript_clicked()
{
EditorWrapper *editor = curEditor();
if(!editor)
return;
if(saveEditorAs(editor))
editor->markModified(false);
}
void PythonShell::on_runScript_clicked()
{
RunScript();
}
void PythonShell::on_debugAttach_clicked()
{
EditorWrapper *editor = curEditor();
if(!editor)
return;
QString filename = editor->filename();
if(editor->isUIExtension())
{
filename = QFileInfo(filename).absolutePath();
for(const ExtensionMetadata &e : m_Ctx.Extensions().GetInstalledExtensions())
{
if(filename.startsWith(QDir(e.filePath).absolutePath()))
{
AttachDebugger(e.package);
return;
}
}
}
else if(!filename.isEmpty())
{
AttachDebugger(QFileInfo(filename).absoluteDir().absolutePath());
}
}
void PythonShell::on_abortRun_clicked()
{
if(scriptContext)
scriptContext->abort();
}
void PythonShell::traceLine(const QString &file, int line)
{
if(!runningScriptEditor)
return;
// we only update the current line on a fixed timer to avoid DoS'ing ourselves with too many rapid
// updates. Both happen on the UI thread so we just need a simple flag
m_CurLine = line;
m_CurLineDirty = true;
}
void PythonShell::exception(const QString &extension, const QString &type, const QString &value,
int finalLine, QList<QString> frames)
{
if(finalLine >= 0)
traceLine(QString(), finalLine);
QString exString;
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);
if(QObject::sender() == (QObject *)interactiveContext)
{
appendText(ui->interactiveOutput, exString);
return;
}
exString.insert(0, QLatin1Char('\n'));
scriptOutputLines.push_back({extension, exString});
updateScriptOutput(false);
}
void PythonShell::textOutput(const QString &extension, bool isStdError, const QString &output)
{
if(QObject::sender() == (QObject *)interactiveContext)
{
appendText(ui->interactiveOutput, output);
return;
}
scriptOutputLines.push_back({extension, output});
updateScriptOutput(false);
}
void PythonShell::on_outputContext_currentIndexChanged(int idx)
{
updateScriptOutput(true);
}
void PythonShell::on_projectExplorer_itemActivated(RDTreeWidgetItem *item, int column)
{
// ignore these, just let them collapse/expand
if(item == m_Examples || item == m_UIExtensions || item == m_RecentFiles)
return;
if(item == m_NewExtension)
{
createExtension_clicked();
}
else if(item->parent() == m_Examples)
{
QString title = tr("Example: ") + item->text(0);
QString text = item->data(0, Qt::UserRole).toString();
for(EditorWrapper *edit : m_Editors)
{
if(edit->filename() == title || edit->title() == title)
{
ToolWindowManager::raiseToolWindow(edit);
return;
}
}
CreateNewScriptEditor("", text);
m_Editors.back()->setTitle(title);
}
else
{
// recent file or UI extension, the user role contains the path
QString filename = item->data(0, Qt::UserRole).toString();
// directories in UI extensions have no filename to activate
if(filename.isEmpty())
return;
bool isExt = false;
RDTreeWidgetItem *parent = item;
while(parent)
{
if(parent == m_UIExtensions)
isExt = true;
parent = parent->parent();
}
for(EditorWrapper *edit : m_Editors)
{
if(edit->filename() == filename)
{
ToolWindowManager::raiseToolWindow(edit);
return;
}
}
if(!QFileInfo(filename).exists())
{
QMessageBox::StandardButton response = RDDialog::question(
this, tr("'%1' does not exist").arg(QFileInfo(filename).fileName()),
tr("File not found at path:\n%1\n\nRemove from recent files list?").arg(filename));
if(response == QMessageBox::Yes)
{
rdcstr f = filename;
m_Ctx.Config().Python_RecentFiles.removeIf([f](const rdcstr &o) { return o == f; });
m_Ctx.Config().Save();
GUIInvoke::defer(this, [this]() { updateRecentFiles(false); });
}
return;
}
LoadScriptFromFilename(filename);
if(isExt)
m_Editors.back()->setUIExtension(true);
editorTab_Changed(-1);
QString unsavedFile = interactiveContext->GetTempFilename(lit("script.py"));
if(filename == unsavedFile)
{
QFile::remove(unsavedFile);
updateRecentFiles(false);
}
}
}
bool PythonShell::checkAllowClose()
{
for(EditorWrapper *edit : m_Editors)
{
if(!edit->checkAllowClose())
return false;
}
return true;
}
void PythonShell::updateScriptOutput(bool fullRefresh)
{
if(fullRefresh)
{
ui->scriptOutput->clear();
lastDisplayedLine = 0;
}
for(size_t i = lastDisplayedLine; i < scriptOutputLines.size(); i++)
{
bool display = false;
if(ui->outputContext->currentIndex() == AllOutputFilter)
display = true;
// Script
else if(ui->outputContext->currentIndex() == ScriptOutputFilter)
display = scriptOutputLines[i].extension.isEmpty();
else if(loadedExtensions.indexOf(scriptOutputLines[i].extension) + FirstExtensionOutputFilter ==
ui->outputContext->currentIndex())
display = true;
if(display)
{
appendText(ui->scriptOutput, scriptOutputLines[i].text);
}
}
lastDisplayedLine = scriptOutputLines.size();
}
bool PythonShell::saveEditorAs(EditorWrapper *editor)
{
QString filename = RDDialog::getSaveFileName(this, tr("Save Python Script"), QString(),
tr("Python scripts (*.py)"));
if(filename.isEmpty())
return false;
return saveEditor(editor, filename);
}
bool PythonShell::saveEditor(EditorWrapper *editor, QString filename)
{
QString oldFilename = editor->filename();
if(!filename.isEmpty())
{
QDir dirinfo = QFileInfo(filename).dir();
if(dirinfo.exists())
{
QFile f(filename);
if(f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
{
// remove the path from watching first so we don't get a notification from the write itself
m_Watcher->removePath(oldFilename);
ScintillaEdit *sc = editor->scintilla();
QString text = QString::fromUtf8(sc->getText(sc->textLength() + 1));
text.remove(QLatin1Char('\r'));
f.write(text.toUtf8());
addRecentFile(filename);
// delay a short while before starting to watch this file. This is highly unlikely to miss
// any real writes (which would have to happen externally after we save), but prevents us
// from identifying our own writes as an external modification.
QTimer::singleShot(200, [this, filename]() { m_Watcher->addPath(filename); });
editor->setFilename(filename);
updateNonDebugWarning();
return true;
}
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:\n%1").arg(filename));
}
}
return false;
}
void PythonShell::removeEditor(EditorWrapper *editor)
{
hideFunccompleteTooltip();
m_Watcher->removePath(editor->filename());
m_Editors.removeOne(editor);
m_Scintillas.removeOne(editor->scintilla());
updateEditorCloseButton();
}
void PythonShell::extensionLoaded(const QString &extension)
{
ui->outputContext->addItem(tr("Extension %1").arg(extension));
loadedExtensions.push_back(extension);
}
void PythonShell::editor_contextMenu(const QPoint &pos)
{
ScintillaEdit *editor = qobject_cast<ScintillaEdit *>(QObject::sender());
if(!editor)
return;
hideFunccompleteTooltip();
m_ContextMenuVisible = true;
QMenu contextMenu(this);
QString typeName;
sptr_t scintillaPos = editor->positionFromPoint(pos.x(), pos.y());
if(scintillaPos >= 0)
{
sptr_t line = editor->lineFromPosition(scintillaPos);
sptr_t col = scintillaPos - editor->positionFromLine(line);
typeName = completionContext->typenameForLoc(line + 1, col);
}
bool valid = !typeName.isEmpty();
QAction help(valid ? tr("Help for '%1'").arg(typeName) : tr("Help"), this);
QObject::connect(&help, &QAction::triggered, [this, typeName] { selectedHelp(typeName); });
help.setEnabled(valid);
contextMenu.addAction(&help);
contextMenu.addSeparator();
QAction undo(tr("Undo"), this);
QAction redo(tr("Redo"), this);
QObject::connect(&undo, &QAction::triggered, [editor] { editor->undo(); });
QObject::connect(&redo, &QAction::triggered, [editor] { editor->redo(); });
undo.setEnabled(editor->canUndo());
redo.setEnabled(editor->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, [editor] { editor->cut(); });
QObject::connect(&copyText, &QAction::triggered, [editor] {
editor->copyRange(editor->selectionStart(), editor->selectionEnd());
});
QObject::connect(&pasteText, &QAction::triggered, [editor] { editor->paste(); });
QObject::connect(&deleteText, &QAction::triggered, [editor] {
editor->deleteRange(editor->selectionStart(), editor->selectionEnd());
});
contextMenu.addAction(&cutText);
contextMenu.addAction(&copyText);
contextMenu.addAction(&pasteText);
contextMenu.addAction(&deleteText);
contextMenu.addSeparator();
if(editor->selectionEmpty())
{
cutText.setEnabled(false);
copyText.setEnabled(false);
deleteText.setEnabled(false);
}
pasteText.setEnabled(editor->canPaste());
QAction selectAll(tr("Select All"), this);
QObject::connect(&selectAll, &QAction::triggered, [editor] { editor->selectAll(); });
contextMenu.addAction(&selectAll);
RDDialog::show(&contextMenu, editor->viewport()->mapToGlobal(pos));
m_ContextMenuVisible = false;
}
void PythonShell::projectExplorer_contextMenu(const QPoint &pos)
{
m_ContextMenuVisible = true;
RDTreeWidgetItem *item = ui->projectExplorer->itemAt(pos);
QMenu contextMenu(this);
QAction expandAll(tr("&Expand All"), this);
expandAll.setIcon(Icons::arrow_out());
QAction collapseAll(tr("&Collapse All"), this);
collapseAll.setIcon(Icons::arrow_in());
expandAll.setEnabled(item && item->childCount() > 0);
collapseAll.setEnabled(expandAll.isEnabled());
QAction reloadExtension(tr("&Reload Extension"), this);
reloadExtension.setIcon(Icons::update());
QAction explorerOpen(tr("&Open in File Explorer"), this);
explorerOpen.setIcon(Icons::folder());
QAction openEditor(tr("&Edit file"), this);
openEditor.setIcon(Icons::page_white_edit());
QAction viewOutput(tr("&View output"), this);
viewOutput.setIcon(Icons::filter());
QAction createExtension(tr("Create &New Extension"), this);
createExtension.setIcon(Icons::plugin_add());
QObject::connect(&expandAll, &QAction::triggered,
[this, item]() { ui->projectExplorer->expandAllItems(item); });
QObject::connect(&collapseAll, &QAction::triggered,
[this, item]() { ui->projectExplorer->collapseAllItems(item); });
contextMenu.addAction(&expandAll);
contextMenu.addAction(&collapseAll);
if(item && !(item == m_UIExtensions || item == m_Examples || item == m_RecentFiles))
{
QString diskLocation = item->data(0, Qt::UserRole).toString();
contextMenu.addSeparator();
// if this is the root node of a UI extension, add options to filter output/reload
if(item->parent() == m_UIExtensions)
{
rdcstr itemPath = item->data(0, Qt::UserRole + 1).toString();
contextMenu.insertAction(contextMenu.actions()[0], &reloadExtension);
contextMenu.insertSeparator(contextMenu.actions()[1]);
contextMenu.addAction(&explorerOpen);
contextMenu.addAction(&viewOutput);
rdcarray<ExtensionMetadata> exts = m_Ctx.Extensions().GetInstalledExtensions();
for(const ExtensionMetadata &m : exts)
{
if(m.package == rdcstr(itemPath))
{
reloadExtension.setEnabled(m.hasChanges);
diskLocation = QFileInfo(m.filePath).absoluteFilePath();
break;
}
}
explorerOpen.setEnabled(!diskLocation.isEmpty());
viewOutput.setEnabled(m_Ctx.Extensions().IsExtensionLoaded(itemPath));
QObject::connect(&reloadExtension, &QAction::triggered,
[this, itemPath]() { m_Ctx.Extensions().LoadExtension(itemPath); });
if(!m_Ctx.Config().AlwaysLoad_Extensions.contains(itemPath))
{
reloadExtension.setEnabled(true);
reloadExtension.setText(tr("Enable extension"));
reloadExtension.setIcon(Icons::add());
QObject::connect(&reloadExtension, &QAction::triggered, [this, itemPath]() {
m_Ctx.Config().AlwaysLoad_Extensions.push_back(itemPath);
m_Ctx.Config().Save();
});
}
QObject::connect(&explorerOpen, &QAction::triggered,
[diskLocation]() { QDesktopServices::openUrl(diskLocation); });
QObject::connect(&viewOutput, &QAction::triggered, [this, itemPath]() {
ShowOutput();
SetExtensionOutputFilter(itemPath);
});
}
else if(item->parent() != m_Examples && !diskLocation.isEmpty())
{
// real files - either recent or in UI extensions - have options to open as editors or in
// explorer, but we ditch the collapse/expand
contextMenu.clear();
// get the containing directory, not the file
diskLocation = QFileInfo(diskLocation).absoluteDir().absolutePath();
contextMenu.addAction(&openEditor);
contextMenu.addAction(&explorerOpen);
QObject::connect(&openEditor, &QAction::triggered,
[this, item]() { on_projectExplorer_itemActivated(item, 0); });
QObject::connect(&explorerOpen, &QAction::triggered,
[diskLocation]() { QDesktopServices::openUrl(diskLocation); });
}
}
else if(item == m_UIExtensions)
{
contextMenu.addSeparator();
contextMenu.addAction(&createExtension);
QObject::connect(&createExtension, &QAction::triggered, [this]() { createExtension_clicked(); });
}
RDDialog::show(&contextMenu, ui->projectExplorer->viewport()->mapToGlobal(pos));
m_ContextMenuVisible = false;
}
void PythonShell::createExtension_clicked()
{
QDialog dialog;
RDLabel label;
RDLineEdit extensionName;
QDialogButtonBox buttons;
dialog.setWindowTitle(tr("Create new UI extension"));
dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
label.setText(
tr("Create a new UI extension, with some example code.\n"
"\n"
"This will create the directory structure for the specified package name, with a default\n"
"extension metadata json and some simple example code to give you a starting point."));
extensionName.setPlaceholderText(tr("myname.example"));
buttons.setOrientation(Qt::Horizontal);
buttons.setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttons.setCenterButtons(true);
QObject::connect(&buttons, &QDialogButtonBox::accepted, [this, &dialog, &extensionName]() {
QString extName = extensionName.text().trimmed();
if(extName.isEmpty())
{
RDDialog::critical(&dialog, tr("Invalid extension name"),
tr("Must specify a name for the new extension."));
return;
}
if(extName.startsWith(lit("renderdoc.")))
{
RDDialog::critical(&dialog, tr("Invalid extension name"),
tr("Extension name conflicts with builtin module 'renderdoc'."));
return;
}
if(extName.contains(QLatin1Char(' ')) || extName.contains(QLatin1Char('\t')))
{
RDDialog::critical(
&dialog, tr("Invalid extension name"),
tr("Extension names should be valid python package names, note including whitespace."));
return;
}
for(const ExtensionMetadata &e : m_Ctx.Extensions().GetInstalledExtensions())
{
if(QString(e.package) == extName)
{
RDDialog::critical(&dialog, tr("Extension name in use"),
tr("The extension name '%1' already exists.").arg(e.package));
return;
}
}
QStringList locations = PythonContext::GetApplicationExtensionsPaths();
if(!locations.empty())
{
QDir dir(locations[0]);
QStringList paths = extName.split(QLatin1Char('.'));
bool nonexist = false;
while(!paths.empty())
{
QString dirname = paths[0];
paths.pop_front();
if(!dir.cd(dirname))
{
nonexist = true;
break;
}
qInfo() << dir.absolutePath();
}
if(!nonexist && dir.exists() && !dir.isEmpty())
{
RDDialog::critical(&dialog, tr("Directory already exists"),
tr("Extension directory already exists:\n%1").arg(dir.absolutePath()));
return;
}
}
dialog.accept();
});
QObject::connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
layout->addWidget(&label);
layout->addWidget(&extensionName);
layout->addWidget(&buttons);
if(!RDDialog::show(&dialog))
return;
if(dialog.result() == QDialog::Accepted)
{
QStringList locations = PythonContext::GetApplicationExtensionsPaths();
QDir dir(locations[0]);
QString extName = extensionName.text().trimmed();
QStringList paths = extName.split(QLatin1Char('.'));
while(!paths.empty())
{
QString dirname = paths[0];
paths.pop_front();
dir.mkdir(dirname);
if(!dir.cd(dirname))
{
RDDialog::critical(&dialog, tr("Couldn't create directory"),
tr("Failed to create %1 in %2").arg(dirname).arg(dir.absolutePath()));
return;
}
}
paths = extName.split(QLatin1Char('.'));
QString metadata = lit(R"({
"extension_api": 1,
"name": "%3",
"version": "1.0",
"minimum_renderdoc": "%1.%2",
"description": "Template extension %4",
"author": "My Name <my.email@example.com>",
"url": "https://github.com/example/example"
}
)")
.arg(RENDERDOC_VERSION_MAJOR)
.arg(RENDERDOC_VERSION_MINOR)
.arg(paths.back())
.arg(extName);
{
QFile ext(dir.absoluteFilePath(lit("extension.json")));
if(ext.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
{
ext.write(metadata.toUtf8());
}
QFile init(dir.absoluteFilePath(lit("__init__.py")));
if(init.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
{
init.write(R"(
# Blank RenderDoc UI extension
import renderdoc as rd
import qrenderdoc as qrd
def register(version: str, pyrenderdoc: qrd.CaptureContext):
print(f"New UI extension loaded in RenderDoc {version}")
def unregister():
print(f"New UI extension being unloaded")
)");
}
}
updateExtensionProjects();
LoadScriptFromFilename(dir.absoluteFilePath(lit("__init__.py")));
m_Editors.back()->setUIExtension(true);
editorTab_Changed(-1);
}
}
void PythonShell::selectedHelp(QString word)
{
ui->helpSearch->setText(word);
refreshCurrentHelp();
}
void PythonShell::refreshCurrentHelp()
{
ToolWindowManager::raiseToolWindow(ui->helpGroup);
ui->helpText->clear();
m_HelpPrinting = true;
completionContext->executeString(lit(R"(
try:
import keyword
if keyword.iskeyword("%1"):
help("%1")
else:
help(%1)
except ImportError:
help(%1)
)")
.arg(ui->helpSearch->text()));
ui->helpText->verticalScrollBar()->setValue(0);
m_HelpPrinting = false;
}
void PythonShell::interactive_keypress(QKeyEvent *event)
{
bool triggerCompletion = false;
if(m_InteractiveCompleter->popup()->isVisible())
{
switch(event->key())
{
// manually trigger a completion with tab
case Qt::Key_Tab:
m_InteractiveCompleter->activated(
m_InteractiveCompleter->popup()->selectionModel()->currentIndex());
m_InteractiveCompleter->popup()->hide();
return;
// if a completion is in progress ignore any events the completer will process
case Qt::Key_Return:
case Qt::Key_Enter: return;
// allow key scrolling
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown: break;
// all other keys close the popup
default: triggerCompletion = true;
}
}
else
{
if(event->text() != QString() && event->text()[0].isPrint() && event->key() != Qt::Key_Return &&
event->key() != Qt::Key_Enter)
triggerCompletion = true;
if(event->key() == Qt::Key_Escape && m_FuncTip && m_ToolTip->isVisible())
hideFunccompleteTooltip();
}
if(triggerCompletion)
{
QString base = ui->lineInput->text();
QStringList completions;
int oldCount = m_CompletionTipList.count();
m_CompletionTipList.clear();
if(base.trimmed() != QString())
{
m_CompletionTipList =
interactiveContext->completionOptions(0, base, m_InteractiveCompletionPrefix);
for(const QPair<QString, QString> &item : m_CompletionTipList)
completions << item.first;
}
if(oldCount != m_CompletionTipList.count())
m_CurrentCompletionTip = -1;
if(completions.isEmpty())
{
if(event->key() == Qt::Key_Tab)
ui->lineInput->insert(lit("\t"));
m_InteractiveCompleter->popup()->hide();
QString prompt = interactiveContext->tryFunctionCompletion(0, base);
if(!prompt.isEmpty())
{
m_ToolTip->configureTip(this, prompt);
QPoint p = ui->lineInput->fontMetrics().boundingRect(base).bottomRight();
p.setY(ui->lineInput->geometry().height());
p = ui->lineInput->mapToGlobal(p);
if(!m_ToolTip->isVisible())
m_ToolTip->showTipAtPos(p);
m_FuncTip = true;
m_FuncTipWidget = ui->lineInput;
}
else
{
hideFunccompleteTooltip();
}
return;
}
hideFunccompleteTooltip();
m_InteractiveCompletionModel->setStringList(completions);
QRect r = ui->lineInput->rect();
QFontMetrics fm = ui->lineInput->fontMetrics();
#if(QT_VERSION < QT_VERSION_CHECK(5, 11, 0))
#define horizontalAdvance width
#endif
int longestWidth = 0;
for(QString &c : completions)
{
longestWidth = qMax(longestWidth, fm.horizontalAdvance(c));
}
base.resize(base.size() - m_InteractiveCompletionPrefix);
r.setLeft(r.left() + fm.horizontalAdvance(base));
r.setWidth(longestWidth + ui->lineInput->style()->pixelMetric(QStyle::PM_ScrollBarExtent) +
ui->lineInput->style()->pixelMetric(QStyle::PM_ButtonMargin));
m_InteractiveCompleter->complete(r);
m_InteractiveCompleter->popup()->selectionModel()->setCurrentIndex(
m_InteractiveCompletionModel->index(0), QItemSelectionModel::ClearAndSelect);
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);
ui->debugAttach->setEnabled(enable && !m_DebuggerAttached && PythonContext::IsDebuggingEnabled());
ui->debugAttach->setToolTip(QString());
EditorWrapper *editor = curEditor();
ui->runScript->setToolTip(QString());
if(enable && m_DebuggerAttached)
{
ui->debugAttach->setToolTip(tr("Debugger is already attached"));
}
else if(enable && !PythonContext::IsDebuggingEnabled())
{
ui->debugAttach->setToolTip(
tr("Debugging not supported - check documentation for setup instructions"));
}
if(editor)
{
if(editor->isUIExtension())
{
ui->runScript->setEnabled(false);
ui->runScript->setToolTip(tr("UI Extension files can't be run"));
}
}
if(editor == NULL || editor->filename().isEmpty())
{
ui->debugAttach->setEnabled(false);
ui->debugAttach->setToolTip(tr("Debugger requires a script saved to disk"));
}
}
void PythonShell::doAutocomplete(ScintillaEdit *editor)
{
sptr_t pos = editor->currentPos();
sptr_t line = editor->lineFromPosition(pos);
sptr_t lineStart = editor->positionFromLine(line);
QByteArray lineText = editor->getLine(line);
lineText.resize(pos - lineStart);
int oldCount = m_CompletionTipList.count();
int prefix_len = 0;
m_CompletionTipList =
completionContext->completionOptions(line, QString::fromUtf8(lineText), prefix_len);
if(m_CompletionTipList.empty())
{
doFunccomplete(editor);
return;
}
QString completion_merged;
for(const QPair<QString, QString> &item : m_CompletionTipList)
{
completion_merged += item.first;
completion_merged += QLatin1Char(' ');
}
completion_merged.remove(completion_merged.count() - 1, 1);
if(oldCount != m_CompletionTipList.count())
m_CurrentCompletionTip = -1;
hideFunccompleteTooltip();
editor->autoCShow(prefix_len, completion_merged.toUtf8().data());
// scintilla doesn't give us a callback/event when an item is highlighted, so we query in a timer
m_CompletionTipTimer->start();
}
void PythonShell::doFunccomplete(ScintillaEdit *editor)
{
sptr_t pos = editor->currentPos();
sptr_t line = editor->lineFromPosition(pos);
sptr_t lineStart = editor->positionFromLine(line);
QByteArray lineText = editor->getLine(line);
lineText.resize(pos - lineStart);
QString prompt = completionContext->tryFunctionCompletion(line, QString::fromUtf8(lineText));
if(!prompt.isEmpty())
{
m_ToolTip->configureTip(this, prompt);
sptr_t tooltipPos = editor->positionFromLine(line + 1);
QPoint p(editor->pointXFromPosition(tooltipPos),
editor->pointYFromPosition(lineStart + lineText.size()) + editor->textHeight(line));
p = editor->mapToGlobal(p);
if(!m_ToolTip->isVisible())
m_ToolTip->showTipAtPos(p);
m_FuncTip = true;
m_FuncTipWidget = editor;
m_FuncTipLine = line;
}
else
{
hideFunccompleteTooltip();
}
}
void PythonShell::hideFunccompleteTooltip()
{
m_ToolTip->hideTip();
m_FuncTip = false;
m_FuncTipWidget = NULL;
// start the syntax check timer in case this naturally disappeared
m_SyntaxCheckTimer->start();
}
void PythonShell::updateCompletionTip()
{
if(m_CompletionTipList.empty() || m_CurrentCompletionTip < 0 ||
m_CurrentCompletionTip >= m_CompletionTipList.count())
{
m_CurrentCompletionTip = -1;
m_CompletionTip->hideTip();
return;
}
m_CompletionTip->configureTip(this, m_CompletionTipList[m_CurrentCompletionTip].second);
{
QPoint pos;
if(m_InteractiveCompleter->popup()->isVisible())
{
pos = m_InteractiveCompleter->popup()->mapToGlobal(
m_InteractiveCompleter->popup()->rect().topRight());
}
else
{
EditorWrapper *editor = curEditor();
if(editor)
{
pos = QPoint(editor->scintilla()->autoCRectRight(), editor->scintilla()->autoCRectTop());
}
}
if(pos != QPoint())
m_CompletionTip->showTipAtPos(pos);
}
}
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", m_ThreadCtx);
}