16 Commits

Author SHA1 Message Date
7264b68f11 BareCode-v1.2.tar.gz entfernt 2026-08-25 14:46:39 +02:00
a1b59ff051 Bug in Anzeige der definierten Variablenanzeige behoben 2026-08-25 14:45:51 +02:00
bd40a583cb Englisch hinzugefügt 2026-08-25 14:28:39 +02:00
53608725aa - Fehler in mehrzeiligen Kommentaren behoben
- Klammerpaare werden farblich hervorgehoben
- Beim einrücken oder ausrücken eines markierten Blocks wird die Zeile, in der der Coursor steht, nicht mehr mitgerückt
- Beim eintippen von Variablen wird eine Liste mit bereits deklarierten Variablen angezeigt
2026-08-21 22:58:08 +02:00
aefa627e42 BareCodeAUR entfernt 2026-08-13 09:22:58 +02:00
2cfa3103e9 README.md aktualisiert 2026-08-13 08:58:45 +02:00
ed15fd40c8 - Auslisten aller im Projekt definierten Funktionen
- Suche nach toten Funktionen im Projekt
- Doppelklick auf Funktion öffnet die Datei mit der Deklaration
2026-08-13 08:43:07 +02:00
d9d60b73f9 BareCode V 1.1.0 2026-08-10 09:28:12 +02:00
663127eaa6 Farbvorschau und Farbauswahl korrigiert 2026-08-09 22:09:08 +02:00
95f1ccb2ee Farbvorschau und Farbauswahl hinter Farbwerten implementiert 2026-08-09 21:57:49 +02:00
0c53c919b7 Anpassung von README.md für Haiku 2026-07-14 11:37:59 +02:00
f3e503f9da Anpassungen für Haiku 2026-07-14 11:09:00 +02:00
cb6617222d Suchen in Dateien hinzugefügt 2026-06-06 02:02:20 +02:00
43e8e332fb Highlighter für Kommentare hinzugefügt. 2026-05-26 18:16:17 +02:00
5b81ef9c12 Vertikale Linien zur Visualisierung von Einsprüngen implementiert 2026-05-25 02:50:52 +02:00
250783f63b - Ausrücken mit Shift-Tab implementiert
- RÜcksprung mit Backspace implementiert
- Anzeige für veränderte Datei implementiert
2026-05-24 13:09:00 +02:00
56 changed files with 6157 additions and 126 deletions

1
.gitignore vendored
View File

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

13
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.rc Normal file
View File

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

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(BareCode project(BareCode
VERSION 1.0.0 VERSION 1.2.0
DESCRIPTION "A modular code editor built with Qt6" DESCRIPTION "A modular code editor built with Qt6"
LANGUAGES CXX LANGUAGES CXX
) )
@@ -14,39 +14,80 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
# Platform-specific settings # ---------------------------------------------------------------------------
# Plattform-Erkennung
# ---------------------------------------------------------------------------
if(WIN32) if(WIN32)
set(CMAKE_WIN32_EXECUTABLE ON) set(PLATFORM_WINDOWS TRUE)
add_compile_definitions(PLATFORM_WINDOWS) add_compile_definitions(PLATFORM_WINDOWS)
elseif(UNIX AND NOT APPLE) set(CMAKE_WIN32_EXECUTABLE ON)
add_compile_definitions(PLATFORM_LINUX) elseif(CMAKE_SYSTEM_NAME STREQUAL "Haiku")
set(PLATFORM_HAIKU TRUE)
add_compile_definitions(PLATFORM_HAIKU)
elseif(APPLE) elseif(APPLE)
set(PLATFORM_MACOS TRUE)
add_compile_definitions(PLATFORM_MACOS) add_compile_definitions(PLATFORM_MACOS)
elseif(UNIX)
set(PLATFORM_LINUX TRUE)
add_compile_definitions(PLATFORM_LINUX)
endif() endif()
# Find Qt6 # ---------------------------------------------------------------------------
find_package(Qt6 REQUIRED COMPONENTS # Qt6
Core # ---------------------------------------------------------------------------
Gui find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Concurrent)
Widgets
)
qt_standard_project_setup() qt_standard_project_setup()
# Include cmake modules
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# Collect sources
add_subdirectory(src) 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 # Resources
# ---------------------------------------------------------------------------
qt_add_resources(BARECODE_RESOURCES resources/resources.qrc) qt_add_resources(BARECODE_RESOURCES resources/resources.qrc)
# Main executable # ---------------------------------------------------------------------------
qt_add_executable(BareCode # Executable
main.cpp # ---------------------------------------------------------------------------
${BARECODE_RESOURCES} 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 target_link_libraries(BareCode PRIVATE
BareCode_Core BareCode_Core
@@ -62,8 +103,57 @@ target_include_directories(BareCode PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src
) )
# Install rules # ---------------------------------------------------------------------------
# Installation
# ---------------------------------------------------------------------------
include(GNUInstallDirs) include(GNUInstallDirs)
install(TARGETS BareCode install(TARGETS BareCode
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 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()

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 Dany Thinnes – Projekt Hirnfrei Copyright (c) 2025 Dany Thinnes / Projekt Hirnfrei
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

36
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
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"
}

215
README.md
View File

@@ -1,95 +1,200 @@
# BareCode # BareCode
A modular code editor built with **C++17**, **Qt6**, and **CMake**. **Schlanker, modularer Code-Editor für Web-Entwickler**
Designed to compile cleanly on **Linux** and **Windows**. 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**.
--- ---
## Features (v1.0) ## 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 | | Feature | Details |
|---|---| |---|---|
| Project tree | Left panel — shows only the selected project directory | | Syntax-Highlighting | HTML, PHP, CSS, C/C++ — inkl. mehrzeiliger Kommentare über Leerzeilen |
| Tabbed editor | Open multiple files simultaneously; tabs are movable and closable | | Tabs | Mehrere Dateien gleichzeitig, verschiebbar und schließbar |
| Syntax highlighting | C / C++ (extensible via `HighlighterFactory`) | | Änderungsindikator | ● im Tab-Titel bei ungespeicherten Änderungen |
| Line numbers | Painted in a dedicated gutter | | Zeilennummern | Eigener Gutter, aktuelle Zeile hervorgehoben |
| Auto-indent | Preserves indentation level on Enter | | Einrück-Führungslinien | Vertikale Linien wie in VS Code |
| Tab → spaces | Configurable; jumps to next tab stop | | Klammerzugehörigkeit | Zusammengehörige Klammern werden grün hervorgehoben, fehlende rot |
| Persistent settings | Window geometry, font, tab size, last project (INI file) | | 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 |
--- ---
## Project Structure ## 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/ BareCode/
├── CMakeLists.txt # Top-level build ├── CMakeLists.txt
├── main.cpp ├── main.cpp
├── LICENSE
├── PKGBUILD # Stabiles AUR-Paket
├── PKGBUILD-git # AUR-Paket (Git-Stand)
├── BareCode.desktop # Linux-Anwendungsmenü
├── resources/ ├── resources/
│ └── resources.qrc │ ├── resources.qrc
│ ├── icon_*.png
│ └── php_functions.json
└── src/ └── src/
├── core/ # MainWindow, ProjectManager, Settings, IPlugin ├── core/ # MainWindow, ProjectManager, Settings,
├── editor/ # EditorPanel, EditorTab, CodeEditor, LineNumberArea │ # ThemeManager, AboutDialog, IPlugin
├── editor/ # EditorPanel, EditorTab, CodeEditor,
│ # SearchPanel, FileSearchPanel,
│ # ColorIndicator, SignatureHelper,
│ # VariableCompleter, FunctionScanner,
│ # FunctionIndex, FunctionListPanel,
│ # FunctionListDialog, DeadCodeAnalyzer,
│ # DeadCodeDialog
├── filetree/ # FileTreePanel ├── filetree/ # FileTreePanel
└── highlighter/ # SyntaxHighlighter, CppHighlighter, HighlighterFactory └── highlighter/ # SyntaxHighlighter, HighlighterFactory
``` ```
Each subdirectory compiles into its own **static library**, keeping modules independent. Jedes Unterverzeichnis kompiliert als eigene statische Bibliothek.
--- ---
## Building ## Abhängigkeiten & Bauen
### Prerequisites
- CMake ≥ 3.16
- Qt6 (Widgets module)
- A C++17-capable compiler (GCC, Clang, MSVC)
### Linux ### Linux
**Ubuntu / Debian:**
```bash ```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 -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc) cmake --build build -j$(nproc)
./build/BareCode ./build/BareCode
``` ```
### Windows (Visual Studio) > Getestet auf Haiku R1/beta5.
```bat
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\BareCode.exe
```
### Windows (MinGW)
```bat
cmake -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release
cmake --build build
build\BareCode.exe
```
--- ---
## Extending BareCode ## Neue Sprache hinzufügen
### Adding a new syntax highlighter 1. Subklasse von `SyntaxHighlighter` in `src/highlighter/` erstellen
2. `m_rules` im Konstruktor befüllen (siehe `CppHighlighter` als Vorlage)
1. Create a subclass of `SyntaxHighlighter` in `src/highlighter/`. 3. Dateiendung in `HighlighterFactory.cpp` registrieren
2. Populate `m_rules` in the constructor (see `CppHighlighter` as a template).
3. Register the file extension(s) in `HighlighterFactory.cpp`.
No other files need to change.
### Adding a new panel / plugin
1. Implement the `IPlugin` interface from `src/core/IPlugin.h`.
2. Create a `QWidget`-derived class for the UI.
3. Instantiate and wire it up in `MainWindow`.
--- ---
## Code Style ## Lizenz
All code uses **Allman brace style** and C++17 throughout. 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)

133
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

View File

@@ -1,4 +1,11 @@
#include <QApplication> #include <QApplication>
#include <QIcon>
#include <QTranslator>
#include <QLocale>
#include <QLibraryInfo>
#include <QSettings>
#include <QDir>
#include <QCoreApplication>
#include "core/MainWindow.h" #include "core/MainWindow.h"
int main(int argc, char *argv[]) int main(int argc, char *argv[])
@@ -6,9 +13,79 @@ int main(int argc, char *argv[])
QApplication app(argc, argv); QApplication app(argc, argv);
app.setApplicationName("BareCode"); app.setApplicationName("BareCode");
app.setApplicationVersion("1.0.0"); app.setApplicationVersion("1.2.0");
app.setOrganizationName("BareCode"); 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; MainWindow window;
window.show(); window.show();

BIN
resources/BareCode.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

BIN
resources/icon_128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
resources/icon_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

BIN
resources/icon_24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
resources/icon_256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
resources/icon_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
resources/icon_48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
resources/icon_512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

BIN
resources/icon_64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

1197
resources/php_functions.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,12 @@
<RCC> <RCC>
<qresource prefix="/"> <qresource prefix="/">
<!-- Icons and other assets can be added here --> <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> </qresource>
</RCC> </RCC>

View File

@@ -74,7 +74,7 @@ AboutDialog::AboutDialog(QWidget *parent)
info->addLayout(row); info->addLayout(row);
}; };
makeRow(tr("Version"), "1.0.0"); makeRow(tr("Version"), "1.2.0");
makeRow(tr("Entwickler"), "Dany Thinnes"); makeRow(tr("Entwickler"), "Dany Thinnes");
makeRow(tr("Projekt"), "Projekt Hirnfrei"); makeRow(tr("Projekt"), "Projekt Hirnfrei");
makeRow(tr("Framework"), QString("Qt %1").arg(qVersion())); makeRow(tr("Framework"), QString("Qt %1").arg(qVersion()));

View File

