mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-04 17:10:47 +00:00
369d93b99c
A new button is added to the UI so that we can cycle the currently active window when there are more windows to capture. It's like pressing the F11 button but it works on Android too.
1292 lines
36 KiB
C++
1292 lines
36 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->cycleActiveWindow->setEnabled(false);
|
|
|
|
ui->target->setText(QString());
|
|
|
|
ui->progressLabel->setVisible(false);
|
|
ui->progressBar->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_cycleActiveWindow_clicked()
|
|
{
|
|
m_Connection->CycleActiveWindow();
|
|
}
|
|
|
|
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 (Active)</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;
|
|
bool notoall = false;
|
|
|
|
QMessageBox::StandardButtons msgFlags = RDDialog::YesNoCancel;
|
|
|
|
if(ui->captures->count() > 1)
|
|
msgFlags |= QMessageBox::NoToAll;
|
|
|
|
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 && !notoall)
|
|
{
|
|
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"))),
|
|
msgFlags);
|
|
|
|
if(res == QMessageBox::NoToAll)
|
|
{
|
|
notoall = true;
|
|
res = QMessageBox::No;
|
|
}
|
|
}
|
|
|
|
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([this](float progress) {
|
|
GUIInvoke::call(this, [this, progress]() {
|
|
if(progress >= 0.0f && progress < 1.0f)
|
|
{
|
|
ui->progressLabel->setText(tr("Copy in Progress:"));
|
|
ui->progressLabel->setVisible(true);
|
|
ui->progressBar->setVisible(true);
|
|
ui->progressBar->setMaximum(1000);
|
|
ui->progressBar->setValue(1000 * progress);
|
|
}
|
|
else
|
|
{
|
|
ui->progressLabel->setVisible(false);
|
|
ui->progressBar->setVisible(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
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->progressLabel->setText(tr("Capture in Progress:"));
|
|
ui->progressLabel->setVisible(true);
|
|
ui->progressBar->setVisible(true);
|
|
ui->progressBar->setMaximum(1000);
|
|
ui->progressBar->setValue(1000 * progress);
|
|
}
|
|
else
|
|
{
|
|
ui->progressLabel->setVisible(false);
|
|
ui->progressBar->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);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(msg.type == TargetControlMessageType::CapturableWindowCount)
|
|
{
|
|
uint32_t windows = msg.capturableWindowCount;
|
|
GUIInvoke::call(this, [this, windows]() { ui->cycleActiveWindow->setEnabled(windows > 1); });
|
|
}
|
|
}
|
|
|
|
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->cycleActiveWindow->setEnabled(false);
|
|
|
|
ui->apiStatus->setText(tr("None"));
|
|
ui->apiIcon->setVisible(false);
|
|
|
|
connectionClosed();
|
|
});
|
|
}
|