add dashboards
This commit is contained in:
@@ -24,6 +24,7 @@ QMAKE_INFO_PLIST = Info.plist.template
|
|||||||
ICON = icons/SavvyIcon.icns
|
ICON = icons/SavvyIcon.icns
|
||||||
|
|
||||||
SOURCES += main.cpp\
|
SOURCES += main.cpp\
|
||||||
|
dashboardwindow.cpp \
|
||||||
canbridgewindow.cpp \
|
canbridgewindow.cpp \
|
||||||
connections/canlogserver.cpp \
|
connections/canlogserver.cpp \
|
||||||
connections/canserver.cpp \
|
connections/canserver.cpp \
|
||||||
@@ -108,6 +109,7 @@ SOURCES += main.cpp\
|
|||||||
pcaplite.cpp
|
pcaplite.cpp
|
||||||
|
|
||||||
HEADERS += mainwindow.h \
|
HEADERS += mainwindow.h \
|
||||||
|
dashboardwindow.h \
|
||||||
can_structs.h \
|
can_structs.h \
|
||||||
canbridgewindow.h \
|
canbridgewindow.h \
|
||||||
canframemodel.h \
|
canframemodel.h \
|
||||||
|
|||||||
@@ -0,0 +1,442 @@
|
|||||||
|
#include "dashboardwindow.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QCloseEvent>
|
||||||
|
#include <QDrag>
|
||||||
|
#include <QDragEnterEvent>
|
||||||
|
#include <QDragLeaveEvent>
|
||||||
|
#include <QDropEvent>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMimeData>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QResizeEvent>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QtMath>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char *TileMimeType = "application/x-giescan-dashboard-tile";
|
||||||
|
}
|
||||||
|
|
||||||
|
class DashboardTile : public QFrame
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit DashboardTile(QWidget *tool, const QString &toolId, QWidget *parent = nullptr) :
|
||||||
|
QFrame(parent),
|
||||||
|
tool(tool),
|
||||||
|
dropOverlay(nullptr),
|
||||||
|
identifier(QUuid::createUuid().toString(QUuid::WithoutBraces))
|
||||||
|
{
|
||||||
|
setAcceptDrops(true);
|
||||||
|
setFrameShape(QFrame::StyledPanel);
|
||||||
|
setMinimumSize(120, 90);
|
||||||
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||||
|
setProperty("dashboardToolId", toolId);
|
||||||
|
|
||||||
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
layout->setSizeConstraint(QLayout::SetNoConstraint);
|
||||||
|
|
||||||
|
titleBar = new QWidget(this);
|
||||||
|
titleBar->setObjectName("dashboardTileTitleBar");
|
||||||
|
titleBar->setCursor(Qt::OpenHandCursor);
|
||||||
|
titleBar->setContextMenuPolicy(Qt::PreventContextMenu);
|
||||||
|
titleBar->installEventFilter(this);
|
||||||
|
|
||||||
|
QHBoxLayout *titleLayout = new QHBoxLayout(titleBar);
|
||||||
|
titleLayout->setContentsMargins(8, 2, 2, 2);
|
||||||
|
titleLayout->setSpacing(6);
|
||||||
|
|
||||||
|
QLabel *title = new QLabel(tool->windowTitle().isEmpty() ? toolId : tool->windowTitle(), titleBar);
|
||||||
|
title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||||
|
title->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
titleLayout->addWidget(title);
|
||||||
|
|
||||||
|
QToolButton *closeButton = new QToolButton(titleBar);
|
||||||
|
closeButton->setText(QString::fromUtf8("×"));
|
||||||
|
closeButton->setToolTip(QObject::tr("Remove from dashboard"));
|
||||||
|
closeButton->setAutoRaise(true);
|
||||||
|
closeButton->setFixedSize(24, 24);
|
||||||
|
titleLayout->addWidget(closeButton);
|
||||||
|
|
||||||
|
layout->addWidget(titleBar);
|
||||||
|
tool->setWindowFlags(Qt::Widget);
|
||||||
|
tool->setMinimumSize(0, 0);
|
||||||
|
tool->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
|
||||||
|
tool->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
||||||
|
layout->addWidget(tool, 1);
|
||||||
|
tool->show();
|
||||||
|
|
||||||
|
dropOverlay = new QFrame(this);
|
||||||
|
dropOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
dropOverlay->setStyleSheet(
|
||||||
|
"QFrame { background-color: rgba(40, 120, 210, 105); border: 3px solid palette(highlight); }"
|
||||||
|
"QLabel { color: white; background: transparent; border: none; font-weight: bold; }");
|
||||||
|
QVBoxLayout *overlayLayout = new QVBoxLayout(dropOverlay);
|
||||||
|
QLabel *dropLabel = new QLabel(QObject::tr("Drop here"), dropOverlay);
|
||||||
|
dropLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
overlayLayout->addWidget(dropLabel);
|
||||||
|
dropOverlay->setGeometry(rect());
|
||||||
|
dropOverlay->hide();
|
||||||
|
|
||||||
|
connect(closeButton, &QToolButton::clicked, this, [this]() {
|
||||||
|
if (closeRequested)
|
||||||
|
closeRequested(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget *toolWidget() const
|
||||||
|
{
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget *takeToolWidget()
|
||||||
|
{
|
||||||
|
QWidget *result = tool;
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
layout()->removeWidget(result);
|
||||||
|
result->setParent(nullptr);
|
||||||
|
}
|
||||||
|
tool = nullptr;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString tileId() const
|
||||||
|
{
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::function<void(DashboardTile *)> closeRequested;
|
||||||
|
std::function<void(const QString &, DashboardTile *)> tileDropped;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override
|
||||||
|
{
|
||||||
|
if (watched != titleBar)
|
||||||
|
return QFrame::eventFilter(watched, event);
|
||||||
|
|
||||||
|
if (event->type() == QEvent::ContextMenu)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (event->type() == QEvent::MouseButtonPress)
|
||||||
|
{
|
||||||
|
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||||
|
if (mouseEvent->button() == Qt::LeftButton)
|
||||||
|
{
|
||||||
|
dragStart = mouseEvent->pos();
|
||||||
|
titleBar->setCursor(Qt::ClosedHandCursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (event->type() == QEvent::MouseButtonRelease)
|
||||||
|
{
|
||||||
|
titleBar->setCursor(Qt::OpenHandCursor);
|
||||||
|
}
|
||||||
|
else if (event->type() == QEvent::MouseMove)
|
||||||
|
{
|
||||||
|
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||||
|
if ((mouseEvent->buttons() & Qt::LeftButton) &&
|
||||||
|
(mouseEvent->pos() - dragStart).manhattanLength() >= QApplication::startDragDistance())
|
||||||
|
{
|
||||||
|
QDrag *drag = new QDrag(this);
|
||||||
|
QMimeData *mimeData = new QMimeData;
|
||||||
|
mimeData->setData(TileMimeType, identifier.toUtf8());
|
||||||
|
drag->setMimeData(mimeData);
|
||||||
|
drag->exec(Qt::MoveAction);
|
||||||
|
titleBar->setCursor(Qt::OpenHandCursor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QFrame::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dragEnterEvent(QDragEnterEvent *event) override
|
||||||
|
{
|
||||||
|
if (event->mimeData()->hasFormat(TileMimeType))
|
||||||
|
{
|
||||||
|
dropOverlay->setGeometry(rect());
|
||||||
|
dropOverlay->raise();
|
||||||
|
dropOverlay->show();
|
||||||
|
event->acceptProposedAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dragLeaveEvent(QDragLeaveEvent *event) override
|
||||||
|
{
|
||||||
|
dropOverlay->hide();
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dropEvent(QDropEvent *event) override
|
||||||
|
{
|
||||||
|
if (!event->mimeData()->hasFormat(TileMimeType))
|
||||||
|
return;
|
||||||
|
|
||||||
|
dropOverlay->hide();
|
||||||
|
if (tileDropped)
|
||||||
|
tileDropped(QString::fromUtf8(event->mimeData()->data(TileMimeType)), this);
|
||||||
|
event->acceptProposedAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resizeEvent(QResizeEvent *event) override
|
||||||
|
{
|
||||||
|
QFrame::resizeEvent(event);
|
||||||
|
dropOverlay->setGeometry(rect());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QWidget *tool;
|
||||||
|
QWidget *titleBar;
|
||||||
|
QFrame *dropOverlay;
|
||||||
|
QString identifier;
|
||||||
|
QPoint dragStart;
|
||||||
|
};
|
||||||
|
|
||||||
|
DashboardWindow::DashboardWindow(const QString &dashboardId, const QString &dashboardName, QWidget *parent) :
|
||||||
|
QMainWindow(parent),
|
||||||
|
id(dashboardId),
|
||||||
|
name(dashboardName),
|
||||||
|
workspace(new QWidget(this)),
|
||||||
|
workspaceLayout(new QVBoxLayout(workspace)),
|
||||||
|
outerSplitter(nullptr)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||||
|
workspaceLayout->setContentsMargins(2, 2, 2, 2);
|
||||||
|
workspaceLayout->setSpacing(0);
|
||||||
|
workspaceLayout->setSizeConstraint(QLayout::SetNoConstraint);
|
||||||
|
workspace->setMinimumSize(0, 0);
|
||||||
|
setCentralWidget(workspace);
|
||||||
|
setDashboardName(name);
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
settings.beginGroup("Dashboards/" + id);
|
||||||
|
const QByteArray geometry = settings.value("Geometry").toByteArray();
|
||||||
|
if (geometry.isEmpty())
|
||||||
|
resize(1200, 760);
|
||||||
|
else
|
||||||
|
restoreGeometry(geometry);
|
||||||
|
settings.endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
DashboardWindow::~DashboardWindow()
|
||||||
|
{
|
||||||
|
saveLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DashboardWindow::dashboardId() const
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DashboardWindow::dashboardName() const
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DashboardWindow::setDashboardName(const QString &dashboardName)
|
||||||
|
{
|
||||||
|
name = dashboardName;
|
||||||
|
setWindowTitle(tr("Dashboard — %1").arg(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DashboardWindow::addToolWindow(QWidget *widget, const QString &toolId, const QRect &savedGeometry)
|
||||||
|
{
|
||||||
|
Q_UNUSED(savedGeometry);
|
||||||
|
if (!widget)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DashboardTile *tile = new DashboardTile(widget, toolId, workspace);
|
||||||
|
tile->closeRequested = [this](DashboardTile *requestedTile) {
|
||||||
|
closeTile(requestedTile);
|
||||||
|
};
|
||||||
|
tile->tileDropped = [this](const QString &sourceId, DashboardTile *target) {
|
||||||
|
moveTile(sourceId, target);
|
||||||
|
};
|
||||||
|
tiles.append(tile);
|
||||||
|
rebuildLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DashboardWindow::removeToolWindow(QWidget *widget)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < tiles.size(); ++i)
|
||||||
|
{
|
||||||
|
DashboardTile *tile = tiles.at(i);
|
||||||
|
if (tile->toolWidget() != widget)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
tiles.removeAt(i);
|
||||||
|
tile->takeToolWidget();
|
||||||
|
tile->deleteLater();
|
||||||
|
widget->setWindowFlags(Qt::Window);
|
||||||
|
rebuildLayout();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QWidget *> DashboardWindow::toolWindows(const QString &toolId) const
|
||||||
|
{
|
||||||
|
QList<QWidget *> result;
|
||||||
|
for (DashboardTile *tile : tiles)
|
||||||
|
{
|
||||||
|
if (tile->property("dashboardToolId").toString() == toolId && tile->toolWidget())
|
||||||
|
result.append(tile->toolWidget());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DashboardWindow::closeTile(DashboardTile *tile)
|
||||||
|
{
|
||||||
|
if (!tile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QWidget *tool = tile->toolWidget();
|
||||||
|
tiles.removeOne(tile);
|
||||||
|
if (tool && tool->property("dashboardPersistent").toBool())
|
||||||
|
{
|
||||||
|
tile->takeToolWidget();
|
||||||
|
tool->setWindowFlags(Qt::Window);
|
||||||
|
tool->hide();
|
||||||
|
}
|
||||||
|
tile->deleteLater();
|
||||||
|
rebuildLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DashboardWindow::moveTile(const QString &sourceId, DashboardTile *target)
|
||||||
|
{
|
||||||
|
int sourceIndex = -1;
|
||||||
|
const int targetIndex = tiles.indexOf(target);
|
||||||
|
for (int i = 0; i < tiles.size(); ++i)
|
||||||
|
{
|
||||||
|
if (tiles.at(i)->tileId() == sourceId)
|
||||||
|
{
|
||||||
|
sourceIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceIndex < 0 || targetIndex < 0 || sourceIndex == targetIndex)
|
||||||
|
return;
|
||||||
|
|
||||||
|
tiles.swapItemsAt(sourceIndex, targetIndex);
|
||||||
|
rebuildLayout(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DashboardWindow::rebuildLayout(bool preserveSizes)
|
||||||
|
{
|
||||||
|
QByteArray outerState;
|
||||||
|
QList<QByteArray> rowStates;
|
||||||
|
if (preserveSizes && outerSplitter)
|
||||||
|
{
|
||||||
|
outerState = outerSplitter->saveState();
|
||||||
|
for (QSplitter *row : rowSplitters)
|
||||||
|
rowStates.append(row->saveState());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DashboardTile *tile : tiles)
|
||||||
|
tile->setParent(workspace);
|
||||||
|
|
||||||
|
while (QLayoutItem *item = workspaceLayout->takeAt(0))
|
||||||
|
{
|
||||||
|
if (item->widget())
|
||||||
|
delete item->widget();
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
outerSplitter = nullptr;
|
||||||
|
rowSplitters.clear();
|
||||||
|
|
||||||
|
if (tiles.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
outerSplitter = new QSplitter(Qt::Vertical, workspace);
|
||||||
|
outerSplitter->setChildrenCollapsible(false);
|
||||||
|
outerSplitter->setHandleWidth(5);
|
||||||
|
workspaceLayout->addWidget(outerSplitter);
|
||||||
|
|
||||||
|
const int columns = qCeil(qSqrt(static_cast<double>(tiles.size())));
|
||||||
|
const int rows = qCeil(static_cast<double>(tiles.size()) / columns);
|
||||||
|
int tileIndex = 0;
|
||||||
|
for (int rowIndex = 0; rowIndex < rows; ++rowIndex)
|
||||||
|
{
|
||||||
|
QSplitter *row = new QSplitter(Qt::Horizontal, outerSplitter);
|
||||||
|
row->setChildrenCollapsible(false);
|
||||||
|
row->setHandleWidth(5);
|
||||||
|
rowSplitters.append(row);
|
||||||
|
|
||||||
|
const int remaining = tiles.size() - tileIndex;
|
||||||
|
const int tilesInRow = qMin(columns, remaining);
|
||||||
|
for (int column = 0; column < tilesInRow; ++column)
|
||||||
|
{
|
||||||
|
DashboardTile *tile = tiles.at(tileIndex++);
|
||||||
|
row->addWidget(tile);
|
||||||
|
tile->show();
|
||||||
|
}
|
||||||
|
outerSplitter->addWidget(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preserveSizes)
|
||||||
|
{
|
||||||
|
outerSplitter->restoreState(outerState);
|
||||||
|
for (int i = 0; i < qMin(rowSplitters.size(), rowStates.size()); ++i)
|
||||||
|
rowSplitters.at(i)->restoreState(rowStates.at(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DashboardWindow::restoreLayout()
|
||||||
|
{
|
||||||
|
if (!outerSplitter)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
settings.beginGroup("Dashboards/" + id);
|
||||||
|
outerSplitter->restoreState(settings.value("OuterSplitter").toByteArray());
|
||||||
|
const int count = settings.beginReadArray("RowSplitters");
|
||||||
|
for (int i = 0; i < qMin(count, rowSplitters.size()); ++i)
|
||||||
|
{
|
||||||
|
settings.setArrayIndex(i);
|
||||||
|
rowSplitters.at(i)->restoreState(settings.value("State").toByteArray());
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
settings.endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DashboardWindow::saveLayout()
|
||||||
|
{
|
||||||
|
QSettings settings;
|
||||||
|
settings.beginGroup("Dashboards/" + id);
|
||||||
|
settings.setValue("Name", name);
|
||||||
|
settings.setValue("Geometry", saveGeometry());
|
||||||
|
if (outerSplitter)
|
||||||
|
settings.setValue("OuterSplitter", outerSplitter->saveState());
|
||||||
|
|
||||||
|
settings.beginWriteArray("RowSplitters", rowSplitters.size());
|
||||||
|
for (int i = 0; i < rowSplitters.size(); ++i)
|
||||||
|
{
|
||||||
|
settings.setArrayIndex(i);
|
||||||
|
settings.setValue("State", rowSplitters.at(i)->saveState());
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
|
||||||
|
settings.beginWriteArray("Windows", tiles.size());
|
||||||
|
for (int i = 0; i < tiles.size(); ++i)
|
||||||
|
{
|
||||||
|
settings.setArrayIndex(i);
|
||||||
|
settings.setValue("Tool", tiles.at(i)->property("dashboardToolId").toString());
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
settings.endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DashboardWindow::closeEvent(QCloseEvent *event)
|
||||||
|
{
|
||||||
|
saveLayout();
|
||||||
|
QMainWindow::closeEvent(event);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#ifndef DASHBOARDWINDOW_H
|
||||||
|
#define DASHBOARDWINDOW_H
|
||||||
|
|
||||||
|
#include <QMainWindow>
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
class QSplitter;
|
||||||
|
class QVBoxLayout;
|
||||||
|
class DashboardTile;
|
||||||
|
|
||||||
|
class DashboardWindow : public QMainWindow
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DashboardWindow(const QString &dashboardId, const QString &name, QWidget *parent = nullptr);
|
||||||
|
~DashboardWindow() override;
|
||||||
|
|
||||||
|
QString dashboardId() const;
|
||||||
|
QString dashboardName() const;
|
||||||
|
void setDashboardName(const QString &name);
|
||||||
|
void addToolWindow(QWidget *widget, const QString &toolId, const QRect &geometry = QRect());
|
||||||
|
bool removeToolWindow(QWidget *widget);
|
||||||
|
QList<QWidget *> toolWindows(const QString &toolId) const;
|
||||||
|
void restoreLayout();
|
||||||
|
void saveLayout();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void closeEvent(QCloseEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void rebuildLayout(bool preserveSizes = false);
|
||||||
|
void closeTile(DashboardTile *tile);
|
||||||
|
void moveTile(const QString &sourceId, DashboardTile *target);
|
||||||
|
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QWidget *workspace;
|
||||||
|
QVBoxLayout *workspaceLayout;
|
||||||
|
QList<DashboardTile *> tiles;
|
||||||
|
QSplitter *outerSplitter;
|
||||||
|
QList<QSplitter *> rowSplitters;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DASHBOARDWINDOW_H
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BUILD_DIR="${PROJECT_DIR}/build"
|
||||||
|
|
||||||
|
if [[ -n "${QT5_QMAKE:-}" ]]; then
|
||||||
|
QMAKE_BIN="${QT5_QMAKE}"
|
||||||
|
elif command -v qmake-qt5 >/dev/null 2>&1; then
|
||||||
|
QMAKE_BIN="$(command -v qmake-qt5)"
|
||||||
|
elif [[ -x /usr/lib64/qt5/bin/qmake ]]; then
|
||||||
|
QMAKE_BIN=/usr/lib64/qt5/bin/qmake
|
||||||
|
elif [[ -x /usr/lib/qt5/bin/qmake ]]; then
|
||||||
|
QMAKE_BIN=/usr/lib/qt5/bin/qmake
|
||||||
|
elif command -v qmake >/dev/null 2>&1; then
|
||||||
|
QMAKE_BIN="$(command -v qmake)"
|
||||||
|
else
|
||||||
|
echo "error: Qt 5 qmake was not found." >&2
|
||||||
|
echo "Install the Qt 5 development packages or set QT5_QMAKE=/path/to/qmake." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
QT_VERSION="$("${QMAKE_BIN}" -query QT_VERSION)"
|
||||||
|
if [[ "${QT_VERSION%%.*}" != 5 ]]; then
|
||||||
|
echo "error: ${QMAKE_BIN} uses Qt ${QT_VERSION}; this branch requires Qt 5." >&2
|
||||||
|
echo "Install Qt 5 or run with QT5_QMAKE=/path/to/qt5/qmake ./dev.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${BUILD_DIR}"
|
||||||
|
|
||||||
|
echo "[dev] Configuring with Qt ${QT_VERSION} (${QMAKE_BIN})"
|
||||||
|
"${QMAKE_BIN}" "${PROJECT_DIR}/SavvyCAN.pro" -o "${BUILD_DIR}/Makefile"
|
||||||
|
|
||||||
|
echo "[dev] Building SavvyCAN"
|
||||||
|
make -C "${BUILD_DIR}" -j"${JOBS:-$(nproc)}"
|
||||||
|
|
||||||
|
echo "[dev] Starting SavvyCAN"
|
||||||
|
exec "${BUILD_DIR}/SavvyCAN" "$@"
|
||||||
+379
-9
@@ -9,9 +9,13 @@
|
|||||||
#include "helpwindow.h"
|
#include "helpwindow.h"
|
||||||
#include "utility.h"
|
#include "utility.h"
|
||||||
#include "filterutility.h"
|
#include "filterutility.h"
|
||||||
|
#include "dashboardwindow.h"
|
||||||
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
|
#include <QInputDialog>
|
||||||
|
#include <QMenu>
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
|
#include <QUuid>
|
||||||
/*
|
/*
|
||||||
Some notes on things I'd like to put into the program but haven't put on github (yet)
|
Some notes on things I'd like to put into the program but haven't put on github (yet)
|
||||||
|
|
||||||
@@ -99,6 +103,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||||||
temporalGraphWindow = nullptr;
|
temporalGraphWindow = nullptr;
|
||||||
dbcComparatorWindow = nullptr;
|
dbcComparatorWindow = nullptr;
|
||||||
canBridgeWindow = nullptr;
|
canBridgeWindow = nullptr;
|
||||||
|
dashboardsMenu = nullptr;
|
||||||
dbcHandler = DBCHandler::getReference();
|
dbcHandler = DBCHandler::getReference();
|
||||||
bDirty = false;
|
bDirty = false;
|
||||||
inhibitFilterUpdate = false;
|
inhibitFilterUpdate = false;
|
||||||
@@ -230,6 +235,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||||||
|
|
||||||
//Automatically create the connection window so it can be updated even if we never opened it.
|
//Automatically create the connection window so it can be updated even if we never opened it.
|
||||||
connectionWindow = new ConnectionWindow();
|
connectionWindow = new ConnectionWindow();
|
||||||
|
connectionWindow->setProperty("dashboardPersistent", true);
|
||||||
connect(this, SIGNAL(suspendCapturing(bool)), connectionWindow, SLOT(setSuspendAll(bool)));
|
connect(this, SIGNAL(suspendCapturing(bool)), connectionWindow, SLOT(setSuspendAll(bool)));
|
||||||
|
|
||||||
//Ctrl+R reloads (resets) all connections, as if resetting each device in the connection window.
|
//Ctrl+R reloads (resets) all connections, as if resetting each device in the connection window.
|
||||||
@@ -242,6 +248,8 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||||||
ui->actionMotorControlConfig->setVisible(false);
|
ui->actionMotorControlConfig->setVisible(false);
|
||||||
ui->actionSingle_Multi_State_2->setVisible(false);
|
ui->actionSingle_Multi_State_2->setVisible(false);
|
||||||
|
|
||||||
|
setupDashboards();
|
||||||
|
|
||||||
QStringList headers;
|
QStringList headers;
|
||||||
headers << "En" << "Bus" << "ID" << "Ext" << "Rem" << "Data"
|
headers << "En" << "Bus" << "ID" << "Ext" << "Rem" << "Data"
|
||||||
<< "Interval" << "Count";
|
<< "Interval" << "Count";
|
||||||
@@ -281,6 +289,11 @@ MainWindow::~MainWindow()
|
|||||||
//but eventually each window should be registered and be able to be iterated.
|
//but eventually each window should be registered and be able to be iterated.
|
||||||
void MainWindow::killEmAll()
|
void MainWindow::killEmAll()
|
||||||
{
|
{
|
||||||
|
for (DashboardWindow *dashboard : dashboardWindows)
|
||||||
|
dashboard->removeToolWindow(connectionWindow);
|
||||||
|
qDeleteAll(dashboardWindows);
|
||||||
|
dashboardWindows.clear();
|
||||||
|
|
||||||
foreach (GraphingWindow *win, graphWindows)
|
foreach (GraphingWindow *win, graphWindows)
|
||||||
{
|
{
|
||||||
killWindow(win);
|
killWindow(win);
|
||||||
@@ -780,9 +793,63 @@ void MainWindow::gridContextMenuRequest(QPoint pos)
|
|||||||
if (idx.column() == 8) //we're over the DATA column
|
if (idx.column() == 8) //we're over the DATA column
|
||||||
{
|
{
|
||||||
contextMenuPosition = pos;
|
contextMenuPosition = pos;
|
||||||
|
contextMenuMessageId = getMessageIDFromPosition(pos);
|
||||||
|
contextMenuSignalName = getSignalNameFromPosition(pos, contextMenuMessageId).trimmed();
|
||||||
menu->addSeparator();
|
menu->addSeparator();
|
||||||
menu->addAction(tr("Add to a new graphing window"), this, SLOT(setupAddToNewGraph()));
|
menu->addAction(tr("Add to a new graphing window"), this, SLOT(setupAddToNewGraph()));
|
||||||
menu->addAction(tr("Add to latest graphing window"), this, SLOT(setupSendToLatestGraphWindow()));
|
menu->addAction(tr("Add to latest graphing window"), this, SLOT(setupSendToLatestGraphWindow()));
|
||||||
|
|
||||||
|
QMenu *dashboardGraphMenu = menu->addMenu(tr("Add to graph in dashboard"));
|
||||||
|
bool foundDashboardGraph = false;
|
||||||
|
QSettings settings;
|
||||||
|
for (const QString &dashboardId : dashboardIds())
|
||||||
|
{
|
||||||
|
int graphCount = 0;
|
||||||
|
if (dashboardWindows.contains(dashboardId))
|
||||||
|
{
|
||||||
|
graphCount = dashboardWindows.value(dashboardId)->toolWindows("graph").size();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settings.beginGroup("Dashboards/" + dashboardId);
|
||||||
|
const int windowCount = settings.beginReadArray("Windows");
|
||||||
|
for (int i = 0; i < windowCount; ++i)
|
||||||
|
{
|
||||||
|
settings.setArrayIndex(i);
|
||||||
|
if (settings.value("Tool").toString() == "graph")
|
||||||
|
++graphCount;
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
settings.endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphCount == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foundDashboardGraph = true;
|
||||||
|
const QString dashboardName = settings.value("Dashboards/" + dashboardId + "/Name", tr("Dashboard")).toString();
|
||||||
|
QMenu *targetMenu = dashboardGraphMenu;
|
||||||
|
if (graphCount > 1)
|
||||||
|
targetMenu = dashboardGraphMenu->addMenu(dashboardName);
|
||||||
|
|
||||||
|
for (int graphIndex = 0; graphIndex < graphCount; ++graphIndex)
|
||||||
|
{
|
||||||
|
const QString label = graphCount == 1 ? dashboardName : tr("Graph %1").arg(graphIndex + 1);
|
||||||
|
QAction *targetAction = targetMenu->addAction(label);
|
||||||
|
connect(targetAction, &QAction::triggered, this, [this, dashboardId, graphIndex]() {
|
||||||
|
DashboardWindow *dashboard = openDashboard(dashboardId);
|
||||||
|
const QList<QWidget *> graphs = dashboard->toolWindows("graph");
|
||||||
|
if (graphIndex < graphs.size())
|
||||||
|
addSelectionToGraph(qobject_cast<GraphingWindow *>(graphs.at(graphIndex)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundDashboardGraph)
|
||||||
|
{
|
||||||
|
QAction *emptyAction = dashboardGraphMenu->addAction(tr("No dashboard contains a graph"));
|
||||||
|
emptyAction->setEnabled(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
menu->popup(ui->canFramesView->viewport()->mapToGlobal(pos));
|
menu->popup(ui->canFramesView->viewport()->mapToGlobal(pos));
|
||||||
@@ -840,19 +907,49 @@ void MainWindow::copySelection()
|
|||||||
clipboard->setText(selectedText);
|
clipboard->setText(selectedText);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MainWindow::getSignalNameFromPosition(QPoint pos)
|
QString MainWindow::getSignalNameFromPosition(QPoint pos, uint32_t messageId)
|
||||||
{
|
{
|
||||||
//there's a bit of an issue to solve. The data column is one big string even if there are a number
|
//there's a bit of an issue to solve. The data column is one big string even if there are a number
|
||||||
//of signals in there. So, the basic idea is to find out how tall the font is and where the user
|
//of signals in there. So, the basic idea is to find out how tall the font is and where the user
|
||||||
//clicked within the cell. Then find out which line that puts us over.
|
//clicked within the cell. Then find out which line that puts us over.
|
||||||
QModelIndex idx = ui->canFramesView->indexAt(pos); //figure out where in the view we clicked (row, column)
|
QModelIndex idx = ui->canFramesView->indexAt(pos); //figure out where in the view we clicked (row, column)
|
||||||
int fontHeight = ui->canFramesView->fontMetrics().height();
|
if (!idx.isValid())
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
const int fontHeight = ui->canFramesView->fontMetrics().height();
|
||||||
int cellBaseY = ui->canFramesView->rowViewportPosition(idx.row());
|
int cellBaseY = ui->canFramesView->rowViewportPosition(idx.row());
|
||||||
int lineOffset = (pos.y() - cellBaseY) / fontHeight;
|
int lineOffset = (pos.y() - cellBaseY) / fontHeight;
|
||||||
qDebug() << "Offset: " << lineOffset;
|
qDebug() << "Offset: " << lineOffset;
|
||||||
QString lineText = idx.data().toString().split("\n")[lineOffset];
|
const QStringList lines = idx.data().toString().split("\n");
|
||||||
qDebug() << "Line Text: " << lineText;
|
DBC_MESSAGE *message = dbcHandler->findMessage(messageId);
|
||||||
return lineText.split(":")[0];
|
|
||||||
|
auto signalOnLine = [message, &lines](int line) -> QString {
|
||||||
|
if (!message || line < 0 || line >= lines.size())
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
const QString lineText = lines.at(line).trimmed();
|
||||||
|
const int separator = lineText.indexOf(':');
|
||||||
|
if (separator < 1)
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
const QString candidate = lineText.left(separator).trimmed();
|
||||||
|
return message->sigHandler->findSignalByName(candidate) ? candidate : QString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normally the calculated line is exact. Depending on the platform style,
|
||||||
|
// however, the delegate can add a few pixels of vertical padding. If that
|
||||||
|
// puts the click on the byte/message line, select the closest real DBC
|
||||||
|
// signal line instead of treating the complete display line as a name.
|
||||||
|
QString signalName = signalOnLine(lineOffset);
|
||||||
|
for (int distance = 1; signalName.isEmpty() && distance < lines.size(); ++distance)
|
||||||
|
{
|
||||||
|
signalName = signalOnLine(lineOffset + distance);
|
||||||
|
if (signalName.isEmpty())
|
||||||
|
signalName = signalOnLine(lineOffset - distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Resolved signal:" << signalName;
|
||||||
|
return signalName;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t MainWindow::getMessageIDFromPosition(QPoint pos)
|
uint32_t MainWindow::getMessageIDFromPosition(QPoint pos)
|
||||||
@@ -872,9 +969,17 @@ void MainWindow::setupAddToNewGraph()
|
|||||||
void MainWindow::setupSendToLatestGraphWindow()
|
void MainWindow::setupSendToLatestGraphWindow()
|
||||||
{
|
{
|
||||||
if (!lastGraphingWindow) showGraphingWindow();
|
if (!lastGraphingWindow) showGraphingWindow();
|
||||||
|
addSelectionToGraph(lastGraphingWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::addSelectionToGraph(GraphingWindow *target)
|
||||||
|
{
|
||||||
|
if (!target)
|
||||||
|
return;
|
||||||
|
|
||||||
GraphParams param;
|
GraphParams param;
|
||||||
QString signalName = getSignalNameFromPosition(contextMenuPosition);
|
const QString signalName = contextMenuSignalName;
|
||||||
param.ID = getMessageIDFromPosition(contextMenuPosition);
|
param.ID = contextMenuMessageId;
|
||||||
DBC_MESSAGE *msg = dbcHandler->findMessage(param.ID);
|
DBC_MESSAGE *msg = dbcHandler->findMessage(param.ID);
|
||||||
if(msg)
|
if(msg)
|
||||||
{
|
{
|
||||||
@@ -897,12 +1002,12 @@ void MainWindow::setupSendToLatestGraphWindow()
|
|||||||
param.drawOnlyPoints = false;
|
param.drawOnlyPoints = false;
|
||||||
param.pointType = 0;
|
param.pointType = 0;
|
||||||
|
|
||||||
lastGraphingWindow->createGraph(param); //add the new graph to the window
|
target->createGraph(param); //add the new graph to the selected window
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
QMessageBox msgbox;
|
QMessageBox msgbox;
|
||||||
QString boxmsg = "Cannot find ID 0x" + QStringLiteral("%1").arg(param.ID, 3, 16, QLatin1Char('0')) + " in DBC message " + msg->name + ". Not adding graph.";
|
QString boxmsg = "Cannot find signal '" + signalName + "' in DBC message " + msg->name + ". Not adding graph.";
|
||||||
msgbox.setText(boxmsg);
|
msgbox.setText(boxmsg);
|
||||||
msgbox.exec();
|
msgbox.exec();
|
||||||
}
|
}
|
||||||
@@ -1959,6 +2064,271 @@ void MainWindow::showConnectionSettingsWindow()
|
|||||||
if (!connectionWindow)
|
if (!connectionWindow)
|
||||||
{
|
{
|
||||||
connectionWindow = new ConnectionWindow();
|
connectionWindow = new ConnectionWindow();
|
||||||
|
connectionWindow->setProperty("dashboardPersistent", true);
|
||||||
|
connect(this, SIGNAL(suspendCapturing(bool)), connectionWindow, SLOT(setSuspendAll(bool)));
|
||||||
}
|
}
|
||||||
|
for (DashboardWindow *dashboard : dashboardWindows)
|
||||||
|
dashboard->removeToolWindow(connectionWindow);
|
||||||
|
connectionWindow->setParent(nullptr);
|
||||||
|
connectionWindow->setWindowFlags(Qt::Window);
|
||||||
connectionWindow->show();
|
connectionWindow->show();
|
||||||
|
connectionWindow->raise();
|
||||||
|
connectionWindow->activateWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::setupDashboards()
|
||||||
|
{
|
||||||
|
dashboardsMenu = new QMenu(tr("Dashboards"), ui->menuBar);
|
||||||
|
ui->menuBar->addMenu(dashboardsMenu);
|
||||||
|
connect(dashboardsMenu, &QMenu::aboutToShow, this, &MainWindow::refreshDashboardsMenu);
|
||||||
|
|
||||||
|
struct ToolMenu
|
||||||
|
{
|
||||||
|
QMenu *menu;
|
||||||
|
QAction *action;
|
||||||
|
const char *id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolMenu tools[] = {
|
||||||
|
{ui->menu_RE_Tools, ui->actionFlow_View, "flow"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionGraph_Dta, "graph"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionFrame_Data_Analysis, "frame-analysis"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionFile_Comparison, "file-comparison"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionDBC_Comparison, "dbc-comparison"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionRange_State_2, "range-state"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionSingle_Multi_State_2, "discrete-state"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionISO_TP_Decoder, "isotp"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionSniffer, "sniffer"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionCapture_Bisector, "bisect"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionSignal_Viewer, "signal-viewer"},
|
||||||
|
{ui->menu_RE_Tools, ui->actionTemporal_Graph, "temporal-graph"},
|
||||||
|
{ui->menuSend_Frames, ui->action_Playback, "playback"},
|
||||||
|
{ui->menuSend_Frames, ui->action_Custom, "frame-sender"},
|
||||||
|
{ui->menuSend_Frames, ui->actionScripting_INterface, "scripting"},
|
||||||
|
{ui->menuSend_Frames, ui->actionFuzzing, "fuzzing"},
|
||||||
|
{ui->menuSend_Frames, ui->actionUDS_Scanner, "uds-scanner"},
|
||||||
|
{ui->menuSend_Frames, ui->actionUDS_Firmware_Update, "uds-firmware"},
|
||||||
|
{ui->menuSend_Frames, ui->actionMotorControlConfig, "motor-controller"},
|
||||||
|
{ui->menuSend_Frames, ui->actionCAN_Bridge, "can-bridge"},
|
||||||
|
{ui->menuConnection, ui->actionSetup, "connection"}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const ToolMenu &tool : tools)
|
||||||
|
{
|
||||||
|
if (tool.action->isVisible())
|
||||||
|
addDashboardTargetMenu(tool.menu, tool.action, QString::fromLatin1(tool.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::addDashboardTargetMenu(QMenu *parentMenu, QAction *windowAction, const QString &toolId)
|
||||||
|
{
|
||||||
|
const QString title = windowAction->text();
|
||||||
|
QMenu *targetMenu = new QMenu(title, parentMenu);
|
||||||
|
parentMenu->insertMenu(windowAction, targetMenu);
|
||||||
|
parentMenu->removeAction(windowAction);
|
||||||
|
windowAction->setText(tr("New Window"));
|
||||||
|
|
||||||
|
connect(targetMenu, &QMenu::aboutToShow, this, [this, targetMenu, windowAction, toolId]() {
|
||||||
|
targetMenu->clear();
|
||||||
|
targetMenu->addAction(windowAction);
|
||||||
|
|
||||||
|
const QStringList ids = dashboardIds();
|
||||||
|
if (!ids.isEmpty())
|
||||||
|
targetMenu->addSeparator();
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
for (const QString &dashboardId : ids)
|
||||||
|
{
|
||||||
|
const QString name = settings.value("Dashboards/" + dashboardId + "/Name", tr("Dashboard")).toString();
|
||||||
|
QAction *dashboardAction = targetMenu->addAction(tr("Add to %1").arg(name));
|
||||||
|
connect(dashboardAction, &QAction::triggered, this, [this, dashboardId, toolId]() {
|
||||||
|
DashboardWindow *dashboard = openDashboard(dashboardId);
|
||||||
|
dashboard->addToolWindow(createDashboardTool(toolId), toolId);
|
||||||
|
dashboard->show();
|
||||||
|
dashboard->raise();
|
||||||
|
dashboard->activateWindow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList MainWindow::dashboardIds() const
|
||||||
|
{
|
||||||
|
QSettings settings;
|
||||||
|
return settings.value("Dashboards/Ids").toStringList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::refreshDashboardsMenu()
|
||||||
|
{
|
||||||
|
dashboardsMenu->clear();
|
||||||
|
dashboardsMenu->addAction(tr("New Dashboard..."), this, &MainWindow::createDashboard);
|
||||||
|
|
||||||
|
const QStringList ids = dashboardIds();
|
||||||
|
if (!ids.isEmpty())
|
||||||
|
dashboardsMenu->addSeparator();
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
for (const QString &dashboardId : ids)
|
||||||
|
{
|
||||||
|
const QString name = settings.value("Dashboards/" + dashboardId + "/Name", tr("Dashboard")).toString();
|
||||||
|
QAction *openAction = dashboardsMenu->addAction(name);
|
||||||
|
connect(openAction, &QAction::triggered, this, [this, dashboardId]() {
|
||||||
|
openDashboard(dashboardId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ids.isEmpty())
|
||||||
|
{
|
||||||
|
dashboardsMenu->addSeparator();
|
||||||
|
dashboardsMenu->addAction(tr("Delete Dashboard..."), this, &MainWindow::deleteDashboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::createDashboard()
|
||||||
|
{
|
||||||
|
bool accepted = false;
|
||||||
|
const QString name = QInputDialog::getText(this, tr("New Dashboard"), tr("Dashboard name:"),
|
||||||
|
QLineEdit::Normal, tr("Dashboard %1").arg(dashboardIds().size() + 1),
|
||||||
|
&accepted).trimmed();
|
||||||
|
if (!accepted || name.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
QStringList ids = dashboardIds();
|
||||||
|
const QString id = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
ids.append(id);
|
||||||
|
settings.setValue("Dashboards/Ids", ids);
|
||||||
|
settings.setValue("Dashboards/" + id + "/Name", name);
|
||||||
|
openDashboard(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::deleteDashboard()
|
||||||
|
{
|
||||||
|
const QStringList ids = dashboardIds();
|
||||||
|
if (ids.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
QStringList names;
|
||||||
|
for (const QString &id : ids)
|
||||||
|
names.append(settings.value("Dashboards/" + id + "/Name", tr("Dashboard")).toString());
|
||||||
|
|
||||||
|
bool accepted = false;
|
||||||
|
const QString selected = QInputDialog::getItem(this, tr("Delete Dashboard"), tr("Dashboard:"), names, 0, false, &accepted);
|
||||||
|
if (!accepted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int index = names.indexOf(selected);
|
||||||
|
if (index < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString id = ids.at(index);
|
||||||
|
if (QMessageBox::question(this, tr("Delete Dashboard"),
|
||||||
|
tr("Delete dashboard '%1' and its saved layout?").arg(selected),
|
||||||
|
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DashboardWindow *dashboard = dashboardWindows.take(id);
|
||||||
|
if (dashboard)
|
||||||
|
dashboard->removeToolWindow(connectionWindow);
|
||||||
|
delete dashboard;
|
||||||
|
settings.remove("Dashboards/" + id);
|
||||||
|
QStringList remaining = ids;
|
||||||
|
remaining.removeAt(index);
|
||||||
|
settings.setValue("Dashboards/Ids", remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
DashboardWindow *MainWindow::openDashboard(const QString &dashboardId)
|
||||||
|
{
|
||||||
|
// Keep the desktop uncluttered: switching dashboards reuses the single
|
||||||
|
// dashboard surface instead of leaving several top-level windows visible.
|
||||||
|
for (auto it = dashboardWindows.cbegin(); it != dashboardWindows.cend(); ++it)
|
||||||
|
{
|
||||||
|
if (it.key() != dashboardId)
|
||||||
|
it.value()->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dashboardWindows.contains(dashboardId))
|
||||||
|
{
|
||||||
|
DashboardWindow *dashboard = dashboardWindows.value(dashboardId);
|
||||||
|
dashboard->show();
|
||||||
|
dashboard->raise();
|
||||||
|
dashboard->activateWindow();
|
||||||
|
return dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
settings.beginGroup("Dashboards/" + dashboardId);
|
||||||
|
const QString name = settings.value("Name", tr("Dashboard")).toString();
|
||||||
|
DashboardWindow *dashboard = new DashboardWindow(dashboardId, name);
|
||||||
|
dashboardWindows.insert(dashboardId, dashboard);
|
||||||
|
|
||||||
|
const int count = settings.beginReadArray("Windows");
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
settings.setArrayIndex(i);
|
||||||
|
const QString toolId = settings.value("Tool").toString();
|
||||||
|
const QRect geometry = settings.value("Geometry").toRect();
|
||||||
|
dashboard->addToolWindow(createDashboardTool(toolId), toolId, geometry);
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
settings.endGroup();
|
||||||
|
dashboard->restoreLayout();
|
||||||
|
|
||||||
|
dashboard->show();
|
||||||
|
dashboard->raise();
|
||||||
|
dashboard->activateWindow();
|
||||||
|
return dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget *MainWindow::createDashboardTool(const QString &toolId)
|
||||||
|
{
|
||||||
|
const QVector<CANFrame> *frames = useFiltered ? model->getFilteredListReference() : model->getListReference();
|
||||||
|
|
||||||
|
if (toolId == "graph")
|
||||||
|
{
|
||||||
|
GraphingWindow *window = new GraphingWindow(model->getListReference());
|
||||||
|
connect(window, SIGNAL(sendCenterTimeID(uint32_t,double)), this, SLOT(gotCenterTimeID(uint32_t,double)));
|
||||||
|
connect(this, SIGNAL(sendCenterTimeID(uint32_t,double)), window, SLOT(gotCenterTimeID(uint32_t,double)));
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
if (toolId == "flow")
|
||||||
|
{
|
||||||
|
FlowViewWindow *window = new FlowViewWindow(frames);
|
||||||
|
connect(window, SIGNAL(sendCenterTimeID(uint32_t,double)), this, SLOT(gotCenterTimeID(uint32_t,double)));
|
||||||
|
connect(this, SIGNAL(sendCenterTimeID(uint32_t,double)), window, SLOT(gotCenterTimeID(uint32_t,double)));
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
if (toolId == "frame-analysis") return new FrameInfoWindow(frames);
|
||||||
|
if (toolId == "file-comparison") return new FileComparatorWindow();
|
||||||
|
if (toolId == "dbc-comparison") return new DBCComparatorWindow();
|
||||||
|
if (toolId == "range-state") return new RangeStateWindow(model->getListReference());
|
||||||
|
if (toolId == "discrete-state") return new DiscreteStateWindow(model->getListReference());
|
||||||
|
if (toolId == "isotp") return new ISOTP_InterpreterWindow(frames);
|
||||||
|
if (toolId == "sniffer") return new SnifferWindow();
|
||||||
|
if (toolId == "bisect") return new BisectWindow(model->getListReference());
|
||||||
|
if (toolId == "signal-viewer") return new SignalViewerWindow(frames);
|
||||||
|
if (toolId == "temporal-graph") return new TemporalGraphWindow(frames);
|
||||||
|
if (toolId == "playback")
|
||||||
|
{
|
||||||
|
FramePlaybackWindow *window = new FramePlaybackWindow(frames);
|
||||||
|
connect(this, SIGNAL(sendCenterTimeID(uint32_t,double)), window, SLOT(gotCenterTimeID(uint32_t,double)));
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
if (toolId == "frame-sender") return new FrameSenderWindow(frames);
|
||||||
|
if (toolId == "scripting") return new ScriptingWindow(model->getListReference());
|
||||||
|
if (toolId == "fuzzing") return new FuzzingWindow(model->getListReference());
|
||||||
|
if (toolId == "uds-scanner") return new UDSScanWindow(model->getListReference());
|
||||||
|
if (toolId == "uds-firmware") return new UDSFirmwareUploaderWindow(model->getListReference());
|
||||||
|
if (toolId == "motor-controller") return new MotorControllerConfigWindow(model->getListReference());
|
||||||
|
if (toolId == "can-bridge") return new CANBridgeWindow(frames);
|
||||||
|
if (toolId == "connection")
|
||||||
|
{
|
||||||
|
for (DashboardWindow *dashboard : dashboardWindows)
|
||||||
|
dashboard->removeToolWindow(connectionWindow);
|
||||||
|
connectionWindow->hide();
|
||||||
|
return connectionWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -34,11 +34,14 @@
|
|||||||
#include "re/dbccomparatorwindow.h"
|
#include "re/dbccomparatorwindow.h"
|
||||||
#include "re/udsfirmwareuploaderwindow.h"
|
#include "re/udsfirmwareuploaderwindow.h"
|
||||||
#include "canbridgewindow.h"
|
#include "canbridgewindow.h"
|
||||||
|
#include <QMap>
|
||||||
|
|
||||||
class CANConnection;
|
class CANConnection;
|
||||||
class ConnectionWindow;
|
class ConnectionWindow;
|
||||||
class ISOTP_InterpreterWindow;
|
class ISOTP_InterpreterWindow;
|
||||||
class ScriptingWindow;
|
class ScriptingWindow;
|
||||||
|
class DashboardWindow;
|
||||||
|
class QMenu;
|
||||||
|
|
||||||
enum SIMP_COL
|
enum SIMP_COL
|
||||||
{
|
{
|
||||||
@@ -128,6 +131,8 @@ private slots:
|
|||||||
void headerClicked (int logicalIndex);
|
void headerClicked (int logicalIndex);
|
||||||
void DBCSettingsUpdated();
|
void DBCSettingsUpdated();
|
||||||
void onSenderCellChanged(int, int);
|
void onSenderCellChanged(int, int);
|
||||||
|
void createDashboard();
|
||||||
|
void deleteDashboard();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void gotFrames(int);
|
void gotFrames(int);
|
||||||
@@ -203,6 +208,9 @@ private:
|
|||||||
DBCComparatorWindow *dbcComparatorWindow;
|
DBCComparatorWindow *dbcComparatorWindow;
|
||||||
CANBridgeWindow *canBridgeWindow;
|
CANBridgeWindow *canBridgeWindow;
|
||||||
|
|
||||||
|
QMenu *dashboardsMenu;
|
||||||
|
QMap<QString, DashboardWindow *> dashboardWindows;
|
||||||
|
|
||||||
//various private storage
|
//various private storage
|
||||||
QLabel lbStatusConnected;
|
QLabel lbStatusConnected;
|
||||||
QLabel lbStatusFilename;
|
QLabel lbStatusFilename;
|
||||||
@@ -211,10 +219,12 @@ private:
|
|||||||
int normalRowHeight;
|
int normalRowHeight;
|
||||||
bool isConnected;
|
bool isConnected;
|
||||||
QPoint contextMenuPosition;
|
QPoint contextMenuPosition;
|
||||||
|
QString contextMenuSignalName;
|
||||||
|
uint32_t contextMenuMessageId = 0;
|
||||||
bool rowExpansionActive = false;
|
bool rowExpansionActive = false;
|
||||||
|
|
||||||
//private methods
|
//private methods
|
||||||
QString getSignalNameFromPosition(QPoint pos);
|
QString getSignalNameFromPosition(QPoint pos, uint32_t messageId);
|
||||||
uint32_t getMessageIDFromPosition(QPoint pos);
|
uint32_t getMessageIDFromPosition(QPoint pos);
|
||||||
void copySelection();
|
void copySelection();
|
||||||
void handleSaveDecodedMethod(bool csv);
|
void handleSaveDecodedMethod(bool csv);
|
||||||
@@ -232,6 +242,13 @@ private:
|
|||||||
void disableAutoRowExpansion();
|
void disableAutoRowExpansion();
|
||||||
void createSenderRow();
|
void createSenderRow();
|
||||||
void processSenderCellChange(int line, int col);
|
void processSenderCellChange(int line, int col);
|
||||||
|
void addSelectionToGraph(GraphingWindow *target);
|
||||||
|
void setupDashboards();
|
||||||
|
void refreshDashboardsMenu();
|
||||||
|
void addDashboardTargetMenu(QMenu *parentMenu, QAction *windowAction, const QString &toolId);
|
||||||
|
DashboardWindow *openDashboard(const QString &dashboardId);
|
||||||
|
QWidget *createDashboardTool(const QString &toolId);
|
||||||
|
QStringList dashboardIds() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // MAINWINDOW_H
|
#endif // MAINWINDOW_H
|
||||||
|
|||||||
Reference in New Issue
Block a user