add dashboards

This commit is contained in:
Void
2026-09-12 13:06:26 +02:00
parent e403282e87
commit 7083574234
6 changed files with 926 additions and 10 deletions
+2
View File
@@ -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 \
+442
View File
@@ -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);
}
+45
View File
@@ -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
Executable
+40
View File
@@ -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
View File
@@ -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
View File
@@ -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