Dateien, die als Parameter übergeben werden, werden nach dem Start direkt geladen

This commit is contained in:
2026-08-25 14:56:27 +02:00
parent 7264b68f11
commit 04c5212d54
78 changed files with 9528 additions and 0 deletions

3
barecode/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
{src/
build/
BareCodeAUR

13
barecode/BareCode.desktop Normal file
View File

@@ -0,0 +1,13 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=BareCode
GenericName=Code Editor
Comment=Modularer Code-Editor für HTML, PHP, CSS und mehr
Exec=BareCode %F
Icon=barecode
Terminal=false
Categories=Development;TextEditor;
MimeType=text/plain;text/html;text/css;text/x-php;text/x-csrc;text/x-chdr;text/x-c++src;text/x-c++hdr;
Keywords=editor;code;html;php;css;c++;
StartupWMClass=BareCode

1
barecode/BareCode.rc Normal file
View File

@@ -0,0 +1 @@
IDI_ICON1 ICON "resources/BareCode.ico"

159
barecode/CMakeLists.txt Normal file
View File

@@ -0,0 +1,159 @@
cmake_minimum_required(VERSION 3.16)
project(BareCode
VERSION 1.2.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)
# ---------------------------------------------------------------------------
# Plattform-Erkennung
# ---------------------------------------------------------------------------
if(WIN32)
set(PLATFORM_WINDOWS TRUE)
add_compile_definitions(PLATFORM_WINDOWS)
set(CMAKE_WIN32_EXECUTABLE ON)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Haiku")
set(PLATFORM_HAIKU TRUE)
add_compile_definitions(PLATFORM_HAIKU)
elseif(APPLE)
set(PLATFORM_MACOS TRUE)
add_compile_definitions(PLATFORM_MACOS)
elseif(UNIX)
set(PLATFORM_LINUX TRUE)
add_compile_definitions(PLATFORM_LINUX)
endif()
# ---------------------------------------------------------------------------
# Qt6
# ---------------------------------------------------------------------------
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Concurrent)
qt_standard_project_setup()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
add_subdirectory(src)
# ---------------------------------------------------------------------------
# Übersetzungen — lrelease → .qm → ins Build-Verzeichnis
# ---------------------------------------------------------------------------
find_program(LRELEASE_EXECUTABLE
NAMES lrelease lrelease-qt6
HINTS "${Qt6_DIR}/../../../bin"
REQUIRED
)
set(TS_FILES
${CMAKE_CURRENT_SOURCE_DIR}/translations/barecode_de.ts
${CMAKE_CURRENT_SOURCE_DIR}/translations/barecode_en.ts
)
set(QM_FILES)
foreach(TS_FILE ${TS_FILES})
get_filename_component(TS_NAME ${TS_FILE} NAME_WE)
set(QM_FILE "${CMAKE_CURRENT_BINARY_DIR}/translations/${TS_NAME}.qm")
add_custom_command(
OUTPUT "${QM_FILE}"
COMMAND ${CMAKE_COMMAND} -E make_directory
"${CMAKE_CURRENT_BINARY_DIR}/translations"
COMMAND ${LRELEASE_EXECUTABLE} "${TS_FILE}" -qm "${QM_FILE}"
DEPENDS "${TS_FILE}"
COMMENT "lrelease: ${TS_NAME}.qm"
VERBATIM
)
list(APPEND QM_FILES "${QM_FILE}")
endforeach()
add_custom_target(BareCode_translations ALL DEPENDS ${QM_FILES})
# ---------------------------------------------------------------------------
# Resources
# ---------------------------------------------------------------------------
qt_add_resources(BARECODE_RESOURCES resources/resources.qrc)
# ---------------------------------------------------------------------------
# Executable
# ---------------------------------------------------------------------------
if(PLATFORM_WINDOWS)
qt_add_executable(BareCode main.cpp BareCode.rc ${BARECODE_RESOURCES})
else()
qt_add_executable(BareCode main.cpp ${BARECODE_RESOURCES})
endif()
add_dependencies(BareCode BareCode_translations)
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
)
# ---------------------------------------------------------------------------
# Installation
# ---------------------------------------------------------------------------
include(GNUInstallDirs)
install(TARGETS BareCode
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(FILES LICENSE
DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/barecode
)
# .qm-Dateien installieren
install(FILES ${QM_FILES}
DESTINATION ${CMAKE_INSTALL_DATADIR}/BareCode/translations
)
if(PLATFORM_LINUX)
foreach(SIZE 16 32 48 64 128 256 512)
install(FILES resources/icon_${SIZE}.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${SIZE}x${SIZE}/apps
RENAME barecode.png
)
endforeach()
install(FILES BareCode.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
)
endif()
if(PLATFORM_HAIKU)
install(FILES resources/icon_256.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/BareCode
RENAME BareCode.png
)
install(CODE "
execute_process(
COMMAND mimeset -f \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/BareCode\"
RESULT_VARIABLE _mimeset_result
)
if(NOT _mimeset_result EQUAL 0)
message(WARNING \"mimeset konnte nicht ausgeführt werden.\")
endif()
")
endif()
if(PLATFORM_MACOS)
set_target_properties(BareCode PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "BareCode"
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION}
)
endif()

21
barecode/LICENSE Normal file
View 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.

36
barecode/PKGBUILD Normal file
View File

@@ -0,0 +1,36 @@
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
# Projekt Hirnfrei - https://www.projekt-hirnfrei.de
pkgname=barecode
pkgver=1.2.0
pkgrel=1
pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++"
arch=('x86_64' 'aarch64')
url="https://www.projekt-hirnfrei.de"
license=('MIT')
depends=('qt6-base')
makedepends=('cmake' 'ninja')
provides=('barecode')
conflicts=('barecode-git')
source=("$pkgname-$pkgver.tar.gz::https://git.projekt-hirnfrei.de/diabolus/BareCode/archive/v$pkgver.tar.gz")
sha256sums=('SKIP') # Nach erstem Download ersetzen: sha256sum barecode-1.1.0.tar.gz
build() {
cd "BareCode"
cmake -B build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_SKIP_RPATH=ON
cmake --build build
}
check() {
:
}
package() {
cd "BareCode"
DESTDIR="$pkgdir" cmake --install build
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}

43
barecode/PKGBUILD-git Normal file
View File

@@ -0,0 +1,43 @@
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
# Projekt Hirnfrei - https://www.projekt-hirnfrei.de
pkgname=barecode-git
pkgver=1.1.0.r0.g0000000
pkgrel=1
pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++ (Git-Version)"
arch=('x86_64' 'aarch64')
url="https://www.projekt-hirnfrei.de"
license=('MIT')
depends=('qt6-base')
makedepends=('cmake' 'ninja' 'git')
provides=('barecode')
conflicts=('barecode')
source=("$pkgname::git+https://git.projekt-hirnfrei.de/diabolus/BareCode.git")
sha256sums=('SKIP')
pkgver() {
cd "$pkgname"
git describe --long --tags 2>/dev/null \
| sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' \
|| printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$pkgname"
cmake -B build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_SKIP_RPATH=ON
cmake --build build
}
check() {
:
}
package() {
cd "$pkgname"
DESTDIR="$pkgdir" cmake --install build
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}

200
barecode/README.md Normal file
View File

@@ -0,0 +1,200 @@
# BareCode
**Schlanker, modularer Code-Editor für Web-Entwickler**
Version 1.2.0 — von **Dany Thinnes** | [Projekt Hirnfrei](https://www.projekt-hirnfrei.de) | [Discord](https://discord.projekt-hirnfrei.de)
Entwickelt mit **C++17**, **Qt6** und **CMake**.
Kompiliert auf **Linux**, **Windows** und **Haiku**.
---
## Installation
### Arch Linux / Manjaro (AUR)
```bash
# Mit yay
yay -S barecode
# Mit paru
paru -S barecode
# Immer aktuellster Git-Stand
yay -S barecode-git
```
### Aus dem Quellcode bauen
Siehe Abschnitt **Bauen** weiter unten.
---
## Features
### Editor
| Feature | Details |
|---|---|
| Syntax-Highlighting | HTML, PHP, CSS, C/C++ — inkl. mehrzeiliger Kommentare über Leerzeilen |
| Tabs | Mehrere Dateien gleichzeitig, verschiebbar und schließbar |
| Änderungsindikator | ● im Tab-Titel bei ungespeicherten Änderungen |
| Zeilennummern | Eigener Gutter, aktuelle Zeile hervorgehoben |
| Einrück-Führungslinien | Vertikale Linien wie in VS Code |
| Klammerzugehörigkeit | Zusammengehörige Klammern werden grün hervorgehoben, fehlende rot |
| Auto-Indent | Einrückungstiefe wird bei Enter übernommen |
| Tab / Shift+Tab | Ein- und Ausrücken für einzelne Zeilen und Selektionen |
| Smart Backspace | Springt zur vorherigen Tab-Stop-Position |
| Farbvorschau | CSS-Farbwerte (#hex, rgb, hsl) werden inline angezeigt |
| Farbauswahl | Klick auf das Farbquadrat öffnet einen Farbauswahl-Dialog |
| Funktionssignatur | Tooltip mit Parametern beim Tippen von PHP-Funktionen |
| Variablen-Popup | Vorschläge für $variablen beim Tippen (nur Anzeige, kein Autocomplete) |
### Navigation
| Feature | Details |
|---|---|
| Projektbaum | Linkes Panel — zeigt nur das gewählte Projektverzeichnis |
| Neue Datei / Ordner | Rechtsklick im Dateibaum |
| Doppelklick-Navigation | Doppelklick auf einen Funktionsnamen springt zur Definition |
| Suchen & Ersetzen | Einzeln, Alle, In Auswahl, Regex, Live-Hervorhebung (Strg+F) |
| In Dateien suchen | Rekursive Projektsuche mit Ergebnisliste (Strg+Shift+F) |
### Projektanalyse
| Feature | Details |
|---|---|
| Projektfunktionen | Alle definierten Funktionen als durchsuchbare Liste (Strg+Shift+P) |
| Tote Funktionen | Analyse ungenutzter Funktionen mit Exclude-Verzeichnissen (Strg+Shift+T) |
| Export | Analyseergebnisse als TXT exportieren |
### Allgemein
| Feature | Details |
|---|---|
| Speichern | Speichern, Speichern unter, Alles speichern |
| Dark / Light Mode | Umschaltbar (Strg+Shift+D), wird gespeichert |
| Session | Geöffnete Dateien und aktiver Tab werden wiederhergestellt |
---
## Tastaturkürzel
| Aktion | Kürzel |
|---|---|
| Datei öffnen | Strg+O |
| Neue Datei | Strg+N |
| Projekt öffnen | Strg+Shift+O |
| Speichern | Strg+S |
| Speichern unter | Strg+Shift+A |
| Alles speichern | Strg+Shift+S |
| Suchen / Ersetzen | Strg+F |
| In Dateien suchen | Strg+Shift+F |
| Projektfunktionen | Strg+Shift+P |
| Tote Funktionen | Strg+Shift+T |
| Dark Mode | Strg+Shift+D |
---
## Projektstruktur
```
BareCode/
├── CMakeLists.txt
├── main.cpp
├── LICENSE
├── PKGBUILD # Stabiles AUR-Paket
├── PKGBUILD-git # AUR-Paket (Git-Stand)
├── BareCode.desktop # Linux-Anwendungsmenü
├── resources/
│ ├── resources.qrc
│ ├── icon_*.png
│ └── php_functions.json
└── src/
├── core/ # MainWindow, ProjectManager, Settings,
│ # ThemeManager, AboutDialog, IPlugin
├── editor/ # EditorPanel, EditorTab, CodeEditor,
│ # SearchPanel, FileSearchPanel,
│ # ColorIndicator, SignatureHelper,
│ # VariableCompleter, FunctionScanner,
│ # FunctionIndex, FunctionListPanel,
│ # FunctionListDialog, DeadCodeAnalyzer,
│ # DeadCodeDialog
├── filetree/ # FileTreePanel
└── highlighter/ # SyntaxHighlighter, HighlighterFactory
```
Jedes Unterverzeichnis kompiliert als eigene statische Bibliothek.
---
## Abhängigkeiten & Bauen
### Linux
**Ubuntu / Debian:**
```bash
sudo apt install cmake ninja-build qt6-base-dev qt6-base-dev-tools
```
**Arch / Manjaro:**
```bash
sudo pacman -S cmake ninja qt6-base
```
**Bauen:**
```bash
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/BareCode
```
**Installieren:**
```bash
sudo cmake --install build
```
---
### Windows
**Abhängigkeiten:**
- [Qt6](https://www.qt.io/download) — Komponenten: Qt6 Base, Qt6 Concurrent
- CMake ≥ 3.16
- Visual Studio 2019 oder neuer
```bat
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
```
---
### Haiku
```bash
pkgman install qt6_base qt6_base_devel
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
./build/BareCode
```
> Getestet auf Haiku R1/beta5.
---
## Neue Sprache hinzufügen
1. Subklasse von `SyntaxHighlighter` in `src/highlighter/` erstellen
2. `m_rules` im Konstruktor befüllen (siehe `CppHighlighter` als Vorlage)
3. Dateiendung in `HighlighterFactory.cpp` registrieren
---
## Lizenz
MIT License — Copyright (c) 2025 Dany Thinnes / Projekt Hirnfrei
---
## Links
- Website: [www.projekt-hirnfrei.de](https://www.projekt-hirnfrei.de)
- Discord: [discord.projekt-hirnfrei.de](https://discord.projekt-hirnfrei.de)
- Quellcode: [git.projekt-hirnfrei.de/diabolus/BareCode](https://git.projekt-hirnfrei.de/diabolus/BareCode)
- AUR: [aur.archlinux.org/packages/barecode](https://aur.archlinux.org/packages/barecode)

13
barecode/barecode.desktop Normal file
View 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

133
barecode/bauen.sh Executable file
View File

@@ -0,0 +1,133 @@
#!/bin/bash
# ---------------------------------------------------------------------------
# BareCode Build-Skript
# Verwendung: ./bauen.sh [release|debug|clean|install]
# ---------------------------------------------------------------------------
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
BUILD_TYPE="${1:-release}"
BUILD_DIR="build"
# Farben
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}==>${NC} $1"; }
success() { echo -e "${GREEN}==>${NC} $1"; }
warn() { echo -e "${YELLOW}==>${NC} $1"; }
error() { echo -e "${RED}==>${NC} $1"; exit 1; }
# ---------------------------------------------------------------------------
# clean
# ---------------------------------------------------------------------------
if [ "$BUILD_TYPE" = "clean" ]; then
info "Build-Verzeichnis wird gelöscht…"
rm -rf "$BUILD_DIR"
success "Fertig."
exit 0
fi
# ---------------------------------------------------------------------------
# Abhängigkeiten prüfen
# ---------------------------------------------------------------------------
info "Prüfe Abhängigkeiten…"
command -v cmake &>/dev/null || error "cmake nicht gefunden. Installation: sudo pacman -S cmake"
command -v ninja &>/dev/null || error "ninja nicht gefunden. Installation: sudo pacman -S ninja"
command -v lrelease &>/dev/null || \
command -v lrelease-qt6 &>/dev/null || \
error "lrelease nicht gefunden. Installation: sudo pacman -S qt6-tools"
# Qt6 prüfen
if ! pkg-config --exists Qt6Core 2>/dev/null; then
if ! cmake --find-package -DNAME=Qt6 -DCOMPILER_ID=GNU \
-DLANGUAGE=CXX -DMODE=EXIST &>/dev/null 2>&1; then
warn "Qt6 konnte nicht automatisch geprüft werden — cmake wird es versuchen."
fi
fi
success "Abhängigkeiten OK."
# ---------------------------------------------------------------------------
# CMake-Build-Typ
# ---------------------------------------------------------------------------
case "$BUILD_TYPE" in
release|Release)
CMAKE_BUILD_TYPE="Release"
;;
debug|Debug)
CMAKE_BUILD_TYPE="Debug"
;;
install)
CMAKE_BUILD_TYPE="Release"
;;
*)
error "Unbekannter Build-Typ: '$BUILD_TYPE'. Erlaubt: release, debug, clean, install"
;;
esac
# ---------------------------------------------------------------------------
# Konfigurieren
# ---------------------------------------------------------------------------
info "Konfiguriere mit CMake (${CMAKE_BUILD_TYPE})…"
cmake -B "$BUILD_DIR" \
-G Ninja \
-DCMAKE_BUILD_TYPE="$CMAKE_BUILD_TYPE" \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
2>&1 || error "CMake-Konfiguration fehlgeschlagen."
success "Konfiguration OK."
# ---------------------------------------------------------------------------
# Bauen
# ---------------------------------------------------------------------------
JOBS=$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 4)
info "Baue mit $JOBS parallelen Jobs…"
cmake --build "$BUILD_DIR" -j"$JOBS" 2>&1 || error "Build fehlgeschlagen."
success "Build erfolgreich!"
# ---------------------------------------------------------------------------
# Installieren (optional)
# ---------------------------------------------------------------------------
if [ "$BUILD_TYPE" = "install" ]; then
info "Installiere nach /usr (sudo erforderlich)…"
sudo cmake --install "$BUILD_DIR" || error "Installation fehlgeschlagen."
success "BareCode wurde installiert."
echo ""
echo " Starten: BareCode"
exit 0
fi
# ---------------------------------------------------------------------------
# Fertig
# ---------------------------------------------------------------------------
BINARY="$BUILD_DIR/BareCode"
if [ -f "$BINARY" ]; then
# .qm-Dateien neben die Binary kopieren damit sie gefunden werden
mkdir -p "$BUILD_DIR/translations"
if compgen -G "$BUILD_DIR/translations/barecode_*.qm" > /dev/null 2>&1; then
success "Übersetzungen bereits vorhanden."
else
# Aus dem cmake-Zwischenverzeichnis holen
find "$BUILD_DIR" -name "barecode_*.qm" -not -path "$BUILD_DIR/translations/*" \
-exec cp {} "$BUILD_DIR/translations/" \; 2>/dev/null || true
fi
echo ""
success "BareCode ist bereit:"
echo " Starten: ./$BINARY"
echo " Installieren: ./bauen.sh install"
echo " Debug-Build: ./bauen.sh debug"
echo " Aufräumen: ./bauen.sh clean"
fi

106
barecode/main.cpp Normal file
View File

@@ -0,0 +1,106 @@
#include <QApplication>
#include <QIcon>
#include <QTranslator>
#include <QLocale>
#include <QLibraryInfo>
#include <QSettings>
#include <QDir>
#include <QCoreApplication>
#include "core/MainWindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setApplicationName("BareCode");
app.setApplicationVersion("1.2.0");
app.setOrganizationName("BareCode");
// Icon
QIcon appIcon;
appIcon.addFile(":/icon_16.png", QSize(16, 16));
appIcon.addFile(":/icon_32.png", QSize(32, 32));
appIcon.addFile(":/icon_48.png", QSize(48, 48));
appIcon.addFile(":/icon_64.png", QSize(64, 64));
appIcon.addFile(":/icon_128.png", QSize(128, 128));
appIcon.addFile(":/icon_256.png", QSize(256, 256));
appIcon.addFile(":/icon_512.png", QSize(512, 512));
app.setWindowIcon(appIcon);
// ---------------------------------------------------------------------------
// Übersetzung laden
// .qm-Dateien liegen im Unterverzeichnis "translations" neben der Binary
// oder unter /usr/share/BareCode/translations nach Installation
// ---------------------------------------------------------------------------
QSettings settings(QSettings::IniFormat, QSettings::UserScope,
"BareCode", "BareCode");
QString locale = settings.value("language/locale", QString()).toString();
if (locale.isEmpty())
{
locale = QLocale::system().name(); // z.B. "de_DE", "en_US"
}
const QString shortLocale = locale.left(2); // "de", "en"
// Suchpfade für .qm-Dateien
QStringList searchPaths;
searchPaths << QCoreApplication::applicationDirPath() + "/translations"
<< QDir::homePath() + "/.local/share/BareCode/translations"
<< "/usr/share/BareCode/translations"
<< "/usr/local/share/BareCode/translations";
// Qt-eigene Übersetzungen (Dialoge, Standard-Buttons)
QTranslator qtTranslator;
if (qtTranslator.load("qt_" + locale,
QLibraryInfo::path(QLibraryInfo::TranslationsPath)))
{
app.installTranslator(&qtTranslator);
}
// BareCode-Übersetzung — in allen Suchpfaden versuchen
QTranslator appTranslator;
bool loaded = false;
for (const QString &path : searchPaths)
{
if (appTranslator.load(QString("barecode_%1").arg(locale), path) ||
appTranslator.load(QString("barecode_%1").arg(shortLocale), path))
{
app.installTranslator(&appTranslator);
loaded = true;
break;
}
}
// Fallback: Englisch
if (!loaded && shortLocale != "de")
{
for (const QString &path : searchPaths)
{
if (appTranslator.load("barecode_en", path))
{
app.installTranslator(&appTranslator);
break;
}
}
}
MainWindow window;
window.show();
// Datei(en) öffnen, die beim Start per Kommandozeile übergeben wurden
// (z. B. Doppelklick auf eine Datei im Dateibrowser → "Öffnen mit BareCode").
// Das erste Argument ist der Programmname selbst und wird übersprungen.
QStringList cliFiles = app.arguments();
if (!cliFiles.isEmpty())
{
cliFiles.removeFirst();
}
if (!cliFiles.isEmpty())
{
window.openFilesFromArguments(cliFiles);
}
return app.exec();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<RCC>
<qresource prefix="/">
<file>icon_512.png</file>
<file>icon_256.png</file>
<file>icon_128.png</file>
<file>icon_64.png</file>
<file>icon_48.png</file>
<file>icon_32.png</file>
<file>icon_16.png</file>
<file>php_functions.json</file>
</qresource>
</RCC>

View File

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

View File

@@ -0,0 +1,105 @@
#include "AboutDialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QFrame>
#include <QFont>
#include <QApplication>
AboutDialog::AboutDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("Über BareCode"));
setFixedSize(440, 310);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
QVBoxLayout *root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
// -----------------------------------------------------------------------
// Header-Banner
// -----------------------------------------------------------------------
QFrame *banner = new QFrame(this);
banner->setFixedHeight(88);
banner->setStyleSheet(
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0,"
" stop:0 #1a1a2e, stop:1 #16213e);"
);
QVBoxLayout *bannerLayout = new QVBoxLayout(banner);
bannerLayout->setContentsMargins(24, 10, 24, 10);
bannerLayout->setSpacing(2);
QLabel *appName = new QLabel("BareCode", banner);
QFont nameFont = appName->font();
nameFont.setPointSize(22);
nameFont.setBold(true);
appName->setFont(nameFont);
appName->setStyleSheet("color: #e0e0ff; background: transparent;");
QLabel *tagline = new QLabel(tr("Modularer Code-Editor"), banner);
tagline->setStyleSheet("color: #8888bb; background: transparent;");
bannerLayout->addWidget(appName);
bannerLayout->addWidget(tagline);
root->addWidget(banner);
// -----------------------------------------------------------------------
// Info-Tabelle
// -----------------------------------------------------------------------
QVBoxLayout *info = new QVBoxLayout();
info->setContentsMargins(28, 20, 28, 8);
info->setSpacing(10);
auto makeRow = [&](const QString &label, const QString &value)
{
QHBoxLayout *row = new QHBoxLayout();
row->setSpacing(12);
QLabel *lbl = new QLabel(label, this);
QFont boldFont = lbl->font();
boldFont.setBold(true);
lbl->setFont(boldFont);
lbl->setFixedWidth(100);
lbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
QLabel *val = new QLabel(value, this);
val->setTextInteractionFlags(Qt::TextSelectableByMouse);
row->addWidget(lbl);
row->addWidget(val, 1);
info->addLayout(row);
};
makeRow(tr("Version"), "1.2.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);
}

View File

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

View File

@@ -0,0 +1,28 @@
set(CORE_SOURCES
MainWindow.cpp
MainWindow.h
IPlugin.h
ProjectManager.cpp
ProjectManager.h
Settings.cpp
Settings.h
ThemeManager.cpp
ThemeManager.h
AboutDialog.cpp
AboutDialog.h
)
add_library(BareCode_Core STATIC ${CORE_SOURCES})
target_link_libraries(BareCode_Core PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Widgets
BareCode_Editor
BareCode_FileTree
)
target_include_directories(BareCode_Core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
)

View File

@@ -0,0 +1,26 @@
#pragma once
#include <QString>
#include <QWidget>
// ---------------------------------------------------------------------------
// IPlugin Interface that every BareCode module / plugin must implement.
// This allows components to be swapped or extended without touching the core.
// ---------------------------------------------------------------------------
class IPlugin
{
public:
virtual ~IPlugin() = default;
// Human-readable name of the plugin
virtual QString pluginName() const = 0;
// Version string, e.g. "1.0.0"
virtual QString pluginVersion() const = 0;
// Called once after all plugins are loaded so plugins can cross-reference
virtual void initialize() {}
// Called before the application shuts down
virtual void shutdown() {}
};

View File

@@ -0,0 +1,412 @@
#include "MainWindow.h"
#include <QApplication>
#include <QFileDialog>
#include <QMessageBox>
#include <QCloseEvent>
#include <QSettings>
#include <QFileInfo>
#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);
}
// Letzte Session wiederherstellen (geöffnete Dateien + aktiver Tab)
m_editor->restoreSession(
m_settings->lastOpenFiles(),
m_settings->lastActiveFile()
);
}
MainWindow::~MainWindow() = default;
// ---------------------------------------------------------------------------
// Datei(en) von der Kommandozeile öffnen
// ---------------------------------------------------------------------------
void MainWindow::openFilesFromArguments(const QStringList &filePaths)
{
for (const QString &path : filePaths)
{
const QFileInfo info(path);
if (info.exists() && info.isFile())
{
m_editor->openFile(info.absoluteFilePath());
}
}
}
// ---------------------------------------------------------------------------
// UI aufbauen
// ---------------------------------------------------------------------------
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);
m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch);
m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F);
mBearbeiten->addSeparator();
m_actFuncList = mBearbeiten->addAction(tr("&Projektfunktionen…"), this, &MainWindow::onShowFunctionList);
m_actFuncList->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P);
m_actDeadCode = mBearbeiten->addAction(tr("&Tote Funktionen suchen…"), this, &MainWindow::onShowDeadCode);
m_actDeadCode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_T);
// ---- 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);
mAnsicht->addSeparator();
// Sprachauswahl
QMenu *mSprache = mAnsicht->addMenu(tr("Sprache"));
m_langGroup = new QActionGroup(this);
m_langGroup->setExclusive(true);
const QSettings langSettings(QSettings::IniFormat, QSettings::UserScope,
"BareCode", "BareCode");
const QString currentLocale = langSettings.value("language/locale",
QLocale::system().name()).toString();
struct LangEntry { QString locale; QString label; };
const QList<LangEntry> languages = {
{ "de", "Deutsch" },
{ "en", "English" },
};
for (const LangEntry &lang : languages)
{
QAction *act = mSprache->addAction(lang.label);
act->setCheckable(true);
act->setData(lang.locale);
act->setChecked(currentLocale.startsWith(lang.locale));
m_langGroup->addAction(act);
connect(act, &QAction::triggered, this, [this, lang]()
{
onLanguageChanged(lang.locale);
});
}
// ---- 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);
}
void MainWindow::onLanguageChanged(const QString &locale)
{
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
s.setValue("language/locale", locale);
QMessageBox::information(this,
tr("Sprache geändert"),
tr("Die Sprache wird beim nächsten Start von BareCode aktiv.")
);
}
// ---------------------------------------------------------------------------
// Slots Hilfe
// ---------------------------------------------------------------------------
void MainWindow::onAbout()
{
AboutDialog dlg(this);
dlg.exec();
}
// ---------------------------------------------------------------------------
// Slots Projekt
// ---------------------------------------------------------------------------
void MainWindow::onShowDeadCode()
{
m_editor->showDeadCode();
}
void MainWindow::onShowFunctionList()
{
m_editor->showFunctionList();
}
void MainWindow::onShowFileSearch()
{
m_editor->showFileSearchPanel();
}
void MainWindow::onProjectOpened(const QString &path)
{
setWindowTitle(QString("BareCode %1").arg(path));
m_fileTree->setRootPath(path);
m_editor->setSearchRoot(path);
statusBar()->showMessage(tr("Projekt geöffnet: %1").arg(path), 4000);
}
void MainWindow::onProjectClosed()
{
setWindowTitle("BareCode");
m_fileTree->clearRoot();
m_editor->setSearchRoot(QString());
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());
// Session speichern
m_settings->setLastOpenFiles(m_editor->openFilePaths());
m_settings->setLastActiveFile(m_editor->activeFilePath());
}
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();
}

