Expose UI extension management via context menu in project explorer

* Allow reloading and viewing output from context. Also add items for viewing in
  file explorer.
This commit is contained in:
baldurk
2026-08-13 21:05:13 +01:00
parent 14e636d1a5
commit 650188f32c
3 changed files with 175 additions and 8 deletions
+20 -2
View File
@@ -10,9 +10,27 @@ Overview
The python shell
You can open the python scripting panel from the window menu. It offers both an interactive ``REPL`` and a window that can open and run scripts and display the output. The editor has limited auto-complete and error checking, though for complex scripts editing in an external IDE is recommended.
You can open the python scripting panel from the window menu. It offers both an interactive ``REPL`` and a window that can open, edit, and run scripts & UI extensions. The editor has limited auto-complete and error checking, though for complex scripts editing in an external IDE is recommended.
A number of included example scripts are included for running to demonstrate use of the python APIs that RenderDoc exposes.
Examples
--------
A number of included example scripts are included for running to demonstrate use of the python APIs that RenderDoc exposes. Opening any of these example scripts will create a new temporary tab with the contents of the example script, but they will not be saved to disk.
UI extensions
-------------
UI extensions that are enumerated by RenderDoc will be listed with all python and markdown files as well as :file:`extension.json`, for editing within RenderDoc.
Right clicking on a UI extension can offer additional management options like enabling or reloading an extension, or revealing it in the file explorer.
Output
------
The output from python scripting is visible in this panel, and can either be shown from all possible sources or filtered to a particular source - e.g. only the output from the running script, or a particular UI extension.
Python documentation
--------------------
The full :doc:`python API <../python_api/index>` documentation contains a guide on how to get started with writing python scripts and UI extensions.
+152 -6
View File
@@ -25,6 +25,7 @@
#include "PythonShell.h"
#include <QAbstractItemView>
#include <QCompleter>
#include <QDesktopServices>
#include <QFileSystemWatcher>
#include <QFontDatabase>
#include <QKeyEvent>
@@ -326,9 +327,39 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
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;
}
});
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());
@@ -364,11 +395,13 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
}
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());
@@ -606,7 +639,7 @@ void PythonShell::editorTab_Menu(const QPoint &pos)
void PythonShell::updateExtensionProjects()
{
RDTreeViewExpansionState expansion;
ui->projectExplorer->saveExpansion(expansion, 0);
ui->projectExplorer->saveExpansion(expansion, 0, Qt::UserRole + 1);
ui->projectExplorer->beginUpdate();
@@ -614,7 +647,16 @@ void PythonShell::updateExtensionProjects()
for(const ExtensionMetadata &ext : m_Ctx.Extensions().GetInstalledExtensions())
{
RDTreeWidgetItem *root = new RDTreeWidgetItem({ext.name});
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));
@@ -623,7 +665,7 @@ void PythonShell::updateExtensionProjects()
ui->projectExplorer->endUpdate();
ui->projectExplorer->applyExpansion(expansion, 0);
ui->projectExplorer->applyExpansion(expansion, 0, Qt::UserRole + 1);
}
void PythonShell::addExtensionDirItems(RDTreeWidgetItem *root, QDir dir)
@@ -640,6 +682,7 @@ void PythonShell::addExtensionDirItems(RDTreeWidgetItem *root, QDir dir)
continue;
RDTreeWidgetItem *item = new RDTreeWidgetItem({child});
item->setData(0, Qt::UserRole + 1, child);
if(fileInfo.isDir())
{
// ignore some directories
@@ -1654,6 +1697,110 @@ void PythonShell::editor_contextMenu(const QPoint &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());
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)
{
QString 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); });
QObject::connect(&explorerOpen, &QAction::triggered,
[this, 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,
[this, diskLocation]() { QDesktopServices::openUrl(diskLocation); });
}
}
RDDialog::show(&contextMenu, ui->projectExplorer->viewport()->mapToGlobal(pos));
m_ContextMenuVisible = false;
}
void PythonShell::selectedHelp(QString word)
{
ui->helpSearch->setText(word);
@@ -1707,9 +1854,8 @@ void PythonShell::interactive_keypress(QKeyEvent *event)
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:
break;
// all other keys close the popup
case Qt::Key_PageDown: break;
// all other keys close the popup
default: triggerCompletion = true;
}
}
+3
View File
@@ -152,6 +152,7 @@ private slots:
void textOutput(const QString &extension, bool isStdError, const QString &output);
void extensionLoaded(const QString &extension);
void editor_contextMenu(const QPoint &pos);
void projectExplorer_contextMenu(const QPoint &pos);
void editorTab_Changed(int index);
void doSyntaxCheck();
@@ -179,6 +180,8 @@ private:
QFileSystemWatcher *m_Watcher = NULL;
QList<QString> m_ModifiedExtensions;
QTimer *m_SyntaxCheckTimer;
QCompleter *m_InteractiveCompleter;