diff --git a/qrenderdoc/Code/CaptureContext.cpp b/qrenderdoc/Code/CaptureContext.cpp index 0826947c3..a6ba0d063 100644 --- a/qrenderdoc/Code/CaptureContext.cpp +++ b/qrenderdoc/Code/CaptureContext.cpp @@ -220,6 +220,13 @@ CaptureContext::CaptureContext(PersistantConfig &cfg) : m_Config(cfg) dir.mkpath(dir.absolutePath()); } + m_ExtensionWatcher = new QFileSystemWatcher({}, GetMainWindow()->Widget()); + + QObject::connect(m_ExtensionWatcher, &QFileSystemWatcher::directoryChanged, + [this](const QString &path) { ExtensionTouched(path); }); + QObject::connect(m_ExtensionWatcher, &QFileSystemWatcher::fileChanged, + [this](const QString &path) { ExtensionTouched(path); }); + rdcarray exts = CaptureContext::GetInstalledExtensions(); for(const ExtensionMetadata &e : exts) @@ -234,6 +241,8 @@ CaptureContext::CaptureContext(PersistantConfig &cfg) : m_Config(cfg) CaptureContext::~CaptureContext() { + delete m_ExtensionWatcher; + delete m_QtHelper; RENDERDOC_UnregisterMemoryRegion(this); delete m_Icon; @@ -340,6 +349,8 @@ rdcarray CaptureContext::GetInstalledExtensions() ext.package = package; ext.filePath = fileinfo.absolutePath(); + ext.hasChanges = m_DirtyExtensions.contains(rdcstr(package)); + if(json.contains(lit("name"))) { ext.name = json[lit("name")].toString(); @@ -483,6 +494,12 @@ rdcstr CaptureContext::LoadExtension(rdcstr name) if(ret.isEmpty()) { m_ExtensionObjects[name].swap(m_PendingExtensionObjects); + + m_DirtyExtensions.removeOne(name); + + for(const ExtensionMetadata &e : GetInstalledExtensions()) + if(e.package == name) + AddExtensionWatches(e.filePath); } else { @@ -2804,6 +2821,47 @@ void CaptureContext::setupDockWindow(QWidget *shad, bool hide) shad->hide(); } +void CaptureContext::AddExtensionWatches(rdcstr filePath) +{ + QDir dir(filePath); + + // ignore directories with no python files + QStringList pyfiles = dir.entryList(QStringList() << lit("*.py")); + if(pyfiles.empty()) + return; + + // don't watch the directory itself. New files will not be usable and we will already catch + // renames. + // m_ExtensionWatcher->addPath(filePath); + for(QString pyfile : pyfiles) + m_ExtensionWatcher->addPath(dir.absoluteFilePath(pyfile)); + + for(QString subdir : dir.entryList(QStringList(), QDir::Dirs | QDir::NoDotAndDotDot)) + AddExtensionWatches(dir.absoluteFilePath(subdir)); +} + +void CaptureContext::ExtensionTouched(const QString &extensionPath) +{ + // find the root extension dir containing this path + for(const ExtensionMetadata &m : GetInstalledExtensions()) + { + if(extensionPath.contains(m.filePath)) + { + // remove all watched paths underneath + QStringList paths = m_ExtensionWatcher->directories(); + paths.append(m_ExtensionWatcher->files()); + for(QString path : paths) + { + if(path.contains(m.filePath)) + m_ExtensionWatcher->removePath(path); + } + + // mark it as 'dirty' / needing reload + m_DirtyExtensions.push_back(m.package); + } + } +} + void CaptureContext::RaiseDockWindow(QWidget *dockWindow) { ToolWindowManager::raiseToolWindow(dockWindow); diff --git a/qrenderdoc/Code/CaptureContext.h b/qrenderdoc/Code/CaptureContext.h index 66b1752b3..e0294098f 100644 --- a/qrenderdoc/Code/CaptureContext.h +++ b/qrenderdoc/Code/CaptureContext.h @@ -342,6 +342,9 @@ private: bool SaveEdits(); void LoadEdits(const QString &data); + void AddExtensionWatches(rdcstr filePath); + void ExtensionTouched(const QString &path); + void CacheResources(); rdcstr GetResourceNameUnsuffixed(const ResourceDescription *desc) const; @@ -428,6 +431,7 @@ private: QList m_PendingExtensionObjects; QMap> m_ExtensionObjects; + rdcarray m_DirtyExtensions; QList> m_RegisteredMenuItems; @@ -436,6 +440,7 @@ private: MiniQtHelper *m_QtHelper = NULL; QFileSystemWatcher *m_Watcher = NULL; + QFileSystemWatcher *m_ExtensionWatcher = NULL; // Windows MainWindow *m_MainWindow = NULL; diff --git a/qrenderdoc/Code/Interface/Extensions.h b/qrenderdoc/Code/Interface/Extensions.h index 6b4387ad1..b200b6775 100644 --- a/qrenderdoc/Code/Interface/Extensions.h +++ b/qrenderdoc/Code/Interface/Extensions.h @@ -357,6 +357,14 @@ struct ExtensionMetadata :type: str )"); rdcstr description; + + DOCUMENT(R"(A flag indicating that the extension has changed on disk since the last time it was loaded. + +This will always be false if the extension is unloaded. + +:type: bool +)"); + bool hasChanges = false; }; DECLARE_REFLECTION_STRUCT(ExtensionMetadata); diff --git a/qrenderdoc/Windows/MainWindow.cpp b/qrenderdoc/Windows/MainWindow.cpp index 66f612889..6ca54d578 100644 --- a/qrenderdoc/Windows/MainWindow.cpp +++ b/qrenderdoc/Windows/MainWindow.cpp @@ -234,17 +234,32 @@ MainWindow::MainWindow(ICaptureContext &ctx) : QMainWindow(NULL), ui(new Ui::Mai extensionStatus->setIcon(Icons::plugin()); extensionStatus->setVisible(false); + extensionReload = new RDToolButton(this); + extensionReload->setAutoRaise(true); + extensionReload->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + ui->statusBar->addWidget(extensionReload); + + extensionReload->setIcon(Icons::update()); + extensionReload->setText(tr("Reload changed extensions")); + extensionReload->setVisible(false); + QObject::connect(extensionStatus, &RDToolButton::clicked, this, &MainWindow::on_action_Manage_Extensions_triggered); + QObject::connect(extensionReload, &RDToolButton::clicked, [this]() { + rdcarray exts = m_Ctx.Extensions().GetInstalledExtensions(); + for(const ExtensionMetadata &m : exts) + if(m.hasChanges) + m_Ctx.Extensions().LoadExtension(m.package); + }); QObject::connect(PythonContext::GetExtensionContext(), &PythonContext::extensionLoaded, this, &MainWindow::PythonStatusUpdate); - QTimer *debuggerTimer = new QTimer(this); - QObject::connect(debuggerTimer, &QTimer::timeout, this, &MainWindow::PythonStatusUpdate); + QTimer *pyStatusTimer = new QTimer(this); + QObject::connect(pyStatusTimer, &QTimer::timeout, this, &MainWindow::PythonStatusUpdate); - debuggerTimer->setSingleShot(false); - debuggerTimer->setInterval(500); - debuggerTimer->start(); + pyStatusTimer->setSingleShot(false); + pyStatusTimer->setInterval(500); + pyStatusTimer->start(); QObject::connect(&m_MessageTick, &QTimer::timeout, this, &MainWindow::messageCheck); m_MessageTick.setSingleShot(false); @@ -1348,6 +1363,19 @@ void MainWindow::PythonStatusUpdate() extensionStatus->setVisible(true); QString text = tr("%n extension(s) active", NULL, num); + + bool reloadVisible = false; + for(const ExtensionMetadata &m : m_Ctx.Extensions().GetInstalledExtensions()) + { + if(m.hasChanges) + { + text += tr(" (changed on disk)"); + reloadVisible = true; + break; + } + } + extensionReload->setVisible(reloadVisible); + if(PythonContext::IsDebuggerConnected()) text += tr(": Python debugger connected"); extensionStatus->setText(text); diff --git a/qrenderdoc/Windows/MainWindow.h b/qrenderdoc/Windows/MainWindow.h index e504df214..0b09776b1 100644 --- a/qrenderdoc/Windows/MainWindow.h +++ b/qrenderdoc/Windows/MainWindow.h @@ -252,6 +252,7 @@ private: RDLabel *statusIcon; RDLabel *statusText; RDToolButton *extensionStatus; + RDToolButton *extensionReload; QProgressBar *statusProgress; RDMenu *contextChooserMenu; QToolButton *contextChooser;