ostinato/client/mainwindow.cpp
Srivats P 1e1e0b0c48 Clear current and selection when restoring default view
This is done for all top level windows - ports, stats, logs

At startup, the local portgroup would automatically become current with
the result that the welcome page would not be visible. Not sure why but
an explicit setFocus of ports window seems to avoid it.
2020-05-07 18:40:31 +05:30

637 lines
20 KiB
C++

/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "mainwindow.h"
#if 0
#include "dbgthread.h"
#endif
#include "clipboardhelper.h"
#include "jumpurl.h"
#include "logsmodel.h"
#include "logswindow.h"
#include "params.h"
#include "portgrouplist.h"
#include "portstatswindow.h"
#include "portswindow.h"
#include "preferences.h"
#include "sessionfileformat.h"
#include "settings.h"
#include "ui_about.h"
#include "updater.h"
#include "fileformat.pb.h"
#include <QDate>
#include <QDesktopServices>
#include <QDockWidget>
#include <QFileDialog>
#include <QMessageBox>
#include <QProcess>
#include <QProgressDialog>
#include <QTimer>
#include <QUrl>
#ifdef Q_OS_WIN32
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#endif
extern const char* version;
extern const char* revision;
PortGroupList *pgl;
LogsModel *appLogs;
ClipboardHelper *clipboardHelper;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow (parent)
{
Updater *updater = new Updater();
if (appParams.optLocalDrone()) {
QString serverApp = QCoreApplication::applicationDirPath();
#ifdef Q_OS_MAC
// applicationDirPath() does not return bundle,
// but executable inside bundle
serverApp.replace("Ostinato.app", "drone.app");
#endif
#ifdef Q_OS_WIN32
serverApp.append("/drone.exe");
#else
serverApp.append("/drone");
#endif
qDebug("staring local server - %s", qPrintable(serverApp));
localServer_ = new QProcess(this);
connect(localServer_, SIGNAL(finished(int, QProcess::ExitStatus)),
SLOT(onLocalServerFinished(int, QProcess::ExitStatus)));
connect(localServer_, SIGNAL(error(QProcess::ProcessError)),
SLOT(onLocalServerError(QProcess::ProcessError)));
localServer_->setProcessChannelMode(QProcess::ForwardedChannels);
localServer_->start(serverApp, QStringList());
QTimer::singleShot(5000, this, SLOT(stopLocalServerMonitor()));
}
else
localServer_ = NULL;
pgl = new PortGroupList;
appLogs = new LogsModel(this);
clipboardHelper = new ClipboardHelper(this);
portsWindow = new PortsWindow(pgl, this);
statsWindow = new PortStatsWindow(pgl, this);
portsDock = new QDockWidget(tr("Ports and Streams"), this);
portsDock->setObjectName("portsDock");
portsDock->setFeatures(
portsDock->features() & ~QDockWidget::DockWidgetClosable);
statsDock = new QDockWidget(tr("Port Statistics"), this);
statsDock->setObjectName("statsDock");
statsDock->setFeatures(
statsDock->features() & ~QDockWidget::DockWidgetClosable);
logsDock_ = new QDockWidget(tr("Logs"), this);
logsDock_->setObjectName("logsDock");
logsDock_->setFeatures(
logsDock_->features() & ~QDockWidget::DockWidgetClosable);
logsWindow_ = new LogsWindow(appLogs, logsDock_);
setupUi(this);
menuFile->insertActions(menuFile->actions().at(3), portsWindow->actions());
menuEdit->addActions(clipboardHelper->actions());
statsDock->setWidget(statsWindow);
addDockWidget(Qt::BottomDockWidgetArea, statsDock);
logsDock_->setWidget(logsWindow_);
addDockWidget(Qt::BottomDockWidgetArea, logsDock_);
tabifyDockWidget(statsDock, logsDock_);
statsDock->show();
statsDock->raise();
portsDock->setWidget(portsWindow);
addDockWidget(Qt::TopDockWidgetArea, portsDock);
#if QT_VERSION >= 0x050600
// Set top and bottom docks to equal height
resizeDocks({portsDock, statsDock}, {height()/2, height()/2}, Qt::Vertical);
#endif
portsWindow->setFocus();
// Save the default window geometry and layout ...
defaultGeometry_ = geometry();
defaultLayout_ = saveState(0);
// ... before restoring the last used settings
QRect geom = appSettings->value(kApplicationWindowGeometryKey).toRect();
if (!geom.isNull())
setGeometry(geom);
QByteArray layout = appSettings->value(kApplicationWindowLayout)
.toByteArray();
if (layout.size())
restoreState(layout, 0);
connect(actionFileExit, SIGNAL(triggered()), this, SLOT(close()));
connect(actionAboutQt, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
connect(actionViewShowMyReservedPortsOnly, SIGNAL(toggled(bool)),
portsWindow, SLOT(showMyReservedPortsOnly(bool)));
connect(actionViewShowMyReservedPortsOnly, SIGNAL(toggled(bool)),
statsWindow, SLOT(showMyReservedPortsOnly(bool)));
connect(updater, SIGNAL(newVersionAvailable(QString)),
this, SLOT(onNewVersion(QString)));
updater->checkForNewVersion();
// Add the "Local" Port Group
if (appParams.optLocalDrone()) {
PortGroup *pg = new PortGroup;
pgl->addPortGroup(*pg);
}
if (appParams.argumentCount()) {
QString fileName = appParams.argument(0);
if (QFile::exists(fileName))
openSession(fileName);
else
QMessageBox::information(NULL, qApp->applicationName(),
QString("File not found: " + fileName));
}
#if 0
{
DbgThread *dbg = new DbgThread(pgl);
dbg->start();
}
#endif
}
MainWindow::~MainWindow()
{
stopLocalServerMonitor();
if (localServer_) {
#ifdef Q_OS_WIN32
//! \todo - find a way to terminate cleanly
localServer_->kill();
#else
localServer_->terminate();
#endif
}
delete pgl;
// We don't want to save state for Stream Stats Docks - so delete them
QList<QDockWidget*> streamStatsDocks
= findChildren<QDockWidget*>("streamStatsDock");
foreach(QDockWidget *dock, streamStatsDocks)
delete dock;
Q_ASSERT(findChildren<QDockWidget*>("streamStatsDock").size() == 0);
QByteArray layout = saveState(0);
appSettings->setValue(kApplicationWindowLayout, layout);
appSettings->setValue(kApplicationWindowGeometryKey, geometry());
if (localServer_) {
localServer_->waitForFinished();
delete localServer_;
}
}
void MainWindow::openSession(QString fileName)
{
qDebug("Open Session Action (%s)", qPrintable(fileName));
static QString dirName;
QStringList fileTypes = SessionFileFormat::supportedFileTypes(
SessionFileFormat::kOpenFile);
QString fileType;
QString errorStr;
bool ret;
if (!fileName.isEmpty())
goto _skip_prompt;
if (portsWindow->portGroupCount()) {
if (QMessageBox::question(this,
tr("Open Session"),
tr("Existing session will be lost. Proceed?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) == QMessageBox::No)
goto _exit;
}
if (fileTypes.size())
fileType = fileTypes.at(0);
fileName = QFileDialog::getOpenFileName(this, tr("Open Session"),
dirName, fileTypes.join(";;"), &fileType);
if (fileName.isEmpty())
goto _exit;
_skip_prompt:
ret = openSession(fileName, errorStr);
if (!ret || !errorStr.isEmpty()) {
QMessageBox msgBox(this);
QStringList str = errorStr.split("\n\n\n\n");
msgBox.setIcon(ret ? QMessageBox::Warning : QMessageBox::Critical);
msgBox.setWindowTitle(qApp->applicationName());
msgBox.setText(str.at(0));
if (str.size() > 1)
msgBox.setDetailedText(str.at(1));
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.exec();
}
dirName = QFileInfo(fileName).absolutePath();
_exit:
return;
}
void MainWindow::on_actionOpenSession_triggered()
{
openSession();
}
void MainWindow::on_actionSaveSession_triggered()
{
qDebug("Save Session Action");
static QString fileName;
QStringList fileTypes = SessionFileFormat::supportedFileTypes(
SessionFileFormat::kSaveFile);
QString fileType;
QString errorStr;
QFileDialog::Options options;
if (portsWindow->reservedPortCount()) {
QString myself = appSettings->value(kUserKey, kUserDefaultValue)
.toString();
if (QMessageBox::question(this,
tr("Save Session"),
QString("Some ports are reserved!\n\nOnly ports reserved by %1 will be saved. Proceed?").arg(myself),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) == QMessageBox::No)
goto _exit;
}
// On Mac OS with Native Dialog, getSaveFileName() ignores fileType.
// Although currently there's only one supported file type, we may
// have more in the future
#if defined(Q_OS_MAC)
options |= QFileDialog::DontUseNativeDialog;
#endif
if (fileTypes.size())
fileType = fileTypes.at(0);
_retry:
fileName = QFileDialog::getSaveFileName(this, tr("Save Session"),
fileName, fileTypes.join(";;"), &fileType, options);
if (fileName.isEmpty())
goto _exit;
if (QFileInfo(fileName).suffix().isEmpty()) {
QString fileExt = fileType.section(QRegExp("[\\*\\)]"), 1, 1);
qDebug("Adding extension '%s' to '%s'",
qPrintable(fileExt), qPrintable(fileName));
fileName.append(fileExt);
if (QFileInfo(fileName).exists()) {
if (QMessageBox::warning(this, tr("Overwrite File?"),
QString("The file \"%1\" already exists.\n\n"
"Do you wish to overwrite it?")
.arg(QFileInfo(fileName).fileName()),
QMessageBox::Yes|QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes)
goto _retry;
}
}
if (!saveSession(fileName, fileType, errorStr))
QMessageBox::critical(this, qApp->applicationName(), errorStr);
else if (!errorStr.isEmpty())
QMessageBox::warning(this, qApp->applicationName(), errorStr);
fileName = QFileInfo(fileName).absolutePath();
_exit:
return;
}
void MainWindow::on_actionPreferences_triggered()
{
Preferences *preferences = new Preferences();
preferences->exec();
delete preferences;
}
void MainWindow::on_actionViewRestoreDefaults_triggered()
{
// Use the saved default geometry/layout, however keep the
// window location same
defaultGeometry_.moveTo(geometry().topLeft());
setGeometry(defaultGeometry_);
restoreState(defaultLayout_, 0);
// Add streamStats as tabs
QList<QDockWidget*> streamStatsDocks
= findChildren<QDockWidget*>("streamStatsDock");
foreach(QDockWidget *dock, streamStatsDocks) {
dock->setFloating(false);
tabifyDockWidget(statsDock, dock);
}
statsDock->show();
statsDock->raise();
actionViewShowMyReservedPortsOnly->setChecked(false);
portsWindow->clearCurrentSelection();
statsWindow->clearCurrentSelection();
logsWindow_->clearCurrentSelection();
}
void MainWindow::on_actionHelpOnline_triggered()
{
QDesktopServices::openUrl(QUrl(jumpUrl("help", "app", "menu")));
}
void MainWindow::on_actionDonate_triggered()
{
QDesktopServices::openUrl(QUrl(jumpUrl("donate", "app", "menu")));
}
void MainWindow::on_actionCheckForUpdates_triggered()
{
Updater *updater = new Updater();
connect(updater, SIGNAL(latestVersion(QString)),
this, SLOT(onLatestVersion(QString)));
updater->checkForNewVersion();
}
void MainWindow::on_actionHelpAbout_triggered()
{
QDialog *aboutDialog = new QDialog;
Ui::About about;
about.setupUi(aboutDialog);
about.versionLabel->setText(
QString("Version: %1 Revision: %2").arg(version).arg(revision));
aboutDialog->exec();
delete aboutDialog;
}
void MainWindow::stopLocalServerMonitor()
{
// We are only interested in startup errors
disconnect(localServer_, SIGNAL(error(QProcess::ProcessError)),
this, SLOT(onLocalServerError(QProcess::ProcessError)));
disconnect(localServer_, SIGNAL(finished(int, QProcess::ExitStatus)),
this, SLOT(onLocalServerFinished(int, QProcess::ExitStatus)));
}
void MainWindow::onLocalServerFinished(int exitCode,
QProcess::ExitStatus /*exitStatus*/)
{
if (exitCode)
reportLocalServerError();
}
void MainWindow::onLocalServerError(QProcess::ProcessError /*error*/)
{
reportLocalServerError();
}
void MainWindow::reportLocalServerError()
{
QMessageBox msgBox(this);
msgBox.setIcon(QMessageBox::Warning);
msgBox.setTextFormat(Qt::RichText);
msgBox.setStyleSheet("messagebox-text-interaction-flags: 5"); // mouse copy
QString errorStr = tr("<p>Failed to start the local drone agent - "
"error 0x%1, exit status 0x%2 exit code 0x%3.</p>")
.arg(localServer_->error(), 0, 16)
.arg(localServer_->exitStatus(), 0, 16)
.arg(localServer_->exitCode(), 0, 16);
if (localServer_->error() == QProcess::FailedToStart)
errorStr.append(tr("<p>The drone program does not exist at %1 or you "
"don't have sufficient permissions to execute it."
"</p>")
.arg(QCoreApplication::applicationDirPath()));
if (localServer_->exitCode() == 1)
errorStr.append(tr("<p>The drone program was not able to bind to "
"TCP port 7878 - maybe a drone process is already "
"running?</p>"));
#ifdef Q_OS_WIN32
if (localServer_->exitCode() == STATUS_DLL_NOT_FOUND)
errorStr.append(tr("<p>This is most likely because Packet.dll "
"was not found - make sure you have "
"<a href='%1'>WinPcap"
"</a> installed.</p>")
.arg(jumpUrl("winpcap")));
#endif
msgBox.setText(errorStr);
msgBox.setInformativeText(tr("Try running drone directly."));
msgBox.exec();
QMessageBox::information(this, QString(),
tr("<p>If you have remote drone agents running, you can still add "
"and connect to them.</p>"
"<p>If you don't want to start the local drone agent at startup, "
"provide the <b>-c</b> option to Ostinato on the command line.</p>"
"<p>Learn about Ostinato's <a href='%1'>Controller-Agent "
"architecture</a></p>").arg(jumpUrl("arch")));
}
void MainWindow::onNewVersion(QString newVersion)
{
QDate today = QDate::currentDate();
QDate lastChecked = QDate::fromString(
appSettings->value(kLastUpdateCheck).toString(),
Qt::ISODate);
if (lastChecked.daysTo(today) >= 5) {
QMessageBox::information(this, tr("Update check"),
tr("<p><b>Ostinato version %1 is now available</b> (you have %2). "
"See <a href='%3'>change log</a>.</p>"
"<p>Visit <a href='%4'>ostinato.org</a> to download.</p>")
.arg(newVersion)
.arg(version)
.arg(jumpUrl("changelog", "app", "status", "update"))
.arg(jumpUrl("download", "app", "status", "update")));
}
else {
QLabel *msg = new QLabel(tr("New Ostinato version %1 available. Visit "
"<a href='%2'>ostinato.org</a> to download")
.arg(newVersion)
.arg(jumpUrl("download", "app", "status", "update")));
msg->setOpenExternalLinks(true);
statusBar()->addPermanentWidget(msg);
}
appSettings->setValue(kLastUpdateCheck, today.toString(Qt::ISODate));
sender()->deleteLater();
}
void MainWindow::onLatestVersion(QString latestVersion)
{
if (version != latestVersion) {
QMessageBox::information(this, tr("Update check"),
tr("<p><b>Ostinato version %1 is now available</b> (you have %2). "
"See <a href='%3'>change log</a>.</p>"
"<p>Visit <a href='%4'>ostinato.org</a> to download.</p>")
.arg(latestVersion)
.arg(version)
.arg(jumpUrl("changelog", "app", "status", "update"))
.arg(jumpUrl("download", "app", "status", "update")));
}
else {
QMessageBox::information(this, tr("Update check"),
tr("You are already running the latest Ostinato version - %1")
.arg(version));
}
sender()->deleteLater();
}
//! Returns true on success (or user cancel) and false on failure
bool MainWindow::openSession(QString fileName, QString &error)
{
bool ret = false;
QDialog *optDialog;
QProgressDialog progress("Opening Session", "Cancel", 0, 0, this);
OstProto::SessionContent session;
SessionFileFormat *fmt = SessionFileFormat::fileFormatFromFile(fileName);
if (fmt == NULL) {
error = tr("Unknown session file format");
goto _fail;
}
if ((optDialog = fmt->openOptionsDialog()))
{
int ret;
optDialog->setParent(this, Qt::Dialog);
ret = optDialog->exec();
optDialog->setParent(0, Qt::Dialog);
if (ret == QDialog::Rejected)
goto _user_opt_cancel;
}
progress.setAutoReset(false);
progress.setAutoClose(false);
progress.setMinimumDuration(0);
progress.show();
setDisabled(true);
progress.setEnabled(true); // to override the mainWindow disable
connect(fmt, SIGNAL(status(QString)),&progress,SLOT(setLabelText(QString)));
connect(fmt, SIGNAL(target(int)), &progress, SLOT(setMaximum(int)));
connect(fmt, SIGNAL(progress(int)), &progress, SLOT(setValue(int)));
connect(&progress, SIGNAL(canceled()), fmt, SLOT(cancel()));
fmt->openAsync(fileName, session, error);
qDebug("after open async");
while (!fmt->isFinished())
qApp->processEvents();
qDebug("wait over for async operation");
if (!fmt->result())
goto _fail;
// process any remaining events posted from the thread
for (int i = 0; i < 10; i++)
qApp->processEvents();
// XXX: user can't cancel operation from here on!
progress.close();
portsWindow->openSession(&session, error);
_user_opt_cancel:
ret = true;
_fail:
progress.close();
setEnabled(true);
return ret;
}
bool MainWindow::saveSession(QString fileName, QString fileType, QString &error)
{
bool ret = false;
QProgressDialog progress("Saving Session", "Cancel", 0, 0, this);
SessionFileFormat *fmt = SessionFileFormat::fileFormatFromType(fileType);
OstProto::SessionContent session;
if (fmt == NULL)
goto _fail;
progress.setAutoReset(false);
progress.setAutoClose(false);
progress.setMinimumDuration(0);
progress.show();
setDisabled(true);
progress.setEnabled(true); // to override the mainWindow disable
// Fill in session
ret = portsWindow->saveSession(&session, error, &progress);
if (!ret)
goto _user_cancel;
connect(fmt, SIGNAL(status(QString)),&progress,SLOT(setLabelText(QString)));
connect(fmt, SIGNAL(target(int)), &progress, SLOT(setMaximum(int)));
connect(fmt, SIGNAL(progress(int)), &progress, SLOT(setValue(int)));
connect(&progress, SIGNAL(canceled()), fmt, SLOT(cancel()));
fmt->saveAsync(session, fileName, error);
qDebug("after save async");
while (!fmt->isFinished())
qApp->processEvents();
qDebug("wait over for async operation");
ret = fmt->result();
goto _exit;
_user_cancel:
goto _exit;
_fail:
error = QString("Unsupported File Type - %1").arg(fileType);
goto _exit;
_exit:
progress.close();
setEnabled(true);
return ret;
}