diff --git a/CMakeLists.txt b/CMakeLists.txt index a0ef7c2..342a8a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) project(BareCode - VERSION 1.2.0 + VERSION 1.3.0 DESCRIPTION "A modular code editor built with Qt6" LANGUAGES CXX ) diff --git a/README.md b/README.md index 94dacb8..a9b358b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # BareCode **Schlanker, modularer Code-Editor für Web-Entwickler** -Version 1.2.0 — von **Dany Thinnes** | [Projekt Hirnfrei](https://www.projekt-hirnfrei.de) | [Discord](https://discord.projekt-hirnfrei.de) +Version 1.3.0 — von **Dany Thinnes** | [Projekt Hirnfrei](https://www.projekt-hirnfrei.de) | [Discord](https://discord.projekt-hirnfrei.de) Entwickelt mit **C++17**, **Qt6** und **CMake**. Kompiliert auf **Linux**, **Windows** und **Haiku**. diff --git a/main.cpp b/main.cpp index f80fe2e..3089297 100644 --- a/main.cpp +++ b/main.cpp @@ -13,7 +13,7 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); app.setApplicationName("BareCode"); - app.setApplicationVersion("1.2.0"); + app.setApplicationVersion("1.3.0"); app.setOrganizationName("BareCode"); // Icon @@ -89,5 +89,18 @@ int main(int argc, char *argv[]) MainWindow window; window.show(); + // Datei(en) öffnen, die beim Start per Kommandozeile übergeben wurden + // (z. B. Doppelklick auf eine Datei im Dateibrowser → "Öffnen mit BareCode"). + // Das erste Argument ist der Programmname selbst und wird übersprungen. + QStringList cliFiles = app.arguments(); + if (!cliFiles.isEmpty()) + { + cliFiles.removeFirst(); + } + if (!cliFiles.isEmpty()) + { + window.openFilesFromArguments(cliFiles); + } + return app.exec(); } diff --git a/src/core/AboutDialog.cpp b/src/core/AboutDialog.cpp index d6621d7..40a13e7 100644 --- a/src/core/AboutDialog.cpp +++ b/src/core/AboutDialog.cpp @@ -74,7 +74,7 @@ AboutDialog::AboutDialog(QWidget *parent) info->addLayout(row); }; - makeRow(tr("Version"), "1.2.0"); + makeRow(tr("Version"), "1.3.0"); makeRow(tr("Entwickler"), "Dany Thinnes"); makeRow(tr("Projekt"), "Projekt Hirnfrei"); makeRow(tr("Framework"), QString("Qt %1").arg(qVersion())); diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp index 254afca..38776bd 100644 --- a/src/core/MainWindow.cpp +++ b/src/core/MainWindow.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "AboutDialog.h" #include "filetree/FileTreePanel.h" @@ -45,6 +46,21 @@ MainWindow::MainWindow(QWidget *parent) MainWindow::~MainWindow() = default; +// --------------------------------------------------------------------------- +// Datei(en) von der Kommandozeile öffnen +// --------------------------------------------------------------------------- +void MainWindow::openFilesFromArguments(const QStringList &filePaths) +{ + for (const QString &path : filePaths) + { + const QFileInfo info(path); + if (info.exists() && info.isFile()) + { + m_editor->openFile(info.absoluteFilePath()); + } + } +} + // --------------------------------------------------------------------------- // UI aufbauen // --------------------------------------------------------------------------- diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h index a137195..beced93 100644 --- a/src/core/MainWindow.h +++ b/src/core/MainWindow.h @@ -26,6 +26,11 @@ public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; + // Datei(en) öffnen, die beim Programmstart per Kommandozeile übergeben + // wurden — z. B. durch Doppelklick auf eine Datei im Dateibrowser + // ("Öffnen mit BareCode"). + void openFilesFromArguments(const QStringList &filePaths); + protected: void closeEvent(QCloseEvent *event) override; diff --git a/src/editor/CodeEditor.cpp b/src/editor/CodeEditor.cpp index 70a06d4..948faa8 100644 --- a/src/editor/CodeEditor.cpp +++ b/src/editor/CodeEditor.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -747,3 +748,15 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) m_varCompleter->handleKeyPress(event); } +// --------------------------------------------------------------------------- +// Fokus verloren — z. B. beim Wechsel zu einem anderen Tab. +// Ein noch offenes Variablen-Popup muss hier geschlossen werden, sonst +// bleibt es als verwaistes Fenster stehen und kann bei mehreren offenen +// Dateien den Fokus blockieren. +// --------------------------------------------------------------------------- +void CodeEditor::focusOutEvent(QFocusEvent *event) +{ + QPlainTextEdit::focusOutEvent(event); + m_varCompleter->notifyFocusLost(); +} + diff --git a/src/editor/CodeEditor.h b/src/editor/CodeEditor.h index 9e24905..af87818 100644 --- a/src/editor/CodeEditor.h +++ b/src/editor/CodeEditor.h @@ -56,11 +56,13 @@ signals: void fileSaved(const QString &filePath); void navigateToRequested(const QString &filePath, int line); -protected: void resizeEvent(QResizeEvent *event) override; +protected: + void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; + void focusOutEvent(QFocusEvent *event) override; private slots: void updateLineNumberAreaWidth(int newBlockCount); diff --git a/src/editor/FunctionListPanel.cpp b/src/editor/FunctionListPanel.cpp index c6441b3..fac5dd5 100644 --- a/src/editor/FunctionListPanel.cpp +++ b/src/editor/FunctionListPanel.cpp @@ -4,6 +4,9 @@ #include #include #include +#include +#include +#include FunctionListPanel::FunctionListPanel(QWidget *parent) : QWidget(parent) @@ -67,6 +70,78 @@ void FunctionListPanel::setupUi() root->addLayout(toolRow); + // ---- Exclude-Verzeichnisse ---- + QHBoxLayout *excludeRow = new QHBoxLayout(); + excludeRow->setContentsMargins(6, 0, 6, 4); + excludeRow->addWidget(new QLabel(tr("Ausschließen:"), this)); + + m_excludeEdit = new QLineEdit(this); + m_excludeEdit->setPlaceholderText(tr("vendor lib node_modules (leerzeichen-getrennt)")); + m_excludeEdit->setToolTip(tr( + "Verzeichnisnamen die beim Scan übersprungen werden.\n" + "Leerzeichen-getrennt, z.B.: vendor lib node_modules cache" + )); + m_excludeEdit->setText(tr("vendor lib node_modules")); + excludeRow->addWidget(m_excludeEdit, 1); + + // Verzeichnis-Auswahl Button + QPushButton *btnBrowse = new QPushButton(tr("+ Verzeichnis"), this); + btnBrowse->setToolTip(tr("Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen")); + connect(btnBrowse, &QPushButton::clicked, this, [this]() + { + const QString startPath = m_projectRoot.isEmpty() + ? QDir::homePath() + : m_projectRoot; + + const QString dir = QFileDialog::getExistingDirectory( + this, + tr("Verzeichnis ausschließen"), + startPath, + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks + ); + + if (dir.isEmpty()) + { + return; + } + + // Nur den letzten Verzeichnisnamen verwenden, nicht den vollen Pfad + // So funktioniert der Exclude auch für gleichnamige Unterverzeichnisse + const QString dirName = QFileInfo(dir).fileName(); + + // Prüfen ob bereits in der Liste + const QStringList current = m_excludeEdit->text() + .simplified() + .split(' ', Qt::SkipEmptyParts); + if (current.contains(dirName, Qt::CaseInsensitive)) + { + return; + } + + // Anhängen + QString newText = m_excludeEdit->text().trimmed(); + if (!newText.isEmpty()) + { + newText += ' '; + } + newText += dirName; + m_excludeEdit->setText(newText); + }); + excludeRow->addWidget(btnBrowse); + + // Löschen-Button — ganzes Feld leeren + QPushButton *btnClear = new QPushButton(tr("✕"), this); + btnClear->setFixedWidth(28); + btnClear->setFlat(true); + btnClear->setToolTip(tr("Ausschlussliste leeren")); + connect(btnClear, &QPushButton::clicked, this, [this]() + { + m_excludeEdit->clear(); + }); + excludeRow->addWidget(btnClear); + + root->addLayout(excludeRow); + // ---- Status ---- m_statusLabel = new QLabel(this); m_statusLabel->setContentsMargins(6, 0, 6, 2); @@ -107,9 +182,14 @@ void FunctionListPanel::setupUi() // --------------------------------------------------------------------------- void FunctionListPanel::setProjectRoot(const QString &path) { + // Alte Exclude-Liste für das vorherige Projekt speichern + saveExcludeDirsForProject(); + m_projectRoot = path; + if (!path.isEmpty()) { + loadExcludeDirsForProject(); refresh(); } else @@ -147,16 +227,21 @@ void FunctionListPanel::refresh() m_watcher->waitForFinished(); } + saveExcludeDirsForProject(); + m_statusLabel->setText(tr("Scanne…")); m_btnRefresh->setEnabled(false); - const QString root = m_projectRoot; + const QString root = m_projectRoot; + const QStringList excludeDirs = m_excludeEdit->text() + .simplified() + .split(' ', Qt::SkipEmptyParts); FunctionScanner *scanner = m_scanner; QFuture> future = - QtConcurrent::run([scanner, root]() + QtConcurrent::run([scanner, root, excludeDirs]() { - return scanner->scanDirectory(root); + return scanner->scanDirectory(root, {"*.php"}, excludeDirs); }); m_watcher->setFuture(future); @@ -330,3 +415,35 @@ void FunctionListPanel::onGroupingChanged(int /*index*/) populateTree(m_lastResult); } } + +// --------------------------------------------------------------------------- +// Ausschlussliste projektspezifisch speichern/laden +// (gleiches Schema wie bei der Suche nach toten Funktionen) +// --------------------------------------------------------------------------- +void FunctionListPanel::saveExcludeDirsForProject() +{ + if (m_projectRoot.isEmpty()) + { + return; + } + + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + const QString key = "funclist/exclude/" + + QString(m_projectRoot).replace('/', '_').replace('\\', '_'); + s.setValue(key, m_excludeEdit->text()); + s.setValue("funclist/excludeDirs", m_excludeEdit->text()); // globaler Fallback +} + +void FunctionListPanel::loadExcludeDirsForProject() +{ + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + const QString key = "funclist/exclude/" + + QString(m_projectRoot).replace('/', '_').replace('\\', '_'); + + // Projektspezifisch vorhanden? Sonst globalen Fallback nehmen + const QString saved = s.value(key, + s.value("funclist/excludeDirs", "vendor lib node_modules").toString() + ).toString(); + + m_excludeEdit->setText(saved); +} diff --git a/src/editor/FunctionListPanel.h b/src/editor/FunctionListPanel.h index fd283be..bf8840f 100644 --- a/src/editor/FunctionListPanel.h +++ b/src/editor/FunctionListPanel.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "FunctionScanner.h" @@ -21,6 +22,10 @@ // Gruppierung: nach Datei oder nach Klasse // Klick: öffnet Datei und springt zur Definition // Aktualisierung: automatisch nach jedem Speichern +// +// Verzeichnisse wie vendor/ oder node_modules/ lassen sich über eine +// Ausschlussliste vom Scan ausnehmen (gleiches Prinzip wie bei der +// Suche nach toten Funktionen). // --------------------------------------------------------------------------- class FunctionListPanel : public QWidget { @@ -48,6 +53,8 @@ private: void setupUi(); void populateTree(const QList &functions); void applyFilter(const QString &text); + void loadExcludeDirsForProject(); + void saveExcludeDirsForProject(); static void formatFunctionItem(QTreeWidgetItem *item, const FunctionScanner::FunctionInfo &f); @@ -59,6 +66,9 @@ private: QLabel *m_statusLabel = nullptr; QTreeWidget *m_tree = nullptr; + // Ausschlussliste — Verzeichnisnamen die beim Scan übersprungen werden + QLineEdit *m_excludeEdit = nullptr; + FunctionScanner *m_scanner = nullptr; QFutureWatcher> *m_watcher = nullptr; diff --git a/src/editor/FunctionScanner.cpp b/src/editor/FunctionScanner.cpp index 10a8a02..20f8478 100644 --- a/src/editor/FunctionScanner.cpp +++ b/src/editor/FunctionScanner.cpp @@ -16,7 +16,8 @@ FunctionScanner::FunctionScanner(QObject *parent) // --------------------------------------------------------------------------- QList FunctionScanner::scanDirectory( const QString &rootPath, - const QStringList &extensions) const + const QStringList &extensions, + const QStringList &excludeDirs) const { QList results; @@ -28,6 +29,27 @@ QList FunctionScanner::scanDirectory( while (it.hasNext()) { const QString path = it.next(); + + // Exclude-Verzeichnisse prüfen (gleiche Logik wie DeadCodeAnalyzer) + bool excluded = false; + if (!excludeDirs.isEmpty()) + { + const QStringList parts = path.split('/'); + for (const QString &excl : excludeDirs) + { + if (parts.contains(excl.trimmed(), Qt::CaseInsensitive)) + { + excluded = true; + break; + } + } + } + + if (excluded) + { + continue; + } + results.append(scanFile(path)); } diff --git a/src/editor/FunctionScanner.h b/src/editor/FunctionScanner.h index cd08243..1bb51a4 100644 --- a/src/editor/FunctionScanner.h +++ b/src/editor/FunctionScanner.h @@ -35,8 +35,11 @@ public: explicit FunctionScanner(QObject *parent = nullptr); // Synchroner Scan — für direkten Aufruf aus Threads + // excludeDirs: Verzeichnisnamen die komplett übersprungen werden + // z.B. {"vendor", "lib", "node_modules"} QList scanDirectory(const QString &rootPath, - const QStringList &extensions = {"*.php"}) const; + const QStringList &extensions = {"*.php"}, + const QStringList &excludeDirs = {}) const; // Scannt eine einzelne Datei QList scanFile(const QString &filePath) const; diff --git a/src/editor/VariableCompleter.cpp b/src/editor/VariableCompleter.cpp index 021ad58..64d57af 100644 --- a/src/editor/VariableCompleter.cpp +++ b/src/editor/VariableCompleter.cpp @@ -14,10 +14,15 @@ VariableCompleter::VariableCompleter(CodeEditor *editor) : QObject(editor) , m_editor(editor) { - // Popup als eigenständiges, rahmenloses Werkzeugfenster - m_popup = new QListWidget(nullptr); - m_popup->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | - Qt::WindowStaysOnTopHint); + // Popup als reines Anzeige-Fenster — dieselbe Fensterart wie + // SignatureTooltip, die nachweislich nie den Fokus an sich reißt. + // Wichtig: Ein echtes Elternfenster (editor->window()) angeben, sonst + // behandelt der Fenstermanager das Popup als eigenständiges Fenster. + // Das führte dazu, dass bei mehreren offenen Dateien/Tabs verwaiste + // Popups entstehen konnten, die den Fokus übernehmen und die + // Texteingabe blockieren. + m_popup = new QListWidget(editor->window()); + m_popup->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint); m_popup->setAttribute(Qt::WA_ShowWithoutActivating); m_popup->setFocusPolicy(Qt::NoFocus); m_popup->setMaximumHeight(200); @@ -155,6 +160,14 @@ void VariableCompleter::hidePopup() m_popup->clear(); } +// --------------------------------------------------------------------------- +// Editor hat den Fokus verloren (z. B. Tab-Wechsel) — Popup schließen +// --------------------------------------------------------------------------- +void VariableCompleter::notifyFocusLost() +{ + hidePopup(); +} + // --------------------------------------------------------------------------- // Variablen im aktuellen Dokument sammeln // --------------------------------------------------------------------------- diff --git a/src/editor/VariableCompleter.h b/src/editor/VariableCompleter.h index 2c67bd9..08743e9 100644 --- a/src/editor/VariableCompleter.h +++ b/src/editor/VariableCompleter.h @@ -24,6 +24,11 @@ public: // Muss nach jedem Tastendruck aufgerufen werden void handleKeyPress(QKeyEvent *event); + // Muss aufgerufen werden, wenn der Editor den Fokus verliert + // (z. B. beim Wechsel des Tabs) — schließt ein evtl. offenes Popup, + // damit es nicht als "Geisterfenster" stehen bleibt. + void notifyFocusLost(); + private slots: void onItemActivated(QListWidgetItem *item);