Erste Version

This commit is contained in:
2026-05-23 13:14:30 +02:00
commit 36e074f43d
39 changed files with 3430 additions and 0 deletions

4
src/CMakeLists.txt Normal file
View File

@@ -0,0 +1,4 @@
add_subdirectory(core)
add_subdirectory(editor)
add_subdirectory(filetree)
add_subdirectory(highlighter)

105
src/core/AboutDialog.cpp Normal file
View File

@@ -0,0 +1,105 @@
#include "AboutDialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QFrame>
#include <QFont>
#include <QApplication>
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);
}

14
src/core/AboutDialog.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <QDialog>
// ---------------------------------------------------------------------------
// AboutDialog Zeigt Versionsinformationen und Entwicklerangaben.
// ---------------------------------------------------------------------------
class AboutDialog : public QDialog
{
Q_OBJECT
public:
explicit AboutDialog(QWidget *parent = nullptr);
};

28
src/core/CMakeLists.txt Normal file
View File

@@ -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}/..
)

26
src/core/IPlugin.h Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include <QString>
#include <QWidget>
// ---------------------------------------------------------------------------
// 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() {}
};

316
src/core/MainWindow.cpp Normal file
View File

@@ -0,0 +1,316 @@
#include "MainWindow.h"
#include <QApplication>
#include <QFileDialog>
#include <QMessageBox>
#include <QCloseEvent>
#include <QSettings>
#include "AboutDialog.h"
#include "filetree/FileTreePanel.h"
#include "editor/EditorPanel.h"
// ---------------------------------------------------------------------------
// Konstruktor
// ---------------------------------------------------------------------------
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, m_projectManager(std::make_unique<ProjectManager>())
, m_settings(std::make_unique<Settings>())
, m_themeManager(std::make_unique<ThemeManager>())
{
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();
}

85
src/core/MainWindow.h Normal file
View File

@@ -0,0 +1,85 @@
#pragma once
#include <QMainWindow>
#include <QSplitter>
#include <QMenuBar>
#include <QStatusBar>
#include <QAction>
#include <memory>
#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<ProjectManager> m_projectManager;
std::unique_ptr<Settings> m_settings;
std::unique_ptr<ThemeManager> 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;
};

View File

@@ -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();
}

27
src/core/ProjectManager.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <QObject>
#include <QString>
// ---------------------------------------------------------------------------
// 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;
};

90
src/core/Settings.cpp Normal file
View File

@@ -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<QFont>();
}
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);
}

45
src/core/Settings.h Normal file
View File

@@ -0,0 +1,45 @@
#pragma once
#include <QObject>
#include <QSettings>
#include <QString>
#include <QFont>
// ---------------------------------------------------------------------------
// 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;
};

115
src/core/ThemeManager.cpp Normal file
View File

@@ -0,0 +1,115 @@
#include "ThemeManager.h"
#include <QApplication>
#include <QStyleFactory>
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;
}

29
src/core/ThemeManager.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <QObject>
#include <QPalette>
// ---------------------------------------------------------------------------
// 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;
};

26
src/editor/CMakeLists.txt Normal file
View File

@@ -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}/..
)

345
src/editor/CodeEditor.cpp Normal file
View File

@@ -0,0 +1,345 @@
#include "CodeEditor.h"
#include "LineNumberArea.h"
#include "core/Settings.h"
#include "highlighter/HighlighterFactory.h"
#include <QPainter>
#include <QTextBlock>
#include <QPaintEvent>
#include <QResizeEvent>
#include <QKeyEvent>
#include <QScrollBar>
#include <QFile>
#include <QTextStream>
#include <QFileInfo>
#include <QFileDialog>
#include <QMessageBox>
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<qreal>(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<int>(blockBoundingGeometry(block).translated(contentOffset()).top());
int bottom = top + static_cast<int>(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<int>(blockBoundingRect(block).height());
++blockNumber;
}
}
// ---------------------------------------------------------------------------
// Current line highlight
// ---------------------------------------------------------------------------
void CodeEditor::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> 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);
}

63
src/editor/CodeEditor.h Normal file
View File

@@ -0,0 +1,63 @@
#pragma once
#include <QPlainTextEdit>
#include <QFont>
#include <QString>
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;
};

157
src/editor/EditorPanel.cpp Normal file
View File

@@ -0,0 +1,157 @@
#include "EditorPanel.h"
#include "EditorTab.h"
#include "CodeEditor.h"
#include "SearchPanel.h"
#include <QFileInfo>
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<EditorTab *>(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<EditorTab *>(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<EditorTab *>(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);
}

50
src/editor/EditorPanel.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
#include <QWidget>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QHash>
#include <QString>
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<QString, EditorTab *> m_openTabs;
};

53
src/editor/EditorTab.cpp Normal file
View File

@@ -0,0 +1,53 @@
#include "EditorTab.h"
#include "CodeEditor.h"
#include <QFileInfo>
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;
}

33
src/editor/EditorTab.h Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
#include <QWidget>
#include <QString>
#include <QVBoxLayout>
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;
};

View File

@@ -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);
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <QWidget>
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;
};

492
src/editor/SearchPanel.cpp Normal file
View File