View File

@@ -0,0 +1,99 @@
#pragma once
#include <QMainWindow>
#include <QSplitter>
#include <QMenuBar>
#include <QStatusBar>
#include <QAction>
#include <QActionGroup>
#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;
// Datei(en) öffnen, die beim Programmstart per Kommandozeile übergeben
// wurden — z. B. durch Doppelklick auf eine Datei im Dateibrowser
// ("Öffnen mit BareCode").
void openFilesFromArguments(const QStringList &filePaths);
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
// Datei
void onNewFile();
void onOpenFile();
void onOpenProject();
void onCloseProject();
void onSave();
void onSaveAs();
void onSaveAll();
// Bearbeiten
void onUndo();
void onRedo();
void onShowSearch();
void onShowFileSearch();
void onShowFunctionList();
void onShowDeadCode();
// Ansicht
void onToggleDarkMode(bool checked);
void onLanguageChanged(const QString &locale);
// 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_actFileSearch = nullptr;
QAction *m_actFuncList = nullptr;
QAction *m_actDeadCode = nullptr;
QAction *m_actDarkMode = nullptr;
QActionGroup *m_langGroup = nullptr;
QAction *m_actAbout = nullptr;
};

View File

@@ -0,0 +1,33 @@
#include "ProjectManager.h"
ProjectManager::ProjectManager(QObject *parent)
: QObject(parent)
{
}
QString ProjectManager::currentProjectPath() const
{
return m_projectPath;
}
void ProjectManager::openProject(const QString &path)
{
if (m_projectPath == path)
{
return;
}
m_projectPath = path;
emit projectOpened(m_projectPath);
}
void ProjectManager::closeProject()
{
if (m_projectPath.isEmpty())
{
return;
}
m_projectPath.clear();
emit projectClosed();
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <QObject>
#include <QString>
// ---------------------------------------------------------------------------
// ProjectManager Tracks the currently open project directory and emits
// signals when the project changes so other components can react.
// ---------------------------------------------------------------------------
class ProjectManager : public QObject
{
Q_OBJECT
public:
explicit ProjectManager(QObject *parent = nullptr);
QString currentProjectPath() const;
void openProject(const QString &path);
void closeProject();
signals:
void projectOpened(const QString &path);
void projectClosed();
private:
QString m_projectPath;
};

View File

@@ -0,0 +1,113 @@
#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);
}
// ---------------------------------------------------------------------------
// Session geöffnete Dateien
// ---------------------------------------------------------------------------
QStringList Settings::lastOpenFiles() const
{
return m_settings.value("session/openFiles", QStringList()).toStringList();
}
void Settings::setLastOpenFiles(const QStringList &files)
{
m_settings.setValue("session/openFiles", files);
}
QString Settings::lastActiveFile() const
{
return m_settings.value("session/activeFile", QString()).toString();
}
void Settings::setLastActiveFile(const QString &file)
{
m_settings.setValue("session/activeFile", file);
}

View File

@@ -0,0 +1,52 @@
#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);
// Zuletzt geöffnete Dateien (Session-Wiederherstellung)
QStringList lastOpenFiles() const;
void setLastOpenFiles(const QStringList &files);
QString lastActiveFile() const;
void setLastActiveFile(const QString &file);
// Recent
QString lastProjectPath() const;
void setLastProjectPath(const QString &path);
// Erscheinungsbild
bool darkMode() const;
void setDarkMode(bool dark);
signals:
void settingsChanged();
private:
QSettings m_settings;
};

View File

@@ -0,0 +1,115 @@
#include "ThemeManager.h"
#include <QApplication>
#include <QStyleFactory>
ThemeManager::ThemeManager(QObject *parent)
: QObject(parent)
{
}
void ThemeManager::applyTheme(Theme theme)
{
m_currentTheme = theme;
QApplication::setStyle(QStyleFactory::create("Fusion"));
if (theme == Theme::Dark)
{
QApplication::setPalette(buildDarkPalette());
}
else
{
QApplication::setPalette(buildLightPalette());
}
emit themeChanged(theme);
}
ThemeManager::Theme ThemeManager::currentTheme() const
{
return m_currentTheme;
}
QPalette ThemeManager::buildDarkPalette()
{
QPalette p;
const QColor bg = QColor("#1e1e1e");
const QColor widget = QColor("#252526");
const QColor alt = QColor("#2d2d30");
const QColor hi = QColor("#264f78");
const QColor hiText = QColor("#ffffff");
const QColor text = QColor("#d4d4d4");
const QColor disabled = QColor("#6d6d6d");
const QColor btn = QColor("#3c3c3c");
const QColor mid = QColor("#333333");
const QColor dark = QColor("#1a1a1a");
const QColor light = QColor("#454545");
const QColor link = QColor("#569cd6");
p.setColor(QPalette::Window, bg);
p.setColor(QPalette::WindowText, text);
p.setColor(QPalette::Base, widget);
p.setColor(QPalette::AlternateBase, alt);
p.setColor(QPalette::Text, text);
p.setColor(QPalette::Button, btn);
p.setColor(QPalette::ButtonText, text);
p.setColor(QPalette::Highlight, hi);
p.setColor(QPalette::HighlightedText, hiText);
p.setColor(QPalette::Link, link);
p.setColor(QPalette::LinkVisited, link.darker(120));
p.setColor(QPalette::Mid, mid);
p.setColor(QPalette::Dark, dark);
p.setColor(QPalette::Light, light);
p.setColor(QPalette::Shadow, QColor("#000000"));
p.setColor(QPalette::ToolTipBase, widget);
p.setColor(QPalette::ToolTipText, text);
p.setColor(QPalette::PlaceholderText, disabled);
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled);
p.setColor(QPalette::Disabled, QPalette::Text, disabled);
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled);
return p;
}
QPalette ThemeManager::buildLightPalette()
{
// Fusion-Standard-Palette
QPalette p;
const QColor bg = QColor("#f3f3f3");
const QColor widget = QColor("#ffffff");
const QColor alt = QColor("#e8e8e8");
const QColor hi = QColor("#0078d4");
const QColor hiText = QColor("#ffffff");
const QColor text = QColor("#1e1e1e");
const QColor disabled = QColor("#a0a0a0");
const QColor btn = QColor("#e1e1e1");
const QColor mid = QColor("#c8c8c8");
const QColor dark = QColor("#a0a0a0");
const QColor light = QColor("#ffffff");
const QColor link = QColor("#0078d4");
p.setColor(QPalette::Window, bg);
p.setColor(QPalette::WindowText, text);
p.setColor(QPalette::Base, widget);
p.setColor(QPalette::AlternateBase, alt);
p.setColor(QPalette::Text, text);
p.setColor(QPalette::Button, btn);
p.setColor(QPalette::ButtonText, text);
p.setColor(QPalette::Highlight, hi);
p.setColor(QPalette::HighlightedText, hiText);
p.setColor(QPalette::Link, link);
p.setColor(QPalette::LinkVisited, link.darker(130));
p.setColor(QPalette::Mid, mid);
p.setColor(QPalette::Dark, dark);
p.setColor(QPalette::Light, light);
p.setColor(QPalette::PlaceholderText, disabled);
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled);
p.setColor(QPalette::Disabled, QPalette::Text, disabled);
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled);
return p;
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <QObject>
#include <QPalette>
// ---------------------------------------------------------------------------
// ThemeManager Schaltet zwischen Hell- und Dunkelmodus um.
// ---------------------------------------------------------------------------
class ThemeManager : public QObject
{
Q_OBJECT
public:
enum class Theme { Light, Dark };
explicit ThemeManager(QObject *parent = nullptr);
void applyTheme(Theme theme);
Theme currentTheme() const;
signals:
void themeChanged(Theme theme);
private:
static QPalette buildDarkPalette();
static QPalette buildLightPalette();
Theme m_currentTheme = Theme::Light;
};

View File

@@ -0,0 +1,49 @@
set(EDITOR_SOURCES
EditorPanel.cpp
EditorPanel.h
CodeEditor.cpp
CodeEditor.h
LineNumberArea.cpp
LineNumberArea.h
EditorTab.cpp
EditorTab.h
SearchPanel.cpp
SearchPanel.h
FileSearchPanel.cpp
FileSearchPanel.h
ColorIndicator.cpp
ColorIndicator.h
SignatureHelper.cpp
SignatureHelper.h
SignatureTooltip.cpp
SignatureTooltip.h
FunctionScanner.cpp
FunctionScanner.h
FunctionIndex.cpp
FunctionIndex.h
FunctionListPanel.cpp
FunctionListPanel.h
FunctionListDialog.cpp
FunctionListDialog.h
DeadCodeAnalyzer.cpp
DeadCodeAnalyzer.h
DeadCodeDialog.cpp
DeadCodeDialog.h
VariableCompleter.cpp
VariableCompleter.h
)
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
target_link_libraries(BareCode_Editor PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Concurrent
BareCode_Highlighter
)
target_include_directories(BareCode_Editor PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
)

View File

@@ -0,0 +1,762 @@
#include "CodeEditor.h"
#include "LineNumberArea.h"
#include "ColorIndicator.h"
#include "SignatureHelper.h"
#include "FunctionIndex.h"
#include "VariableCompleter.h"
#include "core/Settings.h"
#include "highlighter/HighlighterFactory.h"
#include <QPainter>
#include <QTextBlock>
#include <QPaintEvent>
#include <QResizeEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QFocusEvent>
#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);
m_colorIndicator = new ColorIndicator(this);
m_signatureHelper = new SignatureHelper(this);
m_varCompleter = new VariableCompleter(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::paintEvent(QPaintEvent *event)
{
// Zuerst den normalen Editor-Inhalt zeichnen
QPlainTextEdit::paintEvent(event);
// Einrück-Führungslinien
const int tabSize = m_settings->tabSize();
if (tabSize > 0)
{
QPainter painter(viewport());
QColor guideColor = palette().color(QPalette::Text);
guideColor.setAlpha(30);
painter.setPen(QPen(guideColor, 1, Qt::SolidLine));
const QFontMetrics fm(font());
const int spaceWidth = fm.horizontalAdvance(' ');
const int tabPixels = tabSize * spaceWidth;
if (tabPixels > 0)
{
int textOriginX = 0;
{
QTextBlock firstBlock = firstVisibleBlock();
if (!firstBlock.isValid())
{
firstBlock = document()->begin();
}
if (firstBlock.isValid())
{
const QRectF blockRect = blockBoundingGeometry(firstBlock)
.translated(contentOffset());
const QTextLayout *layout = firstBlock.layout();
if (layout && layout->lineCount() > 0)
{
textOriginX = static_cast<int>(blockRect.left()
+ layout->lineAt(0).position().x());
}
else
{
textOriginX = static_cast<int>(blockRect.left());
}
}
}
const int scrollX = horizontalScrollBar()->value();
QTextBlock block = firstVisibleBlock();
const int bottom = event->rect().bottom();
while (block.isValid())
{
const QRectF blockRect = blockBoundingGeometry(block)
.translated(contentOffset());
if (blockRect.top() > bottom) { break; }
if (block.isVisible())
{
const QString text = block.text();
int indentSpaces = 0;
for (const QChar &ch : text)
{
if (ch == ' ') { ++indentSpaces; }
else if (ch == '\t') { indentSpaces = ((indentSpaces / tabSize) + 1) * tabSize; }
else { break; }
}
const int indentStops = indentSpaces / tabSize;
for (int stop = 1; stop <= indentStops; ++stop)
{
const int xPixel = textOriginX + stop * tabPixels - scrollX;
if (xPixel < lineNumberAreaWidth() || xPixel > viewport()->width())
{
continue;
}
painter.drawLine(xPixel,
static_cast<int>(blockRect.top()),
xPixel,
static_cast<int>(blockRect.bottom()));
}
}
block = block.next();
}
}
// Farbvorschau-Quadrate zeichnen
m_colorIndicator->paint(painter, event);
}
}
void CodeEditor::setFunctionIndex(FunctionIndex *index)
{
m_functionIndex = index;
}
void CodeEditor::mouseDoubleClickEvent(QMouseEvent *event)
{
// Zuerst normales Verhalten — markiert das Wort unter dem Cursor
QPlainTextEdit::mouseDoubleClickEvent(event);
if (!m_functionIndex || !m_functionIndex->isReady())
{
return;
}
// Markiertes Wort auslesen
const QString word = textCursor().selectedText().trimmed();
if (word.isEmpty() || word.contains(' '))
{
return;
}
// Im Funktionsindex nachschlagen
const FunctionScanner::FunctionInfo info = m_functionIndex->lookup(word);
if (info.filePath.isEmpty())
{
return; // Nicht gefunden — normales Verhalten bleibt
}
// Nicht zur eigenen Definition springen wenn wir bereits dort sind
if (info.filePath == m_filePath && info.line == textCursor().blockNumber() + 1)
{
return;
}
emit navigateToRequested(info.filePath, info.line);
}
void CodeEditor::mousePressEvent(QMouseEvent *event)
{
// Zuerst prüfen ob ein Farbquadrat geklickt wurde
if (m_colorIndicator->handleMousePress(event))
{
return;
}
QPlainTextEdit::mousePressEvent(event);
}
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 + Klammerzugehörigkeit
// ---------------------------------------------------------------------------
void CodeEditor::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly())
{
// Aktuelle Zeile hervorheben
QTextEdit::ExtraSelection lineSelection;
lineSelection.format.setBackground(palette().color(QPalette::AlternateBase));
lineSelection.format.setProperty(QTextFormat::FullWidthSelection, true);
lineSelection.cursor = textCursor();
lineSelection.cursor.clearSelection();
extraSelections.append(lineSelection);
// Klammerzugehörigkeit
matchBrackets(extraSelections);
}
setExtraSelections(extraSelections);
}
void CodeEditor::matchBrackets(QList<QTextEdit::ExtraSelection> &selections)
{
static const QString openBrackets = "({[";
static const QString closeBrackets = ")}]";
QTextCursor cursor = textCursor();
const QString blockText = cursor.block().text();
const int col = cursor.columnNumber();
// Zeichen unter oder links vom Cursor prüfen
QChar ch;
int charPos = -1;
// Zuerst Zeichen unter dem Cursor
if (col < blockText.length())
{
ch = blockText[col];
if (openBrackets.contains(ch) || closeBrackets.contains(ch))
{
charPos = col;
}
}
// Dann Zeichen links vom Cursor
if (charPos == -1 && col > 0)
{
ch = blockText[col - 1];
if (openBrackets.contains(ch) || closeBrackets.contains(ch))
{
charPos = col - 1;
}
}
if (charPos == -1)
{
return;
}
// Passende Klammer suchen
const bool isOpen = openBrackets.contains(ch);
const int bracketIndex = isOpen
? openBrackets.indexOf(ch)
: closeBrackets.indexOf(ch);
const QChar matchChar = isOpen
? closeBrackets[bracketIndex]
: openBrackets[bracketIndex];
// Dokumentposition der gefundenen Klammer
const int startPos = cursor.block().position() + charPos;
// Passende Klammer suchen — vorwärts oder rückwärts
int depth = 0;
int matchPos = -1;
if (isOpen)
{
// Vorwärts suchen
QTextCursor search(document());
search.setPosition(startPos);
while (!search.atEnd())
{
search.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
const QChar c = search.selectedText()[0];
search.clearSelection();
if (c == ch) { ++depth; }
else if (c == matchChar)
{
--depth;
if (depth == 0)
{
matchPos = search.position() - 1;
break;
}
}
}
}
else
{
// Rückwärts suchen
QTextCursor search(document());
search.setPosition(startPos + 1);
while (search.position() > 0)
{
search.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
const QChar c = search.selectedText()[0];
search.clearSelection();
if (c == ch) { ++depth; }
else if (c == matchChar)
{
--depth;
if (depth == 0)
{
matchPos = search.position();
break;
}
}
}
}
// Formatierung
QTextCharFormat matchFormat;
if (matchPos >= 0)
{
// Gefunden — beide Klammern grün hervorheben
matchFormat.setBackground(QColor("#1a5a1a"));
matchFormat.setForeground(QColor("#88ff88"));
matchFormat.setFontWeight(QFont::Bold);
}
else
{
// Kein Match — rot markieren
matchFormat.setBackground(QColor("#5a1a1a"));
matchFormat.setForeground(QColor("#ff8888"));
matchFormat.setFontWeight(QFont::Bold);
}
// Öffnende / schließende Klammer markieren
QTextEdit::ExtraSelection sel1;
sel1.cursor = QTextCursor(document());
sel1.cursor.setPosition(startPos);
sel1.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
sel1.format = matchFormat;
selections.append(sel1);
// Passende Klammer markieren (nur wenn gefunden)
if (matchPos >= 0)
{
QTextEdit::ExtraSelection sel2;
sel2.cursor = QTextCursor(document());
sel2.cursor.setPosition(matchPos);
sel2.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
sel2.format = matchFormat;
selections.append(sel2);
}
}
// ---------------------------------------------------------------------------
// Key handling Tab/Shift+Tab, Smart-Backspace, Auto-Indent
// ---------------------------------------------------------------------------
void CodeEditor::keyPressEvent(QKeyEvent *event)
{
const int tabSize = m_settings->tabSize();
// -----------------------------------------------------------------------
// Shift+Tab: Einrückung zurückziehen
// -----------------------------------------------------------------------
if (event->key() == Qt::Key_Backtab ||
(event->key() == Qt::Key_Tab && event->modifiers() & Qt::ShiftModifier))
{
QTextCursor cursor = textCursor();
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
// Wenn die Selektion genau am Anfang des letzten Blocks endet,
// diesen Block nicht mit einbeziehen — der Cursor steht dort nur
// mit Position 0, der Nutzer hat die Zeile nicht markiert
if (cursor.hasSelection() &&
cursor.selectionEnd() == endBlock.position() &&
endBlock != startBlock)
{
endBlock = endBlock.previous();
}
cursor.beginEditBlock();
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
{
const QString lineText = b.text();
int toRemove = 0;
if (m_settings->useSpacesForTabs())
{
for (int i = 0; i < tabSize && i < lineText.length(); ++i)
{
if (lineText[i] == ' ') { ++toRemove; }
else { break; }
}
}
else
{
if (!lineText.isEmpty() && lineText[0] == '\t')
{
toRemove = 1;
}
}
if (toRemove > 0)
{
QTextCursor lineCursor(b);
lineCursor.movePosition(QTextCursor::StartOfBlock);
lineCursor.movePosition(QTextCursor::Right,
QTextCursor::KeepAnchor,
toRemove);
lineCursor.removeSelectedText();
}
}
cursor.endEditBlock();
return;
}
// -----------------------------------------------------------------------
// Tab: Einrücken (Leerzeichen oder echter Tab)
// -----------------------------------------------------------------------
if (event->key() == Qt::Key_Tab)
{
QTextCursor cursor = textCursor();
if (cursor.hasSelection())
{
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
// Gleiche Korrektur: Cursor am Zeilenanfang → Zeile nicht einrücken
if (cursor.selectionEnd() == endBlock.position() &&
endBlock != startBlock)
{
endBlock = endBlock.previous();
}
cursor.beginEditBlock();
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
{
QTextCursor lineCursor(b);
lineCursor.movePosition(QTextCursor::StartOfBlock);
if (m_settings->useSpacesForTabs())
{
lineCursor.insertText(QString(tabSize, ' '));
}
else
{
lineCursor.insertText("\t");
}
}
cursor.endEditBlock();
}
else
{
if (m_settings->useSpacesForTabs())
{
// Zum nächsten Tab-Stop auffüllen
const int col = cursor.columnNumber();
const int spacesNeeded = tabSize - (col % tabSize);
cursor.insertText(QString(spacesNeeded, ' '));
}
else
{
cursor.insertText("\t");
}
}
return;
}
// -----------------------------------------------------------------------
// Smart Backspace: springt zur vorherigen Einrückungsstufe
// -----------------------------------------------------------------------
if (event->key() == Qt::Key_Backspace
&& !textCursor().hasSelection()
&& m_settings->useSpacesForTabs())
{
QTextCursor cursor = textCursor();
const int col = cursor.columnNumber();
if (col > 0)
{
// Prüfen ob links vom Cursor nur Leerzeichen bis Zeilenbeginn stehen
const QString lineText = cursor.block().text();
const QString leftOfCursor = lineText.left(col);
const bool onlySpaces = leftOfCursor.trimmed().isEmpty();
if (onlySpaces && col > 0)
{
// Zur vorherigen Tab-Stop-Position springen
const int targetCol = ((col - 1) / tabSize) * tabSize;
const int toDelete = col - targetCol;
cursor.movePosition(QTextCursor::Left,
QTextCursor::KeepAnchor,
toDelete);
cursor.removeSelectedText();
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();
// Führende Leerzeichen der aktuellen Zeile zählen
int leadingSpaces = 0;
for (const QChar &ch : currentLine)
{
if (ch == ' ')
{
++leadingSpaces;
}
else if (ch == '\t')
{
leadingSpaces += tabSize;
}
else
{
break;
}
}
QPlainTextEdit::keyPressEvent(event);
if (leadingSpaces > 0)
{
const QString indent = m_settings->useSpacesForTabs()
? QString(leadingSpaces, ' ')
: QString(leadingSpaces / tabSize, '\t');
textCursor().insertText(indent);
}
return;
}
QPlainTextEdit::keyPressEvent(event);
// Variablen-Popup nach jedem Tastendruck aktualisieren
m_varCompleter->handleKeyPress(event);
}
// ---------------------------------------------------------------------------
// Fokus verloren — z. B. beim Wechsel zu einem anderen Tab.
// Ein noch offenes Variablen-Popup muss hier geschlossen werden, sonst
// bleibt es als verwaistes Fenster stehen und kann bei mehreren offenen
// Dateien den Fokus blockieren.
// ---------------------------------------------------------------------------
void CodeEditor::focusOutEvent(QFocusEvent *event)
{
QPlainTextEdit::focusOutEvent(event);
m_varCompleter->notifyFocusLost();
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include <QPlainTextEdit>
#include <QFont>
#include <QString>
#include <QTextBlock>
class LineNumberArea;
class Settings;
class SyntaxHighlighter;
class ColorIndicator;
class SignatureHelper;
class FunctionIndex;
class VariableCompleter;
// ---------------------------------------------------------------------------
// 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;
// Öffentliche Hilfsmethoden für ColorIndicator
// (die Qt-Originale sind protected und von außen nicht erreichbar)
QTextBlock firstVisibleBlockPublic() const { return firstVisibleBlock(); }
QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); }
QPointF contentOffsetPublic() const { return contentOffset(); }
void setFunctionIndex(FunctionIndex *index);
signals:
void fileSaved(const QString &filePath);
void navigateToRequested(const QString &filePath, int line);
protected:
void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void focusOutEvent(QFocusEvent *event) override;
private slots:
void updateLineNumberAreaWidth(int newBlockCount);
void highlightCurrentLine();
void updateLineNumberArea(const QRect &rect, int dy);
private:
void setupEditor();
void installHighlighter(const QString &filePath);
bool writeToFile(const QString &filePath);
void matchBrackets(QList<QTextEdit::ExtraSelection> &selections);
Settings *m_settings = nullptr;
LineNumberArea *m_lineNumberArea = nullptr;
SyntaxHighlighter *m_highlighter = nullptr;
ColorIndicator *m_colorIndicator = nullptr;
SignatureHelper *m_signatureHelper = nullptr;
FunctionIndex *m_functionIndex = nullptr;
VariableCompleter *m_varCompleter = nullptr;
QString m_filePath;
};

View File

