mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
523 lines
23 KiB
C++
523 lines
23 KiB
C++
|
|
|
|
/** \file
|
|
* @brief member function of the UI_Constructor class
|
|
* displays the callsign activity in the right pane of the UI
|
|
*/
|
|
|
|
#include "JS8_UI/mainwindow.h"
|
|
|
|
void UI_Constructor::displayCallActivity() {
|
|
|
|
auto now = DriftingDateTime::currentDateTimeUtc();
|
|
|
|
// Reset the header label text to accommodate minimal label setting
|
|
int cols = ui->tableWidgetCalls->columnCount();
|
|
for (int c = 0; c < cols; ++c) {
|
|
ui->tableWidgetCalls->horizontalHeaderItem(c)->setText(
|
|
columnLabel(m_origCallActivityHeaderLabelMap[c]));
|
|
}
|
|
|
|
ui->tableWidgetCalls->setFont(m_config.table_font());
|
|
|
|
// Selected callsign
|
|
QString selectedCall = callsignSelected();
|
|
|
|
auto currentScrollPos = ui->tableWidgetCalls->verticalScrollBar()->value();
|
|
|
|
ui->tableWidgetCalls->setUpdatesEnabled(false);
|
|
{
|
|
// Clear the table
|
|
ui->tableWidgetCalls->setRowCount(0);
|
|
ui->tableWidgetCalls->horizontalHeaderItem(8)->setText(
|
|
m_config.miles() ? "mi" : "km");
|
|
|
|
bool showIconColumn = false;
|
|
createGroupCallsignTableRows(
|
|
ui->tableWidgetCalls, selectedCall,
|
|
showIconColumn); // isAllCallIncluded(selectedCall)); // ||
|
|
// isGroupCallIncluded(selectedCall));
|
|
|
|
// Build the table
|
|
|
|
auto const sort = getSortByReverse("callActivity", "callsign");
|
|
auto keys = m_callActivity.keys();
|
|
|
|
auto const compareOffset = [this](QString const &lhsKey,
|
|
QString const &rhsKey) {
|
|
return m_callActivity[lhsKey].offset <
|
|
m_callActivity[rhsKey].offset;
|
|
};
|
|
|
|
auto const compareAzimuth =
|
|
[this, reverse = sort.reverse, my_grid = m_config.my_grid()](
|
|
QString const &lhsKey, QString const &rhsKey) {
|
|
auto const lhs =
|
|
Geodesic::vector(my_grid, m_callActivity[lhsKey].grid)
|
|
.azimuth();
|
|
auto const rhs =
|
|
Geodesic::vector(my_grid, m_callActivity[rhsKey].grid)
|
|
.azimuth();
|
|
|
|
// We always want invalid azimuths to be at the end of the list,
|
|
// and the list is going to be reversed if reverse is set, so we
|
|
// want to set things up so that invalid elements are either all
|
|
// at the beginning in the case of a reverse, or all at the end
|
|
// in the standard case.
|
|
|
|
if (!lhs)
|
|
return reverse && rhs;
|
|
else if (!rhs)
|
|
return !reverse;
|
|
else
|
|
return lhs < rhs;
|
|
};
|
|
|
|
auto const compareDistance =
|
|
[this, reverse = sort.reverse, my_grid = m_config.my_grid()](
|
|
QString const &lhsKey, QString const &rhsKey) {
|
|
auto const lhs =
|
|
Geodesic::vector(my_grid, m_callActivity[lhsKey].grid)
|
|
.distance();
|
|
auto const rhs =
|
|
Geodesic::vector(my_grid, m_callActivity[rhsKey].grid)
|
|
.distance();
|
|
|
|
// We always want invalid distances to be at the end of the
|
|
// list, and the list is going to be reversed if reverse is set,
|
|
// so we want to set things up so that invalid elements are
|
|
// either all at the beginning in the case of a reverse, or all
|
|
// at the end in the standard case.
|
|
|
|
if (!lhs)
|
|
return reverse && rhs;
|
|
else if (!rhs)
|
|
return !reverse;
|
|
else
|
|
return lhs < rhs;
|
|
};
|
|
|
|
auto const compareTimestamp = [this](QString const &lhsKey,
|
|
QString const &rhsKey) {
|
|
return m_callActivity[lhsKey].utcTimestamp <
|
|
m_callActivity[rhsKey].utcTimestamp;
|
|
};
|
|
|
|
auto const compareAckTimestamp = [this](QString const &lhsKey,
|
|
QString const &rhsKey) {
|
|
return m_callActivity[rhsKey].ackTimestamp <
|
|
m_callActivity[lhsKey].ackTimestamp;
|
|
};
|
|
|
|
auto const compareSNR =
|
|
[this, reverse = sort.reverse](QString const &lhsKey,
|
|
QString const &rhsKey) {
|
|
auto lhs = m_callActivity[lhsKey].snr;
|
|
auto rhs = m_callActivity[rhsKey].snr;
|
|
|
|
// We always want insane SNR values to be at the end of the
|
|
// list, and the list is going to be reversed if reverse is set,
|
|
// so we want to set things up so that insane elements are
|
|
// either all at the beginning in the case of a reverse, or all
|
|
// at the end in the standard case. Reverse takes care of
|
|
// itself; we just need to sort out standard.
|
|
|
|
if (!reverse) {
|
|
if (lhs < -60 || lhs > 60)
|
|
lhs = -lhs;
|
|
if (rhs < -60 || rhs > 60)
|
|
rhs = -rhs;
|
|
}
|
|
|
|
return lhs < rhs;
|
|
};
|
|
|
|
auto const compareSubmode = [this](QString const &lhsKey,
|
|
QString const &rhsKey) {
|
|
auto lhs = m_callActivity[lhsKey].submode;
|
|
auto rhs = m_callActivity[rhsKey].submode;
|
|
|
|
// Slow mode isn't at the start of the enumeration; it's in the
|
|
// middle of it. All the other modes are in the expected order.
|
|
|
|
if (lhs == Varicode::JS8CallSlow)
|
|
lhs = -lhs;
|
|
if (rhs == Varicode::JS8CallSlow)
|
|
rhs = -rhs;
|
|
|
|
return lhs < rhs;
|
|
};
|
|
|
|
// Always perform an initial sort by callsign.
|
|
|
|
std::stable_sort(keys.begin(), keys.end());
|
|
|
|
// If something other than callsign was requested as the sort by,
|
|
// perform an additional stable sort by the field requested.
|
|
|
|
if (sort.by == "offset")
|
|
std::stable_sort(keys.begin(), keys.end(), compareOffset);
|
|
else if (sort.by == "distance")
|
|
std::stable_sort(keys.begin(), keys.end(), compareDistance);
|
|
else if (sort.by == "azimuth")
|
|
std::stable_sort(keys.begin(), keys.end(), compareAzimuth);
|
|
else if (sort.by == "timestamp")
|
|
std::stable_sort(keys.begin(), keys.end(), compareTimestamp);
|
|
else if (sort.by == "ackTimestamp")
|
|
std::stable_sort(keys.begin(), keys.end(), compareAckTimestamp);
|
|
else if (sort.by == "snr")
|
|
std::stable_sort(keys.begin(), keys.end(), compareSNR);
|
|
else if (sort.by == "submode")
|
|
std::stable_sort(keys.begin(), keys.end(), compareSubmode);
|
|
|
|
// The sort comparators leave things in forward order. If a reverse sort
|
|
// was requested, reverse the keys.
|
|
|
|
if (sort.reverse)
|
|
std::reverse(keys.begin(), keys.end());
|
|
|
|
// pin messages to the top
|
|
std::stable_sort(keys.begin(), keys.end(),
|
|
[this](QString const &lhsKey, QString const &rhsKey) {
|
|
auto const lhs = (int)!(
|
|
m_rxInboxCountCache.value(lhsKey, 0) > 0);
|
|
auto const rhs = (int)!(
|
|
m_rxInboxCountCache.value(rhsKey, 0) > 0);
|
|
|
|
return lhs < rhs;
|
|
});
|
|
|
|
int callsignAging = m_config.callsign_aging();
|
|
foreach (QString call, keys) {
|
|
if (call.trimmed().isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
CallDetail d = m_callActivity[call];
|
|
if (d.call.trimmed().isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
bool isCallSelected = (call == selectedCall);
|
|
|
|
// icon flags (flag -> star -> empty)
|
|
bool hasMessage = m_rxInboxCountCache.value(d.call, 0) > 0;
|
|
|
|
// display telephone icon if called cq in the past 5 minutes
|
|
bool hasCQ =
|
|
d.cqTimestamp.isValid() && d.cqTimestamp.secsTo(now) / 60 < 5;
|
|
|
|
// display star if they've acked a message from us
|
|
bool hasACK = d.ackTimestamp.isValid();
|
|
|
|
if (!isCallSelected && !hasMessage && callsignAging &&
|
|
d.utcTimestamp.secsTo(now) / 60 >= callsignAging) {
|
|
continue;
|
|
}
|
|
|
|
bool isBlocked = m_config.rx_callsign_blocklist().contains(d.call) ||
|
|
m_config.rx_callsign_blocklist().contains(
|
|
Radio::base_callsign(d.call));
|
|
|
|
ui->tableWidgetCalls->insertRow(ui->tableWidgetCalls->rowCount());
|
|
int row = ui->tableWidgetCalls->rowCount() - 1;
|
|
int col = 0;
|
|
|
|
#if SHOW_THROUGH_CALLS
|
|
QString displayCall =
|
|
d.through.isEmpty()
|
|
? d.call
|
|
: QString("%1>%2").arg(d.through).arg(d.call);
|
|
#else
|
|
QString displayCall = d.call;
|
|
#endif
|
|
bool hasThrough = !d.through.isEmpty();
|
|
|
|
auto iconItem = new QTableWidgetItem(isBlocked ? "\u2715"
|
|
: hasMessage ? "\u2691"
|
|
: hasACK ? "\u2605"
|
|
: hasCQ ? "\u260E"
|
|
: hasThrough ? "\u269F"
|
|
: "");
|
|
|
|
if (isBlocked) {iconItem->setForeground(QColor(Qt::red));}
|
|
|
|
iconItem->setData(Qt::UserRole, QVariant(d.call));
|
|
iconItem->setToolTip(
|
|
isBlocked ? "Blocked Station"
|
|
: hasMessage ? "Message Available"
|
|
: hasACK ? QString("Hearing Your Station (%1)")
|
|
.arg(since(d.ackTimestamp))
|
|
: hasCQ ? QString("Calling CQ (%1)").arg(since(d.cqTimestamp))
|
|
: hasThrough
|
|
? QString("Heard Through Relay (%1)").arg(d.through)
|
|
: "");
|
|
iconItem->setTextAlignment(Qt::AlignCenter);
|
|
ui->tableWidgetCalls->setItem(row, col++, iconItem);
|
|
if (hasMessage || hasACK || hasCQ || hasThrough || isBlocked) {
|
|
showIconColumn = true;
|
|
}
|
|
|
|
auto displayItem = new QTableWidgetItem(displayCall);
|
|
displayItem->setData(Qt::UserRole, QVariant(d.call));
|
|
displayItem->setToolTip(generateCallDetail(displayCall));
|
|
ui->tableWidgetCalls->setItem(row, col++, displayItem);
|
|
|
|
#if ONLY_SHOW_HEARD_CALLSIGNS
|
|
if (d.utcTimestamp.isValid()) {
|
|
#else
|
|
if (true) {
|
|
#endif
|
|
auto ageItem = new QTableWidgetItem(since(d.utcTimestamp));
|
|
ageItem->setTextAlignment(Qt::AlignCenter);
|
|
ageItem->setToolTip(d.utcTimestamp.toString());
|
|
ui->tableWidgetCalls->setItem(row, col++, ageItem);
|
|
|
|
auto snrText = Varicode::formatSNR(d.snr);
|
|
auto snrItem = new QTableWidgetItem(
|
|
snrText.isEmpty()
|
|
? ""
|
|
: QString(columnLabel("%1 dB")).arg(snrText));
|
|
snrItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
|
ui->tableWidgetCalls->setItem(row, col++, snrItem);
|
|
|
|
auto offsetItem = new QTableWidgetItem(
|
|
QString(columnLabel("%1 Hz")).arg(d.offset));
|
|
offsetItem->setData(Qt::UserRole, QVariant(d.offset));
|
|
offsetItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
|
ui->tableWidgetCalls->setItem(row, col++, offsetItem);
|
|
|
|
auto tdriftItem = new QTableWidgetItem(
|
|
QString(columnLabel("%1 ms")).arg((int)(1000 * d.tdrift)));
|
|
tdriftItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
|
ui->tableWidgetCalls->setItem(row, col++, tdriftItem);
|
|
|
|
auto name = JS8::Submode::name(d.submode);
|
|
auto modeItem = (name == "JS8 40" || name == "JS8 60")
|
|
? new QTableWidgetItem(name.right(2))
|
|
: new QTableWidgetItem(name.left(1).replace("H", "N"));
|
|
modeItem->setToolTip(name);
|
|
modeItem->setData(Qt::UserRole, QVariant(name));
|
|
modeItem->setTextAlignment(Qt::AlignCenter);
|
|
ui->tableWidgetCalls->setItem(row, col++, modeItem);
|
|
|
|
auto gridItem = new QTableWidgetItem(
|
|
QString("%1").arg(d.grid.trimmed().left(4)));
|
|
gridItem->setToolTip(d.grid.trimmed());
|
|
ui->tableWidgetCalls->setItem(row, col++, gridItem);
|
|
|
|
auto const vector =
|
|
Geodesic::vector(m_config.my_grid(), d.grid);
|
|
auto const units = !showColumn("call", "labels");
|
|
|
|
auto distanceItem = new QTableWidgetItem(
|
|
vector.distance().toString(m_config.miles(), units));
|
|
distanceItem->setTextAlignment(Qt::AlignRight |
|
|
Qt::AlignVCenter);
|
|
ui->tableWidgetCalls->setItem(row, col++, distanceItem);
|
|
|
|
auto azimuthItem =
|
|
new QTableWidgetItem(vector.azimuth().toString(units));
|
|
if (auto const azimuth = vector.azimuth())
|
|
azimuthItem->setToolTip(azimuth.compass().toString());
|
|
azimuthItem->setTextAlignment(Qt::AlignRight |
|
|
Qt::AlignVCenter);
|
|
ui->tableWidgetCalls->setItem(row, col++, azimuthItem);
|
|
|
|
QString flag;
|
|
if (m_logBook.hasWorkedBefore(d.call, "")) {
|
|
// unicode checkmark
|
|
flag = "\u2713";
|
|
}
|
|
auto workedBeforeItem = new QTableWidgetItem(flag);
|
|
workedBeforeItem->setTextAlignment(Qt::AlignCenter);
|
|
ui->tableWidgetCalls->setItem(row, col++, workedBeforeItem);
|
|
|
|
QString logDetailGrid;
|
|
QString logDetailDate;
|
|
QString logDetailName;
|
|
QString logDetailComment;
|
|
bool gridItemEmpty = gridItem->text().isEmpty();
|
|
|
|
if ((gridItemEmpty && showColumn("call", "grid")) ||
|
|
showColumn("call", "log") ||
|
|
showColumn("call", "logName") ||
|
|
showColumn("call", "logComment")) {
|
|
m_logBook.findCallDetails(d.call, logDetailGrid,
|
|
logDetailDate, logDetailName,
|
|
logDetailComment);
|
|
}
|
|
|
|
if (gridItemEmpty && !logDetailGrid.isEmpty()) {
|
|
gridItem->setText(logDetailGrid.trimmed().left(4));
|
|
gridItem->setToolTip(logDetailGrid.trimmed());
|
|
|
|
auto const vector =
|
|
Geodesic::vector(m_config.my_grid(), d.grid);
|
|
auto const units = !showColumn("call", "labels");
|
|
|
|
distanceItem->setText(
|
|
vector.distance().toString(m_config.miles(), units));
|
|
azimuthItem->setText(vector.azimuth().toString(units));
|
|
if (auto const azimuth = vector.azimuth())
|
|
azimuthItem->setToolTip(azimuth.compass().toString());
|
|
|
|
// update the call activity cache with the loaded grid
|
|
if (m_callActivity.contains(d.call)) {
|
|
m_callActivity[call].grid = logDetailGrid.trimmed();
|
|
}
|
|
}
|
|
|
|
if (!logDetailDate.isEmpty()) {
|
|
auto lastLogged =
|
|
QDate::fromString(logDetailDate, "yyyyMMdd");
|
|
|
|
workedBeforeItem->setToolTip(
|
|
QString("Last Logged: %1").arg(lastLogged.toString()));
|
|
}
|
|
|
|
auto logNameItem = new QTableWidgetItem(logDetailName);
|
|
logNameItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
logNameItem->setToolTip(logDetailName);
|
|
ui->tableWidgetCalls->setItem(row, col++, logNameItem);
|
|
|
|
auto logCommentItem = new QTableWidgetItem(logDetailComment);
|
|
logCommentItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
logCommentItem->setToolTip(logDetailComment);
|
|
ui->tableWidgetCalls->setItem(row, col++, logCommentItem);
|
|
|
|
} else {
|
|
ui->tableWidgetCalls->setItem(row, col++,
|
|
new QTableWidgetItem("")); // age
|
|
ui->tableWidgetCalls->setItem(row, col++,
|
|
new QTableWidgetItem("")); // snr
|
|
ui->tableWidgetCalls->setItem(row, col++,
|
|
new QTableWidgetItem("")); // freq
|
|
ui->tableWidgetCalls->setItem(
|
|
row, col++, new QTableWidgetItem("")); // tdrift
|
|
ui->tableWidgetCalls->setItem(row, col++,
|
|
new QTableWidgetItem("")); // mode
|
|
ui->tableWidgetCalls->setItem(row, col++,
|
|
new QTableWidgetItem("")); // grid
|
|
ui->tableWidgetCalls->setItem(
|
|
row, col++, new QTableWidgetItem("")); // distance
|
|
ui->tableWidgetCalls->setItem(
|
|
row, col++, new QTableWidgetItem("")); // azimuth
|
|
ui->tableWidgetCalls->setItem(
|
|
row, col++, new QTableWidgetItem("")); // worked before
|
|
ui->tableWidgetCalls->setItem(
|
|
row, col++, new QTableWidgetItem("")); // log name
|
|
ui->tableWidgetCalls->setItem(
|
|
row, col++, new QTableWidgetItem("")); // log comment
|
|
}
|
|
|
|
if (isCallSelected) {
|
|
for (int i = 0; i < ui->tableWidgetCalls->columnCount(); i++) {
|
|
ui->tableWidgetCalls->item(row, i)->setSelected(true);
|
|
}
|
|
}
|
|
|
|
if (hasCQ) {
|
|
for (int i = 0; i < ui->tableWidgetCalls->columnCount(); i++) {
|
|
ui->tableWidgetCalls->item(row, i)->setBackground(
|
|
QBrush(m_config.color_CQ()));
|
|
}
|
|
}
|
|
|
|
if (m_config.secondary_highlight_words().contains(call)) {
|
|
for (int i = 0; i < ui->tableWidgetCalls->columnCount(); i++) {
|
|
ui->tableWidgetCalls->item(row, i)->setBackground(
|
|
QBrush(m_config.color_secondary_highlight()));
|
|
}
|
|
}
|
|
|
|
if (m_config.primary_highlight_words().contains(call)) {
|
|
for (int i = 0; i < ui->tableWidgetCalls->columnCount(); i++) {
|
|
ui->tableWidgetCalls->item(row, i)->setBackground(
|
|
QBrush(m_config.color_primary_highlight()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set table color
|
|
auto style = QString(
|
|
"QTableWidget { background:%1; selection-background-color:%2; "
|
|
"alternate-background-color:%1; color:%3; } "
|
|
"QTableWidget::item:selected { background-color: %2; color: %3; }");
|
|
style = style.arg(m_config.color_table_background().name());
|
|
style = style.arg(m_config.color_table_highlight().name());
|
|
style = style.arg(m_config.color_table_foreground().name());
|
|
ui->tableWidgetCalls->setStyleSheet(style);
|
|
|
|
// Set the table palette for inactive selected row
|
|
auto p = ui->tableWidgetCalls->palette();
|
|
p.setColor(QPalette::Highlight, m_config.color_table_highlight());
|
|
p.setColor(QPalette::HighlightedText,
|
|
m_config.color_table_foreground());
|
|
p.setColor(QPalette::Inactive, QPalette::Highlight,
|
|
p.color(QPalette::Active, QPalette::Highlight));
|
|
ui->tableWidgetCalls->setPalette(p);
|
|
|
|
// Set item fonts
|
|
for (int row = 0; row < ui->tableWidgetCalls->rowCount(); row++) {
|
|
auto bold = ui->tableWidgetCalls->item(row, 0)->text() == "\u2691";
|
|
for (int col = 0; col < ui->tableWidgetCalls->columnCount();
|
|
col++) {
|
|
auto item = ui->tableWidgetCalls->item(row, col);
|
|
if (item) {
|
|
auto f = m_config.table_font();
|
|
if (bold) {
|
|
f.setBold(true);
|
|
}
|
|
item->setFont(f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Column labels
|
|
ui->tableWidgetCalls->horizontalHeader()->setVisible(
|
|
showColumn("call", "labels"));
|
|
|
|
// Hide columns
|
|
ui->tableWidgetCalls->setColumnHidden(0, !showIconColumn);
|
|
ui->tableWidgetCalls->setColumnHidden(1,
|
|
!showColumn("call", "callsign"));
|
|
ui->tableWidgetCalls->setColumnHidden(2,
|
|
!showColumn("call", "timestamp"));
|
|
ui->tableWidgetCalls->setColumnHidden(3, !showColumn("call", "snr"));
|
|
ui->tableWidgetCalls->setColumnHidden(4, !showColumn("call", "offset"));
|
|
ui->tableWidgetCalls->setColumnHidden(
|
|
5, !showColumn("call", "tdrift", false));
|
|
ui->tableWidgetCalls->setColumnHidden(
|
|
6, !showColumn("call", "submode", false));
|
|
ui->tableWidgetCalls->setColumnHidden(
|
|
7, !showColumn("call", "grid", false));
|
|
ui->tableWidgetCalls->setColumnHidden(
|
|
8, !showColumn("call", "distance", false));
|
|
ui->tableWidgetCalls->setColumnHidden(
|
|
9, !showColumn("call", "azimuth", false));
|
|
ui->tableWidgetCalls->setColumnHidden(10, !showColumn("call", "log"));
|
|
ui->tableWidgetCalls->setColumnHidden(11,
|
|
!showColumn("call", "logName"));
|
|
ui->tableWidgetCalls->setColumnHidden(
|
|
12, !showColumn("call", "logComment"));
|
|
|
|
// Resize the table columns
|
|
ui->tableWidgetCalls->resizeColumnToContents(0);
|
|
ui->tableWidgetCalls->resizeColumnToContents(1);
|
|
ui->tableWidgetCalls->resizeColumnToContents(2);
|
|
ui->tableWidgetCalls->resizeColumnToContents(3);
|
|
ui->tableWidgetCalls->resizeColumnToContents(4);
|
|
ui->tableWidgetCalls->resizeColumnToContents(5);
|
|
ui->tableWidgetCalls->resizeColumnToContents(6);
|
|
ui->tableWidgetCalls->resizeColumnToContents(7);
|
|
ui->tableWidgetCalls->resizeColumnToContents(8);
|
|
ui->tableWidgetCalls->resizeColumnToContents(9);
|
|
ui->tableWidgetCalls->resizeColumnToContents(10);
|
|
ui->tableWidgetCalls->resizeColumnToContents(11);
|
|
|
|
// Reset the scroll position
|
|
ui->tableWidgetCalls->verticalScrollBar()->setValue(currentScrollPos);
|
|
}
|
|
ui->tableWidgetCalls->setUpdatesEnabled(true);
|
|
}
|