Erste Version
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{src/
|
||||||
|
build/
|
||||||
69
CMakeLists.txt
Normal file
69
CMakeLists.txt
Normal file
@@ -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}
|
||||||
|
)
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -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.
|
||||||
95
README.md
Normal file
95
README.md
Normal file
@@ -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.
|
||||||
13
barecode.desktop
Normal file
13
barecode.desktop
Normal file
@@ -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
|
||||||
16
main.cpp
Normal file
16
main.cpp
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#include <QApplication>
|
||||||
|
#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();
|
||||||
|
}
|
||||||
5
resources/resources.qrc
Normal file
5
resources/resources.qrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/">
|
||||||
|
<!-- Icons and other assets can be added here -->
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
4
src/CMakeLists.txt
Normal file
4
src/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
add_subdirectory(core)
|
||||||
|
add_subdirectory(editor)
|
||||||
|
add_subdirectory(filetree)
|
||||||
|
add_subdirectory(highlighter)
|
||||||
105
src/core/AboutDialog.cpp
Normal file
105
src/core/AboutDialog.cpp
Normal 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
14
src/core/AboutDialog.h
Normal 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
28
src/core/CMakeLists.txt
Normal 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
26
src/core/IPlugin.h
Normal 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
316
src/core/MainWindow.cpp
Normal 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
85
src/core/MainWindow.h
Normal 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;
|
||||||
|
};
|
||||||
33
src/core/ProjectManager.cpp
Normal file
33
src/core/ProjectManager.cpp
Normal 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
27
src/core/ProjectManager.h
Normal 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
90
src/core/Settings.cpp
Normal 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
45
src/core/Settings.h
Normal 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
115
src/core/ThemeManager.cpp
Normal 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
29
src/core/ThemeManager.h
Normal 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
26
src/editor/CMakeLists.txt
Normal 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
345
src/editor/CodeEditor.cpp
Normal 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
63
src/editor/CodeEditor.h
Normal 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
157
src/editor/EditorPanel.cpp
Normal 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
50
src/editor/EditorPanel.h
Normal 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
53
src/editor/EditorTab.cpp
Normal 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
33
src/editor/EditorTab.h
Normal 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;
|
||||||
|
};
|
||||||
18
src/editor/LineNumberArea.cpp
Normal file
18
src/editor/LineNumberArea.cpp
Normal 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);
|
||||||
|
}
|
||||||
25
src/editor/LineNumberArea.h
Normal file
25
src/editor/LineNumberArea.h
Normal 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
492
src/editor/SearchPanel.cpp
Normal 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
79
src/editor/SearchPanel.h
Normal 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;
|
||||||
|
};
|
||||||
17
src/filetree/CMakeLists.txt
Normal file
17
src/filetree/CMakeLists.txt
Normal 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}/..
|
||||||
|
)
|
||||||
259
src/filetree/FileTreePanel.cpp
Normal file
259
src/filetree/FileTreePanel.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/filetree/FileTreePanel.h
Normal file
49
src/filetree/FileTreePanel.h
Normal 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;
|
||||||
|
};
|
||||||
19
src/highlighter/CMakeLists.txt
Normal file
19
src/highlighter/CMakeLists.txt
Normal 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}/..
|
||||||
|
)
|
||||||
45
src/highlighter/HighlighterFactory.cpp
Normal file
45
src/highlighter/HighlighterFactory.cpp
Normal 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;
|
||||||
|
}
|
||||||
18
src/highlighter/HighlighterFactory.h
Normal file
18
src/highlighter/HighlighterFactory.h
Normal 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);
|
||||||
|
};
|
||||||
450
src/highlighter/SyntaxHighlighter.cpp
Normal file
450
src/highlighter/SyntaxHighlighter.cpp
Normal 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 & {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/highlighter/SyntaxHighlighter.h
Normal file
94
src/highlighter/SyntaxHighlighter.h
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user