diff --git a/docs/how/how_python_extension.rst b/docs/how/how_python_extension.rst index e6ebbf9e8..bbaf62ad0 100644 --- a/docs/how/how_python_extension.rst +++ b/docs/how/how_python_extension.rst @@ -8,6 +8,9 @@ Example extensions can be found at the `community contributed repository ` window. + Extensions are simply python modules located in the user's RenderDoc config folder, with a json manifest. The config folder varies by platform, on Windows it's ``%APPDATA%\qrenderdoc\extensions`` and on linux it's ``~/.local/share/qrenderdoc/extensions``. Each extension is a python module subfolder under this root. You can nest subfolders, e.g. ``extensions/foo/bar/first`` would be the extension ``foo.bar.first``, and treated independently from ``extensions/foo/bar/second``. Next to each python module's ``__init__.py`` you should create a file ``extension.json`` following this template: diff --git a/docs/python_api/ui_extensions.rst b/docs/python_api/ui_extensions.rst index cab4460ff..0bc4d3904 100644 --- a/docs/python_api/ui_extensions.rst +++ b/docs/python_api/ui_extensions.rst @@ -14,7 +14,9 @@ In any subdirectory under this path you can register an extension by creating a Extension Manager: Configures installed extensions. -To streamline setup we will ask RenderDoc to create a new extension for us. Open the extension manager by opening the :guilabel:`Tools` menu and select :guilabel:`Manage Extensions`, then click the :guilabel:`Create New...` button and enter a package name such as ``tutorialext``. This will create the ``extension.json`` and ``__init__.py`` files in a new folder ``tutorialext`` for us. +To streamline setup we will ask RenderDoc to create a new extension for us. Open the python scripting window from :guilabel:`Window` → :guilabel:`Python Scripting`. Then either double click the :guilabel:`Create New...` item under the :guilabel:`UI Extensions` section, or right click on the section title and select the option from the context menu. + +From the dialog that appears enter a package name such as ``tutorialext``. This will create the ``extension.json`` and ``__init__.py`` files in a new folder ``tutorialext`` for us. For more information about the registration of python extensions see :doc:`../how/how_python_extension` diff --git a/qrenderdoc/Windows/Dialogs/ExtensionManager.cpp b/qrenderdoc/Windows/Dialogs/ExtensionManager.cpp index bd5a91b21..0f684383f 100644 --- a/qrenderdoc/Windows/Dialogs/ExtensionManager.cpp +++ b/qrenderdoc/Windows/Dialogs/ExtensionManager.cpp @@ -56,8 +56,6 @@ ExtensionManager::ExtensionManager(ICaptureContext &ctx) ui->version->setText(lit("---")); ui->author->setText(lit("---")); ui->URL->setText(lit("---")); - ui->reload->setEnabled(false); - ui->debug->setEnabled(false); ui->alwaysLoad->setEnabled(false); QObject::connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); @@ -105,12 +103,8 @@ ExtensionManager::~ExtensionManager() delete ui; } -void ExtensionManager::on_reload_clicked() +void ExtensionManager::loadExtension(RDTreeWidgetItem *item) { - RDTreeWidgetItem *item = ui->extensions->currentItem(); - if(!item) - return; - int idx = ui->extensions->indexOfTopLevelItem(item); if(idx >= 0 && idx < m_Extensions.count()) @@ -139,85 +133,6 @@ void ExtensionManager::on_reload_clicked() } } -void ExtensionManager::on_debug_clicked() -{ - if(m_Extensions.empty()) - return; - - RDTreeWidgetItem *item = ui->extensions->currentItem(); - if(!item) - return; - - int idx = ui->extensions->indexOfTopLevelItem(item); - - if(idx >= 0 && idx < m_Extensions.count()) - { - const ExtensionMetadata &e = m_Extensions[idx]; - if(!e.name.isEmpty()) - { - PythonContext::PrepareDebuggerWait(); - - LambdaThread *thread = new LambdaThread([this]() { - PythonContext::WaitForDebugger(); - - GUIInvoke::call(this, [this]() { on_reload_clicked(); }); - }); - - thread->selfDelete(true); - thread->start(); - - PythonContext::LaunchDebugger(this, m_Ctx.Config(), QFileInfo(e.filePath).absoluteFilePath()); - } - } -} - -void ExtensionManager::on_output_clicked() -{ - m_Ctx.ShowPythonShell(); - m_Ctx.GetPythonShell()->ShowOutput(); - - RDTreeWidgetItem *item = ui->extensions->currentItem(); - if(item) - { - int idx = ui->extensions->indexOfTopLevelItem(item); - - if(idx >= 0 && idx < m_Extensions.count()) - { - const ExtensionMetadata &e = m_Extensions[idx]; - if(!e.package.isEmpty()) - { - m_Ctx.GetPythonShell()->SetExtensionOutputFilter(e.package); - } - } - } - - accept(); -} - -void ExtensionManager::on_openLocation_clicked() -{ - if(m_Extensions.empty()) - { - QDesktopServices::openUrl(QString(ConfigFilePath("extensions"))); - return; - } - - RDTreeWidgetItem *item = ui->extensions->currentItem(); - if(!item) - return; - - int idx = ui->extensions->indexOfTopLevelItem(item); - - if(idx >= 0 && idx < m_Extensions.count()) - { - const ExtensionMetadata &e = m_Extensions[idx]; - if(!e.name.isEmpty()) - { - QDesktopServices::openUrl(QFileInfo(e.filePath).absoluteFilePath()); - } - } -} - void ExtensionManager::on_alwaysLoad_toggled(bool checked) { RDTreeWidgetItem *item = ui->extensions->currentItem(); @@ -240,176 +155,6 @@ void ExtensionManager::on_alwaysLoad_toggled(bool checked) } } -void ExtensionManager::on_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_Extensions) - { - 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 ", - "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") -)"); - } - } - - PopulateExtensionList(); - } -} - void ExtensionManager::on_extensions_currentItemChanged(RDTreeWidgetItem *item, RDTreeWidgetItem *) { update_currentItem(item); @@ -435,7 +180,7 @@ void ExtensionManager::on_extensions_itemChanged(RDTreeWidgetItem *item, int col if(!loaded) { if(item->checkState(2) == Qt::Checked) - on_reload_clicked(); + loadExtension(item); } } } @@ -474,14 +219,6 @@ void ExtensionManager::update_currentItem(RDTreeWidgetItem *item) ui->author->setText(e.author); bool loaded = item->checkState(2) == Qt::Checked; - ui->reload->setEnabled(true); - ui->reload->setText(loaded ? tr("Reload") : tr("Load")); - ui->output->setEnabled(loaded); - ui->debug->setEnabled(loaded && PythonContext::IsDebuggingEnabled()); - ui->debug->setToolTip(QString()); - if(loaded && !PythonContext::IsDebuggingEnabled()) - ui->debug->setToolTip( - tr("Debugging not supported - check documentation for setup instructions")); ui->alwaysLoad->setEnabled(loaded); ui->alwaysLoad->setChecked(m_Ctx.Config().AlwaysLoad_Extensions.contains(e.package)); diff --git a/qrenderdoc/Windows/Dialogs/ExtensionManager.h b/qrenderdoc/Windows/Dialogs/ExtensionManager.h index 8530177b6..5089c3472 100644 --- a/qrenderdoc/Windows/Dialogs/ExtensionManager.h +++ b/qrenderdoc/Windows/Dialogs/ExtensionManager.h @@ -48,18 +48,14 @@ public: private slots: // automatic slots - void on_reload_clicked(); - void on_debug_clicked(); - void on_output_clicked(); - void on_openLocation_clicked(); void on_alwaysLoad_toggled(bool checked); - void on_createExtension_clicked(); void on_extensions_currentItemChanged(RDTreeWidgetItem *item, RDTreeWidgetItem *); void on_extensions_itemChanged(RDTreeWidgetItem *item, int col); private: void update_currentItem(RDTreeWidgetItem *item); + void loadExtension(RDTreeWidgetItem *item); void PopulateExtensionList(); Ui::ExtensionManager *ui; diff --git a/qrenderdoc/Windows/Dialogs/ExtensionManager.ui b/qrenderdoc/Windows/Dialogs/ExtensionManager.ui index faff5cadd..84ef9647c 100644 --- a/qrenderdoc/Windows/Dialogs/ExtensionManager.ui +++ b/qrenderdoc/Windows/Dialogs/ExtensionManager.ui @@ -50,32 +50,6 @@ - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Create New... - - - - - - @@ -236,34 +210,6 @@ - - - - Open Location - - - - - - - Reload - - - - - - - View output - - - - - - - Debug - - - diff --git a/qrenderdoc/Windows/PythonShell.cpp b/qrenderdoc/Windows/PythonShell.cpp index 1de804155..986a9f938 100644 --- a/qrenderdoc/Windows/PythonShell.cpp +++ b/qrenderdoc/Windows/PythonShell.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -400,6 +401,11 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) m_UIExtensions->setBold(true); m_UIExtensions->setIcon(0, Icons::plugin()); + 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_RecentFiles = new RDTreeWidgetItem({lit("Recent files")}); m_RecentFiles->setData(0, Qt::UserRole + 1, m_UIExtensions->text(0)); m_RecentFiles->setSelectable(false); @@ -663,6 +669,8 @@ void PythonShell::updateExtensionProjects() m_UIExtensions->addChild(root); } + m_UIExtensions->addChild(m_NewExtension); + ui->projectExplorer->endUpdate(); ui->projectExplorer->applyExpansion(expansion, 0, Qt::UserRole + 1); @@ -1418,7 +1426,11 @@ void PythonShell::on_projectExplorer_itemActivated(RDTreeWidgetItem *item, int c if(item == m_Examples || item == m_UIExtensions || item == m_RecentFiles) return; - if(item->parent() == m_Examples) + if(item == m_NewExtension) + { + createExtension_clicked(); + } + else if(item->parent() == m_Examples) { QString filename = tr("Example: ") + item->text(0); QString text = item->data(0, Qt::UserRole).toString(); @@ -1726,6 +1738,9 @@ void PythonShell::projectExplorer_contextMenu(const QPoint &pos) 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); }); @@ -1795,12 +1810,196 @@ void PythonShell::projectExplorer_contextMenu(const QPoint &pos) [this, 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 ", + "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); diff --git a/qrenderdoc/Windows/PythonShell.h b/qrenderdoc/Windows/PythonShell.h index f79f3cdad..cf2703a43 100644 --- a/qrenderdoc/Windows/PythonShell.h +++ b/qrenderdoc/Windows/PythonShell.h @@ -155,6 +155,7 @@ private slots: void projectExplorer_contextMenu(const QPoint &pos); void editorTab_Changed(int index); void doSyntaxCheck(); + void createExtension_clicked(); void openFileModified(const QString &path); void updateExtensionProjects(); @@ -176,7 +177,7 @@ private: bool m_IgnoreRecovered = false; - RDTreeWidgetItem *m_UIExtensions, *m_Examples, *m_RecentFiles; + RDTreeWidgetItem *m_UIExtensions, *m_Examples, *m_RecentFiles, *m_NewExtension; QFileSystemWatcher *m_Watcher = NULL;