@@ -0,0 +1,361 @@
#include "ColorIndicator.h"
#include "CodeEditor.h"
#include <QPainter>
#include <QPaintEvent>
#include <QMouseEvent>
#include <QColorDialog>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
#include <QScrollBar>
#include <QHash>
// ---------------------------------------------------------------------------
// Kombinierter Regex — erfasst alle CSS-Farbformate in einer Runde
// ---------------------------------------------------------------------------
const QRegularExpression ColorIndicator::s_colorRegex(
// #rgb / #rrggbb / #rrggbbaa
R"(#(?:[0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})(?=[^0-9A-Fa-f]|$))"
R"(|rgba?\s*\([^)]+\))"
R"(|hsla?\s*\([^)]+\))",
QRegularExpression::CaseInsensitiveOption
);
// ---------------------------------------------------------------------------
// Konstruktor
// ---------------------------------------------------------------------------
ColorIndicator::ColorIndicator(CodeEditor *editor)
: QObject(editor)
, m_editor(editor)
{
// Cache invalidieren und Viewport neu zeichnen wenn sich der Text ändert
connect(m_editor->document(), &QTextDocument::contentsChanged,
this, [this]()
{
m_cacheFirstBlock = -1;
m_cacheLastBlock = -1;
m_editor->viewport()->update();
});
// Auch beim Scrollen neu zeichnen (Cache bleibt gültig, nur Position ändert sich)
connect(m_editor->verticalScrollBar(), &QScrollBar::valueChanged,
this, [this]()
{
m_cacheFirstBlock = -1;
m_cacheLastBlock = -1;
});
}
// ---------------------------------------------------------------------------
// Paint wird aus CodeEditor::paintEvent aufgerufen
// ---------------------------------------------------------------------------
void ColorIndicator::paint(QPainter &painter, QPaintEvent *event)
{
rebuildCache();
const int squareSize = m_editor->fontMetrics().height() - 4;
const int radius = 2;
for (const ColorMatch &m : m_cache)
{
if (!event->rect().intersects(m.rect))
{
continue;
}
// Rahmen
painter.setPen(QColor(0, 0, 0, 80));
painter.setBrush(m.color);
painter.drawRoundedRect(m.rect, radius, radius);
// Schachbrettmuster als Hintergrund für transparente Farben
if (m.color.alpha() < 255)
{
const int half = squareSize / 2;
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(180, 180, 180));
painter.drawRect(m.rect.x(), m.rect.y(), half, half);
painter.drawRect(m.rect.x() + half, m.rect.y() + half, half, half);
painter.setBrush(m.color);
painter.drawRoundedRect(m.rect, radius, radius);
}
}
}
// ---------------------------------------------------------------------------
// Mouse wird aus CodeEditor::mousePressEvent aufgerufen
// ---------------------------------------------------------------------------
bool ColorIndicator::handleMousePress(QMouseEvent *event)
{
for (const ColorMatch &m : m_cache)
{
if (!m.rect.contains(event->pos()))
{
continue;
}
// Farb-Dialog öffnen
QColorDialog dlg(m.color, m_editor);
dlg.setOption(QColorDialog::ShowAlphaChannel, true);
dlg.setWindowTitle(QObject::tr("Farbe wählen"));
if (dlg.exec() != QDialog::Accepted)
{
return true;
}
const QColor newColor = dlg.selectedColor();
// Ursprünglichen Farbwert im Dokument ersetzen
QTextBlock block = m_editor->document()->findBlockByNumber(m.blockNumber);
if (!block.isValid())
{
return true;
}
// Neuen Farbwert als Hex-String formatieren
QString newValue;
if (newColor.alpha() < 255)
{
newValue = newColor.name(QColor::HexArgb); // #aarrggbb
// CSS erwartet #rrggbbaa — Bytes umstellen
// Qt liefert #aarrggbb, CSS will #rrggbbaa
newValue = QString("#%1%2%3%4")
.arg(newColor.red(), 2, 16, QChar('0'))
.arg(newColor.green(), 2, 16, QChar('0'))
.arg(newColor.blue(), 2, 16, QChar('0'))
.arg(newColor.alpha(), 2, 16, QChar('0'));
}
else
{
newValue = newColor.name(QColor::HexRgb); // #rrggbb
}
QTextCursor cursor(block);
cursor.setPosition(block.position() + m.posInBlock);
cursor.setPosition(block.position() + m.posInBlock + m.length,
QTextCursor::KeepAnchor);
cursor.insertText(newValue);
// Cache invalidieren
m_cache.clear();
m_cacheFirstBlock = -1;
m_cacheLastBlock = -1;
return true;
}
return false;
}
// ---------------------------------------------------------------------------
// Cache aufbauen nur für sichtbare Blöcke
// ---------------------------------------------------------------------------
void ColorIndicator::rebuildCache()
{
QTextBlock firstVisible = m_editor->firstVisibleBlockPublic();
const int firstNum = firstVisible.blockNumber();
// Letzten sichtbaren Block bestimmen
int lastNum = firstNum;
{
QTextBlock b = firstVisible;
const int bot = m_editor->viewport()->height();
while (b.isValid())
{
const QRectF r = m_editor->blockBoundingGeometryPublic(b)
.translated(m_editor->contentOffsetPublic());
if (r.top() > bot)
{
break;
}
lastNum = b.blockNumber();
b = b.next();
}
}
// Cache noch aktuell?
if (firstNum == m_cacheFirstBlock && lastNum == m_cacheLastBlock)
{
return;
}
m_cache.clear();
m_cacheFirstBlock = firstNum;
m_cacheLastBlock = lastNum;
const int squareSize = m_editor->fontMetrics().height() - 4;
const int scrollX = m_editor->horizontalScrollBar()->value();
QTextBlock block = firstVisible;
while (block.isValid() && block.blockNumber() <= lastNum)
{
const QRectF blockRect = m_editor->blockBoundingGeometryPublic(block)
.translated(m_editor->contentOffsetPublic());
const QList<ColorMatch> found = findColorsInBlock(block.text(),
block.blockNumber());
for (ColorMatch m : found)
{
// X-Position des Farbwerts im Viewport berechnen
const QTextLayout *layout = block.layout();
if (!layout || layout->lineCount() == 0)
{
continue;
}
const QTextLine line = layout->lineAt(0);
// Position nach dem Ende des Farbwerts
const qreal endCharX = line.cursorToX(m.posInBlock + m.length);
const int x = static_cast<int>(blockRect.left() + endCharX)
- scrollX + 3;
if (x + squareSize > m_editor->viewport()->width())
{
continue;
}
const int y = static_cast<int>(blockRect.top())
+ (static_cast<int>(blockRect.height()) - squareSize) / 2;
m.rect = QRect(x, y, squareSize, squareSize);
m_cache.append(m);
}
block = block.next();
}
}
// ---------------------------------------------------------------------------
// Farbwerte in einer Zeile suchen
// ---------------------------------------------------------------------------
QList<ColorIndicator::ColorMatch> ColorIndicator::findColorsInBlock(
const QString &text, int blockNumber) const
{
QList<ColorMatch> result;
QRegularExpressionMatchIterator it = s_colorRegex.globalMatch(text);
while (it.hasNext())
{
QRegularExpressionMatch match = it.next();
const QString token = match.captured(0);
const QColor color = parseColor(token);
if (!color.isValid())
{
continue;
}
ColorMatch m;
m.blockNumber = blockNumber;
m.posInBlock = static_cast<int>(match.capturedStart());
m.length = static_cast<int>(match.capturedLength());
m.color = color;
result.append(m);
}
return result;
}
// ---------------------------------------------------------------------------
// Farb-Parser
// ---------------------------------------------------------------------------
QColor ColorIndicator::parseColor(const QString &token)
{
const QString t = token.trimmed();
if (t.startsWith('#')) { return parseHex(t); }
if (t.startsWith("rgba", Qt::CaseInsensitive)) { return parseRgba(t); }
if (t.startsWith("rgb", Qt::CaseInsensitive)) { return parseRgb(t); }
if (t.startsWith("hsla", Qt::CaseInsensitive)) { return parseHsla(t); }
if (t.startsWith("hsl", Qt::CaseInsensitive)) { return parseHsl(t); }
return QColor();
}
QColor ColorIndicator::parseHex(const QString &s)
{
// #rgb → #rrggbb
if (s.length() == 4)
{
return QColor(QString("#%1%1%2%2%3%3")
.arg(s[1]).arg(s[2]).arg(s[3]));
}
// #rrggbb
if (s.length() == 7)
{
return QColor(s);
}
// #rrggbbaa (CSS) → Qt braucht #aarrggbb
if (s.length() == 9)
{
const QString rr = s.mid(1, 2);
const QString gg = s.mid(3, 2);
const QString bb = s.mid(5, 2);
const QString aa = s.mid(7, 2);
return QColor(QString("#%1%2%3%4").arg(aa, rr, gg, bb));
}
return QColor();
}
QColor ColorIndicator::parseRgb(const QString &s)
{
// rgb(r, g, b)
static const QRegularExpression re(
R"(rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch m = re.match(s);
if (!m.hasMatch()) { return QColor(); }
return QColor(m.captured(1).toInt(),
m.captured(2).toInt(),
m.captured(3).toInt());
}
QColor ColorIndicator::parseRgba(const QString &s)
{
// rgba(r, g, b, a) — a ist 0.01.0
static const QRegularExpression re(
R"(rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([0-9.]+)\s*\))",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch m = re.match(s);
if (!m.hasMatch()) { return QColor(); }
return QColor(m.captured(1).toInt(),
m.captured(2).toInt(),
m.captured(3).toInt(),
qRound(m.captured(4).toDouble() * 255.0));
}
QColor ColorIndicator::parseHsl(const QString &s)
{
// hsl(h, s%, l%)
static const QRegularExpression re(
R"(hsl\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\))",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch m = re.match(s);
if (!m.hasMatch()) { return QColor(); }
QColor c;
c.setHsl(m.captured(1).toInt(),
qRound(m.captured(2).toInt() * 2.55),
qRound(m.captured(3).toInt() * 2.55));
return c;
}
QColor ColorIndicator::parseHsla(const QString &s)
{
// hsla(h, s%, l%, a)
static const QRegularExpression re(
R"(hsla\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([0-9.]+)\s*\))",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch m = re.match(s);
if (!m.hasMatch()) { return QColor(); }
QColor c;
c.setHsl(m.captured(1).toInt(),
qRound(m.captured(2).toInt() * 2.55),
qRound(m.captured(3).toInt() * 2.55),
qRound(m.captured(4).toDouble() * 255.0));
return c;
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include <QObject>
#include <QColor>
#include <QRect>
#include <QList>
#include <QRegularExpression>
#include <QString>
class CodeEditor;
class QPainter;
class QPaintEvent;
class QMouseEvent;
// ---------------------------------------------------------------------------
// ColorIndicator Zeichnet kleine Farbquadrate neben CSS-Farbwerten und
// öffnet einen QColorDialog wenn der Nutzer darauf klickt.
//
// Unterstützte Formate:
// #rgb #rrggbb #rrggbbaa
// rgb(r, g, b) rgba(r, g, b, a)
// hsl(h, s%, l%) hsla(h, s%, l%, a)
// 140 benannte CSS-Farben (red, blue, cornflowerblue, ...)
// ---------------------------------------------------------------------------
class ColorIndicator : public QObject
{
Q_OBJECT
public:
explicit ColorIndicator(CodeEditor *editor);
// Wird aus CodeEditor::paintEvent aufgerufen
void paint(QPainter &painter, QPaintEvent *event);
// Wird aus CodeEditor::mousePressEvent aufgerufen
// Gibt true zurück wenn der Klick auf einem Farbquadrat war
bool handleMousePress(QMouseEvent *event);
private:
struct ColorMatch
{
QRect rect; // Position des Quadrats im Viewport
QColor color; // Erkannte Farbe
int blockNumber;
int posInBlock; // Zeichenposition des Farbwerts im Block
int length; // Länge des Farbwerts im Text
};
void rebuildCache();
QList<ColorMatch> findColorsInBlock(const QString &text,
int blockNumber) const;
static QColor parseColor(const QString &token);
static QColor parseHex(const QString &s);
static QColor parseRgb(const QString &s);
static QColor parseRgba(const QString &s);
static QColor parseHsl(const QString &s);
static QColor parseHsla(const QString &s);
CodeEditor *m_editor = nullptr;
QList<ColorMatch> m_cache;
int m_cacheFirstBlock = -1;
int m_cacheLastBlock = -1;
// Kombinierter Regex für alle Farbformate
static const QRegularExpression s_colorRegex;
};

View File

@@ -0,0 +1,181 @@
#include "DeadCodeAnalyzer.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QSet>
#include <QFileInfo>
DeadCodeAnalyzer::DeadCodeAnalyzer(QObject *parent)
: QObject(parent)
{
m_scanner = new FunctionScanner(this);
}
QList<DeadCodeAnalyzer::DeadFunction> DeadCodeAnalyzer::analyze(
const QString &projectRoot,
const QStringList &extensions,
const QStringList &excludeDirs) const
{
// -----------------------------------------------------------------------
// Schritt 1: Alle Dateien sammeln (mit Exclude-Filter)
// -----------------------------------------------------------------------
QStringList allFiles;
QDirIterator it(projectRoot, extensions, QDir::Files,
QDirIterator::Subdirectories);
while (it.hasNext())
{
const QString path = it.next();
// Exclude-Verzeichnisse prüfen
bool excluded = false;
if (!excludeDirs.isEmpty())
{
const QStringList parts = path.split('/');
for (const QString &excl : excludeDirs)
{
if (parts.contains(excl.trimmed(), Qt::CaseInsensitive))
{
excluded = true;
break;
}
}
}
if (!excluded)
{
allFiles.append(path);
}
}
const int totalFiles = allFiles.size();
// -----------------------------------------------------------------------
// Schritt 2: Alle Funktionsdefinitionen aus nicht-excluded Dateien
// -----------------------------------------------------------------------
QHash<QString, FunctionScanner::FunctionInfo> definitions;
for (const QString &path : allFiles)
{
const QList<FunctionScanner::FunctionInfo> funcs = m_scanner->scanFile(path);
for (const FunctionScanner::FunctionInfo &func : funcs)
{
if (func.name.startsWith("__"))
{
continue;
}
const QString key = func.name.toLower();
if (!definitions.contains(key))
{
definitions.insert(key, func);
}
}
}
if (definitions.isEmpty())
{
return {};
}
// -----------------------------------------------------------------------
// Schritt 3: Alle Dateien EINMAL lesen, alle Aufrufe sammeln
// -----------------------------------------------------------------------
QSet<QString> calledFunctions;
static const QRegularExpression identifierRegex(
R"((?:->|::|\b)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()"
R"(|['"]([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)['"])"
);
static const QRegularExpression defLineRegex(
R"(\bfunction\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))"
);
int filesScanned = 0;
for (const QString &filePath : allFiles)
{
++filesScanned;
// Fortschritt signalisieren
emit progressUpdate(filesScanned, totalFiles,
QFileInfo(filePath).fileName());
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
continue;
}
QTextStream stream(&file);
stream.setEncoding(QStringConverter::Utf8);
while (!stream.atEnd())
{
const QString line = stream.readLine();
const QString trimmed = line.trimmed();
if (trimmed.startsWith("//") ||
trimmed.startsWith("*") ||
trimmed.startsWith("#") ||
trimmed.startsWith("/*"))
{
continue;
}
// Definitionen auf dieser Zeile nicht als Aufruf werten
QSet<QString> definedOnThisLine;
{
QRegularExpressionMatchIterator dit = defLineRegex.globalMatch(line);
while (dit.hasNext())
{
definedOnThisLine.insert(dit.next().captured(1).toLower());
}
}
QRegularExpressionMatchIterator mit = identifierRegex.globalMatch(line);
while (mit.hasNext())
{
const QRegularExpressionMatch match = mit.next();
const QString name = match.captured(1).isEmpty()
? match.captured(2).toLower()
: match.captured(1).toLower();
if (name.isEmpty() || definedOnThisLine.contains(name))
{
continue;
}
if (definitions.contains(name))
{
calledFunctions.insert(name);
}
}
}
}
// -----------------------------------------------------------------------
// Schritt 4: Nicht aufgerufene Funktionen zurückgeben
// -----------------------------------------------------------------------
QList<DeadFunction> dead;
for (auto it2 = definitions.begin(); it2 != definitions.end(); ++it2)
{
if (!calledFunctions.contains(it2.key()))
{
const FunctionScanner::FunctionInfo &func = it2.value();
DeadFunction d;
d.info = func;
d.reason = func.className.isEmpty()
? QObject::tr("Globale Funktion — kein Aufruf gefunden")
: QObject::tr("Methode von %1 — kein Aufruf gefunden")
.arg(func.className);
dead.append(d);
}
}
return dead;
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
#include <QList>
#include "FunctionScanner.h"
// ---------------------------------------------------------------------------
// DeadCodeAnalyzer Findet Funktionen die definiert aber nie aufgerufen werden.
// ---------------------------------------------------------------------------
class DeadCodeAnalyzer : public QObject
{
Q_OBJECT
public:
struct DeadFunction
{
FunctionScanner::FunctionInfo info;
QString reason;
};
explicit DeadCodeAnalyzer(QObject *parent = nullptr);
// excludeDirs: Verzeichnisnamen die komplett übersprungen werden
// z.B. {"vendor", "lib", "node_modules"}
QList<DeadFunction> analyze(const QString &projectRoot,
const QStringList &extensions = {"*.php"},
const QStringList &excludeDirs = {}) const;
signals:
// Fortschritt während der Analyse (läuft im Thread)
void progressUpdate(int filesScanned, int filesTotal,
const QString &currentFile) const;
private:
FunctionScanner *m_scanner = nullptr;
};

View File

@@ -0,0 +1,461 @@
#include "DeadCodeDialog.h"
#include <QtConcurrent/QtConcurrent>
#include <QFileInfo>
#include <QDir>
#include <QFont>
#include <QSettings>
#include <QCloseEvent>
#include <QFileDialog>
#include <QTextStream>
#include <QMessageBox>
#include <QHeaderView>
#include <QDateTime>
#include <QMetaObject>
DeadCodeDialog::DeadCodeDialog(QWidget *parent)
: QDialog(parent, Qt::Window)
{
setWindowTitle(tr("Tote Funktionen BareCode"));
setMinimumSize(650, 500);
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
if (s.contains("deadcode/geometry"))
{
restoreGeometry(s.value("deadcode/geometry").toByteArray());
}
else
{
resize(780, 620);
}
m_analyzer = new DeadCodeAnalyzer(this);
m_watcher = new QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>>(this);
connect(m_watcher, &QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>>::finished,
this, &DeadCodeDialog::onAnalysisFinished);
// Fortschritts-Signal aus dem Analyzer-Thread sicher in den UI-Thread leiten
connect(m_analyzer, &DeadCodeAnalyzer::progressUpdate,
this, &DeadCodeDialog::onProgressUpdate,
Qt::QueuedConnection);
setupUi();
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
void DeadCodeDialog::setupUi()
{
QVBoxLayout *root = new QVBoxLayout(this);
root->setSpacing(8);
root->setContentsMargins(12, 12, 12, 12);
// ---- Warnbox ----
QFrame *warningBox = new QFrame(this);
warningBox->setStyleSheet(
"QFrame {"
" background: #3a2a00;"
" border: 1px solid #7a5a00;"
" border-radius: 4px;"
"}"
);
QHBoxLayout *warnLayout = new QHBoxLayout(warningBox);
warnLayout->setContentsMargins(8, 6, 8, 6);
QLabel *warnIcon = new QLabel(warningBox);
warnIcon->setFixedSize(20, 20);
warnIcon->setStyleSheet(
"background: #ffcc00;"
"color: #1a1a00;"
"font-weight: bold;"
"font-size: 13px;"
"border-radius: 10px;"
);
warnIcon->setText("!");
warnIcon->setAlignment(Qt::AlignCenter);
warnLayout->addWidget(warnIcon);
QLabel *warnText = new QLabel(
tr("Kandidatenliste — kein Aufruf per Regex gefunden. "
"Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. "
"Bitte vor dem Löschen manuell prüfen."),
warningBox
);
warnText->setWordWrap(true);
warnText->setStyleSheet("color: #ffcc88;");
warnLayout->addWidget(warnText, 1);
root->addWidget(warningBox);
// ---- Exclude-Verzeichnisse ----
QHBoxLayout *excludeRow = new QHBoxLayout();
excludeRow->addWidget(new QLabel(tr("Ausschließen:"), this));
m_excludeEdit = new QLineEdit(this);
m_excludeEdit->setPlaceholderText(tr("vendor lib node_modules (leerzeichen-getrennt)"));
m_excludeEdit->setToolTip(tr(
"Verzeichnisnamen die bei der Analyse übersprungen werden.\n"
"Leerzeichen-getrennt, z.B.: vendor lib node_modules cache"
));
// Letzte Einstellung wiederherstellen
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
m_excludeEdit->setText(s.value("deadcode/excludeDirs", "vendor lib node_modules").toString());
excludeRow->addWidget(m_excludeEdit, 1);
// Verzeichnis-Auswahl Button
QPushButton *btnBrowse = new QPushButton(tr("+ Verzeichnis"), this);
btnBrowse->setToolTip(tr("Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen"));
connect(btnBrowse, &QPushButton::clicked, this, [this]()
{
const QString startPath = m_projectRoot.isEmpty()
? QDir::homePath()
: m_projectRoot;
const QString dir = QFileDialog::getExistingDirectory(
this,
tr("Verzeichnis ausschließen"),
startPath,
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks
);
if (dir.isEmpty())
{
return;
}
// Nur den letzten Verzeichnisnamen verwenden, nicht den vollen Pfad
// So funktioniert der Exclude auch für gleichnamige Unterverzeichnisse
const QString dirName = QFileInfo(dir).fileName();
// Prüfen ob bereits in der Liste
const QStringList current = m_excludeEdit->text()
.simplified()
.split(' ', Qt::SkipEmptyParts);
if (current.contains(dirName, Qt::CaseInsensitive))
{
return;
}
// Anhängen
QString newText = m_excludeEdit->text().trimmed();
if (!newText.isEmpty())
{
newText += ' ';
}
newText += dirName;
m_excludeEdit->setText(newText);
});
excludeRow->addWidget(btnBrowse);
// Löschen-Button — ganzes Feld leeren
QPushButton *btnClear = new QPushButton(tr(""), this);
btnClear->setFixedWidth(28);
btnClear->setFlat(true);
btnClear->setToolTip(tr("Ausschlussliste leeren"));
connect(btnClear, &QPushButton::clicked, this, [this]()
{
m_excludeEdit->clear();
});
excludeRow->addWidget(btnClear);
root->addLayout(excludeRow);
// ---- Steuerleiste ----
QHBoxLayout *ctrlRow = new QHBoxLayout();
m_btnAnalyze = new QPushButton(tr("Analyse starten"), this);
ctrlRow->addWidget(m_btnAnalyze);
m_btnExport = new QPushButton(tr("Als TXT exportieren"), this);
m_btnExport->setEnabled(false);
ctrlRow->addWidget(m_btnExport);
ctrlRow->addStretch();
m_statusLabel = new QLabel(this);
m_statusLabel->setStyleSheet("color: palette(mid);");
ctrlRow->addWidget(m_statusLabel);
root->addLayout(ctrlRow);
// ---- Fortschrittsbereich ----
m_progress = new QProgressBar(this);
m_progress->setRange(0, 100);
m_progress->setTextVisible(true);
m_progress->setFormat("%v / %m Dateien");
m_progress->hide();
root->addWidget(m_progress);
m_progressLabel = new QLabel(this);
m_progressLabel->setStyleSheet("color: palette(mid); font-size: 11px;");
m_progressLabel->hide();
root->addWidget(m_progressLabel);
// ---- Ergebnistabelle ----
m_results = new QTreeWidget(this);
m_results->setColumnCount(4);
m_results->setHeaderLabels({
tr("Funktion"),
tr("Klasse"),
tr("Datei"),
tr("Zeile")
});
m_results->setRootIsDecorated(false);
m_results->setAlternatingRowColors(true);
m_results->setSortingEnabled(true);
m_results->sortByColumn(0, Qt::AscendingOrder);
m_results->setSelectionMode(QAbstractItemView::SingleSelection);
m_results->header()->setSectionResizeMode(0, QHeaderView::Interactive);
m_results->header()->setSectionResizeMode(1, QHeaderView::Interactive);
m_results->header()->setSectionResizeMode(2, QHeaderView::Stretch);
m_results->header()->setSectionResizeMode(3, QHeaderView::Fixed);
m_results->header()->resizeSection(0, 200);
m_results->header()->resizeSection(1, 120);
m_results->header()->resizeSection(3, 60);
QFont mono("Monospace");
mono.setStyleHint(QFont::Monospace);
m_results->setFont(mono);
root->addWidget(m_results, 1);
// ---- Verbindungen ----
connect(m_btnAnalyze, &QPushButton::clicked, this, &DeadCodeDialog::onAnalyze);
connect(m_btnExport, &QPushButton::clicked, this, &DeadCodeDialog::onExportClicked);
connect(m_results, &QTreeWidget::itemActivated,
this, &DeadCodeDialog::onItemActivated);
}
// ---------------------------------------------------------------------------
// Öffentliche Schnittstelle
// ---------------------------------------------------------------------------
void DeadCodeDialog::setProjectRoot(const QString &path)
{
// Alte Exclude-Liste für das vorherige Projekt speichern
if (!m_projectRoot.isEmpty())
{
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
const QString oldKey = "deadcode/exclude/" +
QString(m_projectRoot).replace('/', '_').replace('\\', '_');
s.setValue(oldKey, m_excludeEdit->text());
}
m_projectRoot = path;
m_results->clear();
m_statusLabel->setText(QString());
m_btnExport->setEnabled(false);
// Exclude-Liste für das neue Projekt laden
if (!path.isEmpty())
{
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
const QString key = "deadcode/exclude/" +
QString(path).replace('/', '_').replace('\\', '_');
// Projektspezifisch vorhanden? Sonst globalen Fallback nehmen
const QString saved = s.value(key,
s.value("deadcode/excludeDirs", "vendor lib node_modules").toString()
).toString();
m_excludeEdit->setText(saved);
}
}
// ---------------------------------------------------------------------------
// Analyse starten
// ---------------------------------------------------------------------------
void DeadCodeDialog::onAnalyze()
{
if (m_projectRoot.isEmpty())
{
QMessageBox::information(this, tr("Analyse"),
tr("Bitte zuerst ein Projekt öffnen."));
return;
}
if (m_watcher->isRunning())
{
m_watcher->cancel();
m_watcher->waitForFinished();
}
// Exclude-Liste projektspezifisch speichern
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
const QString key = "deadcode/exclude/" +
QString(m_projectRoot).replace('/', '_').replace('\\', '_');
s.setValue(key, m_excludeEdit->text());
s.setValue("deadcode/excludeDirs", m_excludeEdit->text()); // globaler Fallback
const QStringList excludeDirs = m_excludeEdit->text()
.simplified()
.split(' ', Qt::SkipEmptyParts);
m_results->clear();
m_btnAnalyze->setEnabled(false);
m_btnExport->setEnabled(false);
m_progress->setValue(0);
m_progress->setMaximum(0); // Unbestimmter Modus bis wir die Dateizahl kennen
m_progress->show();
m_progressLabel->setText(tr("Dateien werden gezählt…"));
m_progressLabel->show();
m_statusLabel->setText(tr("Analysiere…"));
const QString root = m_projectRoot;
DeadCodeAnalyzer *analyzer = m_analyzer;
QFuture<QList<DeadCodeAnalyzer::DeadFunction>> future =
QtConcurrent::run([analyzer, root, excludeDirs]()
{
return analyzer->analyze(root, {"*.php"}, excludeDirs);
});
m_watcher->setFuture(future);
}
// ---------------------------------------------------------------------------
// Fortschritt aktualisieren (QueuedConnection — Thread-sicher)
// ---------------------------------------------------------------------------
void DeadCodeDialog::onProgressUpdate(int filesScanned, int filesTotal,
const QString &currentFile)
{
if (m_progress->maximum() != filesTotal)
{
m_progress->setMaximum(filesTotal);
}
m_progress->setValue(filesScanned);
m_progressLabel->setText(tr("(%1 / %2) %3")
.arg(filesScanned)
.arg(filesTotal)
.arg(currentFile));
}
// ---------------------------------------------------------------------------
// Analyse abgeschlossen
// ---------------------------------------------------------------------------
void DeadCodeDialog::onAnalysisFinished()
{
m_progress->hide();
m_progressLabel->hide();
m_btnAnalyze->setEnabled(true);
if (m_watcher->isCanceled())
{
return;
}
const QList<DeadCodeAnalyzer::DeadFunction> dead = m_watcher->result();
m_results->setSortingEnabled(false);
for (const DeadCodeAnalyzer::DeadFunction &d : dead)
{
QTreeWidgetItem *item = new QTreeWidgetItem(m_results);
item->setText(0, d.info.signature);
item->setText(1, d.info.className.isEmpty() ? tr("(global)") : d.info.className);
item->setText(2, QFileInfo(d.info.filePath).fileName());
item->setText(3, QString::number(d.info.line));
item->setToolTip(0, d.reason);
item->setToolTip(2, d.info.filePath);
item->setData(0, Qt::UserRole, d.info.filePath);
item->setData(0, Qt::UserRole + 1, d.info.line);
if (d.info.className.isEmpty())
{
item->setForeground(1, QColor("#888888"));
}
}
m_results->setSortingEnabled(true);
m_results->sortByColumn(0, Qt::AscendingOrder);
if (dead.isEmpty())
{
m_statusLabel->setText(tr("✓ Keine ungenutzten Funktionen gefunden."));
m_statusLabel->setStyleSheet("color: #44bb44;");
}
else
{
m_statusLabel->setText(tr("%1 Kandidaten gefunden").arg(dead.size()));
m_statusLabel->setStyleSheet("color: palette(mid);");
m_btnExport->setEnabled(true);
}
}
// ---------------------------------------------------------------------------
// Klick auf Treffer
// ---------------------------------------------------------------------------
void DeadCodeDialog::onItemActivated(QTreeWidgetItem *item, int /*column*/)
{
const QString path = item->data(0, Qt::UserRole).toString();
const int line = item->data(0, Qt::UserRole + 1).toInt();
if (!path.isEmpty() && line > 0)
{
emit fileLineRequested(path, line);
}
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
void DeadCodeDialog::onExportClicked()
{
const QString path = QFileDialog::getSaveFileName(
this,
tr("Ergebnis exportieren"),
QString("barecode_deadcode_%1.txt")
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmm")),
tr("Textdateien (*.txt)")
);
if (path.isEmpty()) { return; }
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QMessageBox::warning(this, tr("Export"),
tr("Datei konnte nicht geschrieben werden:\n%1").arg(path));
return;
}
QTextStream out(&file);
out.setEncoding(QStringConverter::Utf8);
out << "BareCode Analyse ungenutzter Funktionen\n";
out << "Projekt: " << m_projectRoot << "\n";
out << "Ausgeschlossen: " << m_excludeEdit->text() << "\n";
out << "Datum: " << QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm") << "\n";
out << QString("").repeated(70) << "\n\n";
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
{
QTreeWidgetItem *item = m_results->topLevelItem(i);
out << item->text(0) << "\n";
out << " Klasse: " << item->text(1) << "\n";
out << " Datei: " << item->toolTip(2) << "\n";
out << " Zeile: " << item->text(3) << "\n\n";
}
out << QString("").repeated(70) << "\n";
out << m_results->topLevelItemCount() << " Kandidaten\n";
out << "Hinweis: Dynamische Aufrufe werden nicht erkannt.\n";
QMessageBox::information(this, tr("Export"),
tr("Exportiert nach:\n%1").arg(path));
}
// ---------------------------------------------------------------------------
// Fenstergeometrie speichern
// ---------------------------------------------------------------------------
void DeadCodeDialog::closeEvent(QCloseEvent *event)
{
if (m_watcher->isRunning())
{
m_watcher->cancel();
}
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
s.setValue("deadcode/geometry", saveGeometry());
event->accept();
}

View File

@@ -0,0 +1,62 @@
#pragma once
#include <QDialog>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QLabel>
#include <QPushButton>
#include <QProgressBar>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFutureWatcher>
#include <QString>
#include "DeadCodeAnalyzer.h"
class DeadCodeDialog : public QDialog
{
Q_OBJECT
public:
explicit DeadCodeDialog(QWidget *parent = nullptr);
void setProjectRoot(const QString &path);
signals:
void fileLineRequested(const QString &filePath, int line);
private slots:
void onAnalyze();
void onAnalysisFinished();
void onProgressUpdate(int filesScanned, int filesTotal,
const QString &currentFile);
void onItemActivated(QTreeWidgetItem *item, int column);
void onExportClicked();
protected:
void closeEvent(QCloseEvent *event) override;
private:
void setupUi();
QString m_projectRoot;
// Einstellungen
QLineEdit *m_excludeEdit = nullptr;
// Steuerung
QPushButton *m_btnAnalyze = nullptr;
QPushButton *m_btnExport = nullptr;
// Fortschritt
QProgressBar *m_progress = nullptr;
QLabel *m_progressLabel = nullptr;
// Status + Ergebnisse
QLabel *m_statusLabel = nullptr;
QTreeWidget *m_results = nullptr;
DeadCodeAnalyzer *m_analyzer = nullptr;
QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>> *m_watcher = nullptr;
};

View File

@@ -0,0 +1,301 @@
#include "EditorPanel.h"
#include "EditorTab.h"
#include "CodeEditor.h"
#include "SearchPanel.h"
#include "FileSearchPanel.h"
#include "FunctionListDialog.h"
#include "DeadCodeDialog.h"
#include "FunctionIndex.h"
#include <QFileInfo>
#include <QFile>
#include <QTextDocument>
#include <QTextBlock>
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_fileSearch = new FileSearchPanel(this);
m_funcDialog = new FunctionListDialog(window());
m_deadCode = new DeadCodeDialog(window());
m_funcIndex = new FunctionIndex(this);
m_layout->addWidget(m_tabWidget, 1);
m_layout->addWidget(m_searchPanel, 0);
m_layout->addWidget(m_fileSearch, 0);
connect(m_tabWidget, &QTabWidget::tabCloseRequested,
this, &EditorPanel::onTabCloseRequested);
connect(m_tabWidget, &QTabWidget::currentChanged,
this, &EditorPanel::onCurrentTabChanged);
connect(m_fileSearch, &FileSearchPanel::fileLineRequested,
this, &EditorPanel::goToLine);
connect(m_funcDialog, &FunctionListDialog::fileLineRequested,
this, &EditorPanel::goToLine);
connect(m_deadCode, &DeadCodeDialog::fileLineRequested,
this, &EditorPanel::goToLine);
}
// ---------------------------------------------------------------------------
// 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);
// FunctionIndex dem Editor mitgeben für Doppelklick-Navigation
tab->editor()->setFunctionIndex(m_funcIndex);
// Doppelklick auf Funktion → zur Definition springen
connect(tab->editor(), &CodeEditor::navigateToRequested,
this, &EditorPanel::goToLine);
// Änderungsindikator im Tab-Titel (● = ungespeichert)
connect(tab->editor()->document(), &QTextDocument::modificationChanged,
this, [this, tab](bool modified)
{
const int idx = m_tabWidget->indexOf(tab);
if (idx == -1)
{
return;
}
const QString name = QFileInfo(tab->filePath()).fileName();
m_tabWidget->setTabText(idx, modified ? "" + name : name);
});
// Tab-Titel nach "Speichern unter" aktualisieren (neuer Dateiname, kein Punkt)
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);
// Index und Funktionsliste aktualisieren
m_funcIndex->refresh();
if (m_funcDialog->isVisible())
{
m_funcDialog->refresh();
}
});
}
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_fileSearch->hide();
m_searchPanel->activate();
}
void EditorPanel::showFileSearchPanel()
{
m_searchPanel->hide();
m_fileSearch->activate();
}
void EditorPanel::showFunctionList()
{
m_funcDialog->show();
m_funcDialog->raise();
m_funcDialog->activateWindow();
}
void EditorPanel::showDeadCode()
{
m_deadCode->show();
m_deadCode->raise();
m_deadCode->activateWindow();
}
void EditorPanel::setSearchRoot(const QString &path)
{
m_fileSearch->setSearchRoot(path);
m_funcDialog->setProjectRoot(path);
m_deadCode->setProjectRoot(path);
m_funcIndex->setProjectRoot(path);
}
void EditorPanel::goToLine(const QString &filePath, int line)
{
// Datei öffnen falls noch nicht geöffnet
openFile(filePath);
EditorTab *tab = m_openTabs.value(filePath, nullptr);
if (!tab)
{
return;
}
m_tabWidget->setCurrentWidget(tab);
// Zur gewünschten Zeile springen
CodeEditor *editor = tab->editor();
QTextBlock block = editor->document()->findBlockByLineNumber(line - 1);
if (block.isValid())
{
QTextCursor cursor(block);
cursor.movePosition(QTextCursor::StartOfBlock);
editor->setTextCursor(cursor);
editor->centerCursor();
editor->setFocus();
}
}
QStringList EditorPanel::openFilePaths() const
{
QStringList paths;
for (int i = 0; i < m_tabWidget->count(); ++i)
{
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(i));
if (tab)
{
paths.append(tab->filePath());
}
}
return paths;
}
QString EditorPanel::activeFilePath() const
{
EditorTab *tab = currentTab();
return tab ? tab->filePath() : QString();
}
void EditorPanel::restoreSession(const QStringList &files, const QString &activeFile)
{
for (const QString &path : files)
{
if (QFile::exists(path))
{
openFile(path);
}
}
// Aktiven Tab wiederherstellen
if (!activeFile.isEmpty())
{
const int idx = findTabForFile(activeFile);
if (idx != -1)
{
m_tabWidget->setCurrentIndex(idx);
}
}
}
void EditorPanel::undo()
{
if (EditorTab *tab = currentTab())
{
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);
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include <QWidget>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QHash>
#include <QString>
class EditorTab;
class Settings;
class SearchPanel;
class FileSearchPanel;
class FunctionListDialog;
class DeadCodeDialog;
class FunctionIndex;
// ---------------------------------------------------------------------------
// 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 setSearchRoot(const QString &path);
void showSearchPanel();
void showFileSearchPanel();
void showFunctionList();
void showDeadCode();
void goToLine(const QString &filePath, int line);
// Session
QStringList openFilePaths() const;
QString activeFilePath() const;
void restoreSession(const QStringList &files, const QString &activeFile);
void undo();
void redo();
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;
FileSearchPanel *m_fileSearch = nullptr;
FunctionListDialog *m_funcDialog = nullptr;
DeadCodeDialog *m_deadCode = nullptr;
FunctionIndex *m_funcIndex = nullptr;
QHash<QString, EditorTab *> m_openTabs;
};

