From 650188f32c78e25d33e4d0b3cd8b91896fc1c889 Mon Sep 17 00:00:00 2001 From: baldurk Date: Wed, 5 Aug 2026 14:04:25 +0100 Subject: [PATCH] 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. --- docs/window/python_scripting.rst | 22 +++- qrenderdoc/Windows/PythonShell.cpp | 158 +++++++++++++++++++++++++++-- qrenderdoc/Windows/PythonShell.h | 3 + 3 files changed, 175 insertions(+), 8 deletions(-) diff --git a/docs/window/python_scripting.rst b/docs/window/python_scripting.rst index 45ee592dd..8730d725b 100644 --- a/docs/window/python_scripting.rst +++ b/docs/window/python_scripting.rst @@ -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. diff --git a/qrenderdoc/Windows/PythonShell.cpp b/qrenderdoc/Windows/PythonShell.cpp index 967f85ab8..1de804155 100644 --- a/qrenderdoc/Windows/PythonShell.cpp +++ b/qrenderdoc/Windows/PythonShell.cpp @@ -25,6 +25,7 @@ #include "PythonShell.h" #include #include +#include #include #include #include @@ -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 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 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; } } diff --git a/qrenderdoc/Windows/PythonShell.h b/qrenderdoc/Windows/PythonShell.h index 349a1ae94..f79f3cdad 100644 --- a/qrenderdoc/Windows/PythonShell.h +++ b/qrenderdoc/Windows/PythonShell.h @@ -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 m_ModifiedExtensions; + QTimer *m_SyntaxCheckTimer; QCompleter *m_InteractiveCompleter;