@@ -35,6 +35,12 @@ MainWindow::MainWindow(QWidget *parent)
{ {
m_projectManager->openProject(lastPath); m_projectManager->openProject(lastPath);
} }
// Letzte Session wiederherstellen (geöffnete Dateien + aktiver Tab)
m_editor->restoreSession(
m_settings->lastOpenFiles(),
m_settings->lastActiveFile()
);
} }
MainWindow::~MainWindow() = default; MainWindow::~MainWindow() = default;
@@ -110,6 +116,17 @@ void MainWindow::setupMenuBar()
m_actSearch = mBearbeiten->addAction(tr("&Suchen / Ersetzen…"), this, &MainWindow::onShowSearch); m_actSearch = mBearbeiten->addAction(tr("&Suchen / Ersetzen…"), this, &MainWindow::onShowSearch);
m_actSearch->setShortcut(QKeySequence::Find); 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 ---- // ---- Ansicht ----
QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht")); QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht"));
@@ -118,6 +135,37 @@ void MainWindow::setupMenuBar()
m_actDarkMode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_D); m_actDarkMode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_D);
connect(m_actDarkMode, &QAction::toggled, this, &MainWindow::onToggleDarkMode); 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 ---- // ---- Hilfe ----
QMenu *mHilfe = menuBar()->addMenu(tr("&Hilfe")); QMenu *mHilfe = menuBar()->addMenu(tr("&Hilfe"));
m_actAbout = mHilfe->addAction(tr("&Über BareCode…"), this, &MainWindow::onAbout); m_actAbout = mHilfe->addAction(tr("&Über BareCode…"), this, &MainWindow::onAbout);
@@ -260,6 +308,17 @@ void MainWindow::onToggleDarkMode(bool checked)
m_settings->setDarkMode(checked); 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 // Slots – Hilfe
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -272,10 +331,26 @@ void MainWindow::onAbout()
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Slots – Projekt // 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) void MainWindow::onProjectOpened(const QString &path)
{ {
setWindowTitle(QString("BareCode – %1").arg(path)); setWindowTitle(QString("BareCode – %1").arg(path));
m_fileTree->setRootPath(path); m_fileTree->setRootPath(path);
m_editor->setSearchRoot(path);
statusBar()->showMessage(tr("Projekt geöffnet: %1").arg(path), 4000); statusBar()->showMessage(tr("Projekt geöffnet: %1").arg(path), 4000);
} }
@@ -283,6 +358,7 @@ void MainWindow::onProjectClosed()
{ {
setWindowTitle("BareCode"); setWindowTitle("BareCode");
m_fileTree->clearRoot(); m_fileTree->clearRoot();
m_editor->setSearchRoot(QString());
statusBar()->showMessage(tr("Projekt geschlossen"), 3000); statusBar()->showMessage(tr("Projekt geschlossen"), 3000);
} }
@@ -294,6 +370,10 @@ void MainWindow::saveWindowState()
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
s.setValue("window/geometry", saveGeometry()); s.setValue("window/geometry", saveGeometry());
s.setValue("window/state", saveState()); s.setValue("window/state", saveState());
// Session speichern
m_settings->setLastOpenFiles(m_editor->openFilePaths());
m_settings->setLastActiveFile(m_editor->activeFilePath());
} }
void MainWindow::restoreWindowState() void MainWindow::restoreWindowState()

View File

@@ -5,6 +5,7 @@
#include <QMenuBar> #include <QMenuBar>
#include <QStatusBar> #include <QStatusBar>
#include <QAction> #include <QAction>
#include <QActionGroup>
#include <memory> #include <memory>
#include "ProjectManager.h" #include "ProjectManager.h"
@@ -41,8 +42,12 @@ private slots:
void onUndo(); void onUndo();
void onRedo(); void onRedo();
void onShowSearch(); void onShowSearch();
void onShowFileSearch();
void onShowFunctionList();
void onShowDeadCode();
// Ansicht // Ansicht
void onToggleDarkMode(bool checked); void onToggleDarkMode(bool checked);
void onLanguageChanged(const QString &locale);
// Hilfe // Hilfe
void onAbout(); void onAbout();
// Intern // Intern
@@ -80,6 +85,10 @@ private:
QAction *m_actUndo = nullptr; QAction *m_actUndo = nullptr;
QAction *m_actRedo = nullptr; QAction *m_actRedo = nullptr;
QAction *m_actSearch = nullptr; QAction *m_actSearch = nullptr;
QAction *m_actFileSearch = nullptr;
QAction *m_actFuncList = nullptr;
QAction *m_actDeadCode = nullptr;
QAction *m_actDarkMode = nullptr; QAction *m_actDarkMode = nullptr;
QActionGroup *m_langGroup = nullptr;
QAction *m_actAbout = nullptr; QAction *m_actAbout = nullptr;
}; };

View File