View File

@@ -0,0 +1,53 @@
#include "EditorTab.h"
#include "CodeEditor.h"
#include <QFileInfo>
EditorTab::EditorTab(const QString &filePath, Settings *settings, QWidget *parent)
: QWidget(parent)
, m_filePath(filePath)
{
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(0);
m_editor = new CodeEditor(settings, this);
m_editor->loadFile(filePath);
m_layout->addWidget(m_editor);
}
QString EditorTab::filePath() const
{
return m_filePath;
}
QString EditorTab::fileName() const
{
return QFileInfo(m_filePath).fileName();
}
CodeEditor *EditorTab::editor() const
{
return m_editor;
}
bool EditorTab::isModified() const
{
return m_editor->isModified();
}
bool EditorTab::save()
{
const bool ok = m_editor->save();
// Path may have changed if this was an untitled buffer saved for the first time
m_filePath = m_editor->filePath();
return ok;
}
bool EditorTab::saveAs()
{
const bool ok = m_editor->saveAs();
m_filePath = m_editor->filePath();
return ok;
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include <QWidget>
#include <QString>
#include <QVBoxLayout>
class CodeEditor;
class Settings;
// ---------------------------------------------------------------------------
// EditorTab Widget placed inside each tab of the tab bar.
// Owns a CodeEditor for a single file.
// ---------------------------------------------------------------------------
class EditorTab : public QWidget
{
Q_OBJECT
public:
explicit EditorTab(const QString &filePath, Settings *settings, QWidget *parent = nullptr);
QString filePath() const;
QString fileName() const;
CodeEditor *editor() const;
bool isModified() const;
bool save();
bool saveAs();
private:
QString m_filePath;
QVBoxLayout *m_layout = nullptr;
CodeEditor *m_editor = nullptr;
};

View File

@@ -0,0 +1,330 @@
#include "FileSearchPanel.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QKeyEvent>
#include <QtConcurrent/QtConcurrent>
#include <QFuture>
// ---------------------------------------------------------------------------
// Konstruktor
// ---------------------------------------------------------------------------
FileSearchPanel::FileSearchPanel(QWidget *parent)
: QWidget(parent)
{
setupUi();
hide();
m_watcher = new QFutureWatcher<QList<Match>>(this);
connect(m_watcher, &QFutureWatcher<QList<Match>>::finished,
this, &FileSearchPanel::onSearchFinished);
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
void FileSearchPanel::setupUi()
{
QVBoxLayout *root = new QVBoxLayout(this);
root->setContentsMargins(6, 4, 6, 4);
root->setSpacing(4);
// ---- Zeile 1: Suchbegriff ----
QHBoxLayout *row1 = new QHBoxLayout();
row1->addWidget(new QLabel(tr("Suchen in Dateien:"), this));
m_searchEdit = new QLineEdit(this);
m_searchEdit->setPlaceholderText(tr("Suchbegriff…"));
m_searchEdit->setClearButtonEnabled(true);
row1->addWidget(m_searchEdit, 1);
m_btnSearch = new QPushButton(tr("Suchen"), this);
m_btnSearch->setDefault(true);
row1->addWidget(m_btnSearch);
m_btnClose = new QPushButton(tr(""), this);
m_btnClose->setFixedWidth(24);
m_btnClose->setFlat(true);
m_btnClose->setToolTip(tr("Schließen"));
row1->addWidget(m_btnClose);
root->addLayout(row1);
// ---- Zeile 2: Optionen + Filter ----
QHBoxLayout *row2 = new QHBoxLayout();
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
m_chkRegex = new QCheckBox(tr("Regex"), this);
row2->addWidget(m_chkCase);
row2->addWidget(m_chkWord);
row2->addWidget(m_chkRegex);
row2->addSpacing(12);
row2->addWidget(new QLabel(tr("Dateitypen:"), this));
m_filterEdit = new QLineEdit(this);
m_filterEdit->setText("*.html *.php *.css *.js *.c *.cpp *.h");
m_filterEdit->setFixedWidth(220);
m_filterEdit->setToolTip(tr("Leerzeichen-getrennte Muster, z.B.: *.php *.html"));
row2->addWidget(m_filterEdit);
row2->addStretch();
root->addLayout(row2);
// ---- Fortschritt + Status ----
m_progress = new QProgressBar(this);
m_progress->setRange(0, 0); // Unbestimmter Modus
m_progress->setFixedHeight(4);
m_progress->hide();
root->addWidget(m_progress);
m_statusLabel = new QLabel(this);
m_statusLabel->setStyleSheet("color: palette(mid);");
root->addWidget(m_statusLabel);
// ---- Ergebnisliste ----
m_results = new QTreeWidget(this);
m_results->setHeaderHidden(true);
m_results->setRootIsDecorated(true);
m_results->setIndentation(16);
m_results->setUniformRowHeights(true);
m_results->setAlternatingRowColors(true);
root->addWidget(m_results, 1);
// ---- Verbindungen ----
connect(m_btnSearch, &QPushButton::clicked, this, &FileSearchPanel::onSearch);
connect(m_searchEdit, &QLineEdit::returnPressed, this, &FileSearchPanel::onSearch);
connect(m_btnClose, &QPushButton::clicked, this, [this]()
{
hide();
});
connect(m_results, &QTreeWidget::itemActivated,
this, &FileSearchPanel::onResultActivated);
}
// ---------------------------------------------------------------------------
// Öffentliche Schnittstelle
// ---------------------------------------------------------------------------
void FileSearchPanel::setSearchRoot(const QString &path)
{
m_searchRoot = path;
}
void FileSearchPanel::activate()
{
show();
m_searchEdit->setFocus();
m_searchEdit->selectAll();
}
// ---------------------------------------------------------------------------
// Suche starten
// ---------------------------------------------------------------------------
void FileSearchPanel::onSearch()
{
const QString needle = m_searchEdit->text().trimmed();
if (needle.isEmpty())
{
return;
}
if (m_searchRoot.isEmpty())
{
m_statusLabel->setText(tr("Kein Projektverzeichnis geöffnet."));
return;
}
// Laufende Suche abbrechen
if (m_watcher->isRunning())
{
m_watcher->cancel();
m_watcher->waitForFinished();
}
m_results->clear();
m_statusLabel->setText(tr("Suche läuft…"));
m_progress->show();
m_btnSearch->setEnabled(false);
const QString root = m_searchRoot;
const bool cs = m_chkCase->isChecked();
const bool word = m_chkWord->isChecked();
const bool regex = m_chkRegex->isChecked();
const QStringList extensions = m_filterEdit->text().simplified().split(' ',
Qt::SkipEmptyParts);
QFuture<QList<Match>> future = QtConcurrent::run(
[this, root, needle, cs, word, regex, extensions]()
{
return searchInFiles(root, needle, cs, word, regex, extensions);
}
);
m_watcher->setFuture(future);
}
// ---------------------------------------------------------------------------
// Suchergebnisse anzeigen
// ---------------------------------------------------------------------------
void FileSearchPanel::onSearchFinished()
{
m_progress->hide();
m_btnSearch->setEnabled(true);
if (m_watcher->isCanceled())
{
return;
}
const QList<Match> matches = m_watcher->result();
// Ergebnisse gruppiert nach Datei aufbauen
QString currentFile;
QTreeWidgetItem *fileItem = nullptr;
int fileCount = 0;
int matchCount = 0;
for (const Match &m : matches)
{
if (m.filePath != currentFile)
{
currentFile = m.filePath;
++fileCount;
fileItem = new QTreeWidgetItem(m_results);
fileItem->setText(0, QFileInfo(m.filePath).fileName());
fileItem->setToolTip(0, m.filePath);
fileItem->setData(0, Qt::UserRole, m.filePath);
fileItem->setData(0, Qt::UserRole + 1, -1);
QFont boldFont = fileItem->font(0);
boldFont.setBold(true);
fileItem->setFont(0, boldFont);
fileItem->setExpanded(true);
}
QTreeWidgetItem *lineItem = new QTreeWidgetItem(fileItem);
lineItem->setText(0, QString(" Zeile %1: %2")
.arg(m.line)
.arg(m.content.trimmed().left(120)));
lineItem->setToolTip(0, m.content.trimmed());
lineItem->setData(0, Qt::UserRole, m.filePath);
lineItem->setData(0, Qt::UserRole + 1, m.line);
++matchCount;
}
// Datei-Titelzeilen um Trefferanzahl ergänzen
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
{
QTreeWidgetItem *item = m_results->topLevelItem(i);
const int count = item->childCount();
item->setText(0, QString("%1 (%2 Treffer)")
.arg(QFileInfo(item->data(0, Qt::UserRole).toString()).fileName())
.arg(count));
}
if (matchCount == 0)
{
m_statusLabel->setText(tr("Keine Treffer gefunden."));
}
else
{
m_statusLabel->setText(tr("%1 Treffer in %2 Datei(en).")
.arg(matchCount)
.arg(fileCount));
}
}
// ---------------------------------------------------------------------------
// Klick auf Treffer → Datei + Zeile öffnen
// ---------------------------------------------------------------------------
void FileSearchPanel::onResultActivated(QTreeWidgetItem *item, int /*column*/)
{
const QString path = item->data(0, Qt::UserRole).toString();
const int line = item->data(0, Qt::UserRole + 1).toInt();
if (path.isEmpty() || line < 0)
{
// Datei-Titelzeile: nur auf-/zuklappen
item->setExpanded(!item->isExpanded());
return;
}
emit fileLineRequested(path, line);
}
// ---------------------------------------------------------------------------
// Eigentliche Suchroutine (läuft in Thread-Pool)
// ---------------------------------------------------------------------------
QList<FileSearchPanel::Match> FileSearchPanel::searchInFiles(
const QString &root,
const QString &needle,
bool caseSensitive,
bool wholeWord,
bool useRegex,
const QStringList &extensions) const
{
QList<Match> results;
// Regulären Ausdruck vorbereiten
QString pattern = useRegex ? needle : QRegularExpression::escape(needle);
if (wholeWord)
{
pattern = "\\b" + pattern + "\\b";
}
QRegularExpression re(pattern,
caseSensitive
? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption);
if (!re.isValid())
{
return results;
}
// Verzeichnis rekursiv durchsuchen
QDirIterator it(root,
extensions.isEmpty()
? QStringList("*")
: extensions,
QDir::Files,
QDirIterator::Subdirectories);
while (it.hasNext())
{
if (m_watcher->isCanceled())
{
break;
}
const QString filePath = it.next();
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
continue;
}
QTextStream stream(&file);
stream.setEncoding(QStringConverter::Utf8);
int lineNumber = 0;
while (!stream.atEnd())
{
++lineNumber;
const QString line = stream.readLine();
if (re.match(line).hasMatch())
{
results.append({ filePath, lineNumber, line });
}
}
}
return results;
}

View File

@@ -0,0 +1,77 @@
#pragma once
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QProgressBar>
#include <QFutureWatcher>
#include <QString>
#include <QStringList>
// ---------------------------------------------------------------------------
// FileSearchPanel Suche in allen Dateien eines Verzeichnisses.
//
// Ergebnisse werden als aufklappbare Liste angezeigt:
// Dateiname (N Treffer)
// └ Zeile 12: <Zeileninhalt>
// └ Zeile 34: <Zeileninhalt>
//
// Klick auf einen Treffer öffnet die Datei im Editor und springt zur Zeile.
// ---------------------------------------------------------------------------
class FileSearchPanel : public QWidget
{
Q_OBJECT
public:
explicit FileSearchPanel(QWidget *parent = nullptr);
void setSearchRoot(const QString &path);
public slots:
void activate();
signals:
void fileLineRequested(const QString &filePath, int line);
private slots:
void onSearch();
void onResultActivated(QTreeWidgetItem *item, int column);
void onSearchFinished();
private:
struct Match
{
QString filePath;
int line;
QString content;
};
void setupUi();
QList<Match> searchInFiles(const QString &root,
const QString &needle,
bool caseSensitive,
bool wholeWord,
bool useRegex,
const QStringList &extensions) const;
QString m_searchRoot;
QLineEdit *m_searchEdit = nullptr;
QCheckBox *m_chkCase = nullptr;
QCheckBox *m_chkWord = nullptr;
QCheckBox *m_chkRegex = nullptr;
QLineEdit *m_filterEdit = nullptr; // Dateiendungen-Filter
QPushButton *m_btnSearch = nullptr;
QPushButton *m_btnClose = nullptr;
QLabel *m_statusLabel = nullptr;
QTreeWidget *m_results = nullptr;
QProgressBar *m_progress = nullptr;
QFutureWatcher<QList<Match>> *m_watcher = nullptr;
};

View File

@@ -0,0 +1,83 @@
#include "FunctionIndex.h"
#include <QtConcurrent/QtConcurrent>
FunctionIndex::FunctionIndex(QObject *parent)
: QObject(parent)
{
m_scanner = new FunctionScanner(this);
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::finished,
this, &FunctionIndex::onScanFinished);
}
void FunctionIndex::setProjectRoot(const QString &path)
{
m_projectRoot = path;
m_ready = false;
m_index.clear();
if (!path.isEmpty())
{
refresh();
}
}
void FunctionIndex::refresh()
{
if (m_projectRoot.isEmpty())
{
return;
}
if (m_watcher->isRunning())
{
m_watcher->cancel();
m_watcher->waitForFinished();
}
FunctionScanner *scanner = m_scanner;
const QString root = m_projectRoot;
m_watcher->setFuture(
QtConcurrent::run([scanner, root]()
{
return scanner->scanDirectory(root);
})
);
}
void FunctionIndex::onScanFinished()
{
if (m_watcher->isCanceled())
{
return;
}
m_index.clear();
const QList<FunctionScanner::FunctionInfo> all = m_watcher->result();
for (const FunctionScanner::FunctionInfo &func : all)
{
const QString key = func.name.toLower();
// Ersten Eintrag bevorzugen (globale Funktionen vor Methoden)
if (!m_index.contains(key) || func.className.isEmpty())
{
m_index.insert(key, func);
}
}
m_ready = true;
emit indexReady();
}
FunctionScanner::FunctionInfo FunctionIndex::lookup(const QString &name) const
{
return m_index.value(name.toLower(), FunctionScanner::FunctionInfo{});
}
bool FunctionIndex::isReady() const
{
return m_ready;
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <QObject>
#include <QHash>
#include <QString>
#include <QStringList>
#include <QFutureWatcher>
#include "FunctionScanner.h"
// ---------------------------------------------------------------------------
// FunctionIndex Singleton-artiger Index aller Projektfunktionen.
// Wird von CodeEditor (Doppelklick-Navigation) und SignatureHelper genutzt.
// Aktualisiert sich asynchron wenn setProjectRoot() aufgerufen wird.
// ---------------------------------------------------------------------------
class FunctionIndex : public QObject
{
Q_OBJECT
public:
explicit FunctionIndex(QObject *parent = nullptr);
void setProjectRoot(const QString &path);
void refresh();
// Sucht eine Funktion nach Name (Groß-/Kleinschreibung ignoriert)
// Gibt ungültige FunctionInfo zurück wenn nicht gefunden (filePath ist leer)
FunctionScanner::FunctionInfo lookup(const QString &name) const;
bool isReady() const;
signals:
void indexReady();
private slots:
void onScanFinished();
private:
QString m_projectRoot;
FunctionScanner *m_scanner = nullptr;
QFutureWatcher<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
// name.toLower() → FunctionInfo
QHash<QString, FunctionScanner::FunctionInfo> m_index;
bool m_ready = false;
};

View File

@@ -0,0 +1,52 @@
#include "FunctionListDialog.h"
#include "FunctionListPanel.h"
#include <QVBoxLayout>
#include <QSettings>
#include <QCloseEvent>
FunctionListDialog::FunctionListDialog(QWidget *parent)
: QDialog(parent, Qt::Window)
{
setWindowTitle(tr("Projektfunktionen BareCode"));
setMinimumSize(500, 400);
// Fenstergröße und -position wiederherstellen
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
if (s.contains("funclist/geometry"))
{
restoreGeometry(s.value("funclist/geometry").toByteArray());
}
else
{
resize(600, 700);
}
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
m_panel = new FunctionListPanel(this);
m_panel->show(); // Immer sichtbar — Panel ist jetzt der gesamte Dialog-Inhalt
layout->addWidget(m_panel);
connect(m_panel, &FunctionListPanel::fileLineRequested,
this, &FunctionListDialog::fileLineRequested);
}
void FunctionListDialog::setProjectRoot(const QString &path)
{
m_panel->setProjectRoot(path);
}
void FunctionListDialog::refresh()
{
m_panel->refresh();
}
void FunctionListDialog::closeEvent(QCloseEvent *event)
{
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
s.setValue("funclist/geometry", saveGeometry());
event->accept();
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <QDialog>
#include <QString>
class FunctionListPanel;
// ---------------------------------------------------------------------------
// FunctionListDialog Eigenständiges Fenster für die Projektfunktionen.
// Bleibt offen während man im Editor arbeitet (non-modal).
// ---------------------------------------------------------------------------
class FunctionListDialog : public QDialog
{
Q_OBJECT
public:
explicit FunctionListDialog(QWidget *parent = nullptr);
void setProjectRoot(const QString &path);
void refresh();
protected:
void closeEvent(QCloseEvent *event) override;
signals:
void fileLineRequested(const QString &filePath, int line);
private:
FunctionListPanel *m_panel = nullptr;
};

View File

@@ -0,0 +1,332 @@
#include "FunctionListPanel.h"
#include <QtConcurrent/QtConcurrent>
#include <QFileInfo>
#include <QFont>
#include <QProgressBar>
FunctionListPanel::FunctionListPanel(QWidget *parent)
: QWidget(parent)
{
m_scanner = new FunctionScanner(this);
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::finished,
this, &FunctionListPanel::onScanFinished);
setupUi();
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
void FunctionListPanel::setupUi()
{
QVBoxLayout *root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
// ---- Kopfzeile ----
QWidget *header = new QWidget(this);
header->setStyleSheet("background: palette(mid);");
QHBoxLayout *headerLayout = new QHBoxLayout(header);
headerLayout->setContentsMargins(6, 4, 6, 4);
headerLayout->setSpacing(4);
QLabel *title = new QLabel(tr("Projektfunktionen"), header);
QFont boldFont = title->font();
boldFont.setBold(true);
title->setFont(boldFont);
headerLayout->addWidget(title);
headerLayout->addStretch();
m_btnRefresh = new QPushButton(tr(""), header);
m_btnRefresh->setFixedSize(24, 24);
m_btnRefresh->setFlat(true);
m_btnRefresh->setToolTip(tr("Neu scannen"));
headerLayout->addWidget(m_btnRefresh);
root->addWidget(header);
// ---- Filter + Gruppierung ----
QHBoxLayout *toolRow = new QHBoxLayout();
toolRow->setContentsMargins(6, 4, 6, 4);
toolRow->setSpacing(4);
m_filterEdit = new QLineEdit(this);
m_filterEdit->setPlaceholderText(tr("Funktion suchen…"));
m_filterEdit->setClearButtonEnabled(true);
toolRow->addWidget(m_filterEdit, 1);
m_groupCombo = new QComboBox(this);
m_groupCombo->addItem(tr("Nach Datei"), "file");
m_groupCombo->addItem(tr("Nach Klasse"), "class");
m_groupCombo->addItem(tr("Alphabetisch"), "alpha");
m_groupCombo->setFixedWidth(120);
toolRow->addWidget(m_groupCombo);
root->addLayout(toolRow);
// ---- Status ----
m_statusLabel = new QLabel(this);
m_statusLabel->setContentsMargins(6, 0, 6, 2);
m_statusLabel->setStyleSheet("color: palette(mid); font-size: 11px;");
root->addWidget(m_statusLabel);
// ---- Ergebnisbaum ----
m_tree = new QTreeWidget(this);
m_tree->setHeaderHidden(true);
m_tree->setRootIsDecorated(true);
m_tree->setIndentation(14);
m_tree->setAlternatingRowColors(false);
m_tree->setAnimated(true);
// Monospace-Font für Parameter
QFont monoFont("Monospace", m_tree->font().pointSize());
monoFont.setStyleHint(QFont::Monospace);
m_tree->setFont(monoFont);
root->addWidget(m_tree, 1);
// ---- Verbindungen ----
connect(m_btnRefresh, &QPushButton::clicked,
this, &FunctionListPanel::refresh);
connect(m_filterEdit, &QLineEdit::textChanged,
this, &FunctionListPanel::onFilterChanged);
connect(m_groupCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &FunctionListPanel::onGroupingChanged);
connect(m_tree, &QTreeWidget::itemActivated,
this, &FunctionListPanel::onItemActivated);
}
// ---------------------------------------------------------------------------
// Öffentliche Schnittstelle
// ---------------------------------------------------------------------------
void FunctionListPanel::setProjectRoot(const QString &path)
{
m_projectRoot = path;
if (!path.isEmpty())
{
refresh();
}
else
{
m_tree->clear();
m_statusLabel->setText(QString());
m_lastResult.clear();
}
}
void FunctionListPanel::activate()
{
m_filterEdit->setFocus();
m_filterEdit->selectAll();
if (m_lastResult.isEmpty() && !m_projectRoot.isEmpty())
{
refresh();
}
}
// ---------------------------------------------------------------------------
// Scan starten
// ---------------------------------------------------------------------------
void FunctionListPanel::refresh()
{
if (m_projectRoot.isEmpty())
{
m_statusLabel->setText(tr("Kein Projekt geöffnet."));
return;
}
if (m_watcher->isRunning())
{
m_watcher->cancel();
m_watcher->waitForFinished();
}
m_statusLabel->setText(tr("Scanne…"));
m_btnRefresh->setEnabled(false);
const QString root = m_projectRoot;
FunctionScanner *scanner = m_scanner;
QFuture<QList<FunctionScanner::FunctionInfo>> future =
QtConcurrent::run([scanner, root]()
{
return scanner->scanDirectory(root);
});
m_watcher->setFuture(future);
}
// ---------------------------------------------------------------------------
// Scan abgeschlossen
// ---------------------------------------------------------------------------
void FunctionListPanel::onScanFinished()
{
m_btnRefresh->setEnabled(true);
if (m_watcher->isCanceled())
{
return;
}
m_lastResult = m_watcher->result();
populateTree(m_lastResult);
}
// ---------------------------------------------------------------------------
// Baum befüllen
// ---------------------------------------------------------------------------
void FunctionListPanel::populateTree(const QList<FunctionScanner::FunctionInfo> &functions)
{
m_tree->clear();
const QString groupBy = m_groupCombo->currentData().toString();
const QString filter = m_filterEdit->text().trimmed().toLower();
// Funktionen filtern
QList<FunctionScanner::FunctionInfo> filtered;
for (const auto &f : functions)
{
if (filter.isEmpty() ||
f.name.toLower().contains(filter) ||
f.parameters.toLower().contains(filter) ||
f.className.toLower().contains(filter))
{
filtered.append(f);
}
}
if (groupBy == "alpha")
{
// Alphabetisch — keine Gruppen
std::sort(filtered.begin(), filtered.end(),
[](const FunctionScanner::FunctionInfo &a,
const FunctionScanner::FunctionInfo &b)
{
return a.name.toLower() < b.name.toLower();
});
for (const auto &f : filtered)
{
QTreeWidgetItem *item = new QTreeWidgetItem(m_tree);
formatFunctionItem(item, f);
}
}
else
{
// Gruppiert nach Datei oder Klasse
QMap<QString, QList<FunctionScanner::FunctionInfo>> groups;
for (const auto &f : filtered)
{
QString key;
if (groupBy == "class")
{
key = f.className.isEmpty() ? tr("(global)") : f.className;
}
else
{
key = QFileInfo(f.filePath).fileName();
}
groups[key].append(f);
}
for (auto it = groups.begin(); it != groups.end(); ++it)
{
QTreeWidgetItem *groupItem = new QTreeWidgetItem(m_tree);
groupItem->setText(0, QString("%1 (%2)")
.arg(it.key())
.arg(it.value().size()));
groupItem->setData(0, Qt::UserRole, QString());
groupItem->setData(0, Qt::UserRole + 1, -1);
QFont boldFont = groupItem->font(0);
boldFont.setBold(true);
groupItem->setFont(0, boldFont);
groupItem->setExpanded(true);
// Funktionen innerhalb der Gruppe alphabetisch
auto &list = it.value();
std::sort(list.begin(), list.end(),
[](const FunctionScanner::FunctionInfo &a,
const FunctionScanner::FunctionInfo &b)
{
return a.name.toLower() < b.name.toLower();
});
for (const auto &f : list)
{
QTreeWidgetItem *item = new QTreeWidgetItem(groupItem);
formatFunctionItem(item, f);
}
}
}
// Statuszeile
const int total = functions.size();
const int shown = filtered.size();
if (filter.isEmpty())
{
m_statusLabel->setText(tr("%1 Funktionen gefunden").arg(total));
}
else
{
m_statusLabel->setText(tr("%1 von %2 Funktionen").arg(shown).arg(total));
}
}
void FunctionListPanel::formatFunctionItem(QTreeWidgetItem *item,
const FunctionScanner::FunctionInfo &f)
{
// Anzeige: name(parameter) — Zeilennummer klein dahinter
const QString display = QString("%1(%2)")
.arg(f.name)
.arg(f.parameters);
item->setText(0, display);
item->setToolTip(0, QString("%1\nZeile %2").arg(f.filePath).arg(f.line));
item->setData(0, Qt::UserRole, f.filePath);
item->setData(0, Qt::UserRole + 1, f.line);
// Zeilennummer in gedimmter Farbe als zweite Spalte simulieren
// (über ToolTip gelöst da wir nur eine Spalte haben)
}
// ---------------------------------------------------------------------------
// Slots
// ---------------------------------------------------------------------------
void FunctionListPanel::onItemActivated(QTreeWidgetItem *item, int /*column*/)
{
const QString path = item->data(0, Qt::UserRole).toString();
const int line = item->data(0, Qt::UserRole + 1).toInt();
if (path.isEmpty() || line < 0)
{
item->setExpanded(!item->isExpanded());
return;
}
emit fileLineRequested(path, line);
}
void FunctionListPanel::onFilterChanged(const QString &text)
{
Q_UNUSED(text)
if (!m_lastResult.isEmpty())
{
populateTree(m_lastResult);
}
}
void FunctionListPanel::onGroupingChanged(int /*index*/)
{
if (!m_lastResult.isEmpty())
{
populateTree(m_lastResult);
}
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include <QWidget>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QLineEdit>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QComboBox>
#include <QFutureWatcher>
#include <QString>
#include "FunctionScanner.h"
// ---------------------------------------------------------------------------
// FunctionListPanel Zeigt alle im Projekt definierten Funktionen
// als durchsuchbare, sortierbare Liste.
//
// Gruppierung: nach Datei oder nach Klasse
// Klick: öffnet Datei und springt zur Definition
// Aktualisierung: automatisch nach jedem Speichern
// ---------------------------------------------------------------------------
class FunctionListPanel : public QWidget
{
Q_OBJECT
public:
explicit FunctionListPanel(QWidget *parent = nullptr);
void setProjectRoot(const QString &path);
public slots:
void refresh();
void activate();
signals:
void fileLineRequested(const QString &filePath, int line);
private slots:
void onScanFinished();
void onItemActivated(QTreeWidgetItem *item, int column);
void onFilterChanged(const QString &text);
void onGroupingChanged(int index);
private:
void setupUi();
void populateTree(const QList<FunctionScanner::FunctionInfo> &functions);
void applyFilter(const QString &text);
static void formatFunctionItem(QTreeWidgetItem *item,
const FunctionScanner::FunctionInfo &f);
QString m_projectRoot;
QLineEdit *m_filterEdit = nullptr;
QComboBox *m_groupCombo = nullptr;
QPushButton *m_btnRefresh = nullptr;
QLabel *m_statusLabel = nullptr;
QTreeWidget *m_tree = nullptr;
FunctionScanner *m_scanner = nullptr;
QFutureWatcher<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
// Letztes Scan-Ergebnis für Filter ohne Neuscan
QList<FunctionScanner::FunctionInfo> m_lastResult;
};

View File

@@ -0,0 +1,158 @@
#include "FunctionScanner.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
FunctionScanner::FunctionScanner(QObject *parent)
: QObject(parent)
{
}
// ---------------------------------------------------------------------------
// Verzeichnis rekursiv scannen
// ---------------------------------------------------------------------------
QList<FunctionScanner::FunctionInfo> FunctionScanner::scanDirectory(
const QString &rootPath,
const QStringList &extensions) const
{
QList<FunctionInfo> results;
QDirIterator it(rootPath,
extensions,
QDir::Files,
QDirIterator::Subdirectories);
while (it.hasNext())
{
const QString path = it.next();
results.append(scanFile(path));
}
return results;
}
// ---------------------------------------------------------------------------
// Einzelne Datei scannen
// ---------------------------------------------------------------------------
QList<FunctionScanner::FunctionInfo> FunctionScanner::scanFile(const QString &filePath) const
{
QList<FunctionInfo> results;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
return results;
}
QTextStream stream(&file);
stream.setEncoding(QStringConverter::Utf8);
QStringList lines;
while (!stream.atEnd())
{
lines.append(stream.readLine());
}
// Regex für Funktionsdefinitionen:
// optional: public/protected/private/static/abstract/final
// gefolgt von: function name(
static const QRegularExpression funcRegex(
R"((?:(?:public|protected|private|static|abstract|final)\s+)*)"
R"(function\s+(&?\s*[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()",
QRegularExpression::CaseInsensitiveOption
);
for (int i = 0; i < lines.size(); ++i)
{
const QString &line = lines[i];
// Zeilen in Kommentaren überspringen
const QString trimmed = line.trimmed();
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("#"))
{
continue;
}
QRegularExpressionMatch match = funcRegex.match(line);
if (!match.hasMatch())
{
continue;
}
FunctionInfo info;
info.name = match.captured(1).trimmed();
info.filePath = filePath;
info.line = i + 1;
info.className = detectClassContext(lines, i);
info.parameters = extractParameters(line, static_cast<int>(match.capturedEnd()) - 1);
info.signature = info.name + "(" + info.parameters + ")";
// Konstruktoren und magische Methoden kennzeichnen
// aber trotzdem aufnehmen — sie sind nützlich in der Liste
results.append(info);
}
return results;
}
// ---------------------------------------------------------------------------
// Parameter aus der Funktionssignatur extrahieren
// Behandelt auch mehrzeilige Signaturen mit öffnender Klammer am Ende
// ---------------------------------------------------------------------------
QString FunctionScanner::extractParameters(const QString &line, int parenPos)
{
// Inhalt zwischen ( und ) extrahieren
// Einfache Version: nur die erste Zeile — reicht für 99% aller Fälle
const int openParen = parenPos;
int depth = 0;
int closePos = -1;
for (int i = openParen; i < line.length(); ++i)
{
if (line[i] == '(') { ++depth; }
else if (line[i] == ')')
{
--depth;
if (depth == 0)
{
closePos = i;
break;
}
}
}
if (closePos == -1)
{
// Klammer geht über Zeilenende — gekürzt anzeigen
return line.mid(openParen + 1).trimmed() + "";
}
return line.mid(openParen + 1, closePos - openParen - 1).trimmed();
}
// ---------------------------------------------------------------------------
// Klassenkontext erkennen — rückwärts durch den Code suchen
// ---------------------------------------------------------------------------
QString FunctionScanner::detectClassContext(const QStringList &lines, int functionLineIndex)
{
static const QRegularExpression classRegex(
R"((?:class|interface|trait|enum)\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))",
QRegularExpression::CaseInsensitiveOption
);
// Rückwärts suchen — letztes class/interface/trait vor dieser Zeile
for (int i = functionLineIndex - 1; i >= 0; --i)
{
QRegularExpressionMatch match = classRegex.match(lines[i]);
if (match.hasMatch())
{
return match.captured(1);
}
}
return QString(); // Globale Funktion
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
#include <QList>
// ---------------------------------------------------------------------------
// FunctionScanner Scannt PHP-Dateien nach Funktionsdefinitionen.
//
// Liefert für jede gefundene Funktion:
// - Name
// - Parameter (vollständige Signatur)
// - Dateipfad
// - Zeilennummer
//
// Wird von FunctionListPanel (Anzeige) und DeadCodeAnalyzer (Stufe 1)
// gemeinsam genutzt.
// ---------------------------------------------------------------------------
class FunctionScanner : public QObject
{
Q_OBJECT
public:
struct FunctionInfo
{
QString name; // Funktionsname
QString parameters; // Parameter wie definiert, z.B. "$id, $name = null"
QString signature; // name(parameters) — fertig formatiert
QString filePath; // Absoluter Pfad zur Datei
int line = 0; // Zeilennummer der Definition
QString className; // Klassen- oder Namespace-Kontext, leer = global
};
explicit FunctionScanner(QObject *parent = nullptr);
// Synchroner Scan — für direkten Aufruf aus Threads
QList<FunctionInfo> scanDirectory(const QString &rootPath,
const QStringList &extensions = {"*.php"}) const;
// Scannt eine einzelne Datei
QList<FunctionInfo> scanFile(const QString &filePath) const;
private:
static QString extractParameters(const QString &line, int parenPos);
static QString detectClassContext(const QStringList &lines, int functionLineIndex);
};

View File

@@ -0,0 +1,18 @@
#include "LineNumberArea.h"
#include "CodeEditor.h"
LineNumberArea::LineNumberArea(CodeEditor *editor)
: QWidget(editor)
, m_codeEditor(editor)
{
}
QSize LineNumberArea::sizeHint() const
{
return QSize(m_codeEditor->lineNumberAreaWidth(), 0);
}
void LineNumberArea::paintEvent(QPaintEvent *event)
{
m_codeEditor->lineNumberAreaPaintEvent(event);
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <QWidget>
class CodeEditor;
// ---------------------------------------------------------------------------
// LineNumberArea Thin widget painted on the left side of the CodeEditor.
// Painted by CodeEditor::lineNumberAreaPaintEvent().
// ---------------------------------------------------------------------------
class LineNumberArea : public QWidget
{
Q_OBJECT
public:
explicit LineNumberArea(CodeEditor *editor);
QSize sizeHint() const override;
protected:
void paintEvent(QPaintEvent *event) override;
private:
CodeEditor *m_codeEditor;
};

View File

@@ -0,0 +1,492 @@
#include "SearchPanel.h"
#include "CodeEditor.h"
#include <QTextCursor>
#include <QTextBlock>
#include <QRegularExpression>
#include <QMessageBox>
#include <QKeyEvent>
#include <QShortcut>
SearchPanel::SearchPanel(QWidget *parent)
: QWidget(parent)
{
setupUi();
hide();
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
void SearchPanel::setupUi()
{
m_grid = new QGridLayout(this);
m_grid->setContentsMargins(6, 4, 6, 4);
m_grid->setSpacing(4);
// ---- Row 0: Suchen ----
m_searchEdit = new QLineEdit(this);
m_searchEdit->setPlaceholderText(tr("Suchen…"));
m_searchEdit->setClearButtonEnabled(true);
m_btnPrev = new QPushButton(tr(""), this);
m_btnNext = new QPushButton(tr(""), this);
m_btnPrev->setFixedWidth(28);
m_btnNext->setFixedWidth(28);
m_btnPrev->setToolTip(tr("Vorheriger Treffer (Shift+F3)"));
m_btnNext->setToolTip(tr("Nächster Treffer (F3)"));
m_matchLabel = new QLabel(this);
m_matchLabel->setMinimumWidth(80);
m_btnClose = new QPushButton(tr(""), this);
m_btnClose->setFixedWidth(24);
m_btnClose->setToolTip(tr("Schließen (Esc)"));
m_btnClose->setFlat(true);
QHBoxLayout *searchRow = new QHBoxLayout();
searchRow->addWidget(new QLabel(tr("Suchen:"), this));
searchRow->addWidget(m_searchEdit, 1);
searchRow->addWidget(m_btnPrev);
searchRow->addWidget(m_btnNext);
searchRow->addWidget(m_matchLabel);
searchRow->addWidget(m_btnClose);
m_grid->addLayout(searchRow, 0, 0);
// ---- Row 1: Ersetzen ----
m_replaceEdit = new QLineEdit(this);
m_replaceEdit->setPlaceholderText(tr("Ersetzen durch…"));
m_replaceEdit->setClearButtonEnabled(true);
m_btnReplace = new QPushButton(tr("Ersetzen"), this);
m_btnReplaceAll = new QPushButton(tr("Alle ersetzen"), this);
m_btnReplaceSelection = new QPushButton(tr("In Auswahl ersetzen"), this);
QHBoxLayout *replaceRow = new QHBoxLayout();
replaceRow->addWidget(new QLabel(tr("Ersetzen:"), this));
replaceRow->addWidget(m_replaceEdit, 1);
replaceRow->addWidget(m_btnReplace);
replaceRow->addWidget(m_btnReplaceAll);
replaceRow->addWidget(m_btnReplaceSelection);
m_grid->addLayout(replaceRow, 1, 0);
// ---- Row 2: Optionen ----
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
m_chkRegex = new QCheckBox(tr("Regulärer Ausdruck"), this);
QHBoxLayout *optRow = new QHBoxLayout();
optRow->addWidget(m_chkCase);
optRow->addWidget(m_chkWord);
optRow->addWidget(m_chkRegex);
optRow->addStretch();
m_grid->addLayout(optRow, 2, 0);
// ---- Connections ----
connect(m_searchEdit, &QLineEdit::textChanged,
this, &SearchPanel::onSearchTextChanged);
connect(m_searchEdit, &QLineEdit::returnPressed,
this, &SearchPanel::findNext);
connect(m_btnNext, &QPushButton::clicked, this, &SearchPanel::findNext);
connect(m_btnPrev, &QPushButton::clicked, this, &SearchPanel::findPrevious);
connect(m_btnReplace, &QPushButton::clicked, this, &SearchPanel::replaceCurrent);
connect(m_btnReplaceAll, &QPushButton::clicked, this, &SearchPanel::replaceAll);
connect(m_btnReplaceSelection, &QPushButton::clicked, this, &SearchPanel::replaceInSelection);
connect(m_btnClose, &QPushButton::clicked, this, &SearchPanel::onCloseClicked);
connect(m_chkCase, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
connect(m_chkWord, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
connect(m_chkRegex, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
}
// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------
void SearchPanel::setEditor(CodeEditor *editor)
{
clearHighlights();
m_editor = editor;
}
void SearchPanel::activate()
{
show();
m_searchEdit->setFocus();
m_searchEdit->selectAll();
// Pre-fill with selected text if short enough
if (m_editor)
{
const QString sel = m_editor->textCursor().selectedText();
if (!sel.isEmpty() && !sel.contains('\n') && sel.length() < 200)
{
m_searchEdit->setText(sel);
}
}
updateMatchLabel();
}
// ---------------------------------------------------------------------------
// Find helpers
// ---------------------------------------------------------------------------
QTextDocument::FindFlags SearchPanel::buildFindFlags(bool backwards) const
{
QTextDocument::FindFlags flags;
if (backwards) { flags |= QTextDocument::FindBackward; }
if (m_chkCase->isChecked()) { flags |= QTextDocument::FindCaseSensitively; }
if (m_chkWord->isChecked()) { flags |= QTextDocument::FindWholeWords; }
return flags;
}
bool SearchPanel::performFind(bool backwards)
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
return false;
}
const QTextDocument::FindFlags flags = buildFindFlags(backwards);
bool found = false;
if (m_chkRegex->isChecked())
{
QRegularExpression re(m_searchEdit->text());
if (m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::NoPatternOption);
}
else
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
found = m_editor->find(re, flags);
// Wrap around
if (!found)
{
QTextCursor c = m_editor->textCursor();
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
m_editor->setTextCursor(c);
found = m_editor->find(re, flags);
}
}
else
{
found = m_editor->find(m_searchEdit->text(), flags);
// Wrap around
if (!found)
{
QTextCursor c = m_editor->textCursor();
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
m_editor->setTextCursor(c);
found = m_editor->find(m_searchEdit->text(), flags);
}
}
return found;
}
void SearchPanel::highlightAllMatches()
{
if (!m_editor)
{
return;
}
QList<QTextEdit::ExtraSelection> extras;
const QString needle = m_searchEdit->text();
if (needle.isEmpty())
{
m_editor->setExtraSelections(extras);
return;
}
QTextCharFormat fmt;
fmt.setBackground(QColor("#3a3a00"));
fmt.setForeground(QColor("#ffff80"));
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
const QTextDocument::FindFlags flags = buildFindFlags(false);
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
QTextEdit::ExtraSelection sel;
sel.cursor = cursor;
sel.format = fmt;
extras.append(sel);
}
m_editor->setExtraSelections(extras);
}
void SearchPanel::clearHighlights()
{
if (m_editor)
{
m_editor->setExtraSelections({});
}
}
void SearchPanel::updateMatchLabel()
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
m_matchLabel->setText(QString());
return;
}
// Count total matches
int count = 0;
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
const QTextDocument::FindFlags flags = buildFindFlags(false);
const QString needle = m_searchEdit->text();
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
++count;
}
if (count == 0)
{
m_matchLabel->setText(tr("Kein Treffer"));
m_matchLabel->setStyleSheet("color: #cc4444;");
}
else
{
m_matchLabel->setText(tr("%1 Treffer").arg(count));
m_matchLabel->setStyleSheet(QString());
}
}
// ---------------------------------------------------------------------------
// Public slots
// ---------------------------------------------------------------------------
void SearchPanel::findNext()
{
performFind(false);
}
void SearchPanel::findPrevious()
{
performFind(true);
}
void SearchPanel::replaceCurrent()
{
if (!m_editor)
{
return;
}
QTextCursor cursor = m_editor->textCursor();
// If current selection matches the search term, replace it
// Otherwise just find the next occurrence first
const bool hasMatch = !cursor.selectedText().isEmpty();
if (!hasMatch)
{
performFind(false);
return;
}
cursor.insertText(m_replaceEdit->text());
// Move to next match
performFind(false);
updateMatchLabel();
highlightAllMatches();
}
void SearchPanel::replaceAll()
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
return;
}
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
int count = 0;
const QTextDocument::FindFlags flags = buildFindFlags(false);
const QString needle = m_searchEdit->text();
const QString replacement = m_replaceEdit->text();
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
cursor.insertText(replacement);
++count;
}
cursor.endEditBlock();
updateMatchLabel();
clearHighlights();
QMessageBox::information(this, tr("Alle ersetzen"),
tr("%1 Ersetzung(en) durchgeführt.").arg(count));
}
void SearchPanel::replaceInSelection()
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
return;
}
QTextCursor selCursor = m_editor->textCursor();
if (!selCursor.hasSelection())
{
QMessageBox::information(this, tr("In Auswahl ersetzen"),
tr("Es ist kein Text ausgewählt."));
return;
}
// Work only within the selected region
const int selStart = selCursor.selectionStart();
const int selEnd = selCursor.selectionEnd();
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
cursor.setPosition(selStart);
cursor.beginEditBlock();
int count = 0;
int offset = 0; // Replacement may be longer/shorter than search term
const QTextDocument::FindFlags flags = buildFindFlags(false);
const QString needle = m_searchEdit->text();
const QString replacement = m_replaceEdit->text();
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
// Stop if we've left the original selection
if (cursor.selectionEnd() > selEnd + offset)
{
break;
}
offset += replacement.length() - cursor.selectedText().length();
cursor.insertText(replacement);
++count;
}
cursor.endEditBlock();
updateMatchLabel();
clearHighlights();
QMessageBox::information(this, tr("In Auswahl ersetzen"),
tr("%1 Ersetzung(en) in der Auswahl durchgeführt.").arg(count));
}
// ---------------------------------------------------------------------------
// Private slots
// ---------------------------------------------------------------------------
void SearchPanel::onSearchTextChanged(const QString &/*text*/)
{
highlightAllMatches();
updateMatchLabel();
}
void SearchPanel::onOptionChanged()
{
highlightAllMatches();
updateMatchLabel();
}
void SearchPanel::onCloseClicked()
{
clearHighlights();
m_matchLabel->setText(QString());
hide();
if (m_editor)
{
m_editor->setFocus();
}
}

