Filtern nach Suchbegriff im ganzen Log implementiert

This commit is contained in:
diabolus committed 2026-10-29 16:39:51 +01:00
1 parent a0c50345d4
commit 4c14e7cb13
13 files changed
+848 -559

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(UARTScope VERSION 1.3.0 LANGUAGES CXX)
project(UARTScope VERSION 1.4.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
+6
View File
@@ -296,6 +296,12 @@ uartscope/
## Changelog
**1.4.0**
- Raw-View: neues Treffer-Panel („☰ Matches“, Strg+Umschalt+F). Listet alle Zeilen auf, die den Suchbegriff enthalten (live beim Tippen, optional Groß-/Kleinschreibung beachten)
- Treffer lassen sich mit Strg/Umschalt mehrfach auswählen (Strg+A bzw. „Select all“ für alle) und mit Strg+C oder „Copy selection“ kopieren – standardmäßig mit Zeitstempel, optional „Copy without timestamp“
- Neue Such-Optionen neben dem Suchfeld: „Aa“ (Groß-/Kleinschreibung beachten) und „Regex“ (regulärer Ausdruck, z.B. `error|fail|timeout`). Sie gelten für F3/Umschalt+F3 und für die Trefferliste; ungültige Muster (und solche, die leeren Text matchen) färben das Suchfeld rot und werden im Panel gemeldet. Hinweis: Zeilen enthalten den Zeitstempel, `^` bezieht sich also auf dessen Anfang
- Doppelklick auf einen Treffer springt zur Zeile im Log; „Refresh“ sucht erneut und übernimmt neue Zeilen (die Liste ist bewusst ein Schnappschuss, damit die Auswahl bei laufendem Log nicht verrutscht)
**1.3.0**
- Toolbar aufgeräumt: nur noch Connect/Disconnect/Clear all sind direkt sichtbar
- Auto-reconnect, Reconnect-Intervall, Tag-Filter, Screenshot-Ordner, Format-Referenz und Über UARTScope in die neue Menüleiste (Einstellungen/Hilfe) verschoben
+33
View File
@@ -10,10 +10,16 @@
#include <QSet>
#include <QRegularExpression>
#include <QTextDocument>
#include <QListWidget>
#include <QSplitter>
#include <QTimer>
// RawView shows the raw UART output with timestamps, search (with next/
// previous navigation and wrap-around), tag suppression, unlimited history,
// H+V scrolling and a copy-to-clipboard button.
// A "Matches" panel (Ctrl+Shift+F) lists every line containing the search
// text; entries can be multi-selected and copied (with timestamps by default)
// or double-clicked to jump to the line in the log.
// Auto-scroll is ON by default.
class RawView : public QWidget
{
@@ -33,6 +39,11 @@ private slots:
void copyToClipboard();
void findNext();
void findPrevious();
void toggleMatchPanel(bool show);
void refreshMatches();
void updateMatchLabel();
void jumpToMatch(QListWidgetItem *item);
void copySelectedMatches();
private:
void setupUi();
@@ -42,6 +53,12 @@ private:
// `wrapAllowed` is true, retries once from the start/end of the
// document so search wraps around instead of dead-ending.
bool searchAndSelect(const QString &text, QTextDocument::FindFlags flags, bool wrapAllowed);
// Regex helpers: makeRegex() honours the "Aa" (match case) checkbox;
// regexError() returns an empty string if the pattern is usable, else
// a short reason (invalid syntax, or it matches empty text which would
// make "find next" stall and match every line in the list).
QRegularExpression makeRegex(const QString &text) const;
static QString regexError(const QRegularExpression &re);
QPlainTextEdit *m_textEdit = nullptr;
QLineEdit *m_searchEdit = nullptr;
@@ -51,6 +68,22 @@ private:
QPushButton *m_copyBtn = nullptr;
QCheckBox *m_autoScrollCb = nullptr;
QLabel *m_lineCountLbl = nullptr;
// Matches panel (list of all lines containing the search text)
QPushButton *m_matchBtn = nullptr;
QWidget *m_matchPanel = nullptr;
QListWidget *m_matchList = nullptr;
QLabel *m_matchCountLbl = nullptr;
QCheckBox *m_caseCb = nullptr; // toolbar: "Aa" match case
QCheckBox *m_regexCb = nullptr; // toolbar: "Regex"
QString m_matchError; // non-empty = invalid regex
QCheckBox *m_noTsCb = nullptr;
QPushButton *m_refreshBtn = nullptr;
QPushButton *m_selectAllBtn = nullptr;
QPushButton *m_copyMatchBtn = nullptr;
QTimer *m_matchTimer = nullptr;
int m_matchTotal = 0;
bool m_matchTruncated = false;
bool m_autoScroll = true; // ON by default
int m_lineCount = 0;
+1 -1
View File
@@ -7,7 +7,7 @@ int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setApplicationName("UARTScope");
app.setApplicationVersion("1.3.0");
app.setApplicationVersion("1.4.0");
app.setOrganizationName("ChicaDev");
app.setStyle(QStyleFactory::create("Fusion"));
+1 -1
View File
@@ -763,7 +763,7 @@ void MainWindow::showAbout()
grid->addWidget(v, row, 1);
};
addRow(0, tr("Version"), "1.3.0");
addRow(0, tr("Version"), "1.4.0");
addRow(1, tr("Entwickler"), "Dany Thinnes");
addRow(2, tr("Projekt"), "Projekt Hirnfrei");
addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR));
+247 -3
View File
@@ -9,6 +9,9 @@
#include <QDateTime>
#include <QShortcut>
#include <QKeySequence>
#include <QTextBlock>
#include <QListWidgetItem>
#include <algorithm>
// Tag names may contain letters, digits, underscore AND hyphen (e.g.
// [I2C-BUS]) - keep this in sync with the tagRe in serialworker.cpp, which
@@ -72,7 +75,25 @@ void RawView::setupUi()
toolbar->addWidget(new QLabel(tr("Search:"), this));
toolbar->addWidget(m_searchEdit, 1);
toolbar->addWidget(m_findPrevBtn);
m_matchBtn = new QPushButton(tr("☰ Matches"), this);
m_matchBtn->setCheckable(true);
m_matchBtn->setToolTip(tr("List all lines containing the search text (Ctrl+Shift+F)"));
connect(m_matchBtn, &QPushButton::toggled, this, &RawView::toggleMatchPanel);
auto *matchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_F), this);
connect(matchShortcut, &QShortcut::activated, m_matchBtn, &QPushButton::toggle);
m_caseCb = new QCheckBox(tr("Aa"), this);
m_caseCb->setToolTip(tr("Match case"));
m_regexCb = new QCheckBox(tr("Regex"), this);
m_regexCb->setToolTip(tr("Treat the search text as a regular expression (e.g. error|fail|timeout)"));
// Re-run the search whenever an option changes (also refreshes the matches list)
connect(m_caseCb, &QCheckBox::toggled, this, [this] { onSearch(m_searchEdit->text()); });
connect(m_regexCb, &QCheckBox::toggled, this, [this] { onSearch(m_searchEdit->text()); });
toolbar->addWidget(m_findNextBtn);
toolbar->addWidget(m_caseCb);
toolbar->addWidget(m_regexCb);
toolbar->addWidget(m_matchBtn);
toolbar->addWidget(m_autoScrollCb);
toolbar->addWidget(m_lineCountLbl);
toolbar->addWidget(m_copyBtn);
@@ -95,7 +116,62 @@ void RawView::setupUi()
connect(m_textEdit->verticalScrollBar(), &QScrollBar::valueChanged,
this, &RawView::onScrollValueChanged);
mainLayout->addWidget(m_textEdit, 1);
// --- Matches panel (hidden until toggled) ---
m_matchPanel = new QWidget(this);
auto *panelLayout = new QVBoxLayout(m_matchPanel);
panelLayout->setContentsMargins(0, 0, 0, 0);
panelLayout->setSpacing(4);
auto *panelBar = new QHBoxLayout();
m_matchCountLbl = new QLabel(tr("Matches: 0"), m_matchPanel);
m_noTsCb = new QCheckBox(tr("Copy without timestamp"), m_matchPanel);
m_refreshBtn = new QPushButton(tr("↻ Refresh"), m_matchPanel);
m_refreshBtn->setToolTip(tr("Search the log again (picks up new lines)"));
m_selectAllBtn = new QPushButton(tr("Select all"), m_matchPanel);
m_copyMatchBtn = new QPushButton(tr("📋 Copy selection"), m_matchPanel);
m_copyMatchBtn->setToolTip(tr("Copy selected lines to clipboard (Ctrl+C)"));
panelBar->addWidget(m_matchCountLbl, 1);
panelBar->addWidget(m_noTsCb);
panelBar->addWidget(m_refreshBtn);
panelBar->addWidget(m_selectAllBtn);
panelBar->addWidget(m_copyMatchBtn);
panelLayout->addLayout(panelBar);
m_matchList = new QListWidget(m_matchPanel);
m_matchList->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_matchList->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_matchList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_matchList->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
m_matchList->setWordWrap(false);
m_matchList->setUniformItemSizes(true);
m_matchList->setFont(monoFont);
m_matchList->setToolTip(tr("Ctrl/Shift to select several lines, double-click to jump to the line in the log"));
panelLayout->addWidget(m_matchList, 1);
// Re-run the search shortly after the user stops typing / changes options
m_matchTimer = new QTimer(this);
m_matchTimer->setSingleShot(true);
m_matchTimer->setInterval(150);
connect(m_matchTimer, &QTimer::timeout, this, &RawView::refreshMatches);
connect(m_refreshBtn, &QPushButton::clicked, this, &RawView::refreshMatches);
connect(m_selectAllBtn, &QPushButton::clicked, m_matchList, &QListWidget::selectAll);
connect(m_copyMatchBtn, &QPushButton::clicked, this, &RawView::copySelectedMatches);
connect(m_matchList, &QListWidget::itemDoubleClicked, this, &RawView::jumpToMatch);
connect(m_matchList, &QListWidget::itemSelectionChanged, this, &RawView::updateMatchLabel);
auto *copyShortcut = new QShortcut(QKeySequence::Copy, m_matchList);
copyShortcut->setContext(Qt::WidgetShortcut);
connect(copyShortcut, &QShortcut::activated, this, &RawView::copySelectedMatches);
m_matchPanel->setVisible(false);
auto *splitter = new QSplitter(Qt::Vertical, this);
splitter->setChildrenCollapsible(false);
splitter->addWidget(m_textEdit);
splitter->addWidget(m_matchPanel);
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 2);
mainLayout->addWidget(splitter, 1);
}
void RawView::applyColorScheme()
@@ -104,6 +180,8 @@ void RawView::applyColorScheme()
p.setColor(QPalette::Base, QColor(0x1e, 0x1e, 0x1e));
p.setColor(QPalette::Text, QColor(0xd4, 0xd4, 0xd4));
m_textEdit->setPalette(p);
if (m_matchList)
m_matchList->setPalette(p);
}
void RawView::appendLine(const QString &line, const QSet<QString> &suppressedTags)
@@ -139,6 +217,12 @@ void RawView::appendLine(const QString &line, const QSet<QString> &suppressedTag
void RawView::clear()
{
m_textEdit->clear();
if (m_matchList) { // block numbers in the list are invalid after a clear
m_matchList->clear();
m_matchTotal = 0;
m_matchTruncated = false;
updateMatchLabel();
}
m_lineCount = 0;
m_lineCountLbl->setText(tr("Lines: 0"));
}
@@ -154,7 +238,23 @@ bool RawView::searchAndSelect(const QString &text, QTextDocument::FindFlags flag
return false;
QTextDocument *doc = m_textEdit->document();
QTextCursor cursor = doc->find(text, m_textEdit->textCursor(), flags);
QTextDocument::FindFlags f = flags;
if (m_caseCb->isChecked())
f |= QTextDocument::FindCaseSensitively;
const bool useRegex = m_regexCb->isChecked();
QRegularExpression re;
if (useRegex) {
re = makeRegex(text);
if (!regexError(re).isEmpty())
return false;
}
auto doFind = [&](const QTextCursor &from) {
return useRegex ? doc->find(re, from, f) : doc->find(text, from, f);
};
QTextCursor cursor = doFind(m_textEdit->textCursor());
if (cursor.isNull() && wrapAllowed) {
// No further match in the search direction from here - wrap around
@@ -163,7 +263,7 @@ bool RawView::searchAndSelect(const QString &text, QTextDocument::FindFlags flag
QTextCursor wrapCursor(doc);
if (flags.testFlag(QTextDocument::FindBackward))
wrapCursor.movePosition(QTextCursor::End);
cursor = doc->find(text, wrapCursor, flags);
cursor = doFind(wrapCursor);
}
if (cursor.isNull())
@@ -175,6 +275,10 @@ bool RawView::searchAndSelect(const QString &text, QTextDocument::FindFlags flag
void RawView::onSearch(const QString &text)
{
// Keep the matches list in sync with the search text while it is open
if (m_matchPanel && !m_matchPanel->isHidden())
m_matchTimer->start();
if (text.isEmpty()) {
m_searchEdit->setStyleSheet(QString());
return;
@@ -223,3 +327,143 @@ void RawView::onAutoScrollToggled(bool enabled)
m_textEdit->verticalScrollBar()->setValue(
m_textEdit->verticalScrollBar()->maximum());
}
// ---------------------------------------------------------------------------
// Matches panel: list every log line containing the search text
// ---------------------------------------------------------------------------
void RawView::toggleMatchPanel(bool show)
{
m_matchPanel->setVisible(show);
if (show)
refreshMatches();
}
void RawView::refreshMatches()
{
// The list is a snapshot of the log at the time of searching, so new
// incoming lines don't shift the user's selection. "Refresh" re-runs it.
constexpr int kMaxMatches = 100000;
m_matchList->clear();
m_matchTotal = 0;
m_matchTruncated = false;
m_matchError.clear();
const QString needle = m_searchEdit->text();
if (!needle.isEmpty()) {
const Qt::CaseSensitivity cs =
m_caseCb->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive;
const bool useRegex = m_regexCb->isChecked();
QRegularExpression re;
if (useRegex) {
re = makeRegex(needle);
m_matchError = regexError(re);
if (!m_matchError.isEmpty()) {
updateMatchLabel();
return;
}
}
m_matchList->setUpdatesEnabled(false);
for (QTextBlock b = m_textEdit->document()->begin(); b.isValid(); b = b.next()) {
const QString text = b.text();
const bool hit = useRegex ? re.match(text).hasMatch()
: text.contains(needle, cs);
if (!hit)
continue;
if (m_matchTotal >= kMaxMatches) {
m_matchTruncated = true;
break;
}
auto *item = new QListWidgetItem(text, m_matchList);
item->setData(Qt::UserRole, b.blockNumber());
++m_matchTotal;
}
m_matchList->setUpdatesEnabled(true);
}
updateMatchLabel();
}
void RawView::updateMatchLabel()
{
const int selected = m_matchList->selectedItems().size();
if (!m_matchError.isEmpty()) {
m_matchCountLbl->setText(tr("Invalid regex: %1").arg(m_matchError));
return;
}
QString text = tr("Matches: %1").arg(m_matchTotal);
if (m_matchTruncated)
text += tr(" (limited)");
if (selected > 0)
text += tr(" – %1 selected").arg(selected);
m_matchCountLbl->setText(text);
}
void RawView::jumpToMatch(QListWidgetItem *item)
{
if (!item)
return;
const QTextBlock block = m_textEdit->document()->findBlockByNumber(
item->data(Qt::UserRole).toInt());
if (!block.isValid())
return;
QTextCursor cursor(block);
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
m_textEdit->setTextCursor(cursor);
m_textEdit->centerCursor();
}
void RawView::copySelectedMatches()
{
QList<QListWidgetItem *> items = m_matchList->selectedItems();
if (items.isEmpty())
return;
// selectedItems() is in selection order - copy in log order instead
std::sort(items.begin(), items.end(),
[this](QListWidgetItem *a, QListWidgetItem *b) {
return m_matchList->row(a) < m_matchList->row(b);
});
static const QRegularExpression tsRe(R"(^\[\d{2}:\d{2}:\d{2}\.\d{3}\]\s+)");
const bool stripTs = m_noTsCb->isChecked();
QStringList lines;
lines.reserve(items.size());
for (QListWidgetItem *it : items) {
QString text = it->text();
if (stripTs)
text.remove(tsRe);
lines << text;
}
QApplication::clipboard()->setText(lines.join('\n'));
}
// ---------------------------------------------------------------------------
// Regex support
// ---------------------------------------------------------------------------
QRegularExpression RawView::makeRegex(const QString &text) const
{
return QRegularExpression(
text,
m_caseCb->isChecked() ? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption);
}
QString RawView::regexError(const QRegularExpression &re)
{
if (!re.isValid())
return re.errorString();
// A pattern like "a*" or "^" matches the empty string: it would match every
// line in the list and make "find next" stall on a zero-length selection.
if (re.match(QString()).hasMatch())
return tr("pattern matches empty text");
return QString();
}
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: diabolus <your@email.com>
pkgname=uartscope
pkgver=1.1.0.r6.g98f6f1c
pkgver=1.1.0.r7.ga0c5034
pkgrel=1
pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect"
arch=('x86_64' 'aarch64')
File diff suppressed because it is too large. Load diff
Binary file not shown.
+2 -2
View File
@@ -3,10 +3,10 @@
pkgname = uartscope
pkgbase = uartscope
xdata = pkgtype=pkg
pkgver = 1.1.0.r6.g98f6f1c-1
pkgver = 1.1.0.r7.ga0c5034-1
pkgdesc = Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect
url = https://git.projekt-hirnfrei.de/diabolus/uartscope
builddate = 1791306653
builddate = 1792095702
packager = Unknown Packager
size = 484465
arch = x86_64
Binary file not shown.
Submodule uartscope-git/src/uartscope updated: 98f6f1c903...a0c50345d4.
+1 -1
View File
@@ -1,3 +1,3 @@
98f6f1c903d808cd169405560139e8cfaa90a815 not-for-merge branch 'main' of https://git.projekt-hirnfrei.de/diabolus/uartscope
a0c50345d40f9910f0e99dedf58c7bd5c21d41ef not-for-merge branch 'main' of https://git.projekt-hirnfrei.de/diabolus/uartscope
cc102c93eb17f7b910d8e74c3505f198bed77f10 not-for-merge tag 'v1.0.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope
36923e16461bc15086c84187790dbcb5f0a5030f not-for-merge tag 'v1.1.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope