mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-05 01:20:42 +00:00
9b7581011e
* This means that if an application uses more than one API, the correct API is listed for each capture.
1250 lines
35 KiB
C++
1250 lines
35 KiB
C++
/******************************************************************************
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2016-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 "LiveCapture.h"
|
|
#include <QDesktopServices>
|
|
#include <QMenu>
|
|
#include <QMetaProperty>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QProcess>
|
|
#include <QScrollBar>
|
|
#include <QStyledItemDelegate>
|
|
#include <QToolBar>
|
|
#include <QToolButton>
|
|
#include "3rdparty/toolwindowmanager/ToolWindowManager.h"
|
|
#include "Code/QRDUtils.h"
|
|
#include "Code/Resources.h"
|
|
#include "Code/qprocessinfo.h"
|
|
#include "Widgets/Extended/RDLabel.h"
|
|
#include "Windows/MainWindow.h"
|
|
#include "ui_LiveCapture.h"
|
|
|
|
static const int PIDRole = Qt::UserRole + 1;
|
|
static const int IdentRole = Qt::UserRole + 2;
|
|
static const int CapPtrRole = Qt::UserRole + 3;
|
|
|
|
class NameEditOnlyDelegate : public QStyledItemDelegate
|
|
{
|
|
public:
|
|
LiveCapture *live;
|
|
NameEditOnlyDelegate(LiveCapture *l) : live(l) {}
|
|
void setEditorData(QWidget *editor, const QModelIndex &index) const override
|
|
{
|
|
QByteArray n = editor->metaObject()->userProperty().name();
|
|
QListWidgetItem *item = live->ui->captures->item(index.row());
|
|
|
|
if(!n.isEmpty() && item)
|
|
{
|
|
LiveCapture::Capture *cap = live->GetCapture(item);
|
|
if(cap)
|
|
editor->setProperty(n, cap->name);
|
|
}
|
|
}
|
|
|
|
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override
|
|
{
|
|
QByteArray n = editor->metaObject()->userProperty().name();
|
|
QListWidgetItem *item = live->ui->captures->item(index.row());
|
|
|
|
if(!n.isEmpty() && item)
|
|
{
|
|
LiveCapture::Capture *cap = live->GetCapture(item);
|
|
if(cap)
|
|
{
|
|
cap->name = editor->property(n).toString();
|
|
item->setText(live->MakeText(cap));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
LiveCapture::LiveCapture(ICaptureContext &ctx, const QString &hostname, const QString &friendlyname,
|
|
uint32_t ident, MainWindow *main, QWidget *parent)
|
|
: QFrame(parent),
|
|
ui(new Ui::LiveCapture),
|
|
m_Ctx(ctx),
|
|
m_Hostname(hostname),
|
|
m_HostFriendlyname(friendlyname),
|
|
m_RemoteIdent(ident),
|
|
m_Main(main)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
m_Disconnect.release();
|
|
|
|
QObject::connect(&childUpdateTimer, &QTimer::timeout, this, &LiveCapture::childUpdate);
|
|
childUpdateTimer.setSingleShot(false);
|
|
childUpdateTimer.setInterval(1000);
|
|
childUpdateTimer.start();
|
|
|
|
QObject::connect(&countdownTimer, &QTimer::timeout, this, &LiveCapture::captureCountdownTick);
|
|
countdownTimer.setSingleShot(true);
|
|
countdownTimer.setInterval(1000);
|
|
|
|
childUpdate();
|
|
|
|
ui->previewSplit->setCollapsible(1, true);
|
|
ui->previewSplit->setSizes({1, 0});
|
|
|
|
QObject::connect(ui->preview, &RDLabel::clicked, this, &LiveCapture::preview_mouseClick);
|
|
QObject::connect(ui->preview, &RDLabel::mouseMoved, this, &LiveCapture::preview_mouseMove);
|
|
|
|
ui->preview->setMouseTracking(true);
|
|
|
|
setTitle(tr("Connecting"));
|
|
ui->connectionStatus->setText(tr("Connecting"));
|
|
ui->connectionIcon->setPixmap(Pixmaps::hourglass(ui->connectionIcon));
|
|
|
|
ui->apiIcon->setVisible(false);
|
|
|
|
ui->triggerDelayedCapture->setEnabled(false);
|
|
ui->triggerImmediateCapture->setEnabled(false);
|
|
ui->queueCap->setEnabled(false);
|
|
|
|
ui->target->setText(QString());
|
|
|
|
ui->captureProgressLabel->setVisible(false);
|
|
ui->captureProgress->setVisible(false);
|
|
|
|
ui->captures->setItemDelegate(new NameEditOnlyDelegate(this));
|
|
|
|
ui->captures->verticalScrollBar()->setSingleStep(20);
|
|
|
|
{
|
|
QToolBar *bottomTools = new QToolBar(this);
|
|
|
|
QWidget *rightAlign = new QWidget(this);
|
|
rightAlign->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
bottomTools->addWidget(rightAlign);
|
|
|
|
previewToggle = new QAction(tr("Preview"), this);
|
|
previewToggle->setCheckable(true);
|
|
|
|
bottomTools->addAction(previewToggle);
|
|
|
|
QMenu *openMenu = new QMenu(tr("&Open in..."), this);
|
|
QAction *thisAction = new QAction(tr("This instance"), this);
|
|
newWindowAction = new QAction(tr("New instance"), this);
|
|
|
|
openMenu->addAction(thisAction);
|
|
openMenu->addAction(newWindowAction);
|
|
|
|
openButton = new QToolButton(this);
|
|
openButton->setText(tr("Open"));
|
|
openButton->setPopupMode(QToolButton::MenuButtonPopup);
|
|
openButton->setMenu(openMenu);
|
|
|
|
bottomTools->addWidget(openButton);
|
|
|
|
saveAction = new QAction(tr("Save"), this);
|
|
deleteAction = new QAction(tr("Delete"), this);
|
|
|
|
bottomTools->addAction(saveAction);
|
|
bottomTools->addAction(deleteAction);
|
|
|
|
QObject::connect(previewToggle, &QAction::toggled, this, &LiveCapture::previewToggle_toggled);
|
|
QObject::connect(openButton, &QToolButton::clicked, this, &LiveCapture::openCapture_triggered);
|
|
QObject::connect(thisAction, &QAction::triggered, this, &LiveCapture::openCapture_triggered);
|
|
QObject::connect(newWindowAction, &QAction::triggered, this,
|
|
&LiveCapture::openNewWindow_triggered);
|
|
QObject::connect(saveAction, &QAction::triggered, this, &LiveCapture::saveCapture_triggered);
|
|
QObject::connect(deleteAction, &QAction::triggered, this, &LiveCapture::deleteCapture_triggered);
|
|
|
|
QObject::connect(ui->captures, &RDListWidget::keyPress, this, &LiveCapture::captures_keyPress);
|
|
|
|
ui->mainLayout->addWidget(bottomTools);
|
|
}
|
|
}
|
|
|
|
LiveCapture::~LiveCapture()
|
|
{
|
|
m_Main->LiveCaptureClosed(this);
|
|
|
|
cleanItems();
|
|
killThread();
|
|
|
|
delete ui;
|
|
}
|
|
|
|
void LiveCapture::QueueCapture(int frameNumber, int numFrames)
|
|
{
|
|
m_QueueCaptureFrameNum = frameNumber;
|
|
m_CaptureNumFrames = numFrames;
|
|
m_QueueCapture = true;
|
|
}
|
|
|
|
void LiveCapture::showEvent(QShowEvent *event)
|
|
{
|
|
if(!m_ConnectThread)
|
|
{
|
|
m_ConnectThread = new LambdaThread([this]() { this->connectionThreadEntry(); });
|
|
m_ConnectThread->start();
|
|
}
|
|
|
|
on_captures_itemSelectionChanged();
|
|
}
|
|
|
|
void LiveCapture::on_captures_mouseClicked(QMouseEvent *e)
|
|
{
|
|
if(e->buttons() & Qt::RightButton && !ui->captures->selectedItems().empty())
|
|
{
|
|
QMenu contextMenu(this);
|
|
|
|
QMenu contextOpenMenu(tr("&Open in..."), this);
|
|
QAction thisAction(tr("This instance"), this);
|
|
QAction newAction(tr("New instance"), this);
|
|
|
|
contextOpenMenu.addAction(&thisAction);
|
|
contextOpenMenu.addAction(&newAction);
|
|
|
|
QAction contextSaveAction(tr("&Save"), this);
|
|
QAction contextDeleteAction(tr("&Delete"), this);
|
|
|
|
contextMenu.addAction(contextOpenMenu.menuAction());
|
|
contextMenu.addAction(&contextSaveAction);
|
|
contextMenu.addAction(&contextDeleteAction);
|
|
|
|
if(ui->captures->selectedItems().size() == 1)
|
|
{
|
|
newAction.setEnabled(GetCapture(ui->captures->selectedItems()[0])->local);
|
|
}
|
|
else
|
|
{
|
|
contextOpenMenu.setEnabled(false);
|
|
contextSaveAction.setEnabled(false);
|
|
}
|
|
|
|
QObject::connect(&thisAction, &QAction::triggered, this, &LiveCapture::openCapture_triggered);
|
|
QObject::connect(&newAction, &QAction::triggered, this, &LiveCapture::openNewWindow_triggered);
|
|
QObject::connect(&contextSaveAction, &QAction::triggered, this,
|
|
&LiveCapture::saveCapture_triggered);
|
|
QObject::connect(&contextDeleteAction, &QAction::triggered, this,
|
|
&LiveCapture::deleteCapture_triggered);
|
|
|
|
m_ContextMenu = &contextMenu;
|
|
RDDialog::show(&contextMenu, QCursor::pos());
|
|
m_ContextMenu = NULL;
|
|
}
|
|
}
|
|
|
|
void LiveCapture::on_captures_itemActivated(QListWidgetItem *item)
|
|
{
|
|
openCapture_triggered();
|
|
}
|
|
|
|
void LiveCapture::on_childProcesses_itemActivated(QListWidgetItem *item)
|
|
{
|
|
QList<QListWidgetItem *> sel = ui->childProcesses->selectedItems();
|
|
if(sel.count() == 1)
|
|
{
|
|
uint32_t ident = sel[0]->data(IdentRole).toUInt();
|
|
if(ident > 0)
|
|
{
|
|
LiveCapture *live =
|
|
new LiveCapture(m_Ctx, m_Hostname, m_HostFriendlyname, ident, m_Main, m_Main);
|
|
m_Main->ShowLiveCapture(live);
|
|
}
|
|
}
|
|
}
|
|
|
|
void LiveCapture::on_queueCap_clicked()
|
|
{
|
|
m_CaptureNumFrames = (int)ui->numFrames->value();
|
|
m_QueueCaptureFrameNum = (int)ui->captureFrame->value();
|
|
m_QueueCapture = true;
|
|
}
|
|
|
|
void LiveCapture::on_triggerImmediateCapture_clicked()
|
|
{
|
|
m_TriggerCapture = true;
|
|
m_CaptureNumFrames = (int)ui->numFrames->value();
|
|
}
|
|
|
|
void LiveCapture::on_triggerDelayedCapture_clicked()
|
|
{
|
|
if(ui->captureDelay->value() == 0.0)
|
|
{
|
|
on_triggerImmediateCapture_clicked();
|
|
}
|
|
else
|
|
{
|
|
m_CaptureCounter = (int)ui->captureDelay->value();
|
|
countdownTimer.start();
|
|
ui->triggerDelayedCapture->setEnabled(false);
|
|
ui->triggerDelayedCapture->setText(tr("Triggering in %1s").arg(m_CaptureCounter));
|
|
}
|
|
}
|
|
|
|
void LiveCapture::openCapture_triggered()
|
|
{
|
|
if(ui->captures->selectedItems().size() == 1)
|
|
openCapture(GetCapture(ui->captures->selectedItems()[0]));
|
|
}
|
|
|
|
void LiveCapture::openNewWindow_triggered()
|
|
{
|
|
if(ui->captures->selectedItems().size() == 1)
|
|
{
|
|
Capture *cap = GetCapture(ui->captures->selectedItems()[0]);
|
|
|
|
QString temppath = m_Ctx.TempCaptureFilename(lit("newwindow"));
|
|
|
|
if(!cap->local)
|
|
{
|
|
RDDialog::critical(this, tr("Cannot open new instance"),
|
|
tr("Can't open capture in new instance with remote server in use"));
|
|
return;
|
|
}
|
|
|
|
QFile f(cap->path);
|
|
|
|
if(!f.copy(temppath))
|
|
{
|
|
RDDialog::critical(this, tr("Cannot save temporary capture"),
|
|
tr("Couldn't save capture to temporary location\n%1").arg(f.errorString()));
|
|
return;
|
|
}
|
|
|
|
QStringList args;
|
|
args << lit("--tempfile") << temppath;
|
|
QProcess::startDetached(qApp->applicationFilePath(), args);
|
|
}
|
|
}
|
|
|
|
void LiveCapture::saveCapture_triggered()
|
|
{
|
|
if(ui->captures->selectedItems().size() == 1)
|
|
saveCapture(GetCapture(ui->captures->selectedItems()[0]));
|
|
}
|
|
|
|
void LiveCapture::deleteCapture_triggered()
|
|
{
|
|
bool allow = checkAllowDelete();
|
|
|
|
if(!allow)
|
|
return;
|
|
|
|
QList<QListWidgetItem *> sel = ui->captures->selectedItems();
|
|
|
|
for(QListWidgetItem *item : sel)
|
|
{
|
|
Capture *cap = GetCapture(item);
|
|
|
|
if(!cap->saved)
|
|
{
|
|
if(cap->path == m_Ctx.GetCaptureFilename())
|
|
{
|
|
m_Main->takeCaptureOwnership();
|
|
m_Main->CloseCapture();
|
|
}
|
|
else
|
|
{
|
|
// if connected, prefer using the live connection
|
|
if(m_Connection && m_Connection->Connected() && !cap->local)
|
|
{
|
|
QMutexLocker l(&m_DeleteCapturesLock);
|
|
m_DeleteCaptures.push_back(cap->remoteID);
|
|
}
|
|
else
|
|
{
|
|
m_Ctx.Replay().DeleteCapture(cap->path, cap->local);
|
|
}
|
|
}
|
|
}
|
|
|
|
delete cap;
|
|
|
|
delete ui->captures->takeItem(ui->captures->row(item));
|
|
}
|
|
}
|
|
|
|
void LiveCapture::childUpdate()
|
|
{
|
|
// first do a small lock and check if the list is currently empty
|
|
{
|
|
QMutexLocker l(&m_ChildrenLock);
|
|
|
|
if(m_Children.empty())
|
|
{
|
|
ui->childProcessLabel->setVisible(false);
|
|
ui->childProcesses->setVisible(false);
|
|
}
|
|
}
|
|
|
|
// enumerate processes outside of the lock
|
|
QProcessList processes = QProcessInfo::enumerate(false);
|
|
|
|
// now since we're adding and removing, we lock around the whole rest of the function. It won't be
|
|
// too slow.
|
|
{
|
|
QMutexLocker l(&m_ChildrenLock);
|
|
|
|
if(!m_Children.empty())
|
|
{
|
|
// remove any stale processes
|
|
for(int i = 0; i < m_Children.size(); i++)
|
|
{
|
|
bool found = false;
|
|
|
|
for(QProcessInfo &p : processes)
|
|
{
|
|
if(p.pid() == m_Children[i].PID)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!found)
|
|
{
|
|
if(m_Children[i].added)
|
|
{
|
|
for(int c = 0; c < ui->childProcesses->count(); c++)
|
|
{
|
|
QListWidgetItem *item = ui->childProcesses->item(c);
|
|
if(item->data(PIDRole).toUInt() == m_Children[i].PID)
|
|
delete ui->childProcesses->takeItem(c);
|
|
}
|
|
}
|
|
|
|
// process expired/doesn't exist anymore
|
|
m_Children.removeAt(i);
|
|
|
|
// don't increment i, check the next element at i (if we weren't at the end
|
|
i--;
|
|
}
|
|
}
|
|
|
|
for(int i = 0; i < m_Children.size(); i++)
|
|
{
|
|
if(!m_Children[i].added)
|
|
{
|
|
QString name = tr("Unknown Process");
|
|
|
|
// find the name
|
|
for(QProcessInfo &p : processes)
|
|
{
|
|
if(p.pid() == m_Children[i].PID)
|
|
{
|
|
name = p.name();
|
|
break;
|
|
}
|
|
}
|
|
|
|
QString text = QFormatStr("%1 [PID %2]").arg(name).arg(m_Children[i].PID);
|
|
|
|
m_Children[i].added = true;
|
|
QListWidgetItem *item = new QListWidgetItem(text, ui->childProcesses);
|
|
item->setData(PIDRole, QVariant(m_Children[i].PID));
|
|
item->setData(IdentRole, QVariant(m_Children[i].ident));
|
|
ui->childProcesses->addItem(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!m_Children.empty())
|
|
{
|
|
ui->childProcessLabel->setVisible(true);
|
|
ui->childProcesses->setVisible(true);
|
|
}
|
|
else
|
|
{
|
|
ui->childProcessLabel->setVisible(false);
|
|
ui->childProcesses->setVisible(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void LiveCapture::captureCountdownTick()
|
|
{
|
|
m_CaptureCounter--;
|
|
|
|
if(m_CaptureCounter == 0)
|
|
{
|
|
m_TriggerCapture = true;
|
|
m_CaptureNumFrames = (int)ui->numFrames->value();
|
|
ui->triggerDelayedCapture->setEnabled(true);
|
|
ui->triggerDelayedCapture->setText(tr("Trigger After Delay"));
|
|
}
|
|
else
|
|
{
|
|
countdownTimer.start();
|
|
ui->triggerDelayedCapture->setText(tr("Triggering in %1s").arg(m_CaptureCounter));
|
|
}
|
|
}
|
|
|
|
void LiveCapture::killThread()
|
|
{
|
|
if(m_ConnectThread)
|
|
{
|
|
m_Disconnect.acquire();
|
|
m_ConnectThread->wait();
|
|
m_ConnectThread->deleteLater();
|
|
}
|
|
}
|
|
|
|
void LiveCapture::setTitle(const QString &title)
|
|
{
|
|
setWindowTitle((!m_HostFriendlyname.isEmpty() ? (m_HostFriendlyname + lit(" - ")) : QString()) +
|
|
title);
|
|
}
|
|
|
|
LiveCapture::Capture *LiveCapture::GetCapture(QListWidgetItem *item)
|
|
{
|
|
return (Capture *)item->data(CapPtrRole).value<void *>();
|
|
}
|
|
|
|
void LiveCapture::AddCapture(QListWidgetItem *item, Capture *cap)
|
|
{
|
|
item->setData(CapPtrRole, QVariant::fromValue<void *>(cap));
|
|
}
|
|
|
|
QImage LiveCapture::MakeThumb(const QImage &screenshot)
|
|
{
|
|
const QSizeF thumbSize(ui->captures->iconSize());
|
|
const QSizeF imSize(screenshot.size());
|
|
|
|
float x = 0, y = 0;
|
|
float width = 0, height = 0;
|
|
|
|
const float srcaspect = imSize.width() / imSize.height();
|
|
const float dstaspect = thumbSize.width() / thumbSize.height();
|
|
|
|
if(srcaspect > dstaspect)
|
|
{
|
|
width = thumbSize.width();
|
|
height = width / srcaspect;
|
|
|
|
y = (thumbSize.height() - height) / 2;
|
|
}
|
|
else
|
|
{
|
|
height = thumbSize.height();
|
|
width = height * srcaspect;
|
|
|
|
x = (thumbSize.width() - width) / 2;
|
|
}
|
|
|
|
QImage ret(thumbSize.width(), thumbSize.height(), QImage::Format_RGBA8888);
|
|
ret.fill(Qt::transparent);
|
|
QPainter paint(&ret);
|
|
paint.drawImage(QRectF(x, y, width, height),
|
|
screenshot.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
|
|
return ret;
|
|
}
|
|
|
|
bool LiveCapture::checkAllowDelete()
|
|
{
|
|
bool needcheck = false;
|
|
|
|
for(int i = 0; i < ui->captures->count(); i++)
|
|
{
|
|
Capture *cap = GetCapture(ui->captures->item(i));
|
|
needcheck |= !cap->saved;
|
|
}
|
|
|
|
if(!needcheck || ui->captures->selectedItems().empty())
|
|
return true;
|
|
|
|
ToolWindowManager::raiseToolWindow(this);
|
|
|
|
QMessageBox::StandardButton res =
|
|
RDDialog::question(this, tr("Unsaved capture(s)", "", ui->captures->selectedItems().size()),
|
|
tr("Are you sure you wish to delete the capture(s)?\nAny capture "
|
|
"currently opened will be closed",
|
|
"", ui->captures->selectedItems().size()),
|
|
RDDialog::YesNoCancel);
|
|
|
|
return (res == QMessageBox::Yes);
|
|
}
|
|
|
|
void LiveCapture::updateAPIStatus()
|
|
{
|
|
QString apiStatus;
|
|
|
|
bool nonpresenting = false;
|
|
|
|
// add any fully working APIs first in the list.
|
|
for(QString api : m_APIs.keys())
|
|
{
|
|
if(m_APIs[api].supported && m_APIs[api].presenting)
|
|
apiStatus += lit(", <b>%1</b>").arg(api);
|
|
}
|
|
|
|
// then add any problem APIs
|
|
for(QString api : m_APIs.keys())
|
|
{
|
|
if(!m_APIs[api].supported)
|
|
{
|
|
apiStatus += tr(", %1 (Unsupported)").arg(api);
|
|
}
|
|
else if(!m_APIs[api].presenting)
|
|
{
|
|
apiStatus += tr(", %1 (Not Presenting)").arg(api);
|
|
nonpresenting = true;
|
|
}
|
|
}
|
|
|
|
// remove the redundant starting ", "
|
|
apiStatus.remove(0, 2);
|
|
|
|
ui->apiStatus->setText(apiStatus);
|
|
|
|
ui->apiIcon->setVisible(nonpresenting);
|
|
}
|
|
|
|
QString LiveCapture::MakeText(Capture *cap)
|
|
{
|
|
QString text = cap->name;
|
|
if(!cap->local)
|
|
text += tr(" (Remote)");
|
|
|
|
text += lit("\n") + cap->api;
|
|
text += lit("\n") + cap->timestamp.toString(lit("yyyy-MM-dd HH:mm:ss"));
|
|
|
|
return text;
|
|
}
|
|
|
|
bool LiveCapture::checkAllowClose()
|
|
{
|
|
m_IgnoreThreadClosed = true;
|
|
|
|
bool suppressRemoteWarning = false;
|
|
|
|
for(int i = 0; i < ui->captures->count(); i++)
|
|
{
|
|
QListWidgetItem *item = ui->captures->item(i);
|
|
Capture *cap = GetCapture(ui->captures->item(i));
|
|
|
|
if(cap->saved)
|
|
continue;
|
|
|
|
ui->captures->clearSelection();
|
|
ToolWindowManager::raiseToolWindow(this);
|
|
ui->captures->setFocus();
|
|
item->setSelected(true);
|
|
|
|
QMessageBox::StandardButton res = QMessageBox::No;
|
|
|
|
if(!suppressRemoteWarning)
|
|
{
|
|
res = RDDialog::question(this, tr("Unsaved capture"),
|
|
tr("Save this capture '%1' at %2?")
|
|
.arg(cap->name)
|
|
.arg(cap->timestamp.toString(lit("HH:mm:ss"))),
|
|
RDDialog::YesNoCancel);
|
|
}
|
|
|
|
if(res == QMessageBox::Cancel)
|
|
{
|
|
m_IgnoreThreadClosed = false;
|
|
return false;
|
|
}
|
|
|
|
// we either have to save or delete the capture. Make sure that if it's remote that we are able
|
|
// to by having an active connection or replay context on that host.
|
|
if(suppressRemoteWarning == false && (!m_Connection || !m_Connection->Connected()) &&
|
|
!cap->local && (!m_Ctx.Replay().CurrentRemote() ||
|
|
QString(m_Ctx.Replay().CurrentRemote()->hostname) != m_Hostname ||
|
|
!m_Ctx.Replay().CurrentRemote()->connected))
|
|
{
|
|
QMessageBox::StandardButton res2 = RDDialog::question(
|
|
this, tr("No active replay context"),
|
|
tr("This capture is on remote host %1 and there is no active replay context on that "
|
|
"host.\n")
|
|
.arg(m_HostFriendlyname) +
|
|
tr("Without an active replay context the capture cannot be %1.\n\n")
|
|
.arg(tr(res == QMessageBox::Yes ? "saved" : "deleted")) +
|
|
tr("Would you like to continue and discard this capture and any others, to be left "
|
|
"in the temporary folder on the remote machine?"),
|
|
RDDialog::YesNoCancel);
|
|
|
|
if(res2 == QMessageBox::Yes)
|
|
{
|
|
suppressRemoteWarning = true;
|
|
res = QMessageBox::No;
|
|
}
|
|
else
|
|
{
|
|
m_IgnoreThreadClosed = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if(res == QMessageBox::Yes)
|
|
{
|
|
bool success = saveCapture(cap);
|
|
|
|
if(!success)
|
|
{
|
|
m_IgnoreThreadClosed = false;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_IgnoreThreadClosed = false;
|
|
return true;
|
|
}
|
|
|
|
void LiveCapture::openCapture(Capture *cap)
|
|
{
|
|
cap->opened = true;
|
|
|
|
if(!cap->local && (!m_Ctx.Replay().CurrentRemote() ||
|
|
QString(m_Ctx.Replay().CurrentRemote()->hostname) != m_Hostname ||
|
|
!m_Ctx.Replay().CurrentRemote()->connected))
|
|
{
|
|
RDDialog::critical(
|
|
this, tr("No active replay context"),
|
|
tr("This capture is on remote host %1 and there is no active replay context on that "
|
|
"host.\nYou can either save the capture locally, or switch to a replay context on %1.")
|
|
.arg(m_HostFriendlyname));
|
|
return;
|
|
}
|
|
|
|
m_Main->LoadCapture(cap->path, !cap->saved, cap->local);
|
|
}
|
|
|
|
bool LiveCapture::saveCapture(Capture *cap)
|
|
{
|
|
QString path = m_Main->GetSavePath();
|
|
|
|
if(QString(m_Ctx.GetCaptureFilename()) == path)
|
|
{
|
|
RDDialog::critical(this, tr("Cannot save"), tr("Can't overwrite currently open capture at %1\n"
|
|
"Close the capture or save to another location.")
|
|
.arg(path));
|
|
return false;
|
|
}
|
|
|
|
// we copy the temp capture to the desired path, but the capture item remains referring to the
|
|
// temp path.
|
|
// This ensures that if the user deletes the saved path we can still open or re-save it.
|
|
if(!path.isEmpty())
|
|
{
|
|
if(cap->local)
|
|
{
|
|
QFile src(cap->path);
|
|
QFile dst(path);
|
|
|
|
// remove any existing file, the user was already prompted to overwrite
|
|
if(dst.exists())
|
|
{
|
|
if(!dst.remove())
|
|
{
|
|
RDDialog::critical(this, tr("Cannot save"),
|
|
tr("Couldn't remove file at %1\n%2").arg(path).arg(dst.errorString()));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if(!src.copy(path))
|
|
{
|
|
RDDialog::critical(this, tr("Cannot save"),
|
|
tr("Couldn't copy file to %1\n%2").arg(path).arg(src.errorString()));
|
|
return false;
|
|
}
|
|
}
|
|
else if(m_Connection && m_Connection->Connected())
|
|
{
|
|
// if we have a current live connection, prefer using it
|
|
m_CopyCaptureLocalPath = path;
|
|
m_CopyCaptureID = cap->remoteID;
|
|
}
|
|
else
|
|
{
|
|
if(!m_Ctx.Replay().CurrentRemote() ||
|
|
QString(m_Ctx.Replay().CurrentRemote()->hostname) != m_Hostname ||
|
|
!m_Ctx.Replay().CurrentRemote()->connected)
|
|
{
|
|
RDDialog::critical(this, tr("No active replay context"),
|
|
tr("This capture is on remote host %1 and there is no active replay "
|
|
"context on that host.\n") +
|
|
tr("Without an active replay context the capture cannot be saved, "
|
|
"try switching to a replay context on %1.")
|
|
.arg(m_Hostname));
|
|
return false;
|
|
}
|
|
|
|
m_Ctx.Replay().CopyCaptureFromRemote(cap->path, path, this);
|
|
|
|
if(!QFile::exists(path))
|
|
{
|
|
RDDialog::critical(this, tr("Cannot save"),
|
|
tr("File couldn't be transferred from remote host"));
|
|
return false;
|
|
}
|
|
|
|
m_Ctx.Replay().DeleteCapture(cap->path, false);
|
|
}
|
|
|
|
// delete the temporary copy
|
|
if(!cap->saved)
|
|
m_Ctx.Replay().DeleteCapture(cap->path, cap->local);
|
|
|
|
cap->saved = true;
|
|
cap->path = path;
|
|
AddRecentFile(m_Ctx.Config().RecentCaptureFiles, path, 10);
|
|
m_Main->PopulateRecentCaptureFiles();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void LiveCapture::cleanItems()
|
|
{
|
|
for(int i = 0; i < ui->captures->count(); i++)
|
|
{
|
|
Capture *cap = GetCapture(ui->captures->item(i));
|
|
|
|
if(!cap->saved)
|
|
{
|
|
if(cap->path == m_Ctx.GetCaptureFilename())
|
|
{
|
|
m_Main->takeCaptureOwnership();
|
|
}
|
|
else
|
|
{
|
|
// if connected, prefer using the live connection
|
|
if(m_Connection && m_Connection->Connected() && !cap->local)
|
|
{
|
|
QMutexLocker l(&m_DeleteCapturesLock);
|
|
m_DeleteCaptures.push_back(cap->remoteID);
|
|
}
|
|
else
|
|
{
|
|
m_Ctx.Replay().DeleteCapture(cap->path, cap->local);
|
|
}
|
|
}
|
|
}
|
|
|
|
delete cap;
|
|
}
|
|
ui->captures->clear();
|
|
}
|
|
|
|
void LiveCapture::previewToggle_toggled(bool checked)
|
|
{
|
|
if(m_IgnorePreviewToggle)
|
|
return;
|
|
|
|
if(checked)
|
|
ui->previewSplit->setSizes({1, 1});
|
|
else
|
|
ui->previewSplit->setSizes({1, 0});
|
|
}
|
|
|
|
void LiveCapture::on_previewSplit_splitterMoved(int pos, int index)
|
|
{
|
|
m_IgnorePreviewToggle = true;
|
|
|
|
QList<int> sizes = ui->previewSplit->sizes();
|
|
|
|
previewToggle->setChecked(sizes[1] != 0);
|
|
|
|
m_IgnorePreviewToggle = false;
|
|
}
|
|
|
|
void LiveCapture::on_apiIcon_clicked(QMouseEvent *event)
|
|
{
|
|
QDesktopServices::openUrl(QUrl(lit("https://renderdoc.org/docs/in_application_api.html")));
|
|
}
|
|
|
|
void LiveCapture::captures_keyPress(QKeyEvent *e)
|
|
{
|
|
if(e->key() == Qt::Key_Delete)
|
|
{
|
|
deleteCapture_triggered();
|
|
}
|
|
}
|
|
|
|
void LiveCapture::preview_mouseClick(QMouseEvent *e)
|
|
{
|
|
QPoint mouse = QCursor::pos();
|
|
if(e->buttons() & Qt::LeftButton)
|
|
{
|
|
previewDragStart = mouse;
|
|
ui->preview->setCursor(QCursor(Qt::SizeAllCursor));
|
|
}
|
|
}
|
|
|
|
void LiveCapture::preview_mouseMove(QMouseEvent *e)
|
|
{
|
|
QPoint mouse = QCursor::pos();
|
|
if(e->buttons() & Qt::LeftButton)
|
|
{
|
|
QScrollBar *h = ui->previewScroll->horizontalScrollBar();
|
|
QScrollBar *v = ui->previewScroll->verticalScrollBar();
|
|
|
|
h->setValue(h->value() + previewDragStart.x() - mouse.x());
|
|
v->setValue(v->value() + previewDragStart.y() - mouse.y());
|
|
|
|
previewDragStart = mouse;
|
|
}
|
|
else
|
|
{
|
|
ui->preview->unsetCursor();
|
|
}
|
|
}
|
|
|
|
void LiveCapture::on_captures_itemSelectionChanged()
|
|
{
|
|
int numSelected = ui->captures->selectedItems().size();
|
|
|
|
deleteAction->setEnabled(numSelected == 1);
|
|
saveAction->setEnabled(numSelected == 1);
|
|
openButton->setEnabled(numSelected == 1);
|
|
|
|
if(ui->captures->selectedItems().size() == 1)
|
|
{
|
|
QListWidgetItem *item = ui->captures->selectedItems()[0];
|
|
Capture *cap = GetCapture(item);
|
|
|
|
newWindowAction->setEnabled(cap->local);
|
|
|
|
if(cap->thumb.width() > 0)
|
|
{
|
|
ui->preview->setPixmap(QPixmap::fromImage(cap->thumb));
|
|
ui->preview->setMinimumSize(cap->thumb.size());
|
|
ui->preview->setMaximumSize(cap->thumb.size());
|
|
}
|
|
else
|
|
{
|
|
ui->preview->setPixmap(QPixmap());
|
|
ui->preview->setMinimumSize(QSize(16, 16));
|
|
ui->preview->setMaximumSize(QSize(16, 16));
|
|
}
|
|
}
|
|
}
|
|
|
|
void LiveCapture::captureCopied(uint32_t ID, const QString &localPath)
|
|
{
|
|
for(int i = 0; i < ui->captures->count(); i++)
|
|
{
|
|
QListWidgetItem *item = ui->captures->item(i);
|
|
Capture *cap = GetCapture(ui->captures->item(i));
|
|
|
|
if(cap && cap->remoteID == ID)
|
|
{
|
|
cap->local = true;
|
|
cap->path = localPath;
|
|
QFont f = item->font();
|
|
f.setItalic(false);
|
|
item->setFont(f);
|
|
item->setText(MakeText(cap));
|
|
}
|
|
}
|
|
}
|
|
|
|
void LiveCapture::captureAdded(uint32_t ID, const QString &executable, const QString &api,
|
|
const bytebuf &thumbnail, int32_t thumbWidth, int32_t thumbHeight,
|
|
QDateTime timestamp, const QString &path, bool local)
|
|
{
|
|
Capture *cap = new Capture();
|
|
cap->remoteID = ID;
|
|
cap->name = executable;
|
|
cap->api = api;
|
|
cap->timestamp = timestamp;
|
|
cap->thumb =
|
|
QImage(thumbnail.data(), thumbWidth, thumbHeight, thumbWidth * 3, QImage::Format_RGB888)
|
|
.copy(0, 0, thumbWidth, thumbHeight);
|
|
cap->saved = false;
|
|
cap->path = path;
|
|
cap->local = local;
|
|
|
|
QListWidgetItem *item = new QListWidgetItem();
|
|
item->setFlags(item->flags() | Qt::ItemIsEditable);
|
|
item->setText(MakeText(cap));
|
|
item->setIcon(QIcon(QPixmap::fromImage(MakeThumb(cap->thumb))));
|
|
if(!local)
|
|
{
|
|
QFont f = item->font();
|
|
f.setItalic(true);
|
|
item->setFont(f);
|
|
}
|
|
|
|
AddCapture(item, cap);
|
|
|
|
ui->captures->addItem(item);
|
|
}
|
|
|
|
void LiveCapture::connectionClosed()
|
|
{
|
|
if(m_IgnoreThreadClosed)
|
|
return;
|
|
|
|
if(ui->captures->count() <= 1)
|
|
{
|
|
if(ui->captures->count() == 1)
|
|
{
|
|
Capture *cap = GetCapture(ui->captures->item(0));
|
|
|
|
// only auto-open a non-local capture if we are successfully connected
|
|
// to this machine as a remote context
|
|
if(!cap->local)
|
|
{
|
|
if(!m_Ctx.Replay().CurrentRemote() ||
|
|
QString(m_Ctx.Replay().CurrentRemote()->hostname) != m_Hostname ||
|
|
!m_Ctx.Replay().CurrentRemote()->connected)
|
|
return;
|
|
}
|
|
|
|
if(cap->opened)
|
|
return;
|
|
|
|
openCapture(cap);
|
|
if(!cap->saved)
|
|
{
|
|
cap->saved = true;
|
|
m_Main->takeCaptureOwnership();
|
|
}
|
|
}
|
|
|
|
// auto-close and load capture if we got a capture. If we
|
|
// don't have any captures but DO have child processes,
|
|
// then don't close just yet.
|
|
if(ui->captures->count() == 1 || m_Children.count() == 0)
|
|
{
|
|
// raise the texture viewer if it exists, instead of falling back to most likely the capture
|
|
// executable dialog which is not useful.
|
|
if(ui->captures->count() == 1 && m_Ctx.HasTextureViewer())
|
|
m_Ctx.ShowTextureViewer();
|
|
selfClose();
|
|
return;
|
|
}
|
|
|
|
// if we have no captures and only one child, close and
|
|
// open up a connection to it (similar to behaviour with
|
|
// only one capture
|
|
if(ui->captures->count() == 0 && m_Children.count() == 1)
|
|
{
|
|
LiveCapture *live =
|
|
new LiveCapture(m_Ctx, m_Hostname, m_HostFriendlyname, m_Children[0].ident, m_Main);
|
|
m_Main->ShowLiveCapture(live);
|
|
selfClose();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void LiveCapture::selfClose()
|
|
{
|
|
if(m_ContextMenu)
|
|
{
|
|
qInfo() << "preventing race";
|
|
// hide the menu and close our window shortly after
|
|
m_ContextMenu->close();
|
|
QTimer *timer = new QTimer(this);
|
|
QObject::connect(timer, &QTimer::timeout, [this]() { ToolWindowManager::closeToolWindow(this); });
|
|
timer->setSingleShot(true);
|
|
timer->start(250);
|
|
}
|
|
else
|
|
{
|
|
ToolWindowManager::closeToolWindow(this);
|
|
}
|
|
}
|
|
|
|
void LiveCapture::connectionThreadEntry()
|
|
{
|
|
m_Connection = RENDERDOC_CreateTargetControl(m_Hostname.toUtf8().data(), m_RemoteIdent,
|
|
GetSystemUsername().toUtf8().data(), true);
|
|
|
|
if(!m_Connection || !m_Connection->Connected())
|
|
{
|
|
GUIInvoke::call(this, [this]() {
|
|
setTitle(tr("Connection failed"));
|
|
ui->connectionStatus->setText(tr("Failed"));
|
|
ui->connectionIcon->setPixmap(Pixmaps::del(ui->connectionIcon));
|
|
|
|
connectionClosed();
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
GUIInvoke::call(this, [this]() {
|
|
uint32_t pid = m_Connection->GetPID();
|
|
QString target = QString::fromUtf8(m_Connection->GetTarget());
|
|
if(pid)
|
|
setTitle(QFormatStr("%1 [PID %2]").arg(target).arg(pid));
|
|
else
|
|
setTitle(target);
|
|
|
|
ui->target->setText(windowTitle());
|
|
ui->connectionIcon->setPixmap(Pixmaps::connect(ui->connectionIcon));
|
|
ui->connectionStatus->setText(tr("Established"));
|
|
});
|
|
|
|
while(m_Connection && m_Connection->Connected())
|
|
{
|
|
if(m_TriggerCapture)
|
|
{
|
|
m_Connection->TriggerCapture((uint)m_CaptureNumFrames);
|
|
m_TriggerCapture = false;
|
|
m_CaptureNumFrames = 1;
|
|
}
|
|
|
|
if(m_QueueCapture)
|
|
{
|
|
m_Connection->QueueCapture((uint32_t)m_QueueCaptureFrameNum, (uint32_t)m_CaptureNumFrames);
|
|
m_QueueCapture = false;
|
|
m_QueueCaptureFrameNum = 0;
|
|
m_CaptureNumFrames = 1;
|
|
}
|
|
|
|
if(!m_CopyCaptureLocalPath.isEmpty())
|
|
{
|
|
m_Connection->CopyCapture(m_CopyCaptureID, m_CopyCaptureLocalPath.toUtf8().data());
|
|
m_CopyCaptureLocalPath = QString();
|
|
m_CopyCaptureID = ~0U;
|
|
}
|
|
|
|
QVector<uint32_t> dels;
|
|
{
|
|
QMutexLocker l(&m_DeleteCapturesLock);
|
|
dels.swap(m_DeleteCaptures);
|
|
}
|
|
|
|
for(uint32_t del : dels)
|
|
m_Connection->DeleteCapture(del);
|
|
|
|
if(!m_Disconnect.available())
|
|
{
|
|
m_Connection->Shutdown();
|
|
m_Connection = NULL;
|
|
return;
|
|
}
|
|
|
|
TargetControlMessage msg = m_Connection->ReceiveMessage();
|
|
|
|
if(msg.type == TargetControlMessageType::RegisterAPI)
|
|
{
|
|
QString api = msg.apiUse.name;
|
|
bool presenting = msg.apiUse.presenting;
|
|
bool supported = msg.apiUse.supported;
|
|
GUIInvoke::call(this, [this, api, presenting, supported]() {
|
|
m_APIs[api] = APIStatus(presenting, supported);
|
|
|
|
if(presenting && supported)
|
|
{
|
|
ui->triggerImmediateCapture->setEnabled(true);
|
|
ui->triggerDelayedCapture->setEnabled(true);
|
|
ui->queueCap->setEnabled(true);
|
|
}
|
|
|
|
updateAPIStatus();
|
|
});
|
|
}
|
|
|
|
if(msg.type == TargetControlMessageType::CaptureProgress)
|
|
{
|
|
float progress = msg.capProgress;
|
|
GUIInvoke::call(this, [this, progress]() {
|
|
|
|
if(progress >= 0.0f && progress < 1.0f)
|
|
{
|
|
ui->captureProgressLabel->setVisible(true);
|
|
ui->captureProgress->setVisible(true);
|
|
ui->captureProgress->setMaximum(1000);
|
|
ui->captureProgress->setValue(1000 * progress);
|
|
}
|
|
else
|
|
{
|
|
ui->captureProgressLabel->setVisible(false);
|
|
ui->captureProgress->setVisible(false);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
if(msg.type == TargetControlMessageType::NewCapture)
|
|
{
|
|
uint32_t capID = msg.newCapture.captureId;
|
|
QDateTime timestamp = QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0));
|
|
timestamp = timestamp.addSecs(msg.newCapture.timestamp).toLocalTime();
|
|
bytebuf thumb = msg.newCapture.thumbnail;
|
|
int32_t thumbWidth = msg.newCapture.thumbWidth;
|
|
int32_t thumbHeight = msg.newCapture.thumbHeight;
|
|
QString path = msg.newCapture.path;
|
|
QString captureAPI = msg.newCapture.api;
|
|
bool local = msg.newCapture.local;
|
|
|
|
GUIInvoke::call(
|
|
this, [this, capID, timestamp, thumb, thumbWidth, thumbHeight, path, captureAPI, local]() {
|
|
QString target = QString::fromUtf8(m_Connection->GetTarget());
|
|
|
|
QString api = captureAPI;
|
|
if(api.isEmpty())
|
|
api = QString::fromUtf8(m_Connection->GetAPI());
|
|
|
|
captureAdded(capID, target, api, thumb, thumbWidth, thumbHeight, timestamp, path, local);
|
|
});
|
|
}
|
|
|
|
if(msg.type == TargetControlMessageType::CaptureCopied)
|
|
{
|
|
uint32_t capID = msg.newCapture.captureId;
|
|
QString path = msg.newCapture.path;
|
|
|
|
GUIInvoke::call(this, [this, capID, path]() { captureCopied(capID, path); });
|
|
}
|
|
|
|
if(msg.type == TargetControlMessageType::NewChild)
|
|
{
|
|
if(msg.newChild.processId != 0)
|
|
{
|
|
ChildProcess c;
|
|
c.PID = (int)msg.newChild.processId;
|
|
c.ident = msg.newChild.ident;
|
|
|
|
{
|
|
QMutexLocker l(&m_ChildrenLock);
|
|
m_Children.push_back(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
GUIInvoke::call(this, [this]() {
|
|
ui->connectionStatus->setText(tr("Closed"));
|
|
ui->connectionIcon->setPixmap(Pixmaps::disconnect(ui->connectionIcon));
|
|
|
|
ui->numFrames->setEnabled(false);
|
|
ui->captureDelay->setEnabled(false);
|
|
ui->captureFrame->setEnabled(false);
|
|
ui->triggerDelayedCapture->setEnabled(false);
|
|
ui->triggerImmediateCapture->setEnabled(false);
|
|
ui->queueCap->setEnabled(false);
|
|
|
|
ui->apiStatus->setText(tr("None"));
|
|
ui->apiIcon->setVisible(false);
|
|
|
|
connectionClosed();
|
|
});
|
|
}
|