add dashboards
This commit is contained in:
+379
-9
@@ -9,9 +9,13 @@
|
||||
#include "helpwindow.h"
|
||||
#include "utility.h"
|
||||
#include "filterutility.h"
|
||||
#include "dashboardwindow.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QInputDialog>
|
||||
#include <QMenu>
|
||||
#include <QShortcut>
|
||||
#include <QUuid>
|
||||
/*
|
||||
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<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));
|
||||
@@ -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<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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user