mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-05 01:20:42 +00:00
7c8628b237
* We remove the now unneeded name fields in buffer/texture descriptions and some of the pipeline state structs. * A single function will give the human-readable name for a resource id. This will look up a custom set of renames, on top of the names from the resource descriptions.
1892 lines
54 KiB
C++
1892 lines
54 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 "MainWindow.h"
|
|
#include <QDesktopServices>
|
|
#include <QFileDialog>
|
|
#include <QFileInfo>
|
|
#include <QMimeData>
|
|
#include <QMouseEvent>
|
|
#include <QPixmapCache>
|
|
#include <QProgressBar>
|
|
#include <QProgressDialog>
|
|
#include <QToolButton>
|
|
#include <QToolTip>
|
|
#include "Code/CaptureContext.h"
|
|
#include "Code/QRDUtils.h"
|
|
#include "Code/Resources.h"
|
|
#include "Resources/resource.h"
|
|
#include "Widgets/Extended/RDLabel.h"
|
|
#include "Windows/Dialogs/AboutDialog.h"
|
|
#include "Windows/Dialogs/CaptureDialog.h"
|
|
#include "Windows/Dialogs/LiveCapture.h"
|
|
#include "Windows/Dialogs/RemoteManager.h"
|
|
#include "Windows/Dialogs/SettingsDialog.h"
|
|
#include "Windows/Dialogs/SuggestRemoteDialog.h"
|
|
#include "Windows/Dialogs/TipsDialog.h"
|
|
#include "ui_MainWindow.h"
|
|
#include "version.h"
|
|
|
|
#define JSON_ID "rdocLayoutData"
|
|
#define JSON_VER 1
|
|
|
|
#if defined(Q_OS_WIN32)
|
|
extern "C" void *__stdcall GetModuleHandleA(const char *);
|
|
#endif
|
|
|
|
MainWindow::MainWindow(ICaptureContext &ctx) : QMainWindow(NULL), ui(new Ui::MainWindow), m_Ctx(ctx)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
#if !defined(Q_OS_WIN32)
|
|
// process injection is not supported on non-Windows, so remove the menu item rather than disable
|
|
// it without a clear way to communicate that it is never supported
|
|
ui->menu_File->removeAction(ui->action_Inject_into_Process);
|
|
#endif
|
|
|
|
QToolTip::setPalette(palette());
|
|
|
|
qApp->installEventFilter(this);
|
|
|
|
setAcceptDrops(true);
|
|
|
|
QObject::connect(ui->action_Load_Default_Layout, &QAction::triggered, this,
|
|
&MainWindow::loadLayout_triggered);
|
|
QObject::connect(ui->action_Load_Layout_1, &QAction::triggered, this,
|
|
&MainWindow::loadLayout_triggered);
|
|
QObject::connect(ui->action_Load_Layout_2, &QAction::triggered, this,
|
|
&MainWindow::loadLayout_triggered);
|
|
QObject::connect(ui->action_Load_Layout_3, &QAction::triggered, this,
|
|
&MainWindow::loadLayout_triggered);
|
|
QObject::connect(ui->action_Load_Layout_4, &QAction::triggered, this,
|
|
&MainWindow::loadLayout_triggered);
|
|
QObject::connect(ui->action_Load_Layout_5, &QAction::triggered, this,
|
|
&MainWindow::loadLayout_triggered);
|
|
QObject::connect(ui->action_Load_Layout_6, &QAction::triggered, this,
|
|
&MainWindow::loadLayout_triggered);
|
|
|
|
QObject::connect(ui->action_Save_Default_Layout, &QAction::triggered, this,
|
|
&MainWindow::saveLayout_triggered);
|
|
QObject::connect(ui->action_Save_Layout_1, &QAction::triggered, this,
|
|
&MainWindow::saveLayout_triggered);
|
|
QObject::connect(ui->action_Save_Layout_2, &QAction::triggered, this,
|
|
&MainWindow::saveLayout_triggered);
|
|
QObject::connect(ui->action_Save_Layout_3, &QAction::triggered, this,
|
|
&MainWindow::saveLayout_triggered);
|
|
QObject::connect(ui->action_Save_Layout_4, &QAction::triggered, this,
|
|
&MainWindow::saveLayout_triggered);
|
|
QObject::connect(ui->action_Save_Layout_5, &QAction::triggered, this,
|
|
&MainWindow::saveLayout_triggered);
|
|
QObject::connect(ui->action_Save_Layout_6, &QAction::triggered, this,
|
|
&MainWindow::saveLayout_triggered);
|
|
|
|
QObject::connect(ui->action_Launch_Application_Window, &QAction::triggered, this,
|
|
&MainWindow::on_action_Launch_Application_triggered);
|
|
|
|
QObject::connect(ui->action_Clear_Capture_History, &QAction::triggered, this,
|
|
&MainWindow::ClearRecentCaptures);
|
|
QObject::connect(ui->action_Clear_Log_History, &QAction::triggered, this,
|
|
&MainWindow::ClearRecentFiles);
|
|
|
|
contextChooserMenu = new QMenu(this);
|
|
|
|
FillRemotesMenu(contextChooserMenu, true);
|
|
|
|
contextChooser = new QToolButton(this);
|
|
contextChooser->setText(tr("Replay Context: %1").arg(tr("Local")));
|
|
contextChooser->setIcon(Icons::house());
|
|
contextChooser->setAutoRaise(true);
|
|
contextChooser->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
|
contextChooser->setPopupMode(QToolButton::InstantPopup);
|
|
contextChooser->setMenu(contextChooserMenu);
|
|
contextChooser->setContextMenuPolicy(Qt::DefaultContextMenu);
|
|
QObject::connect(contextChooserMenu, &QMenu::aboutToShow, this,
|
|
&MainWindow::contextChooser_menuShowing);
|
|
|
|
ui->statusBar->addWidget(contextChooser);
|
|
|
|
statusIcon = new RDLabel(this);
|
|
ui->statusBar->addWidget(statusIcon);
|
|
|
|
statusText = new RDLabel(this);
|
|
ui->statusBar->addWidget(statusText);
|
|
|
|
statusProgress = new QProgressBar(this);
|
|
ui->statusBar->addWidget(statusProgress);
|
|
|
|
statusProgress->setVisible(false);
|
|
statusProgress->setMinimumSize(QSize(200, 0));
|
|
statusProgress->setMinimum(0);
|
|
statusProgress->setMaximum(1000);
|
|
|
|
statusIcon->setText(QString());
|
|
statusIcon->setPixmap(QPixmap());
|
|
statusText->setText(QString());
|
|
|
|
QObject::connect(statusIcon, &RDLabel::doubleClicked, this, &MainWindow::statusDoubleClicked);
|
|
QObject::connect(statusText, &RDLabel::doubleClicked, this, &MainWindow::statusDoubleClicked);
|
|
|
|
QObject::connect(&m_MessageTick, &QTimer::timeout, this, &MainWindow::messageCheck);
|
|
m_MessageTick.setSingleShot(false);
|
|
m_MessageTick.setInterval(500);
|
|
m_MessageTick.start();
|
|
|
|
m_RemoteProbeSemaphore.release();
|
|
m_RemoteProbe = new LambdaThread([this]() {
|
|
while(m_RemoteProbeSemaphore.available())
|
|
{
|
|
// do several small sleeps so we can respond quicker when we need to shut down
|
|
for(int i = 0; i < 50; i++)
|
|
{
|
|
QThread::msleep(150);
|
|
if(!m_RemoteProbeSemaphore.available())
|
|
return;
|
|
}
|
|
remoteProbe();
|
|
}
|
|
});
|
|
m_RemoteProbe->start();
|
|
|
|
SetTitle();
|
|
|
|
PopulateRecentFiles();
|
|
PopulateRecentCaptures();
|
|
|
|
ui->toolWindowManager->setToolWindowCreateCallback([this](const QString &objectName) -> QWidget * {
|
|
return m_Ctx.CreateBuiltinWindow(objectName);
|
|
});
|
|
|
|
ui->action_Start_Replay_Loop->setEnabled(false);
|
|
ui->action_Resolve_Symbols->setEnabled(false);
|
|
ui->action_Resolve_Symbols->setText(tr("Resolve Symbols"));
|
|
|
|
LambdaThread *th = new LambdaThread([this]() {
|
|
m_Ctx.Config().AddAndroidHosts();
|
|
for(RemoteHost *host : m_Ctx.Config().RemoteHosts)
|
|
host->CheckStatus();
|
|
});
|
|
th->selfDelete(true);
|
|
th->start();
|
|
|
|
#if defined(Q_OS_WIN32)
|
|
if(GetModuleHandleA("rdocself.dll"))
|
|
{
|
|
QAction *begin = new QAction(tr("Start Self-hosted Capture"), this);
|
|
QAction *end = new QAction(tr("End Self-hosted Capture"), this);
|
|
end->setEnabled(false);
|
|
|
|
QObject::connect(begin, &QAction::triggered, [begin, end]() {
|
|
begin->setEnabled(false);
|
|
end->setEnabled(true);
|
|
|
|
RENDERDOC_StartSelfHostCapture("rdocself.dll");
|
|
});
|
|
|
|
QObject::connect(end, &QAction::triggered, [begin, end]() {
|
|
begin->setEnabled(true);
|
|
end->setEnabled(false);
|
|
|
|
RENDERDOC_EndSelfHostCapture("rdocself.dll");
|
|
});
|
|
|
|
ui->menu_Tools->addSeparator();
|
|
ui->menu_Tools->addAction(begin);
|
|
ui->menu_Tools->addAction(end);
|
|
}
|
|
#endif
|
|
|
|
m_Ctx.AddLogViewer(this);
|
|
|
|
ui->action_Save_Log->setEnabled(false);
|
|
ui->action_Close_Log->setEnabled(false);
|
|
|
|
QList<QAction *> actions = ui->menuBar->actions();
|
|
|
|
// register all the UI-designer created shortcut keys
|
|
for(int i = 0; i < actions.count(); i++)
|
|
{
|
|
QAction *a = actions[i];
|
|
|
|
QKeySequence ks = a->shortcut();
|
|
if(!ks.isEmpty())
|
|
{
|
|
m_GlobalShortcutCallbacks[ks] = [a]() {
|
|
if(a->isEnabled())
|
|
a->trigger();
|
|
};
|
|
}
|
|
|
|
// recurse into submenus by appending to the end of the list.
|
|
if(a->menu())
|
|
actions.append(a->menu()->actions());
|
|
}
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
m_RemoteProbeSemaphore.acquire();
|
|
m_RemoteProbe->wait();
|
|
m_RemoteProbe->deleteLater();
|
|
delete ui;
|
|
}
|
|
|
|
QString MainWindow::GetLayoutPath(int layout)
|
|
{
|
|
QString filename = lit("DefaultLayout.config");
|
|
|
|
if(layout > 0)
|
|
filename = lit("Layout%1.config").arg(layout);
|
|
|
|
return ConfigFilePath(filename);
|
|
}
|
|
|
|
void MainWindow::on_action_Exit_triggered()
|
|
{
|
|
this->close();
|
|
}
|
|
|
|
void MainWindow::on_action_Open_Log_triggered()
|
|
{
|
|
if(!PromptCloseLog())
|
|
return;
|
|
|
|
QString filename = RDDialog::getOpenFileName(
|
|
this, tr("Select Logfile to open"), m_Ctx.Config().LastLogPath,
|
|
tr("Capture Files (*.rdc);;Image Files (*.dds *.hdr *.exr *.bmp *.jpg "
|
|
"*.jpeg *.png *.tga *.gif *.psd;;All Files (*)"));
|
|
|
|
if(!filename.isEmpty())
|
|
LoadFromFilename(filename, false);
|
|
}
|
|
|
|
void MainWindow::LoadFromFilename(const QString &filename, bool temporary)
|
|
{
|
|
QFileInfo path(filename);
|
|
QString ext = path.suffix().toLower();
|
|
|
|
if(ext == lit("rdc"))
|
|
{
|
|
LoadLogfile(filename, temporary, true);
|
|
}
|
|
else if(ext == lit("cap"))
|
|
{
|
|
OpenCaptureConfigFile(filename, false);
|
|
}
|
|
else if(ext == lit("exe"))
|
|
{
|
|
OpenCaptureConfigFile(filename, true);
|
|
}
|
|
else
|
|
{
|
|
// not a recognised filetype, see if we can load it anyway
|
|
LoadLogfile(filename, temporary, true);
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnCaptureTrigger(const QString &exe, const QString &workingDir,
|
|
const QString &cmdLine, const QList<EnvironmentModification> &env,
|
|
CaptureOptions opts, std::function<void(LiveCapture *)> callback)
|
|
{
|
|
if(!PromptCloseLog())
|
|
return;
|
|
|
|
LambdaThread *th = new LambdaThread([this, exe, workingDir, cmdLine, env, opts, callback]() {
|
|
QString logfile = m_Ctx.TempLogFilename(QFileInfo(exe).baseName());
|
|
|
|
uint32_t ret = m_Ctx.Replay().ExecuteAndInject(exe, workingDir, cmdLine, env, logfile, opts);
|
|
|
|
GUIInvoke::call([this, exe, ret, callback]() {
|
|
if(ret == 0)
|
|
{
|
|
RDDialog::critical(this, tr("Error kicking capture"),
|
|
tr("Error launching %1 for capture.\n\nCheck diagnostic log in Help "
|
|
"menu for more details.")
|
|
.arg(exe));
|
|
return;
|
|
}
|
|
|
|
LiveCapture *live = new LiveCapture(
|
|
m_Ctx,
|
|
m_Ctx.Replay().CurrentRemote() ? m_Ctx.Replay().CurrentRemote()->Hostname : QString(),
|
|
m_Ctx.Replay().CurrentRemote() ? m_Ctx.Replay().CurrentRemote()->Name() : QString(), ret,
|
|
this, this);
|
|
ShowLiveCapture(live);
|
|
callback(live);
|
|
});
|
|
});
|
|
th->start();
|
|
// wait a few ms before popping up a progress bar
|
|
th->wait(500);
|
|
if(th->isRunning())
|
|
{
|
|
ShowProgressDialog(this, tr("Launching %1, please wait...").arg(exe),
|
|
[th]() { return !th->isRunning(); });
|
|
}
|
|
th->deleteLater();
|
|
}
|
|
|
|
void MainWindow::OnInjectTrigger(uint32_t PID, const QList<EnvironmentModification> &env,
|
|
const QString &name, CaptureOptions opts,
|
|
std::function<void(LiveCapture *)> callback)
|
|
{
|
|
if(!PromptCloseLog())
|
|
return;
|
|
|
|
rdcarray<EnvironmentModification> envList = env.toVector().toStdVector();
|
|
|
|
LambdaThread *th = new LambdaThread([this, PID, envList, name, opts, callback]() {
|
|
QString logfile = m_Ctx.TempLogFilename(name);
|
|
|
|
uint32_t ret = RENDERDOC_InjectIntoProcess(PID, envList, logfile.toUtf8().data(), opts, false);
|
|
|
|
GUIInvoke::call([this, PID, ret, callback]() {
|
|
if(ret == 0)
|
|
{
|
|
RDDialog::critical(
|
|
this, tr("Error kicking capture"),
|
|
tr("Error injecting into process %1 for capture.\n\nCheck diagnostic log in "
|
|
"Help menu for more details.")
|
|
.arg(PID));
|
|
return;
|
|
}
|
|
|
|
LiveCapture *live = new LiveCapture(m_Ctx, QString(), QString(), ret, this, this);
|
|
ShowLiveCapture(live);
|
|
});
|
|
});
|
|
th->start();
|
|
// wait a few ms before popping up a progress bar
|
|
th->wait(500);
|
|
if(th->isRunning())
|
|
{
|
|
ShowProgressDialog(this, tr("Injecting into %1, please wait...").arg(PID),
|
|
[th]() { return !th->isRunning(); });
|
|
}
|
|
th->deleteLater();
|
|
}
|
|
|
|
void MainWindow::LoadLogfile(const QString &filename, bool temporary, bool local)
|
|
{
|
|
if(PromptCloseLog())
|
|
{
|
|
if(m_Ctx.LogLoading())
|
|
return;
|
|
|
|
QString driver;
|
|
QString machineIdent;
|
|
ReplaySupport support = ReplaySupport::Unsupported;
|
|
|
|
bool remoteReplay =
|
|
!local || (m_Ctx.Replay().CurrentRemote() && m_Ctx.Replay().CurrentRemote()->Connected);
|
|
|
|
if(local)
|
|
{
|
|
ICaptureFile *file = RENDERDOC_OpenCaptureFile();
|
|
|
|
if(file->OpenFile(filename.toUtf8().data(), "rdc") != ReplayStatus::Succeeded)
|
|
{
|
|
RDDialog::critical(NULL, tr("Error opening capture"),
|
|
tr("Couldn't open file '%1'").arg(filename));
|
|
|
|
file->Shutdown();
|
|
return;
|
|
}
|
|
|
|
driver = QString::fromUtf8(file->DriverName());
|
|
machineIdent = QString::fromUtf8(file->RecordedMachineIdent());
|
|
support = file->LocalReplaySupport();
|
|
|
|
file->Shutdown();
|
|
|
|
// if the return value suggests remote replay, and it's not already selected, AND the user
|
|
// hasn't previously chosen to always replay locally without being prompted, ask if they'd
|
|
// prefer to switch to a remote context for replaying.
|
|
if(support == ReplaySupport::SuggestRemote && !remoteReplay &&
|
|
!m_Ctx.Config().AlwaysReplayLocally)
|
|
{
|
|
SuggestRemoteDialog dialog(driver, machineIdent, this);
|
|
|
|
FillRemotesMenu(dialog.remotesMenu(), false);
|
|
|
|
dialog.remotesAdded();
|
|
|
|
RDDialog::show(&dialog);
|
|
|
|
if(dialog.choice() == SuggestRemoteDialog::Cancel)
|
|
{
|
|
return;
|
|
}
|
|
else if(dialog.choice() == SuggestRemoteDialog::Remote)
|
|
{
|
|
// we only get back here from the dialog once the context switch has begun,
|
|
// so contextChooser will have been disabled.
|
|
// Check once to see if it's enabled before even popping up the dialog in case
|
|
// it has finished already. Otherwise pop up a waiting dialog until it completes
|
|
// one way or another, then process the result.
|
|
|
|
if(!contextChooser->isEnabled())
|
|
{
|
|
ShowProgressDialog(this, tr("Please Wait - Checking remote connection..."),
|
|
[this]() { return contextChooser->isEnabled(); });
|
|
}
|
|
|
|
remoteReplay =
|
|
(m_Ctx.Replay().CurrentRemote() && m_Ctx.Replay().CurrentRemote()->Connected);
|
|
|
|
if(!remoteReplay)
|
|
{
|
|
QString remoteMessage = tr("Failed to make a connection to the remote server.\n\n");
|
|
|
|
remoteMessage += tr("More information may be available in the status bar.");
|
|
|
|
RDDialog::information(this, tr("Couldn't connect to remote server"), remoteMessage);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// nothing to do - we just continue replaying locally
|
|
// however we need to check if the user selected 'always replay locally' and
|
|
// set that bit as sticky in the config
|
|
if(dialog.alwaysReplayLocally())
|
|
{
|
|
m_Ctx.Config().AlwaysReplayLocally = true;
|
|
|
|
m_Ctx.Config().Save();
|
|
}
|
|
}
|
|
}
|
|
|
|
if(remoteReplay)
|
|
{
|
|
support = ReplaySupport::Unsupported;
|
|
|
|
QStringList remoteDrivers = m_Ctx.Replay().GetRemoteSupport();
|
|
|
|
for(const QString &d : remoteDrivers)
|
|
{
|
|
if(driver == d)
|
|
support = ReplaySupport::Supported;
|
|
}
|
|
}
|
|
}
|
|
|
|
QString origFilename = filename;
|
|
|
|
// if driver is empty something went wrong loading the log, let it be handled as usual
|
|
// below. Otherwise indicate that support is missing.
|
|
if(!driver.isEmpty() && support == ReplaySupport::Unsupported)
|
|
{
|
|
if(remoteReplay)
|
|
{
|
|
QString remoteMessage =
|
|
tr("This log was captured with %1 and cannot be replayed on %2.\n\n")
|
|
.arg(driver)
|
|
.arg(m_Ctx.Replay().CurrentRemote()->Name());
|
|
|
|
remoteMessage += tr("Try selecting a different remote context in the status bar.");
|
|
|
|
RDDialog::critical(NULL, tr("Unsupported logfile type"), remoteMessage);
|
|
}
|
|
else
|
|
{
|
|
QString remoteMessage =
|
|
tr("This log was captured with %1 and cannot be replayed locally.\n\n").arg(driver);
|
|
|
|
remoteMessage += tr("Try selecting a remote context in the status bar.");
|
|
|
|
RDDialog::critical(NULL, tr("Unsupported logfile type"), remoteMessage);
|
|
}
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
QString fileToLoad = filename;
|
|
|
|
if(remoteReplay && local)
|
|
{
|
|
fileToLoad = m_Ctx.Replay().CopyCaptureToRemote(filename, this);
|
|
|
|
// deliberately leave local as true so that we keep referring to the locally saved log
|
|
|
|
// some error
|
|
if(fileToLoad.isEmpty())
|
|
{
|
|
RDDialog::critical(NULL, tr("Error copying to remote"),
|
|
tr("Couldn't copy %1 to remote host for replaying").arg(filename));
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_Ctx.LoadLogfile(fileToLoad, origFilename, temporary, local);
|
|
}
|
|
|
|
if(!remoteReplay)
|
|
{
|
|
m_Ctx.Config().LastLogPath = QFileInfo(filename).absolutePath();
|
|
}
|
|
|
|
statusText->setText(tr("Loading %1...").arg(origFilename));
|
|
}
|
|
}
|
|
|
|
void MainWindow::OpenCaptureConfigFile(const QString &filename, bool exe)
|
|
{
|
|
ICaptureDialog *capDialog = m_Ctx.GetCaptureDialog();
|
|
|
|
if(exe)
|
|
capDialog->SetExecutableFilename(filename);
|
|
else
|
|
capDialog->LoadSettings(filename);
|
|
|
|
if(!ui->toolWindowManager->toolWindows().contains(capDialog->Widget()))
|
|
ui->toolWindowManager->addToolWindow(capDialog->Widget(), mainToolArea());
|
|
|
|
ToolWindowManager::raiseToolWindow(capDialog->Widget());
|
|
}
|
|
|
|
QString MainWindow::GetSavePath()
|
|
{
|
|
QString dir;
|
|
|
|
if(!m_Ctx.Config().DefaultCaptureSaveDirectory.isEmpty())
|
|
{
|
|
if(m_LastSaveCapturePath.isEmpty())
|
|
dir = m_Ctx.Config().DefaultCaptureSaveDirectory;
|
|
else
|
|
dir = m_LastSaveCapturePath;
|
|
}
|
|
|
|
QString filename =
|
|
RDDialog::getSaveFileName(this, tr("Save Capture As"), dir, tr("Capture Files (*.rdc)"));
|
|
|
|
if(!filename.isEmpty())
|
|
{
|
|
QDir dirinfo = QFileInfo(filename).dir();
|
|
if(dirinfo.exists())
|
|
m_LastSaveCapturePath = dirinfo.absolutePath();
|
|
|
|
return filename;
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
bool MainWindow::PromptSaveLog()
|
|
{
|
|
QString saveFilename = GetSavePath();
|
|
|
|
if(!saveFilename.isEmpty())
|
|
{
|
|
if(m_Ctx.IsLogLocal() && !QFileInfo(m_Ctx.LogFilename()).exists())
|
|
{
|
|
RDDialog::critical(NULL, tr("File not found"),
|
|
tr("Logfile %1 couldn't be found, cannot save.").arg(m_Ctx.LogFilename()));
|
|
return false;
|
|
}
|
|
|
|
bool success = false;
|
|
QString error;
|
|
|
|
if(m_Ctx.IsLogLocal())
|
|
{
|
|
// we copy the (possibly) temp log to the desired path, but the log item remains referring to
|
|
// the original path.
|
|
// This ensures that if the user deletes the saved path we can still open or re-save it.
|
|
|
|
// QFile::copy won't overwrite, so remove the destination first (the save dialog already
|
|
// prompted for overwrite)
|
|
QFile::remove(saveFilename);
|
|
success = QFile::copy(m_Ctx.LogFilename(), saveFilename);
|
|
|
|
error = tr("Couldn't save to %1").arg(saveFilename);
|
|
}
|
|
else
|
|
{
|
|
m_Ctx.Replay().CopyCaptureFromRemote(m_Ctx.LogFilename(), saveFilename, this);
|
|
success = QFile::exists(saveFilename);
|
|
|
|
error = tr("File couldn't be transferred from remote host");
|
|
}
|
|
|
|
if(!success)
|
|
{
|
|
RDDialog::critical(NULL, tr("Error Saving"), error);
|
|
return false;
|
|
}
|
|
|
|
AddRecentFile(m_Ctx.Config().RecentLogFiles, saveFilename, 10);
|
|
PopulateRecentFiles();
|
|
SetTitle(saveFilename);
|
|
|
|
// we don't prompt to save on closing - if the user deleted the log that we just saved, then
|
|
// that is up to them.
|
|
m_SavedTempLog = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool MainWindow::PromptCloseLog()
|
|
{
|
|
if(!m_Ctx.LogLoaded())
|
|
return true;
|
|
|
|
QString deletepath;
|
|
bool loglocal = false;
|
|
|
|
if(m_OwnTempLog)
|
|
{
|
|
QString temppath = m_Ctx.LogFilename();
|
|
loglocal = m_Ctx.IsLogLocal();
|
|
|
|
QMessageBox::StandardButton res = QMessageBox::No;
|
|
|
|
// unless we've saved the log, prompt to save
|
|
if(!m_SavedTempLog)
|
|
res = RDDialog::question(NULL, tr("Unsaved log"), tr("Save this logfile?"),
|
|
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
|
|
|
|
if(res == QMessageBox::Cancel)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if(res == QMessageBox::Yes)
|
|
{
|
|
bool success = PromptSaveLog();
|
|
|
|
if(!success)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if(temppath != m_Ctx.LogFilename() || res == QMessageBox::No)
|
|
deletepath = temppath;
|
|
m_OwnTempLog = false;
|
|
m_SavedTempLog = false;
|
|
}
|
|
|
|
CloseLogfile();
|
|
|
|
if(!deletepath.isEmpty())
|
|
m_Ctx.Replay().DeleteCapture(deletepath, loglocal);
|
|
|
|
return true;
|
|
}
|
|
|
|
void MainWindow::CloseLogfile()
|
|
{
|
|
m_Ctx.CloseLogfile();
|
|
|
|
ui->action_Save_Log->setEnabled(false);
|
|
}
|
|
|
|
void MainWindow::SetTitle(const QString &filename)
|
|
{
|
|
QString prefix;
|
|
|
|
if(m_Ctx.LogLoaded())
|
|
{
|
|
prefix = QFileInfo(filename).fileName();
|
|
if(m_Ctx.APIProps().degraded)
|
|
prefix += tr(" !DEGRADED PERFORMANCE!");
|
|
prefix += lit(" - ");
|
|
}
|
|
|
|
if(m_Ctx.Replay().CurrentRemote())
|
|
prefix += tr("Remote: %1 - ").arg(m_Ctx.Replay().CurrentRemote()->Name());
|
|
|
|
QString text = prefix + lit("RenderDoc ");
|
|
|
|
if(RENDERDOC_STABLE_BUILD)
|
|
text += lit(FULL_VERSION_STRING);
|
|
else
|
|
text += tr("Unstable release (%1 - %2)").arg(lit(FULL_VERSION_STRING)).arg(lit(GIT_COMMIT_HASH));
|
|
|
|
if(QString::fromLatin1(RENDERDOC_GetVersionString()) != lit(MAJOR_MINOR_VERSION_STRING))
|
|
text += tr(" - !! VERSION MISMATCH DETECTED !!");
|
|
|
|
setWindowTitle(text);
|
|
}
|
|
|
|
void MainWindow::SetTitle()
|
|
{
|
|
SetTitle(m_Ctx.LogFilename());
|
|
}
|
|
|
|
void MainWindow::ClearRecentFiles()
|
|
{
|
|
m_Ctx.Config().RecentLogFiles.clear();
|
|
PopulateRecentFiles();
|
|
}
|
|
|
|
void MainWindow::PopulateRecentFiles()
|
|
{
|
|
ui->menu_Recent_Logs->clear();
|
|
|
|
ui->menu_Recent_Logs->setEnabled(false);
|
|
|
|
int idx = 1;
|
|
for(int i = m_Ctx.Config().RecentLogFiles.size() - 1; i >= 0; i--)
|
|
{
|
|
const QString &filename = m_Ctx.Config().RecentLogFiles[i];
|
|
ui->menu_Recent_Logs->addAction(QFormatStr("&%1 %2").arg(idx).arg(filename),
|
|
[this, filename] { recentLog(filename); });
|
|
idx++;
|
|
|
|
ui->menu_Recent_Logs->setEnabled(true);
|
|
}
|
|
|
|
ui->menu_Recent_Logs->addSeparator();
|
|
ui->menu_Recent_Logs->addAction(ui->action_Clear_Log_History);
|
|
}
|
|
|
|
void MainWindow::ClearRecentCaptures()
|
|
{
|
|
m_Ctx.Config().RecentCaptureSettings.clear();
|
|
PopulateRecentCaptures();
|
|
}
|
|
|
|
void MainWindow::PopulateRecentCaptures()
|
|
{
|
|
ui->menu_Recent_Capture_Settings->clear();
|
|
|
|
ui->menu_Recent_Capture_Settings->setEnabled(false);
|
|
|
|
int idx = 1;
|
|
for(int i = m_Ctx.Config().RecentCaptureSettings.size() - 1; i >= 0; i--)
|
|
{
|
|
const QString &filename = m_Ctx.Config().RecentCaptureSettings[i];
|
|
ui->menu_Recent_Capture_Settings->addAction(QFormatStr("&%1 %2").arg(idx).arg(filename),
|
|
[this, filename] { recentCapture(filename); });
|
|
idx++;
|
|
|
|
ui->menu_Recent_Capture_Settings->setEnabled(true);
|
|
}
|
|
|
|
ui->menu_Recent_Capture_Settings->addSeparator();
|
|
ui->menu_Recent_Capture_Settings->addAction(ui->action_Clear_Capture_History);
|
|
}
|
|
|
|
void MainWindow::ShowLiveCapture(LiveCapture *live)
|
|
{
|
|
m_LiveCaptures.push_back(live);
|
|
|
|
m_Ctx.AddDockWindow(live, DockReference::MainToolArea, this);
|
|
}
|
|
|
|
void MainWindow::LiveCaptureClosed(LiveCapture *live)
|
|
{
|
|
m_LiveCaptures.removeOne(live);
|
|
}
|
|
|
|
ToolWindowManager *MainWindow::mainToolManager()
|
|
{
|
|
return ui->toolWindowManager;
|
|
}
|
|
|
|
ToolWindowManager::AreaReference MainWindow::mainToolArea()
|
|
{
|
|
// bit of a hack. Maybe the ToolWindowManager should track this?
|
|
// Try and identify where to add new windows, by searching a
|
|
// priority list of other windows to use their area
|
|
if(m_Ctx.HasTextureViewer() &&
|
|
ui->toolWindowManager->toolWindows().contains(m_Ctx.GetTextureViewer()->Widget()))
|
|
return ToolWindowManager::AreaReference(
|
|
ToolWindowManager::AddTo, ui->toolWindowManager->areaOf(m_Ctx.GetTextureViewer()->Widget()));
|
|
else if(m_Ctx.HasPipelineViewer() &&
|
|
ui->toolWindowManager->toolWindows().contains(m_Ctx.GetPipelineViewer()->Widget()))
|
|
return ToolWindowManager::AreaReference(
|
|
ToolWindowManager::AddTo, ui->toolWindowManager->areaOf(m_Ctx.GetPipelineViewer()->Widget()));
|
|
else if(m_Ctx.HasMeshPreview() &&
|
|
ui->toolWindowManager->toolWindows().contains(m_Ctx.GetMeshPreview()->Widget()))
|
|
return ToolWindowManager::AreaReference(
|
|
ToolWindowManager::AddTo, ui->toolWindowManager->areaOf(m_Ctx.GetMeshPreview()->Widget()));
|
|
else if(m_Ctx.HasCaptureDialog() &&
|
|
ui->toolWindowManager->toolWindows().contains(m_Ctx.GetCaptureDialog()->Widget()))
|
|
return ToolWindowManager::AreaReference(
|
|
ToolWindowManager::AddTo, ui->toolWindowManager->areaOf(m_Ctx.GetCaptureDialog()->Widget()));
|
|
|
|
// if all else fails just add to the last place we placed something.
|
|
return ToolWindowManager::AreaReference(ToolWindowManager::LastUsedArea);
|
|
}
|
|
|
|
ToolWindowManager::AreaReference MainWindow::leftToolArea()
|
|
{
|
|
// see mainToolArea()
|
|
if(m_Ctx.HasTextureViewer() &&
|
|
ui->toolWindowManager->toolWindows().contains(m_Ctx.GetTextureViewer()->Widget()))
|
|
return ToolWindowManager::AreaReference(
|
|
ToolWindowManager::LeftOf, ui->toolWindowManager->areaOf(m_Ctx.GetTextureViewer()->Widget()));
|
|
else if(m_Ctx.HasPipelineViewer() &&
|
|
ui->toolWindowManager->toolWindows().contains(m_Ctx.GetPipelineViewer()->Widget()))
|
|
return ToolWindowManager::AreaReference(
|
|
ToolWindowManager::LeftOf,
|
|
ui->toolWindowManager->areaOf(m_Ctx.GetPipelineViewer()->Widget()));
|
|
else if(m_Ctx.HasCaptureDialog() &&
|
|
ui->toolWindowManager->toolWindows().contains(m_Ctx.GetCaptureDialog()->Widget()))
|
|
return ToolWindowManager::AreaReference(
|
|
ToolWindowManager::LeftOf, ui->toolWindowManager->areaOf(m_Ctx.GetCaptureDialog()->Widget()));
|
|
|
|
return ToolWindowManager::AreaReference(ToolWindowManager::LastUsedArea);
|
|
}
|
|
|
|
void MainWindow::show()
|
|
{
|
|
bool loaded = LoadLayout(0);
|
|
|
|
// create default layout if layout failed to load
|
|
if(!loaded)
|
|
{
|
|
QWidget *eventBrowser = m_Ctx.GetEventBrowser()->Widget();
|
|
|
|
ui->toolWindowManager->addToolWindow(eventBrowser, ToolWindowManager::EmptySpace);
|
|
|
|
QWidget *textureViewer = m_Ctx.GetTextureViewer()->Widget();
|
|
|
|
ui->toolWindowManager->addToolWindow(
|
|
textureViewer,
|
|
ToolWindowManager::AreaReference(ToolWindowManager::RightOf,
|
|
ui->toolWindowManager->areaOf(eventBrowser), 0.75f));
|
|
|
|
QWidget *pipe = m_Ctx.GetPipelineViewer()->Widget();
|
|
|
|
ui->toolWindowManager->addToolWindow(
|
|
pipe, ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
|
|
ui->toolWindowManager->areaOf(textureViewer)));
|
|
|
|
QWidget *mesh = m_Ctx.GetMeshPreview()->Widget();
|
|
|
|
ui->toolWindowManager->addToolWindow(
|
|
mesh, ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
|
|
ui->toolWindowManager->areaOf(textureViewer)));
|
|
|
|
QWidget *capDialog = m_Ctx.GetCaptureDialog()->Widget();
|
|
|
|
ui->toolWindowManager->addToolWindow(
|
|
capDialog, ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
|
|
ui->toolWindowManager->areaOf(textureViewer)));
|
|
|
|
QWidget *apiInspector = m_Ctx.GetAPIInspector()->Widget();
|
|
|
|
ui->toolWindowManager->addToolWindow(
|
|
apiInspector,
|
|
ToolWindowManager::AreaReference(ToolWindowManager::BottomOf,
|
|
ui->toolWindowManager->areaOf(eventBrowser), 0.3f));
|
|
}
|
|
|
|
QMainWindow::show();
|
|
}
|
|
|
|
void MainWindow::recentLog(const QString &filename)
|
|
{
|
|
if(QFileInfo::exists(filename))
|
|
{
|
|
LoadLogfile(filename, false, true);
|
|
}
|
|
else
|
|
{
|
|
QMessageBox::StandardButton res =
|
|
RDDialog::question(NULL, tr("File not found"),
|
|
tr("File %1 couldn't be found.\nRemove from recent list?").arg(filename));
|
|
|
|
if(res == QMessageBox::Yes)
|
|
{
|
|
m_Ctx.Config().RecentLogFiles.removeOne(filename);
|
|
|
|
PopulateRecentFiles();
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::recentCapture(const QString &filename)
|
|
{
|
|
if(QFileInfo::exists(filename))
|
|
{
|
|
OpenCaptureConfigFile(filename, false);
|
|
}
|
|
else
|
|
{
|
|
QMessageBox::StandardButton res =
|
|
RDDialog::question(NULL, tr("File not found"),
|
|
tr("File %1 couldn't be found.\nRemove from recent list?").arg(filename));
|
|
|
|
if(res == QMessageBox::Yes)
|
|
{
|
|
m_Ctx.Config().RecentCaptureSettings.removeOne(filename);
|
|
|
|
PopulateRecentCaptures();
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::setProgress(float val)
|
|
{
|
|
if(val < 0.0f || val >= 1.0f)
|
|
{
|
|
statusProgress->setVisible(false);
|
|
}
|
|
else
|
|
{
|
|
statusProgress->setVisible(true);
|
|
statusProgress->setValue(1000 * val);
|
|
}
|
|
}
|
|
|
|
void MainWindow::setLogHasErrors(bool errors)
|
|
{
|
|
QString filename = QFileInfo(m_Ctx.LogFilename()).fileName();
|
|
if(errors)
|
|
{
|
|
const QPixmap &del = Pixmaps::del(this);
|
|
QPixmap empty(del.width(), del.height());
|
|
empty.fill(Qt::transparent);
|
|
statusIcon->setPixmap(m_messageAlternate ? empty : del);
|
|
|
|
QString text;
|
|
text = tr("%1 loaded. Capture has %2 errors, warnings or performance notes. "
|
|
"See the 'Errors and Warnings' window.")
|
|
.arg(filename)
|
|
.arg(m_Ctx.DebugMessages().size());
|
|
if(m_Ctx.UnreadMessageCount() > 0)
|
|
text += tr(" %1 Unread.").arg(m_Ctx.UnreadMessageCount());
|
|
statusText->setText(text);
|
|
}
|
|
else
|
|
{
|
|
statusIcon->setPixmap(Pixmaps::tick(this));
|
|
statusText->setText(tr("%1 loaded. No problems detected.").arg(filename));
|
|
}
|
|
}
|
|
|
|
void MainWindow::remoteProbe()
|
|
{
|
|
if(!m_Ctx.LogLoaded() && !m_Ctx.LogLoading())
|
|
{
|
|
for(RemoteHost *host : m_Ctx.Config().RemoteHosts)
|
|
{
|
|
// don't mess with a host we're connected to - this is handled anyway
|
|
if(host->Connected)
|
|
continue;
|
|
|
|
host->CheckStatus();
|
|
|
|
// bail as soon as we notice that we're done
|
|
if(!m_RemoteProbeSemaphore.available())
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::messageCheck()
|
|
{
|
|
if(m_Ctx.LogLoaded())
|
|
{
|
|
m_Ctx.Replay().AsyncInvoke([this](IReplayController *r) {
|
|
rdcarray<DebugMessage> msgs = r->GetDebugMessages();
|
|
|
|
bool disconnected = false;
|
|
|
|
if(m_Ctx.Replay().CurrentRemote())
|
|
{
|
|
bool prev = m_Ctx.Replay().CurrentRemote()->ServerRunning;
|
|
|
|
m_Ctx.Replay().PingRemote();
|
|
|
|
if(prev != m_Ctx.Replay().CurrentRemote()->ServerRunning)
|
|
disconnected = true;
|
|
}
|
|
|
|
GUIInvoke::call([this, disconnected, msgs] {
|
|
// if we just got disconnected while replaying a log, alert the user.
|
|
if(disconnected)
|
|
{
|
|
RDDialog::critical(this, tr("Remote server disconnected"),
|
|
tr("Remote server disconnected during replaying of this capture.\n"
|
|
"The replay will now be non-functional. To restore you will have "
|
|
"to close the capture, allow "
|
|
"RenderDoc to reconnect and load the capture again"));
|
|
}
|
|
|
|
if(m_Ctx.Replay().CurrentRemote() && !m_Ctx.Replay().CurrentRemote()->ServerRunning)
|
|
contextChooser->setIcon(Icons::cross());
|
|
|
|
if(!msgs.empty())
|
|
{
|
|
m_Ctx.AddMessages(msgs);
|
|
}
|
|
|
|
if(m_Ctx.UnreadMessageCount() > 0)
|
|
m_messageAlternate = !m_messageAlternate;
|
|
else
|
|
m_messageAlternate = false;
|
|
|
|
setLogHasErrors(!m_Ctx.DebugMessages().empty());
|
|
});
|
|
});
|
|
}
|
|
else if(!m_Ctx.LogLoaded() && !m_Ctx.LogLoading())
|
|
{
|
|
if(m_Ctx.Replay().CurrentRemote())
|
|
m_Ctx.Replay().PingRemote();
|
|
|
|
GUIInvoke::call([this]() {
|
|
if(m_Ctx.Replay().CurrentRemote() && !m_Ctx.Replay().CurrentRemote()->ServerRunning)
|
|
{
|
|
contextChooser->setIcon(Icons::cross());
|
|
contextChooser->setText(tr("Replay Context: %1").arg(tr("Local")));
|
|
statusText->setText(
|
|
tr("Remote server disconnected. To attempt to reconnect please select it again."));
|
|
|
|
m_Ctx.Replay().DisconnectFromRemoteServer();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void MainWindow::FillRemotesMenu(QMenu *menu, bool includeLocalhost)
|
|
{
|
|
menu->clear();
|
|
|
|
for(int i = 0; i < m_Ctx.Config().RemoteHosts.count(); i++)
|
|
{
|
|
RemoteHost *host = m_Ctx.Config().RemoteHosts[i];
|
|
|
|
// add localhost at the end
|
|
if(host->Hostname == lit("localhost"))
|
|
continue;
|
|
|
|
QAction *action = new QAction(menu);
|
|
|
|
action->setIcon(host->ServerRunning && !host->VersionMismatch ? Icons::tick() : Icons::cross());
|
|
if(host->Connected)
|
|
action->setText(tr("%1 (Connected)").arg(host->Name()));
|
|
else if(host->ServerRunning && host->VersionMismatch)
|
|
action->setText(tr("%1 (Bad Version)").arg(host->Name()));
|
|
else if(host->ServerRunning && host->Busy)
|
|
action->setText(tr("%1 (Busy)").arg(host->Name()));
|
|
else if(host->ServerRunning)
|
|
action->setText(tr("%1 (Online)").arg(host->Name()));
|
|
else
|
|
action->setText(tr("%1 (Offline)").arg(host->Name()));
|
|
QObject::connect(action, &QAction::triggered, this, &MainWindow::switchContext);
|
|
action->setData(i);
|
|
|
|
// don't allow switching to the connected host
|
|
if(host->Connected)
|
|
action->setEnabled(false);
|
|
|
|
menu->addAction(action);
|
|
}
|
|
|
|
if(includeLocalhost)
|
|
{
|
|
QAction *localContext = new QAction(menu);
|
|
|
|
localContext->setText(tr("Local"));
|
|
localContext->setIcon(Icons::house());
|
|
|
|
QObject::connect(localContext, &QAction::triggered, this, &MainWindow::switchContext);
|
|
localContext->setData(-1);
|
|
|
|
menu->addAction(localContext);
|
|
}
|
|
}
|
|
|
|
void MainWindow::switchContext()
|
|
{
|
|
QAction *item = qobject_cast<QAction *>(QObject::sender());
|
|
|
|
if(!item)
|
|
return;
|
|
|
|
bool ok = false;
|
|
int hostIdx = item->data().toInt(&ok);
|
|
|
|
if(!ok)
|
|
return;
|
|
|
|
RemoteHost *host = NULL;
|
|
|
|
if(hostIdx >= 0 && hostIdx < m_Ctx.Config().RemoteHosts.count())
|
|
{
|
|
host = m_Ctx.Config().RemoteHosts[hostIdx];
|
|
}
|
|
|
|
for(LiveCapture *live : m_LiveCaptures)
|
|
{
|
|
// allow live captures to this host to stay open, that way
|
|
// we can connect to a live capture, then switch into that
|
|
// context
|
|
if(host && live->hostname() == host->Hostname)
|
|
continue;
|
|
|
|
if(!live->checkAllowClose())
|
|
return;
|
|
}
|
|
|
|
if(!PromptCloseLog())
|
|
return;
|
|
|
|
for(LiveCapture *live : m_LiveCaptures)
|
|
{
|
|
// allow live captures to this host to stay open, that way
|
|
// we can connect to a live capture, then switch into that
|
|
// context
|
|
if(host && live->hostname() == host->Hostname)
|
|
continue;
|
|
|
|
live->cleanItems();
|
|
live->close();
|
|
}
|
|
|
|
m_Ctx.Replay().DisconnectFromRemoteServer();
|
|
|
|
if(!host)
|
|
{
|
|
contextChooser->setIcon(Icons::house());
|
|
contextChooser->setText(tr("Replay Context: %1").arg(tr("Local")));
|
|
|
|
ui->action_Inject_into_Process->setEnabled(true);
|
|
|
|
statusText->setText(QString());
|
|
|
|
SetTitle();
|
|
}
|
|
else
|
|
{
|
|
contextChooser->setText(tr("Replay Context: %1").arg(host->Name()));
|
|
contextChooser->setIcon(host->ServerRunning ? Icons::connect() : Icons::disconnect());
|
|
|
|
// disable until checking is done
|
|
contextChooser->setEnabled(false);
|
|
|
|
ui->action_Inject_into_Process->setEnabled(false);
|
|
|
|
SetTitle();
|
|
|
|
statusText->setText(tr("Checking remote server status..."));
|
|
|
|
LambdaThread *th = new LambdaThread([this, host]() {
|
|
// see if the server is up
|
|
host->CheckStatus();
|
|
|
|
if(!host->ServerRunning && !host->RunCommand.isEmpty())
|
|
{
|
|
GUIInvoke::call([this]() { statusText->setText(tr("Running remote server command...")); });
|
|
|
|
host->Launch();
|
|
|
|
// check if it's running now
|
|
host->CheckStatus();
|
|
}
|
|
|
|
ReplayStatus status = ReplayStatus::Succeeded;
|
|
|
|
if(host->ServerRunning && !host->Busy)
|
|
{
|
|
status = m_Ctx.Replay().ConnectToRemoteServer(host);
|
|
}
|
|
|
|
GUIInvoke::call([this, host, status]() {
|
|
contextChooser->setIcon(host->ServerRunning && !host->Busy ? Icons::connect()
|
|
: Icons::disconnect());
|
|
|
|
if(status != ReplayStatus::Succeeded)
|
|
{
|
|
contextChooser->setIcon(Icons::cross());
|
|
contextChooser->setText(tr("Replay Context: %1").arg(tr("Local")));
|
|
statusText->setText(tr("Connection failed: %1").arg(ToQStr(status)));
|
|
}
|
|
else if(host->VersionMismatch)
|
|
{
|
|
statusText->setText(
|
|
tr("Remote server is not running RenderDoc %1").arg(lit(FULL_VERSION_STRING)));
|
|
}
|
|
else if(host->Busy)
|
|
{
|
|
statusText->setText(tr("Remote server in use elsewhere"));
|
|
}
|
|
else if(host->ServerRunning)
|
|
{
|
|
statusText->setText(tr("Remote server ready"));
|
|
}
|
|
else
|
|
{
|
|
if(!host->RunCommand.isEmpty())
|
|
statusText->setText(tr("Remote server not running or failed to start"));
|
|
else
|
|
statusText->setText(tr("Remote server not running - no start command configured"));
|
|
}
|
|
|
|
contextChooser->setEnabled(true);
|
|
});
|
|
});
|
|
th->selfDelete(true);
|
|
th->start();
|
|
}
|
|
}
|
|
|
|
void MainWindow::contextChooser_menuShowing()
|
|
{
|
|
FillRemotesMenu(contextChooserMenu, true);
|
|
}
|
|
|
|
void MainWindow::statusDoubleClicked(QMouseEvent *event)
|
|
{
|
|
showDebugMessageView();
|
|
}
|
|
|
|
void MainWindow::OnLogfileLoaded()
|
|
{
|
|
ui->action_Save_Log->setEnabled(true);
|
|
ui->action_Close_Log->setEnabled(true);
|
|
|
|
// don't allow changing context while log is open
|
|
contextChooser->setEnabled(false);
|
|
|
|
statusProgress->setVisible(false);
|
|
|
|
ui->action_Start_Replay_Loop->setEnabled(true);
|
|
|
|
setLogHasErrors(!m_Ctx.DebugMessages().empty());
|
|
|
|
ui->action_Resolve_Symbols->setEnabled(false);
|
|
|
|
m_Ctx.Replay().AsyncInvoke([this](IReplayController *) {
|
|
bool hasResolver = m_Ctx.Replay().GetResolver()->HasCallstacks();
|
|
|
|
GUIInvoke::call([this, hasResolver]() {
|
|
ui->action_Resolve_Symbols->setEnabled(hasResolver);
|
|
ui->action_Resolve_Symbols->setText(hasResolver ? tr("Resolve Symbols")
|
|
: tr("Resolve Symbols - None in log"));
|
|
|
|
ui->action_Save_Log->setEnabled(true);
|
|
});
|
|
});
|
|
|
|
SetTitle();
|
|
|
|
PopulateRecentFiles();
|
|
|
|
ToolWindowManager::raiseToolWindow(m_Ctx.GetEventBrowser()->Widget());
|
|
}
|
|
|
|
void MainWindow::OnLogfileClosed()
|
|
{
|
|
ui->action_Save_Log->setEnabled(false);
|
|
ui->action_Close_Log->setEnabled(false);
|
|
|
|
ui->action_Start_Replay_Loop->setEnabled(false);
|
|
|
|
contextChooser->setEnabled(true);
|
|
|
|
statusText->setText(QString());
|
|
statusIcon->setPixmap(QPixmap());
|
|
statusProgress->setVisible(false);
|
|
|
|
ui->action_Resolve_Symbols->setEnabled(false);
|
|
ui->action_Resolve_Symbols->setText(tr("Resolve Symbols"));
|
|
|
|
SetTitle();
|
|
|
|
// if the remote sever disconnected during log replay, resort back to a 'disconnected' state
|
|
if(m_Ctx.Replay().CurrentRemote() && !m_Ctx.Replay().CurrentRemote()->ServerRunning)
|
|
{
|
|
statusText->setText(
|
|
tr("Remote server disconnected. To attempt to reconnect please select it again."));
|
|
contextChooser->setText(tr("Replay Context: %1").arg(tr("Local")));
|
|
m_Ctx.Replay().DisconnectFromRemoteServer();
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnEventChanged(uint32_t eventID)
|
|
{
|
|
}
|
|
|
|
void MainWindow::RegisterShortcut(const QString &shortcut, QWidget *widget, ShortcutCallback callback)
|
|
{
|
|
QKeySequence ks = QKeySequence::fromString(shortcut);
|
|
|
|
if(widget)
|
|
{
|
|
m_WidgetShortcutCallbacks[ks][widget] = callback;
|
|
}
|
|
else
|
|
{
|
|
if(m_GlobalShortcutCallbacks[ks])
|
|
{
|
|
qCritical() << "Assigning duplicate global shortcut for" << ks;
|
|
return;
|
|
}
|
|
|
|
m_GlobalShortcutCallbacks[ks] = callback;
|
|
}
|
|
}
|
|
|
|
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
|
|
{
|
|
if(event->type() == QEvent::ShortcutOverride)
|
|
{
|
|
QKeyEvent *ke = (QKeyEvent *)event;
|
|
|
|
QKeySequence pressed(ke->modifiers() | ke->key());
|
|
|
|
// first see if there's a widget shortcut registered for this key. If so, check the focus
|
|
// hierarchy to see if we have any matches
|
|
QWidget *focus = QApplication::focusWidget();
|
|
|
|
if(focus && m_WidgetShortcutCallbacks.contains(pressed))
|
|
{
|
|
const QMap<QWidget *, ShortcutCallback> callbacks = m_WidgetShortcutCallbacks[pressed];
|
|
QList<QWidget *> widgets = callbacks.keys();
|
|
|
|
while(focus)
|
|
{
|
|
// if we find a direct ancestor to the focus widget which is registered for this shortcut,
|
|
// then use that callback
|
|
if(widgets.contains(focus))
|
|
{
|
|
callbacks[focus]();
|
|
event->accept();
|
|
return true;
|
|
}
|
|
|
|
// keep searching up the hierarchy
|
|
focus = focus->parentWidget();
|
|
}
|
|
}
|
|
|
|
// if we didn't find matches or no such shortcut is registered, try global shortcuts
|
|
if(m_GlobalShortcutCallbacks.contains(pressed))
|
|
{
|
|
m_GlobalShortcutCallbacks[pressed]();
|
|
event->accept();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return QMainWindow::eventFilter(watched, event);
|
|
}
|
|
|
|
void MainWindow::on_action_Close_Log_triggered()
|
|
{
|
|
PromptCloseLog();
|
|
}
|
|
|
|
void MainWindow::on_action_Save_Log_triggered()
|
|
{
|
|
PromptSaveLog();
|
|
}
|
|
|
|
void MainWindow::on_action_About_triggered()
|
|
{
|
|
AboutDialog about(this);
|
|
RDDialog::show(&about);
|
|
}
|
|
|
|
void MainWindow::on_action_Mesh_Output_triggered()
|
|
{
|
|
QWidget *meshPreview = m_Ctx.GetMeshPreview()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(meshPreview))
|
|
ToolWindowManager::raiseToolWindow(meshPreview);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(meshPreview, mainToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_API_Inspector_triggered()
|
|
{
|
|
QWidget *apiInspector = m_Ctx.GetAPIInspector()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(apiInspector))
|
|
{
|
|
ToolWindowManager::raiseToolWindow(apiInspector);
|
|
}
|
|
else
|
|
{
|
|
if(m_Ctx.HasEventBrowser() &&
|
|
ui->toolWindowManager->toolWindows().contains(m_Ctx.GetEventBrowser()->Widget()))
|
|
{
|
|
ToolWindowManager::AreaReference ref(
|
|
ToolWindowManager::BottomOf,
|
|
ui->toolWindowManager->areaOf(m_Ctx.GetEventBrowser()->Widget()));
|
|
ui->toolWindowManager->addToolWindow(apiInspector, ref);
|
|
}
|
|
else
|
|
{
|
|
ui->toolWindowManager->addToolWindow(apiInspector, leftToolArea());
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_action_Event_Browser_triggered()
|
|
{
|
|
QWidget *eventBrowser = m_Ctx.GetEventBrowser()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(eventBrowser))
|
|
ToolWindowManager::raiseToolWindow(eventBrowser);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(eventBrowser, leftToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_Texture_Viewer_triggered()
|
|
{
|
|
QWidget *textureViewer = m_Ctx.GetTextureViewer()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(textureViewer))
|
|
ToolWindowManager::raiseToolWindow(textureViewer);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(textureViewer, mainToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_Pipeline_State_triggered()
|
|
{
|
|
QWidget *pipelineViewer = m_Ctx.GetPipelineViewer()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(pipelineViewer))
|
|
ToolWindowManager::raiseToolWindow(pipelineViewer);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(pipelineViewer, mainToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_Launch_Application_triggered()
|
|
{
|
|
ICaptureDialog *capDialog = m_Ctx.GetCaptureDialog();
|
|
|
|
capDialog->SetInjectMode(false);
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(capDialog->Widget()))
|
|
ToolWindowManager::raiseToolWindow(capDialog->Widget());
|
|
else
|
|
ui->toolWindowManager->addToolWindow(capDialog->Widget(), mainToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_Inject_into_Process_triggered()
|
|
{
|
|
ICaptureDialog *capDialog = m_Ctx.GetCaptureDialog();
|
|
|
|
capDialog->SetInjectMode(true);
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(capDialog->Widget()))
|
|
ToolWindowManager::raiseToolWindow(capDialog->Widget());
|
|
else
|
|
ui->toolWindowManager->addToolWindow(capDialog->Widget(), mainToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_Errors_and_Warnings_triggered()
|
|
{
|
|
QWidget *debugMessages = m_Ctx.GetDebugMessageView()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(debugMessages))
|
|
ToolWindowManager::raiseToolWindow(debugMessages);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(debugMessages, mainToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_Statistics_Viewer_triggered()
|
|
{
|
|
QWidget *stats = m_Ctx.GetStatisticsViewer()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(stats))
|
|
ToolWindowManager::raiseToolWindow(stats);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(stats, mainToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_Timeline_triggered()
|
|
{
|
|
QWidget *stats = m_Ctx.GetTimelineBar()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(stats))
|
|
ToolWindowManager::raiseToolWindow(stats);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(
|
|
stats,
|
|
ToolWindowManager::AreaReference(ToolWindowManager::TopWindowSide, mainToolArea().area()));
|
|
}
|
|
|
|
void MainWindow::on_action_Python_Shell_triggered()
|
|
{
|
|
QWidget *py = m_Ctx.GetPythonShell()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(py))
|
|
ToolWindowManager::raiseToolWindow(py);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(py, mainToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_Resolve_Symbols_triggered()
|
|
{
|
|
if(!m_Ctx.Replay().GetResolver())
|
|
{
|
|
RDDialog::critical(
|
|
this, tr("Not Available"),
|
|
tr("Callstack resolution is not available.\n\nCheck remote server connection."));
|
|
return;
|
|
}
|
|
|
|
float progress = 0.0f;
|
|
bool finished = false;
|
|
|
|
m_Ctx.Replay().AsyncInvoke([this, &progress, &finished](IReplayController *) {
|
|
bool success = m_Ctx.Replay().GetResolver()->InitResolver(&progress, NULL);
|
|
|
|
if(!success)
|
|
{
|
|
RDDialog::critical(
|
|
this, tr("Error loading symbols"),
|
|
tr("Couldn't load symbols for callstack resolution.\n\nCheck diagnostic log in "
|
|
"Help menu for more details."));
|
|
}
|
|
|
|
finished = true;
|
|
});
|
|
|
|
ShowProgressDialog(this, tr("Resolving symbols, please wait..."),
|
|
[&finished]() { return finished; }, [&progress]() { return progress; });
|
|
|
|
if(m_Ctx.HasAPIInspector())
|
|
m_Ctx.GetAPIInspector()->Refresh();
|
|
}
|
|
|
|
void MainWindow::on_action_Start_Replay_Loop_triggered()
|
|
{
|
|
if(!m_Ctx.LogLoaded())
|
|
return;
|
|
|
|
QDialog popup;
|
|
popup.setWindowFlags(popup.windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
popup.setWindowIcon(windowIcon());
|
|
|
|
const TextureDescription *displayTex = NULL;
|
|
|
|
const DrawcallDescription *lastDraw = m_Ctx.GetLastDrawcall();
|
|
|
|
displayTex = m_Ctx.GetTexture(lastDraw->copyDestination);
|
|
if(!displayTex)
|
|
displayTex = m_Ctx.GetTexture(lastDraw->outputs[0]);
|
|
|
|
if(!displayTex)
|
|
{
|
|
// if no texture was bound, then use the first colour swapbuffer
|
|
for(const TextureDescription &tex : m_Ctx.GetTextures())
|
|
{
|
|
if((tex.creationFlags & TextureCategory::SwapBuffer) &&
|
|
tex.format.compType != CompType::Depth && tex.format.type != ResourceFormatType::D16S8 &&
|
|
tex.format.type != ResourceFormatType::D24S8 && tex.format.type != ResourceFormatType::D32S8)
|
|
{
|
|
displayTex = &tex;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
ResourceId id;
|
|
|
|
if(displayTex)
|
|
{
|
|
id = displayTex->ID;
|
|
popup.resize((int)displayTex->width, (int)displayTex->height);
|
|
popup.setWindowTitle(
|
|
tr("Looping replay of %1 Displaying %2").arg(m_Ctx.LogFilename()).arg(m_Ctx.GetResourceName(id)));
|
|
}
|
|
else
|
|
{
|
|
popup.resize(100, 100);
|
|
popup.setWindowTitle(
|
|
tr("Looping replay of %1 Displaying %2").arg(m_Ctx.LogFilename()).arg(tr("nothing")));
|
|
}
|
|
|
|
WindowingSystem winSys = m_Ctx.CurWindowingSystem();
|
|
void *winData = m_Ctx.FillWindowingData(popup.winId());
|
|
|
|
m_Ctx.Replay().AsyncInvoke(
|
|
[winSys, winData, id](IReplayController *r) { r->ReplayLoop(winSys, winData, id); });
|
|
|
|
RDDialog::show(&popup);
|
|
|
|
m_Ctx.Replay().CancelReplayLoop();
|
|
}
|
|
|
|
void MainWindow::on_action_Attach_to_Running_Instance_triggered()
|
|
{
|
|
on_action_Manage_Remote_Servers_triggered();
|
|
}
|
|
|
|
void MainWindow::on_action_Manage_Remote_Servers_triggered()
|
|
{
|
|
RemoteManager *rm = new RemoteManager(m_Ctx, this);
|
|
RDDialog::show(rm);
|
|
// now that we're done with it, the manager deletes itself when all lookups terminate (or
|
|
// immediately if there are no lookups ongoing).
|
|
rm->closeWhenFinished();
|
|
}
|
|
|
|
void MainWindow::on_action_Settings_triggered()
|
|
{
|
|
SettingsDialog about(m_Ctx, this);
|
|
RDDialog::show(&about);
|
|
}
|
|
|
|
void MainWindow::on_action_View_Documentation_triggered()
|
|
{
|
|
QFileInfo fi(QGuiApplication::applicationFilePath());
|
|
QDir curDir = QFileInfo(QGuiApplication::applicationFilePath()).absoluteDir();
|
|
|
|
if(fi.absoluteDir().exists(lit("renderdoc.chm")))
|
|
QDesktopServices::openUrl(
|
|
QUrl::fromLocalFile(fi.absoluteDir().absoluteFilePath(lit("renderdoc.chm"))));
|
|
else
|
|
QDesktopServices::openUrl(QUrl::fromUserInput(lit("https://renderdoc.org/docs")));
|
|
}
|
|
|
|
void MainWindow::on_action_View_Diagnostic_Log_File_triggered()
|
|
{
|
|
QString logPath = QString::fromUtf8(RENDERDOC_GetLogFile());
|
|
if(QFileInfo::exists(logPath))
|
|
QDesktopServices::openUrl(QUrl::fromLocalFile(logPath));
|
|
}
|
|
|
|
void MainWindow::on_action_Source_on_GitHub_triggered()
|
|
{
|
|
QDesktopServices::openUrl(QUrl::fromUserInput(lit("https://github.com/baldurk/renderdoc")));
|
|
}
|
|
|
|
void MainWindow::on_action_Build_Release_Downloads_triggered()
|
|
{
|
|
QDesktopServices::openUrl(QUrl::fromUserInput(lit("https://renderdoc.org/builds")));
|
|
}
|
|
|
|
void MainWindow::on_action_Show_Tips_triggered()
|
|
{
|
|
TipsDialog tipsDialog(m_Ctx, this);
|
|
RDDialog::show(&tipsDialog);
|
|
}
|
|
|
|
void MainWindow::on_action_Counter_Viewer_triggered()
|
|
{
|
|
QWidget *performanceCounterViewer = m_Ctx.GetPerformanceCounterViewer()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(performanceCounterViewer))
|
|
ToolWindowManager::raiseToolWindow(performanceCounterViewer);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(performanceCounterViewer, mainToolArea());
|
|
}
|
|
|
|
void MainWindow::on_action_Resource_Inspector_triggered()
|
|
{
|
|
QWidget *resourceInspector = m_Ctx.GetResourceInspector()->Widget();
|
|
|
|
if(ui->toolWindowManager->toolWindows().contains(resourceInspector))
|
|
ToolWindowManager::raiseToolWindow(resourceInspector);
|
|
else
|
|
ui->toolWindowManager->addToolWindow(resourceInspector, mainToolArea());
|
|
}
|
|
|
|
void MainWindow::saveLayout_triggered()
|
|
{
|
|
LoadSaveLayout(qobject_cast<QAction *>(QObject::sender()), true);
|
|
}
|
|
|
|
void MainWindow::loadLayout_triggered()
|
|
{
|
|
LoadSaveLayout(qobject_cast<QAction *>(QObject::sender()), false);
|
|
}
|
|
|
|
void MainWindow::closeEvent(QCloseEvent *event)
|
|
{
|
|
for(LiveCapture *live : m_LiveCaptures)
|
|
{
|
|
if(!live->checkAllowClose())
|
|
{
|
|
event->ignore();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(RENDERDOC_IsGlobalHookActive())
|
|
{
|
|
RDDialog::critical(this, tr("Global hook active"),
|
|
tr("Cannot close RenderDoc while global hook is active."));
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
if(!PromptCloseLog())
|
|
{
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
for(LiveCapture *live : m_LiveCaptures)
|
|
{
|
|
live->cleanItems();
|
|
delete live;
|
|
}
|
|
|
|
SaveLayout(0);
|
|
}
|
|
|
|
void MainWindow::changeEvent(QEvent *event)
|
|
{
|
|
if(event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
|
|
QPixmapCache::clear();
|
|
}
|
|
|
|
QString MainWindow::dragFilename(const QMimeData *mimeData)
|
|
{
|
|
if(mimeData->hasUrls())
|
|
{
|
|
QList<QUrl> urls = mimeData->urls();
|
|
if(urls.size() == 1 && urls[0].isLocalFile())
|
|
{
|
|
QFileInfo f(urls[0].toLocalFile());
|
|
if(f.exists())
|
|
return f.absoluteFilePath();
|
|
}
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
|
|
{
|
|
if(!dragFilename(event->mimeData()).isEmpty())
|
|
event->acceptProposedAction();
|
|
}
|
|
|
|
void MainWindow::dropEvent(QDropEvent *event)
|
|
{
|
|
QString fn = dragFilename(event->mimeData());
|
|
if(!fn.isEmpty())
|
|
{
|
|
// we defer this so we can return immediately and unblock whichever application dropped the
|
|
// item.
|
|
GUIInvoke::defer([this, fn]() { LoadFromFilename(fn, false); });
|
|
}
|
|
}
|
|
|
|
void MainWindow::LoadSaveLayout(QAction *action, bool save)
|
|
{
|
|
if(action == NULL)
|
|
{
|
|
qWarning() << "NULL action passed to LoadSaveLayout - bad signal?";
|
|
return;
|
|
}
|
|
|
|
bool success = false;
|
|
|
|
if(action == ui->action_Save_Default_Layout)
|
|
{
|
|
success = SaveLayout(0);
|
|
}
|
|
else if(action == ui->action_Load_Default_Layout)
|
|
{
|
|
success = LoadLayout(0);
|
|
}
|
|
else
|
|
{
|
|
QString name = action->objectName();
|
|
name.remove(0, name.size() - 1);
|
|
int idx = name.toInt();
|
|
|
|
if(idx > 0)
|
|
{
|
|
if(save)
|
|
success = SaveLayout(idx);
|
|
else
|
|
success = LoadLayout(idx);
|
|
}
|
|
}
|
|
|
|
if(!success)
|
|
{
|
|
if(save)
|
|
RDDialog::critical(this, tr("Error saving layout"), tr("Couldn't save layout"));
|
|
else
|
|
RDDialog::critical(this, tr("Error loading layout"), tr("Couldn't load layout"));
|
|
}
|
|
}
|
|
|
|
QVariantMap MainWindow::saveState()
|
|
{
|
|
QVariantMap state = ui->toolWindowManager->saveState();
|
|
|
|
state[lit("mainWindowGeometry")] = saveGeometry().toBase64();
|
|
|
|
return state;
|
|
}
|
|
|
|
bool MainWindow::restoreState(QVariantMap &state)
|
|
{
|
|
restoreGeometry(QByteArray::fromBase64(state[lit("mainWindowGeometry")].toByteArray()));
|
|
|
|
ui->toolWindowManager->restoreState(state);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MainWindow::SaveLayout(int layout)
|
|
{
|
|
QString path = GetLayoutPath(layout);
|
|
|
|
QVariantMap state = saveState();
|
|
|
|
QFile f(path);
|
|
if(f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
|
|
return SaveToJSON(state, f, JSON_ID, JSON_VER);
|
|
|
|
qWarning() << "Couldn't write to " << path << " " << f.errorString();
|
|
|
|
return false;
|
|
}
|
|
|
|
bool MainWindow::LoadLayout(int layout)
|
|
{
|
|
QString path = GetLayoutPath(layout);
|
|
|
|
QFile f(path);
|
|
if(f.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
{
|
|
QVariantMap state;
|
|
|
|
bool success = LoadFromJSON(state, f, JSON_ID, JSON_VER);
|
|
|
|
if(!success)
|
|
return false;
|
|
|
|
return restoreState(state);
|
|
}
|
|
|
|
qInfo() << "Couldn't load layout from " << path << " " << f.errorString();
|
|
|
|
return false;
|
|
}
|