#include "FileSearchPanel.h" #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Konstruktor // --------------------------------------------------------------------------- FileSearchPanel::FileSearchPanel(QWidget *parent) : QWidget(parent) { setupUi(); hide(); m_watcher = new QFutureWatcher>(this); connect(m_watcher, &QFutureWatcher>::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> 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 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::searchInFiles( const QString &root, const QString &needle, bool caseSensitive, bool wholeWord, bool useRegex, const QStringList &extensions) const { QList 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; }