@@ -88,3 +88,26 @@ void Settings::setLastProjectPath(const QString &path)
{ {
m_settings.setValue("project/lastPath", 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

@@ -29,6 +29,13 @@ public:
int fileTreeWidth() const; int fileTreeWidth() const;
void setFileTreeWidth(int width); 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 // Recent
QString lastProjectPath() const; QString lastProjectPath() const;
void setLastProjectPath(const QString &path); void setLastProjectPath(const QString &path);

View File

@@ -9,6 +9,28 @@ set(EDITOR_SOURCES
EditorTab.h EditorTab.h
SearchPanel.cpp SearchPanel.cpp
SearchPanel.h 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}) add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
@@ -17,6 +39,7 @@ target_link_libraries(BareCode_Editor PUBLIC
Qt6::Core Qt6::Core
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
Qt6::Concurrent
BareCode_Highlighter BareCode_Highlighter
) )

View File

@@ -1,5 +1,9 @@
#include "CodeEditor.h" #include "CodeEditor.h"
#include "LineNumberArea.h" #include "LineNumberArea.h"
#include "ColorIndicator.h"
#include "SignatureHelper.h"
#include "FunctionIndex.h"
#include "VariableCompleter.h"
#include "core/Settings.h" #include "core/Settings.h"
#include "highlighter/HighlighterFactory.h" #include "highlighter/HighlighterFactory.h"
@@ -9,6 +13,7 @@
#include <QPaintEvent> #include <QPaintEvent>
#include <QResizeEvent> #include <QResizeEvent>
#include <QKeyEvent> #include <QKeyEvent>
#include <QMouseEvent>
#include <QScrollBar> #include <QScrollBar>
#include <QFile> #include <QFile>
#include <QTextStream> #include <QTextStream>
@@ -21,6 +26,9 @@ CodeEditor::CodeEditor(Settings *settings, QWidget *parent)
, m_settings(settings) , m_settings(settings)
{ {
m_lineNumberArea = new LineNumberArea(this); m_lineNumberArea = new LineNumberArea(this);
m_colorIndicator = new ColorIndicator(this);
m_signatureHelper = new SignatureHelper(this);
m_varCompleter = new VariableCompleter(this);
setupEditor(); setupEditor();
connect(this, &CodeEditor::blockCountChanged, connect(this, &CodeEditor::blockCountChanged,
@@ -194,6 +202,144 @@ void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
} }
} }
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) void CodeEditor::resizeEvent(QResizeEvent *event)
{ {
QPlainTextEdit::resizeEvent(event); QPlainTextEdit::resizeEvent(event);
@@ -246,7 +392,7 @@ void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Current line highlight // Current line highlight + Klammerzugehörigkeit
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void CodeEditor::highlightCurrentLine() void CodeEditor::highlightCurrentLine()
{ {
@@ -254,62 +400,318 @@ void CodeEditor::highlightCurrentLine()
if (!isReadOnly()) if (!isReadOnly())
{ {
QTextEdit::ExtraSelection selection; // 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);
const QColor lineColor = palette().color(QPalette::AlternateBase); // Klammerzugehörigkeit
selection.format.setBackground(lineColor); matchBrackets(extraSelections);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
} }
setExtraSelections(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 – auto-indent + Tab → spaces // Key handling – Tab/Shift+Tab, Smart-Backspace, Auto-Indent
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void CodeEditor::keyPressEvent(QKeyEvent *event) void CodeEditor::keyPressEvent(QKeyEvent *event)
{
// Tab key: insert spaces instead of a real tab character
if (event->key() == Qt::Key_Tab && m_settings->useSpacesForTabs())
{ {
const int tabSize = m_settings->tabSize(); 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(); QTextCursor cursor = textCursor();
if (cursor.hasSelection()) if (cursor.hasSelection())
{ {
// Indent selected lines
QTextBlock startBlock = document()->findBlock(cursor.selectionStart()); QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd()); 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(); cursor.beginEditBlock();
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next()) for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
{ {
QTextCursor lineCursor(b); QTextCursor lineCursor(b);
lineCursor.movePosition(QTextCursor::StartOfBlock);
if (m_settings->useSpacesForTabs())
{
lineCursor.insertText(QString(tabSize, ' ')); lineCursor.insertText(QString(tabSize, ' '));
} }
else
{
lineCursor.insertText("\t");
}
}
cursor.endEditBlock(); cursor.endEditBlock();
} }
else else
{ {
// Calculate spaces needed to reach next tab stop if (m_settings->useSpacesForTabs())
{
// Zum nächsten Tab-Stop auffüllen
const int col = cursor.columnNumber(); const int col = cursor.columnNumber();
const int spacesNeeded = tabSize - (col % tabSize); const int spacesNeeded = tabSize - (col % tabSize);
cursor.insertText(QString(spacesNeeded, ' ')); cursor.insertText(QString(spacesNeeded, ' '));
} }
else
{
cursor.insertText("\t");
}
}
return; return;
} }
// Enter / Return: auto-indent // -----------------------------------------------------------------------
// 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) if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = textCursor();
const QString currentLine = cursor.block().text(); const QString currentLine = cursor.block().text();
// Count leading whitespace // Führende Leerzeichen der aktuellen Zeile zählen
int leadingSpaces = 0; int leadingSpaces = 0;
for (const QChar &ch : currentLine) for (const QChar &ch : currentLine)
{ {
@@ -319,7 +721,7 @@ void CodeEditor::keyPressEvent(QKeyEvent *event)
} }
else if (ch == '\t') else if (ch == '\t')
{ {
leadingSpaces += m_settings->tabSize(); leadingSpaces += tabSize;
} }
else else
{ {
@@ -327,19 +729,21 @@ void CodeEditor::keyPressEvent(QKeyEvent *event)
} }
} }
// Let the base class insert the newline first
QPlainTextEdit::keyPressEvent(event); QPlainTextEdit::keyPressEvent(event);
// Then re-indent
if (leadingSpaces > 0) if (leadingSpaces > 0)
{ {
const QString indent = m_settings->useSpacesForTabs() const QString indent = m_settings->useSpacesForTabs()
? QString(leadingSpaces, ' ') ? QString(leadingSpaces, ' ')
: QString(leadingSpaces / m_settings->tabSize(), '\t'); : QString(leadingSpaces / tabSize, '\t');
textCursor().insertText(indent); textCursor().insertText(indent);
} }
return; return;
} }
QPlainTextEdit::keyPressEvent(event); QPlainTextEdit::keyPressEvent(event);
// Variablen-Popup nach jedem Tastendruck aktualisieren
m_varCompleter->handleKeyPress(event);
} }

