Filtern nach Suchbegriff im ganzen Log implementiert
This commit is contained in:
1 parent
a0c50345d4
commit
4c14e7cb13
13 files changed
+848
-559
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
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 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|||||||
@@ -296,6 +296,12 @@ uartscope/
|
|||||||
|
|
||||||
## Changelog
|
## 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**
|
**1.3.0**
|
||||||
- Toolbar aufgeräumt: nur noch Connect/Disconnect/Clear all sind direkt sichtbar
|
- 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
|
- Auto-reconnect, Reconnect-Intervall, Tag-Filter, Screenshot-Ordner, Format-Referenz und Über UARTScope in die neue Menüleiste (Einstellungen/Hilfe) verschoben
|
||||||
|
|||||||
@@ -10,10 +10,16 @@
|
|||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
// RawView shows the raw UART output with timestamps, search (with next/
|
// RawView shows the raw UART output with timestamps, search (with next/
|
||||||
// previous navigation and wrap-around), tag suppression, unlimited history,
|
// previous navigation and wrap-around), tag suppression, unlimited history,
|
||||||
// H+V scrolling and a copy-to-clipboard button.
|
// 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.
|
// Auto-scroll is ON by default.
|
||||||
class RawView : public QWidget
|
class RawView : public QWidget
|
||||||
{
|
{
|
||||||
@@ -33,6 +39,11 @@ private slots:
|
|||||||
void copyToClipboard();
|
void copyToClipboard();
|
||||||
void findNext();
|
void findNext();
|
||||||
void findPrevious();
|
void findPrevious();
|
||||||
|
void toggleMatchPanel(bool show);
|
||||||
|
void refreshMatches();
|
||||||
|
void updateMatchLabel();
|
||||||
|
void jumpToMatch(QListWidgetItem *item);
|
||||||
|
void copySelectedMatches();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupUi();
|
void setupUi();
|
||||||
@@ -42,6 +53,12 @@ private:
|
|||||||
// `wrapAllowed` is true, retries once from the start/end of the
|
// `wrapAllowed` is true, retries once from the start/end of the
|
||||||
// document so search wraps around instead of dead-ending.
|
// document so search wraps around instead of dead-ending.
|
||||||
bool searchAndSelect(const QString &text, QTextDocument::FindFlags flags, bool wrapAllowed);
|
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;
|
QPlainTextEdit *m_textEdit = nullptr;
|
||||||
QLineEdit *m_searchEdit = nullptr;
|
QLineEdit *m_searchEdit = nullptr;
|
||||||
@@ -51,6 +68,22 @@ private:
|
|||||||
QPushButton *m_copyBtn = nullptr;
|
QPushButton *m_copyBtn = nullptr;
|
||||||
QCheckBox *m_autoScrollCb = nullptr;
|
QCheckBox *m_autoScrollCb = nullptr;
|
||||||
QLabel *m_lineCountLbl = 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
|
bool m_autoScroll = true; // ON by default
|
||||||
int m_lineCount = 0;
|
int m_lineCount = 0;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ int main(int argc, char *argv[])
|
|||||||
{
|
{
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
app.setApplicationName("UARTScope");
|
app.setApplicationName("UARTScope");
|
||||||
app.setApplicationVersion("1.3.0");
|
app.setApplicationVersion("1.4.0");
|
||||||
app.setOrganizationName("ChicaDev");
|
app.setOrganizationName("ChicaDev");
|
||||||
|
|
||||||
app.setStyle(QStyleFactory::create("Fusion"));
|
app.setStyle(QStyleFactory::create("Fusion"));
|
||||||
|
|||||||
+1
-1
@@ -763,7 +763,7 @@ void MainWindow::showAbout()
|
|||||||
grid->addWidget(v, row, 1);
|
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(1, tr("Entwickler"), "Dany Thinnes");
|
||||||
addRow(2, tr("Projekt"), "Projekt Hirnfrei");
|
addRow(2, tr("Projekt"), "Projekt Hirnfrei");
|
||||||
addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR));
|
addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR));
|
||||||
|
|||||||
+247
-3
@@ -9,6 +9,9 @@
|
|||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
#include <QKeySequence>
|
#include <QKeySequence>
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QListWidgetItem>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
// Tag names may contain letters, digits, underscore AND hyphen (e.g.
|
// Tag names may contain letters, digits, underscore AND hyphen (e.g.
|
||||||
// [I2C-BUS]) - keep this in sync with the tagRe in serialworker.cpp, which
|
// [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(new QLabel(tr("Search:"), this));
|
||||||
toolbar->addWidget(m_searchEdit, 1);
|
toolbar->addWidget(m_searchEdit, 1);
|
||||||
toolbar->addWidget(m_findPrevBtn);
|
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_findNextBtn);
|
||||||
|
toolbar->addWidget(m_caseCb);
|
||||||
|
toolbar->addWidget(m_regexCb);
|
||||||
|
toolbar->addWidget(m_matchBtn);
|
||||||
toolbar->addWidget(m_autoScrollCb);
|
toolbar->addWidget(m_autoScrollCb);
|
||||||
toolbar->addWidget(m_lineCountLbl);
|
toolbar->addWidget(m_lineCountLbl);
|
||||||
toolbar->addWidget(m_copyBtn);
|
toolbar->addWidget(m_copyBtn);
|
||||||
@@ -95,7 +116,62 @@ void RawView::setupUi()
|
|||||||
connect(m_textEdit->verticalScrollBar(), &QScrollBar::valueChanged,
|
connect(m_textEdit->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||||
this, &RawView::onScrollValueChanged);
|
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()
|
void RawView::applyColorScheme()
|
||||||
@@ -104,6 +180,8 @@ void RawView::applyColorScheme()
|
|||||||
p.setColor(QPalette::Base, QColor(0x1e, 0x1e, 0x1e));
|
p.setColor(QPalette::Base, QColor(0x1e, 0x1e, 0x1e));
|
||||||
p.setColor(QPalette::Text, QColor(0xd4, 0xd4, 0xd4));
|
p.setColor(QPalette::Text, QColor(0xd4, 0xd4, 0xd4));
|
||||||
m_textEdit->setPalette(p);
|
m_textEdit->setPalette(p);
|
||||||
|
if (m_matchList)
|
||||||
|
m_matchList->setPalette(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RawView::appendLine(const QString &line, const QSet<QString> &suppressedTags)
|
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()
|
void RawView::clear()
|
||||||
{
|
{
|
||||||
m_textEdit->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_lineCount = 0;
|
||||||
m_lineCountLbl->setText(tr("Lines: 0"));
|
m_lineCountLbl->setText(tr("Lines: 0"));
|
||||||
}
|
}
|
||||||
@@ -154,7 +238,23 @@ bool RawView::searchAndSelect(const QString &text, QTextDocument::FindFlags flag
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
QTextDocument *doc = m_textEdit->document();
|
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) {
|
if (cursor.isNull() && wrapAllowed) {
|
||||||
// No further match in the search direction from here - wrap around
|
// 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);
|
QTextCursor wrapCursor(doc);
|
||||||
if (flags.testFlag(QTextDocument::FindBackward))
|
if (flags.testFlag(QTextDocument::FindBackward))
|
||||||
wrapCursor.movePosition(QTextCursor::End);
|
wrapCursor.movePosition(QTextCursor::End);
|
||||||
cursor = doc->find(text, wrapCursor, flags);
|
cursor = doFind(wrapCursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor.isNull())
|
if (cursor.isNull())
|
||||||
@@ -175,6 +275,10 @@ bool RawView::searchAndSelect(const QString &text, QTextDocument::FindFlags flag
|
|||||||
|
|
||||||
void RawView::onSearch(const QString &text)
|
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()) {
|
if (text.isEmpty()) {
|
||||||
m_searchEdit->setStyleSheet(QString());
|
m_searchEdit->setStyleSheet(QString());
|
||||||
return;
|
return;
|
||||||
@@ -223,3 +327,143 @@ void RawView::onAutoScrollToggled(bool enabled)
|
|||||||
m_textEdit->verticalScrollBar()->setValue(
|
m_textEdit->verticalScrollBar()->setValue(
|
||||||
m_textEdit->verticalScrollBar()->maximum());
|
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,6 +1,6 @@
|
|||||||
# Maintainer: diabolus <your@email.com>
|
# Maintainer: diabolus <your@email.com>
|
||||||
pkgname=uartscope
|
pkgname=uartscope
|
||||||
pkgver=1.1.0.r6.g98f6f1c
|
pkgver=1.1.0.r7.ga0c5034
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect"
|
pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect"
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -3,10 +3,10 @@
|
|||||||
pkgname = uartscope
|
pkgname = uartscope
|
||||||
pkgbase = uartscope
|
pkgbase = uartscope
|
||||||
xdata = pkgtype=pkg
|
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
|
pkgdesc = Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect
|
||||||
url = https://git.projekt-hirnfrei.de/diabolus/uartscope
|
url = https://git.projekt-hirnfrei.de/diabolus/uartscope
|
||||||
builddate = 1791306653
|
builddate = 1792095702
|
||||||
packager = Unknown Packager
|
packager = Unknown Packager
|
||||||
size = 484465
|
size = 484465
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
|
|||||||
Binary file not shown.
Submodule uartscope-git/src/uartscope updated: 98f6f1c903...a0c50345d4.
@@ -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
|
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
|
36923e16461bc15086c84187790dbcb5f0a5030f not-for-merge tag 'v1.1.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope
|
||||||
Reference in new issue
Block a user