commit 36e074f43d5b76d940d953956b0ed80899e71848 Author: Dany Thinnes Date: Sat May 23 13:14:30 2026 +0200 Erste Version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20292e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +{src/ +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..89f3cda --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.16) + +project(BareCode + VERSION 1.0.0 + DESCRIPTION "A modular code editor built with Qt6" + LANGUAGES CXX +) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +# Platform-specific settings +if(WIN32) + set(CMAKE_WIN32_EXECUTABLE ON) + add_compile_definitions(PLATFORM_WINDOWS) +elseif(UNIX AND NOT APPLE) + add_compile_definitions(PLATFORM_LINUX) +elseif(APPLE) + add_compile_definitions(PLATFORM_MACOS) +endif() + +# Find Qt6 +find_package(Qt6 REQUIRED COMPONENTS + Core + Gui + Widgets +) + +qt_standard_project_setup() + +# Include cmake modules +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +# Collect sources +add_subdirectory(src) + +# Resources +qt_add_resources(BARECODE_RESOURCES resources/resources.qrc) + +# Main executable +qt_add_executable(BareCode + main.cpp + ${BARECODE_RESOURCES} +) + +target_link_libraries(BareCode PRIVATE + BareCode_Core + BareCode_Editor + BareCode_FileTree + BareCode_Highlighter + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +target_include_directories(BareCode PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +# Install rules +include(GNUInstallDirs) +install(TARGETS BareCode + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f657ddf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Dany Thinnes – Projekt Hirnfrei + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..74a112f --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# BareCode + +A modular code editor built with **C++17**, **Qt6**, and **CMake**. +Designed to compile cleanly on **Linux** and **Windows**. + +--- + +## Features (v1.0) + +| Feature | Details | +|---|---| +| Project tree | Left panel — shows only the selected project directory | +| Tabbed editor | Open multiple files simultaneously; tabs are movable and closable | +| Syntax highlighting | C / C++ (extensible via `HighlighterFactory`) | +| Line numbers | Painted in a dedicated gutter | +| Auto-indent | Preserves indentation level on Enter | +| Tab → spaces | Configurable; jumps to next tab stop | +| Persistent settings | Window geometry, font, tab size, last project (INI file) | + +--- + +## Project Structure + +``` +BareCode/ +├── CMakeLists.txt # Top-level build +├── main.cpp +├── resources/ +│ └── resources.qrc +└── src/ + ├── core/ # MainWindow, ProjectManager, Settings, IPlugin + ├── editor/ # EditorPanel, EditorTab, CodeEditor, LineNumberArea + ├── filetree/ # FileTreePanel + └── highlighter/ # SyntaxHighlighter, CppHighlighter, HighlighterFactory +``` + +Each subdirectory compiles into its own **static library**, keeping modules independent. + +--- + +## Building + +### Prerequisites + +- CMake ≥ 3.16 +- Qt6 (Widgets module) +- A C++17-capable compiler (GCC, Clang, MSVC) + +### Linux + +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j$(nproc) +./build/BareCode +``` + +### Windows (Visual Studio) + +```bat +cmake -B build -G "Visual Studio 17 2022" -A x64 +cmake --build build --config Release +build\Release\BareCode.exe +``` + +### Windows (MinGW) + +```bat +cmake -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release +cmake --build build +build\BareCode.exe +``` + +--- + +## Extending BareCode + +### Adding a new syntax highlighter + +1. Create a subclass of `SyntaxHighlighter` in `src/highlighter/`. +2. Populate `m_rules` in the constructor (see `CppHighlighter` as a template). +3. Register the file extension(s) in `HighlighterFactory.cpp`. + +No other files need to change. + +### Adding a new panel / plugin + +1. Implement the `IPlugin` interface from `src/core/IPlugin.h`. +2. Create a `QWidget`-derived class for the UI. +3. Instantiate and wire it up in `MainWindow`. + +--- + +## Code Style + +All code uses **Allman brace style** and C++17 throughout. diff --git a/barecode.desktop b/barecode.desktop new file mode 100644 index 0000000..e6b348c --- /dev/null +++ b/barecode.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Name=BareCode +GenericName=Code-Editor +Comment=Modularer Code-Editor von Projekt Hirnfrei +Exec=BareCode %F +Icon=barecode +Terminal=false +Categories=Development;TextEditor;IDE; +MimeType=text/plain;text/x-csrc;text/x-chdr;text/x-c++src;text/x-c++hdr; +Keywords=editor;code;programmierung;entwicklung; +StartupNotify=true +StartupWMClass=BareCode diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..c39869b --- /dev/null +++ b/main.cpp @@ -0,0 +1,16 @@ +#include +#include "core/MainWindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + app.setApplicationName("BareCode"); + app.setApplicationVersion("1.0.0"); + app.setOrganizationName("BareCode"); + + MainWindow window; + window.show(); + + return app.exec(); +} diff --git a/resources/resources.qrc b/resources/resources.qrc new file mode 100644 index 0000000..9f62ef3 --- /dev/null +++ b/resources/resources.qrc @@ -0,0 +1,5 @@ + + + + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..bb9abf8 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,4 @@ +add_subdirectory(core) +add_subdirectory(editor) +add_subdirectory(filetree) +add_subdirectory(highlighter) diff --git a/src/core/AboutDialog.cpp b/src/core/AboutDialog.cpp new file mode 100644 index 0000000..bc0ebd9 --- /dev/null +++ b/src/core/AboutDialog.cpp @@ -0,0 +1,105 @@ +#include "AboutDialog.h" + +#include +#include +#include +#include +#include +#include +#include + +AboutDialog::AboutDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(tr("Über BareCode")); + setFixedSize(440, 310); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + QVBoxLayout *root = new QVBoxLayout(this); + root->setContentsMargins(0, 0, 0, 0); + root->setSpacing(0); + + // ----------------------------------------------------------------------- + // Header-Banner + // ----------------------------------------------------------------------- + QFrame *banner = new QFrame(this); + banner->setFixedHeight(88); + banner->setStyleSheet( + "background: qlineargradient(x1:0, y1:0, x2:1, y2:0," + " stop:0 #1a1a2e, stop:1 #16213e);" + ); + + QVBoxLayout *bannerLayout = new QVBoxLayout(banner); + bannerLayout->setContentsMargins(24, 10, 24, 10); + bannerLayout->setSpacing(2); + + QLabel *appName = new QLabel("BareCode", banner); + QFont nameFont = appName->font(); + nameFont.setPointSize(22); + nameFont.setBold(true); + appName->setFont(nameFont); + appName->setStyleSheet("color: #e0e0ff; background: transparent;"); + + QLabel *tagline = new QLabel(tr("Modularer Code-Editor"), banner); + tagline->setStyleSheet("color: #8888bb; background: transparent;"); + + bannerLayout->addWidget(appName); + bannerLayout->addWidget(tagline); + root->addWidget(banner); + + // ----------------------------------------------------------------------- + // Info-Tabelle + // ----------------------------------------------------------------------- + QVBoxLayout *info = new QVBoxLayout(); + info->setContentsMargins(28, 20, 28, 8); + info->setSpacing(10); + + auto makeRow = [&](const QString &label, const QString &value) + { + QHBoxLayout *row = new QHBoxLayout(); + row->setSpacing(12); + + QLabel *lbl = new QLabel(label, this); + QFont boldFont = lbl->font(); + boldFont.setBold(true); + lbl->setFont(boldFont); + lbl->setFixedWidth(100); + lbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + + QLabel *val = new QLabel(value, this); + val->setTextInteractionFlags(Qt::TextSelectableByMouse); + + row->addWidget(lbl); + row->addWidget(val, 1); + info->addLayout(row); + }; + + makeRow(tr("Version"), "1.0.0"); + makeRow(tr("Entwickler"), "Dany Thinnes"); + makeRow(tr("Projekt"), "Projekt Hirnfrei"); + makeRow(tr("Framework"), QString("Qt %1").arg(qVersion())); + makeRow(tr("Sprache"), "C++17"); + + root->addLayout(info); + root->addStretch(); + + // ----------------------------------------------------------------------- + // Trennlinie + Schließen-Button + // ----------------------------------------------------------------------- + QFrame *line = new QFrame(this); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + root->addWidget(line); + + QHBoxLayout *btnRow = new QHBoxLayout(); + btnRow->setContentsMargins(12, 8, 12, 12); + btnRow->addStretch(); + + QPushButton *btnClose = new QPushButton(tr("Schließen"), this); + btnClose->setDefault(true); + btnClose->setFixedWidth(110); + connect(btnClose, &QPushButton::clicked, this, &QDialog::accept); + btnRow->addWidget(btnClose); + + root->addLayout(btnRow); +} diff --git a/src/core/AboutDialog.h b/src/core/AboutDialog.h new file mode 100644 index 0000000..d90939a --- /dev/null +++ b/src/core/AboutDialog.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +// --------------------------------------------------------------------------- +// AboutDialog – Zeigt Versionsinformationen und Entwicklerangaben. +// --------------------------------------------------------------------------- +class AboutDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AboutDialog(QWidget *parent = nullptr); +}; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..e3b822e --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,28 @@ +set(CORE_SOURCES + MainWindow.cpp + MainWindow.h + IPlugin.h + ProjectManager.cpp + ProjectManager.h + Settings.cpp + Settings.h + ThemeManager.cpp + ThemeManager.h + AboutDialog.cpp + AboutDialog.h +) + +add_library(BareCode_Core STATIC ${CORE_SOURCES}) + +target_link_libraries(BareCode_Core PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets + BareCode_Editor + BareCode_FileTree +) + +target_include_directories(BareCode_Core PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/src/core/IPlugin.h b/src/core/IPlugin.h new file mode 100644 index 0000000..c7c3571 --- /dev/null +++ b/src/core/IPlugin.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +// --------------------------------------------------------------------------- +// IPlugin – Interface that every BareCode module / plugin must implement. +// This allows components to be swapped or extended without touching the core. +// --------------------------------------------------------------------------- +class IPlugin +{ +public: + virtual ~IPlugin() = default; + + // Human-readable name of the plugin + virtual QString pluginName() const = 0; + + // Version string, e.g. "1.0.0" + virtual QString pluginVersion() const = 0; + + // Called once after all plugins are loaded so plugins can cross-reference + virtual void initialize() {} + + // Called before the application shuts down + virtual void shutdown() {} +}; diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp new file mode 100644 index 0000000..d45ce0c --- /dev/null +++ b/src/core/MainWindow.cpp @@ -0,0 +1,316 @@ +#include "MainWindow.h" + +#include +#include +#include +#include +#include + +#include "AboutDialog.h" +#include "filetree/FileTreePanel.h" +#include "editor/EditorPanel.h" + +// --------------------------------------------------------------------------- +// Konstruktor +// --------------------------------------------------------------------------- +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , m_projectManager(std::make_unique()) + , m_settings(std::make_unique()) + , m_themeManager(std::make_unique()) +{ + setWindowTitle("BareCode"); + setMinimumSize(900, 600); + + setupUi(); + setupMenuBar(); + setupStatusBar(); + connectSignals(); + restoreWindowState(); + applyInitialTheme(); + + // Letztes Projekt wieder öffnen + const QString lastPath = m_settings->lastProjectPath(); + if (!lastPath.isEmpty()) + { + m_projectManager->openProject(lastPath); + } +} + +MainWindow::~MainWindow() = default; + +// --------------------------------------------------------------------------- +// UI aufbauen +// --------------------------------------------------------------------------- +void MainWindow::setupUi() +{ + m_splitter = new QSplitter(Qt::Horizontal, this); + setCentralWidget(m_splitter); + + m_fileTree = new FileTreePanel(m_splitter); + m_editor = new EditorPanel(m_settings.get(), m_splitter); + + m_splitter->addWidget(m_fileTree); + m_splitter->addWidget(m_editor); + + const int treeWidth = m_settings->fileTreeWidth(); + m_splitter->setSizes({treeWidth, width() - treeWidth}); + m_splitter->setStretchFactor(0, 0); + m_splitter->setStretchFactor(1, 1); +} + +// --------------------------------------------------------------------------- +// Menüleiste +// --------------------------------------------------------------------------- +void MainWindow::setupMenuBar() +{ + // ---- Datei ---- + QMenu *mDatei = menuBar()->addMenu(tr("&Datei")); + + m_actNewFile = mDatei->addAction(tr("&Neue Datei…"), this, &MainWindow::onNewFile); + m_actNewFile->setShortcut(QKeySequence::New); + + mDatei->addSeparator(); + + m_actOpenFile = mDatei->addAction(tr("Datei &öffnen…"), this, &MainWindow::onOpenFile); + m_actOpenFile->setShortcut(QKeySequence::Open); + + m_actOpenProject = mDatei->addAction(tr("&Projekt öffnen…"), this, &MainWindow::onOpenProject); + m_actOpenProject->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_O); + + m_actClose = mDatei->addAction(tr("Projekt &schließen"), this, &MainWindow::onCloseProject); + + mDatei->addSeparator(); + + m_actSave = mDatei->addAction(tr("&Speichern"), this, &MainWindow::onSave); + m_actSave->setShortcut(QKeySequence::Save); + + m_actSaveAs = mDatei->addAction(tr("Speichern &unter…"), this, &MainWindow::onSaveAs); + m_actSaveAs->setShortcut(QKeySequence::SaveAs); + + m_actSaveAll = mDatei->addAction(tr("&Alles speichern"), this, &MainWindow::onSaveAll); + m_actSaveAll->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_S); + + mDatei->addSeparator(); + + m_actQuit = mDatei->addAction(tr("&Beenden"), qApp, &QApplication::quit); + m_actQuit->setShortcut(QKeySequence::Quit); + + // ---- Bearbeiten ---- + QMenu *mBearbeiten = menuBar()->addMenu(tr("&Bearbeiten")); + + m_actUndo = mBearbeiten->addAction(tr("&Rückgängig"), this, &MainWindow::onUndo); + m_actUndo->setShortcut(QKeySequence::Undo); + + m_actRedo = mBearbeiten->addAction(tr("&Wiederholen"), this, &MainWindow::onRedo); + m_actRedo->setShortcut(QKeySequence::Redo); + + mBearbeiten->addSeparator(); + + m_actSearch = mBearbeiten->addAction(tr("&Suchen / Ersetzen…"), this, &MainWindow::onShowSearch); + m_actSearch->setShortcut(QKeySequence::Find); + + // ---- Ansicht ---- + QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht")); + + m_actDarkMode = mAnsicht->addAction(tr("&Dark Mode")); + m_actDarkMode->setCheckable(true); + m_actDarkMode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_D); + connect(m_actDarkMode, &QAction::toggled, this, &MainWindow::onToggleDarkMode); + + // ---- Hilfe ---- + QMenu *mHilfe = menuBar()->addMenu(tr("&Hilfe")); + m_actAbout = mHilfe->addAction(tr("&Über BareCode…"), this, &MainWindow::onAbout); +} + +void MainWindow::setupStatusBar() +{ + statusBar()->showMessage(tr("Bereit")); +} + +// --------------------------------------------------------------------------- +// Signale verbinden +// --------------------------------------------------------------------------- +void MainWindow::connectSignals() +{ + connect(m_projectManager.get(), &ProjectManager::projectOpened, + this, &MainWindow::onProjectOpened); + + connect(m_projectManager.get(), &ProjectManager::projectClosed, + this, &MainWindow::onProjectClosed); + + // Dateibaum → Editor + connect(m_fileTree, &FileTreePanel::fileActivated, + m_editor, &EditorPanel::openFile); + + connect(m_fileTree, &FileTreePanel::fileCreated, + m_editor, &EditorPanel::openFile); + + // Gespeichert → Statusleiste + connect(m_editor, &EditorPanel::currentFileSaved, this, [this](const QString &path) + { + statusBar()->showMessage(tr("Gespeichert: %1").arg(path), 3000); + }); + + // Splitter-Breite merken + connect(m_splitter, &QSplitter::splitterMoved, this, [this](int pos, int) + { + m_settings->setFileTreeWidth(pos); + }); +} + +// --------------------------------------------------------------------------- +// Theme beim Start +// --------------------------------------------------------------------------- +void MainWindow::applyInitialTheme() +{ + const bool dark = m_settings->darkMode(); + // Block damit toggled-Signal nicht doppelt feuert + m_actDarkMode->blockSignals(true); + m_actDarkMode->setChecked(dark); + m_actDarkMode->blockSignals(false); + + m_themeManager->applyTheme(dark ? ThemeManager::Theme::Dark + : ThemeManager::Theme::Light); +} + +// --------------------------------------------------------------------------- +// Slots – Datei +// --------------------------------------------------------------------------- +void MainWindow::onNewFile() +{ + m_fileTree->triggerNewFile(); +} + +void MainWindow::onOpenFile() +{ + const QString path = QFileDialog::getOpenFileName( + this, + tr("Datei öffnen"), + m_settings->lastProjectPath() + ); + + if (!path.isEmpty()) + { + m_editor->openFile(path); + } +} + +void MainWindow::onOpenProject() +{ + const QString path = QFileDialog::getExistingDirectory( + this, + tr("Projektverzeichnis öffnen"), + m_settings->lastProjectPath() + ); + + if (!path.isEmpty()) + { + m_projectManager->openProject(path); + m_settings->setLastProjectPath(path); + } +} + +void MainWindow::onCloseProject() +{ + m_projectManager->closeProject(); +} + +void MainWindow::onSave() +{ + m_editor->saveCurrentFile(); +} + +void MainWindow::onSaveAs() +{ + m_editor->saveCurrentFileAs(); +} + +void MainWindow::onSaveAll() +{ + m_editor->saveAllFiles(); + statusBar()->showMessage(tr("Alle Dateien gespeichert"), 3000); +} + +// --------------------------------------------------------------------------- +// Slots – Bearbeiten +// --------------------------------------------------------------------------- +void MainWindow::onUndo() +{ + m_editor->undo(); +} + +void MainWindow::onRedo() +{ + m_editor->redo(); +} + +void MainWindow::onShowSearch() +{ + m_editor->showSearchPanel(); +} + +// --------------------------------------------------------------------------- +// Slots – Ansicht +// --------------------------------------------------------------------------- +void MainWindow::onToggleDarkMode(bool checked) +{ + m_themeManager->applyTheme(checked ? ThemeManager::Theme::Dark + : ThemeManager::Theme::Light); + m_settings->setDarkMode(checked); +} + +// --------------------------------------------------------------------------- +// Slots – Hilfe +// --------------------------------------------------------------------------- +void MainWindow::onAbout() +{ + AboutDialog dlg(this); + dlg.exec(); +} + +// --------------------------------------------------------------------------- +// Slots – Projekt +// --------------------------------------------------------------------------- +void MainWindow::onProjectOpened(const QString &path) +{ + setWindowTitle(QString("BareCode – %1").arg(path)); + m_fileTree->setRootPath(path); + statusBar()->showMessage(tr("Projekt geöffnet: %1").arg(path), 4000); +} + +void MainWindow::onProjectClosed() +{ + setWindowTitle("BareCode"); + m_fileTree->clearRoot(); + statusBar()->showMessage(tr("Projekt geschlossen"), 3000); +} + +// --------------------------------------------------------------------------- +// Fenster-Zustand +// --------------------------------------------------------------------------- +void MainWindow::saveWindowState() +{ + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + s.setValue("window/geometry", saveGeometry()); + s.setValue("window/state", saveState()); +} + +void MainWindow::restoreWindowState() +{ + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + if (s.contains("window/geometry")) + { + restoreGeometry(s.value("window/geometry").toByteArray()); + } + if (s.contains("window/state")) + { + restoreState(s.value("window/state").toByteArray()); + } +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + saveWindowState(); + event->accept(); +} diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h new file mode 100644 index 0000000..7c00848 --- /dev/null +++ b/src/core/MainWindow.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "ProjectManager.h" +#include "Settings.h" +#include "ThemeManager.h" + +class FileTreePanel; +class EditorPanel; + +// --------------------------------------------------------------------------- +// MainWindow – Hauptfenster. Besitzt alle zentralen Dienste und das Layout. +// --------------------------------------------------------------------------- +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +protected: + void closeEvent(QCloseEvent *event) override; + +private slots: + // Datei + void onNewFile(); + void onOpenFile(); + void onOpenProject(); + void onCloseProject(); + void onSave(); + void onSaveAs(); + void onSaveAll(); + // Bearbeiten + void onUndo(); + void onRedo(); + void onShowSearch(); + // Ansicht + void onToggleDarkMode(bool checked); + // Hilfe + void onAbout(); + // Intern + void onProjectOpened(const QString &path); + void onProjectClosed(); + +private: + void setupUi(); + void setupMenuBar(); + void setupStatusBar(); + void connectSignals(); + void applyInitialTheme(); + void saveWindowState(); + void restoreWindowState(); + + // Dienste + std::unique_ptr m_projectManager; + std::unique_ptr m_settings; + std::unique_ptr m_themeManager; + + // Layout + QSplitter *m_splitter = nullptr; + FileTreePanel *m_fileTree = nullptr; + EditorPanel *m_editor = nullptr; + + // Aktionen + QAction *m_actNewFile = nullptr; + QAction *m_actOpenFile = nullptr; + QAction *m_actOpenProject = nullptr; + QAction *m_actClose = nullptr; + QAction *m_actSave = nullptr; + QAction *m_actSaveAs = nullptr; + QAction *m_actSaveAll = nullptr; + QAction *m_actQuit = nullptr; + QAction *m_actUndo = nullptr; + QAction *m_actRedo = nullptr; + QAction *m_actSearch = nullptr; + QAction *m_actDarkMode = nullptr; + QAction *m_actAbout = nullptr; +}; diff --git a/src/core/ProjectManager.cpp b/src/core/ProjectManager.cpp new file mode 100644 index 0000000..c4ee879 --- /dev/null +++ b/src/core/ProjectManager.cpp @@ -0,0 +1,33 @@ +#include "ProjectManager.h" + +ProjectManager::ProjectManager(QObject *parent) + : QObject(parent) +{ +} + +QString ProjectManager::currentProjectPath() const +{ + return m_projectPath; +} + +void ProjectManager::openProject(const QString &path) +{ + if (m_projectPath == path) + { + return; + } + + m_projectPath = path; + emit projectOpened(m_projectPath); +} + +void ProjectManager::closeProject() +{ + if (m_projectPath.isEmpty()) + { + return; + } + + m_projectPath.clear(); + emit projectClosed(); +} diff --git a/src/core/ProjectManager.h b/src/core/ProjectManager.h new file mode 100644 index 0000000..9aedfcc --- /dev/null +++ b/src/core/ProjectManager.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +// --------------------------------------------------------------------------- +// ProjectManager – Tracks the currently open project directory and emits +// signals when the project changes so other components can react. +// --------------------------------------------------------------------------- +class ProjectManager : public QObject +{ + Q_OBJECT + +public: + explicit ProjectManager(QObject *parent = nullptr); + + QString currentProjectPath() const; + void openProject(const QString &path); + void closeProject(); + +signals: + void projectOpened(const QString &path); + void projectClosed(); + +private: + QString m_projectPath; +}; diff --git a/src/core/Settings.cpp b/src/core/Settings.cpp new file mode 100644 index 0000000..8e4cc6e --- /dev/null +++ b/src/core/Settings.cpp @@ -0,0 +1,90 @@ +#include "Settings.h" + +Settings::Settings(QObject *parent) + : QObject(parent) + , m_settings(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode") +{ +} + +// --------------------------------------------------------------------------- +// Editor font +// --------------------------------------------------------------------------- +QFont Settings::editorFont() const +{ + QFont defaultFont("Monospace", 11); + defaultFont.setStyleHint(QFont::Monospace); + return m_settings.value("editor/font", defaultFont).value(); +} + +void Settings::setEditorFont(const QFont &font) +{ + m_settings.setValue("editor/font", font); + emit settingsChanged(); +} + +// --------------------------------------------------------------------------- +// Tab size +// --------------------------------------------------------------------------- +int Settings::tabSize() const +{ + return m_settings.value("editor/tabSize", 4).toInt(); +} + +void Settings::setTabSize(int size) +{ + m_settings.setValue("editor/tabSize", size); + emit settingsChanged(); +} + +// --------------------------------------------------------------------------- +// Spaces vs. tabs +// --------------------------------------------------------------------------- +bool Settings::useSpacesForTabs() const +{ + return m_settings.value("editor/useSpacesForTabs", true).toBool(); +} + +void Settings::setUseSpacesForTabs(bool use) +{ + m_settings.setValue("editor/useSpacesForTabs", use); + emit settingsChanged(); +} + +// --------------------------------------------------------------------------- +// File tree width +// --------------------------------------------------------------------------- +int Settings::fileTreeWidth() const +{ + return m_settings.value("layout/fileTreeWidth", 240).toInt(); +} + +void Settings::setFileTreeWidth(int width) +{ + m_settings.setValue("layout/fileTreeWidth", width); +} + +// --------------------------------------------------------------------------- +// Dark mode +// --------------------------------------------------------------------------- +bool Settings::darkMode() const +{ + return m_settings.value("appearance/darkMode", false).toBool(); +} + +void Settings::setDarkMode(bool dark) +{ + m_settings.setValue("appearance/darkMode", dark); +} + +// --------------------------------------------------------------------------- +// Last project path +// --------------------------------------------------------------------------- +QString Settings::lastProjectPath() const +{ + return m_settings.value("project/lastPath", QString()).toString(); +} + +void Settings::setLastProjectPath(const QString &path) +{ + m_settings.setValue("project/lastPath", path); +} diff --git a/src/core/Settings.h b/src/core/Settings.h new file mode 100644 index 0000000..502afbd --- /dev/null +++ b/src/core/Settings.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Settings – Centralised persistent application settings. +// --------------------------------------------------------------------------- +class Settings : public QObject +{ + Q_OBJECT + +public: + explicit Settings(QObject *parent = nullptr); + + // Editor + QFont editorFont() const; + void setEditorFont(const QFont &font); + + int tabSize() const; + void setTabSize(int size); + + bool useSpacesForTabs() const; + void setUseSpacesForTabs(bool use); + + // Layout + int fileTreeWidth() const; + void setFileTreeWidth(int width); + + // Recent + QString lastProjectPath() const; + void setLastProjectPath(const QString &path); + + // Erscheinungsbild + bool darkMode() const; + void setDarkMode(bool dark); + +signals: + void settingsChanged(); + +private: + QSettings m_settings; +}; diff --git a/src/core/ThemeManager.cpp b/src/core/ThemeManager.cpp new file mode 100644 index 0000000..d9c9ba7 --- /dev/null +++ b/src/core/ThemeManager.cpp @@ -0,0 +1,115 @@ +#include "ThemeManager.h" + +#include +#include + +ThemeManager::ThemeManager(QObject *parent) + : QObject(parent) +{ +} + +void ThemeManager::applyTheme(Theme theme) +{ + m_currentTheme = theme; + QApplication::setStyle(QStyleFactory::create("Fusion")); + + if (theme == Theme::Dark) + { + QApplication::setPalette(buildDarkPalette()); + } + else + { + QApplication::setPalette(buildLightPalette()); + } + + emit themeChanged(theme); +} + +ThemeManager::Theme ThemeManager::currentTheme() const +{ + return m_currentTheme; +} + +QPalette ThemeManager::buildDarkPalette() +{ + QPalette p; + + const QColor bg = QColor("#1e1e1e"); + const QColor widget = QColor("#252526"); + const QColor alt = QColor("#2d2d30"); + const QColor hi = QColor("#264f78"); + const QColor hiText = QColor("#ffffff"); + const QColor text = QColor("#d4d4d4"); + const QColor disabled = QColor("#6d6d6d"); + const QColor btn = QColor("#3c3c3c"); + const QColor mid = QColor("#333333"); + const QColor dark = QColor("#1a1a1a"); + const QColor light = QColor("#454545"); + const QColor link = QColor("#569cd6"); + + p.setColor(QPalette::Window, bg); + p.setColor(QPalette::WindowText, text); + p.setColor(QPalette::Base, widget); + p.setColor(QPalette::AlternateBase, alt); + p.setColor(QPalette::Text, text); + p.setColor(QPalette::Button, btn); + p.setColor(QPalette::ButtonText, text); + p.setColor(QPalette::Highlight, hi); + p.setColor(QPalette::HighlightedText, hiText); + p.setColor(QPalette::Link, link); + p.setColor(QPalette::LinkVisited, link.darker(120)); + p.setColor(QPalette::Mid, mid); + p.setColor(QPalette::Dark, dark); + p.setColor(QPalette::Light, light); + p.setColor(QPalette::Shadow, QColor("#000000")); + p.setColor(QPalette::ToolTipBase, widget); + p.setColor(QPalette::ToolTipText, text); + p.setColor(QPalette::PlaceholderText, disabled); + + p.setColor(QPalette::Disabled, QPalette::WindowText, disabled); + p.setColor(QPalette::Disabled, QPalette::Text, disabled); + p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled); + + return p; +} + +QPalette ThemeManager::buildLightPalette() +{ + // Fusion-Standard-Palette + QPalette p; + + const QColor bg = QColor("#f3f3f3"); + const QColor widget = QColor("#ffffff"); + const QColor alt = QColor("#e8e8e8"); + const QColor hi = QColor("#0078d4"); + const QColor hiText = QColor("#ffffff"); + const QColor text = QColor("#1e1e1e"); + const QColor disabled = QColor("#a0a0a0"); + const QColor btn = QColor("#e1e1e1"); + const QColor mid = QColor("#c8c8c8"); + const QColor dark = QColor("#a0a0a0"); + const QColor light = QColor("#ffffff"); + const QColor link = QColor("#0078d4"); + + p.setColor(QPalette::Window, bg); + p.setColor(QPalette::WindowText, text); + p.setColor(QPalette::Base, widget); + p.setColor(QPalette::AlternateBase, alt); + p.setColor(QPalette::Text, text); + p.setColor(QPalette::Button, btn); + p.setColor(QPalette::ButtonText, text); + p.setColor(QPalette::Highlight, hi); + p.setColor(QPalette::HighlightedText, hiText); + p.setColor(QPalette::Link, link); + p.setColor(QPalette::LinkVisited, link.darker(130)); + p.setColor(QPalette::Mid, mid); + p.setColor(QPalette::Dark, dark); + p.setColor(QPalette::Light, light); + p.setColor(QPalette::PlaceholderText, disabled); + + p.setColor(QPalette::Disabled, QPalette::WindowText, disabled); + p.setColor(QPalette::Disabled, QPalette::Text, disabled); + p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled); + + return p; +} diff --git a/src/core/ThemeManager.h b/src/core/ThemeManager.h new file mode 100644 index 0000000..4096918 --- /dev/null +++ b/src/core/ThemeManager.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +// --------------------------------------------------------------------------- +// ThemeManager – Schaltet zwischen Hell- und Dunkelmodus um. +// --------------------------------------------------------------------------- +class ThemeManager : public QObject +{ + Q_OBJECT + +public: + enum class Theme { Light, Dark }; + + explicit ThemeManager(QObject *parent = nullptr); + + void applyTheme(Theme theme); + Theme currentTheme() const; + +signals: + void themeChanged(Theme theme); + +private: + static QPalette buildDarkPalette(); + static QPalette buildLightPalette(); + + Theme m_currentTheme = Theme::Light; +}; diff --git a/src/editor/CMakeLists.txt b/src/editor/CMakeLists.txt new file mode 100644 index 0000000..7893137 --- /dev/null +++ b/src/editor/CMakeLists.txt @@ -0,0 +1,26 @@ +set(EDITOR_SOURCES + EditorPanel.cpp + EditorPanel.h + CodeEditor.cpp + CodeEditor.h + LineNumberArea.cpp + LineNumberArea.h + EditorTab.cpp + EditorTab.h + SearchPanel.cpp + SearchPanel.h +) + +add_library(BareCode_Editor STATIC ${EDITOR_SOURCES}) + +target_link_libraries(BareCode_Editor PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets + BareCode_Highlighter +) + +target_include_directories(BareCode_Editor PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/src/editor/CodeEditor.cpp b/src/editor/CodeEditor.cpp new file mode 100644 index 0000000..8ad7049 --- /dev/null +++ b/src/editor/CodeEditor.cpp @@ -0,0 +1,345 @@ +#include "CodeEditor.h" +#include "LineNumberArea.h" + +#include "core/Settings.h" +#include "highlighter/HighlighterFactory.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CodeEditor::CodeEditor(Settings *settings, QWidget *parent) + : QPlainTextEdit(parent) + , m_settings(settings) +{ + m_lineNumberArea = new LineNumberArea(this); + setupEditor(); + + connect(this, &CodeEditor::blockCountChanged, + this, &CodeEditor::updateLineNumberAreaWidth); + + connect(this, &CodeEditor::updateRequest, + this, &CodeEditor::updateLineNumberArea); + + connect(this, &CodeEditor::cursorPositionChanged, + this, &CodeEditor::highlightCurrentLine); + + updateLineNumberAreaWidth(0); + highlightCurrentLine(); +} + +CodeEditor::~CodeEditor() = default; + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- +void CodeEditor::setupEditor() +{ + applySettings(); + setLineWrapMode(QPlainTextEdit::NoWrap); +} + +void CodeEditor::applySettings() +{ + setFont(m_settings->editorFont()); + + const int tabStop = m_settings->tabSize(); + // Set tab stop width in pixels using font metrics + QFontMetrics fm(m_settings->editorFont()); + setTabStopDistance(static_cast(tabStop) * fm.horizontalAdvance(' ')); +} + +// --------------------------------------------------------------------------- +// File I/O +// --------------------------------------------------------------------------- +void CodeEditor::loadFile(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QMessageBox::warning(this, tr("Open File"), + tr("Cannot open file:\n%1").arg(filePath)); + return; + } + + m_filePath = filePath; + + QTextStream in(&file); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + in.setEncoding(QStringConverter::Utf8); +#else + in.setCodec("UTF-8"); +#endif + + setPlainText(in.readAll()); + document()->setModified(false); + + installHighlighter(filePath); +} + +QString CodeEditor::filePath() const +{ + return m_filePath; +} + +bool CodeEditor::save() +{ + if (m_filePath.isEmpty()) + { + return saveAs(); + } + + return writeToFile(m_filePath); +} + +bool CodeEditor::saveAs() +{ + const QString path = QFileDialog::getSaveFileName( + this, + tr("Speichern unter"), + m_filePath + ); + + if (path.isEmpty()) + { + return false; + } + + m_filePath = path; + installHighlighter(m_filePath); + return writeToFile(m_filePath); +} + +bool CodeEditor::writeToFile(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QMessageBox::warning(this, tr("Speichern"), + tr("Datei konnte nicht gespeichert werden:\n%1").arg(filePath)); + return false; + } + + QTextStream out(&file); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + out.setEncoding(QStringConverter::Utf8); +#else + out.setCodec("UTF-8"); +#endif + + out << toPlainText(); + document()->setModified(false); + emit fileSaved(filePath); + return true; +} + +void CodeEditor::installHighlighter(const QString &filePath) +{ + // Remove old highlighter first + delete m_highlighter; + m_highlighter = nullptr; + + m_highlighter = HighlighterFactory::createForFile(filePath, document()); +} + +bool CodeEditor::isModified() const +{ + return document()->isModified(); +} + +// --------------------------------------------------------------------------- +// Line number area +// --------------------------------------------------------------------------- +int CodeEditor::lineNumberAreaWidth() const +{ + int digits = 1; + int max = qMax(1, blockCount()); + while (max >= 10) + { + max /= 10; + ++digits; + } + + const int padding = 8; + return fontMetrics().horizontalAdvance('9') * digits + padding * 2; +} + +void CodeEditor::updateLineNumberAreaWidth(int /*newBlockCount*/) +{ + setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); +} + +void CodeEditor::updateLineNumberArea(const QRect &rect, int dy) +{ + if (dy != 0) + { + m_lineNumberArea->scroll(0, dy); + } + else + { + m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height()); + } + + if (rect.contains(viewport()->rect())) + { + updateLineNumberAreaWidth(0); + } +} + +void CodeEditor::resizeEvent(QResizeEvent *event) +{ + QPlainTextEdit::resizeEvent(event); + + const QRect cr = contentsRect(); + m_lineNumberArea->setGeometry( + QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()) + ); +} + +void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event) +{ + QPainter painter(m_lineNumberArea); + + // Background + const QColor bgColor = palette().color(QPalette::Window).darker(110); + painter.fillRect(event->rect(), bgColor); + + const QColor lineNumColor = palette().color(QPalette::Mid); + const QColor activeColor = palette().color(QPalette::Text); + + const int currentLine = textCursor().blockNumber(); + + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber(); + int top = static_cast(blockBoundingGeometry(block).translated(contentOffset()).top()); + int bottom = top + static_cast(blockBoundingRect(block).height()); + + while (block.isValid() && top <= event->rect().bottom()) + { + if (block.isVisible() && bottom >= event->rect().top()) + { + const QString number = QString::number(blockNumber + 1); + painter.setPen(blockNumber == currentLine ? activeColor : lineNumColor); + painter.drawText( + 0, + top, + m_lineNumberArea->width() - 4, + fontMetrics().height(), + Qt::AlignRight, + number + ); + } + + block = block.next(); + top = bottom; + bottom = top + static_cast(blockBoundingRect(block).height()); + ++blockNumber; + } +} + +// --------------------------------------------------------------------------- +// Current line highlight +// --------------------------------------------------------------------------- +void CodeEditor::highlightCurrentLine() +{ + QList extraSelections; + + if (!isReadOnly()) + { + QTextEdit::ExtraSelection selection; + + const QColor lineColor = palette().color(QPalette::AlternateBase); + selection.format.setBackground(lineColor); + selection.format.setProperty(QTextFormat::FullWidthSelection, true); + selection.cursor = textCursor(); + selection.cursor.clearSelection(); + + extraSelections.append(selection); + } + + setExtraSelections(extraSelections); +} + +// --------------------------------------------------------------------------- +// Key handling – auto-indent + Tab → spaces +// --------------------------------------------------------------------------- +void CodeEditor::keyPressEvent(QKeyEvent *event) +{ + // Tab key: insert spaces instead of a real tab character + if (event->key() == Qt::Key_Tab && m_settings->useSpacesForTabs()) + { + const int tabSize = m_settings->tabSize(); + QTextCursor cursor = textCursor(); + + if (cursor.hasSelection()) + { + // Indent selected lines + QTextBlock startBlock = document()->findBlock(cursor.selectionStart()); + QTextBlock endBlock = document()->findBlock(cursor.selectionEnd()); + + cursor.beginEditBlock(); + for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next()) + { + QTextCursor lineCursor(b); + lineCursor.insertText(QString(tabSize, ' ')); + } + cursor.endEditBlock(); + } + else + { + // Calculate spaces needed to reach next tab stop + const int col = cursor.columnNumber(); + const int spacesNeeded = tabSize - (col % tabSize); + cursor.insertText(QString(spacesNeeded, ' ')); + } + return; + } + + // Enter / Return: auto-indent + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) + { + QTextCursor cursor = textCursor(); + const QString currentLine = cursor.block().text(); + + // Count leading whitespace + int leadingSpaces = 0; + for (const QChar &ch : currentLine) + { + if (ch == ' ') + { + ++leadingSpaces; + } + else if (ch == '\t') + { + leadingSpaces += m_settings->tabSize(); + } + else + { + break; + } + } + + // Let the base class insert the newline first + QPlainTextEdit::keyPressEvent(event); + + // Then re-indent + if (leadingSpaces > 0) + { + const QString indent = m_settings->useSpacesForTabs() + ? QString(leadingSpaces, ' ') + : QString(leadingSpaces / m_settings->tabSize(), '\t'); + textCursor().insertText(indent); + } + return; + } + + QPlainTextEdit::keyPressEvent(event); +} diff --git a/src/editor/CodeEditor.h b/src/editor/CodeEditor.h new file mode 100644 index 0000000..dd52331 --- /dev/null +++ b/src/editor/CodeEditor.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +class LineNumberArea; +class Settings; +class SyntaxHighlighter; + +// --------------------------------------------------------------------------- +// CodeEditor – Core editing widget. +// Features: +// • Line number gutter +// • Current-line highlight +// • Auto-indent on Enter +// • Tab → spaces (configurable) +// • Syntax highlighting (via pluggable SyntaxHighlighter) +// --------------------------------------------------------------------------- +class CodeEditor : public QPlainTextEdit +{ + Q_OBJECT + +public: + explicit CodeEditor(Settings *settings, QWidget *parent = nullptr); + ~CodeEditor() override; + + void loadFile(const QString &filePath); + void applySettings(); + + // Speichern + bool save(); + bool saveAs(); + + // Called by LineNumberArea + int lineNumberAreaWidth() const; + void lineNumberAreaPaintEvent(QPaintEvent *event); + + QString filePath() const; + bool isModified() const; + +signals: + void fileSaved(const QString &filePath); + +protected: + void resizeEvent(QResizeEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + +private slots: + void updateLineNumberAreaWidth(int newBlockCount); + void highlightCurrentLine(); + void updateLineNumberArea(const QRect &rect, int dy); + +private: + void setupEditor(); + void installHighlighter(const QString &filePath); + bool writeToFile(const QString &filePath); + + Settings *m_settings = nullptr; + LineNumberArea *m_lineNumberArea = nullptr; + SyntaxHighlighter *m_highlighter = nullptr; + QString m_filePath; +}; diff --git a/src/editor/EditorPanel.cpp b/src/editor/EditorPanel.cpp new file mode 100644 index 0000000..bffe04e --- /dev/null +++ b/src/editor/EditorPanel.cpp @@ -0,0 +1,157 @@ +#include "EditorPanel.h" +#include "EditorTab.h" +#include "CodeEditor.h" +#include "SearchPanel.h" + +#include + +EditorPanel::EditorPanel(Settings *settings, QWidget *parent) + : QWidget(parent) + , m_settings(settings) +{ + setupUi(); +} + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- +void EditorPanel::setupUi() +{ + m_layout = new QVBoxLayout(this); + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->setSpacing(0); + + m_tabWidget = new QTabWidget(this); + m_tabWidget->setTabsClosable(true); + m_tabWidget->setMovable(true); + m_tabWidget->setDocumentMode(true); + + m_searchPanel = new SearchPanel(this); + + m_layout->addWidget(m_tabWidget, 1); + m_layout->addWidget(m_searchPanel, 0); + + connect(m_tabWidget, &QTabWidget::tabCloseRequested, + this, &EditorPanel::onTabCloseRequested); + + connect(m_tabWidget, &QTabWidget::currentChanged, + this, &EditorPanel::onCurrentTabChanged); +} + +// --------------------------------------------------------------------------- +// Hilfsmethoden +// --------------------------------------------------------------------------- +EditorTab *EditorPanel::currentTab() const +{ + return qobject_cast(m_tabWidget->currentWidget()); +} + +int EditorPanel::findTabForFile(const QString &filePath) const +{ + EditorTab *tab = m_openTabs.value(filePath, nullptr); + return tab ? m_tabWidget->indexOf(tab) : -1; +} + +// --------------------------------------------------------------------------- +// Öffentliche Slots +// --------------------------------------------------------------------------- +void EditorPanel::openFile(const QString &filePath) +{ + const int existing = findTabForFile(filePath); + if (existing != -1) + { + m_tabWidget->setCurrentIndex(existing); + return; + } + + EditorTab *tab = new EditorTab(filePath, m_settings, m_tabWidget); + const int index = m_tabWidget->addTab(tab, tab->fileName()); + m_tabWidget->setCurrentIndex(index); + m_tabWidget->setTabToolTip(index, filePath); + m_openTabs.insert(filePath, tab); + + // Tab-Titel nach "Speichern unter" aktualisieren + connect(tab->editor(), &CodeEditor::fileSaved, this, [this, tab](const QString &savedPath) + { + const int idx = m_tabWidget->indexOf(tab); + if (idx != -1) + { + m_tabWidget->setTabText(idx, QFileInfo(savedPath).fileName()); + m_tabWidget->setTabToolTip(idx, savedPath); + } + emit currentFileSaved(savedPath); + }); +} + +void EditorPanel::saveCurrentFile() +{ + if (EditorTab *tab = currentTab()) + { + tab->save(); + } +} + +void EditorPanel::saveCurrentFileAs() +{ + if (EditorTab *tab = currentTab()) + { + tab->saveAs(); + } +} + +void EditorPanel::saveAllFiles() +{ + for (int i = 0; i < m_tabWidget->count(); ++i) + { + EditorTab *tab = qobject_cast(m_tabWidget->widget(i)); + if (tab && tab->isModified()) + { + tab->save(); + } + } +} + +void EditorPanel::showSearchPanel() +{ + m_searchPanel->activate(); +} + +void EditorPanel::undo() +{ + if (EditorTab *tab = currentTab()) + { + tab->editor()->undo(); + } +} + +void EditorPanel::redo() +{ + if (EditorTab *tab = currentTab()) + { + tab->editor()->redo(); + } +} + +// --------------------------------------------------------------------------- +// Private Slots +// --------------------------------------------------------------------------- +void EditorPanel::onTabCloseRequested(int index) +{ + EditorTab *tab = qobject_cast(m_tabWidget->widget(index)); + if (!tab) + { + return; + } + + m_openTabs.remove(tab->filePath()); + m_tabWidget->removeTab(index); + tab->deleteLater(); + + m_searchPanel->setEditor(currentTab() ? currentTab()->editor() : nullptr); +} + +void EditorPanel::onCurrentTabChanged(int /*index*/) +{ + EditorTab *tab = currentTab(); + m_searchPanel->setEditor(tab ? tab->editor() : nullptr); +} diff --git a/src/editor/EditorPanel.h b/src/editor/EditorPanel.h new file mode 100644 index 0000000..2fce31e --- /dev/null +++ b/src/editor/EditorPanel.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include + +class EditorTab; +class Settings; +class SearchPanel; + +// --------------------------------------------------------------------------- +// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel. +// --------------------------------------------------------------------------- +class EditorPanel : public QWidget +{ + Q_OBJECT + +public: + explicit EditorPanel(Settings *settings, QWidget *parent = nullptr); + +public slots: + void openFile(const QString &filePath); + void saveCurrentFile(); + void saveCurrentFileAs(); + void saveAllFiles(); + void showSearchPanel(); + void undo(); + void redo(); + +signals: + void currentFileSaved(const QString &filePath); + +private slots: + void onTabCloseRequested(int index); + void onCurrentTabChanged(int index); + +private: + void setupUi(); + 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; + + QHash m_openTabs; +}; diff --git a/src/editor/EditorTab.cpp b/src/editor/EditorTab.cpp new file mode 100644 index 0000000..0b0b8fa --- /dev/null +++ b/src/editor/EditorTab.cpp @@ -0,0 +1,53 @@ +#include "EditorTab.h" +#include "CodeEditor.h" + +#include + +EditorTab::EditorTab(const QString &filePath, Settings *settings, QWidget *parent) + : QWidget(parent) + , m_filePath(filePath) +{ + m_layout = new QVBoxLayout(this); + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->setSpacing(0); + + m_editor = new CodeEditor(settings, this); + m_editor->loadFile(filePath); + + m_layout->addWidget(m_editor); +} + +QString EditorTab::filePath() const +{ + return m_filePath; +} + +QString EditorTab::fileName() const +{ + return QFileInfo(m_filePath).fileName(); +} + +CodeEditor *EditorTab::editor() const +{ + return m_editor; +} + +bool EditorTab::isModified() const +{ + return m_editor->isModified(); +} + +bool EditorTab::save() +{ + const bool ok = m_editor->save(); + // Path may have changed if this was an untitled buffer saved for the first time + m_filePath = m_editor->filePath(); + return ok; +} + +bool EditorTab::saveAs() +{ + const bool ok = m_editor->saveAs(); + m_filePath = m_editor->filePath(); + return ok; +} diff --git a/src/editor/EditorTab.h b/src/editor/EditorTab.h new file mode 100644 index 0000000..3bf0d8f --- /dev/null +++ b/src/editor/EditorTab.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +class CodeEditor; +class Settings; + +// --------------------------------------------------------------------------- +// EditorTab – Widget placed inside each tab of the tab bar. +// Owns a CodeEditor for a single file. +// --------------------------------------------------------------------------- +class EditorTab : public QWidget +{ + Q_OBJECT + +public: + explicit EditorTab(const QString &filePath, Settings *settings, QWidget *parent = nullptr); + + QString filePath() const; + QString fileName() const; + CodeEditor *editor() const; + bool isModified() const; + + bool save(); + bool saveAs(); + +private: + QString m_filePath; + QVBoxLayout *m_layout = nullptr; + CodeEditor *m_editor = nullptr; +}; diff --git a/src/editor/LineNumberArea.cpp b/src/editor/LineNumberArea.cpp new file mode 100644 index 0000000..44ef836 --- /dev/null +++ b/src/editor/LineNumberArea.cpp @@ -0,0 +1,18 @@ +#include "LineNumberArea.h" +#include "CodeEditor.h" + +LineNumberArea::LineNumberArea(CodeEditor *editor) + : QWidget(editor) + , m_codeEditor(editor) +{ +} + +QSize LineNumberArea::sizeHint() const +{ + return QSize(m_codeEditor->lineNumberAreaWidth(), 0); +} + +void LineNumberArea::paintEvent(QPaintEvent *event) +{ + m_codeEditor->lineNumberAreaPaintEvent(event); +} diff --git a/src/editor/LineNumberArea.h b/src/editor/LineNumberArea.h new file mode 100644 index 0000000..1a6963a --- /dev/null +++ b/src/editor/LineNumberArea.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +class CodeEditor; + +// --------------------------------------------------------------------------- +// LineNumberArea – Thin widget painted on the left side of the CodeEditor. +// Painted by CodeEditor::lineNumberAreaPaintEvent(). +// --------------------------------------------------------------------------- +class LineNumberArea : public QWidget +{ + Q_OBJECT + +public: + explicit LineNumberArea(CodeEditor *editor); + + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + CodeEditor *m_codeEditor; +}; diff --git a/src/editor/SearchPanel.cpp b/src/editor/SearchPanel.cpp new file mode 100644 index 0000000..39704f6 --- /dev/null +++ b/src/editor/SearchPanel.cpp @@ -0,0 +1,492 @@ +#include "SearchPanel.h" +#include "CodeEditor.h" + +#include +#include +#include +#include +#include +#include + +SearchPanel::SearchPanel(QWidget *parent) + : QWidget(parent) +{ + setupUi(); + hide(); +} + +// --------------------------------------------------------------------------- +// UI +// --------------------------------------------------------------------------- +void SearchPanel::setupUi() +{ + m_grid = new QGridLayout(this); + m_grid->setContentsMargins(6, 4, 6, 4); + m_grid->setSpacing(4); + + // ---- Row 0: Suchen ---- + m_searchEdit = new QLineEdit(this); + m_searchEdit->setPlaceholderText(tr("Suchen…")); + m_searchEdit->setClearButtonEnabled(true); + + m_btnPrev = new QPushButton(tr("▲"), this); + m_btnNext = new QPushButton(tr("▼"), this); + m_btnPrev->setFixedWidth(28); + m_btnNext->setFixedWidth(28); + m_btnPrev->setToolTip(tr("Vorheriger Treffer (Shift+F3)")); + m_btnNext->setToolTip(tr("Nächster Treffer (F3)")); + + m_matchLabel = new QLabel(this); + m_matchLabel->setMinimumWidth(80); + + m_btnClose = new QPushButton(tr("✕"), this); + m_btnClose->setFixedWidth(24); + m_btnClose->setToolTip(tr("Schließen (Esc)")); + m_btnClose->setFlat(true); + + QHBoxLayout *searchRow = new QHBoxLayout(); + searchRow->addWidget(new QLabel(tr("Suchen:"), this)); + searchRow->addWidget(m_searchEdit, 1); + searchRow->addWidget(m_btnPrev); + searchRow->addWidget(m_btnNext); + searchRow->addWidget(m_matchLabel); + searchRow->addWidget(m_btnClose); + m_grid->addLayout(searchRow, 0, 0); + + // ---- Row 1: Ersetzen ---- + m_replaceEdit = new QLineEdit(this); + m_replaceEdit->setPlaceholderText(tr("Ersetzen durch…")); + m_replaceEdit->setClearButtonEnabled(true); + + m_btnReplace = new QPushButton(tr("Ersetzen"), this); + m_btnReplaceAll = new QPushButton(tr("Alle ersetzen"), this); + m_btnReplaceSelection = new QPushButton(tr("In Auswahl ersetzen"), this); + + QHBoxLayout *replaceRow = new QHBoxLayout(); + replaceRow->addWidget(new QLabel(tr("Ersetzen:"), this)); + replaceRow->addWidget(m_replaceEdit, 1); + replaceRow->addWidget(m_btnReplace); + replaceRow->addWidget(m_btnReplaceAll); + replaceRow->addWidget(m_btnReplaceSelection); + m_grid->addLayout(replaceRow, 1, 0); + + // ---- Row 2: Optionen ---- + m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this); + m_chkWord = new QCheckBox(tr("Ganzes Wort"), this); + m_chkRegex = new QCheckBox(tr("Regulärer Ausdruck"), this); + + QHBoxLayout *optRow = new QHBoxLayout(); + optRow->addWidget(m_chkCase); + optRow->addWidget(m_chkWord); + optRow->addWidget(m_chkRegex); + optRow->addStretch(); + m_grid->addLayout(optRow, 2, 0); + + // ---- Connections ---- + connect(m_searchEdit, &QLineEdit::textChanged, + this, &SearchPanel::onSearchTextChanged); + + connect(m_searchEdit, &QLineEdit::returnPressed, + this, &SearchPanel::findNext); + + connect(m_btnNext, &QPushButton::clicked, this, &SearchPanel::findNext); + connect(m_btnPrev, &QPushButton::clicked, this, &SearchPanel::findPrevious); + + connect(m_btnReplace, &QPushButton::clicked, this, &SearchPanel::replaceCurrent); + connect(m_btnReplaceAll, &QPushButton::clicked, this, &SearchPanel::replaceAll); + connect(m_btnReplaceSelection, &QPushButton::clicked, this, &SearchPanel::replaceInSelection); + + connect(m_btnClose, &QPushButton::clicked, this, &SearchPanel::onCloseClicked); + + connect(m_chkCase, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged); + connect(m_chkWord, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged); + connect(m_chkRegex, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged); +} + +// --------------------------------------------------------------------------- +// Public interface +// --------------------------------------------------------------------------- +void SearchPanel::setEditor(CodeEditor *editor) +{ + clearHighlights(); + m_editor = editor; +} + +void SearchPanel::activate() +{ + show(); + m_searchEdit->setFocus(); + m_searchEdit->selectAll(); + + // Pre-fill with selected text if short enough + if (m_editor) + { + const QString sel = m_editor->textCursor().selectedText(); + if (!sel.isEmpty() && !sel.contains('\n') && sel.length() < 200) + { + m_searchEdit->setText(sel); + } + } + + updateMatchLabel(); +} + +// --------------------------------------------------------------------------- +// Find helpers +// --------------------------------------------------------------------------- +QTextDocument::FindFlags SearchPanel::buildFindFlags(bool backwards) const +{ + QTextDocument::FindFlags flags; + if (backwards) { flags |= QTextDocument::FindBackward; } + if (m_chkCase->isChecked()) { flags |= QTextDocument::FindCaseSensitively; } + if (m_chkWord->isChecked()) { flags |= QTextDocument::FindWholeWords; } + return flags; +} + +bool SearchPanel::performFind(bool backwards) +{ + if (!m_editor || m_searchEdit->text().isEmpty()) + { + return false; + } + + const QTextDocument::FindFlags flags = buildFindFlags(backwards); + bool found = false; + + if (m_chkRegex->isChecked()) + { + QRegularExpression re(m_searchEdit->text()); + if (m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::NoPatternOption); + } + else + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + found = m_editor->find(re, flags); + + // Wrap around + if (!found) + { + QTextCursor c = m_editor->textCursor(); + c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start); + m_editor->setTextCursor(c); + found = m_editor->find(re, flags); + } + } + else + { + found = m_editor->find(m_searchEdit->text(), flags); + + // Wrap around + if (!found) + { + QTextCursor c = m_editor->textCursor(); + c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start); + m_editor->setTextCursor(c); + found = m_editor->find(m_searchEdit->text(), flags); + } + } + + return found; +} + +void SearchPanel::highlightAllMatches() +{ + if (!m_editor) + { + return; + } + + QList extras; + + const QString needle = m_searchEdit->text(); + if (needle.isEmpty()) + { + m_editor->setExtraSelections(extras); + return; + } + + QTextCharFormat fmt; + fmt.setBackground(QColor("#3a3a00")); + fmt.setForeground(QColor("#ffff80")); + + QTextDocument *doc = m_editor->document(); + QTextCursor cursor(doc); + + const QTextDocument::FindFlags flags = buildFindFlags(false); + + while (true) + { + if (m_chkRegex->isChecked()) + { + QRegularExpression re(needle); + if (!m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + cursor = doc->find(re, cursor, flags); + } + else + { + cursor = doc->find(needle, cursor, flags); + } + + if (cursor.isNull()) + { + break; + } + + QTextEdit::ExtraSelection sel; + sel.cursor = cursor; + sel.format = fmt; + extras.append(sel); + } + + m_editor->setExtraSelections(extras); +} + +void SearchPanel::clearHighlights() +{ + if (m_editor) + { + m_editor->setExtraSelections({}); + } +} + +void SearchPanel::updateMatchLabel() +{ + if (!m_editor || m_searchEdit->text().isEmpty()) + { + m_matchLabel->setText(QString()); + return; + } + + // Count total matches + int count = 0; + QTextDocument *doc = m_editor->document(); + QTextCursor cursor(doc); + const QTextDocument::FindFlags flags = buildFindFlags(false); + const QString needle = m_searchEdit->text(); + + while (true) + { + if (m_chkRegex->isChecked()) + { + QRegularExpression re(needle); + if (!m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + cursor = doc->find(re, cursor, flags); + } + else + { + cursor = doc->find(needle, cursor, flags); + } + + if (cursor.isNull()) + { + break; + } + ++count; + } + + if (count == 0) + { + m_matchLabel->setText(tr("Kein Treffer")); + m_matchLabel->setStyleSheet("color: #cc4444;"); + } + else + { + m_matchLabel->setText(tr("%1 Treffer").arg(count)); + m_matchLabel->setStyleSheet(QString()); + } +} + +// --------------------------------------------------------------------------- +// Public slots +// --------------------------------------------------------------------------- +void SearchPanel::findNext() +{ + performFind(false); +} + +void SearchPanel::findPrevious() +{ + performFind(true); +} + +void SearchPanel::replaceCurrent() +{ + if (!m_editor) + { + return; + } + + QTextCursor cursor = m_editor->textCursor(); + + // If current selection matches the search term, replace it + // Otherwise just find the next occurrence first + const bool hasMatch = !cursor.selectedText().isEmpty(); + if (!hasMatch) + { + performFind(false); + return; + } + + cursor.insertText(m_replaceEdit->text()); + + // Move to next match + performFind(false); + updateMatchLabel(); + highlightAllMatches(); +} + +void SearchPanel::replaceAll() +{ + if (!m_editor || m_searchEdit->text().isEmpty()) + { + return; + } + + QTextDocument *doc = m_editor->document(); + QTextCursor cursor(doc); + cursor.beginEditBlock(); + + int count = 0; + const QTextDocument::FindFlags flags = buildFindFlags(false); + const QString needle = m_searchEdit->text(); + const QString replacement = m_replaceEdit->text(); + + while (true) + { + if (m_chkRegex->isChecked()) + { + QRegularExpression re(needle); + if (!m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + cursor = doc->find(re, cursor, flags); + } + else + { + cursor = doc->find(needle, cursor, flags); + } + + if (cursor.isNull()) + { + break; + } + + cursor.insertText(replacement); + ++count; + } + + cursor.endEditBlock(); + + updateMatchLabel(); + clearHighlights(); + + QMessageBox::information(this, tr("Alle ersetzen"), + tr("%1 Ersetzung(en) durchgeführt.").arg(count)); +} + +void SearchPanel::replaceInSelection() +{ + if (!m_editor || m_searchEdit->text().isEmpty()) + { + return; + } + + QTextCursor selCursor = m_editor->textCursor(); + if (!selCursor.hasSelection()) + { + QMessageBox::information(this, tr("In Auswahl ersetzen"), + tr("Es ist kein Text ausgewählt.")); + return; + } + + // Work only within the selected region + const int selStart = selCursor.selectionStart(); + const int selEnd = selCursor.selectionEnd(); + + QTextDocument *doc = m_editor->document(); + QTextCursor cursor(doc); + cursor.setPosition(selStart); + cursor.beginEditBlock(); + + int count = 0; + int offset = 0; // Replacement may be longer/shorter than search term + const QTextDocument::FindFlags flags = buildFindFlags(false); + const QString needle = m_searchEdit->text(); + const QString replacement = m_replaceEdit->text(); + + while (true) + { + if (m_chkRegex->isChecked()) + { + QRegularExpression re(needle); + if (!m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + cursor = doc->find(re, cursor, flags); + } + else + { + cursor = doc->find(needle, cursor, flags); + } + + if (cursor.isNull()) + { + break; + } + + // Stop if we've left the original selection + if (cursor.selectionEnd() > selEnd + offset) + { + break; + } + + offset += replacement.length() - cursor.selectedText().length(); + cursor.insertText(replacement); + ++count; + } + + cursor.endEditBlock(); + + updateMatchLabel(); + clearHighlights(); + + QMessageBox::information(this, tr("In Auswahl ersetzen"), + tr("%1 Ersetzung(en) in der Auswahl durchgeführt.").arg(count)); +} + +// --------------------------------------------------------------------------- +// Private slots +// --------------------------------------------------------------------------- +void SearchPanel::onSearchTextChanged(const QString &/*text*/) +{ + highlightAllMatches(); + updateMatchLabel(); +} + +void SearchPanel::onOptionChanged() +{ + highlightAllMatches(); + updateMatchLabel(); +} + +void SearchPanel::onCloseClicked() +{ + clearHighlights(); + m_matchLabel->setText(QString()); + hide(); + if (m_editor) + { + m_editor->setFocus(); + } +} diff --git a/src/editor/SearchPanel.h b/src/editor/SearchPanel.h new file mode 100644 index 0000000..b3e1158 --- /dev/null +++ b/src/editor/SearchPanel.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class CodeEditor; + +// --------------------------------------------------------------------------- +// SearchPanel – Collapsible find/replace bar that operates on a CodeEditor. +// +// Capabilities: +// • Nächsten / Vorherigen Treffer suchen +// • Einzeln ersetzen +// • Alle ersetzen +// • Nur in Auswahl ersetzen +// • Optionen: Groß-/Kleinschreibung, Ganzes Wort, Reguläre Ausdrücke +// --------------------------------------------------------------------------- +class SearchPanel : public QWidget +{ + Q_OBJECT + +public: + explicit SearchPanel(QWidget *parent = nullptr); + + // Must be called whenever the active editor changes + void setEditor(CodeEditor *editor); + + // Toggle visibility and focus the search field + void activate(); + +public slots: + void findNext(); + void findPrevious(); + void replaceCurrent(); + void replaceAll(); + void replaceInSelection(); + +private slots: + void onSearchTextChanged(const QString &text); + void onOptionChanged(); // Für Checkbox-Signale (bool-Parameter wird ignoriert) + void onCloseClicked(); + +private: + void setupUi(); + + QTextDocument::FindFlags buildFindFlags(bool backwards = false) const; + bool performFind(bool backwards = false); + void highlightAllMatches(); + void clearHighlights(); + void updateMatchLabel(); + + CodeEditor *m_editor = nullptr; + + // Search row + QLineEdit *m_searchEdit = nullptr; + QPushButton *m_btnPrev = nullptr; + QPushButton *m_btnNext = nullptr; + QLabel *m_matchLabel = nullptr; + QPushButton *m_btnClose = nullptr; + + // Replace row + QLineEdit *m_replaceEdit = nullptr; + QPushButton *m_btnReplace = nullptr; + QPushButton *m_btnReplaceAll = nullptr; + QPushButton *m_btnReplaceSelection = nullptr; + + // Options row + QCheckBox *m_chkCase = nullptr; + QCheckBox *m_chkWord = nullptr; + QCheckBox *m_chkRegex = nullptr; + + QGridLayout *m_grid = nullptr; +}; diff --git a/src/filetree/CMakeLists.txt b/src/filetree/CMakeLists.txt new file mode 100644 index 0000000..f269f5e --- /dev/null +++ b/src/filetree/CMakeLists.txt @@ -0,0 +1,17 @@ +set(FILETREE_SOURCES + FileTreePanel.cpp + FileTreePanel.h +) + +add_library(BareCode_FileTree STATIC ${FILETREE_SOURCES}) + +target_link_libraries(BareCode_FileTree PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +target_include_directories(BareCode_FileTree PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/src/filetree/FileTreePanel.cpp b/src/filetree/FileTreePanel.cpp new file mode 100644 index 0000000..1ed0221 --- /dev/null +++ b/src/filetree/FileTreePanel.cpp @@ -0,0 +1,259 @@ +#include "FileTreePanel.h" + +#include +#include +#include +#include +#include +#include + +FileTreePanel::FileTreePanel(QWidget *parent) + : QWidget(parent) +{ + setupUi(); +} + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- +void FileTreePanel::setupUi() +{ + m_layout = new QVBoxLayout(this); + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->setSpacing(0); + + // Small header label showing the project name + m_label = new QLabel(tr("Kein Projekt geöffnet"), this); + m_label->setContentsMargins(6, 4, 6, 4); + m_label->setStyleSheet("font-weight: bold; background: palette(mid);"); + m_label->setWordWrap(true); + m_layout->addWidget(m_label); + + // File system model – show only the project subtree + m_model = new QFileSystemModel(this); + m_model->setReadOnly(false); + m_model->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot); + + // Tree view + m_tree = new QTreeView(this); + m_tree->setModel(m_model); + m_tree->setAnimated(true); + m_tree->setIndentation(16); + m_tree->setSortingEnabled(true); + m_tree->sortByColumn(0, Qt::AscendingOrder); + m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_tree->setHeaderHidden(true); + m_tree->setContextMenuPolicy(Qt::CustomContextMenu); + + // Hide all columns except the file name + for (int col = 1; col < m_model->columnCount(); ++col) + { + m_tree->hideColumn(col); + } + + m_layout->addWidget(m_tree); + + connect(m_tree, &QTreeView::activated, + this, &FileTreePanel::onItemActivated); + + connect(m_tree, &QTreeView::customContextMenuRequested, + this, &FileTreePanel::onContextMenuRequested); +} + +// --------------------------------------------------------------------------- +// Public interface +// --------------------------------------------------------------------------- +void FileTreePanel::setRootPath(const QString &path) +{ + const QModelIndex root = m_model->setRootPath(path); + m_tree->setRootIndex(root); + + const QString projectName = QDir(path).dirName(); + m_label->setText(projectName.isEmpty() ? path : projectName); +} + +void FileTreePanel::clearRoot() +{ + m_model->setRootPath(QString()); + m_tree->setRootIndex(QModelIndex()); + m_label->setText(tr("Kein Projekt geöffnet")); +} + +void FileTreePanel::triggerNewFile() +{ + onNewFile(); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +QString FileTreePanel::selectedDirectory() const +{ + const QModelIndex index = m_tree->currentIndex(); + if (!index.isValid()) + { + return m_model->rootPath(); + } + + const QString path = m_model->filePath(index); + const QFileInfo info(path); + return info.isDir() ? path : info.absolutePath(); +} + +// --------------------------------------------------------------------------- +// Slots +// --------------------------------------------------------------------------- +void FileTreePanel::onItemActivated(const QModelIndex &index) +{ + const QString path = m_model->filePath(index); + const QFileInfo info(path); + + if (info.isFile()) + { + emit fileActivated(path); + } +} + +void FileTreePanel::onContextMenuRequested(const QPoint &pos) +{ + QMenu menu(this); + + QAction *actNewFile = menu.addAction(tr("Neue Datei…")); + QAction *actNewFolder = menu.addAction(tr("Neuer Ordner…")); + menu.addSeparator(); + QAction *actDelete = menu.addAction(tr("Löschen")); + + // Disable delete if nothing is selected + const QModelIndex index = m_tree->indexAt(pos); + actDelete->setEnabled(index.isValid()); + + QAction *chosen = menu.exec(m_tree->viewport()->mapToGlobal(pos)); + + if (chosen == actNewFile) + { + onNewFile(); + } + else if (chosen == actNewFolder) + { + onNewFolder(); + } + else if (chosen == actDelete) + { + onDeleteEntry(); + } +} + +void FileTreePanel::onNewFile() +{ + const QString dir = selectedDirectory(); + if (dir.isEmpty()) + { + return; + } + + bool ok = false; + const QString name = QInputDialog::getText( + this, + tr("Neue Datei"), + tr("Dateiname:"), + QLineEdit::Normal, + QString(), + &ok + ); + + if (!ok || name.trimmed().isEmpty()) + { + return; + } + + const QString filePath = QDir(dir).filePath(name.trimmed()); + + if (QFile::exists(filePath)) + { + QMessageBox::warning(this, tr("Neue Datei"), + tr("Eine Datei mit diesem Namen existiert bereits:\n%1").arg(filePath)); + return; + } + + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) + { + QMessageBox::critical(this, tr("Neue Datei"), + tr("Datei konnte nicht angelegt werden:\n%1").arg(filePath)); + return; + } + file.close(); + + emit fileCreated(filePath); + + // Select the new file in the tree + const QModelIndex newIndex = m_model->index(filePath); + m_tree->setCurrentIndex(newIndex); + m_tree->scrollTo(newIndex); +} + +void FileTreePanel::onNewFolder() +{ + const QString dir = selectedDirectory(); + if (dir.isEmpty()) + { + return; + } + + bool ok = false; + const QString name = QInputDialog::getText( + this, + tr("Neuer Ordner"), + tr("Ordnername:"), + QLineEdit::Normal, + QString(), + &ok + ); + + if (!ok || name.trimmed().isEmpty()) + { + return; + } + + if (!QDir(dir).mkdir(name.trimmed())) + { + QMessageBox::critical(this, tr("Neuer Ordner"), + tr("Ordner konnte nicht angelegt werden:\n%1") + .arg(QDir(dir).filePath(name.trimmed()))); + } +} + +void FileTreePanel::onDeleteEntry() +{ + const QModelIndex index = m_tree->currentIndex(); + if (!index.isValid()) + { + return; + } + + const QString path = m_model->filePath(index); + const QFileInfo info(path); + const QString what = info.isDir() ? tr("Ordner") : tr("Datei"); + + const auto answer = QMessageBox::question( + this, + tr("%1 löschen").arg(what), + tr("%1 wirklich löschen?\n%2").arg(what, path), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No + ); + + if (answer != QMessageBox::Yes) + { + return; + } + + if (info.isDir()) + { + QDir(path).removeRecursively(); + } + else + { + QFile::remove(path); + } +} diff --git a/src/filetree/FileTreePanel.h b/src/filetree/FileTreePanel.h new file mode 100644 index 0000000..32c5c9e --- /dev/null +++ b/src/filetree/FileTreePanel.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// FileTreePanel – Left panel showing the project directory tree. +// Emits fileActivated(path) when the user double-clicks a file. +// Supports creating new files/folders via context menu. +// --------------------------------------------------------------------------- +class FileTreePanel : public QWidget +{ + Q_OBJECT + +public: + explicit FileTreePanel(QWidget *parent = nullptr); + + void setRootPath(const QString &path); + void clearRoot(); + void triggerNewFile(); // Called from MainWindow menu action + +signals: + void fileActivated(const QString &filePath); + void fileCreated(const QString &filePath); + +private slots: + void onItemActivated(const QModelIndex &index); + void onContextMenuRequested(const QPoint &pos); + void onNewFile(); + void onNewFolder(); + void onDeleteEntry(); + +private: + void setupUi(); + + // Returns the directory of the currently selected item + QString selectedDirectory() const; + + QVBoxLayout *m_layout = nullptr; + QLabel *m_label = nullptr; + QTreeView *m_tree = nullptr; + QFileSystemModel *m_model = nullptr; +}; diff --git a/src/highlighter/CMakeLists.txt b/src/highlighter/CMakeLists.txt new file mode 100644 index 0000000..3cbc2eb --- /dev/null +++ b/src/highlighter/CMakeLists.txt @@ -0,0 +1,19 @@ +set(HIGHLIGHTER_SOURCES + SyntaxHighlighter.cpp + SyntaxHighlighter.h + HighlighterFactory.cpp + HighlighterFactory.h +) + +add_library(BareCode_Highlighter STATIC ${HIGHLIGHTER_SOURCES}) + +target_link_libraries(BareCode_Highlighter PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +target_include_directories(BareCode_Highlighter PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/src/highlighter/HighlighterFactory.cpp b/src/highlighter/HighlighterFactory.cpp new file mode 100644 index 0000000..250e3a2 --- /dev/null +++ b/src/highlighter/HighlighterFactory.cpp @@ -0,0 +1,45 @@ +#include "HighlighterFactory.h" + +#include +#include +#include + +SyntaxHighlighter *HighlighterFactory::createForFile(const QString &filePath, + QTextDocument *document) +{ + // Erweiterung → Highlighter-Fabrik + // Neue Sprache hinzufügen: einfach einen Eintrag ergänzen. + static const QHash> registry = + { + // C / C++ + { "c", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "cc", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "cpp", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "cxx", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "h", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "hpp", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "hxx", [](QTextDocument *d) { return new CppHighlighter(d); } }, + + // CSS + { "css", [](QTextDocument *d) { return new CssHighlighter(d); } }, + + // HTML / Templates + { "html", [](QTextDocument *d) { return new HtmlHighlighter(d); } }, + { "htm", [](QTextDocument *d) { return new HtmlHighlighter(d); } }, + { "xhtml",[](QTextDocument *d) { return new HtmlHighlighter(d); } }, + + // PHP (HTML + eingebettetes PHP) + { "php", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "phtml",[](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php3", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php4", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php5", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php7", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php8", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + }; + + const QString ext = QFileInfo(filePath).suffix().toLower(); + const auto it = registry.find(ext); + + return (it != registry.end()) ? it.value()(document) : nullptr; +} diff --git a/src/highlighter/HighlighterFactory.h b/src/highlighter/HighlighterFactory.h new file mode 100644 index 0000000..8392758 --- /dev/null +++ b/src/highlighter/HighlighterFactory.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include "SyntaxHighlighter.h" + +// --------------------------------------------------------------------------- +// HighlighterFactory – Maps file extensions to the correct highlighter. +// To add a new language, register it in HighlighterFactory.cpp. +// --------------------------------------------------------------------------- +class HighlighterFactory +{ +public: + // Creates and returns a highlighter for the given file path. + // Returns nullptr if no highlighter is registered for this file type. + static SyntaxHighlighter *createForFile(const QString &filePath, + QTextDocument *document); +}; diff --git a/src/highlighter/SyntaxHighlighter.cpp b/src/highlighter/SyntaxHighlighter.cpp new file mode 100644 index 0000000..562e7d2 --- /dev/null +++ b/src/highlighter/SyntaxHighlighter.cpp @@ -0,0 +1,450 @@ +#include "SyntaxHighlighter.h" +#include + +// =========================================================================== +// SyntaxHighlighter – Basis +// =========================================================================== +SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent) + : QSyntaxHighlighter(parent) +{ +} + +void SyntaxHighlighter::highlightBlock(const QString &text) +{ + for (const HighlightRule &rule : m_rules) + { + QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text); + while (it.hasNext()) + { + QRegularExpressionMatch match = it.next(); + setFormat( + static_cast(match.capturedStart()), + static_cast(match.capturedLength()), + rule.format + ); + } + } + + if (!m_hasMultiLineComment) + { + return; + } + + setCurrentBlockState(0); + + int startIndex = 0; + if (previousBlockState() != 1) + { + QRegularExpressionMatch m = m_commentStartExpression.match(text); + startIndex = m.hasMatch() ? static_cast(m.capturedStart()) : -1; + } + + while (startIndex >= 0) + { + QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, startIndex); + int commentLength = 0; + + if (endMatch.hasMatch()) + { + commentLength = static_cast(endMatch.capturedStart()) + - startIndex + + static_cast(endMatch.capturedLength()); + } + else + { + setCurrentBlockState(1); + commentLength = text.length() - startIndex; + } + + setFormat(startIndex, commentLength, m_multiLineCommentFormat); + + if (!endMatch.hasMatch()) + { + break; + } + + QRegularExpressionMatch nextStart = + m_commentStartExpression.match(text, startIndex + commentLength); + startIndex = nextStart.hasMatch() + ? static_cast(nextStart.capturedStart()) + : -1; + } +} + +// =========================================================================== +// CppHighlighter +// =========================================================================== +CppHighlighter::CppHighlighter(QTextDocument *parent) + : SyntaxHighlighter(parent) +{ + m_hasMultiLineComment = true; + + QTextCharFormat keywordFormat; + keywordFormat.setForeground(QColor("#569CD6")); + keywordFormat.setFontWeight(QFont::Bold); + + const QStringList keywords = { + "alignas","alignof","and","and_eq","asm","auto","bitand","bitor", + "bool","break","case","catch","char","char8_t","char16_t","char32_t", + "class","compl","concept","const","consteval","constexpr","constinit", + "const_cast","continue","co_await","co_return","co_yield","decltype", + "default","delete","do","double","dynamic_cast","else","enum", + "explicit","export","extern","false","float","for","friend","goto", + "if","inline","int","long","mutable","namespace","new","noexcept", + "not","not_eq","nullptr","operator","or","or_eq","private","protected", + "public","register","reinterpret_cast","requires","return","short", + "signed","sizeof","static","static_assert","static_cast","struct", + "switch","template","this","thread_local","throw","true","try", + "typedef","typeid","typename","union","unsigned","using","virtual", + "void","volatile","wchar_t","while","xor","xor_eq","override","final" + }; + + for (const QString &kw : keywords) + { + HighlightRule rule; + rule.pattern = QRegularExpression(QString("\\b%1\\b").arg(kw)); + rule.format = keywordFormat; + m_rules.append(rule); + } + + QTextCharFormat preprocFormat; + preprocFormat.setForeground(QColor("#C586C0")); + { HighlightRule r; r.pattern = QRegularExpression("^\\s*#\\s*\\w+"); r.format = preprocFormat; m_rules.append(r); } + + QTextCharFormat stringFormat; + stringFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*")"); r.format = stringFormat; m_rules.append(r); } + { HighlightRule r; r.pattern = QRegularExpression(R"('(?:[^'\\]|\\.)*')"); r.format = stringFormat; m_rules.append(r); } + + QTextCharFormat numberFormat; + numberFormat.setForeground(QColor("#B5CEA8")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b(0[xX][0-9A-Fa-f]+[uUlL]*|[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?[fFlLuU]*)\b)"); r.format = numberFormat; m_rules.append(r); } + + QTextCharFormat commentFormat; + commentFormat.setForeground(QColor("#6A9955")); + commentFormat.setFontItalic(true); + { HighlightRule r; r.pattern = QRegularExpression("//[^\n]*"); r.format = commentFormat; m_rules.append(r); } + + m_multiLineCommentFormat = commentFormat; + m_commentStartExpression = QRegularExpression(R"(/\*)"); + m_commentEndExpression = QRegularExpression(R"(\*/)"); +} + +void CppHighlighter::highlightBlock(const QString &text) +{ + SyntaxHighlighter::highlightBlock(text); +} + +// =========================================================================== +// CssHighlighter +// =========================================================================== +CssHighlighter::CssHighlighter(QTextDocument *parent) + : SyntaxHighlighter(parent) +{ + m_hasMultiLineComment = true; + + // Selektoren: .klasse #id element ::pseudo :pseudo + QTextCharFormat selectorFormat; + selectorFormat.setForeground(QColor("#D7BA7D")); + { HighlightRule r; r.pattern = QRegularExpression(R"([.#]?[\w-]+\s*(?=\s*[,{]))"); r.format = selectorFormat; m_rules.append(r); } + { HighlightRule r; r.pattern = QRegularExpression(R"(:{1,2}[\w-]+)"); r.format = selectorFormat; m_rules.append(r); } + + // Eigenschaften (property:) + QTextCharFormat propFormat; + propFormat.setForeground(QColor("#9CDCFE")); + { HighlightRule r; r.pattern = QRegularExpression(R"([\w-]+\s*(?=:))"); r.format = propFormat; m_rules.append(r); } + + // Werte – Farben #hex + QTextCharFormat colorFormat; + colorFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"(#[0-9A-Fa-f]{3,8}\b)"); r.format = colorFormat; m_rules.append(r); } + + // Zahlen + Einheiten + QTextCharFormat numberFormat; + numberFormat.setForeground(QColor("#B5CEA8")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b\d+\.?\d*(px|em|rem|%|vh|vw|pt|cm|mm|s|ms)?\b)"); r.format = numberFormat; m_rules.append(r); } + + // Strings + QTextCharFormat stringFormat; + stringFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = stringFormat; m_rules.append(r); } + + // !important + QTextCharFormat importantFormat; + importantFormat.setForeground(QColor("#F44747")); + importantFormat.setFontWeight(QFont::Bold); + { HighlightRule r; r.pattern = QRegularExpression(R"(!important)"); r.format = importantFormat; m_rules.append(r); } + + // @-Regeln + QTextCharFormat atFormat; + atFormat.setForeground(QColor("#C586C0")); + { HighlightRule r; r.pattern = QRegularExpression(R"(@[\w-]+)"); r.format = atFormat; m_rules.append(r); } + + QTextCharFormat commentFormat; + commentFormat.setForeground(QColor("#6A9955")); + commentFormat.setFontItalic(true); + m_multiLineCommentFormat = commentFormat; + m_commentStartExpression = QRegularExpression(R"(/\*)"); + m_commentEndExpression = QRegularExpression(R"(\*/)"); +} + +void CssHighlighter::highlightBlock(const QString &text) +{ + SyntaxHighlighter::highlightBlock(text); +} + +// =========================================================================== +// HtmlHighlighter +// =========================================================================== +HtmlHighlighter::HtmlHighlighter(QTextDocument *parent) + : SyntaxHighlighter(parent) +{ + // Tag-Namen
+ QTextCharFormat tagFormat; + tagFormat.setForeground(QColor("#569CD6")); + { HighlightRule r; r.pattern = QRegularExpression(R"()"); r.format = tagFormat; m_rules.append(r); } + + // Attribute name= + QTextCharFormat attrFormat; + attrFormat.setForeground(QColor("#9CDCFE")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b[\w:-]+=)"); r.format = attrFormat; m_rules.append(r); } + + // Attributwerte "wert" 'wert' + QTextCharFormat valueFormat; + valueFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = valueFormat; m_rules.append(r); } + + // DOCTYPE + QTextCharFormat doctypeFormat; + doctypeFormat.setForeground(QColor("#808080")); + { HighlightRule r; r.pattern = QRegularExpression(R"(]*>)", QRegularExpression::CaseInsensitiveOption); r.format = doctypeFormat; m_rules.append(r); } + + // Entities & { + QTextCharFormat entityFormat; + entityFormat.setForeground(QColor("#D7BA7D")); + { HighlightRule r; r.pattern = QRegularExpression(R"(&(?:#\d+|#x[0-9A-Fa-f]+|[\w]+);)"); r.format = entityFormat; m_rules.append(r); } + + // Kommentare (mehrzeilig) + QTextCharFormat commentFormat; + commentFormat.setForeground(QColor("#6A9955")); + commentFormat.setFontItalic(true); + m_multiLineCommentFormat = commentFormat; + m_commentStartExpression = QRegularExpression(""); + m_hasMultiLineComment = true; +} + +void HtmlHighlighter::highlightBlock(const QString &text) +{ + SyntaxHighlighter::highlightBlock(text); +} + +// =========================================================================== +// PhpHighlighter +// =========================================================================== +PhpHighlighter::PhpHighlighter(QTextDocument *parent) + : SyntaxHighlighter(parent) +{ + // ---- HTML-Regeln (Basis, für den Teil außerhalb von ) ---- + + QTextCharFormat tagFormat; + tagFormat.setForeground(QColor("#569CD6")); + { HighlightRule r; r.pattern = QRegularExpression(R"()"); r.format = tagFormat; m_rules.append(r); } + + QTextCharFormat attrFormat; + attrFormat.setForeground(QColor("#9CDCFE")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b[\w:-]+=)"); r.format = attrFormat; m_rules.append(r); } + + QTextCharFormat valueFormat; + valueFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = valueFormat; m_rules.append(r); } + + QTextCharFormat entityFormat; + entityFormat.setForeground(QColor("#D7BA7D")); + { HighlightRule r; r.pattern = QRegularExpression(R"(&(?:#\d+|#x[0-9A-Fa-f]+|[\w]+);)"); r.format = entityFormat; m_rules.append(r); } + + // HTML-Kommentare + QTextCharFormat htmlCommentFormat; + htmlCommentFormat.setForeground(QColor("#6A9955")); + htmlCommentFormat.setFontItalic(true); + m_multiLineCommentFormat = htmlCommentFormat; + m_commentStartExpression = QRegularExpression(""); + m_hasMultiLineComment = true; + + // ---- PHP-Tags hervorheben ---- + m_phpTagFormat.setForeground(QColor("#C586C0")); + m_phpTagFormat.setFontWeight(QFont::Bold); + + // ---- PHP-spezifische Regeln ---- + m_phpStringFormat.setForeground(QColor("#CE9178")); + m_phpCommentFormat.setForeground(QColor("#6A9955")); + m_phpCommentFormat.setFontItalic(true); + + // Keywords + QTextCharFormat kwFormat; + kwFormat.setForeground(QColor("#569CD6")); + kwFormat.setFontWeight(QFont::Bold); + + const QStringList phpKeywords = { + "abstract","and","array","as","break","callable","case","catch", + "class","clone","const","continue","declare","default","die","do", + "echo","else","elseif","empty","enddeclare","endfor","endforeach", + "endif","endswitch","endwhile","enum","extends","final","finally", + "fn","for","foreach","function","global","goto","if","implements", + "include","include_once","instanceof","insteadof","interface", + "isset","list","match","namespace","new","or","print","private", + "protected","public","readonly","require","require_once","return", + "static","switch","throw","trait","try","unset","use","var", + "while","xor","yield","null","true","false","NULL","TRUE","FALSE" + }; + + for (const QString &kw : phpKeywords) + { + HighlightRule r; + r.pattern = QRegularExpression(QString("\\b%1\\b").arg(kw)); + r.format = kwFormat; + m_phpRules.append(r); + } + + // Variablen $var + QTextCharFormat varFormat; + varFormat.setForeground(QColor("#9CDCFE")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\$[\w]+)"); r.format = varFormat; m_phpRules.append(r); } + + // Strings + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = m_phpStringFormat; m_phpRules.append(r); } + + // Zahlen + QTextCharFormat numFormat; + numFormat.setForeground(QColor("#B5CEA8")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b\d+\.?\d*\b)"); r.format = numFormat; m_phpRules.append(r); } + + // Einzeilige Kommentare + { HighlightRule r; r.pattern = QRegularExpression(R"((//|#)[^\n]*)"); r.format = m_phpCommentFormat; m_phpRules.append(r); } + + // Eingebaute Funktionen (Auswahl der häufigsten) + QTextCharFormat builtinFormat; + builtinFormat.setForeground(QColor("#DCDCAA")); + const QStringList builtins = { + "array_map","array_filter","array_keys","array_values","array_merge", + "array_push","array_pop","array_shift","array_slice","array_splice", + "count","strlen","substr","strpos","strtolower","strtoupper","trim", + "ltrim","rtrim","explode","implode","str_replace","preg_match", + "preg_replace","sprintf","printf","print_r","var_dump","isset", + "empty","unset","intval","floatval","strval","is_array","is_string", + "is_int","is_float","is_null","is_bool","is_numeric","date","time", + "mktime","json_encode","json_decode","header","session_start", + "htmlspecialchars","htmlentities","strip_tags","nl2br","round", + "floor","ceil","abs","min","max","rand","in_array","array_key_exists", + "sort","rsort","usort","ksort","krsort","ob_start","ob_get_clean" + }; + + for (const QString &fn : builtins) + { + HighlightRule r; + r.pattern = QRegularExpression(QString("\\b%1\\b").arg(fn)); + r.format = builtinFormat; + m_phpRules.append(r); + } +} + +void PhpHighlighter::highlightPhpRange(const QString &text, int start, int length) +{ + if (length <= 0) + { + return; + } + + const QString phpText = text.mid(start, length); + + for (const HighlightRule &rule : m_phpRules) + { + QRegularExpressionMatchIterator it = rule.pattern.globalMatch(phpText); + while (it.hasNext()) + { + QRegularExpressionMatch match = it.next(); + setFormat( + start + static_cast(match.capturedStart()), + static_cast(match.capturedLength()), + rule.format + ); + } + } +} + +void PhpHighlighter::highlightBlock(const QString &text) +{ + // Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden + SyntaxHighlighter::highlightBlock(text); + + // Dann PHP-Blöcke und finden und überschreiben + // Block-Zustände: 0 = HTML, 2 = innerhalb PHP-Block + setCurrentBlockState(0); + + static const QRegularExpression phpOpen(R"(<\?(?:php|=)?\s?)", + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression phpClose(R"(\?>)"); + + int pos = 0; + + if (previousBlockState() == 2) + { + // Wir befinden uns bereits in einem PHP-Block + QRegularExpressionMatch closeMatch = phpClose.match(text, 0); + if (closeMatch.hasMatch()) + { + const int end = static_cast(closeMatch.capturedStart()) + + static_cast(closeMatch.capturedLength()); + highlightPhpRange(text, 0, end); + setFormat(static_cast(closeMatch.capturedStart()), + static_cast(closeMatch.capturedLength()), + m_phpTagFormat); + pos = end; + setCurrentBlockState(0); + } + else + { + highlightPhpRange(text, 0, text.length()); + setCurrentBlockState(2); + return; + } + } + + while (pos < text.length()) + { + QRegularExpressionMatch openMatch = phpOpen.match(text, pos); + if (!openMatch.hasMatch()) + { + break; + } + + const int openStart = static_cast(openMatch.capturedStart()); + const int openEnd = openStart + static_cast(openMatch.capturedLength()); + + // (openMatch.capturedLength()), m_phpTagFormat); + + QRegularExpressionMatch closeMatch = phpClose.match(text, openEnd); + if (closeMatch.hasMatch()) + { + const int closeStart = static_cast(closeMatch.capturedStart()); + const int closeEnd = closeStart + static_cast(closeMatch.capturedLength()); + + highlightPhpRange(text, openEnd, closeStart - openEnd); + setFormat(closeStart, static_cast(closeMatch.capturedLength()), m_phpTagFormat); + + pos = closeEnd; + setCurrentBlockState(0); + } + else + { + // PHP-Block geht über Zeilenende hinaus + highlightPhpRange(text, openEnd, text.length() - openEnd); + setCurrentBlockState(2); + return; + } + } +} diff --git a/src/highlighter/SyntaxHighlighter.h b/src/highlighter/SyntaxHighlighter.h new file mode 100644 index 0000000..9c330ae --- /dev/null +++ b/src/highlighter/SyntaxHighlighter.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// SyntaxHighlighter – Regelbasierte Basis-Klasse. +// Unterklassen befüllen m_rules und können highlightBlock() überschreiben +// um mehrzeilige Konstrukte (Block-Kommentare, heredocs, …) zu behandeln. +// --------------------------------------------------------------------------- +class SyntaxHighlighter : public QSyntaxHighlighter +{ + Q_OBJECT + +public: + explicit SyntaxHighlighter(QTextDocument *parent = nullptr); + +protected: + struct HighlightRule + { + QRegularExpression pattern; + QTextCharFormat format; + }; + + void highlightBlock(const QString &text) override; + + // Unterklassen befüllen dies im Konstruktor + QVector m_rules; + + // Mehrzeilige Block-Kommentare (/* ... */) + QRegularExpression m_commentStartExpression; + QRegularExpression m_commentEndExpression; + QTextCharFormat m_multiLineCommentFormat; + bool m_hasMultiLineComment = false; +}; + +// --------------------------------------------------------------------------- +// CppHighlighter – C und C++ +// --------------------------------------------------------------------------- +class CppHighlighter : public SyntaxHighlighter +{ + Q_OBJECT +public: + explicit CppHighlighter(QTextDocument *parent = nullptr); +protected: + void highlightBlock(const QString &text) override; +}; + +// --------------------------------------------------------------------------- +// CssHighlighter – CSS +// --------------------------------------------------------------------------- +class CssHighlighter : public SyntaxHighlighter +{ + Q_OBJECT +public: + explicit CssHighlighter(QTextDocument *parent = nullptr); +protected: + void highlightBlock(const QString &text) override; +}; + +// --------------------------------------------------------------------------- +// HtmlHighlighter – HTML (mit eingebettetem CSS in