From 70835742340b0e42f34cedf3e0f80d2c1bf0e928 Mon Sep 17 00:00:00 2001 From: Void <103266275+TSC-Home@users.noreply.github.com> Date: Sat, 12 Sep 2026 13:06:26 +0200 Subject: [PATCH] add dashboards --- SavvyCAN.pro | 2 + dashboardwindow.cpp | 442 ++++++++++++++++++++++++++++++++++++++++++++ dashboardwindow.h | 45 +++++ dev.sh | 40 ++++ mainwindow.cpp | 388 +++++++++++++++++++++++++++++++++++++- mainwindow.h | 19 +- 6 files changed, 926 insertions(+), 10 deletions(-) create mode 100644 dashboardwindow.cpp create mode 100644 dashboardwindow.h create mode 100755 dev.sh diff --git a/SavvyCAN.pro b/SavvyCAN.pro index a4d399c..c0c5782 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -24,6 +24,7 @@ QMAKE_INFO_PLIST = Info.plist.template ICON = icons/SavvyIcon.icns SOURCES += main.cpp\ + dashboardwindow.cpp \ canbridgewindow.cpp \ connections/canlogserver.cpp \ connections/canserver.cpp \ @@ -108,6 +109,7 @@ SOURCES += main.cpp\ pcaplite.cpp HEADERS += mainwindow.h \ + dashboardwindow.h \ can_structs.h \ canbridgewindow.h \ canframemodel.h \ diff --git a/dashboardwindow.cpp b/dashboardwindow.cpp new file mode 100644 index 0000000..c2afed5 --- /dev/null +++ b/dashboardwindow.cpp @@ -0,0 +1,442 @@ +#include "dashboardwindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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 closeRequested; + std::function 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(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(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 DashboardWindow::toolWindows(const QString &toolId) const +{ + QList 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 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(tiles.size()))); + const int rows = qCeil(static_cast(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); +} diff --git a/dashboardwindow.h b/dashboardwindow.h new file mode 100644 index 0000000..8ee2fc1 --- /dev/null +++ b/dashboardwindow.h @@ -0,0 +1,45 @@ +#ifndef DASHBOARDWINDOW_H +#define DASHBOARDWINDOW_H + +#include + +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 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 tiles; + QSplitter *outerSplitter; + QList rowSplitters; +}; + +#endif // DASHBOARDWINDOW_H diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..b1b7994 --- /dev/null +++ b/dev.sh @@ -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" "$@" diff --git a/mainwindow.cpp b/mainwindow.cpp index b26b1c6..486ad2b 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -9,9 +9,13 @@ #include "helpwindow.h" #include "utility.h" #include "filterutility.h" +#include "dashboardwindow.h" #include +#include +#include #include +#include /* 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; dbcComparatorWindow = nullptr; canBridgeWindow = nullptr; + dashboardsMenu = nullptr; dbcHandler = DBCHandler::getReference(); bDirty = 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. connectionWindow = new ConnectionWindow(); + connectionWindow->setProperty("dashboardPersistent", true); connect(this, SIGNAL(suspendCapturing(bool)), connectionWindow, SLOT(setSuspendAll(bool))); //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->actionSingle_Multi_State_2->setVisible(false); + setupDashboards(); + QStringList headers; headers << "En" << "Bus" << "ID" << "Ext" << "Rem" << "Data" << "Interval" << "Count"; @@ -281,6 +289,11 @@ MainWindow::~MainWindow() //but eventually each window should be registered and be able to be iterated. void MainWindow::killEmAll() { + for (DashboardWindow *dashboard : dashboardWindows) + dashboard->removeToolWindow(connectionWindow); + qDeleteAll(dashboardWindows); + dashboardWindows.clear(); + foreach (GraphingWindow *win, graphWindows) { killWindow(win); @@ -780,9 +793,63 @@ void MainWindow::gridContextMenuRequest(QPoint pos) if (idx.column() == 8) //we're over the DATA column { contextMenuPosition = pos; + contextMenuMessageId = getMessageIDFromPosition(pos); + contextMenuSignalName = getSignalNameFromPosition(pos, contextMenuMessageId).trimmed(); menu->addSeparator(); menu->addAction(tr("Add to a new graphing window"), this, SLOT(setupAddToNewGraph())); 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 graphs = dashboard->toolWindows("graph"); + if (graphIndex < graphs.size()) + addSelectionToGraph(qobject_cast(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)); @@ -840,19 +907,49 @@ void MainWindow::copySelection() 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 //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. 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 lineOffset = (pos.y() - cellBaseY) / fontHeight; qDebug() << "Offset: " << lineOffset; - QString lineText = idx.data().toString().split("\n")[lineOffset]; - qDebug() << "Line Text: " << lineText; - return lineText.split(":")[0]; + const QStringList lines = idx.data().toString().split("\n"); + DBC_MESSAGE *message = dbcHandler->findMessage(messageId); + + 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) @@ -872,9 +969,17 @@ void MainWindow::setupAddToNewGraph() void MainWindow::setupSendToLatestGraphWindow() { if (!lastGraphingWindow) showGraphingWindow(); + addSelectionToGraph(lastGraphingWindow); +} + +void MainWindow::addSelectionToGraph(GraphingWindow *target) +{ + if (!target) + return; + GraphParams param; - QString signalName = getSignalNameFromPosition(contextMenuPosition); - param.ID = getMessageIDFromPosition(contextMenuPosition); + const QString signalName = contextMenuSignalName; + param.ID = contextMenuMessageId; DBC_MESSAGE *msg = dbcHandler->findMessage(param.ID); if(msg) { @@ -897,12 +1002,12 @@ void MainWindow::setupSendToLatestGraphWindow() param.drawOnlyPoints = false; 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 { 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.exec(); } @@ -1959,6 +2064,271 @@ void MainWindow::showConnectionSettingsWindow() if (!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->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 *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; } diff --git a/mainwindow.h b/mainwindow.h index f56683a..e44b30c 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -34,11 +34,14 @@ #include "re/dbccomparatorwindow.h" #include "re/udsfirmwareuploaderwindow.h" #include "canbridgewindow.h" +#include class CANConnection; class ConnectionWindow; class ISOTP_InterpreterWindow; class ScriptingWindow; +class DashboardWindow; +class QMenu; enum SIMP_COL { @@ -128,6 +131,8 @@ private slots: void headerClicked (int logicalIndex); void DBCSettingsUpdated(); void onSenderCellChanged(int, int); + void createDashboard(); + void deleteDashboard(); public slots: void gotFrames(int); @@ -203,6 +208,9 @@ private: DBCComparatorWindow *dbcComparatorWindow; CANBridgeWindow *canBridgeWindow; + QMenu *dashboardsMenu; + QMap dashboardWindows; + //various private storage QLabel lbStatusConnected; QLabel lbStatusFilename; @@ -211,10 +219,12 @@ private: int normalRowHeight; bool isConnected; QPoint contextMenuPosition; + QString contextMenuSignalName; + uint32_t contextMenuMessageId = 0; bool rowExpansionActive = false; //private methods - QString getSignalNameFromPosition(QPoint pos); + QString getSignalNameFromPosition(QPoint pos, uint32_t messageId); uint32_t getMessageIDFromPosition(QPoint pos); void copySelection(); void handleSaveDecodedMethod(bool csv); @@ -232,6 +242,13 @@ private: void disableAutoRowExpansion(); void createSenderRow(); 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