#include "tagpanel.h" #include #include #include #include #include #include #include #include TagPanel::TagPanel(const QString &tag, QWidget *parent) : QGroupBox(QStringLiteral("[%1]").arg(tag), parent) , m_tag(tag) { auto *layout = new QVBoxLayout(this); layout->setContentsMargins(6, 14, 6, 6); layout->setSpacing(4); m_tabs = new QTabWidget(this); // ── "Aktuell" tab: latest value(s), updates in-place ─────────────── auto *currentTab = new QWidget(m_tabs); auto *currentLayout = new QVBoxLayout(currentTab); currentLayout->setContentsMargins(0, 4, 0, 0); currentLayout->setSpacing(4); m_table = new QTableWidget(0, 2, currentTab); m_table->setHorizontalHeaderLabels({tr("Key"), tr("Value")}); m_table->horizontalHeader()->setStretchLastSection(true); m_table->verticalHeader()->hide(); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); m_table->setAlternatingRowColors(true); m_table->setMaximumHeight(220); m_table->verticalHeader()->setDefaultSectionSize(22); currentLayout->addWidget(m_table); m_rawLabel = new QLabel(currentTab); m_rawLabel->setWordWrap(true); m_rawLabel->setStyleSheet("font-family: monospace;"); m_rawLabel->hide(); currentLayout->addWidget(m_rawLabel); m_tabs->addTab(currentTab, tr("Aktuell")); // ── "Verlauf" tab: every value ever received, timestamped ────────── auto *historyTab = new QWidget(m_tabs); auto *historyLayout = new QVBoxLayout(historyTab); historyLayout->setContentsMargins(0, 4, 0, 0); historyLayout->setSpacing(4); m_historyEdit = new QPlainTextEdit(historyTab); m_historyEdit->setReadOnly(true); m_historyEdit->setMaximumHeight(220); m_historyEdit->setLineWrapMode(QPlainTextEdit::NoWrap); QFont mono("Monospace"); mono.setStyleHint(QFont::Monospace); mono.setPointSize(9); m_historyEdit->setFont(mono); historyLayout->addWidget(m_historyEdit); auto *historyBtnRow = new QHBoxLayout(); auto *clearHistBtn = new QPushButton(tr("Verlauf leeren"), historyTab); clearHistBtn->setToolTip(tr("Nur den Verlauf dieses Tags leeren (Panel bleibt bestehen)")); connect(clearHistBtn, &QPushButton::clicked, this, &TagPanel::clearHistory); historyBtnRow->addStretch(); historyBtnRow->addWidget(clearHistBtn); historyLayout->addLayout(historyBtnRow); m_tabs->addTab(historyTab, tr("Verlauf")); layout->addWidget(m_tabs); auto *btnRow = new QHBoxLayout(); btnRow->addStretch(); auto *copyBtn = new QPushButton(tr("📋 Copy"), this); copyBtn->setToolTip(tr("Copy the active tab's content to clipboard")); copyBtn->setMaximumWidth(90); connect(copyBtn, &QPushButton::clicked, this, &TagPanel::copyToClipboard); btnRow->addWidget(copyBtn); layout->addLayout(btnRow); } void TagPanel::update(const QString &value) { appendHistory(value); static const QRegularExpression kvRe(R"((\w+)=(\S+))"); if (kvRe.match(value).hasMatch()) { parseKeyValue(value); m_rawLabel->hide(); m_table->show(); } else { showRaw(value); m_table->hide(); m_rawLabel->show(); } } void TagPanel::appendHistory(const QString &value) { const QString ts = QDateTime::currentDateTime().toString("hh:mm:ss.zzz"); m_historyEdit->appendPlainText(QStringLiteral("[%1] %2").arg(ts, value)); m_historyEdit->verticalScrollBar()->setValue( m_historyEdit->verticalScrollBar()->maximum()); ++m_historyCount; if (m_historyCount > kMaxHistoryEntries) { // Drop the oldest entry (topmost line) to keep memory use bounded // for very chatty or long-running tags. QTextCursor cursor(m_historyEdit->document()); cursor.movePosition(QTextCursor::Start); cursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor); cursor.removeSelectedText(); --m_historyCount; } } void TagPanel::clearHistory() { m_historyEdit->clear(); m_historyCount = 0; } void TagPanel::parseKeyValue(const QString &value) { static const QRegularExpression kvRe(R"((\w+)=(\S+))"); auto it = kvRe.globalMatch(value); while (it.hasNext()) { const auto match = it.next(); const QString key = match.captured(1); const QString raw = match.captured(2); // Split on ';' -> list of values. A plain (non-list) value yields // a QStringList with exactly one element, which still goes through // the same code path for consistency. QStringList values = raw.split(';', Qt::SkipEmptyParts); if (values.isEmpty()) values << raw; // preserve even an empty/odd value as-is setKeyValues(key, values); } } void TagPanel::setKeyValues(const QString &key, const QStringList &values) { const int newCount = values.size(); if (m_keyRanges.contains(key)) { // Key already exists – remove its current rows, then re-insert at // the same starting position with the new row count. const KeyRange old = m_keyRanges[key]; for (int i = 0; i < old.count; ++i) m_table->removeRow(old.firstRow); // Shift all OTHER key ranges that started after this one for (auto it = m_keyRanges.begin(); it != m_keyRanges.end(); ++it) { if (it.key() != key && it->firstRow > old.firstRow) it->firstRow += (newCount - old.count); } int insertAt = old.firstRow; for (int i = 0; i < newCount; ++i) { m_table->insertRow(insertAt + i); auto *keyItem = new QTableWidgetItem(i == 0 ? key : QString()); auto *valItem = new QTableWidgetItem(values[i]); valItem->setBackground(QColor(0x2d, 0x5a, 0x2d)); // flash green on update m_table->setItem(insertAt + i, 0, keyItem); m_table->setItem(insertAt + i, 1, valItem); } m_keyRanges[key] = {insertAt, newCount}; } else { // New key – append at the bottom const int insertAt = m_table->rowCount(); for (int i = 0; i < newCount; ++i) { m_table->insertRow(insertAt + i); auto *keyItem = new QTableWidgetItem(i == 0 ? key : QString()); auto *valItem = new QTableWidgetItem(values[i]); valItem->setBackground(QColor(0x2d, 0x5a, 0x2d)); m_table->setItem(insertAt + i, 0, keyItem); m_table->setItem(insertAt + i, 1, valItem); } m_keyRanges[key] = {insertAt, newCount}; } } void TagPanel::showRaw(const QString &value) { m_rawLabel->setText(value); } void TagPanel::copyToClipboard() { QStringList lines; lines << QStringLiteral("[%1]").arg(m_tag); if (m_tabs->currentIndex() == 1) { // "Verlauf" tab active - copy the full timestamped history as-is. QApplication::clipboard()->setText( lines.join('\n') + '\n' + m_historyEdit->toPlainText()); return; } if (m_table->isVisible()) { QString lastKey; for (int r = 0; r < m_table->rowCount(); ++r) { const QString key = m_table->item(r, 0)->text(); const QString val = m_table->item(r, 1)->text(); if (!key.isEmpty()) { lines << QStringLiteral(" %1 = %2").arg(key, val); lastKey = key; } else { lines << QStringLiteral(" %1").arg(val); // continuation row } } } else { lines << QStringLiteral(" %1").arg(m_rawLabel->text()); } QApplication::clipboard()->setText(lines.join('\n')); }