@@ -0,0 +1,492 @@
#include "SearchPanel.h"
#include "CodeEditor.h"
#include <QTextCursor>
#include <QTextBlock>
#include <QRegularExpression>
#include <QMessageBox>
#include <QKeyEvent>
#include <QShortcut>
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<QTextEdit::ExtraSelection> 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();
}
}

79
src/editor/SearchPanel.h Normal file
View File

@@ -0,0 +1,79 @@
#pragma once
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QLabel>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QTextDocument>
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;
};

View File

@@ -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}/..
)

View File

@@ -0,0 +1,259 @@
#include "FileTreePanel.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QInputDialog>
#include <QMessageBox>
#include <QPoint>
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);
}
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <QWidget>
#include <QTreeView>
#include <QFileSystemModel>
#include <QVBoxLayout>
#include <QLabel>
#include <QString>
#include <QMenu>
#include <QAction>
// ---------------------------------------------------------------------------
// 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;
};

View File

@@ -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}/..
)

View File

@@ -0,0 +1,45 @@
#include "HighlighterFactory.h"
#include <QFileInfo>
#include <QHash>
#include <functional>
SyntaxHighlighter *HighlighterFactory::createForFile(const QString &filePath,
QTextDocument *document)
{
// Erweiterung → Highlighter-Fabrik
// Neue Sprache hinzufügen: einfach einen Eintrag ergänzen.
static const QHash<QString, std::function<SyntaxHighlighter *(QTextDocument *)>> 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;
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include <QString>
#include <QTextDocument>
#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);
};

View File

@@ -0,0 +1,450 @@
#include "SyntaxHighlighter.h"
#include <QTextDocument>
// ===========================================================================
// 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<int>(match.capturedStart()),
static_cast<int>(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<int>(m.capturedStart()) : -1;
}
while (startIndex >= 0)
{
QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, startIndex);
int commentLength = 0;
if (endMatch.hasMatch())
{
commentLength = static_cast<int>(endMatch.capturedStart())
- startIndex
+ static_cast<int>(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<int>(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 <div </div />
QTextCharFormat tagFormat;
tagFormat.setForeground(QColor("#569CD6"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(</?[\w:-]+)"); r.format = tagFormat; m_rules.append(r); }
{ 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"(<!DOCTYPE[^>]*>)", QRegularExpression::CaseInsensitiveOption); r.format = doctypeFormat; m_rules.append(r); }
// Entities &amp; &#123;
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_commentEndExpression = 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 <?php ?>) ----
QTextCharFormat tagFormat;
tagFormat.setForeground(QColor("#569CD6"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(</?[\w:-]+)"); r.format = tagFormat; m_rules.append(r); }
{ 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_commentEndExpression = 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<int>(match.capturedStart()),
static_cast<int>(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 <?php ... ?> 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<int>(closeMatch.capturedStart())
+ static_cast<int>(closeMatch.capturedLength());
highlightPhpRange(text, 0, end);
setFormat(static_cast<int>(closeMatch.capturedStart()),
static_cast<int>(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<int>(openMatch.capturedStart());
const int openEnd = openStart + static_cast<int>(openMatch.capturedLength());
// <?php-Tag selbst einfärben
setFormat(openStart, static_cast<int>(openMatch.capturedLength()), m_phpTagFormat);
QRegularExpressionMatch closeMatch = phpClose.match(text, openEnd);
if (closeMatch.hasMatch())
{
const int closeStart = static_cast<int>(closeMatch.capturedStart());
const int closeEnd = closeStart + static_cast<int>(closeMatch.capturedLength());
highlightPhpRange(text, openEnd, closeStart - openEnd);
setFormat(closeStart, static_cast<int>(closeMatch.capturedLength()), m_phpTagFormat);
pos = closeEnd;
setCurrentBlockState(0);
}
else
{
// PHP-Block geht über Zeilenende hinaus
highlightPhpRange(text, openEnd, text.length() - openEnd);
setCurrentBlockState(2);
return;
}
}
}

View File

@@ -0,0 +1,94 @@
#pragma once
#include <QSyntaxHighlighter>
#include <QTextCharFormat>
#include <QRegularExpression>
#include <QVector>
// ---------------------------------------------------------------------------
// 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<HighlightRule> 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 <style> und JS in <script>)
// ---------------------------------------------------------------------------
class HtmlHighlighter : public SyntaxHighlighter
{
Q_OBJECT
public:
explicit HtmlHighlighter(QTextDocument *parent = nullptr);
protected:
void highlightBlock(const QString &text) override;
};
// ---------------------------------------------------------------------------
// PhpHighlighter PHP (HTML + eingebettetes PHP zwischen <?php ... ?>)
// ---------------------------------------------------------------------------
class PhpHighlighter : public SyntaxHighlighter
{
Q_OBJECT
public:
explicit PhpHighlighter(QTextDocument *parent = nullptr);
protected:
void highlightBlock(const QString &text) override;
private:
// Hilfsmethode: wendet PHP-Regeln auf einen Teilbereich an
void highlightPhpRange(const QString &text, int start, int length);
QVector<HighlightRule> m_phpRules;
QTextCharFormat m_phpStringFormat;
QTextCharFormat m_phpCommentFormat;
QTextCharFormat m_phpTagFormat;
};