View File

@@ -3,10 +3,15 @@
#include <QPlainTextEdit> #include <QPlainTextEdit>
#include <QFont> #include <QFont>
#include <QString> #include <QString>
#include <QTextBlock>
class LineNumberArea; class LineNumberArea;
class Settings; class Settings;
class SyntaxHighlighter; class SyntaxHighlighter;
class ColorIndicator;
class SignatureHelper;
class FunctionIndex;
class VariableCompleter;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// CodeEditor – Core editing widget. // CodeEditor – Core editing widget.
@@ -39,12 +44,23 @@ public:
QString filePath() const; QString filePath() const;
bool isModified() 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: signals:
void fileSaved(const QString &filePath); void fileSaved(const QString &filePath);
void navigateToRequested(const QString &filePath, int line);
protected: protected: void resizeEvent(QResizeEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override; void keyPressEvent(QKeyEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
private slots: private slots:
void updateLineNumberAreaWidth(int newBlockCount); void updateLineNumberAreaWidth(int newBlockCount);
@@ -55,9 +71,14 @@ private:
void setupEditor(); void setupEditor();
void installHighlighter(const QString &filePath); void installHighlighter(const QString &filePath);
bool writeToFile(const QString &filePath); bool writeToFile(const QString &filePath);
void matchBrackets(QList<QTextEdit::ExtraSelection> &selections);
Settings *m_settings = nullptr; Settings *m_settings = nullptr;
LineNumberArea *m_lineNumberArea = nullptr; LineNumberArea *m_lineNumberArea = nullptr;
SyntaxHighlighter *m_highlighter = nullptr; SyntaxHighlighter *m_highlighter = nullptr;
ColorIndicator *m_colorIndicator = nullptr;
SignatureHelper *m_signatureHelper = nullptr;
FunctionIndex *m_functionIndex = nullptr;
VariableCompleter *m_varCompleter = nullptr;
QString m_filePath; 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.0–1.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

@@ -2,8 +2,15 @@
#include "EditorTab.h" #include "EditorTab.h"
#include "CodeEditor.h" #include "CodeEditor.h"
#include "SearchPanel.h" #include "SearchPanel.h"
#include "FileSearchPanel.h"
#include "FunctionListDialog.h"
#include "DeadCodeDialog.h"
#include "FunctionIndex.h"
#include <QFileInfo> #include <QFileInfo>
#include <QFile>
#include <QTextDocument>
#include <QTextBlock>
EditorPanel::EditorPanel(Settings *settings, QWidget *parent) EditorPanel::EditorPanel(Settings *settings, QWidget *parent)
: QWidget(parent) : QWidget(parent)
@@ -27,15 +34,29 @@ void EditorPanel::setupUi()
m_tabWidget->setDocumentMode(true); m_tabWidget->setDocumentMode(true);
m_searchPanel = new SearchPanel(this); 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_tabWidget, 1);
m_layout->addWidget(m_searchPanel, 0); m_layout->addWidget(m_searchPanel, 0);
m_layout->addWidget(m_fileSearch, 0);
connect(m_tabWidget, &QTabWidget::tabCloseRequested, connect(m_tabWidget, &QTabWidget::tabCloseRequested,
this, &EditorPanel::onTabCloseRequested); this, &EditorPanel::onTabCloseRequested);
connect(m_tabWidget, &QTabWidget::currentChanged, connect(m_tabWidget, &QTabWidget::currentChanged,
this, &EditorPanel::onCurrentTabChanged); 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);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -70,7 +91,27 @@ void EditorPanel::openFile(const QString &filePath)
m_tabWidget->setTabToolTip(index, filePath); m_tabWidget->setTabToolTip(index, filePath);
m_openTabs.insert(filePath, tab); m_openTabs.insert(filePath, tab);
// Tab-Titel nach "Speichern unter" aktualisieren // 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) connect(tab->editor(), &CodeEditor::fileSaved, this, [this, tab](const QString &savedPath)
{ {
const int idx = m_tabWidget->indexOf(tab); const int idx = m_tabWidget->indexOf(tab);
@@ -80,6 +121,13 @@ void EditorPanel::openFile(const QString &filePath)
m_tabWidget->setTabToolTip(idx, savedPath); m_tabWidget->setTabToolTip(idx, savedPath);
} }
emit currentFileSaved(savedPath); emit currentFileSaved(savedPath);
// Index und Funktionsliste aktualisieren
m_funcIndex->refresh();
if (m_funcDialog->isVisible())
{
m_funcDialog->refresh();
}
}); });
} }
@@ -113,9 +161,105 @@ void EditorPanel::saveAllFiles()
void EditorPanel::showSearchPanel() void EditorPanel::showSearchPanel()
{ {
m_fileSearch->hide();
m_searchPanel->activate(); 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() void EditorPanel::undo()
{ {
if (EditorTab *tab = currentTab()) if (EditorTab *tab = currentTab())

View File

@@ -9,6 +9,10 @@
class EditorTab; class EditorTab;
class Settings; class Settings;
class SearchPanel; class SearchPanel;
class FileSearchPanel;
class FunctionListDialog;
class DeadCodeDialog;
class FunctionIndex;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel. // EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
@@ -25,7 +29,17 @@ public slots:
void saveCurrentFile(); void saveCurrentFile();
void saveCurrentFileAs(); void saveCurrentFileAs();
void saveAllFiles(); void saveAllFiles();
void setSearchRoot(const QString &path);
void showSearchPanel(); 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 undo();
void redo(); void redo();
@@ -45,6 +59,10 @@ private:
QVBoxLayout *m_layout = nullptr; QVBoxLayout *m_layout = nullptr;
QTabWidget *m_tabWidget = nullptr; QTabWidget *m_tabWidget = nullptr;
SearchPanel *m_searchPanel = 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; QHash<QString, EditorTab *> m_openTabs;
}; };

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,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,243 @@
#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 eigenständiges, rahmenloses Werkzeugfenster
m_popup = new QListWidget(nullptr);
m_popup->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint |
Qt::WindowStaysOnTopHint);
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();
}
// ---------------------------------------------------------------------------
// 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,38 @@
#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);
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

