#include "rawview.h" #include #include #include #include #include #include #include #include #include #include // Tag names may contain letters, digits, underscore AND hyphen (e.g. // [I2C-BUS]) - keep this in sync with the tagRe in serialworker.cpp, which // is what actually decides whether a tag is detected/emitted in the first // place. If the two regexes disagree, hyphenated tags can end up detected // by one but not recognised as "having a tag" by the other (breaking either // cyan highlighting or the tag filter). const QRegularExpression RawView::s_tagRe( R"(\[([A-Z][A-Z0-9_-]*)\])", QRegularExpression::CaseInsensitiveOption); RawView::RawView(QWidget *parent) : QWidget(parent) { setupUi(); applyColorScheme(); } void RawView::setupUi() { auto *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(4, 4, 4, 4); mainLayout->setSpacing(4); auto *toolbar = new QHBoxLayout(); m_searchEdit = new QLineEdit(this); m_searchEdit->setPlaceholderText(tr("Search…")); m_searchEdit->setClearButtonEnabled(true); connect(m_searchEdit, &QLineEdit::textChanged, this, &RawView::onSearch); connect(m_searchEdit, &QLineEdit::returnPressed, this, &RawView::findNext); m_findPrevBtn = new QPushButton(tr("▲"), this); m_findPrevBtn->setMaximumWidth(28); m_findPrevBtn->setToolTip(tr("Previous match (Shift+F3)")); connect(m_findPrevBtn, &QPushButton::clicked, this, &RawView::findPrevious); m_findNextBtn = new QPushButton(tr("▼"), this); m_findNextBtn->setMaximumWidth(28); m_findNextBtn->setToolTip(tr("Next match (F3 / Enter)")); connect(m_findNextBtn, &QPushButton::clicked, this, &RawView::findNext); auto *nextShortcut = new QShortcut(QKeySequence(Qt::Key_F3), this); connect(nextShortcut, &QShortcut::activated, this, &RawView::findNext); auto *prevShortcut = new QShortcut(QKeySequence(Qt::SHIFT | Qt::Key_F3), this); connect(prevShortcut, &QShortcut::activated, this, &RawView::findPrevious); m_autoScrollCb = new QCheckBox(tr("Auto-scroll"), this); m_autoScrollCb->setChecked(true); // default ON connect(m_autoScrollCb, &QCheckBox::toggled, this, &RawView::onAutoScrollToggled); m_lineCountLbl = new QLabel(tr("Lines: 0"), this); m_lineCountLbl->setMinimumWidth(90); m_copyBtn = new QPushButton(tr("📋 Copy"), this); m_copyBtn->setToolTip(tr("Copy all raw output to clipboard")); connect(m_copyBtn, &QPushButton::clicked, this, &RawView::copyToClipboard); m_clearBtn = new QPushButton(tr("Clear"), this); connect(m_clearBtn, &QPushButton::clicked, this, &RawView::clear); toolbar->addWidget(new QLabel(tr("Search:"), this)); toolbar->addWidget(m_searchEdit, 1); toolbar->addWidget(m_findPrevBtn); toolbar->addWidget(m_findNextBtn); toolbar->addWidget(m_autoScrollCb); toolbar->addWidget(m_lineCountLbl); toolbar->addWidget(m_copyBtn); toolbar->addWidget(m_clearBtn); mainLayout->addLayout(toolbar); m_textEdit = new QPlainTextEdit(this); m_textEdit->setReadOnly(true); m_textEdit->setLineWrapMode(QPlainTextEdit::NoWrap); m_textEdit->setMaximumBlockCount(0); QFont monoFont("Monospace"); monoFont.setStyleHint(QFont::Monospace); monoFont.setPointSize(10); m_textEdit->setFont(monoFont); m_textEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); m_textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(m_textEdit->verticalScrollBar(), &QScrollBar::valueChanged, this, &RawView::onScrollValueChanged); mainLayout->addWidget(m_textEdit, 1); } void RawView::applyColorScheme() { QPalette p = m_textEdit->palette(); p.setColor(QPalette::Base, QColor(0x1e, 0x1e, 0x1e)); p.setColor(QPalette::Text, QColor(0xd4, 0xd4, 0xd4)); m_textEdit->setPalette(p); } void RawView::appendLine(const QString &line, const QSet &suppressedTags) { if (!suppressedTags.isEmpty()) { const auto match = s_tagRe.match(line); if (match.hasMatch() && suppressedTags.contains(match.captured(1).toUpper())) return; } ++m_lineCount; m_lineCountLbl->setText(tr("Lines: %1").arg(m_lineCount)); const QString ts = QDateTime::currentDateTime().toString("hh:mm:ss.zzz"); const bool hasTag = s_tagRe.match(line).hasMatch(); QTextCursor cursor(m_textEdit->document()); cursor.movePosition(QTextCursor::End); QTextCharFormat tsFmt; tsFmt.setForeground(QColor(0x60, 0x60, 0x60)); cursor.insertText(QStringLiteral("[%1] ").arg(ts), tsFmt); QTextCharFormat fmt; fmt.setForeground(hasTag ? QColor(0x56, 0xb6, 0xc2) : QColor(0xd4, 0xd4, 0xd4)); cursor.insertText(line + '\n', fmt); if (m_autoScroll) m_textEdit->verticalScrollBar()->setValue( m_textEdit->verticalScrollBar()->maximum()); } void RawView::clear() { m_textEdit->clear(); m_lineCount = 0; m_lineCountLbl->setText(tr("Lines: 0")); } void RawView::copyToClipboard() { QApplication::clipboard()->setText(m_textEdit->toPlainText()); } bool RawView::searchAndSelect(const QString &text, QTextDocument::FindFlags flags, bool wrapAllowed) { if (text.isEmpty()) return false; QTextDocument *doc = m_textEdit->document(); QTextCursor cursor = doc->find(text, m_textEdit->textCursor(), flags); if (cursor.isNull() && wrapAllowed) { // No further match in the search direction from here - wrap around // to the start (or end, for backward search) and try once more so // the user can keep pressing "next"/"previous" in a loop. QTextCursor wrapCursor(doc); if (flags.testFlag(QTextDocument::FindBackward)) wrapCursor.movePosition(QTextCursor::End); cursor = doc->find(text, wrapCursor, flags); } if (cursor.isNull()) return false; m_textEdit->setTextCursor(cursor); return true; } void RawView::onSearch(const QString &text) { if (text.isEmpty()) { m_searchEdit->setStyleSheet(QString()); return; } // Whenever the search text itself changes, restart from the top of the // document so editing the query doesn't leave the view stranded at // wherever the cursor happened to be from a previous search. QTextCursor cursor(m_textEdit->document()); m_textEdit->setTextCursor(cursor); const bool found = searchAndSelect(text, {}, true); m_searchEdit->setStyleSheet(found ? QString() : "background: #5c2222;"); } void RawView::findNext() { const QString text = m_searchEdit->text(); if (text.isEmpty()) return; const bool found = searchAndSelect(text, {}, true); m_searchEdit->setStyleSheet(found ? QString() : "background: #5c2222;"); } void RawView::findPrevious() { const QString text = m_searchEdit->text(); if (text.isEmpty()) return; const bool found = searchAndSelect(text, QTextDocument::FindBackward, true); m_searchEdit->setStyleSheet(found ? QString() : "background: #5c2222;"); } void RawView::onScrollValueChanged(int value) { if (value < m_textEdit->verticalScrollBar()->maximum() - 5) { m_autoScroll = false; m_autoScrollCb->setChecked(false); } } void RawView::onAutoScrollToggled(bool enabled) { m_autoScroll = enabled; if (enabled) m_textEdit->verticalScrollBar()->setValue( m_textEdit->verticalScrollBar()->maximum()); }