Merge branch 'master' of github.com:collin80/SavvyCAN

This commit is contained in:
Collin Kidder
2026-05-14 21:03:14 -04:00
25 changed files with 7821 additions and 83 deletions
+7
View File
@@ -54,10 +54,13 @@ jobs:
run: | run: |
qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro
make -j`sysctl kern.aioprocmax | awk '{print $2}'` make -j`sysctl kern.aioprocmax | awk '{print $2}'`
lrelease translations/*.ts
- name: Package - name: Package
run: | run: |
mkdir -p SavvyCAN.app/Contents/MacOS/translations
mkdir -p SavvyCAN.app/Contents/MacOS/help mkdir -p SavvyCAN.app/Contents/MacOS/help
cp -R translations/*.qm SavvyCAN.app/Contents/MacOS/translations
cp -R help/* SavvyCAN.app/Contents/MacOS/help cp -R help/* SavvyCAN.app/Contents/MacOS/help
macdeployqt SavvyCAN.app -dmg macdeployqt SavvyCAN.app -dmg
mv SavvyCAN.dmg SavvyCAN_x64.dmg mv SavvyCAN.dmg SavvyCAN_x64.dmg
@@ -84,10 +87,13 @@ jobs:
run: | run: |
qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro
make -j`sysctl kern.aioprocmax | awk '{print $2}'` make -j`sysctl kern.aioprocmax | awk '{print $2}'`
lrelease translations/*.ts
- name: Package - name: Package
run: | run: |
mkdir -p SavvyCAN.app/Contents/MacOS/translations
mkdir -p SavvyCAN.app/Contents/MacOS/help mkdir -p SavvyCAN.app/Contents/MacOS/help
cp -R translations/*.qm SavvyCAN.app/Contents/MacOS/translations
cp -R help/* SavvyCAN.app/Contents/MacOS/help cp -R help/* SavvyCAN.app/Contents/MacOS/help
macdeployqt SavvyCAN.app -dmg macdeployqt SavvyCAN.app -dmg
mv SavvyCAN.dmg SavvyCAN_arm64.dmg mv SavvyCAN.dmg SavvyCAN_arm64.dmg
@@ -120,6 +126,7 @@ jobs:
mkdir package mkdir package
copy "release/SavvyCAN.exe" package/ copy "release/SavvyCAN.exe" package/
mkdir package/help mkdir package/help
copy translations/*.qm package/translations/
copy help/*.md package/help/ copy help/*.md package/help/
mkdir package/help/images mkdir package/help/images
copy help/images/*.* package/help/images/ copy help/images/*.* package/help/images/
+4
View File
@@ -7,6 +7,9 @@ build/
*.o *.o
*.obj *.obj
# Compiled translations
*.qm
# Precompiled Headers # Precompiled Headers
*.gch *.gch
*.pch *.pch
@@ -36,6 +39,7 @@ SavvyCAN
**/moc_* **/moc_*
**/ui_*.h **/ui_*.h
**/qrc_*.cpp **/qrc_*.cpp
.qtc_clangd
SavvyCAN.pro.user.* SavvyCAN.pro.user.*
.qmake.stash .qmake.stash
Makefile Makefile
+1 -1
View File
@@ -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. 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 It appears that the current binary build for MacOS requires at least MacOS 10.15
+4
View File
@@ -1,11 +1,15 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=SavvyCAN Name=SavvyCAN
Name[pt_BR]=SavvyCAN
GenericName=CANBus reverse engineering tool GenericName=CANBus reverse engineering tool
GenericName[pt_BR]=Ferramenta Rede CAN de engenharia reversa
Comment=Facilitates reverse engineering of canbus captures Comment=Facilitates reverse engineering of canbus captures
Comment[pt_BR]=Facilitar engenharia reversa de capturas de Rede CAN
Exec=SavvyCAN Exec=SavvyCAN
Icon=SavvyCAN Icon=SavvyCAN
Terminal=false Terminal=false
Categories=Development;Electronics;IDE; Categories=Development;Electronics;IDE;
MimeType=text/x-application; MimeType=text/x-application;
Keywords=embedded electronics;electronics;canbus;reverse engineering; Keywords=embedded electronics;electronics;canbus;reverse engineering;
Keywords[pt_BR]=eletronica embarcada;eletronicos;canbus;engenharia reversa;
+6
View File
@@ -279,3 +279,9 @@ INSTALLS += helpfiles
INSTALLS += target INSTALLS += target
TRANSLATIONS += \
translations/SavvyCAN_en.ts \
translations/SavvyCAN_pt_BR.ts
DISTFILES += \
translations/pt_BR.qph
+38 -17
View File
@@ -3,6 +3,8 @@
#include <QFile> #include <QFile>
#include <QApplication> #include <QApplication>
#include <QPalette> #include <QPalette>
#include <QColor>
#include <QBrush>
#include <QDateTime> #include <QDateTime>
#include <QSettings> #include <QSettings>
#include "utility.h" #include "utility.h"
@@ -76,6 +78,7 @@ CANFrameModel::CANFrameModel(QObject *parent)
overwriteDups = false; overwriteDups = false;
filtersPersistDuringClear = false; filtersPersistDuringClear = false;
useHexMode = true; useHexMode = true;
useColorsByCanId = false;
timeStyle = TS_MICROS; timeStyle = TS_MICROS;
timeOffset = 0; timeOffset = 0;
needFilterRefresh = false; 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) void CANFrameModel::setTimeStyle(TimeStyle newStyle)
{ {
if (timeStyle != newStyle) if (timeStyle != newStyle)
@@ -401,7 +414,6 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const
{ {
QString tempString; QString tempString;
CANFrame thisFrame; CANFrame thisFrame;
static bool rowFlip = false;
QVariant ts; QVariant ts;
if (!index.isValid()) if (!index.isValid())
@@ -425,9 +437,31 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const
return msg->bgColor; return msg->bgColor;
} }
} }
rowFlip = (index.row() % 2);
if (rowFlip) return QApplication::palette().color(QPalette::Base); if (useColorsByCanId)
else return QApplication::palette().color(QPalette::AlternateBase); {
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) 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) { if (role == Qt::DisplayRole) {
switch (Column(index.column())) switch (Column(index.column()))
{ {
+2
View File
@@ -47,6 +47,7 @@ public:
bool getInterpretMode(); bool getInterpretMode();
void setOverwriteMode(bool); void setOverwriteMode(bool);
void setHexMode(bool); void setHexMode(bool);
void setUseColorsByCanId(bool);
void setClearMode(bool mode); void setClearMode(bool mode);
void setTimeStyle(TimeStyle newStyle); void setTimeStyle(TimeStyle newStyle);
void setIgnoreDBCColors(bool mode); void setIgnoreDBCColors(bool mode);
@@ -94,6 +95,7 @@ private:
QString timeFormat; QString timeFormat;
TimeStyle timeStyle; TimeStyle timeStyle;
bool useHexMode; bool useHexMode;
bool useColorsByCanId;
bool needFilterRefresh; bool needFilterRefresh;
bool ignoreDBCColors; bool ignoreDBCColors;
int64_t timeOffset; int64_t timeOffset;
+35 -19
View File
@@ -17,6 +17,7 @@ LAWICELSerial::LAWICELSerial(QString portName, int serialSpeed, int lawicelSpeed
serial = nullptr; serial = nullptr;
isAutoRestart = false; isAutoRestart = false;
rebuildLocalTimeBasis();
readSettings(); readSettings();
} }
@@ -203,6 +204,13 @@ bool LAWICELSerial::piSendFrame(const CANFrame& frame)
/****************************************************************/ /****************************************************************/
void LAWICELSerial::rebuildLocalTimeBasis()
{
timeBasis = 0;
lastHWTimestamp = -1;
wrapAdder = 0;
}
void LAWICELSerial::readSettings() void LAWICELSerial::readSettings()
{ {
QSettings settings; QSettings settings;
@@ -254,6 +262,8 @@ void LAWICELSerial::deviceConnected()
{ {
sendDebug("Connecting to LAWICEL Device!"); sendDebug("Connecting to LAWICEL Device!");
rebuildLocalTimeBasis();
QByteArray output; QByteArray output;
output.clear(); output.clear();
@@ -474,8 +484,6 @@ void LAWICELSerial::readSerialData()
QByteArray data; QByteArray data;
unsigned char c; unsigned char c;
QString debugBuild; QString debugBuild;
CANFrame buildFrame;
QByteArray buildData;
if (serial) data = serial->readAll(); if (serial) data = serial->readAll();
@@ -491,22 +499,29 @@ void LAWICELSerial::readSerialData()
{ {
qDebug() << "Got CR!"; qDebug() << "Got CR!";
if (useSystemTime) CANFrame buildFrame;
{ QByteArray buildData;
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul));
} buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, QDateTime::currentMSecsSinceEpoch() * 1000ll));
else if (!useSystemTime)
{ {
bool isFD = false;
char offset; char offset;
char len_index; char len_index;
switch(mBuildLine[0].toLatin1()) { switch(mBuildLine[0].toLatin1()) {
case 't': case 'b':
case 'd': case 'd':
isFD = true;
[[fallthrough]];
case 't':
offset = 5; offset = 5;
len_index = 4; len_index = 4;
break; break;
case 'T': case 'B':
case 'D': case 'D':
isFD = true;
[[fallthrough]];
case 'T':
offset = 10; offset = 10;
len_index = 9; len_index = 9;
break; break;
@@ -515,17 +530,18 @@ void LAWICELSerial::readSerialData()
len_index = 4; 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. qint64 hardwareMs = mBuildLine.mid(frameStringLen, 4).toInt(nullptr, 16);
buildTimestamp = mBuildLine.mid(offset + mBuildLine.mid(len_index, 1).toInt() * 2, 4).toInt(nullptr, 16); if (lastHWTimestamp >= 0 && hardwareMs < lastHWTimestamp) { wrapAdder += 60000; }
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); lastHWTimestamp = hardwareMs;
} qint64 unwrappedMs = wrapAdder + hardwareMs;
else if (timeBasis == 0) { timeBasis = QDateTime::currentMSecsSinceEpoch() - unwrappedMs; }
{ buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, (timeBasis + unwrappedMs) * 1000ll));
//Default to system time if timestamps are disabled.
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul));
} }
} }
+3 -2
View File
@@ -60,8 +60,9 @@ protected:
bool isAutoRestart; bool isAutoRestart;
QSerialPort *serial; QSerialPort *serial;
int framesRapid; int framesRapid;
CANFrame buildFrame; qint64 timeBasis;
qint64 buildTimestamp; qint64 lastHWTimestamp;
qint64 wrapAdder;
bool can0Enabled; bool can0Enabled;
bool can0ListenOnly; bool can0ListenOnly;
bool canFd; bool canFd;
+7 -1
View File
@@ -66,6 +66,13 @@ public:
QString descript; QString descript;
}; };
class DBC_VAL_TABLE
{
public:
QString name;
QList<DBC_VAL_ENUM_ENTRY> valList;
};
class DBC_NODE class DBC_NODE
{ {
public: public:
@@ -176,4 +183,3 @@ public:
#endif // DBC_CLASSES_H #endif // DBC_CLASSES_H
+126 -27
View File
@@ -16,6 +16,28 @@
DBCHandler* DBCHandler::instance = nullptr; 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) DBC_SIGNAL* DBCSignalHandler::findSignalByIdx(int idx)
{ {
if (sigs.count() == 0) return nullptr; if (sigs.count() == 0) return nullptr;
@@ -316,6 +338,8 @@ DBCFile::DBCFile(const DBCFile& cpy) : QObject()
assocBuses = cpy.assocBuses; assocBuses = cpy.assocBuses;
dbc_nodes.clear(); dbc_nodes.clear();
dbc_nodes.append(cpy.dbc_nodes); dbc_nodes.append(cpy.dbc_nodes);
dbc_value_tables.clear();
dbc_value_tables.append(cpy.dbc_value_tables);
dbc_attributes.clear(); dbc_attributes.clear();
dbc_attributes.append(cpy.dbc_attributes); dbc_attributes.append(cpy.dbc_attributes);
isDirty = cpy.isDirty; isDirty = cpy.isDirty;
@@ -331,6 +355,8 @@ DBCFile& DBCFile::operator=(const DBCFile& cpy)
assocBuses = cpy.assocBuses; assocBuses = cpy.assocBuses;
dbc_nodes.clear(); dbc_nodes.clear();
dbc_nodes.append(cpy.dbc_nodes); dbc_nodes.append(cpy.dbc_nodes);
dbc_value_tables.clear();
dbc_value_tables.append(cpy.dbc_value_tables);
dbc_attributes.clear(); dbc_attributes.clear();
dbc_attributes.append(cpy.dbc_attributes); dbc_attributes.append(cpy.dbc_attributes);
} }
@@ -382,6 +408,19 @@ DBC_NODE* DBCFile::findNodeByNameAndComment(QString fullname)
return nullptr; 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() QString DBCFile::getFullFilename()
{ {
return filePath + fileName; 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) bool DBCFile::parseValueLine(QString line)
{ {
QRegularExpression regex; QRegularExpression regex;
@@ -733,26 +795,19 @@ bool DBCFile::parseValueLine(QString line)
if (sig != nullptr) if (sig != nullptr)
{ {
QString tokenString = match.captured(3); QString tokenString = match.captured(3);
DBC_VAL_ENUM_ENTRY val; QList<DBC_VAL_ENUM_ENTRY> values;
while (tokenString.length() > 2) DBC_VAL_TABLE *valueTable = findValueTableByName(tokenString.trimmed());
if (valueTable)
{ {
regex.setPattern("(\\d+) \\\"(.*?)\\\"(.*)"); sig->valList = valueTable->valList;
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 = "";
}
return true; return true;
} }
if (parseValueDescriptions(tokenString, values))
{
sig->valList.append(values);
return true;
}
}
} }
} }
return false; return false;
@@ -919,6 +974,8 @@ bool DBCFile::loadFile(QString fileName)
QString fileBaseName = QFileInfo(fileName).baseName(); QString fileBaseName = QFileInfo(fileName).baseName();
bool inMultilineBU = false; bool inMultilineBU = false;
bool inMultilineValTable = false;
QString multilineValTable;
qDebug() << "DBC File: " << fileName; qDebug() << "DBC File: " << fileName;
@@ -931,6 +988,7 @@ bool DBCFile::loadFile(QString fileName)
qDebug() << "Starting DBC load"; qDebug() << "Starting DBC load";
dbc_nodes.clear(); dbc_nodes.clear();
dbc_value_tables.clear();
messageHandler->removeAllMessages(); messageHandler->removeAllMessages();
messageHandler->setMatchingCriteria(EXACT); messageHandler->setMatchingCriteria(EXACT);
messageHandler->setFilterLabeling(false); messageHandler->setFilterLabeling(false);
@@ -962,6 +1020,18 @@ bool DBCFile::loadFile(QString fileName)
else inMultilineBU = false; else inMultilineBU = false;
} }
if (inMultilineValTable)
{
multilineValTable.append(" " + line);
if (line.endsWith(";"))
{
parseValueTableLine(multilineValTable);
multilineValTable.clear();
inMultilineValTable = false;
}
continue;
}
if (!inMultilineBU) if (!inMultilineBU)
{ {
if (line.startsWith("BO_ ")) //defines a message 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") ; //VAL_ (1090) (VCUPresentParkLightOC) (1 "Error present" 0 "Error not present") ;
if (line.startsWith("VAL_ ")) if (line.startsWith("VAL_ "))
{ {
@@ -1347,8 +1430,8 @@ bool DBCFile::saveFile(QString fileName)
int msgNumber = 1; int msgNumber = 1;
int sigNumber = 1; int sigNumber = 1;
QFile *outFile = new QFile(fileName); QFile *outFile = new QFile(fileName);
QString nodesOutput, msgOutput, commentsOutput, valuesOutput, extMultiplexOutput; QString nodesOutput, msgOutput, commentsOutput, valuesOutput, valueTableOutput, extMultiplexOutput;
QString defaultsOutput, attrValOutput; QString defaultsOutput, attrValOutput, sigValTypeOutput;
bool hasExtendedMultiplexing = false; bool hasExtendedMultiplexing = false;
if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text))
@@ -1433,6 +1516,23 @@ bool DBCFile::saveFile(QString fileName)
nodesOutput.append("\n"); nodesOutput.append("\n");
outFile->write(nodesOutput.toUtf8()); 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. //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++) for (int x = 0; x < messageHandler->getCount(); x++)
{ {
@@ -1512,17 +1612,11 @@ bool DBCFile::saveFile(QString fileName)
else msgOutput.append("0+"); else msgOutput.append("0+");
break; break;
case SIGNED_INT: case SIGNED_INT:
case SP_FLOAT:
case DP_FLOAT:
if (sig->intelByteOrder) msgOutput.append("1-"); if (sig->intelByteOrder) msgOutput.append("1-");
else msgOutput.append("0-"); else msgOutput.append("0-");
break; 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: case STRING:
msgOutput.append("4-"); msgOutput.append("4-");
break; break;
@@ -1533,6 +1627,8 @@ bool DBCFile::saveFile(QString fileName)
msgOutput.append(" (" + QString::number(sig->factor) + "," + QString::number(sig->bias) + ") [" + msgOutput.append(" (" + QString::number(sig->factor) + "," + QString::number(sig->bias) + ") [" +
QString::number(sig->min) + "|" + QString::number(sig->max) + "] \"" + sig->unitName QString::number(sig->min) + "|" + QString::number(sig->max) + "] \"" + sig->unitName
+ "\" " + sig->receiver->name + "\n"); + "\" " + 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) if (sig->comment.length() > 0)
{ {
commentsOutput.append("CM_ SG_ " + QString::number(ID) + " " + sig->name + " \"" + sig->comment + "\";\n"); 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(attrValOutput.toUtf8());
outFile->write(valuesOutput.toUtf8()); outFile->write(valuesOutput.toUtf8());
outFile->write(extMultiplexOutput.toUtf8()); outFile->write(extMultiplexOutput.toUtf8());
if (!sigValTypeOutput.isEmpty())
outFile->write(sigValTypeOutput.toUtf8());
attrValOutput.clear(); attrValOutput.clear();
defaultsOutput.clear(); defaultsOutput.clear();
valuesOutput.clear(); valuesOutput.clear();
extMultiplexOutput.clear(); extMultiplexOutput.clear();
sigValTypeOutput.clear();
outFile->close(); outFile->close();
delete outFile; delete outFile;
+3
View File
@@ -75,6 +75,7 @@ public:
DBC_NODE *findNodeByName(QString name); DBC_NODE *findNodeByName(QString name);
DBC_NODE *findNodeByNameAndComment(QString fullname); DBC_NODE *findNodeByNameAndComment(QString fullname);
DBC_NODE *findNodeByIdx(int idx); 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 *findAttributeByName(QString name, DBC_ATTRIBUTE_TYPE type = ATTR_TYPE_ANY);
DBC_ATTRIBUTE *findAttributeByIdx(int idx); DBC_ATTRIBUTE *findAttributeByIdx(int idx);
void findAttributesByType(DBC_ATTRIBUTE_TYPE typ, QList<DBC_ATTRIBUTE> *list); void findAttributesByType(DBC_ATTRIBUTE_TYPE typ, QList<DBC_ATTRIBUTE> *list);
@@ -93,6 +94,7 @@ public:
DBCMessageHandler *messageHandler; DBCMessageHandler *messageHandler;
QList<DBC_NODE> dbc_nodes; QList<DBC_NODE> dbc_nodes;
QList<DBC_VAL_TABLE> dbc_value_tables;
QList<DBC_ATTRIBUTE> dbc_attributes; QList<DBC_ATTRIBUTE> dbc_attributes;
private: private:
QString fileName; QString fileName;
@@ -105,6 +107,7 @@ private:
DBC_SIGNAL* parseSignalLine(QString line, DBC_MESSAGE *msg); DBC_SIGNAL* parseSignalLine(QString line, DBC_MESSAGE *msg);
bool parseSignalMultiplexValueLine(QString line); bool parseSignalMultiplexValueLine(QString line);
DBC_MESSAGE* parseMessageLine(QString line); DBC_MESSAGE* parseMessageLine(QString line);
bool parseValueTableLine(QString line);
bool parseValueLine(QString line); bool parseValueLine(QString line);
bool parseSignalValueTypeLine(QString line); bool parseSignalValueTypeLine(QString line);
bool parseAttributeLine(QString line); bool parseAttributeLine(QString line);
+24 -1
View File
@@ -1,5 +1,8 @@
#include "mainwindow.h" #include "mainwindow.h"
#include <QApplication> #include <QApplication>
#include <QTranslator>
#include <QLocale>
#include <QDebug>
class SavvyCANApplication : public QApplication class SavvyCANApplication : public QApplication
{ {
@@ -39,11 +42,31 @@ int main(int argc, char *argv[])
a.setOrganizationName("EVTV"); a.setOrganizationName("EVTV");
a.setApplicationName("SavvyCAN"); a.setApplicationName("SavvyCAN");
a.setOrganizationDomain("evtv.me"); a.setOrganizationDomain("evtv.me");
QSettings::setDefaultFormat(QSettings::IniFormat); 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(); a.mainWindow = new MainWindow();
QSettings settings;
int fontSize = settings.value("Main/FontSize", 9).toUInt(); int fontSize = settings.value("Main/FontSize", 9).toUInt();
QFont sysFont = QFont(); //get default font QFont sysFont = QFont(); //get default font
sysFont.setPointSize(fontSize); sysFont.setPointSize(fontSize);
+53
View File
@@ -3,6 +3,10 @@
#include "helpwindow.h" #include "helpwindow.h"
#include <qevent.h> #include <qevent.h>
#include <QDebug> #include <QDebug>
#include <QDir>
#include <QRegExp>
#include <QCoreApplication>
#include <QDebug>
#include "simplecrypt.h" #include "simplecrypt.h"
//using this simple encryption library to obfuscate stored password a bit. It's not super secure but better than //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 //whatever key, you'd see it. Just behave yourselves
SimpleCrypt crypto(Q_UINT64_C(0xdeadbeefface6285)); 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) : MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
QDialog(parent), QDialog(parent),
ui(new Ui::MainSettingsDialog) ui(new Ui::MainSettingsDialog)
@@ -17,6 +57,8 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
QSettings settings; QSettings settings;
ui->setupUi(this); 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. //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("None"));
ui->comboSendingBus->addItem(tr("0")); ui->comboSendingBus->addItem(tr("0"));
@@ -90,7 +132,15 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
ui->cbUseFiltered->setChecked(settings.value("Main/UseFiltered", false).toBool()); ui->cbUseFiltered->setChecked(settings.value("Main/UseFiltered", false).toBool());
ui->cbUseOpenGL->setChecked(settings.value("Main/UseOpenGL", 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->cbFilterLabeling->setChecked(settings.value("Main/FilterLabeling", true).toBool());
ui->cbIgnoreDBCColors->setChecked(settings.value("Main/IgnoreDBCColors", false).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; int maxFramesDefault;
if (QSysInfo::WordSize > 32) if (QSysInfo::WordSize > 32)
@@ -108,6 +158,7 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
ui->spinBytesPerLine->setValue(settings.value("Main/BytesPerLine", 8).toInt()); 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 //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->cbDisplayHex, SIGNAL(toggled(bool)), this, SLOT(updateSettings()));
connect(ui->cbFlowAutoRef, SIGNAL(toggled(bool)), this, SLOT(updateSettings())); connect(ui->cbFlowAutoRef, SIGNAL(toggled(bool)), this, SLOT(updateSettings()));
connect(ui->cbFlowUseTimestamp, SIGNAL(toggled(bool)), this, SLOT(updateSettings())); connect(ui->cbFlowUseTimestamp, SIGNAL(toggled(bool)), this, SLOT(updateSettings()));
@@ -177,6 +228,7 @@ void MainSettingsDialog::updateSettings()
{ {
QSettings settings; QSettings settings;
settings.setValue("Main/Language", ui->comboLanguage->currentData().toString());
settings.setValue("Main/UseHex", ui->cbDisplayHex->isChecked()); settings.setValue("Main/UseHex", ui->cbDisplayHex->isChecked());
settings.setValue("FlowView/AutoRef", ui->cbFlowAutoRef->isChecked()); settings.setValue("FlowView/AutoRef", ui->cbFlowAutoRef->isChecked());
settings.setValue("FlowView/UseTimestamp", ui->cbFlowUseTimestamp->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/MaximumFrames", ui->spinMaximumFrames->value());
settings.setValue("Main/BytesPerLine", ui->spinBytesPerLine->value()); settings.setValue("Main/BytesPerLine", ui->spinBytesPerLine->value());
settings.setValue("Main/FontFixedWidth", ui->cbFontFixedWidth->isChecked()); settings.setValue("Main/FontFixedWidth", ui->cbFontFixedWidth->isChecked());
settings.setValue("Main/ColorsByCanId", ui->cbColorsByCanId->isChecked());
settings.sync(); settings.sync();
emit updatedSettings(); emit updatedSettings();
+1
View File
@@ -25,6 +25,7 @@ public slots:
private: private:
Ui::MainSettingsDialog *ui; Ui::MainSettingsDialog *ui;
void populateLanguageCombo();
void closeEvent(QCloseEvent *event); void closeEvent(QCloseEvent *event);
bool eventFilter(QObject *obj, QEvent *event); bool eventFilter(QObject *obj, QEvent *event);
}; };
+73 -9
View File
@@ -10,6 +10,7 @@
#include "utility.h" #include "utility.h"
#include "filterutility.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) 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 #endif
useHex = true; useHex = true;
useColorsByCanId = false;
selfRef = this; selfRef = this;
this->setWindowTitle("Savvy CAN V" + QString::number(VERSION) + " [Built " + QString(__DATE__) +"]"); 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::clicked, this, &MainWindow::gridClicked);
connect(ui->canFramesView, &QAbstractItemView::doubleClicked, this, &MainWindow::gridDoubleClicked); connect(ui->canFramesView, &QAbstractItemView::doubleClicked, this, &MainWindow::gridDoubleClicked);
ui->canFramesView->setContextMenuPolicy(Qt::CustomContextMenu); 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(ui->canFramesView, &QAbstractItemView::customContextMenuRequested, this, &MainWindow::gridContextMenuRequest);
connect(model, &CANFrameModel::updatedFiltersList, this, &MainWindow::updateFilterList); connect(model, &CANFrameModel::updatedFiltersList, this, &MainWindow::updateFilterList);
@@ -415,6 +421,9 @@ void MainWindow::readUpdateableSettings()
model->setHexMode(useHex); model->setHexMode(useHex);
Utility::decimalMode = !useHex; Utility::decimalMode = !useHex;
useColorsByCanId = settings.value("Main/ColorsByCanId", false).toBool();
model->setUseColorsByCanId(useColorsByCanId);
bool tempBool; bool tempBool;
TimeStyle ts = TS_MICROS; TimeStyle ts = TS_MICROS;
tempBool = settings.value("Main/TimeSeconds", false).toBool(); 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) 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() << "Pos: " << pos << " Row :" << idx.row() << " Col: " << idx.column();
qDebug() << "Data: " << idx.data();
if (idx.column() == 8) //we're over the DATA column if (!idx.isValid()) return;
{
contextMenuPosition = pos;
QMenu *menu = new QMenu(this); QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose); menu->setAttribute(Qt::WA_DeleteOnClose);
menu->addAction(tr("Add to a new graphing window"), this, SLOT(setupAddToNewGraph())); menu->addAction(copyAct);
menu->addSeparator();
menu->addAction(tr("Add to latest graphing window"), this, SLOT(setupSendToLatestGraphWindow()));
menu->popup(ui->canFramesView->mapToGlobal(pos)); if (idx.column() == 8) //we're over the DATA column
{
contextMenuPosition = pos;
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->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 &current : 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) QString MainWindow::getSignalNameFromPosition(QPoint pos)
+4
View File
@@ -110,6 +110,7 @@ private slots:
void gridClicked(QModelIndex); void gridClicked(QModelIndex);
void gridDoubleClicked(QModelIndex); void gridDoubleClicked(QModelIndex);
void gridContextMenuRequest(QPoint pos); void gridContextMenuRequest(QPoint pos);
void copyFromTable();
void setupAddToNewGraph(); void setupAddToNewGraph();
void setupSendToLatestGraphWindow(); void setupSendToLatestGraphWindow();
void interpretToggled(bool); void interpretToggled(bool);
@@ -147,6 +148,7 @@ signals:
private: private:
Ui::MainWindow *ui; Ui::MainWindow *ui;
QAction *copyAct;
static MainWindow *selfRef; static MainWindow *selfRef;
//canbus related data //canbus related data
@@ -160,6 +162,7 @@ private:
int rxFrames; int rxFrames;
bool inhibitFilterUpdate; bool inhibitFilterUpdate;
bool useHex; bool useHex;
bool useColorsByCanId;
bool allowCapture; bool allowCapture;
bool ignoreDBCColors; bool ignoreDBCColors;
bool CSVAbsTime; bool CSVAbsTime;
@@ -213,6 +216,7 @@ private:
//private methods //private methods
QString getSignalNameFromPosition(QPoint pos); QString getSignalNameFromPosition(QPoint pos);
uint32_t getMessageIDFromPosition(QPoint pos); uint32_t getMessageIDFromPosition(QPoint pos);
void copySelection();
void handleSaveDecodedMethod(bool csv); void handleSaveDecodedMethod(bool csv);
void saveDecodedTextFile(QString); void saveDecodedTextFile(QString);
void saveDecodedTextFileAsColumns(QString); void saveDecodedTextFileAsColumns(QString);
+13
View File
@@ -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
```
+16
View File
@@ -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."
+16
View File
@@ -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
+47
View File
@@ -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>
+21
View File
@@ -53,6 +53,20 @@
</item> </item>
</layout> </layout>
</item> </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> </layout>
</widget> </widget>
</item> </item>
@@ -97,6 +111,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="cbColorsByCanId">
<property name="text">
<string>Color rows by CAN ID</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QCheckBox" name="cbUseOpenGL"> <widget class="QCheckBox" name="cbUseOpenGL">
<property name="text"> <property name="text">
+70
View File
@@ -10,6 +10,9 @@
#include <QRect> #include <QRect>
#include <QComboBox> #include <QComboBox>
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QColor>
#include <QString>
#include <array>
//#include <QDesktopWidget> //#include <QDesktopWidget>
enum TimeStyle enum TimeStyle
@@ -20,6 +23,44 @@ enum TimeStyle
TS_CLOCK 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 class Utility
{ {
public: public:
@@ -344,6 +385,35 @@ public:
return result; 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 #endif // UTILITY_H