diff --git a/CMakeLists.txt b/CMakeLists.txt index 9321a67..b8bd994 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets + Concurrent ) qt_standard_project_setup() diff --git a/resources/BareCode.ico b/resources/BareCode.ico index 0cae478..10ecf22 100644 Binary files a/resources/BareCode.ico and b/resources/BareCode.ico differ diff --git a/resources/icon_128.png b/resources/icon_128.png index 8af0c1b..bb49b56 100644 Binary files a/resources/icon_128.png and b/resources/icon_128.png differ diff --git a/resources/icon_16.png b/resources/icon_16.png index ef73abe..7cba1e4 100644 Binary files a/resources/icon_16.png and b/resources/icon_16.png differ diff --git a/resources/icon_24.png b/resources/icon_24.png new file mode 100644 index 0000000..67356ff Binary files /dev/null and b/resources/icon_24.png differ diff --git a/resources/icon_256.png b/resources/icon_256.png index c890237..f18f69f 100644 Binary files a/resources/icon_256.png and b/resources/icon_256.png differ diff --git a/resources/icon_32.png b/resources/icon_32.png index 8f82923..5982252 100644 Binary files a/resources/icon_32.png and b/resources/icon_32.png differ diff --git a/resources/icon_48.png b/resources/icon_48.png index 3ddc136..b792afe 100644 Binary files a/resources/icon_48.png and b/resources/icon_48.png differ diff --git a/resources/icon_512.png b/resources/icon_512.png index 5019bd9..5b843e5 100644 Binary files a/resources/icon_512.png and b/resources/icon_512.png differ diff --git a/resources/icon_64.png b/resources/icon_64.png index ca5de95..1025e84 100644 Binary files a/resources/icon_64.png and b/resources/icon_64.png differ diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp index d45ce0c..37d588f 100644 --- a/src/core/MainWindow.cpp +++ b/src/core/MainWindow.cpp @@ -35,6 +35,12 @@ MainWindow::MainWindow(QWidget *parent) { m_projectManager->openProject(lastPath); } + + // Letzte Session wiederherstellen (geöffnete Dateien + aktiver Tab) + m_editor->restoreSession( + m_settings->lastOpenFiles(), + m_settings->lastActiveFile() + ); } MainWindow::~MainWindow() = default; @@ -110,6 +116,9 @@ void MainWindow::setupMenuBar() m_actSearch = mBearbeiten->addAction(tr("&Suchen / Ersetzen…"), this, &MainWindow::onShowSearch); m_actSearch->setShortcut(QKeySequence::Find); + m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch); + m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F); + // ---- Ansicht ---- QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht")); @@ -272,10 +281,16 @@ void MainWindow::onAbout() // --------------------------------------------------------------------------- // Slots – Projekt // --------------------------------------------------------------------------- +void MainWindow::onShowFileSearch() +{ + m_editor->showFileSearchPanel(); +} + void MainWindow::onProjectOpened(const QString &path) { setWindowTitle(QString("BareCode – %1").arg(path)); m_fileTree->setRootPath(path); + m_editor->setSearchRoot(path); statusBar()->showMessage(tr("Projekt geöffnet: %1").arg(path), 4000); } @@ -283,6 +298,7 @@ void MainWindow::onProjectClosed() { setWindowTitle("BareCode"); m_fileTree->clearRoot(); + m_editor->setSearchRoot(QString()); statusBar()->showMessage(tr("Projekt geschlossen"), 3000); } @@ -294,6 +310,10 @@ void MainWindow::saveWindowState() QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); s.setValue("window/geometry", saveGeometry()); s.setValue("window/state", saveState()); + + // Session speichern + m_settings->setLastOpenFiles(m_editor->openFilePaths()); + m_settings->setLastActiveFile(m_editor->activeFilePath()); } void MainWindow::restoreWindowState() diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h index 7c00848..3ae9b17 100644 --- a/src/core/MainWindow.h +++ b/src/core/MainWindow.h @@ -41,6 +41,7 @@ private slots: void onUndo(); void onRedo(); void onShowSearch(); + void onShowFileSearch(); // Ansicht void onToggleDarkMode(bool checked); // Hilfe @@ -80,6 +81,7 @@ private: QAction *m_actUndo = nullptr; QAction *m_actRedo = nullptr; QAction *m_actSearch = nullptr; + QAction *m_actFileSearch = nullptr; QAction *m_actDarkMode = nullptr; QAction *m_actAbout = nullptr; }; diff --git a/src/core/Settings.cpp b/src/core/Settings.cpp index 8e4cc6e..4ebc21f 100644 --- a/src/core/Settings.cpp +++ b/src/core/Settings.cpp @@ -88,3 +88,26 @@ void Settings::setLastProjectPath(const QString &path) { m_settings.setValue("project/lastPath", path); } + +// --------------------------------------------------------------------------- +// Session – geöffnete Dateien +// --------------------------------------------------------------------------- +QStringList Settings::lastOpenFiles() const +{ + return m_settings.value("session/openFiles", QStringList()).toStringList(); +} + +void Settings::setLastOpenFiles(const QStringList &files) +{ + m_settings.setValue("session/openFiles", files); +} + +QString Settings::lastActiveFile() const +{ + return m_settings.value("session/activeFile", QString()).toString(); +} + +void Settings::setLastActiveFile(const QString &file) +{ + m_settings.setValue("session/activeFile", file); +} diff --git a/src/core/Settings.h b/src/core/Settings.h index 502afbd..1122864 100644 --- a/src/core/Settings.h +++ b/src/core/Settings.h @@ -29,6 +29,13 @@ public: int fileTreeWidth() const; void setFileTreeWidth(int width); + // Zuletzt geöffnete Dateien (Session-Wiederherstellung) + QStringList lastOpenFiles() const; + void setLastOpenFiles(const QStringList &files); + + QString lastActiveFile() const; + void setLastActiveFile(const QString &file); + // Recent QString lastProjectPath() const; void setLastProjectPath(const QString &path); diff --git a/src/editor/CMakeLists.txt b/src/editor/CMakeLists.txt index 7893137..2d5e180 100644 --- a/src/editor/CMakeLists.txt +++ b/src/editor/CMakeLists.txt @@ -9,6 +9,8 @@ set(EDITOR_SOURCES EditorTab.h SearchPanel.cpp SearchPanel.h + FileSearchPanel.cpp + FileSearchPanel.h ) add_library(BareCode_Editor STATIC ${EDITOR_SOURCES}) @@ -17,6 +19,7 @@ target_link_libraries(BareCode_Editor PUBLIC Qt6::Core Qt6::Gui Qt6::Widgets + Qt6::Concurrent BareCode_Highlighter ) diff --git a/src/editor/EditorPanel.cpp b/src/editor/EditorPanel.cpp index ec06d60..e2c424c 100644 --- a/src/editor/EditorPanel.cpp +++ b/src/editor/EditorPanel.cpp @@ -2,9 +2,12 @@ #include "EditorTab.h" #include "CodeEditor.h" #include "SearchPanel.h" +#include "FileSearchPanel.h" #include +#include #include +#include EditorPanel::EditorPanel(Settings *settings, QWidget *parent) : QWidget(parent) @@ -28,15 +31,20 @@ void EditorPanel::setupUi() m_tabWidget->setDocumentMode(true); m_searchPanel = new SearchPanel(this); + m_fileSearch = new FileSearchPanel(this); m_layout->addWidget(m_tabWidget, 1); m_layout->addWidget(m_searchPanel, 0); + m_layout->addWidget(m_fileSearch, 0); connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &EditorPanel::onTabCloseRequested); connect(m_tabWidget, &QTabWidget::currentChanged, this, &EditorPanel::onCurrentTabChanged); + + connect(m_fileSearch, &FileSearchPanel::fileLineRequested, + this, &EditorPanel::goToLine); } // --------------------------------------------------------------------------- @@ -127,9 +135,88 @@ void EditorPanel::saveAllFiles() void EditorPanel::showSearchPanel() { + m_fileSearch->hide(); m_searchPanel->activate(); } +void EditorPanel::showFileSearchPanel() +{ + m_searchPanel->hide(); + m_fileSearch->activate(); +} + +void EditorPanel::setSearchRoot(const QString &path) +{ + m_fileSearch->setSearchRoot(path); +} + +void EditorPanel::goToLine(const QString &filePath, int line) +{ + // Datei öffnen falls noch nicht geöffnet + openFile(filePath); + + EditorTab *tab = m_openTabs.value(filePath, nullptr); + if (!tab) + { + return; + } + + m_tabWidget->setCurrentWidget(tab); + + // Zur gewünschten Zeile springen + CodeEditor *editor = tab->editor(); + QTextBlock block = editor->document()->findBlockByLineNumber(line - 1); + if (block.isValid()) + { + QTextCursor cursor(block); + cursor.movePosition(QTextCursor::StartOfBlock); + editor->setTextCursor(cursor); + editor->centerCursor(); + editor->setFocus(); + } +} + +QStringList EditorPanel::openFilePaths() const +{ + QStringList paths; + for (int i = 0; i < m_tabWidget->count(); ++i) + { + EditorTab *tab = qobject_cast(m_tabWidget->widget(i)); + if (tab) + { + paths.append(tab->filePath()); + } + } + return paths; +} + +QString EditorPanel::activeFilePath() const +{ + EditorTab *tab = currentTab(); + return tab ? tab->filePath() : QString(); +} + +void EditorPanel::restoreSession(const QStringList &files, const QString &activeFile) +{ + for (const QString &path : files) + { + if (QFile::exists(path)) + { + openFile(path); + } + } + + // Aktiven Tab wiederherstellen + if (!activeFile.isEmpty()) + { + const int idx = findTabForFile(activeFile); + if (idx != -1) + { + m_tabWidget->setCurrentIndex(idx); + } + } +} + void EditorPanel::undo() { if (EditorTab *tab = currentTab()) diff --git a/src/editor/EditorPanel.h b/src/editor/EditorPanel.h index 2fce31e..5c789ac 100644 --- a/src/editor/EditorPanel.h +++ b/src/editor/EditorPanel.h @@ -9,6 +9,7 @@ class EditorTab; class Settings; class SearchPanel; +class FileSearchPanel; // --------------------------------------------------------------------------- // EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel. @@ -25,7 +26,15 @@ public slots: void saveCurrentFile(); void saveCurrentFileAs(); void saveAllFiles(); + void setSearchRoot(const QString &path); void showSearchPanel(); + void showFileSearchPanel(); + void goToLine(const QString &filePath, int line); + + // Session + QStringList openFilePaths() const; + QString activeFilePath() const; + void restoreSession(const QStringList &files, const QString &activeFile); void undo(); void redo(); @@ -41,10 +50,11 @@ private: int findTabForFile(const QString &filePath) const; EditorTab *currentTab() const; - Settings *m_settings = nullptr; - QVBoxLayout *m_layout = nullptr; - QTabWidget *m_tabWidget = nullptr; - SearchPanel *m_searchPanel = nullptr; + Settings *m_settings = nullptr; + QVBoxLayout *m_layout = nullptr; + QTabWidget *m_tabWidget = nullptr; + SearchPanel *m_searchPanel = nullptr; + FileSearchPanel *m_fileSearch = nullptr; QHash m_openTabs; }; diff --git a/src/editor/FileSearchPanel.cpp b/src/editor/FileSearchPanel.cpp new file mode 100644 index 0000000..f7d0030 --- /dev/null +++ b/src/editor/FileSearchPanel.cpp @@ -0,0 +1,330 @@ +#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; +} diff --git a/src/editor/FileSearchPanel.h b/src/editor/FileSearchPanel.h new file mode 100644 index 0000000..97b9766 --- /dev/null +++ b/src/editor/FileSearchPanel.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// FileSearchPanel – Suche in allen Dateien eines Verzeichnisses. +// +// Ergebnisse werden als aufklappbare Liste angezeigt: +// Dateiname (N Treffer) +// └ Zeile 12: +// └ Zeile 34: +// +// Klick auf einen Treffer öffnet die Datei im Editor und springt zur Zeile. +// --------------------------------------------------------------------------- +class FileSearchPanel : public QWidget +{ + Q_OBJECT + +public: + explicit FileSearchPanel(QWidget *parent = nullptr); + + void setSearchRoot(const QString &path); + +public slots: + void activate(); + +signals: + void fileLineRequested(const QString &filePath, int line); + +private slots: + void onSearch(); + void onResultActivated(QTreeWidgetItem *item, int column); + void onSearchFinished(); + +private: + struct Match + { + QString filePath; + int line; + QString content; + }; + + void setupUi(); + QList searchInFiles(const QString &root, + const QString &needle, + bool caseSensitive, + bool wholeWord, + bool useRegex, + const QStringList &extensions) const; + + QString m_searchRoot; + + QLineEdit *m_searchEdit = nullptr; + QCheckBox *m_chkCase = nullptr; + QCheckBox *m_chkWord = nullptr; + QCheckBox *m_chkRegex = nullptr; + QLineEdit *m_filterEdit = nullptr; // Dateiendungen-Filter + QPushButton *m_btnSearch = nullptr; + QPushButton *m_btnClose = nullptr; + QLabel *m_statusLabel = nullptr; + QTreeWidget *m_results = nullptr; + QProgressBar *m_progress = nullptr; + + QFutureWatcher> *m_watcher = nullptr; +};