View File

@@ -0,0 +1,79 @@
#pragma once
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QLabel>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QTextDocument>
class CodeEditor;
// ---------------------------------------------------------------------------
// SearchPanel Collapsible find/replace bar that operates on a CodeEditor.
//
// Capabilities:
// • Nächsten / Vorherigen Treffer suchen
// • Einzeln ersetzen
// • Alle ersetzen
// • Nur in Auswahl ersetzen
// • Optionen: Groß-/Kleinschreibung, Ganzes Wort, Reguläre Ausdrücke
// ---------------------------------------------------------------------------
class SearchPanel : public QWidget
{
Q_OBJECT
public:
explicit SearchPanel(QWidget *parent = nullptr);
// Must be called whenever the active editor changes
void setEditor(CodeEditor *editor);
// Toggle visibility and focus the search field
void activate();
public slots:
void findNext();
void findPrevious();
void replaceCurrent();
void replaceAll();
void replaceInSelection();
private slots:
void onSearchTextChanged(const QString &text);
void onOptionChanged(); // Für Checkbox-Signale (bool-Parameter wird ignoriert)
void onCloseClicked();
private:
void setupUi();
QTextDocument::FindFlags buildFindFlags(bool backwards = false) const;
bool performFind(bool backwards = false);
void highlightAllMatches();
void clearHighlights();
void updateMatchLabel();
CodeEditor *m_editor = nullptr;
// Search row
QLineEdit *m_searchEdit = nullptr;
QPushButton *m_btnPrev = nullptr;
QPushButton *m_btnNext = nullptr;
QLabel *m_matchLabel = nullptr;
QPushButton *m_btnClose = nullptr;
// Replace row
QLineEdit *m_replaceEdit = nullptr;
QPushButton *m_btnReplace = nullptr;
QPushButton *m_btnReplaceAll = nullptr;
QPushButton *m_btnReplaceSelection = nullptr;
// Options row
QCheckBox *m_chkCase = nullptr;
QCheckBox *m_chkWord = nullptr;
QCheckBox *m_chkRegex = nullptr;
QGridLayout *m_grid = nullptr;
};

