diff --git a/qrenderdoc/Code/CaptureContext.cpp b/qrenderdoc/Code/CaptureContext.cpp index 681ca7398..787e94ddf 100644 --- a/qrenderdoc/Code/CaptureContext.cpp +++ b/qrenderdoc/Code/CaptureContext.cpp @@ -25,6 +25,7 @@ #include "CaptureContext.h" #include #include +#include #include #include #include @@ -32,7 +33,9 @@ #include #include #include +#include #include +#include "Code/pyrenderdoc/PythonContext.h" #include "Windows/APIInspector.h" #include "Windows/BufferViewer.h" #include "Windows/CommentView.h" @@ -54,6 +57,7 @@ #include "Windows/TimelineBar.h" #include "QRDUtils.h" #include "RGPInterop.h" +#include "version.h" #include "pipestate.inl" @@ -103,6 +107,24 @@ CaptureContext::CaptureContext(QString paramFilename, QString remoteHost, uint32 m_MainWindow->takeCaptureOwnership(); } } + + { + QDir dir(configFilePath("extensions")); + + if(!dir.exists()) + dir.mkpath(dir.absolutePath()); + } + + rdcarray exts = CaptureContext::GetInstalledExtensions(); + + for(const ExtensionMetadata &e : exts) + { + if(cfg.AlwaysLoad_Extensions.contains(e.Package)) + { + qInfo() << "Automatically loading extension" << QString(e.Package); + LoadExtension(e.Package); + } + } } CaptureContext::~CaptureContext() @@ -139,6 +161,197 @@ rdcstr CaptureContext::TempCaptureFilename(const rdcstr &appname) .arg(QDateTime::currentDateTimeUtc().toString(lit("yyyy.MM.dd_HH.mm.ss")))); } +rdcarray CaptureContext::GetInstalledExtensions() +{ + QString extensionFolder = configFilePath("extensions"); + + rdcarray ret; + + QDirIterator it(extensionFolder, QDirIterator::Subdirectories); + + extensionFolder = extensionFolder.toLower(); + + while(it.hasNext()) + { + QFileInfo fileinfo(it.next()); + + if(fileinfo.fileName().toLower() == lit("extension.json")) + { + QFile f(fileinfo.absoluteFilePath()); + + QString package = fileinfo.absolutePath() + .toLower() + .replace(extensionFolder, QString()) + .replace(QLatin1Char('/'), QLatin1Char('.')); + + while(package[0] == QLatin1Char('.')) + package.remove(0, 1); + + while(package[package.size() - 1] == QLatin1Char('.')) + package.remove(package.size() - 1, 1); + + if(f.exists() && f.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QVariantMap json = JSONToVariant(QString::fromUtf8(f.readAll())); + + if(json.empty()) + { + qCritical() << fileinfo.absoluteFilePath() << "is corrupt, cannot parse json"; + continue; + } + + ExtensionMetadata ext; + + ext.Package = package; + ext.FilePath = fileinfo.absolutePath(); + + if(json.contains(lit("name"))) + { + ext.Name = json[lit("name")].toString(); + } + else + { + qCritical() << "Extension" << package << "is corrupt, no name entry"; + continue; + } + + ext.ExtensionAPI = 1; + if(json.contains(lit("extension_api"))) + { + ext.ExtensionAPI = json[lit("extension_api")].toInt(); + } + else + { + qCritical() << "Extension" << QString(ext.Name) << "is corrupt, no api version entry"; + continue; + } + + if(json.contains(lit("version"))) + { + ext.Version = json[lit("version")].toString(); + } + else + { + qCritical() << "Extension" << QString(ext.Name) << "is corrupt, no version entry"; + continue; + } + + if(json.contains(lit("description"))) + { + ext.Description = json[lit("description")].toString(); + } + else + { + qCritical() << "Extension" << QString(ext.Name) << "is corrupt, no description entry"; + continue; + } + + if(json.contains(lit("author"))) + { + ext.Author = json[lit("author")].toString(); + } + else + { + qCritical() << "Extension" << QString(ext.Name) << "is corrupt, no author entry"; + continue; + } + + if(json.contains(lit("url"))) + { + ext.URL = json[lit("url")].toString(); + } + else + { + qCritical() << "Extension" << QString(ext.Name) << "is corrupt, no URL entry"; + continue; + } + + if(json.contains(lit("minimum_renderdoc"))) + { + QString minVer = json[lit("minimum_renderdoc")].toString(); + + QRegularExpression re(lit("([0-9]*).([0-9]*)")); + QRegularExpressionMatch match = re.match(minVer); + + bool ok = false, badversion = false; + + if(match.hasMatch()) + { + int major = match.captured(1).toInt(&ok); + + if(ok) + { + int minor = match.captured(2).toInt(&ok); + + // if it needs a higher major version, we can't load it + if(major > RENDERDOC_VERSION_MAJOR) + badversion = true; + + // if major versions are the same and it needs a higher minor, we can't load it either + if(major == RENDERDOC_VERSION_MAJOR && minor > RENDERDOC_VERSION_MINOR) + badversion = true; + } + } + + if(!ok) + { + qCritical() << "Extension" << QString(ext.Name) + << "is corrupt, minimum_renderdoc doesn't match a MAJOR.MINOR version"; + continue; + } + + if(badversion) + { + qInfo() << "Extension" << QString(ext.Name) << "declares minimum_renderdoc" << minVer + << "so skipping"; + continue; + } + } + + ret.push_back(ext); + } + } + } + + return ret; +} + +bool CaptureContext::IsExtensionLoaded(rdcstr name) +{ + return m_ExtensionObjects.contains(name); +} + +bool CaptureContext::LoadExtension(rdcstr name) +{ + bool ret = false; + + PythonContext::ProcessExtensionWork([this, &ret, name]() { + for(QObject *o : m_ExtensionObjects[name]) + delete o; + + m_ExtensionObjects[name].clear(); + + ret = PythonContext::LoadExtension(*this, name); + + if(ret) + { + m_ExtensionObjects[name].swap(m_PendingExtensionObjects); + } + else + { + m_ExtensionObjects.remove(name); + + for(QObject *o : m_PendingExtensionObjects) + delete o; + + m_PendingExtensionObjects.clear(); + } + }); + + m_MainWindow->CleanMenus(); + + return ret; +} void CaptureContext::LoadCapture(const rdcstr &captureFile, const rdcstr &origFilename, bool temporary, bool local) { diff --git a/qrenderdoc/Code/CaptureContext.h b/qrenderdoc/Code/CaptureContext.h index 6a270b29d..c2df2521e 100644 --- a/qrenderdoc/Code/CaptureContext.h +++ b/qrenderdoc/Code/CaptureContext.h @@ -52,7 +52,7 @@ class TimelineBar; class PythonShell; class ResourceInspector; -class CaptureContext : public ICaptureContext +class CaptureContext : public ICaptureContext, IExtensionManager { Q_DECLARE_TR_FUNCTIONS(CaptureContext); @@ -65,6 +65,13 @@ public: rdcstr TempCaptureFilename(const rdcstr &appname) override; + ////////////////////////////////////////////////////////////////////////////// + // IExtensionManager + + rdcarray GetInstalledExtensions() override; + bool IsExtensionLoaded(rdcstr name) override; + bool LoadExtension(rdcstr name) override; + ////////////////////////////////////////////////////////////////////////////// // Control functions @@ -99,6 +106,7 @@ public: // Accessors IReplayManager &Replay() override { return m_Replay; } + IExtensionManager &Extensions() override { return *this; } bool IsCaptureLoaded() override { return m_CaptureLoaded; } bool IsCaptureLocal() override { return m_CaptureLocal; } bool IsCaptureTemporary() override { return m_CaptureTemporary; } @@ -330,6 +338,9 @@ private: QIcon *m_Icon = NULL; + QList m_PendingExtensionObjects; + QMap> m_ExtensionObjects; + // Windows MainWindow *m_MainWindow = NULL; EventBrowser *m_EventBrowser = NULL; diff --git a/qrenderdoc/Code/Interface/PersistantConfig.h b/qrenderdoc/Code/Interface/PersistantConfig.h index c51341aa1..fe2d1dda7 100644 --- a/qrenderdoc/Code/Interface/PersistantConfig.h +++ b/qrenderdoc/Code/Interface/PersistantConfig.h @@ -354,6 +354,8 @@ DECLARE_REFLECTION_STRUCT(BugReport); \ CONFIG_SETTING(public, QVariantList, rdcarray, CrashReport_ReportedBugs) \ \ + CONFIG_SETTING(public, QVariantList, rdcarray, AlwaysLoad_Extensions) \ + \ CONFIG_SETTING(private, QVariantMap, rdcstrpairs, ConfigSettings) \ \ CONFIG_SETTING(private, QVariantList, rdcarray, RemoteHostList) @@ -691,6 +693,12 @@ For more information about some of these settings that are user-facing see A list of :class:`BugReport` detailing previously submitted bugs that we're watching for updates. +.. data:: AlwaysLoad_Extensions + + A list of strings with extension packages to always load on startup, without needing manual + enabling. + + )"); class PersistantConfig { diff --git a/qrenderdoc/Code/Interface/QRDInterface.h b/qrenderdoc/Code/Interface/QRDInterface.h index 097ee1a7a..1329be356 100644 --- a/qrenderdoc/Code/Interface/QRDInterface.h +++ b/qrenderdoc/Code/Interface/QRDInterface.h @@ -1022,6 +1022,110 @@ protected: DECLARE_REFLECTION_STRUCT(IRGPInterop); + +DOCUMENT("The metadata for an extension."); +struct ExtensionMetadata +{ + DOCUMENT(""); + bool operator==(const ExtensionMetadata &o) const + { + return ExtensionAPI == o.ExtensionAPI && FilePath == o.FilePath && Package == o.Package && + Name == o.Name && Version == o.Version && Author == o.Author && URL == o.URL && + Description == o.Description; + } + bool operator<(const ExtensionMetadata &o) const + { + if(!(ExtensionAPI == o.ExtensionAPI)) + return ExtensionAPI < o.ExtensionAPI; + if(!(FilePath == o.FilePath)) + return FilePath < o.FilePath; + if(!(Package == o.Package)) + return Package < o.Package; + if(!(Name == o.Name)) + return Name < o.Name; + if(!(Version == o.Version)) + return Version < o.Version; + if(!(Author == o.Author)) + return Author < o.Author; + if(!(URL == o.URL)) + return URL < o.URL; + if(!(Description == o.Description)) + return Description < o.Description; + return false; + } + + DOCUMENT("The version of the extension API that this extension is written against"); + int ExtensionAPI; + + DOCUMENT("The location of this package on disk"); + rdcstr FilePath; + + DOCUMENT("The python package for this extension, e.g. foo.bar"); + rdcstr Package; + + DOCUMENT("The short friendly name for the extension"); + rdcstr Name; + + DOCUMENT("The version of the extension"); + rdcstr Version; + + DOCUMENT("The author of the extension, optionally with an email contact"); + rdcstr Author; + + DOCUMENT("The URL for where the extension is fetched from"); + rdcstr URL; + + DOCUMENT("A longer description of what the extension does"); + rdcstr Description; +}; + +DECLARE_REFLECTION_STRUCT(ExtensionMetadata); + +DOCUMENT(R"(A manager for listing available and active extensions, as well as the interface for +extensions to register hooks and additional functionality. + +.. function:: ExtensionCallback(context) + + Not a member function - the signature for any ``ExtensionCallback`` callbacks. + + Callback for extensions to register entry points with, used in many situations depending on how it + was registered. + + :param CaptureContext context: The current capture context. +)"); +struct IExtensionManager +{ + typedef std::function ExtensionCallback; + + DOCUMENT(R"(Retrieve a list of installed extensions. + +:return: The list of installed extensions. +:rtype: ``list`` of :class:`ExtensionMetadata`. +)"); + virtual rdcarray GetInstalledExtensions() = 0; + + DOCUMENT(R"(Check if an installed extension is enabled. + +:param str name: The qualified name of the extension, e.g. ``foo.bar`` +:return: If the extension is enabled or not. +:rtype: bool +)"); + virtual bool IsExtensionLoaded(rdcstr name) = 0; + + DOCUMENT(R"(Enable an extension by name. If the extension is already enabled, this will reload it. + +:param str name: The qualified name of the extension, e.g. ``foo.bar`` +)"); + virtual bool LoadExtension(rdcstr name) = 0; + +protected: + DOCUMENT(""); + IExtensionManager() = default; + ~IExtensionManager() = default; +}; + +DECLARE_REFLECTION_STRUCT(IExtensionManager); + DOCUMENT("The capture context that the python script is running in.") struct ICaptureContext { @@ -1924,6 +2028,13 @@ capture's API. )"); virtual PersistantConfig &Config() = 0; + DOCUMENT(R"(Retrieve the manager for extensions. + +:return: The current extension manager. +:rtype: ExtensionManager +)"); + virtual IExtensionManager &Extensions() = 0; + protected: ICaptureContext() = default; ~ICaptureContext() = default; diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp index 0ff7320fc..98d3e342e 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp @@ -506,6 +506,17 @@ bool PythonContext::LoadExtension(ICaptureContext &ctx, const rdcstr &extension) { qInfo() << "Reloading " << QString(extension); + // call unregister() if it exists + PyObject *unregister_func = PyObject_GetAttrString(extensions[extension], "unregister"); + + if(unregister_func) + { + PyObject *retval = PyObject_CallFunction(unregister_func, ""); + + // discard the return value, regardless of error we don't abort the reload + Py_XDECREF(retval); + } + // if the extension is a package, we need to manually reload any loaded submodules PyObject *sysmodules = PyObject_GetAttrString(sysobj, "modules"); diff --git a/qrenderdoc/Code/pyrenderdoc/qrenderdoc.i b/qrenderdoc/Code/pyrenderdoc/qrenderdoc.i index ca7125ee4..7419bbad6 100644 --- a/qrenderdoc/Code/pyrenderdoc/qrenderdoc.i +++ b/qrenderdoc/Code/pyrenderdoc/qrenderdoc.i @@ -83,6 +83,7 @@ TEMPLATE_ARRAY_INSTANTIATE(rdcarray, EventBookmark) TEMPLATE_ARRAY_INSTANTIATE(rdcarray, ShaderProcessingTool) TEMPLATE_ARRAY_INSTANTIATE(rdcarray, rdcstrpair) TEMPLATE_ARRAY_INSTANTIATE(rdcarray, BugReport) +TEMPLATE_ARRAY_INSTANTIATE(rdcarray, ExtensionMetadata) TEMPLATE_ARRAY_INSTANTIATE_PTR(rdcarray, ICaptureViewer) // unignore the function from above diff --git a/qrenderdoc/Windows/Dialogs/ExtensionManager.cpp b/qrenderdoc/Windows/Dialogs/ExtensionManager.cpp new file mode 100644 index 000000000..b46281927 --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/ExtensionManager.cpp @@ -0,0 +1,241 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2018 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "ExtensionManager.h" +#include +#include +#include +#include +#include "Code/Interface/QRDInterface.h" +#include "Code/Resources.h" +#include "Widgets/Extended/RDHeaderView.h" +#include "Windows/MainWindow.h" +#include "ui_ExtensionManager.h" + +Q_DECLARE_METATYPE(ExtensionMetadata); + +ExtensionManager::ExtensionManager(ICaptureContext &ctx) + : QDialog(NULL), ui(new Ui::ExtensionManager), m_Ctx(ctx) +{ + ui->setupUi(this); + + { + RDHeaderView *header = new RDHeaderView(Qt::Horizontal, this); + ui->extensions->setHeader(header); + + ui->extensions->setColumns({tr("Package"), tr("Name"), tr("Loaded")}); + header->setColumnStretchHints({1, 4, -1}); + } + + ui->name->setText(lit("---")); + ui->version->setText(lit("---")); + ui->author->setText(lit("---")); + ui->URL->setText(lit("---")); + ui->reload->setEnabled(false); + ui->alwaysLoad->setEnabled(false); + + QObject::connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + + QString extensionFolder = configFilePath("extensions"); + + m_Extensions = m_Ctx.Extensions().GetInstalledExtensions(); + + if(m_Extensions.isEmpty()) + { + ui->extensions->addTopLevelItem( + new RDTreeWidgetItem({QString(), lit("No extensions found available"), QString()})); + ui->extensions->addTopLevelItem(new RDTreeWidgetItem( + {QString(), lit("Create packages in %1").arg(extensionFolder), QString()})); + } + else + { + for(const ExtensionMetadata &e : m_Extensions) + { + RDTreeWidgetItem *item = new RDTreeWidgetItem({e.Package, e.Name, QString()}); + + item->setCheckState( + 2, m_Ctx.Extensions().IsExtensionLoaded(e.Package) ? Qt::Checked : Qt::Unchecked); + + ui->extensions->addTopLevelItem(item); + } + + ui->extensions->setCurrentItem(ui->extensions->topLevelItem(0)); + } +} + +ExtensionManager::~ExtensionManager() +{ + delete ui; +} + +void ExtensionManager::on_reload_clicked() +{ + 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()) + { + // if the load succeeds, set us as checked. Otherwise, unchecked + if(m_Ctx.Extensions().LoadExtension(e.Package)) + { + item->setCheckState(2, Qt::Checked); + } + else + { + item->setCheckState(2, Qt::Unchecked); + RDDialog::critical(this, tr("Failed to load extension"), + tr("Failed to load extension '%1'.\n" + "Check the diagnostic log for python errors") + .arg(e.Name)); + } + + update_currentItem(item); + } + } +} + +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(); + 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()) + { + m_Ctx.Config().AlwaysLoad_Extensions.removeOne(e.Package); + if(checked) + m_Ctx.Config().AlwaysLoad_Extensions.push_back(e.Package); + + m_Ctx.Config().Save(); + } + } +} + +void ExtensionManager::on_extensions_currentItemChanged(RDTreeWidgetItem *item, RDTreeWidgetItem *) +{ + update_currentItem(item); +} + +void ExtensionManager::on_extensions_itemChanged(RDTreeWidgetItem *item, int col) +{ + if(col == 2) + { + ui->extensions->setCurrentItem(item); + + bool loaded = m_Ctx.Extensions().IsExtensionLoaded(item->text(0)); + + // if the extension is loaded, don't allow unchecking + if(loaded && item->checkState(2) != Qt::Checked) + { + item->setCheckState(2, Qt::Checked); + return; + } + + // if the extension is unloaded, if we're now checked then try to load it. If + // we're unchecked allow that (it is a code-change after we failed to load) + if(!loaded) + { + if(item->checkState(2) == Qt::Checked) + on_reload_clicked(); + } + } +} + +void ExtensionManager::update_currentItem(RDTreeWidgetItem *item) +{ + if(!item) + return; + + if(item != ui->extensions->currentItem()) + { + ui->extensions->setCurrentItem(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()) + { + QRegularExpression authRE(lit("^(.*) <(.*)>$")); + + ui->name->setText(e.Name); + ui->version->setText(e.Version); + ui->URL->setText(QFormatStr("%1").arg(e.URL)); + ui->description->setText(e.Description); + + QRegularExpressionMatch match = authRE.match(QString(e.Author).trimmed()); + + if(match.hasMatch() && match.captured(2).contains(QLatin1Char('@'))) + ui->author->setText( + QFormatStr("%1").arg(match.captured(1)).arg(match.captured(2))); + else + 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->alwaysLoad->setEnabled(loaded); + + ui->alwaysLoad->setChecked(m_Ctx.Config().AlwaysLoad_Extensions.contains(e.Package)); + } + } +} \ No newline at end of file diff --git a/qrenderdoc/Windows/Dialogs/ExtensionManager.h b/qrenderdoc/Windows/Dialogs/ExtensionManager.h new file mode 100644 index 000000000..26be1c8c0 --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/ExtensionManager.h @@ -0,0 +1,64 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2018 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#pragma once + +#include +#include +#include +#include "Code/Interface/QRDInterface.h" + +namespace Ui +{ +class ExtensionManager; +} + +class RDTreeWidgetItem; +struct ICaptureContext; +class MainWindow; + +class ExtensionManager : public QDialog +{ + Q_OBJECT + +public: + explicit ExtensionManager(ICaptureContext &ctx); + ~ExtensionManager(); + +private slots: + // automatic slots + void on_reload_clicked(); + void on_openLocation_clicked(); + void on_alwaysLoad_toggled(bool checked); + void on_extensions_currentItemChanged(RDTreeWidgetItem *item, RDTreeWidgetItem *); + void on_extensions_itemChanged(RDTreeWidgetItem *item, int col); + +private: + void update_currentItem(RDTreeWidgetItem *item); + + Ui::ExtensionManager *ui; + ICaptureContext &m_Ctx; + + rdcarray m_Extensions; +}; diff --git a/qrenderdoc/Windows/Dialogs/ExtensionManager.ui b/qrenderdoc/Windows/Dialogs/ExtensionManager.ui new file mode 100644 index 000000000..a2d7f42a7 --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/ExtensionManager.ui @@ -0,0 +1,244 @@ + + + ExtensionManager + + + + 0 + 0 + 556 + 544 + + + + + 400 + 0 + + + + Extension Manager + + + true + + + + + + + 0 + 0 + + + + Extension List + + + + + + QAbstractItemView::NoEditTriggers + + + 0 + + + false + + + + + + + + + + Details + + + + + + URL: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + Name: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + Author: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + Version: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + + + + + + + + + + true + + + + + + + + + + true + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 0 + + + + + Open Location + + + + + + + Reload + + + + + + + Always Load + + + + + + + + + + + Description: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + 0 + 0 + + + + + + + + + + + QFrame::Panel + + + QFrame::Plain + + + true + + + Qt::NoTextInteraction + + + + + + + + + + QDialogButtonBox::Ok + + + + + + + + RDTreeWidget + QTreeView +
Widgets/Extended/RDTreeWidget.h
+
+
+ + +
diff --git a/qrenderdoc/Windows/MainWindow.cpp b/qrenderdoc/Windows/MainWindow.cpp index 6569f2bef..6066cbb9a 100644 --- a/qrenderdoc/Windows/MainWindow.cpp +++ b/qrenderdoc/Windows/MainWindow.cpp @@ -42,6 +42,7 @@ #include "Windows/Dialogs/AboutDialog.h" #include "Windows/Dialogs/CaptureDialog.h" #include "Windows/Dialogs/CrashDialog.h" +#include "Windows/Dialogs/ExtensionManager.h" #include "Windows/Dialogs/LiveCapture.h" #include "Windows/Dialogs/RemoteManager.h" #include "Windows/Dialogs/SettingsDialog.h" @@ -2432,6 +2433,12 @@ void MainWindow::on_action_Attach_to_Running_Instance_triggered() on_action_Manage_Remote_Servers_triggered(); } +void MainWindow::on_action_Manage_Extensions_triggered() +{ + ExtensionManager manager(m_Ctx); + RDDialog::show(&manager); +} + void MainWindow::on_action_Manage_Remote_Servers_triggered() { RemoteManager *rm = new RemoteManager(m_Ctx, this); diff --git a/qrenderdoc/Windows/MainWindow.h b/qrenderdoc/Windows/MainWindow.h index 50afa54e0..2eca72ce4 100644 --- a/qrenderdoc/Windows/MainWindow.h +++ b/qrenderdoc/Windows/MainWindow.h @@ -131,6 +131,7 @@ private slots: void on_action_Open_RGP_Profile_triggered(); void on_action_Create_RGP_Profile_triggered(); void on_action_Attach_to_Running_Instance_triggered(); + void on_action_Manage_Extensions_triggered(); void on_action_Manage_Remote_Servers_triggered(); void on_action_Settings_triggered(); void on_action_View_Documentation_triggered(); diff --git a/qrenderdoc/Windows/MainWindow.ui b/qrenderdoc/Windows/MainWindow.ui index 036445478..9807a269e 100644 --- a/qrenderdoc/Windows/MainWindow.ui +++ b/qrenderdoc/Windows/MainWindow.ui @@ -57,6 +57,7 @@ + @@ -466,6 +467,11 @@ Open RGP Profile + + + Manage Extensions + + diff --git a/qrenderdoc/Windows/PythonShell.cpp b/qrenderdoc/Windows/PythonShell.cpp index 806f73c81..b56d8e9b5 100644 --- a/qrenderdoc/Windows/PythonShell.cpp +++ b/qrenderdoc/Windows/PythonShell.cpp @@ -52,6 +52,7 @@ struct CaptureContextInvoker : ICaptureContext { return m_Ctx.TempCaptureFilename(appname); } + virtual IExtensionManager &Extensions() override { return m_Ctx.Extensions(); } virtual IReplayManager &Replay() override { return m_Ctx.Replay(); } virtual bool IsCaptureLoaded() override { return m_Ctx.IsCaptureLoaded(); } virtual bool IsCaptureLocal() override { return m_Ctx.IsCaptureLocal(); } diff --git a/qrenderdoc/qrenderdoc.pro b/qrenderdoc/qrenderdoc.pro index 55b12dca2..2b92ad291 100644 --- a/qrenderdoc/qrenderdoc.pro +++ b/qrenderdoc/qrenderdoc.pro @@ -217,6 +217,7 @@ SOURCES += Code/qrenderdoc.cpp \ Windows/Dialogs/SuggestRemoteDialog.cpp \ Windows/Dialogs/VirtualFileDialog.cpp \ Windows/Dialogs/RemoteManager.cpp \ + Windows/Dialogs/ExtensionManager.cpp \ Windows/PixelHistoryView.cpp \ Widgets/PipelineFlowChart.cpp \ Windows/Dialogs/EnvironmentEditor.cpp \ @@ -291,6 +292,7 @@ HEADERS += Code/CaptureContext.h \ Windows/Dialogs/SuggestRemoteDialog.h \ Windows/Dialogs/VirtualFileDialog.h \ Windows/Dialogs/RemoteManager.h \ + Windows/Dialogs/ExtensionManager.h \ Windows/PixelHistoryView.h \ Widgets/PipelineFlowChart.h \ Windows/Dialogs/EnvironmentEditor.h \ @@ -331,6 +333,7 @@ FORMS += Windows/Dialogs/AboutDialog.ui \ Windows/Dialogs/SuggestRemoteDialog.ui \ Windows/Dialogs/VirtualFileDialog.ui \ Windows/Dialogs/RemoteManager.ui \ + Windows/Dialogs/ExtensionManager.ui \ Windows/PixelHistoryView.ui \ Windows/Dialogs/EnvironmentEditor.ui \ Widgets/FindReplace.ui \ diff --git a/qrenderdoc/qrenderdoc_local.vcxproj b/qrenderdoc/qrenderdoc_local.vcxproj index b087de4dc..c83776775 100644 --- a/qrenderdoc/qrenderdoc_local.vcxproj +++ b/qrenderdoc/qrenderdoc_local.vcxproj @@ -613,6 +613,7 @@ + @@ -726,6 +727,7 @@ + @@ -934,6 +936,7 @@ + @@ -1209,6 +1212,12 @@ MOC %(Filename).h $(IntDir)generated\moc_%(Filename).cpp + + %(Fullpath);$(ProjectDir)3rdparty\qt\$(Platform)\bin\moc.exe;%(AdditionalInputs) + "$(ProjectDir)3rdparty\qt\$(Platform)\bin\moc.exe" -DUNICODE -DWIN32 -DWIN64 -D_WIN32 -D_WIN64 -DRENDERDOC_PLATFORM_WIN32 -DSCINTILLA_QT=1 -DSCI_LEXER=1 -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -D_MSC_VER=1900 -I"$(ProjectDir)." -I"$(SolutionDir)\renderdoc\api\replay" -I"$(ProjectDir)3rdparty\qt\$(Platform)\mkspecs/win32-msvc2015" -I"$(ProjectDir)3rdparty\qt\include" -I"$(ProjectDir)3rdparty\qt\include\QtWidgets" -I"$(ProjectDir)3rdparty\qt\include\QtGui" -I"$(ProjectDir)3rdparty\qt\include\QtCore" "%(Fullpath)" -o "$(IntDir)generated\moc_%(Filename).cpp" + MOC %(Filename).h + $(IntDir)generated\moc_%(Filename).cpp + %(Fullpath);$(ProjectDir)3rdparty\qt\$(Platform)\bin\moc.exe;%(AdditionalInputs) "$(ProjectDir)3rdparty\qt\$(Platform)\bin\moc.exe" -DUNICODE -DWIN32 -DWIN64 -D_WIN32 -D_WIN64 -DRENDERDOC_PLATFORM_WIN32 -DSCINTILLA_QT=1 -DSCI_LEXER=1 -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -D_MSC_VER=1900 -I"$(ProjectDir)." -I"$(SolutionDir)\renderdoc\api\replay" -I"$(ProjectDir)3rdparty\qt\$(Platform)\mkspecs/win32-msvc2015" -I"$(ProjectDir)3rdparty\qt\include" -I"$(ProjectDir)3rdparty\qt\include\QtWidgets" -I"$(ProjectDir)3rdparty\qt\include\QtGui" -I"$(ProjectDir)3rdparty\qt\include\QtCore" "%(Fullpath)" -o "$(IntDir)generated\moc_%(Filename).cpp" @@ -1456,6 +1465,12 @@ UIC %(Filename).ui $(IntDir)generated\ui_%(Filename).h + + %(Fullpath);$(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe;%(AdditionalInputs) + "$(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe" "%(Fullpath)" -o "$(IntDir)generated\ui_%(Filename).h" + UIC %(Filename).ui + $(IntDir)generated\ui_%(Filename).h + %(Fullpath);$(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe;%(AdditionalInputs) "$(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe" "%(Fullpath)" -o "$(IntDir)generated\ui_%(Filename).h" diff --git a/qrenderdoc/qrenderdoc_local.vcxproj.filters b/qrenderdoc/qrenderdoc_local.vcxproj.filters index 00522047e..609cdb2d3 100644 --- a/qrenderdoc/qrenderdoc_local.vcxproj.filters +++ b/qrenderdoc/qrenderdoc_local.vcxproj.filters @@ -711,6 +711,12 @@ Code\Interface + + Generated Files + + + Windows\Dialogs + @@ -1061,6 +1067,9 @@ Code + + Generated Files + @@ -1436,6 +1445,12 @@ Widgets + + Windows\Dialogs + + + Windows\Dialogs +