mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-05 01:20:42 +00:00
33ff48811b
* Log is an overloaded term since it can also mean the debug log. We now consistently refer to capture files as capture files or just captures for short. The log is just for log messages and diagnostics. * The user-facing UI was mostly already consistent, but many of the public interfaces exposed to python needed to be renamed, and it made more sense just to make everything consistent.
1166 lines
32 KiB
C++
1166 lines
32 KiB
C++
/******************************************************************************
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2016-2017 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 <QMenu>
|
|
#include <QMetaProperty>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QProcess>
|
|
#include <QScrollBar>
|
|
#include <QStyledItemDelegate>
|
|
#include <QToolBar>
|
|
#include <QToolButton>
|
|
#include "3rdparty/toolwindowmanager/ToolWindowManager.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->captures->setItemDelegate(new NameEditOnlyDelegate(this));
|
|
|
|
{
|
|
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)
|
|
{
|
|
m_CaptureFrameNum = frameNumber;
|
|
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())
|
|
{
|
|
QListWidgetItem *item = ui->captures->itemAt(e->pos());
|
|
|
|
if(item != NULL)
|
|
{
|
|
ui->captures->clearSelection();
|
|
item->setSelected(true);
|
|
}
|
|
|
|
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_CaptureFrameNum = (int)ui->captureFrame->value();
|
|
m_QueueCapture = true;
|
|
}
|
|
|
|
void LiveCapture::on_triggerCapture_clicked()
|
|
{
|
|
m_CaptureNumFrames = (int)ui->numFrames->value();
|
|
if(ui->captureDelay->value() == 0.0)
|
|
{
|
|
m_TriggerCapture = true;
|
|
}
|
|
else
|
|
{
|
|
m_CaptureCounter = (int)ui->captureDelay->value();
|
|
countdownTimer.start();
|
|
ui->triggerCapture->setEnabled(false);
|
|
ui->triggerCapture->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();
|
|
|
|
// 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;
|
|
ui->triggerCapture->setEnabled(true);
|
|
ui->triggerCapture->setText(tr("Trigger Capture"));
|
|
}
|
|
else
|
|
{
|
|
countdownTimer.start();
|
|
ui->triggerCapture->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);
|
|
}
|
|
|
|
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() || 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;
|
|
}
|
|
}
|
|
|
|
if(res == QMessageBox::No)
|
|
{
|
|
// treat this capture as saved now, so we don't prompt about it again.
|
|
cap->saved = true;
|
|
}
|
|
}
|
|
|
|
m_IgnoreThreadClosed = false;
|
|
return true;
|
|
}
|
|
|
|
void LiveCapture::openCapture(Capture *cap)
|
|
{
|
|
cap->opened = true;
|
|
|
|
if(!cap->local &&
|
|
(!m_Ctx.Replay().CurrentRemote() || 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();
|
|
|
|
// 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() || 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::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, 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() || 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)
|
|
{
|
|
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]() {
|
|
setTitle(tr("Connection failed"));
|
|
ui->connectionStatus->setText(tr("Connection failed"));
|
|
ui->connectionIcon->setPixmap(Pixmaps::del(ui->connectionIcon));
|
|
|
|
connectionClosed();
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
GUIInvoke::call([this]() {
|
|
QString api = QString::fromUtf8(m_Connection->GetAPI());
|
|
if(api.isEmpty())
|
|
api = tr("No API detected");
|
|
|
|
QString target = QString::fromUtf8(m_Connection->GetTarget());
|
|
uint32_t pid = m_Connection->GetPID();
|
|
|
|
if(pid == 0)
|
|
{
|
|
ui->connectionStatus->setText(tr("Connection established to %1 (%2)").arg(target).arg(api));
|
|
setTitle(target);
|
|
}
|
|
else
|
|
{
|
|
ui->connectionStatus->setText(
|
|
tr("Connection established to %1 [PID %2] (%3)").arg(target).arg(pid).arg(api));
|
|
setTitle(QFormatStr("%1 [PID %2]").arg(target).arg(pid));
|
|
}
|
|
ui->connectionIcon->setPixmap(Pixmaps::connect(ui->connectionIcon));
|
|
});
|
|
|
|
while(m_Connection && m_Connection->Connected())
|
|
{
|
|
if(m_TriggerCapture)
|
|
{
|
|
m_Connection->TriggerCapture((uint)m_CaptureNumFrames);
|
|
m_TriggerCapture = false;
|
|
}
|
|
|
|
if(m_QueueCapture)
|
|
{
|
|
m_Connection->QueueCapture((uint)m_CaptureFrameNum);
|
|
m_QueueCapture = false;
|
|
m_CaptureFrameNum = 0;
|
|
}
|
|
|
|
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.RegisterAPI.APIName;
|
|
GUIInvoke::call([this, api]() {
|
|
QString target = QString::fromUtf8(m_Connection->GetTarget());
|
|
uint32_t pid = m_Connection->GetPID();
|
|
|
|
if(pid == 0)
|
|
{
|
|
ui->connectionStatus->setText(tr("Connection established to %1 (%2)").arg(target).arg(api));
|
|
setTitle(target);
|
|
}
|
|
else
|
|
{
|
|
ui->connectionStatus->setText(
|
|
tr("Connection established to %1 [PID %2] (%3)").arg(target).arg(pid).arg(api));
|
|
setTitle(QFormatStr("%1 [PID %2]").arg(target).arg(pid));
|
|
}
|
|
ui->connectionIcon->setPixmap(Pixmaps::connect(ui->connectionIcon));
|
|
});
|
|
}
|
|
|
|
if(msg.Type == TargetControlMessageType::NewCapture)
|
|
{
|
|
uint32_t capID = msg.NewCapture.ID;
|
|
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;
|
|
bool local = msg.NewCapture.local;
|
|
|
|
GUIInvoke::call([this, capID, timestamp, thumb, thumbWidth, thumbHeight, path, local]() {
|
|
QString target = QString::fromUtf8(m_Connection->GetTarget());
|
|
QString 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.ID;
|
|
QString path = msg.NewCapture.path;
|
|
|
|
GUIInvoke::call([=]() { captureCopied(capID, path); });
|
|
}
|
|
|
|
if(msg.Type == TargetControlMessageType::NewChild)
|
|
{
|
|
if(msg.NewChild.PID != 0)
|
|
{
|
|
ChildProcess c;
|
|
c.PID = (int)msg.NewChild.PID;
|
|
c.ident = msg.NewChild.ident;
|
|
|
|
{
|
|
QMutexLocker l(&m_ChildrenLock);
|
|
m_Children.push_back(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
GUIInvoke::call([this]() {
|
|
ui->connectionStatus->setText(tr("Connection closed"));
|
|
ui->connectionIcon->setPixmap(Pixmaps::disconnect(ui->connectionIcon));
|
|
|
|
ui->numFrames->setEnabled(false);
|
|
ui->captureDelay->setEnabled(false);
|
|
ui->captureFrame->setEnabled(false);
|
|
ui->triggerCapture->setEnabled(false);
|
|
ui->queueCap->setEnabled(false);
|
|
|
|
connectionClosed();
|
|
});
|
|
}
|