View File

@@ -0,0 +1,222 @@
#include "SignatureHelper.h"
#include "SignatureTooltip.h"
#include "CodeEditor.h"
#include <QTextCursor>
#include <QTextBlock>
#include <QFile>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QRect>
#include <QRegularExpression>
SignatureHelper::SignatureHelper(CodeEditor *editor)
: QObject(editor)
, m_editor(editor)
{
// Tooltip als Kind des Viewports — bleibt im Fenster
m_tooltip = new SignatureTooltip(editor->window());
loadDatabase();
connect(m_editor, &CodeEditor::cursorPositionChanged,
this, &SignatureHelper::onCursorPositionChanged);
}
// ---------------------------------------------------------------------------
// Datenbank laden
// ---------------------------------------------------------------------------
void SignatureHelper::loadDatabase()
{
QFile f(":/php_functions.json");
if (!f.open(QIODevice::ReadOnly))
{
return;
}
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
if (!doc.isArray())
{
return;
}
for (const QJsonValue &val : doc.array())
{
const QJsonObject obj = val.toObject();
const QString name = obj["name"].toString();
if (name.isEmpty())
{
continue;
}
FunctionInfo info;
info.signature = obj["signature"].toString();
info.description = obj["desc"].toString();
m_functions.insert(name.toLower(), info);
}
}
// ---------------------------------------------------------------------------
// Cursor-Bewegung auswerten
// ---------------------------------------------------------------------------
void SignatureHelper::onCursorPositionChanged()
{
const QTextCursor cursor = m_editor->textCursor();
const QString block = cursor.block().text();
const int col = cursor.columnNumber();
const QString leftText = block.left(col);
const QString funcName = extractFunctionName(leftText);
if (funcName.isEmpty())
{
m_tooltip->hide();
return;
}
// Klammern zählen — wenn alle geschlossen, Tooltip ausblenden
if (countOpenParens(leftText) <= 0)
{
m_tooltip->hide();
return;
}
const QString key = funcName.toLower();
if (!m_functions.contains(key))
{
m_tooltip->hide();
return;
}
const FunctionInfo &info = m_functions[key];
// Position unter dem Cursor berechnen
const QRect cursorRect = m_editor->cursorRect(cursor);
const QPoint globalPos = m_editor->viewport()->mapToGlobal(
QPoint(cursorRect.left(), cursorRect.bottom() + 4)
);
m_tooltip->showSignature(info.signature, info.description, globalPos);
}
// ---------------------------------------------------------------------------
// Funktionsnamen links vor der öffnenden Klammer extrahieren
// ---------------------------------------------------------------------------
QString SignatureHelper::extractFunctionName(const QString &text) const
{
// Wir suchen das letzte '(' das zu einem Funktionsnamen gehört.
// Dabei müssen wir verschachtelte Klammern korrekt behandeln.
int depth = 0;
int openPos = -1;
for (int i = text.length() - 1; i >= 0; --i)
{
const QChar ch = text[i];
if (ch == ')')
{
++depth;
}
else if (ch == '(')
{
if (depth == 0)
{
openPos = i;
break;
}
--depth;
}
}
if (openPos <= 0)
{
return QString();
}
// Funktionsnamen direkt links von '(' lesen
int end = openPos - 1;
// Leerzeichen überspringen
while (end >= 0 && text[end].isSpace())
{
--end;
}
if (end < 0)
{
return QString();
}
// Bezeichner-Zeichen sammeln (Buchstaben, Ziffern, _, :, \)
int start = end;
while (start > 0 &&
(text[start - 1].isLetterOrNumber() ||
text[start - 1] == '_' ||
text[start - 1] == ':' ||
text[start - 1] == '\\'))
{
--start;
}
const QString name = text.mid(start, end - start + 1);
// Schlüsselwörter und leere Namen ausschließen
static const QStringList keywords = {
"if", "else", "elseif", "while", "for", "foreach",
"switch", "match", "catch", "function", "fn"
};
if (name.isEmpty() || keywords.contains(name.toLower()))
{
return QString();
}
// Nur den letzten Teil nach :: oder -> nehmen
const int colonPos = name.lastIndexOf("::");
const int arrowPos = name.lastIndexOf("->");
const int backslashPos = name.lastIndexOf("\\");
const int splitPos = qMax(backslashPos, qMax(colonPos, arrowPos));
if (splitPos >= 0)
{
return name.mid(splitPos + (name[splitPos] == ':' ? 2 : (name[splitPos] == '\\' ? 1 : 2)));
}
return name;
}
// ---------------------------------------------------------------------------
// Offene Klammern zählen
// ---------------------------------------------------------------------------
int SignatureHelper::countOpenParens(const QString &text) const
{
int depth = 0;
bool inString = false;
QChar stringChar;
for (int i = 0; i < text.length(); ++i)
{
const QChar ch = text[i];
// Einfache String-Erkennung (kein vollständiger PHP-Parser)
if (!inString && (ch == '\'' || ch == '"'))
{
inString = true;
stringChar = ch;
continue;
}
if (inString)
{
if (ch == stringChar && (i == 0 || text[i - 1] != '\\'))
{
inString = false;
}
continue;
}
if (ch == '(') { ++depth; }
else if (ch == ')') { --depth; }
}
return depth;
}

View File

@@ -0,0 +1,51 @@
#pragma once
#include <QObject>
#include <QString>
#include <QHash>
class CodeEditor;
class SignatureTooltip;
// ---------------------------------------------------------------------------
// SignatureHelper Lädt die PHP-Funktionsdatenbank und zeigt beim Tippen
// automatisch die passende Funktionssignatur als Tooltip.
//
// Logik:
// • Bei jedem Tastendruck: Text links vom Cursor analysieren
// • Wenn "funktionsname(" erkannt wird → Tooltip anzeigen
// • Wenn ")" die öffnende Klammer schließt → Tooltip verstecken
// • Wenn Cursor sich weg bewegt → Tooltip verstecken
// ---------------------------------------------------------------------------
class SignatureHelper : public QObject
{
Q_OBJECT
public:
explicit SignatureHelper(CodeEditor *editor);
private slots:
void onCursorPositionChanged();
private:
struct FunctionInfo
{
QString signature;
QString description;
};
void loadDatabase();
void loadProjectFunctions();
// Extrahiert den Funktionsnamen direkt links vor dem letzten '('
// Gibt leeren String zurück wenn kein Kontext gefunden
QString extractFunctionName(const QString &textUpToCursor) const;
// Zählt offene Klammern — bei 0 ist der Aufruf abgeschlossen
int countOpenParens(const QString &textUpToCursor) const;
CodeEditor *m_editor = nullptr;
SignatureTooltip *m_tooltip = nullptr;
QHash<QString, FunctionInfo> m_functions; // name → info
};

View File