@@ -11,6 +11,7 @@ SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent)
void SyntaxHighlighter::highlightBlock(const QString &text) void SyntaxHighlighter::highlightBlock(const QString &text)
{ {
// Einzel-Zeilen-Regeln anwenden
for (const HighlightRule &rule : m_rules) for (const HighlightRule &rule : m_rules)
{ {
QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text); QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text);
@@ -27,21 +28,65 @@ void SyntaxHighlighter::highlightBlock(const QString &text)
if (!m_hasMultiLineComment) if (!m_hasMultiLineComment)
{ {
setCurrentBlockState(0);
return; return;
} }
// Mehrzeilige Kommentare
// Zustand 0 = normal, 1 = mitten in /* ... */
setCurrentBlockState(0); setCurrentBlockState(0);
int startIndex = 0; // Leere Zeile mitten im Kommentar — Zustand weitertragen
if (previousBlockState() != 1) 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); QRegularExpressionMatch m = m_commentStartExpression.match(text);
startIndex = m.hasMatch() ? static_cast<int>(m.capturedStart()) : -1; startIndex = m.hasMatch() ? static_cast<int>(m.capturedStart()) : -1;
} }
while (startIndex >= 0) while (startIndex >= 0)
{ {
QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, startIndex); QRegularExpressionMatch endMatch =
m_commentEndExpression.match(text, startIndex);
int commentLength = 0; int commentLength = 0;
if (endMatch.hasMatch()) if (endMatch.hasMatch())
@@ -52,6 +97,7 @@ void SyntaxHighlighter::highlightBlock(const QString &text)
} }
else else
{ {
// Kommentar geht über Zeilenende
setCurrentBlockState(1); setCurrentBlockState(1);
commentLength = text.length() - startIndex; commentLength = text.length() - startIndex;
} }
@@ -63,6 +109,7 @@ void SyntaxHighlighter::highlightBlock(const QString &text)
break; break;
} }
// Nach weiteren Kommentaren in dieser Zeile suchen
QRegularExpressionMatch nextStart = QRegularExpressionMatch nextStart =
m_commentStartExpression.match(text, startIndex + commentLength); m_commentStartExpression.match(text, startIndex + commentLength);
startIndex = nextStart.hasMatch() startIndex = nextStart.hasMatch()
@@ -373,16 +420,104 @@ void PhpHighlighter::highlightPhpRange(const QString &text, int start, int lengt
); );
} }
} }
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) void PhpHighlighter::highlightBlock(const QString &text)
{ {
// Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden // Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden
// (setzt auch den State für HTML <!-- --> Kommentare)
SyntaxHighlighter::highlightBlock(text); SyntaxHighlighter::highlightBlock(text);
// Dann PHP-Blöcke <?php ... ?> und <?= ... ?> finden und überschreiben // Block-Zustände:
// Block-Zustände: 0 = HTML, 2 = innerhalb PHP-Block // 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); 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?)", static const QRegularExpression phpOpen(R"(<\?(?:php|=)?\s?)",
QRegularExpression::CaseInsensitiveOption); QRegularExpression::CaseInsensitiveOption);
@@ -390,9 +525,9 @@ void PhpHighlighter::highlightBlock(const QString &text)
int pos = 0; int pos = 0;
if (previousBlockState() == 2) if (previousBlockState() == 2 || previousBlockState() == 3)
{ {
// Wir befinden uns bereits in einem PHP-Block // Wir befinden uns bereits in einem PHP-Block (ggf. in einem Kommentar)
QRegularExpressionMatch closeMatch = phpClose.match(text, 0); QRegularExpressionMatch closeMatch = phpClose.match(text, 0);
if (closeMatch.hasMatch()) if (closeMatch.hasMatch())
{ {
@@ -408,7 +543,10 @@ void PhpHighlighter::highlightBlock(const QString &text)
else else
{ {
highlightPhpRange(text, 0, text.length()); highlightPhpRange(text, 0, text.length());
if (currentBlockState() != 3)
{
setCurrentBlockState(2); setCurrentBlockState(2);
}
return; return;
} }
} }
@@ -424,7 +562,6 @@ void PhpHighlighter::highlightBlock(const QString &text)
const int openStart = static_cast<int>(openMatch.capturedStart()); const int openStart = static_cast<int>(openMatch.capturedStart());
const int openEnd = openStart + static_cast<int>(openMatch.capturedLength()); const int openEnd = openStart + static_cast<int>(openMatch.capturedLength());
// <?php-Tag selbst einfärben
setFormat(openStart, static_cast<int>(openMatch.capturedLength()), m_phpTagFormat); setFormat(openStart, static_cast<int>(openMatch.capturedLength()), m_phpTagFormat);
QRegularExpressionMatch closeMatch = phpClose.match(text, openEnd); QRegularExpressionMatch closeMatch = phpClose.match(text, openEnd);
@@ -441,9 +578,11 @@ void PhpHighlighter::highlightBlock(const QString &text)
} }
else else
{ {
// PHP-Block geht über Zeilenende hinaus
highlightPhpRange(text, openEnd, text.length() - openEnd); highlightPhpRange(text, openEnd, text.length() - openEnd);
if (currentBlockState() != 3)
{
setCurrentBlockState(2); setCurrentBlockState(2);
}
return; return;
} }
} }

198
translations/barecode_de.ts Normal file
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>

194
translations/barecode_en.ts Normal file
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>