Merge branch 'master' of github.com:collin80/SavvyCAN
This commit is contained in:
@@ -54,10 +54,13 @@ jobs:
|
||||
run: |
|
||||
qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro
|
||||
make -j`sysctl kern.aioprocmax | awk '{print $2}'`
|
||||
lrelease translations/*.ts
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir -p SavvyCAN.app/Contents/MacOS/translations
|
||||
mkdir -p SavvyCAN.app/Contents/MacOS/help
|
||||
cp -R translations/*.qm SavvyCAN.app/Contents/MacOS/translations
|
||||
cp -R help/* SavvyCAN.app/Contents/MacOS/help
|
||||
macdeployqt SavvyCAN.app -dmg
|
||||
mv SavvyCAN.dmg SavvyCAN_x64.dmg
|
||||
@@ -84,10 +87,13 @@ jobs:
|
||||
run: |
|
||||
qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro
|
||||
make -j`sysctl kern.aioprocmax | awk '{print $2}'`
|
||||
|
||||
lrelease translations/*.ts
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir -p SavvyCAN.app/Contents/MacOS/translations
|
||||
mkdir -p SavvyCAN.app/Contents/MacOS/help
|
||||
cp -R translations/*.qm SavvyCAN.app/Contents/MacOS/translations
|
||||
cp -R help/* SavvyCAN.app/Contents/MacOS/help
|
||||
macdeployqt SavvyCAN.app -dmg
|
||||
mv SavvyCAN.dmg SavvyCAN_arm64.dmg
|
||||
@@ -120,6 +126,7 @@ jobs:
|
||||
mkdir package
|
||||
copy "release/SavvyCAN.exe" package/
|
||||
mkdir package/help
|
||||
copy translations/*.qm package/translations/
|
||||
copy help/*.md package/help/
|
||||
mkdir package/help/images
|
||||
copy help/images/*.* package/help/images/
|
||||
|
||||
@@ -7,6 +7,9 @@ build/
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Compiled translations
|
||||
*.qm
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
@@ -36,6 +39,7 @@ SavvyCAN
|
||||
**/moc_*
|
||||
**/ui_*.h
|
||||
**/qrc_*.cpp
|
||||
.qtc_clangd
|
||||
SavvyCAN.pro.user.*
|
||||
.qmake.stash
|
||||
Makefile
|
||||
|
||||
@@ -48,7 +48,7 @@ to download it separately.
|
||||
|
||||
This project requires 5.14.0 or higher because of a dependency on QtSerialBus and other new additions to Qt.
|
||||
|
||||
NOTE: As the code in this master branch sits, it does compile with QT6. Support for QT6 is approximately "beta" quality. Most all functions should work, please send issues if found.
|
||||
NOTE: There is a work-in-progress branch QT6WIP that can compile with QT6 (Tested with Qt 6.9 on 2026-03-11). Support for QT6 is approximately "beta" quality. Most all functions should work, please send issues if found.
|
||||
|
||||
It appears that the current binary build for MacOS requires at least MacOS 10.15
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=SavvyCAN
|
||||
Name[pt_BR]=SavvyCAN
|
||||
GenericName=CANBus reverse engineering tool
|
||||
GenericName[pt_BR]=Ferramenta Rede CAN de engenharia reversa
|
||||
Comment=Facilitates reverse engineering of canbus captures
|
||||
Comment[pt_BR]=Facilitar engenharia reversa de capturas de Rede CAN
|
||||
Exec=SavvyCAN
|
||||
Icon=SavvyCAN
|
||||
Terminal=false
|
||||
Categories=Development;Electronics;IDE;
|
||||
MimeType=text/x-application;
|
||||
Keywords=embedded electronics;electronics;canbus;reverse engineering;
|
||||
Keywords[pt_BR]=eletronica embarcada;eletronicos;canbus;engenharia reversa;
|
||||
|
||||
@@ -279,3 +279,9 @@ INSTALLS += helpfiles
|
||||
|
||||
INSTALLS += target
|
||||
|
||||
TRANSLATIONS += \
|
||||
translations/SavvyCAN_en.ts \
|
||||
translations/SavvyCAN_pt_BR.ts
|
||||
|
||||
DISTFILES += \
|
||||
translations/pt_BR.qph
|
||||
|
||||
+38
-17
@@ -3,6 +3,8 @@
|
||||
#include <QFile>
|
||||
#include <QApplication>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
#include <QBrush>
|
||||
#include <QDateTime>
|
||||
#include <QSettings>
|
||||
#include "utility.h"
|
||||
@@ -76,6 +78,7 @@ CANFrameModel::CANFrameModel(QObject *parent)
|
||||
overwriteDups = false;
|
||||
filtersPersistDuringClear = false;
|
||||
useHexMode = true;
|
||||
useColorsByCanId = false;
|
||||
timeStyle = TS_MICROS;
|
||||
timeOffset = 0;
|
||||
needFilterRefresh = false;
|
||||
@@ -101,6 +104,16 @@ void CANFrameModel::setHexMode(bool mode)
|
||||
}
|
||||
}
|
||||
|
||||
void CANFrameModel::setUseColorsByCanId(bool mode)
|
||||
{
|
||||
if (useColorsByCanId != mode)
|
||||
{
|
||||
this->beginResetModel();
|
||||
useColorsByCanId = mode;
|
||||
this->endResetModel();
|
||||
}
|
||||
}
|
||||
|
||||
void CANFrameModel::setTimeStyle(TimeStyle newStyle)
|
||||
{
|
||||
if (timeStyle != newStyle)
|
||||
@@ -401,7 +414,6 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
QString tempString;
|
||||
CANFrame thisFrame;
|
||||
static bool rowFlip = false;
|
||||
QVariant ts;
|
||||
|
||||
if (!index.isValid())
|
||||
@@ -425,9 +437,31 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const
|
||||
return msg->bgColor;
|
||||
}
|
||||
}
|
||||
rowFlip = (index.row() % 2);
|
||||
if (rowFlip) return QApplication::palette().color(QPalette::Base);
|
||||
else return QApplication::palette().color(QPalette::AlternateBase);
|
||||
|
||||
if (useColorsByCanId)
|
||||
{
|
||||
QString frameStr = Utility::formatCANID(thisFrame.frameId(), thisFrame.hasExtendedFrameFormat());
|
||||
// Color rows by the value in the "category" column
|
||||
return QBrush(Utility::colorForString(frameStr));
|
||||
}
|
||||
|
||||
return (index.row() % 2) ?
|
||||
QApplication::palette().color(QPalette::Base) :
|
||||
QApplication::palette().color(QPalette::AlternateBase);
|
||||
}
|
||||
|
||||
if (role == Qt::ForegroundRole)
|
||||
{
|
||||
if (dbcHandler != nullptr && interpretFrames && !ignoreDBCColors)
|
||||
{
|
||||
DBC_MESSAGE *msg = dbcHandler->findMessage(thisFrame);
|
||||
if (msg != nullptr)
|
||||
{
|
||||
return msg->fgColor;
|
||||
}
|
||||
}
|
||||
|
||||
return QApplication::palette().color(QPalette::WindowText);
|
||||
}
|
||||
|
||||
if (role == Qt::TextAlignmentRole)
|
||||
@@ -448,19 +482,6 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
}
|
||||
|
||||
if (role == Qt::ForegroundRole)
|
||||
{
|
||||
if (dbcHandler != nullptr && interpretFrames && !ignoreDBCColors)
|
||||
{
|
||||
DBC_MESSAGE *msg = dbcHandler->findMessage(thisFrame);
|
||||
if (msg != nullptr)
|
||||
{
|
||||
return msg->fgColor;
|
||||
}
|
||||
}
|
||||
return QApplication::palette().color(QPalette::WindowText);
|
||||
}
|
||||
|
||||
if (role == Qt::DisplayRole) {
|
||||
switch (Column(index.column()))
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
bool getInterpretMode();
|
||||
void setOverwriteMode(bool);
|
||||
void setHexMode(bool);
|
||||
void setUseColorsByCanId(bool);
|
||||
void setClearMode(bool mode);
|
||||
void setTimeStyle(TimeStyle newStyle);
|
||||
void setIgnoreDBCColors(bool mode);
|
||||
@@ -94,6 +95,7 @@ private:
|
||||
QString timeFormat;
|
||||
TimeStyle timeStyle;
|
||||
bool useHexMode;
|
||||
bool useColorsByCanId;
|
||||
bool needFilterRefresh;
|
||||
bool ignoreDBCColors;
|
||||
int64_t timeOffset;
|
||||
|
||||
@@ -17,6 +17,7 @@ LAWICELSerial::LAWICELSerial(QString portName, int serialSpeed, int lawicelSpeed
|
||||
|
||||
serial = nullptr;
|
||||
isAutoRestart = false;
|
||||
rebuildLocalTimeBasis();
|
||||
|
||||
readSettings();
|
||||
}
|
||||
@@ -203,6 +204,13 @@ bool LAWICELSerial::piSendFrame(const CANFrame& frame)
|
||||
|
||||
/****************************************************************/
|
||||
|
||||
void LAWICELSerial::rebuildLocalTimeBasis()
|
||||
{
|
||||
timeBasis = 0;
|
||||
lastHWTimestamp = -1;
|
||||
wrapAdder = 0;
|
||||
}
|
||||
|
||||
void LAWICELSerial::readSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
@@ -254,6 +262,8 @@ void LAWICELSerial::deviceConnected()
|
||||
{
|
||||
sendDebug("Connecting to LAWICEL Device!");
|
||||
|
||||
rebuildLocalTimeBasis();
|
||||
|
||||
QByteArray output;
|
||||
|
||||
output.clear();
|
||||
@@ -474,8 +484,6 @@ void LAWICELSerial::readSerialData()
|
||||
QByteArray data;
|
||||
unsigned char c;
|
||||
QString debugBuild;
|
||||
CANFrame buildFrame;
|
||||
QByteArray buildData;
|
||||
|
||||
if (serial) data = serial->readAll();
|
||||
|
||||
@@ -491,41 +499,49 @@ void LAWICELSerial::readSerialData()
|
||||
{
|
||||
qDebug() << "Got CR!";
|
||||
|
||||
if (useSystemTime)
|
||||
{
|
||||
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul));
|
||||
}
|
||||
else
|
||||
CANFrame buildFrame;
|
||||
QByteArray buildData;
|
||||
|
||||
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, QDateTime::currentMSecsSinceEpoch() * 1000ll));
|
||||
if (!useSystemTime)
|
||||
{
|
||||
bool isFD = false;
|
||||
char offset;
|
||||
char len_index;
|
||||
switch(mBuildLine[0].toLatin1()) {
|
||||
case 't':
|
||||
case 'b':
|
||||
case 'd':
|
||||
isFD = true;
|
||||
[[fallthrough]];
|
||||
case 't':
|
||||
offset = 5;
|
||||
len_index = 4;
|
||||
break;
|
||||
case 'T':
|
||||
break;
|
||||
case 'B':
|
||||
case 'D':
|
||||
isFD = true;
|
||||
[[fallthrough]];
|
||||
case 'T':
|
||||
offset = 10;
|
||||
len_index = 9;
|
||||
break;
|
||||
break;
|
||||
default:
|
||||
offset = 5;
|
||||
len_index = 4;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
//If total length is greater than command, header and data, timestamps must be enabled.
|
||||
if (data.length() > (offset + mBuildLine.mid(len_index, 1).toInt() * 2 + 1))
|
||||
|
||||
int dlc = mBuildLine.mid(len_index, 1).toInt();
|
||||
int byteCount = isFD ? (int)dlc_code_to_bytes(dlc) : dlc;
|
||||
int frameStringLen = offset + byteCount * 2;
|
||||
if (mBuildLine.length() > frameStringLen + 1)
|
||||
{
|
||||
//Four bytes after the end of the data bytes.
|
||||
buildTimestamp = mBuildLine.mid(offset + mBuildLine.mid(len_index, 1).toInt() * 2, 4).toInt(nullptr, 16);
|
||||
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp));
|
||||
}
|
||||
else
|
||||
{
|
||||
//Default to system time if timestamps are disabled.
|
||||
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul));
|
||||
qint64 hardwareMs = mBuildLine.mid(frameStringLen, 4).toInt(nullptr, 16);
|
||||
if (lastHWTimestamp >= 0 && hardwareMs < lastHWTimestamp) { wrapAdder += 60000; }
|
||||
lastHWTimestamp = hardwareMs;
|
||||
qint64 unwrappedMs = wrapAdder + hardwareMs;
|
||||
if (timeBasis == 0) { timeBasis = QDateTime::currentMSecsSinceEpoch() - unwrappedMs; }
|
||||
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, (timeBasis + unwrappedMs) * 1000ll));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,9 @@ protected:
|
||||
bool isAutoRestart;
|
||||
QSerialPort *serial;
|
||||
int framesRapid;
|
||||
CANFrame buildFrame;
|
||||
qint64 buildTimestamp;
|
||||
qint64 timeBasis;
|
||||
qint64 lastHWTimestamp;
|
||||
qint64 wrapAdder;
|
||||
bool can0Enabled;
|
||||
bool can0ListenOnly;
|
||||
bool canFd;
|
||||
|
||||
+7
-1
@@ -66,6 +66,13 @@ public:
|
||||
QString descript;
|
||||
};
|
||||
|
||||
class DBC_VAL_TABLE
|
||||
{
|
||||
public:
|
||||
QString name;
|
||||
QList<DBC_VAL_ENUM_ENTRY> valList;
|
||||
};
|
||||
|
||||
class DBC_NODE
|
||||
{
|
||||
public:
|
||||
@@ -176,4 +183,3 @@ public:
|
||||
|
||||
|
||||
#endif // DBC_CLASSES_H
|
||||
|
||||
|
||||
+126
-27
@@ -16,6 +16,28 @@
|
||||
|
||||
DBCHandler* DBCHandler::instance = nullptr;
|
||||
|
||||
static bool parseValueDescriptions(QString tokenString, QList<DBC_VAL_ENUM_ENTRY> &values)
|
||||
{
|
||||
QRegularExpression regex;
|
||||
QRegularExpressionMatch match;
|
||||
DBC_VAL_ENUM_ENTRY val;
|
||||
|
||||
values.clear();
|
||||
tokenString = tokenString.trimmed();
|
||||
while (tokenString.length() > 2)
|
||||
{
|
||||
regex.setPattern("^([+-]?\\d+) \\\"(.*?)\\\"(.*)");
|
||||
match = regex.match(tokenString);
|
||||
if (!match.hasMatch()) return false;
|
||||
|
||||
val.value = match.captured(1).toInt();
|
||||
val.descript = match.captured(2);
|
||||
values.append(val);
|
||||
tokenString = match.captured(3).trimmed();
|
||||
}
|
||||
return values.count() > 0;
|
||||
}
|
||||
|
||||
DBC_SIGNAL* DBCSignalHandler::findSignalByIdx(int idx)
|
||||
{
|
||||
if (sigs.count() == 0) return nullptr;
|
||||
@@ -316,6 +338,8 @@ DBCFile::DBCFile(const DBCFile& cpy) : QObject()
|
||||
assocBuses = cpy.assocBuses;
|
||||
dbc_nodes.clear();
|
||||
dbc_nodes.append(cpy.dbc_nodes);
|
||||
dbc_value_tables.clear();
|
||||
dbc_value_tables.append(cpy.dbc_value_tables);
|
||||
dbc_attributes.clear();
|
||||
dbc_attributes.append(cpy.dbc_attributes);
|
||||
isDirty = cpy.isDirty;
|
||||
@@ -331,6 +355,8 @@ DBCFile& DBCFile::operator=(const DBCFile& cpy)
|
||||
assocBuses = cpy.assocBuses;
|
||||
dbc_nodes.clear();
|
||||
dbc_nodes.append(cpy.dbc_nodes);
|
||||
dbc_value_tables.clear();
|
||||
dbc_value_tables.append(cpy.dbc_value_tables);
|
||||
dbc_attributes.clear();
|
||||
dbc_attributes.append(cpy.dbc_attributes);
|
||||
}
|
||||
@@ -382,6 +408,19 @@ DBC_NODE* DBCFile::findNodeByNameAndComment(QString fullname)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DBC_VAL_TABLE* DBCFile::findValueTableByName(QString name)
|
||||
{
|
||||
if (dbc_value_tables.length() == 0) return nullptr;
|
||||
for (int i = 0; i < dbc_value_tables.length(); i++)
|
||||
{
|
||||
if (name.compare(dbc_value_tables[i].name, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
return &dbc_value_tables[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString DBCFile::getFullFilename()
|
||||
{
|
||||
return filePath + fileName;
|
||||
@@ -712,6 +751,29 @@ bool DBCFile::parseSignalValueTypeLine(QString line)
|
||||
}
|
||||
|
||||
|
||||
bool DBCFile::parseValueTableLine(QString line)
|
||||
{
|
||||
QRegularExpression regex;
|
||||
QRegularExpressionMatch match;
|
||||
|
||||
regex.setPattern("^VAL\\_TABLE\\_ ([-\\w]+) (.*);");
|
||||
match = regex.match(line);
|
||||
//captured 1 is the table name
|
||||
//captured 2 is a series of values in the form (number "text")
|
||||
if (match.hasMatch())
|
||||
{
|
||||
DBC_VAL_TABLE table;
|
||||
table.name = match.captured(1);
|
||||
if (!parseValueDescriptions(match.captured(2), table.valList)) return false;
|
||||
|
||||
DBC_VAL_TABLE *existingTable = findValueTableByName(table.name);
|
||||
if (existingTable) existingTable->valList = table.valList;
|
||||
else dbc_value_tables.append(table);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DBCFile::parseValueLine(QString line)
|
||||
{
|
||||
QRegularExpression regex;
|
||||
@@ -733,25 +795,18 @@ bool DBCFile::parseValueLine(QString line)
|
||||
if (sig != nullptr)
|
||||
{
|
||||
QString tokenString = match.captured(3);
|
||||
DBC_VAL_ENUM_ENTRY val;
|
||||
while (tokenString.length() > 2)
|
||||
QList<DBC_VAL_ENUM_ENTRY> values;
|
||||
DBC_VAL_TABLE *valueTable = findValueTableByName(tokenString.trimmed());
|
||||
if (valueTable)
|
||||
{
|
||||
regex.setPattern("(\\d+) \\\"(.*?)\\\"(.*)");
|
||||
match = regex.match(tokenString);
|
||||
if (match.hasMatch())
|
||||
{
|
||||
val.value = match.captured(1).toULong() & 0x1FFFFFFFul;
|
||||
val.descript = match.captured(2);
|
||||
//qDebug() << "sig val " << val.value << " desc " <<val.descript;
|
||||
sig->valList.append(val);
|
||||
int rightSize = tokenString.length() - match.captured(1).length() - match.captured(2).length() - 4;
|
||||
if (rightSize > 0) tokenString = tokenString.right(rightSize);
|
||||
else tokenString = "";
|
||||
//qDebug() << "New token string: " << tokenString;
|
||||
}
|
||||
else tokenString = "";
|
||||
sig->valList = valueTable->valList;
|
||||
return true;
|
||||
}
|
||||
if (parseValueDescriptions(tokenString, values))
|
||||
{
|
||||
sig->valList.append(values);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -919,6 +974,8 @@ bool DBCFile::loadFile(QString fileName)
|
||||
QString fileBaseName = QFileInfo(fileName).baseName();
|
||||
|
||||
bool inMultilineBU = false;
|
||||
bool inMultilineValTable = false;
|
||||
QString multilineValTable;
|
||||
|
||||
qDebug() << "DBC File: " << fileName;
|
||||
|
||||
@@ -931,6 +988,7 @@ bool DBCFile::loadFile(QString fileName)
|
||||
|
||||
qDebug() << "Starting DBC load";
|
||||
dbc_nodes.clear();
|
||||
dbc_value_tables.clear();
|
||||
messageHandler->removeAllMessages();
|
||||
messageHandler->setMatchingCriteria(EXACT);
|
||||
messageHandler->setFilterLabeling(false);
|
||||
@@ -962,6 +1020,18 @@ bool DBCFile::loadFile(QString fileName)
|
||||
else inMultilineBU = false;
|
||||
}
|
||||
|
||||
if (inMultilineValTable)
|
||||
{
|
||||
multilineValTable.append(" " + line);
|
||||
if (line.endsWith(";"))
|
||||
{
|
||||
parseValueTableLine(multilineValTable);
|
||||
multilineValTable.clear();
|
||||
inMultilineValTable = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inMultilineBU)
|
||||
{
|
||||
if (line.startsWith("BO_ ")) //defines a message
|
||||
@@ -1064,6 +1134,19 @@ bool DBCFile::loadFile(QString fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (line.startsWith("VAL_TABLE_ "))
|
||||
{
|
||||
if (line.endsWith(";"))
|
||||
{
|
||||
parseValueTableLine(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
multilineValTable = line;
|
||||
inMultilineValTable = true;
|
||||
}
|
||||
}
|
||||
|
||||
//VAL_ (1090) (VCUPresentParkLightOC) (1 "Error present" 0 "Error not present") ;
|
||||
if (line.startsWith("VAL_ "))
|
||||
{
|
||||
@@ -1347,8 +1430,8 @@ bool DBCFile::saveFile(QString fileName)
|
||||
int msgNumber = 1;
|
||||
int sigNumber = 1;
|
||||
QFile *outFile = new QFile(fileName);
|
||||
QString nodesOutput, msgOutput, commentsOutput, valuesOutput, extMultiplexOutput;
|
||||
QString defaultsOutput, attrValOutput;
|
||||
QString nodesOutput, msgOutput, commentsOutput, valuesOutput, valueTableOutput, extMultiplexOutput;
|
||||
QString defaultsOutput, attrValOutput, sigValTypeOutput;
|
||||
bool hasExtendedMultiplexing = false;
|
||||
|
||||
if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
@@ -1433,6 +1516,23 @@ bool DBCFile::saveFile(QString fileName)
|
||||
nodesOutput.append("\n");
|
||||
outFile->write(nodesOutput.toUtf8());
|
||||
|
||||
for (int x = 0; x < dbc_value_tables.count(); x++)
|
||||
{
|
||||
DBC_VAL_TABLE table = dbc_value_tables[x];
|
||||
valueTableOutput.append("VAL_TABLE_ " + table.name);
|
||||
for (int v = 0; v < table.valList.count(); v++)
|
||||
{
|
||||
DBC_VAL_ENUM_ENTRY val = table.valList[v];
|
||||
valueTableOutput.append(" " + QString::number(val.value) + " \"" + val.descript + "\"");
|
||||
}
|
||||
valueTableOutput.append(";\n");
|
||||
}
|
||||
if (!valueTableOutput.isEmpty())
|
||||
{
|
||||
outFile->write(valueTableOutput.toUtf8());
|
||||
valueTableOutput.clear();
|
||||
}
|
||||
|
||||
//Go through all messages one at at time issuing the message line then all signals in there too.
|
||||
for (int x = 0; x < messageHandler->getCount(); x++)
|
||||
{
|
||||
@@ -1512,17 +1612,11 @@ bool DBCFile::saveFile(QString fileName)
|
||||
else msgOutput.append("0+");
|
||||
break;
|
||||
case SIGNED_INT:
|
||||
case SP_FLOAT:
|
||||
case DP_FLOAT:
|
||||
if (sig->intelByteOrder) msgOutput.append("1-");
|
||||
else msgOutput.append("0-");
|
||||
break;
|
||||
case SP_FLOAT:
|
||||
if (sig->intelByteOrder) msgOutput.append("5-");
|
||||
else msgOutput.append("2-");
|
||||
break;
|
||||
case DP_FLOAT:
|
||||
if (sig->intelByteOrder) msgOutput.append("6-");
|
||||
else msgOutput.append("3-");
|
||||
break;
|
||||
case STRING:
|
||||
msgOutput.append("4-");
|
||||
break;
|
||||
@@ -1533,6 +1627,8 @@ bool DBCFile::saveFile(QString fileName)
|
||||
msgOutput.append(" (" + QString::number(sig->factor) + "," + QString::number(sig->bias) + ") [" +
|
||||
QString::number(sig->min) + "|" + QString::number(sig->max) + "] \"" + sig->unitName
|
||||
+ "\" " + sig->receiver->name + "\n");
|
||||
if (sig->valType == SP_FLOAT || sig->valType == DP_FLOAT)
|
||||
sigValTypeOutput.append("SIG_VALTYPE_ " + QString::number(ID) + " " + sig->name + " : " + QString::number(sig->valType == SP_FLOAT ? 1 : 2) + ";\n");
|
||||
if (sig->comment.length() > 0)
|
||||
{
|
||||
commentsOutput.append("CM_ SG_ " + QString::number(ID) + " " + sig->name + " \"" + sig->comment + "\";\n");
|
||||
@@ -1685,11 +1781,14 @@ bool DBCFile::saveFile(QString fileName)
|
||||
outFile->write(attrValOutput.toUtf8());
|
||||
outFile->write(valuesOutput.toUtf8());
|
||||
outFile->write(extMultiplexOutput.toUtf8());
|
||||
if (!sigValTypeOutput.isEmpty())
|
||||
outFile->write(sigValTypeOutput.toUtf8());
|
||||
|
||||
attrValOutput.clear();
|
||||
defaultsOutput.clear();
|
||||
valuesOutput.clear();
|
||||
extMultiplexOutput.clear();
|
||||
sigValTypeOutput.clear();
|
||||
|
||||
outFile->close();
|
||||
delete outFile;
|
||||
|
||||
@@ -75,6 +75,7 @@ public:
|
||||
DBC_NODE *findNodeByName(QString name);
|
||||
DBC_NODE *findNodeByNameAndComment(QString fullname);
|
||||
DBC_NODE *findNodeByIdx(int idx);
|
||||
DBC_VAL_TABLE *findValueTableByName(QString name);
|
||||
DBC_ATTRIBUTE *findAttributeByName(QString name, DBC_ATTRIBUTE_TYPE type = ATTR_TYPE_ANY);
|
||||
DBC_ATTRIBUTE *findAttributeByIdx(int idx);
|
||||
void findAttributesByType(DBC_ATTRIBUTE_TYPE typ, QList<DBC_ATTRIBUTE> *list);
|
||||
@@ -93,6 +94,7 @@ public:
|
||||
|
||||
DBCMessageHandler *messageHandler;
|
||||
QList<DBC_NODE> dbc_nodes;
|
||||
QList<DBC_VAL_TABLE> dbc_value_tables;
|
||||
QList<DBC_ATTRIBUTE> dbc_attributes;
|
||||
private:
|
||||
QString fileName;
|
||||
@@ -105,6 +107,7 @@ private:
|
||||
DBC_SIGNAL* parseSignalLine(QString line, DBC_MESSAGE *msg);
|
||||
bool parseSignalMultiplexValueLine(QString line);
|
||||
DBC_MESSAGE* parseMessageLine(QString line);
|
||||
bool parseValueTableLine(QString line);
|
||||
bool parseValueLine(QString line);
|
||||
bool parseSignalValueTypeLine(QString line);
|
||||
bool parseAttributeLine(QString line);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "mainwindow.h"
|
||||
#include <QApplication>
|
||||
#include <QTranslator>
|
||||
#include <QLocale>
|
||||
#include <QDebug>
|
||||
|
||||
class SavvyCANApplication : public QApplication
|
||||
{
|
||||
@@ -39,11 +42,31 @@ int main(int argc, char *argv[])
|
||||
a.setOrganizationName("EVTV");
|
||||
a.setApplicationName("SavvyCAN");
|
||||
a.setOrganizationDomain("evtv.me");
|
||||
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
QSettings settings;
|
||||
|
||||
QString localeString = settings.value("Main/Language").toString();
|
||||
if (localeString.isEmpty()) {
|
||||
QLocale sysLocale = QLocale::system();
|
||||
localeString = sysLocale.name();
|
||||
settings.setValue("Main/Language", localeString);
|
||||
}
|
||||
|
||||
QTranslator translator;
|
||||
QLocale locale(localeString);
|
||||
QString lang = locale.name();
|
||||
QString shortLang = locale.name().left(2);
|
||||
|
||||
if (QString translationDir = QCoreApplication::applicationDirPath() + "/translations"; !translator.load("SavvyCAN_" + lang, translationDir)) {
|
||||
translator.load("SavvyCAN_" + shortLang, translationDir);
|
||||
}
|
||||
a.installTranslator(&translator);
|
||||
|
||||
qInfo() << "Locale Value is:" << locale.name();
|
||||
|
||||
a.mainWindow = new MainWindow();
|
||||
|
||||
QSettings settings;
|
||||
int fontSize = settings.value("Main/FontSize", 9).toUInt();
|
||||
QFont sysFont = QFont(); //get default font
|
||||
sysFont.setPointSize(fontSize);
|
||||
@@ -52,8 +75,8 @@ int main(int argc, char *argv[])
|
||||
a.mainWindow->show();
|
||||
|
||||
int retCode = a.exec();
|
||||
|
||||
|
||||
delete a.mainWindow; a.mainWindow = NULL;
|
||||
|
||||
|
||||
return retCode;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#include "helpwindow.h"
|
||||
#include <qevent.h>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QRegExp>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include "simplecrypt.h"
|
||||
|
||||
//using this simple encryption library to obfuscate stored password a bit. It's not super secure but better than
|
||||
@@ -10,6 +14,42 @@
|
||||
//whatever key, you'd see it. Just behave yourselves
|
||||
SimpleCrypt crypto(Q_UINT64_C(0xdeadbeefface6285));
|
||||
|
||||
static QStringList availableLanguageCodes()
|
||||
{
|
||||
QStringList codes;
|
||||
QDir transDir(QCoreApplication::applicationDirPath() + "/translations");
|
||||
QStringList list = transDir.entryList(QStringList() << "SavvyCAN_*.qm");
|
||||
//* just for test purposes
|
||||
if (list.isEmpty()) {
|
||||
// maybe only ts files exist during development
|
||||
list = transDir.entryList(QStringList() << "SavvyCAN_*.ts");
|
||||
}
|
||||
for (QString file : list) {
|
||||
QRegExp re("SavvyCAN_(.*)\\.(qm|ts)");
|
||||
if (re.indexIn(file) != -1) {
|
||||
codes << re.cap(1);
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
|
||||
void MainSettingsDialog::populateLanguageCombo()
|
||||
{
|
||||
ui->comboLanguage->clear();
|
||||
QStringList codes = availableLanguageCodes();
|
||||
if (codes.isEmpty()) {
|
||||
codes << "en"; // fallback
|
||||
}
|
||||
for (const QString &code : codes) {
|
||||
QLocale locale(code);
|
||||
QString name = locale.nativeLanguageName();
|
||||
qInfo() << code;
|
||||
if (name.isEmpty())
|
||||
name = code;
|
||||
ui->comboLanguage->addItem(name, code);
|
||||
}
|
||||
}
|
||||
|
||||
MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::MainSettingsDialog)
|
||||
@@ -17,6 +57,8 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
|
||||
QSettings settings;
|
||||
ui->setupUi(this);
|
||||
|
||||
populateLanguageCombo();
|
||||
|
||||
//TODO: This is still hard coded to support only two buses. Sometimes there is none, sometimes 1, sometimes much more than 2. Fix this.
|
||||
ui->comboSendingBus->addItem(tr("None"));
|
||||
ui->comboSendingBus->addItem(tr("0"));
|
||||
@@ -90,7 +132,15 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
|
||||
ui->cbUseFiltered->setChecked(settings.value("Main/UseFiltered", false).toBool());
|
||||
ui->cbUseOpenGL->setChecked(settings.value("Main/UseOpenGL", false).toBool());
|
||||
ui->cbFilterLabeling->setChecked(settings.value("Main/FilterLabeling", true).toBool());
|
||||
ui->cbFilterLabeling->setChecked(settings.value("Main/FilterLabeling", true).toBool());
|
||||
ui->cbIgnoreDBCColors->setChecked(settings.value("Main/IgnoreDBCColors", false).toBool());
|
||||
ui->cbColorsByCanId->setChecked(settings.value("Main/ColorsByCanId", false).toBool());
|
||||
|
||||
if (QString savedLang = settings.value("Main/Language").toString(); !savedLang.isEmpty()) {
|
||||
int idx = ui->comboLanguage->findData(savedLang);
|
||||
if (idx >= 0)
|
||||
ui->comboLanguage->setCurrentIndex(idx);
|
||||
}
|
||||
|
||||
int maxFramesDefault;
|
||||
if (QSysInfo::WordSize > 32)
|
||||
@@ -108,6 +158,7 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
|
||||
ui->spinBytesPerLine->setValue(settings.value("Main/BytesPerLine", 8).toInt());
|
||||
|
||||
//just for simplicity they all call the same function and that function updates all settings at once
|
||||
connect(ui->comboLanguage, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSettings()));
|
||||
connect(ui->cbDisplayHex, SIGNAL(toggled(bool)), this, SLOT(updateSettings()));
|
||||
connect(ui->cbFlowAutoRef, SIGNAL(toggled(bool)), this, SLOT(updateSettings()));
|
||||
connect(ui->cbFlowUseTimestamp, SIGNAL(toggled(bool)), this, SLOT(updateSettings()));
|
||||
@@ -177,6 +228,7 @@ void MainSettingsDialog::updateSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
settings.setValue("Main/Language", ui->comboLanguage->currentData().toString());
|
||||
settings.setValue("Main/UseHex", ui->cbDisplayHex->isChecked());
|
||||
settings.setValue("FlowView/AutoRef", ui->cbFlowAutoRef->isChecked());
|
||||
settings.setValue("FlowView/UseTimestamp", ui->cbFlowUseTimestamp->isChecked());
|
||||
@@ -208,6 +260,7 @@ void MainSettingsDialog::updateSettings()
|
||||
settings.setValue("Main/MaximumFrames", ui->spinMaximumFrames->value());
|
||||
settings.setValue("Main/BytesPerLine", ui->spinBytesPerLine->value());
|
||||
settings.setValue("Main/FontFixedWidth", ui->cbFontFixedWidth->isChecked());
|
||||
settings.setValue("Main/ColorsByCanId", ui->cbColorsByCanId->isChecked());
|
||||
|
||||
settings.sync();
|
||||
emit updatedSettings();
|
||||
|
||||
@@ -25,6 +25,7 @@ public slots:
|
||||
private:
|
||||
Ui::MainSettingsDialog *ui;
|
||||
|
||||
void populateLanguageCombo();
|
||||
void closeEvent(QCloseEvent *event);
|
||||
bool eventFilter(QObject *obj, QEvent *event);
|
||||
};
|
||||
|
||||
+73
-9
@@ -10,6 +10,7 @@
|
||||
#include "utility.h"
|
||||
#include "filterutility.h"
|
||||
|
||||
#include <QClipboard>
|
||||
/*
|
||||
Some notes on things I'd like to put into the program but haven't put on github (yet)
|
||||
|
||||
@@ -36,7 +37,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
||||
#endif
|
||||
|
||||
useHex = true;
|
||||
|
||||
useColorsByCanId = false;
|
||||
selfRef = this;
|
||||
|
||||
this->setWindowTitle("Savvy CAN V" + QString::number(VERSION) + " [Built " + QString(__DATE__) +"]");
|
||||
@@ -143,6 +144,11 @@ MainWindow::MainWindow(QWidget *parent) :
|
||||
connect(ui->canFramesView, &QAbstractItemView::clicked, this, &MainWindow::gridClicked);
|
||||
connect(ui->canFramesView, &QAbstractItemView::doubleClicked, this, &MainWindow::gridDoubleClicked);
|
||||
ui->canFramesView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
copyAct = new QAction(tr("Copy"), this);
|
||||
copyAct->setShortcut(QKeySequence::Copy);
|
||||
connect(copyAct, &QAction::triggered, this, &MainWindow::copyFromTable);
|
||||
ui->canFramesView->addAction(copyAct);
|
||||
connect(ui->canFramesView, &QAbstractItemView::customContextMenuRequested, this, &MainWindow::gridContextMenuRequest);
|
||||
|
||||
connect(model, &CANFrameModel::updatedFiltersList, this, &MainWindow::updateFilterList);
|
||||
@@ -415,6 +421,9 @@ void MainWindow::readUpdateableSettings()
|
||||
model->setHexMode(useHex);
|
||||
Utility::decimalMode = !useHex;
|
||||
|
||||
useColorsByCanId = settings.value("Main/ColorsByCanId", false).toBool();
|
||||
model->setUseColorsByCanId(useColorsByCanId);
|
||||
|
||||
bool tempBool;
|
||||
TimeStyle ts = TS_MICROS;
|
||||
tempBool = settings.value("Main/TimeSeconds", false).toBool();
|
||||
@@ -741,20 +750,75 @@ void MainWindow::gridContextMenuRequest(QPoint pos)
|
||||
{
|
||||
QModelIndex idx = ui->canFramesView->indexAt(pos); //figure out where in the view we clicked (row, column)
|
||||
qDebug() << "Pos: " << pos << " Row :" << idx.row() << " Col: " << idx.column();
|
||||
qDebug() << "Data: " << idx.data();
|
||||
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
QMenu *menu = new QMenu(this);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
menu->addAction(copyAct);
|
||||
|
||||
if (idx.column() == 8) //we're over the DATA column
|
||||
{
|
||||
contextMenuPosition = pos;
|
||||
|
||||
QMenu *menu = new QMenu(this);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
menu->addAction(tr("Add to a new graphing window"), this, SLOT(setupAddToNewGraph()));
|
||||
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()));
|
||||
|
||||
menu->popup(ui->canFramesView->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
menu->popup(ui->canFramesView->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void MainWindow::copyFromTable()
|
||||
{
|
||||
copySelection();
|
||||
}
|
||||
|
||||
void MainWindow::copySelection()
|
||||
{
|
||||
QItemSelectionModel *selectionModel = ui->canFramesView->selectionModel();
|
||||
QModelIndexList selectedIndexes = selectionModel->selectedIndexes();
|
||||
|
||||
if(selectedIndexes.isEmpty())
|
||||
return;
|
||||
|
||||
// QModelIndex::operator< sorts by row and then by column.
|
||||
std::sort(selectedIndexes.begin(), selectedIndexes.end());
|
||||
|
||||
QString selectedText;
|
||||
int currentRow = -1;
|
||||
int lastRow = selectedIndexes.last().row();
|
||||
|
||||
for(const QModelIndex ¤t : selectedIndexes)
|
||||
{
|
||||
if (currentRow != -1 && current.row() != currentRow)
|
||||
{
|
||||
// remove last tab
|
||||
if (selectedText.endsWith(QLatin1Char('\t')))
|
||||
selectedText.chop(1);
|
||||
selectedText.append(QLatin1Char('\n'));
|
||||
}
|
||||
currentRow = current.row();
|
||||
|
||||
QString cellText = current.data(Qt::DisplayRole).toString();
|
||||
|
||||
// Replace newlines within a cell to avoid breaking the table structure in Excel
|
||||
cellText.replace(QLatin1Char('\n'), QLatin1String(" "));
|
||||
|
||||
selectedText.append(cellText);
|
||||
|
||||
if (current.row() != lastRow || current != selectedIndexes.last())
|
||||
{
|
||||
selectedText.append(QLatin1Char('\t'));
|
||||
}
|
||||
}
|
||||
|
||||
// remove last tab if it exists
|
||||
if (selectedText.endsWith(QLatin1Char('\t')))
|
||||
selectedText.chop(1);
|
||||
|
||||
QClipboard *clipboard = QApplication::clipboard();
|
||||
clipboard->setText(selectedText);
|
||||
}
|
||||
|
||||
QString MainWindow::getSignalNameFromPosition(QPoint pos)
|
||||
|
||||
@@ -110,6 +110,7 @@ private slots:
|
||||
void gridClicked(QModelIndex);
|
||||
void gridDoubleClicked(QModelIndex);
|
||||
void gridContextMenuRequest(QPoint pos);
|
||||
void copyFromTable();
|
||||
void setupAddToNewGraph();
|
||||
void setupSendToLatestGraphWindow();
|
||||
void interpretToggled(bool);
|
||||
@@ -147,6 +148,7 @@ signals:
|
||||
|
||||
private:
|
||||
Ui::MainWindow *ui;
|
||||
QAction *copyAct;
|
||||
static MainWindow *selfRef;
|
||||
|
||||
//canbus related data
|
||||
@@ -160,6 +162,7 @@ private:
|
||||
int rxFrames;
|
||||
bool inhibitFilterUpdate;
|
||||
bool useHex;
|
||||
bool useColorsByCanId;
|
||||
bool allowCapture;
|
||||
bool ignoreDBCColors;
|
||||
bool CSVAbsTime;
|
||||
@@ -213,6 +216,7 @@ private:
|
||||
//private methods
|
||||
QString getSignalNameFromPosition(QPoint pos);
|
||||
uint32_t getMessageIDFromPosition(QPoint pos);
|
||||
void copySelection();
|
||||
void handleSaveDecodedMethod(bool csv);
|
||||
void saveDecodedTextFile(QString);
|
||||
void saveDecodedTextFileAsColumns(QString);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Scripts
|
||||
|
||||
To run this scripts, first you must authorize your terminal to run:
|
||||
|
||||
```bash
|
||||
chmod +x script/*.sh
|
||||
```
|
||||
|
||||
Then you can run by using this command:
|
||||
|
||||
```bash
|
||||
./scripts/the/script/you/want/to/run
|
||||
```
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_ROOT="$(pwd)"
|
||||
|
||||
# To add this tool in Qt Creator:
|
||||
# Category: Linguist
|
||||
# Name: Release Translations (lrelease)
|
||||
# Executable: lrelease
|
||||
# Arguments: SavvyCAN.pro
|
||||
# Working Directory: %{ActiveProject:Path}
|
||||
|
||||
echo "Generating .qm files..."
|
||||
|
||||
lrelease -verbose "$PROJECT_ROOT"/SavvyCAN.pro
|
||||
|
||||
echo ".qm files generated."
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_ROOT="$(pwd)"
|
||||
|
||||
# To add this tool in Qt Creator:
|
||||
# Category: Linguist
|
||||
# Name: Update Translations (lupdate)
|
||||
# Executable: lupdate
|
||||
# Arguments: -project SavvyCAN.pro
|
||||
# Working Directory: %{ActiveProject:Path}
|
||||
|
||||
echo "Updating translation files..."
|
||||
|
||||
lupdate -verbose "$PROJECT_ROOT"/SavvyCAN.pro
|
||||
|
||||
echo "Translation files updated."
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE QPH>
|
||||
<QPH sourcelanguage="en" language="pt_BR">
|
||||
<phrase>
|
||||
<source>New</source>
|
||||
<target>Novo</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>Save</source>
|
||||
<target>Salvar</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>Cancel</source>
|
||||
<target>Cancelar</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>Connect</source>
|
||||
<target>Conectar</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>Disconnect</source>
|
||||
<target>Desconectar</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>Yes</source>
|
||||
<target>Sim</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>No</source>
|
||||
<target>Não</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>Bus</source>
|
||||
<target>Barramento</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>Frame</source>
|
||||
<target>Frame</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>Frames</source>
|
||||
<target>Frames</target>
|
||||
</phrase>
|
||||
<phrase>
|
||||
<source>Scan</source>
|
||||
<target>Varredura</target>
|
||||
</phrase>
|
||||
</QPH>
|
||||
@@ -53,6 +53,20 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_13">
|
||||
<property name="text">
|
||||
<string>Language</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboLanguage"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -97,6 +111,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbColorsByCanId">
|
||||
<property name="text">
|
||||
<string>Color rows by CAN ID</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbUseOpenGL">
|
||||
<property name="text">
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include <QRect>
|
||||
#include <QComboBox>
|
||||
#include <QStandardItemModel>
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include <array>
|
||||
//#include <QDesktopWidget>
|
||||
|
||||
enum TimeStyle
|
||||
@@ -20,6 +23,44 @@ enum TimeStyle
|
||||
TS_CLOCK
|
||||
};
|
||||
|
||||
|
||||
// 32 dark, saturated colors — all designed for readability on white backgrounds.
|
||||
// Minimum contrast ratio ≥ 4.5:1 against white (WCAG AA).
|
||||
static constexpr std::array<QRgb, 32> kRowPalette = {{
|
||||
0xFFC62828, // 0 Red 800
|
||||
0xFFE53935, // 1 Red 600
|
||||
0xFFD84315, // 2 Deep Orange 800
|
||||
0xFFEF6C00, // 3 Orange 800
|
||||
0xFFF9A825, // 4 Amber 800
|
||||
0xFF558B2F, // 5 Green 800
|
||||
0xFF2E7D32, // 6 Green 900
|
||||
0xFF00695C, // 7 Teal 800
|
||||
0xFF00838F, // 8 Cyan 800
|
||||
0xFF0277BD, // 9 Blue 800
|
||||
0xFF1565C0, // 10 Blue 900
|
||||
0xFF283593, // 11 Indigo 900
|
||||
0xFF4527A0, // 12 Deep Purple 800
|
||||
0xFF6A1B9A, // 13 Purple 900
|
||||
0xFF880E4F, // 14 Pink 900
|
||||
0xFFAD1457, // 15 Pink 800
|
||||
0xFF37474F, // 16 Blue Grey 800
|
||||
0xFF4E342E, // 17 Brown 800
|
||||
0xFF827717, // 18 Lime 900
|
||||
0xFF1B5E20, // 19 Green 900 alt
|
||||
0xFF006064, // 20 Cyan 900
|
||||
0xFF01579B, // 21 Light Blue 900
|
||||
0xFF0D47A1, // 22 Blue 900 alt
|
||||
0xFF311B92, // 23 Deep Purple 900
|
||||
0xFF4A148C, // 24 Purple 900 alt
|
||||
0xFFB71C1C, // 25 Red 900
|
||||
0xFFBF360C, // 26 Deep Orange 900
|
||||
0xFFE65100, // 27 Orange 900
|
||||
0xFF33691E, // 28 Light Green 900
|
||||
0xFF004D40, // 29 Teal 900
|
||||
0xFF263238, // 30 Blue Grey 900
|
||||
0xFF5D4037, // 31 Brown 700
|
||||
}};
|
||||
|
||||
class Utility
|
||||
{
|
||||
public:
|
||||
@@ -344,6 +385,35 @@ public:
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// FNV-1a 32-bit hash — fast, good distribution, no dependencies.
|
||||
static quint32 hashString(const QString &s)
|
||||
{
|
||||
quint32 hash = 2166136261u;
|
||||
for (const QChar ch : s) {
|
||||
hash ^= static_cast<quint32>(ch.unicode());
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// Returns a background QColor deterministically chosen from the palette.
|
||||
// The same string always maps to the same color.
|
||||
static QColor colorForString(const QString &s)
|
||||
{
|
||||
const quint32 idx = hashString(s) % static_cast<quint32>(kRowPalette.size());
|
||||
// Use a light tint (alpha ~18%) as the row background so text stays readable.
|
||||
QColor base(kRowPalette[idx]);
|
||||
base.setAlpha(46); // ~18% — comfortable on white, still clearly tinted
|
||||
return base;
|
||||
}
|
||||
|
||||
// Opaque variant — useful for badges, tags, or legend swatches.
|
||||
static QColor solidColorForString(const QString &s)
|
||||
{
|
||||
const quint32 idx = hashString(s) % static_cast<quint32>(kRowPalette.size());
|
||||
return QColor(kRowPalette[idx]);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // UTILITY_H
|
||||
|
||||
Reference in New Issue
Block a user