@@ -0,0 +1,91 @@
#include "SignatureTooltip.h"
#include <QApplication>
#include <QScreen>
SignatureTooltip::SignatureTooltip(QWidget *parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setFrameShape(QFrame::StyledPanel);
setFrameShadow(QFrame::Raised);
setAttribute(Qt::WA_ShowWithoutActivating);
// Dezentes Styling passend zu Hell- und Dunkeltheme
setStyleSheet(
"SignatureTooltip {"
" background: palette(toolTipBase);"
" border: 1px solid palette(mid);"
" border-radius: 4px;"
" padding: 4px;"
"}"
);
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(8, 6, 8, 6);
m_layout->setSpacing(3);
// Signatur — Monospace, deutlich hervorgehoben
m_sigLabel = new QLabel(this);
m_sigLabel->setTextFormat(Qt::PlainText);
m_sigLabel->setWordWrap(false);
QFont sigFont = m_sigLabel->font();
sigFont.setFamily("Monospace");
sigFont.setStyleHint(QFont::Monospace);
sigFont.setPointSize(sigFont.pointSize());
m_sigLabel->setFont(sigFont);
m_sigLabel->setStyleSheet("color: palette(toolTipText); font-weight: bold;");
m_layout->addWidget(m_sigLabel);
// Beschreibung — kleiner, gedimmt
m_descLabel = new QLabel(this);
m_descLabel->setTextFormat(Qt::PlainText);
m_descLabel->setWordWrap(false);
m_descLabel->setStyleSheet("color: palette(mid);");
QFont descFont = m_descLabel->font();
descFont.setPointSize(qMax(descFont.pointSize() - 1, 8));
m_descLabel->setFont(descFont);
m_layout->addWidget(m_descLabel);
hide();
}
void SignatureTooltip::showSignature(const QString &signature,
const QString &description,
const QPoint &globalPos)
{
m_sigLabel->setText(signature);
if (description.isEmpty())
{
m_descLabel->hide();
}
else
{
m_descLabel->setText(description);
m_descLabel->show();
}
adjustSize();
// Position so wählen dass das Popup nicht aus dem Bildschirm ragt
QPoint pos = globalPos;
const QRect screen = QApplication::primaryScreen()->availableGeometry();
if (pos.x() + width() > screen.right())
{
pos.setX(screen.right() - width() - 4);
}
if (pos.y() + height() > screen.bottom())
{
pos.setY(globalPos.y() - height() - 24);
}
move(pos);
show();
raise();
}
void SignatureTooltip::hide()
{
QFrame::hide();
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <QFrame>
#include <QLabel>
#include <QString>
#include <QVBoxLayout>
// ---------------------------------------------------------------------------
// SignatureTooltip Schwebendes Popup das die Signatur einer Funktion zeigt.
// Erscheint unter dem Cursor, verschwindet automatisch wenn der Nutzer
// die Klammer schließt oder den Kontext verlässt.
// ---------------------------------------------------------------------------
class SignatureTooltip : public QFrame
{
Q_OBJECT
public:
explicit SignatureTooltip(QWidget *parent = nullptr);
void showSignature(const QString &signature,
const QString &description,
const QPoint &globalPos);
void hide();
private:
QVBoxLayout *m_layout = nullptr;
QLabel *m_sigLabel = nullptr;
QLabel *m_descLabel = nullptr;
};

View File

@@ -0,0 +1,256 @@
#include "VariableCompleter.h"
#include "CodeEditor.h"
#include <QTextCursor>
#include <QTextBlock>
#include <QKeyEvent>
#include <QListWidgetItem>
#include <QAbstractItemView>
#include <QScrollBar>
#include <QApplication>
#include <QScreen>
VariableCompleter::VariableCompleter(CodeEditor *editor)
: QObject(editor)
, m_editor(editor)
{
// Popup als reines Anzeige-Fenster — dieselbe Fensterart wie
// SignatureTooltip, die nachweislich nie den Fokus an sich reißt.
// Wichtig: Ein echtes Elternfenster (editor->window()) angeben, sonst
// behandelt der Fenstermanager das Popup als eigenständiges Fenster.
// Das führte dazu, dass bei mehreren offenen Dateien/Tabs verwaiste
// Popups entstehen konnten, die den Fokus übernehmen und die
// Texteingabe blockieren.
m_popup = new QListWidget(editor->window());
m_popup->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint);
m_popup->setAttribute(Qt::WA_ShowWithoutActivating);
m_popup->setFocusPolicy(Qt::NoFocus);
m_popup->setMaximumHeight(200);
m_popup->setMinimumWidth(180);
m_popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_popup->setStyleSheet(
"QListWidget {"
" border: 1px solid palette(mid);"
" background: palette(toolTipBase);"
" color: palette(toolTipText);"
" font-family: Monospace;"
"}"
"QListWidget::item:selected {"
" background: palette(highlight);"
" color: palette(highlightedText);"
"}"
);
connect(m_popup, &QListWidget::itemActivated,
this, &VariableCompleter::onItemActivated);
}
// ---------------------------------------------------------------------------
// Nach jedem Tastendruck aufrufen
// ---------------------------------------------------------------------------
void VariableCompleter::handleKeyPress(QKeyEvent *event)
{
// Popup-Navigation
if (m_popup->isVisible())
{
if (event->key() == Qt::Key_Escape)
{
hidePopup();
return;
}
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
{
if (m_popup->currentItem())
{
onItemActivated(m_popup->currentItem());
}
hidePopup();
return;
}
if (event->key() == Qt::Key_Down)
{
const int next = qMin(m_popup->currentRow() + 1,
m_popup->count() - 1);
m_popup->setCurrentRow(next);
return;
}
if (event->key() == Qt::Key_Up)
{
const int prev = qMax(m_popup->currentRow() - 1, 0);
m_popup->setCurrentRow(prev);
return;
}
}
// Nach dem normalen Tastendruck Popup aktualisieren
// (der Event wurde bereits an QPlainTextEdit weitergegeben)
updatePopup();
}
// ---------------------------------------------------------------------------
// Popup aktualisieren
// ---------------------------------------------------------------------------
void VariableCompleter::updatePopup()
{
const QString prefix = currentPrefix();
// Nur bei $ und mindestens einem weiteren Zeichen anzeigen
if (prefix.length() < 2 || !prefix.startsWith('$'))
{
hidePopup();
return;
}
const QStringList allVars = collectVariables();
const QString filter = prefix.toLower();
QStringList matches;
for (const QString &var : allVars)
{
if (var.toLower().startsWith(filter) && var != prefix)
{
matches.append(var);
}
}
if (matches.isEmpty())
{
hidePopup();
return;
}
// Popup befüllen
m_popup->clear();
for (const QString &var : matches)
{
m_popup->addItem(var);
}
m_popup->setCurrentRow(0);
// Größe anpassen
const int itemHeight = m_popup->sizeHintForRow(0) + 2;
const int height = qMin(matches.size() * itemHeight + 4, 200);
m_popup->setFixedHeight(height);
// Position unter dem Cursor
const QRect cursorRect = m_editor->cursorRect();
QPoint pos = m_editor->viewport()->mapToGlobal(
QPoint(cursorRect.left(), cursorRect.bottom() + 2)
);
// Nicht aus dem Bildschirm herausragen
const QRect screen = QApplication::primaryScreen()->availableGeometry();
if (pos.x() + m_popup->width() > screen.right())
{
pos.setX(screen.right() - m_popup->width());
}
if (pos.y() + height > screen.bottom())
{
pos.setY(cursorRect.top() - height - 2);
}
m_popup->move(pos);
m_popup->show();
m_popup->raise();
}
void VariableCompleter::hidePopup()
{
m_popup->hide();
m_popup->clear();
}
// ---------------------------------------------------------------------------
// Editor hat den Fokus verloren (z. B. Tab-Wechsel) — Popup schließen
// ---------------------------------------------------------------------------
void VariableCompleter::notifyFocusLost()
{
hidePopup();
}
// ---------------------------------------------------------------------------
// Variablen im aktuellen Dokument sammeln
// ---------------------------------------------------------------------------
QStringList VariableCompleter::collectVariables() const
{
static const QRegularExpression varRegex(R"(\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)");
QSet<QString> seen;
QStringList result;
const QString text = m_editor->toPlainText();
QRegularExpressionMatchIterator it = varRegex.globalMatch(text);
while (it.hasNext())
{
const QString var = it.next().captured(0);
if (!seen.contains(var))
{
seen.insert(var);
result.append(var);
}
}
result.sort(Qt::CaseInsensitive);
return result;
}
// ---------------------------------------------------------------------------
// Text links vom Cursor — das angefangene $variable
// ---------------------------------------------------------------------------
QString VariableCompleter::currentPrefix() const
{
const QTextCursor cursor = m_editor->textCursor();
const QString line = cursor.block().text();
const int col = cursor.columnNumber();
if (col == 0)
{
return QString();
}
// Rückwärts gehen bis zum $ oder einem Nicht-Bezeichner-Zeichen
int start = col - 1;
while (start > 0)
{
const QChar c = line[start - 1];
if (!c.isLetterOrNumber() && c != '_' && c != '$')
{
break;
}
--start;
}
const QString token = line.mid(start, col - start);
return token.startsWith('$') ? token : QString();
}
// ---------------------------------------------------------------------------
// Klick oder Enter — Variable einfügen
// ---------------------------------------------------------------------------
void VariableCompleter::onItemActivated(QListWidgetItem *item)
{
if (!item)
{
return;
}
const QString selected = item->text();
const QString prefix = currentPrefix();
if (prefix.isEmpty())
{
hidePopup();
return;
}
// Prefix durch den vollständigen Variablennamen ersetzen
QTextCursor cursor = m_editor->textCursor();
cursor.movePosition(QTextCursor::Left,
QTextCursor::KeepAnchor,
prefix.length());
cursor.insertText(selected);
m_editor->setTextCursor(cursor);
hidePopup();
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include <QObject>
#include <QListWidget>
#include <QStringList>
#include <QRegularExpression>
class CodeEditor;
// ---------------------------------------------------------------------------
// VariableCompleter Zeigt ein Popup mit passenden Variablennamen
// wenn der Nutzer $ tippt und weiter eingibt.
//
// Kein Autocomplete — nur Anzeige. Klick oder Enter übernimmt.
// Escape oder Weiterschreiben ohne Treffer schließt das Popup.
// ---------------------------------------------------------------------------
class VariableCompleter : public QObject
{
Q_OBJECT
public:
explicit VariableCompleter(CodeEditor *editor);
// Muss nach jedem Tastendruck aufgerufen werden
void handleKeyPress(QKeyEvent *event);
// Muss aufgerufen werden, wenn der Editor den Fokus verliert
// (z. B. beim Wechsel des Tabs) — schließt ein evtl. offenes Popup,
// damit es nicht als "Geisterfenster" stehen bleibt.
void notifyFocusLost();
private slots:
void onItemActivated(QListWidgetItem *item);
private:
void updatePopup();
void hidePopup();
QStringList collectVariables() const;
QString currentPrefix() const; // "$me" etc. links vom Cursor
CodeEditor *m_editor = nullptr;
QListWidget *m_popup = nullptr;
};

View File

@@ -0,0 +1,17 @@
set(FILETREE_SOURCES
FileTreePanel.cpp
FileTreePanel.h
)
add_library(BareCode_FileTree STATIC ${FILETREE_SOURCES})
target_link_libraries(BareCode_FileTree PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Widgets
)
target_include_directories(BareCode_FileTree PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
)

View File

@@ -0,0 +1,259 @@
#include "FileTreePanel.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QInputDialog>
#include <QMessageBox>
#include <QPoint>
FileTreePanel::FileTreePanel(QWidget *parent)
: QWidget(parent)
{
setupUi();
}
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
void FileTreePanel::setupUi()
{
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(0);
// Small header label showing the project name
m_label = new QLabel(tr("Kein Projekt geöffnet"), this);
m_label->setContentsMargins(6, 4, 6, 4);
m_label->setStyleSheet("font-weight: bold; background: palette(mid);");
m_label->setWordWrap(true);
m_layout->addWidget(m_label);
// File system model show only the project subtree
m_model = new QFileSystemModel(this);
m_model->setReadOnly(false);
m_model->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot);
// Tree view
m_tree = new QTreeView(this);
m_tree->setModel(m_model);
m_tree->setAnimated(true);
m_tree->setIndentation(16);
m_tree->setSortingEnabled(true);
m_tree->sortByColumn(0, Qt::AscendingOrder);
m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_tree->setHeaderHidden(true);
m_tree->setContextMenuPolicy(Qt::CustomContextMenu);
// Hide all columns except the file name
for (int col = 1; col < m_model->columnCount(); ++col)
{
m_tree->hideColumn(col);
}
m_layout->addWidget(m_tree);
connect(m_tree, &QTreeView::activated,
this, &FileTreePanel::onItemActivated);
connect(m_tree, &QTreeView::customContextMenuRequested,
this, &FileTreePanel::onContextMenuRequested);
}
// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------
void FileTreePanel::setRootPath(const QString &path)
{
const QModelIndex root = m_model->setRootPath(path);
m_tree->setRootIndex(root);
const QString projectName = QDir(path).dirName();
m_label->setText(projectName.isEmpty() ? path : projectName);
}
void FileTreePanel::clearRoot()
{
m_model->setRootPath(QString());
m_tree->setRootIndex(QModelIndex());
m_label->setText(tr("Kein Projekt geöffnet"));
}
void FileTreePanel::triggerNewFile()
{
onNewFile();
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
QString FileTreePanel::selectedDirectory() const
{
const QModelIndex index = m_tree->currentIndex();
if (!index.isValid())
{
return m_model->rootPath();
}
const QString path = m_model->filePath(index);
const QFileInfo info(path);
return info.isDir() ? path : info.absolutePath();
}
// ---------------------------------------------------------------------------
// Slots
// ---------------------------------------------------------------------------
void FileTreePanel::onItemActivated(const QModelIndex &index)
{
const QString path = m_model->filePath(index);
const QFileInfo info(path);
if (info.isFile())
{
emit fileActivated(path);
}
}
void FileTreePanel::onContextMenuRequested(const QPoint &pos)
{
QMenu menu(this);
QAction *actNewFile = menu.addAction(tr("Neue Datei…"));
QAction *actNewFolder = menu.addAction(tr("Neuer Ordner…"));
menu.addSeparator();
QAction *actDelete = menu.addAction(tr("Löschen"));
// Disable delete if nothing is selected
const QModelIndex index = m_tree->indexAt(pos);
actDelete->setEnabled(index.isValid());
QAction *chosen = menu.exec(m_tree->viewport()->mapToGlobal(pos));
if (chosen == actNewFile)
{
onNewFile();
}
else if (chosen == actNewFolder)
{
onNewFolder();
}
else if (chosen == actDelete)
{
onDeleteEntry();
}
}
void FileTreePanel::onNewFile()
{
const QString dir = selectedDirectory();
if (dir.isEmpty())
{
return;
}
bool ok = false;
const QString name = QInputDialog::getText(
this,
tr("Neue Datei"),
tr("Dateiname:"),
QLineEdit::Normal,
QString(),
&ok
);
if (!ok || name.trimmed().isEmpty())
{
return;
}
const QString filePath = QDir(dir).filePath(name.trimmed());
if (QFile::exists(filePath))
{
QMessageBox::warning(this, tr("Neue Datei"),
tr("Eine Datei mit diesem Namen existiert bereits:\n%1").arg(filePath));
return;
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly))
{
QMessageBox::critical(this, tr("Neue Datei"),
tr("Datei konnte nicht angelegt werden:\n%1").arg(filePath));
return;
}
file.close();
emit fileCreated(filePath);
// Select the new file in the tree
const QModelIndex newIndex = m_model->index(filePath);
m_tree->setCurrentIndex(newIndex);
m_tree->scrollTo(newIndex);
}
void FileTreePanel::onNewFolder()
{
const QString dir = selectedDirectory();
if (dir.isEmpty())
{
return;
}
bool ok = false;
const QString name = QInputDialog::getText(
this,
tr("Neuer Ordner"),
tr("Ordnername:"),
QLineEdit::Normal,
QString(),
&ok
);
if (!ok || name.trimmed().isEmpty())
{
return;
}
if (!QDir(dir).mkdir(name.trimmed()))
{
QMessageBox::critical(this, tr("Neuer Ordner"),
tr("Ordner konnte nicht angelegt werden:\n%1")
.arg(QDir(dir).filePath(name.trimmed())));
}
}
void FileTreePanel::onDeleteEntry()
{
const QModelIndex index = m_tree->currentIndex();
if (!index.isValid())
{
return;
}
const QString path = m_model->filePath(index);
const QFileInfo info(path);
const QString what = info.isDir() ? tr("Ordner") : tr("Datei");
const auto answer = QMessageBox::question(
this,
tr("%1 löschen").arg(what),
tr("%1 wirklich löschen?\n%2").arg(what, path),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No
);
if (answer != QMessageBox::Yes)
{
return;
}
if (info.isDir())
{
QDir(path).removeRecursively();
}
else
{
QFile::remove(path);
}
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <QWidget>
#include <QTreeView>
#include <QFileSystemModel>
#include <QVBoxLayout>
#include <QLabel>
#include <QString>
#include <QMenu>
#include <QAction>
// ---------------------------------------------------------------------------
// FileTreePanel Left panel showing the project directory tree.
// Emits fileActivated(path) when the user double-clicks a file.
// Supports creating new files/folders via context menu.
// ---------------------------------------------------------------------------
class FileTreePanel : public QWidget
{
Q_OBJECT
public:
explicit FileTreePanel(QWidget *parent = nullptr);
void setRootPath(const QString &path);
void clearRoot();
void triggerNewFile(); // Called from MainWindow menu action
signals:
void fileActivated(const QString &filePath);
void fileCreated(const QString &filePath);
private slots:
void onItemActivated(const QModelIndex &index);
void onContextMenuRequested(const QPoint &pos);
void onNewFile();
void onNewFolder();
void onDeleteEntry();
private:
void setupUi();
// Returns the directory of the currently selected item
QString selectedDirectory() const;
QVBoxLayout *m_layout = nullptr;
QLabel *m_label = nullptr;
QTreeView *m_tree = nullptr;
QFileSystemModel *m_model = nullptr;
};

View File

@@ -0,0 +1,19 @@
set(HIGHLIGHTER_SOURCES
SyntaxHighlighter.cpp
SyntaxHighlighter.h
HighlighterFactory.cpp
HighlighterFactory.h
)
add_library(BareCode_Highlighter STATIC ${HIGHLIGHTER_SOURCES})
target_link_libraries(BareCode_Highlighter PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Widgets
)
target_include_directories(BareCode_Highlighter PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
)

View File

@@ -0,0 +1,45 @@
#include "HighlighterFactory.h"
#include <QFileInfo>
#include <QHash>
#include <functional>
SyntaxHighlighter *HighlighterFactory::createForFile(const QString &filePath,
QTextDocument *document)
{
// Erweiterung → Highlighter-Fabrik
// Neue Sprache hinzufügen: einfach einen Eintrag ergänzen.
static const QHash<QString, std::function<SyntaxHighlighter *(QTextDocument *)>> registry =
{
// C / C++
{ "c", [](QTextDocument *d) { return new CppHighlighter(d); } },
{ "cc", [](QTextDocument *d) { return new CppHighlighter(d); } },
{ "cpp", [](QTextDocument *d) { return new CppHighlighter(d); } },
{ "cxx", [](QTextDocument *d) { return new CppHighlighter(d); } },
{ "h", [](QTextDocument *d) { return new CppHighlighter(d); } },
{ "hpp", [](QTextDocument *d) { return new CppHighlighter(d); } },
{ "hxx", [](QTextDocument *d) { return new CppHighlighter(d); } },
// CSS
{ "css", [](QTextDocument *d) { return new CssHighlighter(d); } },
// HTML / Templates
{ "html", [](QTextDocument *d) { return new HtmlHighlighter(d); } },
{ "htm", [](QTextDocument *d) { return new HtmlHighlighter(d); } },
{ "xhtml",[](QTextDocument *d) { return new HtmlHighlighter(d); } },
// PHP (HTML + eingebettetes PHP)
{ "php", [](QTextDocument *d) { return new PhpHighlighter(d); } },
{ "phtml",[](QTextDocument *d) { return new PhpHighlighter(d); } },
{ "php3", [](QTextDocument *d) { return new PhpHighlighter(d); } },
{ "php4", [](QTextDocument *d) { return new PhpHighlighter(d); } },
{ "php5", [](QTextDocument *d) { return new PhpHighlighter(d); } },
{ "php7", [](QTextDocument *d) { return new PhpHighlighter(d); } },
{ "php8", [](QTextDocument *d) { return new PhpHighlighter(d); } },
};
const QString ext = QFileInfo(filePath).suffix().toLower();
const auto it = registry.find(ext);
return (it != registry.end()) ? it.value()(document) : nullptr;
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include <QString>
#include <QTextDocument>
#include "SyntaxHighlighter.h"
// ---------------------------------------------------------------------------
// HighlighterFactory Maps file extensions to the correct highlighter.
// To add a new language, register it in HighlighterFactory.cpp.
// ---------------------------------------------------------------------------
class HighlighterFactory
{
public:
// Creates and returns a highlighter for the given file path.
// Returns nullptr if no highlighter is registered for this file type.
static SyntaxHighlighter *createForFile(const QString &filePath,
QTextDocument *document);
};

View File

@@ -0,0 +1,589 @@
#include "SyntaxHighlighter.h"
#include <QTextDocument>
// ===========================================================================
// SyntaxHighlighter Basis
// ===========================================================================
SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent)
: QSyntaxHighlighter(parent)
{
}
void SyntaxHighlighter::highlightBlock(const QString &text)
{
// Einzel-Zeilen-Regeln anwenden
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)
{
setCurrentBlockState(0);
return;
}
// Mehrzeilige Kommentare
// Zustand 0 = normal, 1 = mitten in /* ... */
setCurrentBlockState(0);
// Leere Zeile mitten im Kommentar — Zustand weitertragen
if (text.isEmpty())
{
if (previousBlockState() == 1)
{
setCurrentBlockState(1);
}
return;
}
int startIndex = 0;
if (previousBlockState() == 1)
{
// Vorherige Zeile war mitten im Kommentar — ab Zeilenanfang nach Ende suchen
QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, 0);
if (endMatch.hasMatch())
{
// Ende des Kommentars gefunden
const int commentLength = static_cast<int>(endMatch.capturedStart())
+ static_cast<int>(endMatch.capturedLength());
setFormat(0, commentLength, m_multiLineCommentFormat);
// Nach weiteren Kommentaren in der gleichen Zeile suchen
QRegularExpressionMatch nextStart =
m_commentStartExpression.match(text, commentLength);
startIndex = nextStart.hasMatch()
? static_cast<int>(nextStart.capturedStart())
: -1;
}
else
{
// Noch kein Ende — gesamte Zeile ist Kommentar
setFormat(0, text.length(), m_multiLineCommentFormat);
setCurrentBlockState(1);
return;
}
}
else
{
// Neuen Kommentaranfang suchen
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
{
// Kommentar geht über Zeilenende
setCurrentBlockState(1);
commentLength = text.length() - startIndex;
}
setFormat(startIndex, commentLength, m_multiLineCommentFormat);
if (!endMatch.hasMatch())
{
break;
}
// Nach weiteren Kommentaren in dieser Zeile suchen
QRegularExpressionMatch nextStart =
m_commentStartExpression.match(text, startIndex + commentLength);
startIndex = nextStart.hasMatch()
? static_cast<int>(nextStart.capturedStart())
: -1;
}
}
// ===========================================================================
// CppHighlighter
// ===========================================================================
CppHighlighter::CppHighlighter(QTextDocument *parent)
: SyntaxHighlighter(parent)
{
m_hasMultiLineComment = true;
QTextCharFormat keywordFormat;
keywordFormat.setForeground(QColor("#569CD6"));
keywordFormat.setFontWeight(QFont::Bold);
const QStringList keywords = {
"alignas","alignof","and","and_eq","asm","auto","bitand","bitor",
"bool","break","case","catch","char","char8_t","char16_t","char32_t",
"class","compl","concept","const","consteval","constexpr","constinit",
"const_cast","continue","co_await","co_return","co_yield","decltype",
"default","delete","do","double","dynamic_cast","else","enum",
"explicit","export","extern","false","float","for","friend","goto",
"if","inline","int","long","mutable","namespace","new","noexcept",
"not","not_eq","nullptr","operator","or","or_eq","private","protected",
"public","register","reinterpret_cast","requires","return","short",
"signed","sizeof","static","static_assert","static_cast","struct",
"switch","template","this","thread_local","throw","true","try",
"typedef","typeid","typename","union","unsigned","using","virtual",
"void","volatile","wchar_t","while","xor","xor_eq","override","final"
};
for (const QString &kw : keywords)
{
HighlightRule rule;
rule.pattern = QRegularExpression(QString("\\b%1\\b").arg(kw));
rule.format = keywordFormat;
m_rules.append(rule);
}
QTextCharFormat preprocFormat;
preprocFormat.setForeground(QColor("#C586C0"));
{ HighlightRule r; r.pattern = QRegularExpression("^\\s*#\\s*\\w+"); r.format = preprocFormat; m_rules.append(r); }
QTextCharFormat stringFormat;
stringFormat.setForeground(QColor("#CE9178"));
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*")"); r.format = stringFormat; m_rules.append(r); }
{ HighlightRule r; r.pattern = QRegularExpression(R"('(?:[^'\\]|\\.)*')"); r.format = stringFormat; m_rules.append(r); }
QTextCharFormat numberFormat;
numberFormat.setForeground(QColor("#B5CEA8"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b(0[xX][0-9A-Fa-f]+[uUlL]*|[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?[fFlLuU]*)\b)"); r.format = numberFormat; m_rules.append(r); }
QTextCharFormat commentFormat;
commentFormat.setForeground(QColor("#6A9955"));
commentFormat.setFontItalic(true);
{ HighlightRule r; r.pattern = QRegularExpression("//[^\n]*"); r.format = commentFormat; m_rules.append(r); }
m_multiLineCommentFormat = commentFormat;
m_commentStartExpression = QRegularExpression(R"(/\*)");
m_commentEndExpression = QRegularExpression(R"(\*/)");
}
void CppHighlighter::highlightBlock(const QString &text)
{
SyntaxHighlighter::highlightBlock(text);
}
// ===========================================================================
// CssHighlighter
// ===========================================================================
CssHighlighter::CssHighlighter(QTextDocument *parent)
: SyntaxHighlighter(parent)
{
m_hasMultiLineComment = true;
// Selektoren: .klasse #id element ::pseudo :pseudo
QTextCharFormat selectorFormat;
selectorFormat.setForeground(QColor("#D7BA7D"));
{ HighlightRule r; r.pattern = QRegularExpression(R"([.#]?[\w-]+\s*(?=\s*[,{]))"); r.format = selectorFormat; m_rules.append(r); }
{ HighlightRule r; r.pattern = QRegularExpression(R"(:{1,2}[\w-]+)"); r.format = selectorFormat; m_rules.append(r); }
// Eigenschaften (property:)
QTextCharFormat propFormat;
propFormat.setForeground(QColor("#9CDCFE"));
{ HighlightRule r; r.pattern = QRegularExpression(R"([\w-]+\s*(?=:))"); r.format = propFormat; m_rules.append(r); }
// Werte Farben #hex
QTextCharFormat colorFormat;
colorFormat.setForeground(QColor("#CE9178"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(#[0-9A-Fa-f]{3,8}\b)"); r.format = colorFormat; m_rules.append(r); }
// Zahlen + Einheiten
QTextCharFormat numberFormat;
numberFormat.setForeground(QColor("#B5CEA8"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b\d+\.?\d*(px|em|rem|%|vh|vw|pt|cm|mm|s|ms)?\b)"); r.format = numberFormat; m_rules.append(r); }
// Strings
QTextCharFormat stringFormat;
stringFormat.setForeground(QColor("#CE9178"));
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = stringFormat; m_rules.append(r); }
// !important
QTextCharFormat importantFormat;
importantFormat.setForeground(QColor("#F44747"));
importantFormat.setFontWeight(QFont::Bold);
{ HighlightRule r; r.pattern = QRegularExpression(R"(!important)"); r.format = importantFormat; m_rules.append(r); }
// @-Regeln
QTextCharFormat atFormat;
atFormat.setForeground(QColor("#C586C0"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(@[\w-]+)"); r.format = atFormat; m_rules.append(r); }
QTextCharFormat commentFormat;
commentFormat.setForeground(QColor("#6A9955"));
commentFormat.setFontItalic(true);
m_multiLineCommentFormat = commentFormat;
m_commentStartExpression = QRegularExpression(R"(/\*)");
m_commentEndExpression = QRegularExpression(R"(\*/)");
}
void CssHighlighter::highlightBlock(const QString &text)
{
SyntaxHighlighter::highlightBlock(text);
}
// ===========================================================================
// HtmlHighlighter
// ===========================================================================
HtmlHighlighter::HtmlHighlighter(QTextDocument *parent)
: SyntaxHighlighter(parent)
{
// Tag-Namen <div </div />
QTextCharFormat tagFormat;
tagFormat.setForeground(QColor("#569CD6"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(</?[\w:-]+)"); r.format = tagFormat; m_rules.append(r); }
{ HighlightRule r; r.pattern = QRegularExpression(R"(/?>)"); r.format = tagFormat; m_rules.append(r); }
// Attribute name=
QTextCharFormat attrFormat;
attrFormat.setForeground(QColor("#9CDCFE"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b[\w:-]+=)"); r.format = attrFormat; m_rules.append(r); }
// Attributwerte "wert" 'wert'
QTextCharFormat valueFormat;
valueFormat.setForeground(QColor("#CE9178"));
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = valueFormat; m_rules.append(r); }
// DOCTYPE
QTextCharFormat doctypeFormat;
doctypeFormat.setForeground(QColor("#808080"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(<!DOCTYPE[^>]*>)", QRegularExpression::CaseInsensitiveOption); r.format = doctypeFormat; m_rules.append(r); }
// Entities &amp; &#123;
QTextCharFormat entityFormat;
entityFormat.setForeground(QColor("#D7BA7D"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(&(?:#\d+|#x[0-9A-Fa-f]+|[\w]+);)"); r.format = entityFormat; m_rules.append(r); }
// Kommentare <!-- ... --> (mehrzeilig)
QTextCharFormat commentFormat;
commentFormat.setForeground(QColor("#6A9955"));
commentFormat.setFontItalic(true);
m_multiLineCommentFormat = commentFormat;
m_commentStartExpression = QRegularExpression("<!--");
m_commentEndExpression = QRegularExpression("-->");
m_hasMultiLineComment = true;
}
void HtmlHighlighter::highlightBlock(const QString &text)
{
SyntaxHighlighter::highlightBlock(text);
}
// ===========================================================================
// PhpHighlighter
// ===========================================================================
PhpHighlighter::PhpHighlighter(QTextDocument *parent)
: SyntaxHighlighter(parent)
{
// ---- HTML-Regeln (Basis, für den Teil außerhalb von <?php ?>) ----
QTextCharFormat tagFormat;
tagFormat.setForeground(QColor("#569CD6"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(</?[\w:-]+)"); r.format = tagFormat; m_rules.append(r); }
{ HighlightRule r; r.pattern = QRegularExpression(R"(/?>)"); r.format = tagFormat; m_rules.append(r); }
QTextCharFormat attrFormat;
attrFormat.setForeground(QColor("#9CDCFE"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b[\w:-]+=)"); r.format = attrFormat; m_rules.append(r); }
QTextCharFormat valueFormat;
valueFormat.setForeground(QColor("#CE9178"));
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = valueFormat; m_rules.append(r); }
QTextCharFormat entityFormat;
entityFormat.setForeground(QColor("#D7BA7D"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(&(?:#\d+|#x[0-9A-Fa-f]+|[\w]+);)"); r.format = entityFormat; m_rules.append(r); }
// HTML-Kommentare
QTextCharFormat htmlCommentFormat;
htmlCommentFormat.setForeground(QColor("#6A9955"));
htmlCommentFormat.setFontItalic(true);
m_multiLineCommentFormat = htmlCommentFormat;
m_commentStartExpression = QRegularExpression("<!--");
m_commentEndExpression = QRegularExpression("-->");
m_hasMultiLineComment = true;
// ---- PHP-Tags hervorheben ----
m_phpTagFormat.setForeground(QColor("#C586C0"));
m_phpTagFormat.setFontWeight(QFont::Bold);
// ---- PHP-spezifische Regeln ----
m_phpStringFormat.setForeground(QColor("#CE9178"));
m_phpCommentFormat.setForeground(QColor("#6A9955"));
m_phpCommentFormat.setFontItalic(true);
// Keywords
QTextCharFormat kwFormat;
kwFormat.setForeground(QColor("#569CD6"));
kwFormat.setFontWeight(QFont::Bold);
const QStringList phpKeywords = {
"abstract","and","array","as","break","callable","case","catch",
"class","clone","const","continue","declare","default","die","do",
"echo","else","elseif","empty","enddeclare","endfor","endforeach",
"endif","endswitch","endwhile","enum","extends","final","finally",
"fn","for","foreach","function","global","goto","if","implements",
"include","include_once","instanceof","insteadof","interface",
"isset","list","match","namespace","new","or","print","private",
"protected","public","readonly","require","require_once","return",
"static","switch","throw","trait","try","unset","use","var",
"while","xor","yield","null","true","false","NULL","TRUE","FALSE"
};
for (const QString &kw : phpKeywords)
{
HighlightRule r;
r.pattern = QRegularExpression(QString("\\b%1\\b").arg(kw));
r.format = kwFormat;
m_phpRules.append(r);
}
// Variablen $var
QTextCharFormat varFormat;
varFormat.setForeground(QColor("#9CDCFE"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(\$[\w]+)"); r.format = varFormat; m_phpRules.append(r); }
// Strings
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = m_phpStringFormat; m_phpRules.append(r); }
// Zahlen
QTextCharFormat numFormat;
numFormat.setForeground(QColor("#B5CEA8"));
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b\d+\.?\d*\b)"); r.format = numFormat; m_phpRules.append(r); }
// Einzeilige Kommentare
{ HighlightRule r; r.pattern = QRegularExpression(R"((//|#)[^\n]*)"); r.format = m_phpCommentFormat; m_phpRules.append(r); }
// Eingebaute Funktionen (Auswahl der häufigsten)
QTextCharFormat builtinFormat;
builtinFormat.setForeground(QColor("#DCDCAA"));
const QStringList builtins = {
"array_map","array_filter","array_keys","array_values","array_merge",
"array_push","array_pop","array_shift","array_slice","array_splice",
"count","strlen","substr","strpos","strtolower","strtoupper","trim",
"ltrim","rtrim","explode","implode","str_replace","preg_match",
"preg_replace","sprintf","printf","print_r","var_dump","isset",
"empty","unset","intval","floatval","strval","is_array","is_string",
"is_int","is_float","is_null","is_bool","is_numeric","date","time",
"mktime","json_encode","json_decode","header","session_start",
"htmlspecialchars","htmlentities","strip_tags","nl2br","round",
"floor","ceil","abs","min","max","rand","in_array","array_key_exists",
"sort","rsort","usort","ksort","krsort","ob_start","ob_get_clean"
};
for (const QString &fn : builtins)
{
HighlightRule r;
r.pattern = QRegularExpression(QString("\\b%1\\b").arg(fn));
r.format = builtinFormat;
m_phpRules.append(r);
}
}
void PhpHighlighter::highlightPhpRange(const QString &text, int start, int length)
{
if (length <= 0)
{
return;
}
const QString phpText = text.mid(start, length);
for (const HighlightRule &rule : m_phpRules)
{
QRegularExpressionMatchIterator it = rule.pattern.globalMatch(phpText);
while (it.hasNext())
{
QRegularExpressionMatch match = it.next();
setFormat(
start + static_cast<int>(match.capturedStart()),
static_cast<int>(match.capturedLength()),
rule.format
);
}
}
static const QRegularExpression blockOpen(R"(/\*)");
static const QRegularExpression blockClose(R"(\*/)");
int searchFrom = 0;
// Leere Zeile mitten im PHP-Blockkommentar — Zustand weitertragen
if (phpText.trimmed().isEmpty())
{
if (previousBlockState() == 3)
{
setFormat(start, length, m_phpCommentFormat);
setCurrentBlockState(3);
}
return;
}
if (previousBlockState() == 3)
{
QRegularExpressionMatch closeMatch = blockClose.match(phpText, 0);
if (closeMatch.hasMatch())
{
const int end = static_cast<int>(closeMatch.capturedStart())
+ static_cast<int>(closeMatch.capturedLength());
setFormat(start, end, m_phpCommentFormat);
searchFrom = end;
}
else
{
setFormat(start, length, m_phpCommentFormat);
setCurrentBlockState(3);
return;
}
}
while (searchFrom < phpText.length())
{
QRegularExpressionMatch openMatch = blockOpen.match(phpText, searchFrom);
if (!openMatch.hasMatch())
{
break;
}
const int openPos = static_cast<int>(openMatch.capturedStart());
QRegularExpressionMatch closeMatch = blockClose.match(phpText, openPos + 2);
if (closeMatch.hasMatch())
{
const int closeEnd = static_cast<int>(closeMatch.capturedStart())
+ static_cast<int>(closeMatch.capturedLength());
setFormat(start + openPos, closeEnd - openPos, m_phpCommentFormat);
searchFrom = closeEnd;
}
else
{
setFormat(start + openPos, length - openPos, m_phpCommentFormat);
setCurrentBlockState(3);
return;
}
}
}
void PhpHighlighter::highlightBlock(const QString &text)
{
// Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden
// (setzt auch den State für HTML <!-- --> Kommentare)
SyntaxHighlighter::highlightBlock(text);
// Block-Zustände:
// 0 = HTML-Modus
// 1 = HTML <!-- --> Kommentar (von Basisklasse verwaltet)
// 2 = innerhalb PHP-Block (kein /* */ Kommentar)
// 3 = innerhalb PHP /* */ Block-Kommentar
// State 1 (HTML-Kommentar) wurde von der Basisklasse gesetzt — nicht überschreiben
if (currentBlockState() == 1)
{
return;
}
// Wenn der vorherige Block ein HTML-Kommentar war und dieser noch nicht
// abgeschlossen wurde, hat die Basisklasse das bereits korrekt behandelt.
// Wir setzen nur dann auf 0 zurück wenn wir sicher nicht in HTML-Kommentar sind.
if (previousBlockState() != 1)
{
setCurrentBlockState(0);
}
// Leere Zeile — Zustand weitertragen
if (text.isEmpty())
{
const int prev = previousBlockState();
if (prev == 2 || prev == 3)
{
setCurrentBlockState(prev);
}
return;
}
static const QRegularExpression phpOpen(R"(<\?(?:php|=)?\s?)",
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression phpClose(R"(\?>)");
int pos = 0;
if (previousBlockState() == 2 || previousBlockState() == 3)
{
// Wir befinden uns bereits in einem PHP-Block (ggf. in einem Kommentar)
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());
if (currentBlockState() != 3)
{
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());
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
{
highlightPhpRange(text, openEnd, text.length() - openEnd);
if (currentBlockState() != 3)
{
setCurrentBlockState(2);
}
return;
}
}
}

View File

@@ -0,0 +1,94 @@
#pragma once
#include <QSyntaxHighlighter>
#include <QTextCharFormat>
#include <QRegularExpression>
#include <QVector>
// ---------------------------------------------------------------------------
// SyntaxHighlighter Regelbasierte Basis-Klasse.
// Unterklassen befüllen m_rules und können highlightBlock() überschreiben
// um mehrzeilige Konstrukte (Block-Kommentare, heredocs, …) zu behandeln.
// ---------------------------------------------------------------------------
class SyntaxHighlighter : public QSyntaxHighlighter
{
Q_OBJECT
public:
explicit SyntaxHighlighter(QTextDocument *parent = nullptr);
protected:
struct HighlightRule
{
QRegularExpression pattern;
QTextCharFormat format;
};
void highlightBlock(const QString &text) override;
// Unterklassen befüllen dies im Konstruktor
QVector<HighlightRule> m_rules;
// Mehrzeilige Block-Kommentare (/* ... */)
QRegularExpression m_commentStartExpression;
QRegularExpression m_commentEndExpression;
QTextCharFormat m_multiLineCommentFormat;
bool m_hasMultiLineComment = false;
};
// ---------------------------------------------------------------------------
// CppHighlighter C und C++
// ---------------------------------------------------------------------------
class CppHighlighter : public SyntaxHighlighter
{
Q_OBJECT
public:
explicit CppHighlighter(QTextDocument *parent = nullptr);
protected:
void highlightBlock(const QString &text) override;
};
// ---------------------------------------------------------------------------
// CssHighlighter CSS
// ---------------------------------------------------------------------------
class CssHighlighter : public SyntaxHighlighter
{
Q_OBJECT
public:
explicit CssHighlighter(QTextDocument *parent = nullptr);
protected:
void highlightBlock(const QString &text) override;
};
// ---------------------------------------------------------------------------
// HtmlHighlighter HTML (mit eingebettetem CSS in <style> und JS in <script>)
// ---------------------------------------------------------------------------
class HtmlHighlighter : public SyntaxHighlighter
{
Q_OBJECT
public:
explicit HtmlHighlighter(QTextDocument *parent = nullptr);
protected:
void highlightBlock(const QString &text) override;
};
// ---------------------------------------------------------------------------
// PhpHighlighter PHP (HTML + eingebettetes PHP zwischen <?php ... ?>)
// ---------------------------------------------------------------------------
class PhpHighlighter : public SyntaxHighlighter
{
Q_OBJECT
public:
explicit PhpHighlighter(QTextDocument *parent = nullptr);
protected:
void highlightBlock(const QString &text) override;
private:
// Hilfsmethode: wendet PHP-Regeln auf einen Teilbereich an
void highlightPhpRange(const QString &text, int start, int length);
QVector<HighlightRule> m_phpRules;
QTextCharFormat m_phpStringFormat;
QTextCharFormat m_phpCommentFormat;
QTextCharFormat m_phpTagFormat;
};

View File

@@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE" sourcelanguage="de_DE">
<context>
<name>MainWindow</name>
<message><source>&amp;Datei</source><translation>&amp;Datei</translation></message>
<message><source>&amp;Neue Datei</source><translation>&amp;Neue Datei</translation></message>
<message><source>Datei &amp;öffnen</source><translation>Datei &amp;öffnen</translation></message>
<message><source>&amp;Projekt öffnen</source><translation>&amp;Projekt öffnen</translation></message>
<message><source>Projekt &amp;schließen</source><translation>Projekt &amp;schließen</translation></message>
<message><source>&amp;Speichern</source><translation>&amp;Speichern</translation></message>
<message><source>Speichern &amp;unter</source><translation>Speichern &amp;unter</translation></message>
<message><source>&amp;Alles speichern</source><translation>&amp;Alles speichern</translation></message>
<message><source>&amp;Beenden</source><translation>&amp;Beenden</translation></message>
<message><source>&amp;Bearbeiten</source><translation>&amp;Bearbeiten</translation></message>
<message><source>&amp;Rückgängig</source><translation>&amp;Rückgängig</translation></message>
<message><source>&amp;Wiederholen</source><translation>&amp;Wiederholen</translation></message>
<message><source>&amp;Suchen / Ersetzen</source><translation>&amp;Suchen / Ersetzen</translation></message>
<message><source>In &amp;Dateien suchen</source><translation>In &amp;Dateien suchen</translation></message>
<message><source>&amp;Projektfunktionen</source><translation>&amp;Projektfunktionen</translation></message>
<message><source>&amp;Tote Funktionen suchen</source><translation>&amp;Tote Funktionen suchen</translation></message>
<message><source>&amp;Ansicht</source><translation>&amp;Ansicht</translation></message>
<message><source>&amp;Dark Mode</source><translation>&amp;Dark Mode</translation></message>
<message><source>Sprache</source><translation>Sprache</translation></message>
<message><source>&amp;Hilfe</source><translation>&amp;Hilfe</translation></message>
<message><source>&amp;Über BareCode</source><translation>&amp;Über BareCode</translation></message>
<message><source>Bereit</source><translation>Bereit</translation></message>
<message><source>Alle Dateien gespeichert</source><translation>Alle Dateien gespeichert</translation></message>
<message><source>Gespeichert: %1</source><translation>Gespeichert: %1</translation></message>
<message><source>Projekt geöffnet: %1</source><translation>Projekt geöffnet: %1</translation></message>
<message><source>Projekt geschlossen</source><translation>Projekt geschlossen</translation></message>
<message><source>Sprache geändert</source><translation>Sprache geändert</translation></message>
<message><source>Die Sprache wird beim nächsten Start von BareCode aktiv.</source><translation>Die Sprache wird beim nächsten Start von BareCode aktiv.</translation></message>
</context>
<context>
<name>AboutDialog</name>
<message><source>Über BareCode</source><translation>Über BareCode</translation></message>
<message><source>Modularer Code-Editor</source><translation>Modularer Code-Editor</translation></message>
<message><source>Version</source><translation>Version</translation></message>
<message><source>Entwickler</source><translation>Entwickler</translation></message>
<message><source>Projekt</source><translation>Projekt</translation></message>
<message><source>Framework</source><translation>Framework</translation></message>
<message><source>Sprache</source><translation>Sprache</translation></message>
<message><source>Schließen</source><translation>Schließen</translation></message>
</context>
<context>
<name>FileTreePanel</name>
<message><source>Kein Projekt geöffnet</source><translation>Kein Projekt geöffnet</translation></message>
<message><source>Neue Datei</source><translation>Neue Datei</translation></message>
<message><source>Neuer Ordner</source><translation>Neuer Ordner</translation></message>
<message><source>Löschen</source><translation>Löschen</translation></message>
<message><source>Neue Datei</source><translation>Neue Datei</translation></message>
<message><source>Dateiname:</source><translation>Dateiname:</translation></message>
<message><source>Eine Datei mit diesem Namen existiert bereits:
%1</source><translation>Eine Datei mit diesem Namen existiert bereits:
%1</translation></message>
<message><source>Datei konnte nicht angelegt werden:
%1</source><translation>Datei konnte nicht angelegt werden:
%1</translation></message>
<message><source>Neuer Ordner</source><translation>Neuer Ordner</translation></message>
<message><source>Ordnername:</source><translation>Ordnername:</translation></message>
<message><source>Ordner konnte nicht angelegt werden:
%1</source><translation>Ordner konnte nicht angelegt werden:
%1</translation></message>
<message><source>%1 löschen</source><translation>%1 löschen</translation></message>
<message><source>%1 wirklich löschen?
%2</source><translation>%1 wirklich löschen?
%2</translation></message>
<message><source>Datei</source><translation>Datei</translation></message>
<message><source>Ordner</source><translation>Ordner</translation></message>
</context>
<context>
<name>EditorTab</name>
<message><source>Speichern</source><translation>Speichern</translation></message>
<message><source>Datei konnte nicht gespeichert werden:
%1</source><translation>Datei konnte nicht gespeichert werden:
%1</translation></message>
<message><source>Speichern unter</source><translation>Speichern unter</translation></message>
<message><source>Datei konnte nicht geschrieben werden:
%1</source><translation>Datei konnte nicht geschrieben werden:
%1</translation></message>
</context>
<context>
<name>CodeEditor</name>
<message><source>Datei öffnen</source><translation>Datei öffnen</translation></message>
<message><source>Open File</source><translation>Datei öffnen</translation></message>
<message><source>Cannot open file:
%1</source><translation>Datei kann nicht geöffnet werden:
%1</translation></message>
</context>
<context>
<name>SearchPanel</name>
<message><source>Suchen</source><translation>Suchen</translation></message>
<message><source>Suchen:</source><translation>Suchen:</translation></message>
<message><source></source><translation></translation></message>
<message><source></source><translation></translation></message>
<message><source>Vorheriger Treffer (Shift+F3)</source><translation>Vorheriger Treffer (Shift+F3)</translation></message>
<message><source>Nächster Treffer (F3)</source><translation>Nächster Treffer (F3)</translation></message>
<message><source>Schließen (Esc)</source><translation>Schließen (Esc)</translation></message>
<message><source></source><translation></translation></message>
<message><source>Ersetzen durch</source><translation>Ersetzen durch</translation></message>
<message><source>Ersetzen:</source><translation>Ersetzen:</translation></message>
<message><source>Ersetzen</source><translation>Ersetzen</translation></message>
<message><source>Alle ersetzen</source><translation>Alle ersetzen</translation></message>
<message><source>In Auswahl ersetzen</source><translation>In Auswahl ersetzen</translation></message>
<message><source>Groß-/Kleinschreibung</source><translation>Groß-/Kleinschreibung</translation></message>
<message><source>Ganzes Wort</source><translation>Ganzes Wort</translation></message>
<message><source>Regulärer Ausdruck</source><translation>Regulärer Ausdruck</translation></message>
<message><source>Kein Treffer</source><translation>Kein Treffer</translation></message>
<message><source>%1 Treffer</source><translation>%1 Treffer</translation></message>
<message><source>%1 Ersetzung(en) durchgeführt.</source><translation>%1 Ersetzung(en) durchgeführt.</translation></message>
<message><source>Es ist kein Text ausgewählt.</source><translation>Es ist kein Text ausgewählt.</translation></message>
<message><source>%1 Ersetzung(en) in der Auswahl durchgeführt.</source><translation>%1 Ersetzung(en) in der Auswahl durchgeführt.</translation></message>
</context>
<context>
<name>FileSearchPanel</name>
<message><source>Suchen in Dateien:</source><translation>Suchen in Dateien:</translation></message>
<message><source>Suchbegriff</source><translation>Suchbegriff</translation></message>
<message><source>Suchen</source><translation>Suchen</translation></message>
<message><source></source><translation></translation></message>
<message><source>Schließen</source><translation>Schließen</translation></message>
<message><source>Groß-/Kleinschreibung</source><translation>Groß-/Kleinschreibung</translation></message>
<message><source>Ganzes Wort</source><translation>Ganzes Wort</translation></message>
<message><source>Regex</source><translation>Regex</translation></message>
<message><source>Dateitypen:</source><translation>Dateitypen:</translation></message>
<message><source>Leerzeichen-getrennte Muster, z.B.: *.php *.html</source><translation>Leerzeichen-getrennte Muster, z.B.: *.php *.html</translation></message>
<message><source>Suche läuft</source><translation>Suche läuft</translation></message>
<message><source>Kein Projektverzeichnis geöffnet.</source><translation>Kein Projektverzeichnis geöffnet.</translation></message>
<message><source>Keine Treffer gefunden.</source><translation>Keine Treffer gefunden.</translation></message>
<message><source>%1 Treffer in %2 Datei(en).</source><translation>%1 Treffer in %2 Datei(en).</translation></message>
</context>
<context>
<name>FunctionListPanel</name>
<message><source>Projektfunktionen</source><translation>Projektfunktionen</translation></message>
<message><source></source><translation></translation></message>
<message><source>Neu scannen</source><translation>Neu scannen</translation></message>
<message><source>Funktion suchen</source><translation>Funktion suchen</translation></message>
<message><source>Nach Datei</source><translation>Nach Datei</translation></message>
<message><source>Nach Klasse</source><translation>Nach Klasse</translation></message>
<message><source>Alphabetisch</source><translation>Alphabetisch</translation></message>
<message><source>Kein Projekt geöffnet.</source><translation>Kein Projekt geöffnet.</translation></message>
<message><source>Scanne</source><translation>Scanne</translation></message>
<message><source>%1 Funktionen gefunden</source><translation>%1 Funktionen gefunden</translation></message>
<message><source>%1 von %2 Funktionen</source><translation>%1 von %2 Funktionen</translation></message>
<message><source>(global)</source><translation>(global)</translation></message>
</context>
<context>
<name>FunctionListDialog</name>
<message><source>Projektfunktionen BareCode</source><translation>Projektfunktionen BareCode</translation></message>
</context>
<context>
<name>DeadCodeAnalyzer</name>
<message><source>Globale Funktion kein Aufruf gefunden</source><translation>Globale Funktion kein Aufruf gefunden</translation></message>
<message><source>Methode von %1 kein Aufruf gefunden</source><translation>Methode von %1 kein Aufruf gefunden</translation></message>
</context>
<context>
<name>DeadCodeDialog</name>
<message><source>Tote Funktionen BareCode</source><translation>Tote Funktionen BareCode</translation></message>
<message><source>Kandidatenliste kein Aufruf per Regex gefunden. Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. Bitte vor dem Löschen manuell prüfen.</source><translation>Kandidatenliste kein Aufruf per Regex gefunden. Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. Bitte vor dem Löschen manuell prüfen.</translation></message>
<message><source>Ausschließen:</source><translation>Ausschließen:</translation></message>
<message><source>vendor lib node_modules (leerzeichen-getrennt)</source><translation>vendor lib node_modules (leerzeichen-getrennt)</translation></message>
<message><source>Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen</source><translation>Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen</translation></message>
<message><source>+ Verzeichnis</source><translation>+ Verzeichnis</translation></message>
<message><source>Ausschlussliste leeren</source><translation>Ausschlussliste leeren</translation></message>
<message><source>Verzeichnis ausschließen</source><translation>Verzeichnis ausschließen</translation></message>
<message><source>Analyse starten</source><translation>Analyse starten</translation></message>
<message><source>Als TXT exportieren</source><translation>Als TXT exportieren</translation></message>
<message><source>Analysiere</source><translation>Analysiere</translation></message>
<message><source>Dateien werden gezählt</source><translation>Dateien werden gezählt</translation></message>
<message><source>(%1 / %2) %3</source><translation>(%1 / %2) %3</translation></message>
<message><source>Funktion</source><translation>Funktion</translation></message>
<message><source>Klasse</source><translation>Klasse</translation></message>
<message><source>Datei</source><translation>Datei</translation></message>
<message><source>Zeile</source><translation>Zeile</translation></message>
<message><source>(global)</source><translation>(global)</translation></message>
<message><source> Keine ungenutzten Funktionen gefunden.</source><translation> Keine ungenutzten Funktionen gefunden.</translation></message>
<message><source>%1 Kandidaten gefunden</source><translation>%1 Kandidaten gefunden</translation></message>
<message><source>Analyse</source><translation>Analyse</translation></message>
<message><source>Bitte zuerst ein Projekt öffnen.</source><translation>Bitte zuerst ein Projekt öffnen.</translation></message>
<message><source>Ergebnis exportieren</source><translation>Ergebnis exportieren</translation></message>
<message><source>Textdateien (*.txt)</source><translation>Textdateien (*.txt)</translation></message>
<message><source>Export</source><translation>Export</translation></message>
<message><source>Datei konnte nicht geschrieben werden:
%1</source><translation>Datei konnte nicht geschrieben werden:
%1</translation></message>
<message><source>Exportiert nach:
%1</source><translation>Exportiert nach:
%1</translation></message>
</context>
<context>
<name>ColorIndicator</name>
<message><source>Farbe wählen</source><translation>Farbe wählen</translation></message>
</context>
<context>
<name>SignatureTooltip</name>
<message><source>Farbe wählen</source><translation>Farbe wählen</translation></message>
</context>
</TS>

View File

@@ -0,0 +1,194 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US" sourcelanguage="de_DE">
<context>
<name>MainWindow</name>
<message><source>&amp;Datei</source><translation>&amp;File</translation></message>
<message><source>&amp;Neue Datei</source><translation>&amp;New File</translation></message>
<message><source>Datei &amp;öffnen</source><translation>&amp;Open File</translation></message>
<message><source>&amp;Projekt öffnen</source><translation>Open &amp;Project</translation></message>
<message><source>Projekt &amp;schließen</source><translation>&amp;Close Project</translation></message>
<message><source>&amp;Speichern</source><translation>&amp;Save</translation></message>
<message><source>Speichern &amp;unter</source><translation>Save &amp;As</translation></message>
<message><source>&amp;Alles speichern</source><translation>Save A&amp;ll</translation></message>
<message><source>&amp;Beenden</source><translation>&amp;Quit</translation></message>
<message><source>&amp;Bearbeiten</source><translation>&amp;Edit</translation></message>
<message><source>&amp;Rückgängig</source><translation>&amp;Undo</translation></message>
<message><source>&amp;Wiederholen</source><translation>&amp;Redo</translation></message>
<message><source>&amp;Suchen / Ersetzen</source><translation>&amp;Find / Replace</translation></message>
<message><source>In &amp;Dateien suchen</source><translation>Find in &amp;Files</translation></message>
<message><source>&amp;Projektfunktionen</source><translation>&amp;Project Functions</translation></message>
<message><source>&amp;Tote Funktionen suchen</source><translation>Find &amp;Dead Code</translation></message>
<message><source>&amp;Ansicht</source><translation>&amp;View</translation></message>
<message><source>&amp;Dark Mode</source><translation>&amp;Dark Mode</translation></message>
<message><source>Sprache</source><translation>Language</translation></message>
<message><source>&amp;Hilfe</source><translation>&amp;Help</translation></message>
<message><source>&amp;Über BareCode</source><translation>&amp;About BareCode</translation></message>
<message><source>Bereit</source><translation>Ready</translation></message>
<message><source>Alle Dateien gespeichert</source><translation>All files saved</translation></message>
<message><source>Gespeichert: %1</source><translation>Saved: %1</translation></message>
<message><source>Projekt geöffnet: %1</source><translation>Project opened: %1</translation></message>
<message><source>Projekt geschlossen</source><translation>Project closed</translation></message>
<message><source>Sprache geändert</source><translation>Language changed</translation></message>
<message><source>Die Sprache wird beim nächsten Start von BareCode aktiv.</source><translation>The language will be applied the next time BareCode starts.</translation></message>
</context>
<context>
<name>AboutDialog</name>
<message><source>Über BareCode</source><translation>About BareCode</translation></message>
<message><source>Modularer Code-Editor</source><translation>Modular Code Editor</translation></message>
<message><source>Version</source><translation>Version</translation></message>
<message><source>Entwickler</source><translation>Developer</translation></message>
<message><source>Projekt</source><translation>Project</translation></message>
<message><source>Framework</source><translation>Framework</translation></message>
<message><source>Sprache</source><translation>Language</translation></message>
<message><source>Schließen</source><translation>Close</translation></message>
</context>
<context>
<name>FileTreePanel</name>
<message><source>Kein Projekt geöffnet</source><translation>No project open</translation></message>
<message><source>Neue Datei</source><translation>New File</translation></message>
<message><source>Neuer Ordner</source><translation>New Folder</translation></message>
<message><source>Löschen</source><translation>Delete</translation></message>
<message><source>Neue Datei</source><translation>New File</translation></message>
<message><source>Dateiname:</source><translation>File name:</translation></message>
<message><source>Eine Datei mit diesem Namen existiert bereits:
%1</source><translation>A file with this name already exists:
%1</translation></message>
<message><source>Datei konnte nicht angelegt werden:
%1</source><translation>Could not create file:
%1</translation></message>
<message><source>Neuer Ordner</source><translation>New Folder</translation></message>
<message><source>Ordnername:</source><translation>Folder name:</translation></message>
<message><source>Ordner konnte nicht angelegt werden:
%1</source><translation>Could not create folder:
%1</translation></message>
<message><source>%1 löschen</source><translation>Delete %1</translation></message>
<message><source>%1 wirklich löschen?
%2</source><translation>Really delete %1?
%2</translation></message>
<message><source>Datei</source><translation>file</translation></message>
<message><source>Ordner</source><translation>folder</translation></message>
</context>
<context>
<name>EditorTab</name>
<message><source>Speichern</source><translation>Save</translation></message>
<message><source>Datei konnte nicht gespeichert werden:
%1</source><translation>Could not save file:
%1</translation></message>
<message><source>Speichern unter</source><translation>Save As</translation></message>
<message><source>Datei konnte nicht geschrieben werden:
%1</source><translation>Could not write file:
%1</translation></message>
</context>
<context>
<name>CodeEditor</name>
<message><source>Datei öffnen</source><translation>Open File</translation></message>
<message><source>Open File</source><translation>Open File</translation></message>
<message><source>Cannot open file:
%1</source><translation>Cannot open file:
%1</translation></message>
</context>
<context>
<name>SearchPanel</name>
<message><source>Suchen</source><translation>Search</translation></message>
<message><source>Suchen:</source><translation>Find:</translation></message>
<message><source></source><translation></translation></message>
<message><source></source><translation></translation></message>
<message><source>Vorheriger Treffer (Shift+F3)</source><translation>Previous match (Shift+F3)</translation></message>
<message><source>Nächster Treffer (F3)</source><translation>Next match (F3)</translation></message>
<message><source>Schließen (Esc)</source><translation>Close (Esc)</translation></message>
<message><source></source><translation></translation></message>
<message><source>Ersetzen durch</source><translation>Replace with</translation></message>
<message><source>Ersetzen:</source><translation>Replace:</translation></message>
<message><source>Ersetzen</source><translation>Replace</translation></message>
<message><source>Alle ersetzen</source><translation>Replace All</translation></message>
<message><source>In Auswahl ersetzen</source><translation>Replace in Selection</translation></message>
<message><source>Groß-/Kleinschreibung</source><translation>Case Sensitive</translation></message>
<message><source>Ganzes Wort</source><translation>Whole Word</translation></message>
<message><source>Regulärer Ausdruck</source><translation>Regular Expression</translation></message>
<message><source>Kein Treffer</source><translation>No matches</translation></message>
<message><source>%1 Treffer</source><translation>%1 matches</translation></message>
<message><source>%1 Ersetzung(en) durchgeführt.</source><translation>%1 replacement(s) made.</translation></message>
<message><source>Es ist kein Text ausgewählt.</source><translation>No text selected.</translation></message>
<message><source>%1 Ersetzung(en) in der Auswahl durchgeführt.</source><translation>%1 replacement(s) in selection.</translation></message>
</context>
<context>
<name>FileSearchPanel</name>
<message><source>Suchen in Dateien:</source><translation>Search in files:</translation></message>
<message><source>Suchbegriff</source><translation>Search term</translation></message>
<message><source>Suchen</source><translation>Search</translation></message>
<message><source></source><translation></translation></message>
<message><source>Schließen</source><translation>Close</translation></message>
<message><source>Groß-/Kleinschreibung</source><translation>Case Sensitive</translation></message>
<message><source>Ganzes Wort</source><translation>Whole Word</translation></message>
<message><source>Regex</source><translation>Regex</translation></message>
<message><source>Dateitypen:</source><translation>File types:</translation></message>
<message><source>Leerzeichen-getrennte Muster, z.B.: *.php *.html</source><translation>Space-separated patterns, e.g.: *.php *.html</translation></message>
<message><source>Suche läuft</source><translation>Searching</translation></message>
<message><source>Kein Projektverzeichnis geöffnet.</source><translation>No project directory open.</translation></message>
<message><source>Keine Treffer gefunden.</source><translation>No matches found.</translation></message>
<message><source>%1 Treffer in %2 Datei(en).</source><translation>%1 match(es) in %2 file(s).</translation></message>
</context>
<context>
<name>FunctionListPanel</name>
<message><source>Projektfunktionen</source><translation>Project Functions</translation></message>
<message><source></source><translation></translation></message>
<message><source>Neu scannen</source><translation>Rescan</translation></message>
<message><source>Funktion suchen</source><translation>Search function</translation></message>
<message><source>Nach Datei</source><translation>By File</translation></message>
<message><source>Nach Klasse</source><translation>By Class</translation></message>
<message><source>Alphabetisch</source><translation>Alphabetically</translation></message>
<message><source>Kein Projekt geöffnet.</source><translation>No project open.</translation></message>
<message><source>Scanne</source><translation>Scanning</translation></message>
<message><source>%1 Funktionen gefunden</source><translation>%1 functions found</translation></message>
<message><source>%1 von %2 Funktionen</source><translation>%1 of %2 functions</translation></message>
<message><source>(global)</source><translation>(global)</translation></message>
</context>
<context>
<name>FunctionListDialog</name>
<message><source>Projektfunktionen BareCode</source><translation>Project Functions BareCode</translation></message>
</context>
<context>
<name>DeadCodeAnalyzer</name>
<message><source>Globale Funktion kein Aufruf gefunden</source><translation>Global function no call found</translation></message>
<message><source>Methode von %1 kein Aufruf gefunden</source><translation>Method of %1 no call found</translation></message>
</context>
<context>
<name>DeadCodeDialog</name>
<message><source>Tote Funktionen BareCode</source><translation>Dead Code BareCode</translation></message>
<message><source>Kandidatenliste kein Aufruf per Regex gefunden. Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. Bitte vor dem Löschen manuell prüfen.</source><translation>Candidate list no call found via regex. Dynamic calls (call_user_func, strings, hooks) are not detected. Please verify manually before deleting.</translation></message>
<message><source>Ausschließen:</source><translation>Exclude:</translation></message>
<message><source>vendor lib node_modules (leerzeichen-getrennt)</source><translation>vendor lib node_modules (space-separated)</translation></message>
<message><source>Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen</source><translation>Select a directory from the project to add to the exclusion list</translation></message>
<message><source>+ Verzeichnis</source><translation>+ Directory</translation></message>
<message><source>Ausschlussliste leeren</source><translation>Clear exclusion list</translation></message>
<message><source>Verzeichnis ausschließen</source><translation>Exclude Directory</translation></message>
<message><source>Analyse starten</source><translation>Start Analysis</translation></message>
<message><source>Als TXT exportieren</source><translation>Export as TXT</translation></message>
<message><source>Analysiere</source><translation>Analysing</translation></message>
<message><source>Dateien werden gezählt</source><translation>Counting files</translation></message>
<message><source>(%1 / %2) %3</source><translation>(%1 / %2) %3</translation></message>
<message><source>Funktion</source><translation>Function</translation></message>
<message><source>Klasse</source><translation>Class</translation></message>
<message><source>Datei</source><translation>File</translation></message>
<message><source>Zeile</source><translation>Line</translation></message>
<message><source>(global)</source><translation>(global)</translation></message>
<message><source> Keine ungenutzten Funktionen gefunden.</source><translation> No unused functions found.</translation></message>
<message><source>%1 Kandidaten gefunden</source><translation>%1 candidates found</translation></message>
<message><source>Analyse</source><translation>Analysis</translation></message>
<message><source>Bitte zuerst ein Projekt öffnen.</source><translation>Please open a project first.</translation></message>
<message><source>Ergebnis exportieren</source><translation>Export Result</translation></message>
<message><source>Textdateien (*.txt)</source><translation>Text files (*.txt)</translation></message>
<message><source>Export</source><translation>Export</translation></message>
<message><source>Datei konnte nicht geschrieben werden:
%1</source><translation>Could not write file:
%1</translation></message>
<message><source>Exportiert nach:
%1</source><translation>Exported to:
%1</translation></message>
</context>
<context>
<name>ColorIndicator</name>
<message><source>Farbe wählen</source><translation>Choose Color</translation></message>
</context>
</TS>