331 lines
9.9 KiB
C++
331 lines
9.9 KiB
C++
#include "FileSearchPanel.h"
|
|
|
|
#include <QDir>
|
|
#include <QDirIterator>
|
|
#include <QFile>
|
|
#include <QTextStream>
|
|
#include <QRegularExpression>
|
|
#include <QKeyEvent>
|
|
#include <QtConcurrent/QtConcurrent>
|
|
#include <QFuture>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Konstruktor
|
|
// ---------------------------------------------------------------------------
|
|
FileSearchPanel::FileSearchPanel(QWidget *parent)
|
|
: QWidget(parent)
|
|
{
|
|
setupUi();
|
|
hide();
|
|
|
|
m_watcher = new QFutureWatcher<QList<Match>>(this);
|
|
connect(m_watcher, &QFutureWatcher<QList<Match>>::finished,
|
|
this, &FileSearchPanel::onSearchFinished);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UI
|
|
// ---------------------------------------------------------------------------
|
|
void FileSearchPanel::setupUi()
|
|
{
|
|
QVBoxLayout *root = new QVBoxLayout(this);
|
|
root->setContentsMargins(6, 4, 6, 4);
|
|
root->setSpacing(4);
|
|
|
|
// ---- Zeile 1: Suchbegriff ----
|
|
QHBoxLayout *row1 = new QHBoxLayout();
|
|
row1->addWidget(new QLabel(tr("Suchen in Dateien:"), this));
|
|
|
|
m_searchEdit = new QLineEdit(this);
|
|
m_searchEdit->setPlaceholderText(tr("Suchbegriff…"));
|
|
m_searchEdit->setClearButtonEnabled(true);
|
|
row1->addWidget(m_searchEdit, 1);
|
|
|
|
m_btnSearch = new QPushButton(tr("Suchen"), this);
|
|
m_btnSearch->setDefault(true);
|
|
row1->addWidget(m_btnSearch);
|
|
|
|
m_btnClose = new QPushButton(tr("✕"), this);
|
|
m_btnClose->setFixedWidth(24);
|
|
m_btnClose->setFlat(true);
|
|
m_btnClose->setToolTip(tr("Schließen"));
|
|
row1->addWidget(m_btnClose);
|
|
|
|
root->addLayout(row1);
|
|
|
|
// ---- Zeile 2: Optionen + Filter ----
|
|
QHBoxLayout *row2 = new QHBoxLayout();
|
|
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
|
|
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
|
|
m_chkRegex = new QCheckBox(tr("Regex"), this);
|
|
|
|
row2->addWidget(m_chkCase);
|
|
row2->addWidget(m_chkWord);
|
|
row2->addWidget(m_chkRegex);
|
|
row2->addSpacing(12);
|
|
|
|
row2->addWidget(new QLabel(tr("Dateitypen:"), this));
|
|
m_filterEdit = new QLineEdit(this);
|
|
m_filterEdit->setText("*.html *.php *.css *.js *.c *.cpp *.h");
|
|
m_filterEdit->setFixedWidth(220);
|
|
m_filterEdit->setToolTip(tr("Leerzeichen-getrennte Muster, z.B.: *.php *.html"));
|
|
row2->addWidget(m_filterEdit);
|
|
row2->addStretch();
|
|
|
|
root->addLayout(row2);
|
|
|
|
// ---- Fortschritt + Status ----
|
|
m_progress = new QProgressBar(this);
|
|
m_progress->setRange(0, 0); // Unbestimmter Modus
|
|
m_progress->setFixedHeight(4);
|
|
m_progress->hide();
|
|
root->addWidget(m_progress);
|
|
|
|
m_statusLabel = new QLabel(this);
|
|
m_statusLabel->setStyleSheet("color: palette(mid);");
|
|
root->addWidget(m_statusLabel);
|
|
|
|
// ---- Ergebnisliste ----
|
|
m_results = new QTreeWidget(this);
|
|
m_results->setHeaderHidden(true);
|
|
m_results->setRootIsDecorated(true);
|
|
m_results->setIndentation(16);
|
|
m_results->setUniformRowHeights(true);
|
|
m_results->setAlternatingRowColors(true);
|
|
root->addWidget(m_results, 1);
|
|
|
|
// ---- Verbindungen ----
|
|
connect(m_btnSearch, &QPushButton::clicked, this, &FileSearchPanel::onSearch);
|
|
connect(m_searchEdit, &QLineEdit::returnPressed, this, &FileSearchPanel::onSearch);
|
|
connect(m_btnClose, &QPushButton::clicked, this, [this]()
|
|
{
|
|
hide();
|
|
});
|
|
connect(m_results, &QTreeWidget::itemActivated,
|
|
this, &FileSearchPanel::onResultActivated);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Öffentliche Schnittstelle
|
|
// ---------------------------------------------------------------------------
|
|
void FileSearchPanel::setSearchRoot(const QString &path)
|
|
{
|
|
m_searchRoot = path;
|
|
}
|
|
|
|
void FileSearchPanel::activate()
|
|
{
|
|
show();
|
|
m_searchEdit->setFocus();
|
|
m_searchEdit->selectAll();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Suche starten
|
|
// ---------------------------------------------------------------------------
|
|
void FileSearchPanel::onSearch()
|
|
{
|
|
const QString needle = m_searchEdit->text().trimmed();
|
|
if (needle.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_searchRoot.isEmpty())
|
|
{
|
|
m_statusLabel->setText(tr("Kein Projektverzeichnis geöffnet."));
|
|
return;
|
|
}
|
|
|
|
// Laufende Suche abbrechen
|
|
if (m_watcher->isRunning())
|
|
{
|
|
m_watcher->cancel();
|
|
m_watcher->waitForFinished();
|
|
}
|
|
|
|
m_results->clear();
|
|
m_statusLabel->setText(tr("Suche läuft…"));
|
|
m_progress->show();
|
|
m_btnSearch->setEnabled(false);
|
|
|
|
const QString root = m_searchRoot;
|
|
const bool cs = m_chkCase->isChecked();
|
|
const bool word = m_chkWord->isChecked();
|
|
const bool regex = m_chkRegex->isChecked();
|
|
const QStringList extensions = m_filterEdit->text().simplified().split(' ',
|
|
Qt::SkipEmptyParts);
|
|
|
|
QFuture<QList<Match>> future = QtConcurrent::run(
|
|
[this, root, needle, cs, word, regex, extensions]()
|
|
{
|
|
return searchInFiles(root, needle, cs, word, regex, extensions);
|
|
}
|
|
);
|
|
|
|
m_watcher->setFuture(future);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Suchergebnisse anzeigen
|
|
// ---------------------------------------------------------------------------
|
|
void FileSearchPanel::onSearchFinished()
|
|
{
|
|
m_progress->hide();
|
|
m_btnSearch->setEnabled(true);
|
|
|
|
if (m_watcher->isCanceled())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const QList<Match> matches = m_watcher->result();
|
|
|
|
// Ergebnisse gruppiert nach Datei aufbauen
|
|
QString currentFile;
|
|
QTreeWidgetItem *fileItem = nullptr;
|
|
int fileCount = 0;
|
|
int matchCount = 0;
|
|
|
|
for (const Match &m : matches)
|
|
{
|
|
if (m.filePath != currentFile)
|
|
{
|
|
currentFile = m.filePath;
|
|
++fileCount;
|
|
|
|
fileItem = new QTreeWidgetItem(m_results);
|
|
fileItem->setText(0, QFileInfo(m.filePath).fileName());
|
|
fileItem->setToolTip(0, m.filePath);
|
|
fileItem->setData(0, Qt::UserRole, m.filePath);
|
|
fileItem->setData(0, Qt::UserRole + 1, -1);
|
|
|
|
QFont boldFont = fileItem->font(0);
|
|
boldFont.setBold(true);
|
|
fileItem->setFont(0, boldFont);
|
|
fileItem->setExpanded(true);
|
|
}
|
|
|
|
QTreeWidgetItem *lineItem = new QTreeWidgetItem(fileItem);
|
|
lineItem->setText(0, QString(" Zeile %1: %2")
|
|
.arg(m.line)
|
|
.arg(m.content.trimmed().left(120)));
|
|
lineItem->setToolTip(0, m.content.trimmed());
|
|
lineItem->setData(0, Qt::UserRole, m.filePath);
|
|
lineItem->setData(0, Qt::UserRole + 1, m.line);
|
|
|
|
++matchCount;
|
|
}
|
|
|
|
// Datei-Titelzeilen um Trefferanzahl ergänzen
|
|
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
|
|
{
|
|
QTreeWidgetItem *item = m_results->topLevelItem(i);
|
|
const int count = item->childCount();
|
|
item->setText(0, QString("%1 (%2 Treffer)")
|
|
.arg(QFileInfo(item->data(0, Qt::UserRole).toString()).fileName())
|
|
.arg(count));
|
|
}
|
|
|
|
if (matchCount == 0)
|
|
{
|
|
m_statusLabel->setText(tr("Keine Treffer gefunden."));
|
|
}
|
|
else
|
|
{
|
|
m_statusLabel->setText(tr("%1 Treffer in %2 Datei(en).")
|
|
.arg(matchCount)
|
|
.arg(fileCount));
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Klick auf Treffer → Datei + Zeile öffnen
|
|
// ---------------------------------------------------------------------------
|
|
void FileSearchPanel::onResultActivated(QTreeWidgetItem *item, int /*column*/)
|
|
{
|
|
const QString path = item->data(0, Qt::UserRole).toString();
|
|
const int line = item->data(0, Qt::UserRole + 1).toInt();
|
|
|
|
if (path.isEmpty() || line < 0)
|
|
{
|
|
// Datei-Titelzeile: nur auf-/zuklappen
|
|
item->setExpanded(!item->isExpanded());
|
|
return;
|
|
}
|
|
|
|
emit fileLineRequested(path, line);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Eigentliche Suchroutine (läuft in Thread-Pool)
|
|
// ---------------------------------------------------------------------------
|
|
QList<FileSearchPanel::Match> FileSearchPanel::searchInFiles(
|
|
const QString &root,
|
|
const QString &needle,
|
|
bool caseSensitive,
|
|
bool wholeWord,
|
|
bool useRegex,
|
|
const QStringList &extensions) const
|
|
{
|
|
QList<Match> results;
|
|
|
|
// Regulären Ausdruck vorbereiten
|
|
QString pattern = useRegex ? needle : QRegularExpression::escape(needle);
|
|
if (wholeWord)
|
|
{
|
|
pattern = "\\b" + pattern + "\\b";
|
|
}
|
|
|
|
QRegularExpression re(pattern,
|
|
caseSensitive
|
|
? QRegularExpression::NoPatternOption
|
|
: QRegularExpression::CaseInsensitiveOption);
|
|
|
|
if (!re.isValid())
|
|
{
|
|
return results;
|
|
}
|
|
|
|
// Verzeichnis rekursiv durchsuchen
|
|
QDirIterator it(root,
|
|
extensions.isEmpty()
|
|
? QStringList("*")
|
|
: extensions,
|
|
QDir::Files,
|
|
QDirIterator::Subdirectories);
|
|
|
|
while (it.hasNext())
|
|
{
|
|
if (m_watcher->isCanceled())
|
|
{
|
|
break;
|
|
}
|
|
|
|
const QString filePath = it.next();
|
|
|
|
QFile file(filePath);
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
QTextStream stream(&file);
|
|
stream.setEncoding(QStringConverter::Utf8);
|
|
|
|
int lineNumber = 0;
|
|
while (!stream.atEnd())
|
|
{
|
|
++lineNumber;
|
|
const QString line = stream.readLine();
|
|
|
|
if (re.match(line).hasMatch())
|
|
{
|
|
results.append({ filePath, lineNumber, line });
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|